Edited at

Twitterデータを用いたチャットボットの訓練 -その2 処理性能とメモリ使用量改善

 本稿では、TwitterAPIによって収集した大量の訓練データを使って、KerasベースのSeq2Seqニューラルネットワークを訓練する際の、処理性能とメモリ使用量改善について考察します。


1. はじめに

本稿は筆者の投稿「Twitterデータを用いたチャットボットの訓練」の続編です。

 TwitterAPIを用いて会話データを収集できるようになったことで、ニューラルネットワーク訓練用のデータは事実上、無尽蔵に準備できるようになりました。

 大量データを準備することによって、応答文生成の精度はいくらでも上げられると期待したのですが、ここで以下の新たな制約が現れました。


  • メモリ使用量増加対策としてバッチサイズを小さくしたため、訓練により時間がかかる

  • 学習精度が上がらない

 本稿では、これらへの対処方法について、記述します。

 講じた対策によって訓練したニューラルネットワークは、Twitter上で利用できるようにしてあります。スクリーンネーム@Gacky01Bにつぶやくと、ニューラルネットワークが生成した応答文をリプライします。以下のような感じです。

fig_20190326.jpg

 なお、本稿の前提となるソフトウェア環境は、以下の通りです。


  • Ubuntu 16.04 LTS

  • Python 3.6.4

  • Anaconda 5.1.0

  • TensorFlow-gpu 1.3.0

  • Keras 2.1.4

  • Jupyter 4.4.0

 また、後述の訓練処理が非常に重いので、ハードウェア環境としては、GPUマシンか、クラウドサービスの利用を推奨します。


2. 訓練時間増加対策

 訓練データ増加によって訓練時間が増大するのは、以下の理由によります。


  1. 1エポック当たりの処理量がそもそも増えた

  2. 語彙数が増加するため、出力時のsoftmax計算の負荷が高まる

  3. 同じ原因により、出力次元が大きくなるため、メモリ使用量が増加する。メモリ不足を回避するため、ミニバッチサイズを小さくする必要があるが、これにより訓練時間が長くなる

 項番1は止むを得ないところですが、残りの2つは原因が同一なので、これを回避する方策を検討します。すなわち、softmaxの使用をやめるか、またはsoftmax計算次元数の削減について検討します。


2-1. softmaxの使用回避

 出力次元数が大きいときのsoftmax計算量削減手法として、階層化softmaxというものがあるので、これを適用します。

 階層化softmaxとは、単語に付与したインデックス番号の二進数表示をラベルデータとし、二進数表示の各「ビット」に対してsigmoid関数を活性化関数に用いることで、損失関数の計算量を大幅に削減する手法です。

 例えば、語彙数が100,000であったとき、softmaxの計算には10万回の指数計算が必要ですが、階層化softmaxでは$\log_2 100000 \fallingdotseq 16.6$なので、17回のsigmoid計算で足ります。また、出力次元数も10万から17に大きく減少し、使用メモリ量が大幅に減少します。

 計算量削減効果は圧倒的で、処理時間はsoftmaxの10分の1になりました。

 しかし、階層化softmaxには大きな問題があります。それは、すべてのビットが正解にならないと単語予測が正解にならないという点です。

 この制約は結構厳しく、訓練が相当進んでも、頓珍漢な文章しか生成されませんでした。ビット符号化にHuffman木を適用することも行ってみましたが、状況は変わりませんでした。以下のような感じです。

>> おはよう!

こちら大丈夫です。
>> 今何してる?
私も️️大丈夫です。
>> ご飯食べた?
持つレベルですか?。
>> こんにちは。
大丈夫ですです。
>> それでは御免蒙りまするでござります。
謝り大丈夫ですです。

 そこで、別の手法を試してみることにします。

 


2-2. softmax計算次元数の削減

 出現単語を出現頻度によって複数のカテゴリー(今回の実装では8)に分け、各カテゴリー内で一意のインデックスを付与します。各単語はカテゴリー番号と、カテゴリーインデックスの2つの数字を持つことになりますが、これらのそれぞれに対してsoftmax関数を活性化関数に用います。

 活性化関数計算量と出力次元数がそれぞれ8分の1になるため、処理時間短縮効果は大きく、6分の1程度に削減できました。

 今回の手法も、カテゴリーとインデックスの双方を当てないと正解にならないという制約はありますが、逆に言うと2つ当てればよいということだからなのか、応答文生成精度は体感的には、普通のsoftmaxと遜色がないように見えます。

 本稿では、こちらの手法を採用して、ニューラルネットワークの訓練を進めました。


3. 学習精度向上対策

 訓練データ量が多くなると、訓練に伴う損失関数の減少が進みにくくなってきます。訓練データが増えると入力のばらつきも大きくなるのですから、訓練が進みにくくなるのはある程度は止むを得ないことなのかもしれませんが、対策として、最適化誤差低減手法の定番であるBatch Normalizationを、エンコーダーの最後に追加しました。これの効果は確かにあり、perplexityが数ポイント下がりました。

figure_20180123.png


4. ソースコード

 以下のソースコードは、すべて同一フォルダ上に配置して実行してください。


4-1. 訓練データ生成

 2-2節の内容に従ってラベルの構造を変えましたので、これに伴って、前回の投稿3-5節のソースコードを、以下のように変更します。


generate_data.py

# coding: utf-8


#*******************************************************************************
# *
# 訓練データ/ラベルテンソル作成処理 *
# *
#*******************************************************************************
def generate_tensors(maxlen_e,maxlen_d) :
#--------------------------------------------------------------------------*
# *
# 単語配列、コーパス配列、辞書のロード *
# *
#--------------------------------------------------------------------------*

