LoginSignup
5
4

More than 3 years have passed since last update.

LINEトーク履歴を学習し、話者を特定してみた

Last updated at Posted at 2019-10-19
1 / 2

1, はじめに

 ページを開いていただきありがとうございます。
 私は、とあるプログラミング学習サービスでpythonによる機械学習の自然言語処理を主に学び、この度学習が終わりを迎えようとしています。学んだ成果をアウトプットすべく、この度記事を作成させていただきました。
 内容としましては、自然言語処理を使って、LINEでの発話に対して、私と妻のどちらの発話であるかを自動で推定する分類モデルを構築することを目的とします。下記を参考とされることで、何方でも同様のものを実装可能だと思いますので、是非ご覧になって行ってください。 

2, 環境について

(1) 開発環境
 ・ macOS Catalina 10.15
 ・ MacBook Pro (13-inch, Early 2017)
 ・ Python 3.7.3
 ・ jupyter 1.0.0

(2) 使用したライブラリ


import neologdn
import re
import numpy as np
from janome.tokenizer import Tokenizer
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.ensemble import RandomForestClassifier
import pickle
from sklearn.metrics import f1_score
from sklearn.model_selection import GridSearchCV

バージョン
・ neologdn 0.4
・ numpy  1.17.2
・ Janome  0.3.9

3, 手順

 今回の手順は、
    ①LINEからトーク履歴を収集
    ②収集したtxtデータの前処理
    ③Tf-idfなどを用いて発話を特徴ベクトルへ変換し、Random forestを使って特徴の重要度を学習
    ④実装
となります。

4, ①LINEからトーク履歴を収集

 今回は、私と妻とのLINEトーク履歴を用います。
 早速取り出します。やり方は、
    ①LINEアプリを開き対象の相手とのトーク画面を開く
    ②トーク設定を開き、『トーク履歴を送信』を選択
で出来ると思います。
スクリーンショット 0001-10-12 22.54.44.png
 こんな感じです。

5, ②収集したtxtデータの前処理

 重複や欠損データなどを取り除いて、データの精度を高めることを、データの前処理とかデータクレンジングと言うそうです。
 今回は、データの正規化や形態素解析などを行います。
 正規化とは、全角を半角に統一や大文字を小文字に統一など、ルールベースで文字を変換することです。
 形態素解析とは、辞書を利用して形態素(意味を持つ最小の言語単位のこと)に分割し、さらに形態素ごとに品詞などの情報の付与を行うことです。
 以下のコードにより前処理をします。


#コーパスを作成する関数
def gen_corpus():
    my_texts_sum = []
    wife_texts_sum = []
    comped_texts = []
    comped_labels = []
    filename = "talk_with_wife.txt"

    #ファイル読み込み
    with open(filename, 'r',encoding="utf-8") as f:
    datas = f.readlines()

    #特定の話者の発言のみをリストにまとめる
    my_text = [re.split("\t", data)[2][:-1] for data in datas if len(re.split("\t", data))
                != 1 and re.split("\t", data)[1]== "Keisuke.O" ]
    wife_text = [re.split("\t", data)[2][:-1] for data in datas if len(re.split("\t", data)) 
                   != 1 and re.split("\t", data)[1]== "wife" ]

    #正規化する
    nomalize_ = lambda x: neologdn.normalize(re.sub("\d","",re.sub("\[スタンプ\]", "",re.sub("\[写真\]", "",re.sub("\[動画\]", "",re.sub("\[アルバム\]","",x.replace("\n", "")))))))
    my_texts = list(map(nomalize_, my_text))
    wife_texts = list(map(nomalize_, wife_text))

    #統合する
    my_texts_sum.extend(my_texts)
    wife_texts_sum.extend(wife_texts)

    # それぞれの空白要素を除去
    my_texts_sum = [my_text_sum for my_text_sum in my_texts_sum
                    if my_text_sum != ""]
    wife_texts_sum = [wife_text_sum for wife_text_sum in wife_texts_sum
                      if wife_text_sum != ""]

    # 統合する
    comped_texts.extend(my_texts_sum[:100000])
    comped_texts.extend(wife_texts_sum[:100000])

    # 名詞と動詞、助詞、助動詞、記号を抽出する
    t = Tokenizer()
    tokenize_ = lambda x: [word.surface for word in t.tokenize(x) if word.part_of_speech.split
                           (",")[0]=="名詞" or word.part_of_speech.split(",")[0]=="動詞" or word.part_of_speech.split(",")[0]=="助詞" or word.part_of_speech.split(",")[0]=="助動詞" or word.part_of_speech.split(",")[0]=="記号"]
    comped_texts = list(map(tokenize_, comped_texts))

    comped_texts = [" ".join(text) for text in comped_texts]
    # ラベルの作成
    my_labels = np.zeros(1810, dtype="int")
    wife_labels = np.ones(1810, dtype="int")
    comped_labels.extend(my_labels)
    comped_labels.extend(wife_labels)

    return comped_texts, comped_labels

  今回は名詞、動詞、助詞、助動詞、記号の5つに分ける形態素解析を行います。それを


print(gen_corpus())

 すると、
スクリーンショット 0001-10-13 1.52.34.png
 こんな感じで出てきます。

6, ③Tf-idfなどを用いて発話を特徴ベクトルへ変換し、Random forestを使って特徴の重要度を学習

 いよいよ前処理したデータを学習していくわけですが、コンピュータは我々人間のように何となく空気を読んで処理を進めてくれるということはありません。言語を数値化して学習させる必要があります。
 そこで出てくるのが、Tf-idfです。これによって、テキストをtfidfで重みがついた単語のベクトルとして表現します。

