#はじめに
機械学習を使ったチャットボットの仕組みを理解するために、テキストを訓練データとする簡単なニューラルネットワークを作成した際の備忘録。
#目的
英文テキストで作成したルールベース型チャットボットを、日本語テキストにも適用して動作させること。日本語テキストを前処理し、それをニューラルネットワークへ通せることを確認する。訓練データとして、Niantic社の"Pokemon GO"に関連したサポートページをWebスクレイピングしたものを使用した。
#マルチクラス分類
予め用意された応答文を入力にあわせて返す「ルールベース型」を参考に、"Intents"(意図)を識別して予測するマルチクラス分類の部分までを形にした。
「生成型」ではなく、入力情報から関連した「よくある質問(FAQ)」を予測するものであるため、”RNN”ではなく通常のニューラルネットワーク層でモデルを作成。
#環境
Jupyter notebookは使わずに、仮想環境を構築。
- macOS Mojave 10.14.6
- Python 3.6.0
- TensorFlow 1.9.0
- MeCab(日本語の形態素解析ツール)
#コード
###訓練データの読み込みと、日本語テキストの前処理
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を用いてモデル作成
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ベクトルのインデックス0番の要素数に合わせる。
- 出力層の活性化関数は、マルチクラス分類に対応する”softmax”を選択。
- モデルのコンパイルでは、マルチクラス分類用のロス計算方法を設定。
- 学習アルゴリズムに”Adam”を使用。
###入力画面の作成
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の種類
- お問い合わせ
- 不具合
- スタートガイド
- アクセサリー
- バトル
- イベント
- 相棒
- セキュリティ
- ショップ
#実行
定義した関数を呼び出して実行。
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)
"results: "のなかの数値は、各カテゴリーの該当する確率を表す。
「ポケモンの捕まえ方」という入力に対し、「スタートガイド」を予測。つづいて「ポケコインとアイテム」に対し「ショップ」を予測。どちらも適切なカテゴリーを予測できた。