#単語ファイルロード
with open('words.pickle', 'rb') as f :
words=pickle.load(f)

#作成した辞書をロード
with open('word_indices.pickle', 'rb') as f :
word_indices=pickle.load(f)

with open('indices_word.pickle', 'rb') as g :
indices_word = pickle.load(g)

#コーパスロード
with open('mat_urtext.pickle', 'rb') as ff :
mat_urtext = pickle.load(ff)

#--------------------------------------------------------------------------*
# *
# コーパスをエンコーダ入力、デコーダ入力応答文のテンソルに変換 *
# *
#--------------------------------------------------------------------------*
req = word_indices['REQREQ']
res = word_indices['RESRES']

delimiters = []

#コーパス上のデリミタの位置を特定する
for i in range(0,mat_urtext.shape[0]) :
if mat_urtext[i,0] == req or mat_urtext[i,0] == res:
delimiters.append(i)
if i % 10000000 == 0 :
print(i)

print(len(delimiters))
n=len(delimiters) // 2

#入力、ラベルテンソルの初期値定義(0値マトリックス)
enc_input = np.zeros((n,maxlen_e))
dec_input = np.zeros((n,maxlen_d))
target = np.zeros((n,maxlen_d))

#デリミタを目印に、コーパスから文章データを切り出して入力/ラベルマトリックスにコピー
j = 0
err_cnt = 0

for i in range(0,n) :
index1=2*i+err_cnt # 「REQREQ」のインデックス
index2=2*i+1+err_cnt # 「RESRES」のインデックス
index3=2*i+2+err_cnt # 次の「REQREQ」のインデックス
if index3 >= n :
break
#発話/応答の組が崩れていた時はスキップする
if mat_urtext[delimiters[index1],0] != req or mat_urtext[delimiters[index2],0] != res :
print('シーケンスエラー ',i)
print(index1,index2)
print(delimiters[index1],delimiters[index2])
err_cnt += 1
continue
len_e = delimiters[index2] - delimiters[index1] - 1
len_d = delimiters[index3] - delimiters[index2]
#系列長より短い会話のみ、入力/ラベルマトリックスに書き込む
if len_e <= maxlen_e and len_d <= maxlen_d :
enc_input[j,0:len_e] = mat_urtext[delimiters[index1]+1:delimiters[index2],0].T
dec_input[j,0:len_d] = mat_urtext[delimiters[index2]:delimiters[index3],0].T
target[j,0:len_d] = mat_urtext[delimiters[index2]+1:delimiters[index3]+1,0].T
j += 1
if i % 1000000 == 0 :
print(i)

#会話文データを書き込んだ分だけ切り出す
e = enc_input[0:j,:].reshape(j,maxlen_e,1)
d = dec_input[0:j,:].reshape(j,maxlen_d,1)
t_ = target[0:j,:].reshape(j,maxlen_d,1)

#ラベルテンソル定義
t = np.zeros((j,maxlen_d,2),dtype='int32')

#各単語の出現頻度カウント
cnt = np.zeros(len(words),dtype='int32')
for i in range (0,mat_urtext.shape[0]) :
cnt[mat_urtext[i,0]] += 1

#出現頻度の降順配列
freq_indices = np.argsort(cnt)[::-1] #出現頻度からインデックス検索
indices_freq = np.argsort(freq_indices) #インデックスから出現頻度検索

#
dim = math.ceil(len(words) / 8)

#ラベルテンソル作成
for i in range(0,j) :
for k in range(0,maxlen_d) :
if t_[i,k] != 0 :
freq = indices_freq[int(t_[i,k])]
t[i,k,0] = freq // dim
t[i,k,1] = freq % dim
else :
break

# シャッフル処理
z = list(zip(e, d, t))
nr.seed(12345)
nr.shuffle(z) #シャッフル
e,d,t=zip(*z)
nr.seed()

e = np.array(e).reshape(j,maxlen_e,1)
d = np.array(d).reshape(j,maxlen_d,1)
t = np.array(t).reshape(j,maxlen_d,2)

print(e.shape,d.shape,t.shape)

#--------------------------------------------------------------------------*
# *
# 作成したエンコーダ/デコーダ入力テンソル、ラベルテンソルをセーブ *
# *
#--------------------------------------------------------------------------*
#Encoder Inputデータをセーブ
with open('e.pickle', 'wb') as f :
pickle.dump(e , f)

#Decoder Inputデータをセーブ
with open('d.pickle', 'wb') as g :
pickle.dump(d , g)

#ラベルデータをセーブ
with open('t.pickle', 'wb') as h :
pickle.dump(t , h)

#maxlenセーブ
with open('maxlen.pickle', 'wb') as maxlen :
pickle.dump([maxlen_e, maxlen_d] , maxlen)

#各単語の出現頻度順位(降順)
with open('freq_indices.pickle', 'wb') as f :
pickle.dump(freq_indices , f)
#出現頻度→インデックス変換
with open('indices_freq.pickle', 'wb') as f :
pickle.dump(indices_freq , f)

#*******************************************************************************
# *
# メイン処理 *
# *
#*******************************************************************************
if __name__ == '__main__':

import numpy.random as nr
import pickle
import numpy as np
import math
import sys

args = sys.argv

#args[1] = 50 # jupyter上で実行するとき用
#args[2] = 50 # jupyter上で実行するとき用

generate_tensors(int(args[1]) ,int(args[2]))


 主な変更点は、ラベルデータの処理追加です。以下の処理が追加されています。


  • 単語の出現回数カウント

  • 各単語のインデックスと、出現頻度で決まるカテゴリーおよびカテゴリー内インデックスとの両引き辞書作成

  • ラベルデータのインデックスを、カテゴリーおよびカテゴリー内インデックスに置き換え

 追加部分のコードは以下の通りです。


