本稿では、Seq2Seq(Sequence to Sequence)モデルによるチャットボットをKerasベースで作成するにあたり、学習用の日本語会話データ収集、整形、品詞分解手順を記述します。実行環境は、Google Colaboratoryを想定します。
#1. はじめに
Kerasは少ないコードでニューラルネットワークを構築することができ、大変重宝しています。あまりに便利なので、KerasベースでSeq2Seqを実装しようと思ったときにも、「Seq2Seqレイヤー」のようなものがすでにあって、1行で実装完了!などと言ったことを期待していましたが、残念ながらそうではありませんでした。
そこで、Keras : Ex-Tutorials : Seq2Seq 学習へのイントロを参考に、Kerasベースの日本語チャットボット作成に挑戦してみます。
#2. 本稿のゴール
以下の段取りを踏んで、Seq2Seqモデルによるチャットボットを作成していきます。
これらの内容を順次投稿していく予定ですが、今回はそれらに先立つものとして、日本語コーパスの元データ入手、整形、品詞分解などを行います。品詞分解には、京大黒橋・河原研究室のJUMAN++を使用します。
なお、本稿の前提となるソフトウェア環境は、冒頭で述べたとおり、Google Coraboratoryです。
#3. コーパス元データの入手
##3-1. データ入手元について
会話データを大量に入手する方法としては、Twitterを利用する手法などがありますが、今回は既存の会話データに頼ることにしました。
インターネットから入手可能な会話データとしては、名大会話コーパスが有名ですので、これを利用させていただきます。
ただし、もう少しコーパスのボリュームを増やしたかったので、青空文庫の戯曲データも利用することにしました。
更には、会話といえば落語だ!ということで、こちらのページ(手垢のついたものですが)の落語はろー落語速記編から、落語のテキストデータを入手いたしました。
それぞれのリンク先からZIPファイルをダウンロードし、適当なフォルダに展開しておきます。
##3-2. 文字コード変換
入手したファイルの文字コードはShift-JISなので、これをUtf-8に変換します。以下のコマンドで変換しますが、複数ファイル指定ができるので便利です。
$ nkf -w -Lu --overwrite *.txt
##3−3. Google Driveへのアップロード
Google Drive上に以下の構成のフォルダを用意し、コード変換した会話データファイルをアップロードしてください。
└── メイン作業フォルダ(好きな名称を付与してください)
├── corpus ←整形後会話データ格納用
├── drama ←戯曲データ用
├── nucc ←名大会話コーパス用
└── rakugo ←落語データ用
Google DriveはGoogleアカウントを取得すると使えるようになります。GoogleアカウントはGoogle Coraboratoryを使う際にも必要なので、お持ちでない方は事前に取得しておいてください。
#4. Google Coraboratoryの使い方
##4−1. インストール
Google Drive画面の左上の「+新規」アイコンをクリックし、現れたメニューから「+アプリ追加」メニューをクリックして、Google Colaboratoryをインストールしてください。
##4−2. 設定
デフォルトではコードのインデントが2カラムなので、これを4に変更します。
Google Colaboratory画面の右上の歯車アイコンをクリックすると、設定画面が開きます。そこで「エディタ」メニューを選択すると、インデント設定画面が現れるので、「4」を設定します。
##4−3. ノートブックの作成
Google Drive上に作成したメイン作業フォルダで、「+新規」アイコンをクリックし、「Google Colaboratory」を選択します。
新規ノートブックの画面が開きますので、左端のフォルダアイコンをクリックしてください。しばらくすると、以下のような画面になりますので、「ドライブをマウント」をクリックします。これでGoogle Driveのマイドライブ以下が、/content/drive/
配下にMy Drive
という名称で、マウントされます。
各cellに、以下の章のソースコードをコピペしてください。コピペが終了したら、ノートブックに適当な名称をつけて保存します。保存先はGoogle Drive上の、ノートブック新規作成を実行したフォルダです。
##4−4. ノートブックの実行
Jupyter notebookと同じように、cell単位に順次実行していきます。また、画面の上の方にある、「ランタイム」タブをクリックし、「すべてのセルを実行」メニューを選択すると、一気に処理が実行されます。
#5. 訓練データの作成
##5-1. 基本方針
以下の3つのフェーズで実施します。
1. 会話文の整形
元データから会話部分を抜き出し、不要文字を削除して共通フォーマットに整える
2. 品詞分解
JUMAN++を用いて会話文を品詞分解し、単語が一列に並んだリストを生成する
3. 単語リストから訓練データを生成する
各単語に一意のインデックスを付与し、訓練データ及びラベルデータに編成する
以上を実現するためのソースコードは、本稿では以下の5つのプログラムファイルによって構成されます。いずれもノートブック形式です。ファイル名は、好きなものをつけていただいて結構です。
ファイル名 | 概要 |
---|---|
0100_corpus_MEIDAI.ipynb | 名大会話コーパスの整形 |
0200_corpus_drama.ipynb | 戯曲データの整形 |
0300_corpus_rakugo.ipynb | 落語データの整形 |
0400_deconpose.ipynb | 品詞分解 |
0500_create_data.ipynb | 訓練データ作成 |
##5-2. 会話文の整形
###5-2-1. 概要
一般に、ChatBotの学習データは、発話文と応答文の対で構成されています。しかし会話というものは、前の文を受けて次の文が発話されるのだから、すべての文が発話文であり、かつ応答文であると解釈することにしました。これにより、コーパスの量が単純に2倍になります。
この方針のもと、会話データファイルは発話用と応答用の2種類作成するのではなく、すべての会話を発話順に1つのファイルに収録しました。また、以下のように、セパレータ「SSSS」+文章、という形に整形することにしました。
SSSSご飯食べた?
SSSSうん。
SSSS何食べた?
SSSS「ベラ」っていうやつ。
ところで、会話文の中には長いものもありますが、これがニューラルネットワークで定義する系列長を超える場合は、学習データに使えず、捨てざるを得なくなります。これはもったいないので、一定長より長い文章は、句点のところにセパレータを挿入して、複数の文章に分割することにしました。同一人物の単一発言が発話と応答に分かれることになりますが、そこは捨てるよりはましと、割り切ります。
整形処理の主な内容は、会話文の抜き出しと、補足説明などのメタ情報の削除です。処理結果の確認とソースの修正を繰り返すことで、決定していきました。元データの種類によって会話文の記述方法が異なっていますので、処理もそれぞれ異なったものになっています。
###5-2-2. 名大会話コーパスの整形
名大会話コーパスの各ファイルは、基本的に以下のような構造になっています。
- 最初と最後に、「@」で始まるヘッダおよびフッタがある
- 発言者名は、「Mnnn」または「Fnnn」の半角英数字4文字で表記される(nは0から9までの数字)
- 発言行は、「発言者名」+半角「:」+「発言内容」で構成される(F123:おはよう!)
- 伏字が全角アスタリスク「***」で表記されている。また、強調等の意味合いで*が挿入されていることがある
- 発言内容に、発言者名(「Mnnn」または「Fnnn」)が現れることがある
これらを踏まえて、以下の処理を実行します。
まず、整形処理の本体です。会話文の中に現れる「***」や発言者名を、不明単語を表す文字列「UNK」に置き換えます。また、()で挟まれる補足説明等を削除します。コードの最後のほうに、長い会話文を分割する処理が入っています。
上記の処理を、元データのファイルごとに実行します。フォルダ「nucc」配下に、元データファイルが格納されていますが、ファイル名の一覧を取得して、ファイル単位に順次実行します。また、整形データはファイル「nucc2/corpus.txt」に書き込まれます。
ソースコードは以下のとおりです。 先頭のcdコマンドで、メイン作業フォルダに遷移させます。実環境に合わせて、遷移先を修正してください。
%cd /content/drive/My Drive/GoogleColab/001_create_data
import numpy as np
import csv
import glob
import re
def make_data(fname,data2) :
f = open(fname, 'r')
df1 = csv.reader(f)
data1 = [ v for v in df1]
print(len(data1))
#ファイル読み込み
text = ''
for i in range(0,len(data1)):
if len(data1[i]) == 0:
print('null')
continue
s = data1[i][0]
if s[0:5] == "%com:" :
continue
if s[0] != '@' :
#不明文字をUNKに置き換え
s = s.replace('***','UNK')
#会話文セパレータ
if s[0] == 'F' or s[0] == 'M':
s = 'SSSS'+s[5:]
if s[0:2] == 'X:':
s = 'SSSS'+s[2:]
s = re.sub('F[0-9]{3}',"UNK",s)
s = re.sub('M[0-9]{3}',"UNK",s)
s = s.replace("*","")
else :
continue
while s.find("(") != -1 :
start_1 = s.find("(")
if s.find(")") != -1 :
end_1 = s.find(")")
if start_1 >= end_1 :
s = s.replace(s[end_1],"")
else :
s = s.replace(s[start_1:end_1+1],"")
if len(s) == 0 :
continue
else :
s=s[0:start_1]
while s.find("[") != -1 :
start_2 = s.find("[")
if s.find("]") != -1 :
end_2=s.find("]")
s=s.replace(s[start_2:end_2+1],"")
else :
s=s[0:start_2]
while s.find("<") != -1 :
start_3 = s.find("<")
if s.find(">") != -1 :
end_3 = s.find(">")
s = s.replace(s[start_3:end_3+1],"")
else :
s = s[0:start_3]
while s.find("【") != -1 :
start_4 = s.find("【")
if s.find("】") != -1 :
end_4 = s.find("】")
s = s.replace(s[start_4:end_4+1],"")
else :
s = s[0:start_4]
#いろいろ削除したあとに文字が残っていたら出力文字列に追加
if s != "\n" and s != "SSSS" :
text += s
#セパレータごとにファイル書き込み
text =text[4:]
while text.find("SSSS") != -1 :
end_s = text.find("SSSS")
t = text[0:end_s]
#長い会話文を分割
if end_s > 100 :
while len(t) > 100 :
if t.find("。") != -1 :
n_period = t.find("。")
data2.append("SSSS"+t[0:n_period+1])
t = t[n_period+1:]
else :
break
data2.append("SSSS"+t)
text = text[end_s+4:]
f.close()
return
file_list = glob.glob('nucc/*')
print(len(file_list))
data2=[]
for j in range(0,len(file_list)) :
print(file_list[j])
make_data(file_list[j],data2)
#ファイルセーブ
f = open('corpus/corpus_MEIDAI.txt','w')
for i in range(0,len(data2)):
f.write(str(data2[i])+"\n")
f.close()
print(len(data2))
ノートブックが出来上がったら、4-4節の要領でノートブックを実行します。
###5-2-3. 戯曲データの整形
戯曲の各ファイルは、以下のような特徴があります。
- 会話でない文章として、ヘッダ、フッタおよび、会話中にもト書きが現れるが、特定の文字から始まるということが無いので、プログラムによる識別が難しい
- 発言行において、発言者名と発言内容の区切りは、半角スペースの場合と半角「:」の場合がある
- ルビやその他の補足説明などのメタ情報が、《》や[]に挟まれて出現する
ソースコードは以下の通りです。行頭のチェック(最初の文字が「底本」かどうかの判定、など)は、会話文かどうかの判定処理です。
%cd /content/drive/My Drive/GoogleColab/001_create_data
import numpy as np
import csv
import glob
import re
def make_data(fname,data2) :
f=open(fname, 'r')
df1 = csv.reader(f)
data1 = [ v for v in df1]
#ファイル読み込み
for i in range(0,len(data1)):
if len(data1[i]) == 0:
continue
s=data1[i][0]
if s[0] == " " :
continue
if s[0:2]=="底本" :
continue
if s[0:3]=="[#]" :
continue
if s[0:2]=="校正" :
continue
if s[0:2]=="初出" :
continue
if s[0:3]=="(例)" :
continue
if s.find("! ") != -1 :
s=s.replace("! ","!")
if s.find("? ") != -1 :
s=s.replace("? ","?")
if s.find("|") != -1 :
s=s.replace("|","")
while s.find("(") != -1 :
start_1 =s.find("(")
if s.find(")") != -1 :
end_1=s.find(")")
s=s.replace(s[start_1:end_1+1],"")
if len(s) == 0 :
continue
else :
s=s[0:start_1]
while s.find("[") != -1 :
start_2 =s.find("[")
if s.find("]") != -1 :
end_2=s.find("]")
s=s.replace(s[start_2:end_2+1],"")
else :
s=s[0:start_2]
while s.find("《") != -1 :
start_3 =s.find("《")
if s.find("》") != -1 :
end_3=s.find("》")
s=s.replace(s[start_3:end_3+1],"")
else :
s=s[0:start_3]
if s.find("た み") != -1 :
s=s.replace("た み","たみ")
if s.find("良 三") !=-1 :
s=s.replace("良 三","良三")
if s.find(" ") !=-1 :
sp=s.find(" ")
s=s.replace(s[0:sp+1],"SSSS")
else :
continue
if s.find(" ") != -1 :
s=s.replace(" ","")
if len(s) > 50 :
while len(s) > 50 :
if s.find("。") !=-1 :
n_period=s.find("。")
else :
n_period = len(s)
if s.find("!") !=-1 :
n_exclamation = s.find("!")
else :
n_exclamation = len(s)
if s.find("?") !=-1 :
n_question = s.find("?")
else :
n_question = len(s)
index = min(n_period, n_exclamation, n_question)
data2.append(s[0: index+1])
s="SSSS" + s[index+1:]
if s !="\n" and s != "SSSS":
data2.append(s)
f.close()
return
file_list=glob.glob('drama/*')
print(len(file_list))
data2=[]
for j in range(0,len(file_list)) :
print(file_list[j])
make_data(file_list[j],data2)
#ファイルセーブ
f=open('corpus/corpus_drama.txt','w')
for i in range(0,len(data2)):
f.write(str(data2[i])+"\n")
f.close()
print(len(data2))
4章の記述に従って、ノートブックを作成して実行してください。
###5-2-4. 落語データの整形
落語データの各ファイルは、以下のような特徴があります。
- 会話文はかならず「」で括られている
- 会話の中に、()等で挟まれたメタ情報が現れることがある
ソースコードは以下の通りです。
%cd /content/drive/My Drive/GoogleColab/001_create_data
import numpy as np
import csv
import glob
import re
def make_data(fname,data2) :
f=open(fname, 'r')
df1 = csv.reader(f)
data1 = [ v for v in df1]
#ファイル読み込み
for i in range(0,len(data1)):
if len(data1[i]) == 0:
continue
s=data1[i][0]
if s[0] == " " :
continue
if s.find("「") != -1 :
start_1 =s.find("「")+1
if s.find("」") != -1 :
end_1=s.find("」")
else :
end_1=len(s)
s="SSSS"+s[start_1:end_1]
else :
continue
if s.find("|") != -1 :
s=s.replace("|","")
while s.find("(") != -1 :
start_1 =s.find("(")
if s.find(")") != -1 :
end_1=s.find(")", start_1)
s=s.replace(s[start_1:end_1+1],"")
if len(s) == 0 :
continue
else :
s=s[0:start_1]
while s.find("[") != -1 :
start_2 =s.find("[")
if s.find("]") != -1 :
end_2=s.find("]")
s=s.replace(s[start_2:end_2+1],"")
else :
s=s[0:start_2]
while s.find("《") != -1 :
start_3 =s.find("《")
if s.find("》") != -1 :
end_3=s.find("》")
s=s.replace(s[start_3:end_3+1],"")
else :
s=s[0:start_3]
if s.find(" ") != -1 :
s=s.replace(" ","")
#if s[len(s)-1] !="。" :
# s=s+"。"
if len(s) > 50 :
while len(s) > 50 :
if s.find("。") !=-1 :
n_period=s.find("。")
else :
n_period = len(s)
if s.find("……") !=-1 :
n_exclamation = s.find("……") + 1
else :
n_exclamation = len(s)
index = min(n_period, n_exclamation)
if s != 'SSSS……' :
data2.append(s[0: index+1])
s="SSSS" + s[index+1:]
if s !="\n" and s != "SSSS" :
data2.append(s)
f.close()
return
file_list=glob.glob('rakugo/*')
print(len(file_list))
data2=[]
for j in range(0,len(file_list)) :
print(file_list[j])
make_data(file_list[j],data2)
#ファイルセーブ
f=open('rakugo2/corpus_rakugo.txt','w')
for i in range(0,len(data2)):
f.write(str(data2[i])+"\n")
f.close()
print(len(data2))
4章の記述に従って、ノートブックを作成して実行してください。
ここまでの処理で、3つの会話文ファイル「corpus_MEIDAI.txt
」、「corpus_drama.txt
」、「corpus_rakugo.txt
」がフォルダ「corpus
」内に出来上がります。
##5-3. Google Colaboratory上のJuman++環境構築
前述のJUMAN++を使って、5-2節で作成したファイルを品詞分解しますが、そのために、Google Colaboratory上にJuman++動作環境を構築します。
###5−3−1. Juman++のインストール
まず、Google Driveの適当な場所に、Juman++のアーカイブファイルを置きます。今回はv2.0.0rcをインストールしましたので、以下、それに合わせた手順になっています。
次にGoogle Colaboratoryの新規ノートブックをオープンし、セル内でcdコマンドを実行して、アーカイブファイルの置き場に移動します。手順はこちらの記事を参考にさせていただきました。
Google Driveは/content/drive/My Drive
配下にマウントされています。なお、他のコマンドもすべて、セル内で実行します。
%cd /content/drive/My Drive/アーカイブファイルの置き場
アーカイブファイルを解凍します。
!tar xvf jumanpp-2.0.0-rc2.tar.xz
jumanpp-2.0.0-rc2
というフォルダができていますので、そこに遷移し、さらにビルド用のフォルダbuild
を作成して、そこに遷移します。
%cd jumanpp-2.0.0-rc2/
%mkdir build
%cd build/
cmake
を実行します。インストール先は/usr/local
を指定してあります。
!cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local
make
を実行します。
!make
インストールします。
!sudo make install
これでJuman++のインストールが出来ました。動作確認してみます。
!echo "覚書として記載しておきます。" | jumanpp
覚書 おぼえがき 覚書 名詞 6 普通名詞 1 * 0 * 0 "代表表記:覚え書き/おぼえがき カテゴリ:人工物-その他"
と と と 助詞 9 格助詞 1 * 0 * 0 NIL
して して する 動詞 2 * 0 サ変動詞 16 タ系連用テ形 14 "代表表記:する/する 自他動詞:自:成る/なる 付属動詞候補(基本)"
記載 きさい 記載 名詞 6 サ変名詞 2 * 0 * 0 "代表表記:記載/きさい カテゴリ:抽象物"
して して する 動詞 2 * 0 サ変動詞 16 タ系連用テ形 14 "代表表記:する/する 自他動詞:自:成る/なる 付属動詞候補(基本)"
おき おき おく 接尾辞 14 動詞性接尾辞 7 子音動詞カ行 2 基本連用形 8 "代表表記:おく/おく"
ます ます ます 接尾辞 14 動詞性接尾辞 7 動詞性接尾辞ます型 31 基本形 2 "代表表記:ます/ます"
。 。 。 特殊 1 句点 1 * 0 * 0 NIL
EOS
###5−3−2. pyknpのインストール
python
からJuman++
を利用するためのパッケージpyknp
をインストールします。これは普通にpip
でインストールできます。
!pip install pyknp
動作確認してみます。
from pyknp import Juman
jumanpp = Juman()
result = jumanpp.analysis("国境の長いトンネルを抜けると雪国だった。")
for mrph in result.mrph_list() :
print(mrph.midasi)
国境
の
長い
トンネル
を
抜ける
と
雪国
だった
。
###5−3−3. 実行ファイルのセーブとロード
せっかくインストールしたJuman++ですが、Google Colaboratoryのランタイムが終了すると、チャラになってしまいます。
そこで、こちらのページを参考に、Juman++の実行ファイルをGoogle Driveに退避し、改めてGoogle Colaboratoryを利用する際に、退避先から書き戻すことにします。
退避先への実行ファイルコピー手順は、以下のとおりです。Juman++
をGoogle Colaboratory実行環境にインストールした時に、1度だけ実行します。
!cp -rf /usr/local/bin/jumanpp 退避先/juman/bin/
!cp -rf /usr/local/libexec/jumanpp 退避先/juman/libexec/
退避先からGoogle Colaboratory実行環境への書き戻し手順は、以下のとおりです。notobookごとに実行が必要なようなので、Juman++を使用するnotebookの先頭で実行します。
%cd /content/drive/My Drive/退避先
!cp -rvf ./juman/bin/jumanpp /usr/local/bin/
%mkdir /usr/local/libexec/
!cp -rvf ./juman/libexec/jumanpp /usr/local/libexec/
!chmod 755 /usr/local/bin/jumanpp
!chmod 755 /usr/local/libexec/jumanpp/jumandic.config
!chmod 755 /usr/local/libexec/jumanpp/jumandic.jppmdl
!ls -l /usr/local/bin/jumanpp
!ls -l /usr/local/libexec/jumanpp/*
!pip install pyknp
##5-4. 品詞分解
5−2節の会話文整形処理を実施し、フォルダ「corpus
」内に会話文ファイルが出来上がっている状態で、JUMAN++を使って品詞分解を行います。
JUMAN++の出力結果は、JUMAN++のページにあるように、デリミタが半角スペースのCSVファイルの形態をとっています。これに対し、
- 1列目を取り出す
- 「SSSSXXX」や「UNKXXX」(XXXはいろいろな文字列)を「SSSS」+「XXX」や「UNK」+「XXX」に分解する
などの加工をします。これを単語の出現順に1列に並べて、リストに整形します。
コードは以下の通りです。1つ目のcellは、5-3-3項に記述した、Juman++実行ファイルロード等の処理です。
また、2つ目のcellの「decomposition
」関数において、データ長を4096バイトに制限しているのは、Juman++の入力制限に依ります。
#*******************************************************************************
# *
# Juman++環境復元 *
# *
#*******************************************************************************
%cd /content/drive/My Drive/GoogleColab
!cp -rvf ./juman/bin/jumanpp /usr/local/bin/
%mkdir /usr/local/libexec/
!cp -rvf ./juman/libexec/jumanpp /usr/local/libexec/
!chmod 755 /usr/local/bin/jumanpp
!chmod 755 /usr/local/libexec/jumanpp/jumandic.config
!chmod 755 /usr/local/libexec/jumanpp/jumandic.jppmdl
!ls -l /usr/local/bin/jumanpp
!ls -l /usr/local/libexec/jumanpp/*
!pip install pyknp
%cd /content/drive/My Drive/GoogleColab/001_create_data
import numpy as np
import csv
import glob
import pickle
#*******************************************************************************
# *
# 単語補正 *
# *
#*******************************************************************************
def modification(word) :
if len(word) > 7 and word[:7] == 'SSSSUNK' :
modified = ['SSSS', word[7:]]
elif len(word) > 4 and word[:4] == 'SSSS' :
modified = ['SSSS', word[4:]]
elif word == 'UNKUNK' :
modified = ['UNK']
elif len(word) > 3 and word[:3] == 'UNK' :
modified = ['UNK', word[3:]]
else :
modified = [word]
return modified
#*******************************************************************************
# *
# 品詞分解 *
# *
#*******************************************************************************
def decomposition(file, jumanpp) :
f=open(file, 'r')
df1 = csv.reader(f)
data = [ v for v in df1]
print('number of rows :', len(data))
parts = []
for i in range(len(data)) :
if len(data[i][0].encode('utf-8')) <= 4096 :
result = jumanpp.analysis(data[i][0])
else :
print(i, ' skip')
continue
for mrph in result.mrph_list():
parts += modification(mrph.midasi)
if i % 5000 == 0 :
print(i)
return parts
#*******************************************************************************
# *
# メイン処理 *
# *
#*******************************************************************************
from pyknp import Juman
jumanpp = Juman()
file_list=glob.glob('corpus/*')
file_list.sort()
print(len(file_list))
parts_list = []
for j in range(len(file_list)) :
print(file_list[j])
parts_list += decomposition(file_list[j], jumanpp)
with open('parts_list.pickle', 'wb') as f :
pickle.dump(parts_list , f)
4章の記述に従って、ノートブックを作成して実行してください。
実行後、メイン作業フォルダに「parts_list.pickle
」というファイルが出来ます。
##5−5. 訓練データ作成
###5−5-1. 概要
本章の目的は、前項までに作成した単語の1次元配列を自然数の配列に変換し、更に、Seq2Seqニューラルネットワークの訓練データに再編成することです。
配列の数字化は、登場する単語に一意のインデックスを付与することによって実現します。訓練データの方は、Seq2Seqニューラルネットワークの特徴上、少し変わった構成をとります。
Seq2Seqのニューラルネットワークは、以下の図のように、エンコーダーとデコーダーの2つのニューラルネットワークから構成されています。
それぞれのニューラルネットワークに入力が必要なので、入力は2種類必要になります。出力はデコーダーの方にだけあるので、ラベルデータは1種類必要となります。都合、3種類のデータを生成する必要があります。
エンコーダー用の入力データEncoder Inputは、発話文から生成します。デコーダー用の入力データDecoder Inputとラベルデータは、応答文から生成します。どちらも同じものから生成しますが、デコーダーは入力単語の1つ先の単語を予測するように訓練しますので、この2つは1単語分ずれています。
###5−5−2. 辞書ファイルの作成と、単語→インデックス変換
前章までに作成した3つのファイルを1つの配列にマージし、そこから単語←→インデックス両引き辞書を作成します。次いで、この辞書を使って、単語をインデックスに変換した配列を作成します。この配列が、訓練データの元になります。
インデックス0は、ニューラルネットワークのMasking用に予約したいので、これに単語がアサインされないよう、辞書ファイルに、単語ソート時に必ず先頭に来る「\t」(タブ)を追加します。
また、出現頻度の低い単語は、学習コストの割に予測精度が上がらないので、出現頻度が3回以下の単語は思い切って、十把一絡げに不明単語を表す文字列「UNK」に置き換えます。
###5−5−3. 訓練データの生成
前節で作成したインデックス配列から、訓練データの配列を生成します。配列の種類はEncoder Input、Decoder Input、およびラベルの3種類です。いずれも2次元テンソルで、行数は会話対の数、列幅は系列長です。
以下の図のように、まず会話文のリストを作成し、そこから生成します。
もとの会話文は可変長ですが、これを固定長にするため、系列長より短い文は0パディングします。系列長より長い文は系列長より先を切り落とします。
更に、Decoder Inputの会話文に「UNK」が含まれている場合や、その会話文の元の長さが系列長を超えていた場合は、訓練対象から外します。これにより、「UNK」が含まれる応答文や、系列長を超える長さの応答文が生成されないようにします。
###5−5−4. ソースコード
以下のとおりです。
%cd /content/drive/My Drive/GoogleColab/001_create_data
import numpy as np
import pickle
#*******************************************************************************
# *
# 単語←→インデックス変換辞書作成処理 *
# *
#*******************************************************************************
def create_dict() :
# 品詞リストロード
with open('parts_list.pickle', 'rb') as f :
parts_list = pickle.load(f)
words = sorted(list(set(parts_list)))
print('total words :', len(words))
cnt = np.zeros(len(words), dtype='int32')
#単語をキーにインデックス検索
word_indices = dict((w, i) for i, w in enumerate(words))
#インデックスをキーに単語を検索
indices_word = dict((i, w) for i, w in enumerate(words))
#単語の出現数をカウント
for j in range (0,len(parts_list)):
cnt[word_indices[parts_list[j]]] += 1
# 出現頻度の低い単語を'UNK'に置き換え
for k in range(len(words)):
if cnt[k] <= 3 :
words[k] = 'UNK'
#低頻度単語をUNKに置き換えたので、辞書作り直し
words = list(set(words))
#0パディング対策。インデックス0用キャラクタを追加
words.append('\t')
words = sorted(words)
print('new total words:', len(words))
#単語をキーにインデックス検索
word_indices = dict((w, i) for i, w in enumerate(words))
#インデックスをキーに単語を検索
indices_word = dict((i, w) for i, w in enumerate(words))
#単語インデックス配列作成
list_urtext = [word_indices[parts_list[i]]
if parts_list[i] in word_indices else word_indices['UNK']
for i in range(len(parts_list))]
mat_urtext = np.array(list_urtext)
return words, word_indices, indices_word, mat_urtext
import numpy.random as nr
#*******************************************************************************
# *
# 訓練データ作成処理 *
# *
#*******************************************************************************
def create_training_data(maxlen_e, maxlen_d,
words, word_indices, indices_word, mat_urtext) :
#
#コーパスを会話文のリストに変換
#
separater = word_indices['SSSS']
data=[]
for i in range(len(mat_urtext)-1) :
if mat_urtext[i] == separater :
row = [separater]
else :
row.append(mat_urtext[i])
if mat_urtext[i+1] == separater and len(row) > 1:
data.append(np.array(row))
elif i == len(mat_urtext) - 2 :
row.append(mat_urtext[i+1])
data.append(np.array(row))
print(len(data))
#
# 訓練データ(会話文のリスト)作成
#
e_input = []
d_input = []
t_l=[]
for i in range(len(data)-1) :
if np.any(data[i+1] == word_indices['UNK']) or \
len(data[i+1]) >= maxlen_d + 1:
continue
e_input_row = np.zeros((maxlen_e,), dtype='int32')
d_input_row = np.zeros((maxlen_d,), dtype='int32')
t_row = np.zeros((maxlen_d,), dtype='int32')
e_data = data[i]
d_data = data[i+1]
e_end = min(len(e_data), maxlen_e + 1)
d_end = min(len(d_data), maxlen_d)
t_end = min(len(d_data), maxlen_d + 1)
e_input_row[:e_end-1] = e_data[1:e_end]
d_input_row[:d_end] =d_data[:d_end]
t_row[:t_end-1] = d_data[1:t_end]
if t_end <= maxlen_d :
t_row[t_end-1] = separater
e_input.append(e_input_row)
d_input.append(d_input_row)
t_l.append(t_row)
#
#シャッフル
#
z = list(zip(e_input, d_input, t_l))
nr.seed(12345)
nr.shuffle(z) #シャッフル
e,d,t=zip(*z)
nr.seed()
e = np.array(e).reshape(len(e_input), maxlen_e)
d = np.array(d).reshape(len(d_input), maxlen_d)
t = np.array(t).reshape(len(t_l), maxlen_d)
print(e.shape,d.shape,t.shape)
return e, d, t
#*******************************************************************************
# *
# メイン処理 *
# *
#*******************************************************************************
#@title パラメータ入力フォーム
maxlen_e = 50 #@param {type:"integer"}
maxlen_d = 50 #@param {type:"integer"}
folder = '../002_seq2seq_single_layer/' #@param {type:"string"}
words, word_indices, indices_word, mat_urtext = create_dict()
e, d, t = create_training_data(maxlen_e, maxlen_d,
words, word_indices, indices_word, mat_urtext)
# 辞書ファイルセーブ
with open(folder + 'word_indices.pickle', 'wb') as f :
pickle.dump(word_indices , f)
with open(folder + 'indices_word.pickle', 'wb') as g :
pickle.dump(indices_word , g)
#単語ファイルセーブ
with open(folder + 'words.pickle', 'wb') as h :
pickle.dump(words , h)
#Encoder Inputデータをセーブ
with open(folder + 'e.pickle', 'wb') as f :
pickle.dump(e , f)
#Decoder Inputデータをセーブ
with open(folder + 'd.pickle', 'wb') as g :
pickle.dump(d , g)
#ラベルデータをセーブ
with open(folder + 't.pickle', 'wb') as h :
pickle.dump(t , h)
#maxlenセーブ
with open(folder + 'maxlen.pickle', 'wb') as maxlen :
pickle.dump([maxlen_e, maxlen_d] , maxlen)
出来上がった訓練データをZIPにしてシャッフルしていますが、ここでseed指定しているのは、実行時のランダム要素をなるべく排除して、ソースを修正、再実行したときの結果を確認しやすくするためです。
###5−5−5. 訓練データ作成処理実行
4章の記述に従ってノートブックを作成し、ソースコードをコピペします。
最後のcellにはパラメータ入力フォームが有ります(下の画像)。発話文系列長(maxlen_e
)、応答文系列長(maxlen_d
)、訓練データ格納フォルダ(folder
)が指定できるようになっていますので、適当な値を設定して、ノートブックを実行してください。
作成された訓練データ、辞書等は、指定したフォルダに格納されます。これらは実際に訓練するときやチャットの応答文作成時にロードして使用します。
#6. おわりに
訓練に必要な各種データがそろいましたので、次回以降、ニューラルネットワークの構築と訓練、および会話応答文の生成を行っていきます。
その内容は以下の投稿にまとめてあります。
- Kerasで実装するSeq2Seq -その2 単層LSTM
- Kerasで実装するSeq2Seq -その3 多層LSTMとBidirectional
- Kerasで実装するSeq2Seq -その4 Attention
また、訓練データをTwitterから取得し、それを使用してニューラルネットワークを訓練する方法について、以下のように投稿しましたので、ご覧ください。
訓練したニューラルネットワークは、Twitter上で利用できるようにしてあります。スクリーンネーム@Gacky01Bにつぶやくと、ニューラルネットワークが生成した応答文をリプライします。以下のような感じです。
変更履歴
項番 | 日付 | 変更箇所 | 内容 |
---|---|---|---|
1 | 2018/10/01 | - | 初版 |
2 | 2018/10/04 | 5章 | 5-2節のコードと5-3節のコードを連続して実行する旨を追記 |
3 | 2018/11/29 | 3-1節 | 「名大会話コーパス」へのリンクが切れたので再リンク |
4 | 2018/12/15 | 3-1節 | 筆者の投稿「TwitterAPIを用いた会話データ収集」へのリンク追加 |
5 | 2019/1/30 | 6章 | 筆者の投稿「Twitterデータを用いたチャットボットの訓練」「Twitterデータを用いたチャットボットの訓練 -その2 処理性能とメモリ使用量改善」へのリンク追加 |
6 | 2019/3/25 | 6章 | Twitterボット@Gacky01Bへのリンク追加 |
7 | 2020/6/14 | 全体 | Google Colaboratory対応に伴う記述見直し |