Tf-idfとは

・ 文書内での単語の重要度tf(term frequency)
・ 一般的な単語の希少度idf(inverse document frequency)
をかけ合わせたものです。

これを用いることで、複数の文章を比較し

・ 任意の単語の傾向
・ 任意の文章の傾向
を同時に探ることができます。

参考にさせていただいた記事 
Tf-idfベクトルってなんだ?

 続いて、ランダムフォレスとでデータの学習をします。
 ランダムフォレストは、

アンサンブル学習法(複数の分類器を集めて構成される分類器)の一つ。
決定木を複数集めて使うので、木が集まってフォレスト(森)として使う。

参考にさせていただいた記事
決定木とランダムフォレスト


# 学習データの作成
texts, labels = gen_corpus()
train_data, test_data, train_labels, test_labels = train_test_split(texts, labels, test_size=0.2, random_state=0)

# tf-idfでトレイニングデータとテストデータをベクトル化する。
vectorizer = TfidfVectorizer(use_idf=True, norm='l2', smooth_idf=True ,token_pattern=u'(?u)\\b\\w+\\b')
tfidf_condition = vectorizer.fit(train_data)
train_matrix = vectorizer.transform(train_data)
test_matrix = vectorizer.transform(test_data)

# ランダムフォレストで学習
model = RandomForestClassifier()
model.fit(train_matrix, train_labels)

# 精度の出力
print("train: ", model.score(train_matrix, train_labels))
print("test: ", model.score(test_matrix, test_labels))

with open('tfidf_condition.pickle', mode='wb') as fp:
    pickle.dump(tfidf_condition, fp)

with open('model.pickle', mode='wb') as fp:
    pickle.dump(model, fp)

実行すると・・・


train:  0.8853591160220995
test:  0.669889502762431

という数値が出ました。
 ここまで、チューニングをやっていませんので、グリッドサーチを行います。
 グリッドサーチとは、モデルの精度を向上させるために用いられる手法のことです。


model_param_set_random = {RandomForestClassifier():{"n_estimators":[i for i in range(10,100)],"max_depth" :[i for i in range(1,11)],"random_state":[0]}}

max_score = 0
best_param = None
model_name = None

for model, param in model_param_set_random.items():
    clf = GridSearchCV(model, param)
    clf.fit(train_matrix, train_labels)
    pred_labels = clf.predict(test_matrix)
    score = f1_score(test_labels, pred_labels, average="micro")
    if max_score < score:
        max_score = score
        best_param = clf.best_params_
        model_name = model.__class__.__name__

print("学習モデル:{}\nパラメーター:{}".format(model_name, best_param))

結果


学習モデル:RandomForestClassifier
パラメーター:{'max_depth': 10, 'n_estimators': 73, 'random_state': 0}

 これでまた学習すると・・・


train:  0.8888121546961326
test:  0.669889502762431

 少し良くなった、のか!?
 まだまだやり方はあるのでしょうが、大人の事情(私の学習不足と時間的な制約)により今回はこのまま実装させていただきます。

7, ④実装

いよいよ実装です。ワクワク♪


classes = ["Keisuke.O","wife"]
t = Tokenizer()

# tf-idfベクトルの成分をロードする
with open('tfidf_condition.pickle', mode='rb') as fp:
    tfidf_vectorizer  = pickle.load(fp)

#学習済みモデルをロードする
with open('model.pickle', mode='rb') as fp:
    model  = pickle.load(fp)


text = input("文字を入力してください:")

tokenize_ = lambda x: [word.surface for word in t.tokenize(x) if word.part_of_speech.split(",")[0]=="名詞" or word.part_of_speech.split(",")[0]=="動詞" or word.part_of_speech.split(",")[0]=="助詞" or word.part_of_speech.split(",")[0]=="助動詞" or word.part_of_speech.split(",")[0]=="記号"]
text = tokenize_(text)
text = " ".join(text)


# モデルが学習したtf-idfベクトルに変換する
text = tfidf_vectorizer.transform([text])
#データをモデルに渡して予測する
result = model.predict(text)[0]

pred_answer = "これは " + classes[result] + " です"


print(pred_answer)

 保存したtf-idfとmodelをロードし、input関数に適当な言葉を入力してみます。
 因みに、『Keisuke.O』が私、『wife』が妻です。


文字を入力してください今日どこ行こうか
これは Keisuke.O です

 お!当たりです。私が言いそうな言葉です。
 次にこれはどうでしょう。


文字を入力してくださいいつ頃帰ってくる
これは wife です

 おお!これも正解。

 でも、これは何の役に立つのだろう😓凄まじく自己満足な内容であることに今頃気が付きました(笑)

8, 終わりに

 私がプログラミングを初め、早3ヶ月が経過しました。
 当初、初学者でありながら、転職を目標に掲げこの世界に飛び込みましたが、そんなに甘くないということを痛感した期間でした。が、一定の成長は出来たかと思います。何より、エンジニアの世界の肌感覚や、機械学習や自然言語処理にはまだまだ可能性がある領域であるということが改めて分かったことは収穫ではないかと思います。
 今回は、人様の力を多数貸していただき作成した猿真似レポートですが、今後は学習を重ね、少しずつでも独り立ち出来るよう励みたいと思います。
 お力を貸していただいた皆様、この場を借りてお礼申し上げます。どうもありがとうございました。

参考とさせていただいた記事
1, 自然言語処理における前処理の種類とその威力

2, リスト内包表記の活用と悪用

3, 【Python】スライス操作についてまとめ

4, 分かりやすいpythonの正規表現の例

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4