#ラベルテンソル定義
t = np.zeros((j,maxlen_d,2),dtype='int32')

#各単語の出現頻度カウント
cnt = np.zeros(len(words),dtype='int32')
for i in range (0,mat_urtext.shape[0]) :
cnt[mat_urtext[i,0]] += 1

#出現頻度の降順配列
freq_indices = np.argsort(cnt)[::-1] #出現頻度からインデックス検索
indices_freq = np.argsort(freq_indices) #インデックスから出現頻度検索

#
dim = math.ceil(len(words) / 8)

#ラベルテンソル作成
for i in range(0,j) :
for k in range(0,maxlen_d) :
if t_[i,k] != 0 :
freq = indices_freq[int(t_[i,k])]
t[i,k,0] = freq // dim
t[i,k,1] = freq % dim
else :
break


4-2. ニューラルネットワーク定義クラス

 大筋は筆者のこちらの投稿3-2-3項と同じですが、ラベルの構造変更に伴ってデコーダーの出力が変わっているところと、3章のBatch Normalization追加が主な変更点です。


dialog_categorize.py

# coding: utf-8

from keras.layers.core import Dense
from keras.layers.core import Masking
from keras.layers.core import Activation
from keras.layers import Input
from keras.layers import Lambda
from keras.models import Model
from keras.layers.recurrent import SimpleRNN
from keras.layers.recurrent import LSTM
from keras.layers.embeddings import Embedding
from keras.layers.normalization import BatchNormalization
from keras.initializers import glorot_uniform
from keras.initializers import uniform
from keras.initializers import orthogonal
from keras.initializers import TruncatedNormal
from keras import regularizers
from keras import backend as K
from keras.utils import np_utils

import tensorflow as tf
import numpy as np
import csv
import random
import numpy.random as nr
import keras
import sys
import math
import pickle
import time
import gc
import os

class Color:
BLACK = '\033[30m'
RED = '\033[31m'
GREEN = '\033[38;5;10m'
YELLOW = '\033[33m'
BLUE = '\033[34m'
PURPLE = '\033[35m'
CYAN = '\033[38;5;14m'
WHITE = '\033[37m'
END = '\033[0m'
BOLD = '\038[1m'
UNDERLINE = '\033[4m'
INVISIBLE = '\033[08m'
REVERCE = '\033[07m'

class Dialog :
def __init__(self,maxlen_e,maxlen_d,n_hidden,input_dim,vec_dim,output_dim):
self.maxlen_e=maxlen_e
self.maxlen_d=maxlen_d
self.n_hidden=n_hidden
self.input_dim=input_dim
self.vec_dim=vec_dim
self.output_dim=output_dim

#**************************************************************
# *
# ニューラルネットワーク定義 *
# *
#**************************************************************
def create_model(self):
print('#3')
#=========================================================
#エンコーダー(学習/応答文作成兼用)
#=========================================================
encoder_input = Input(shape=(self.maxlen_e,), dtype='int32', name='encorder_input')
e_i = Embedding(output_dim=self.vec_dim, input_dim=self.input_dim, #input_length=self.maxlen_e,
mask_zero=True,
embeddings_initializer=uniform(seed=20170719))(encoder_input)
e_i=BatchNormalization(axis=-1)(e_i)
e_i=Masking(mask_value=0.0)(e_i)
e_i_fw1, state_h_fw1, state_c_fw1 =LSTM(self.n_hidden, name='encoder_LSTM_fw1' , #前向き1段目
return_sequences=True,return_state=True,
kernel_initializer=glorot_uniform(seed=20170719),
recurrent_initializer=orthogonal(gain=1.0, seed=20170719),
)(e_i)
encoder_LSTM_fw2 =LSTM(self.n_hidden, name='encoder_LSTM_fw2' , #前向き2段目
return_sequences=True,return_state=True,
kernel_initializer=glorot_uniform(seed=20170719),
recurrent_initializer=orthogonal(gain=1.0, seed=20170719),
dropout=0.5, recurrent_dropout=0.5
)

e_i_fw2, state_h_fw2, state_c_fw2 = encoder_LSTM_fw2(e_i_fw1)
e_i_bw0=e_i
e_i_bw1, state_h_bw1, state_c_bw1 =LSTM(self.n_hidden, name='encoder_LSTM_bw1' , #後ろ向き1段目
return_sequences=True,return_state=True, go_backwards=True,
kernel_initializer=glorot_uniform(seed=20170719),
recurrent_initializer=orthogonal(gain=1.0, seed=20170719),
)(e_i_bw0)
e_i_bw2, state_h_bw2, state_c_bw2 =LSTM(self.n_hidden, name='encoder_LSTM_bw2' , #後ろ向き2段目
return_sequences=True,return_state=True, go_backwards=True,
kernel_initializer=glorot_uniform(seed=20170719),
recurrent_initializer=orthogonal(gain=1.0, seed=20170719),
dropout=0.5, recurrent_dropout=0.5
)(e_i_bw1)

encoder_outputs = keras.layers.add([e_i_fw2,e_i_bw2],name='encoder_outputs')
state_h_1=keras.layers.add([state_h_fw1,state_h_bw1],name='state_h_1')
state_c_1=keras.layers.add([state_c_fw1,state_c_bw1],name='state_c_1')
state_h_2=keras.layers.add([state_h_fw2,state_h_bw2],name='state_h_2')
state_c_2=keras.layers.add([state_c_fw2,state_c_bw2],name='state_c_2')

