機械学習のための日本語前処理

#はじめに

機械学習を使ったチャットボットの仕組みを理解するために、テキストを訓練データとする簡単なニューラルネットワークを作成した際の備忘録。

#目的
英文テキストで作成したルールベース型チャットボットを、日本語テキストにも適用して動作させること。日本語テキストを前処理し、それをニューラルネットワークへ通せることを確認する。訓練データとして、Niantic社の"Pokemon GO"に関連したサポートページをWebスクレイピングしたものを使用した。


[Nianticサポートページ](https://niantic.helpshift.com/a/pokemon-go/?l=ja&p=web)

[使用しているCSVファイル(GitHub)](https://github.com/KazumaMinami/pgo_ml/blob/master/pgo_train_texts.csv)

#マルチクラス分類
予め用意された応答文を入力にあわせて返す「ルールベース型」を参考に、"Intents"（意図）を識別して予測するマルチクラス分類の部分までを形にした。

「生成型」ではなく、入力情報から関連した「よくある質問（FAQ）」を予測するものであるため、”RNN”ではなく通常のニューラルネットワーク層でモデルを作成。

#環境
Jupyter notebookは使わずに、仮想環境を構築。

- macOS Mojave 10.14.6
- Python 3.6.0
- TensorFlow 1.9.0
- MeCab(日本語の形態素解析ツール)

[MeCabインストールの参考ページ](https://qiita.com/berry-clione/items/b3a537962c84244a2a09)

#コード
###訓練データの読み込みと、日本語テキストの前処理
```wakatigaki.py
import MeCab
import csv
import numpy as np
import tensorflow as tf
from tensorflow.keras.preprocessing.sequence import pad_sequences

def create_tokenizer() :
# CSVファイルを読み込む
    text_list = []
    with open("pgo_train_texts.csv", "r") as csvfile :
        texts = csv.reader(csvfile)

        for text in texts :
            text_list.append(text)

# MeCabを使い、日本語テキストを分かち書きする。
        wakati_list = []
        label_list = []
        for label, text in text_list :
            text = text.lower()

            wakati = MeCab.Tagger("-O wakati")
            text_wakati = wakati.parse(text).strip()
            wakati_list.append(text_wakati)
            label_list.append(label)

#　文章のうち最大のものの要素数を調べる。
#　トークナイザーで使用するテキストデータのリストを作成。
        max_len = -1
        split_list = []
        sentences = []
        for text in wakati_list :
            text = text.split()
            split_list.extend(text)
            sentences.append(text)

            if len(text) > max_len :
                max_len = len(text)
        print("Max length of texts: ", max_len)
        vocab_size = len(set(split_list))
        print("Vocabularay size: ", vocab_size)
        label_size = len(set(label_list))

#　Tokenizerを使い、単語にインデックス1から番号を割り当てる。
#　辞書も作成。
        tokenizer = tf.keras.preprocessing.text.Tokenizer(oov_token="<oov>")
        tokenizer.fit_on_texts(split_list)
        word_index = tokenizer.word_index
        print("Dictionary size: ", len(word_index))
        sequences = tokenizer.texts_to_sequences(sentences)

#　教師あり学習に使うラベルデータも、Tokenizerを使い番号をふる。
        label_tokenizer = tf.keras.preprocessing.text.Tokenizer()
        label_tokenizer.fit_on_texts(label_list)
        label_index = label_tokenizer.word_index
        label_sequences = label_tokenizer.texts_to_sequences(label_list)

#　Tokenizerは1から番号をわりあてるのに対し、実際のラベルは0番からインデックスを開始するため−1する。
        label_seq = []
        for label in label_sequences :
            l = label[0] - 1
            label_seq.append(l)

# to_categorical() を使い、モデルに渡す実際のラベルデータであるOne-Hotベクトルを作成。
        one_hot_y = tf.keras.utils.to_categorical(label_seq)

#　訓練データのサイズを揃えるため、短いテキストにもっとも長いテキストデータに合わせて0を追加する。
        padded = pad_sequences(sequences, maxlen=max_len, padding="post", truncating="post")
        print("padded sequences: ", padded)

        reverse_index = dict()
        for intent, i in label_index.items() :
            reverse_index[i] = intent

    return padded, one_hot_y, word_index, reverse_index, tokenizer, max_len, vocab_size
```

###TensorFlowを用いてモデル作成
```model.py
import tensorflow as tf

def model(training, label, vocab_size) :
    model = tf.keras.models.Sequential([
        tf.keras.layers.Embedding(input_dim=vocab_size, output_dim=16, input_length=len(training[0])),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(30, activation="relu"),
        tf.keras.layers.Dense(len(label[0]), activation="softmax")
    ])

    model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
    model.fit(x=training, y=label, epochs=100)

    model.summary()

    return model
```
- 最初にEmbeddingレイヤーを使い、単語同士の関係をベクトルで捉えられるようにする。
- Flatten()を挟み、Fully connectedであるDenseレイヤーへ渡せるようにEmbedding Matrixを平坦化する。
    - 代わりにAveragePooling1D()を使うと、ニューラルネットワークのパラメータ数を少なくでき、計算コストを減らせる。
- 識別したい"Intents（意図）”の数はラベルの種類と同一であるため、One-Hotベクトルのインデックス０番の要素数に合わせる。
- 出力層の活性化関数は、マルチクラス分類に対応する”softmax”を選択。
- モデルのコンパイルでは、マルチクラス分類用のロス計算方法を設定。
- 学習アルゴリズムに”Adam”を使用。


###入力画面の作成

```chat.py

import MeCab
import numpy as np
import tensorflow as tf
from tensorflow.keras.preprocessing.sequence import pad_sequences

#　コンソールで受けつけたテキストをモデルが処理できるように整える。
def prepro_wakati(input, tokenizer, max_len) :
    sentence = []

    input = input.lower()
    wakati = MeCab.Tagger("-O wakati")
    text_wakati = wakati.parse(input).strip()
    sentence.append(text_wakati)
    print(sentence)

    seq = tokenizer.texts_to_sequences(sentence)
    seq = list(seq)
    padded = pad_sequences(seq, maxlen=max_len, padding="post", truncating="post")
    print(padded)

    return padded

def chat(model, tokenizer, label_index, max_len) :
    print("Start talking with the bot (type quit to stop): ")
    while True :
        input_text = input("You: ")
        if input_text.lower() == "quit" :
            break

        x = prepro_wakati(input_text, tokenizer, max_len)
        results = model.predict(x, batch_size=1)
        print("results: ", results)
        results_index = np.argmax(results)
        print("Predicted index: ", results_index)

        intent = label_index[results_index + 1]

        print("Type of intent: ", intent)
```
コンソール画面。
入力されたテキストを訓練済みモデルにわたし、9つある”Intents”のうちどれに当てはまるかを予測する。

- Intentsの種類
    - お問い合わせ
    - 不具合
    - スタートガイド
    - アクセサリー
    - バトル
    - イベント
    - 相棒
    - セキュリティ
    - ショップ


#実行
定義した関数を呼び出して実行。

```ex.py
import wakatigaki
import model
import chat

padded, one_hot_y, word_index, label_index, tokenizer, max_len, vocab_size = wakatigaki.create_tokenizer()

model = model.model(padded, one_hot_y, vocab_size)

chat.chat(model, tokenizer, label_index, max_len)
```
#訓練結果
![スクリーンショット 2020-04-29 16.39.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552502/8e235907-9832-3e2d-c164-4fc56b0d998a.png)

"results: "のなかの数値は、各カテゴリーの該当する確率を表す。

「ポケモンの捕まえ方」という入力に対し、「スタートガイド」を予測。つづいて「ポケコインとアイテム」に対し「ショップ」を予測。どちらも適切なカテゴリーを予測できた。