# Batch Normalization
encoder_outputs = BatchNormalization(axis=-1)(encoder_outputs)
state_h_1 = BatchNormalization(axis=-1)(state_h_1)
state_c_1 = BatchNormalization(axis=-1)(state_c_1)
state_h_2 = BatchNormalization(axis=-1)(state_h_2)
state_c_2 = BatchNormalization(axis=-1)(state_c_2)

encoder_states1 = [state_h_1,state_c_1]
encoder_states2 = [state_h_2,state_c_2]

encoder_model = Model(inputs=encoder_input,
outputs=[encoder_outputs,state_h_1,state_c_1,state_h_2,state_c_2]) #エンコーダモデル

print('#4')
#=========================================================
#デコーダー(学習用)
# デコーダを、完全な出力シークエンスを返し、内部状態もまた返すように設定します。
# 訓練モデルではreturn_sequencesを使用しませんが、推論では使用します。
#=========================================================
a_states1=encoder_states1
a_states2=encoder_states2
#---------------------------------------------------------
#レイヤー定義
#---------------------------------------------------------
decode_LSTM1 = LSTM(self.n_hidden, name='decode_LSTM1',
return_sequences=True, return_state=True,
kernel_initializer=glorot_uniform(seed=20170719),
recurrent_initializer=orthogonal(gain=1.0, seed=20170719),
)
decode_LSTM2 =LSTM(self.n_hidden, name='decode_LSTM2',
return_sequences=True, return_state=True,
kernel_initializer=glorot_uniform(seed=20170719),
recurrent_initializer=orthogonal(gain=1.0, seed=20170719),
dropout=0.5, recurrent_dropout=0.5
)

Dense1=Dense(self.n_hidden,name='Dense1',
kernel_initializer=glorot_uniform(seed=20170719))
Dense2=Dense(self.n_hidden,name='Dense2', #次元を減らす
kernel_initializer=glorot_uniform(seed=20170719))
a_Concat1=keras.layers.Concatenate(axis=-1)
a_decode_input_slice1 = Lambda(lambda x: x[:,0,:],output_shape=(1,self.vec_dim,),name='slice1')
a_decode_input_slice2 = Lambda(lambda x: x[:,1:,:],name='slice2')
a_Reshape1 = keras.layers.Reshape((1,self.vec_dim))

a_Dot1 = keras.layers.Dot(-1,name='a_Dot1')
a_Softmax = keras.layers.Softmax(axis=-1,name='a_Softmax')
a_transpose = keras.layers.Reshape((self.maxlen_e,1),name='Transpose')
a_Dot2 = keras.layers.Dot(1,name='a_Dot2')
a_Concat2 = keras.layers.Concatenate(-1,name='a_Concat2')
a_tanh = Lambda(lambda x: K.tanh(x),name='tanh')
a_Concat3 = keras.layers.Concatenate(axis=-1,name='a_Concat3')

decoder_Dense_cat = Dense(8, name='decoder_Dense_cat',activation='softmax' ,
kernel_initializer=glorot_uniform(seed=20170719))
decoder_Dense_mod = Dense(self.output_dim, name='decoder_Dense_mod',activation='softmax' ,
kernel_initializer=glorot_uniform(seed=20170719))

#--------------------------------------------------------
#ループ前処理
#--------------------------------------------------------
a_output=Lambda(lambda x: K.zeros_like(x[:,-1,:]),output_shape=(1,self.n_hidden,))(encoder_outputs)
a_output=keras.layers.Reshape((1,self.n_hidden))(a_output)
#---------------------------------------------------------
#入力定義
#---------------------------------------------------------
decoder_inputs = Input(shape=(self.maxlen_d,), dtype='int32', name='decorder_inputs')
d_i = Embedding(output_dim=self.vec_dim, input_dim=self.input_dim, #input_length=self.maxlen_d,
mask_zero=True,
embeddings_initializer=uniform(seed=20170719))(decoder_inputs)
d_i=BatchNormalization(axis=-1)(d_i)
d_i=Masking(mask_value=0.0)(d_i)
d_input=d_i
#---------------------------------------------------------
# メイン処理(ループ)
#---------------------------------------------------------
for i in range(0,self.maxlen_d) :
d_i_timeslice = a_decode_input_slice1(d_i)
if i <= self.maxlen_d-2 :
d_i=a_decode_input_slice2(d_i)
d_i_timeslice=a_Reshape1(d_i_timeslice)

lstm_input = a_Concat1([a_output,d_i_timeslice]) #前段出力とdcode_inputをconcat
d_i_1, h1, c1 =decode_LSTM1(lstm_input,initial_state=a_states1)
h_output, h2, c2 =decode_LSTM2(d_i_1,initial_state=a_states2)

a_states1=[h1,c1]
a_states2=[h2,c2]
#------------------------------------------------------
#attention
#------------------------------------------------------
a_o = h_output
a_o=Dense1(a_o)
a_o = a_Dot1([a_o,encoder_outputs]) #encoder出力の転置行列を掛ける
a_o= a_Softmax(a_o) #softmax
a_o= a_transpose (a_o)
a_o = a_Dot2([a_o,encoder_outputs]) #encoder出力行列を掛ける
a_o = a_Concat2([a_o,h_output]) #ここまでの計算結果とLSTM出力をconcat
a_o = Dense2(a_o)
a_o = a_tanh(a_o) #tanh
a_output=a_o #次段attention処理向け出力
#a_output = decoder_output_BatchNormal(a_output) # 次段attention処理向け出力のbatchNomalization
if i == 0 : #docoder_output
d_output=a_o
else :
d_output=a_Concat3([d_output,a_o])

d_output=keras.layers.Reshape((self.maxlen_d,self.n_hidden))(d_output)

print('#5')
#---------------------------------------------------------
# 出力、モデル定義、コンパイル
#---------------------------------------------------------
decoder_outputs_cat = decoder_Dense_cat(d_output)
decoder_outputs_mod = decoder_Dense_mod(d_output)
model = Model(inputs=[encoder_input, decoder_inputs], outputs=[decoder_outputs_cat ,decoder_outputs_mod])
model.compile(loss='categorical_crossentropy',optimizer="Adam", metrics=['accuracy'])

#=========================================================
#デコーダー(応答文作成)
#=========================================================
print('#6')
#---------------------------------------------------------
#入力定義
#---------------------------------------------------------
decoder_state_input_h_1 = Input(shape=(self.n_hidden,),name='input_h_1')
decoder_state_input_c_1 = Input(shape=(self.n_hidden,),name='input_c_1')
decoder_state_input_h_2 = Input(shape=(self.n_hidden,),name='input_h_2')
decoder_state_input_c_2 = Input(shape=(self.n_hidden,),name='input_c_2')
decoder_states_inputs_1 = [decoder_state_input_h_1, decoder_state_input_c_1]
decoder_states_inputs_2 = [decoder_state_input_h_2, decoder_state_input_c_2]
decoder_states_inputs=[decoder_state_input_h_1, decoder_state_input_c_1,
decoder_state_input_h_2, decoder_state_input_c_2]
decoder_input_c = Input(shape=(1,self.n_hidden),name='decoder_input_c')
decoder_input_encoded = Input(shape=(self.maxlen_e,self.n_hidden),name='decoder_input_encoded')
#---------------------------------------------------------
# LSTM
#---------------------------------------------------------
#LSTM1段目
decoder_i_timeslice = a_Reshape1(a_decode_input_slice1(d_input))
l_input = a_Concat1([decoder_input_c, decoder_i_timeslice]) #前段出力とdcode_inputをconcat
#l_input = decoder_input_BatchNorm(l_input) # decoderLSTM入力のbatchNomalization
decoder_lstm_1,state_h_1, state_c_1 =decode_LSTM1(l_input,
initial_state=decoder_states_inputs_1) #initial_stateが学習の時と違う
#LSTM2段目
decoder_lstm_2, state_h_2, state_c_2 =decode_LSTM2(decoder_lstm_1,
initial_state=decoder_states_inputs_2)
decoder_states=[state_h_1,state_c_1,state_h_2, state_c_2]
#---------------------------------------------------------
# Attention
#---------------------------------------------------------
attention_o = Dense1(decoder_lstm_2)
attention_o = a_Dot1([attention_o, decoder_input_encoded]) #encoder出力の転置行列を掛ける
attention_o = a_Softmax(attention_o) #softmax
attention_o = a_transpose (attention_o)
attention_o = a_Dot2([attention_o, decoder_input_encoded]) #encoder出力行列を掛ける
attention_o = a_Concat2([attention_o, decoder_lstm_2]) #ここまでの計算結果とLSTM出力をconcat

attention_o = Dense2(attention_o)
decoder_o = a_tanh(attention_o) #tanh

print('#7')
#---------------------------------------------------------
# 出力、モデル定義
#---------------------------------------------------------
decoder_res_cat = decoder_Dense_cat(decoder_o)
decoder_res_mod = decoder_Dense_mod(decoder_o)
decoder_model = Model(
[decoder_inputs,decoder_input_c,decoder_input_encoded] + decoder_states_inputs,
[decoder_res_cat, decoder_res_mod, decoder_o] + decoder_states)

return model ,encoder_model ,decoder_model

#**************************************************************
# *
# 評価 *
# *
#**************************************************************
def eval_perplexity(self,model,e_test,d_test,t_test,batch_size) :
row=e_test.shape[0]
s_time = time.time()
n_batch = math.ceil(row/batch_size)
n_loss=0
sum_loss=0.

for i in range(0,n_batch) :
s = i*batch_size
e = min([(i+1) * batch_size,row])
e_on_batch = e_test[s:e,:]
d_on_batch = d_test[s:e,:]
t_on_batch = t_test[s:e,:,:]
t_on_batch_cat = np_utils.to_categorical(t_on_batch[:,:,0],8)
t_on_batch_mod = np_utils.to_categorical(t_on_batch[:,:,1],self.output_dim)
mask_cat = np.zeros((e-s,self.maxlen_d,8),dtype=np.float32)
mask_mod = np.zeros((e-s,self.maxlen_d,self.output_dim),dtype=np.float32)
for j in range(0,e-s) :
n_dim=self.maxlen_d-list(d_on_batch[j,:]).count(0.)
mask_cat[j,0:n_dim,:]=1
mask_mod[j,0:n_dim,:]=1
n_loss += n_dim

#予測
y_pred_cat,y_pred_mod = model.predict_on_batch([e_on_batch, d_on_batch])

#categorical_crossentropy計算
y_pred_cat = -np.log(np.maximum(y_pred_cat,1e-7))
y_pred_mod = -np.log(np.maximum(y_pred_mod,1e-7))
loss_cat = t_on_batch_cat * y_pred_cat
loss_mod = t_on_batch_mod * y_pred_mod
sum_loss += (mask_cat * loss_cat).sum() + (mask_mod * loss_mod).sum()

#perplexity計算
perplexity = pow(math.e, sum_loss/n_loss)
elapsed_time = time.time() - s_time
sys.stdout.write(Color.GREEN + "\r"+str(e)+"/"+str(row)+" "+str(int(elapsed_time))+"s "+"\t"+
"{0:.4f}".format(perplexity)+" " + Color.END)
sys.stdout.flush()

print()

return perplexity

#**************************************************************
# *
# train_on_batchメイン処理 *
# *
#**************************************************************
def on_batch(self,model,j,e_input,d_input,target,batch_size) :
e_i=e_input
d_i=d_input
t_l=target
z=list(zip(e_i,d_i,t_l))
nr.shuffle(z) #シャッフル
e_i,d_i,t_l=zip(*z)
e_train=np.array(e_i).reshape(len(e_i),self.maxlen_e)
d_train=np.array(d_i).reshape(len(d_i),self.maxlen_d)
t_train=np.array(t_l).reshape(len(t_l),self.maxlen_d ,2)

n_split=int(e_train.shape[0]*0.05)
e_val=e_train[:n_split,:]
d_val=d_train[:n_split,:]
t_val=t_train[:n_split,:,:]

#損失関数、評価関数の平均計算用リスト
list_loss_cat =[]
list_loss_mod =[]
list_loss = []
list_accuracy_cat =[]
list_accuracy_mod =[]

s_time = time.time()
row=e_train.shape[0]
n_batch = math.ceil(row/batch_size)
for i in range(0,n_batch) :
s = i*batch_size
e = min([(i+1) * batch_size,row])
e_on_batch = e_train[s:e,:]
d_on_batch = d_train[s:e,:]
# ラベルテンソルをカテゴリビットごとにスライスする
t_on_batch = t_train[s:e,:,:]
t_on_batch_cat = np_utils.to_categorical(t_on_batch[:,:,0],8)
t_on_batch_mod = np_utils.to_categorical(t_on_batch[:,:,1],self.output_dim)
result=model.train_on_batch([e_on_batch, d_on_batch],[t_on_batch_cat, t_on_batch_mod])
list_loss_cat.append(result[0])
list_loss_mod.append(result[1])
list_loss.append(result[2])
list_accuracy_cat.append(result[3])
list_accuracy_mod.append(result[4])
elapsed_time = time.time() - s_time
sys.stdout.write(Color.CYAN + "\r"+str(e)+"/"+str(row)+" "+str(int(elapsed_time))+"s "+"\t"+
"{0:.4f}".format(np.average(list_loss_cat))+"\t"+
"{0:.4f}".format(np.average(list_loss_mod))+"\t"+
"{0:.4f}".format(np.average(list_accuracy_cat))+"\t"+
"{0:.4f}".format(np.average(list_accuracy_mod)) + Color.END)
sys.stdout.flush()
del e_on_batch,d_on_batch,t_on_batch, t_on_batch_cat, t_on_batch_mod

#perplexity評価
print()
val_perplexity=self.eval_perplexity(model,e_val,d_val,t_val,batch_size)
loss= np.average(list_loss)

return val_perplexity, loss

#**************************************************************
# *
# 学習 *
# *
#**************************************************************
def train(self, e_input, d_input,target,batch_size,epochs, emb_param) :

print ('#2',target.shape)
model, _, _ = self.create_model()
if os.path.isfile(emb_param) :
model.load_weights(emb_param) #埋め込みパラメータセット
print ('#8')
#=========================================================
# train on batch
#=========================================================
e_i = e_input
d_i = d_input
t_l = target

row=e_input.shape[0]
loss_bk =10000
patience = 0

for j in range(0,epochs) :
print(Color.CYAN,"Epoch ",j+1,"/",epochs,Color.END)
val_perplexity,val_loss = self.on_batch(model,j,e_i,d_i,t_l,batch_size)
model.save_weights(emb_param)
#-----------------------------------------------------
# EarlyStopping
#-----------------------------------------------------
if j == 0 or val_loss <= loss_bk:
loss_bk = val_loss
patience = 0
elif patience < 2 :
patience += 1
else :
print('EarlyStopping')
break

return model



4−3. 訓練処理

 訓練処理です。引数は3つあり、以下のとおりです。

引数
内容
設定例

第1引数
重み行列のファイル名(拡張子を除く)。指定ファイルが存在しない時は、その名称のファイルが新たに作成される
param_01

第2引数
訓練エポック数
10

第3引数
ミニバッチサイズ
400


seq2seq_twitter.py


# coding: utf-8
from __future__ import print_function
#*********************************************************************************************
# *
# 訓練データ、ラベルデータ等をロードする *
# *
#*********************************************************************************************
def load_data() :
#import pickle
#import numpy as np
#単語ファイルロード
with open('words.pickle', 'rb') as ff :
words=pickle.load(ff)

#Encoder Inputデータをロード
with open('e.pickle', 'rb') as f :
e = pickle.load(f)

#Decoder Inputデータをロード
with open('d.pickle', 'rb') as g :
d = pickle.load(g)

#ラベルデータをロード
with open('t.pickle', 'rb') as h :
t = pickle.load(h)

#maxlenロード
with open('maxlen.pickle', 'rb') as maxlen :
[maxlen_e, maxlen_d] = pickle.load(maxlen)

n_split=int(e.shape[0]*0.95) #訓練データとテストデータを95:5に分割
e_train,e_test=np.vsplit(e,[n_split]) #エンコーダインプットデータを訓練用とテスト用に分割
d_train,d_test=np.vsplit(d,[n_split]) #デコーダインプットデータを訓練用とテスト用に分割
t_train,t_test=np.vsplit(t,[n_split]) #ラベルデータを訓練用とテスト用に分割
print(e_train.shape,d_train.shape,t_train.shape)
return e_train,e_test,d_train,d_test,t_train,t_test, maxlen_e, maxlen_d, words

#*********************************************************************************************
# *
# 訓練処理 *
# *
#*********************************************************************************************

def prediction(epochs, batch_size ,input_dim, param_name, e, e_t, d, d_t, t, t_t) :
#import math
#from dialog_categorize import Dialog

vec_dim = 400
#input_dim = len(words)
output_dim = math.ceil(input_dim / 8)
n_hidden = int(vec_dim*1.5 ) #隠れ層の次元

prediction = Dialog(maxlen_e,maxlen_d,n_hidden,input_dim,vec_dim,output_dim)
emb_param = param_name+'.hdf5'
row = e.shape[0]

e_train = e.reshape(row,maxlen_e)
d_train = d.reshape(row,maxlen_d)
t_train = t

#t_train = t_train.reshape(row,maxlen_d)
model = prediction.train(e_train, d_train,t_train,batch_size,epochs,emb_param)
plot_model(model, show_shapes=True,to_file='seq2seq0212.png') #ネットワーク図出力

model.save_weights(emb_param) #学習済みパラメータセーブ

row2 = e_t.shape[0]
e_test = e_t.reshape(row2,maxlen_e)
d_test = d_t.reshape(row2,maxlen_d)
t_test = t_t
print()
print(t_test.shape)
perplexity = prediction.eval_perplexity(model,e_test,d_test,t_test,batch_size)
print('Perplexity=',perplexity)

del prediction

#*********************************************************************************************
# *
# メイン処理 *
# *
#*********************************************************************************************

if __name__ == '__main__':

from dialog_categorize import Dialog

import tensorflow as tf
import numpy as np
import csv
import random
import numpy.random as nr
import keras
import sys
import math
import time
import pickle
import gc
import os

from keras.utils import plot_model
from pyknp import Jumanpp
import codecs

args = sys.argv
#args = ['','param_001','40','500'] # jupyter上で実行するとき用

param_name = args[1]
epochs = int(args[2])
batch_size = int(args[3])

e_train,e_test,d_train,d_test,t_train,t_test, maxlen_e, maxlen_d, words = load_data()

input_dim = len(words)

prediction(epochs, batch_size ,input_dim, param_name, e_train,e_test,d_train,d_test,t_train,t_test)



4−4. 応答文生成

 応答文生成処理です。引数は1つあり、重み行列ファイル名(拡張子を除く)を指定します。実行すると、発話文入力用のプロンプト「>>」が表示されます。「q(半角)」を入力すると、処理は終了します。


response.py


# coding: utf-8

#*************************************************************************************
# *
# import宣言 *
# *
#*************************************************************************************

from __future__ import print_function
from dialog_categorize import Dialog

import tensorflow as tf
import numpy as np
import csv
import random
import numpy.random as nr
import keras
import sys
import math
import time
import pickle
import gc
import os

from keras.utils import plot_model
from pyknp import Jumanpp
import codecs

#*************************************************************************************
# *
# 辞書ファイル等ロード *
# *
#*************************************************************************************

def load_data() :

#辞書をロード
with open('word_indices.pickle', 'rb') as f :
word_indices=pickle.load(f) #単語をキーにインデックス検索

with open('indices_word.pickle', 'rb') as g :
indices_word=pickle.load(g) #インデックスをキーに単語を検索

#単語ファイルロード
with open('words.pickle', 'rb') as ff :
words=pickle.load(ff)

#maxlenロード
with open('maxlen.pickle', 'rb') as maxlen :
[maxlen_e, maxlen_d] = pickle.load(maxlen)

#各単語の出現頻度順位(降順)
with open('freq_indices.pickle', 'rb') as f :
freq_indices = pickle.load(f)

#出現頻度→インデックス変換
with open('indices_freq.pickle', 'rb') as f :
indices_freq = pickle.load(f)

return word_indices ,indices_word ,words ,maxlen_e, maxlen_d, freq_indices

#*************************************************************************************
# *
# モデル初期化 *
# *
#*************************************************************************************

def initialize_models(emb_param ,maxlen_e, maxlen_d ,vec_dim, input_dim,output_dim, n_hidden) :

dialog= Dialog(maxlen_e, 1, n_hidden, input_dim, vec_dim, output_dim)
model ,encoder_model ,decoder_model = dialog.create_model()

param_file = emb_param + '.hdf5'
model.load_weights(param_file)

plot_model(encoder_model, show_shapes=True,to_file='seq2seq0212_encoder.png')
plot_model(decoder_model, show_shapes=True,to_file='seq2seq0212_decoder.png')

return model, encoder_model ,decoder_model

#*************************************************************************************
# *
# 入力文の品詞分解とインデックス化 *
# *
#*************************************************************************************

def encode_request(cns_input, maxlen_e, word_indices, words, encoder_model) :
# Use Juman++ in subprocess mode
jumanpp = Jumanpp()
result = jumanpp.analysis(cns_input)
input_text=[]
for mrph in result.mrph_list():
input_text.append(mrph.midasi)

mat_input=np.array(input_text)

#入力データe_inputに入力文の単語インデックスを設定
e_input=np.zeros((1,maxlen_e))
for i in range(0,len(mat_input)) :
if mat_input[i] in words :
e_input[0,i] = word_indices[mat_input[i]]
else :
e_input[0,i] = word_indices['UNK']

return e_input

#*************************************************************************************
# *
# 応答文組み立て *
# *
#*************************************************************************************

def generate_response(e_input, n_hidden, maxlen_d, output_dim, word_indices,
freq_indices, indices_word, encoder_model, decoder_model) :
# Encode the input as state vectors.
encoder_outputs, state_h_1, state_c_1, state_h_2, state_c_2 = encoder_model.predict(e_input)
states_value= [state_h_1, state_c_1, state_h_2, state_c_2]
decoder_input_c = np.zeros((1,1,n_hidden) ,dtype='int32')

decoded_sentence = ''
target_seq = np.zeros((1,1) ,dtype='int32')
# Populate the first character of target sequence with the start character.
target_seq[0, 0] = word_indices['RESRES']
# 応答文字予測

for i in range(0,maxlen_d) :
output_tokens_cat, output_tokens_mod, d_output, h1, c1, h2, c2 = decoder_model.predict(
[target_seq,decoder_input_c,encoder_outputs]+ states_value)

# 予測単語の出現頻度算出
n_cat = np.argmax(output_tokens_cat[0, 0, :])
n_mod = np.argmax(output_tokens_mod[0, 0, :])
freq = (n_cat * output_dim + n_mod).astype(int)
#予測単語のインデックス値を求める
sampled_token_index = freq_indices[freq]
#予測単語
sampled_char = indices_word[sampled_token_index]
# Exit condition: find stop character.
if sampled_char == 'REQREQ' :
break
decoded_sentence += sampled_char

# Update the target sequence (of length 1).
if i == maxlen_d-1:
break
target_seq[0,0] = sampled_token_index

decoder_input_c = d_output
# Update states
states_value = [h1, c1, h2, c2]

return decoded_sentence

#*************************************************************************************
# *
# メイン処理 *
# *
#*************************************************************************************

if __name__ == '__main__':

vec_dim = 400
n_hidden = int(vec_dim*1.5 ) #隠れ層の次元

args = sys.argv

#args[1] = 'param_003' # jupyter上で実行するとき用

#データロード
word_indices ,indices_word ,words ,maxlen_e, maxlen_d ,freq_indices = load_data()
#入出力次元
input_dim = len(words)
output_dim = math.ceil(len(words) / 8)
#モデル初期化
model, encoder_model ,decoder_model = initialize_models(args[1] ,maxlen_e, maxlen_d,
vec_dim, input_dim, output_dim, n_hidden)

sys.stdin = codecs.getreader('utf_8')(sys.stdin)

while True:
cns_input = input(">> ")
if cns_input == "q":
print("終了")
break

#--------------------------------------------------------------*
# 入力文の品詞分解とインデックス化 *
#--------------------------------------------------------------*
e_input = encode_request(cns_input, maxlen_e, word_indices, words, encoder_model)

#--------------------------------------------------------------*
# 応答文組み立て *
#--------------------------------------------------------------*
decoded_sentence = generate_response(e_input, n_hidden, maxlen_d, output_dim, word_indices,
freq_indices, indices_word, encoder_model, decoder_model)

print(decoded_sentence)



5. 訓練

 前回投稿の3-1節〜3-4節までを実行したあとで、本稿4-1節の訓練データ生成処理を実行します。本処理では引数としてエンコーダ系列長、デコーダ系列長を指定するようになっています。以下の例では、双方とも20を指定してあります。

$ python generate_data.py 20 20

 訓練データ生成が終了したら、以下のようにコマンドを入力して、4-3節の訓練処理を実行します。引数の指定値は例です。

$ python seq2seq_twitter.py param_01 10 400


6. 応答文生成

 以下のコマンドを投入し(引数は例)、プロンプト「>>」が表示されたら発話文を入力すると、応答文が表示されます。コマンドを終了する際には、半角「q」を入力します。

$ python response.py param_01


7. 訓練結果

 系列データ長50(入力、出力とも)、埋め込みベクトル次元400、隠れ層次元600という条件のもとで、36時間程度訓練したあとの結果です。perplexityは55程度で、このあたりが限界でした。訓練データ量は、約77万会話対です。

 1エポックあたりの時間は約7000秒です。前回は系列データ長20(入力、出力とも)、訓練データ量約31万会話対という条件で、1エポックあたり5000秒でした。同一条件だと、800秒で処理できるので、約6分の1に高速化できたことになります。

>> おはよう!

おはようございます。今日も一日騙さです。
>> 今何してる?
私もです!
>> ご飯食べた?
私もお昼ご飯に行きたい!
>> こんにちは。
こんにちは。️はお休みですか?
>> それでは御免蒙りまするでござります。
私もいつか、UNKさんの絵を見てみたいです。

 前回の結果と比較すると、「騙さです」とか謎ですが、比較的受け答えがしっかりしてきました。

 一方、「今何してる?」という問いかけはどうも苦手らしく、無難に「私もです!」と逃げる傾向があります。

 訓練データを360万会話対に増やした結果は以下のとおりです。入力系列長60、出力系列長50という条件で、1エポックあたりの処理時間は8時間超です。perplexityは43程度になりました。

>> おはよう!

おはようございます。今日も寒いですね。
>> 今何してる?
私は今から仕事です
>> ご飯食べた?
私は食べました!
>> こんにちは。
こんにちは。今日もよろしくです。
>> それでは御免蒙りまするでござります。
私もです。

 だいぶ、自然な受け答えになってきました。

 訓練したニューラルネットワークは、Twitter上で利用できるようにしてあります。スクリーンネーム@Gacky01Bにつぶやくと、ニューラルネットワークが生成した応答文をリプライします。


8. おわりに

 以上、訓練データの増量に対応するための、処理性能及びメモリ使用量改善施策について記述しました。2-2節の施策によって処理の高速化を図ることが出来、訓練データ量をさらに増加させても、訓練を進められるめどが立ちました。

 訓練データ量の増加により、文書生成の精度には、明らかに改善が見られます。データ量の量的拡大には確かに、効果があったと考えています。


変更履歴

項番
日付
変更箇所
内容

1
2019/1/28
-
初版

2
2019/2/6
4-4
import宣言をプログラムの先頭に集めた

3
2019/3/17
4-2
ニューラルネットワーク定義クラスdialog_cagegorize.pyencoder_inputおよびdecoder_inputsのデータ属性を、int16からint32に訂正

4
2019/3/22
7
訓練データ量を360万会話対に増加させた時の結果を追記

5
2019/3/25
1,7
Twitterボット@Gacky01Bへのリンク追加