TensorFlow を用いた、Seq2Seq モデルの1つである RNN Encoder-Decoder による自然言語処理(質問応答、ワード予想等)の練習用実装コード集。
この README.md
ファイルには、各コードの実行結果、概要、RNN の背景理論の説明を記載しています。
分かりやすいように main.py
ファイル毎に1つの完結した実行コードにしています。
自然言語処理(NLP)に関しての、背景理論は以下のサイトに記載してあります。
TensorFlow ライブラリ
tf.contrib.rnn.LSTMRNNCell(...)
:
時系列に沿った LSTM 構造を提供するクラスLSTMCell
のcell
を返す。
このcell
は、内部(プロパティ)で state(隠れ層の状態)を保持しており、これを次の時間の隠れ層に順々に渡していくことで、時間軸の逆伝搬を実現する。https://www.tensorflow.org/api_docs/python/tf/contrib/rnn/LSTMCell
tf.einsum(...)
: Tensor 積等の Tensor 間演算をアインシュタイン縮約記法で指定する。
その他ライブラリ
- Project Gutenberg ( https://www.gutenberg.org/ ) にある文学作品のデータ
RNN Encoder-Decoder(LSTM 使用) による自然言語処理の応用例として、質問応答(QA)があるが、ここでは、この質問応答(QA)の簡単な例として、指定された数字の足し算を答える応答問題を考える。
以下、コードの説明。
- まず、指定された数字の足し算を答える応答用に、整数の加算演算データセット(サンプル数
n_samples
= 20000 個)を生成する関数MLPreProcess.generate_add_uint_operation_dataset(...)
を実装する。
- この関数の処理は、以下のコードのようになる。
- 指定された桁数の整数字をランダムに生成する処理を、関数ブロックで以下のコードのように実装しておく。(後述の処理で使用)
[MLPreProcess.py] def generate_add_uint_operation_dataset( ... ): ... def generate_number_uint( digits ): """ 指定された桁数の整数字をランダムに生成する。 """ number = "" # 1 ~ digit 番目の桁数に関してのループ for i in range( numpy.random.randint(1, digits+1) ): number += numpy.random.choice( list("0123456789") ) return int(number)
- そして、各シーケンスの空白文字の Padding 処理による桁合わせ処理を、関数ブロックで以下のコードのように実装する。(後述の処理で使用)
[MLPreProcess.py] def generate_add_uint_operation_dataset( ... ): ... def padding( str, max_len ): """ 空白文字の Padding 処理による桁合わせ """ # 空白 × 埋め合わせ数 str_padding = str + " " * ( max_len - len(str) ) return str_padding
- ランダムに生成した、桁数
digits
からなる2つの整数の足し算の式uint_x + uint_y
と、この式の解に対応するデータ(応答データ)を、指定されたサンプル数n_samples
個ぶん作成し、それらを空白文字" "
で padding 処理してシーケンス長を揃えておく。
[MLPreProcess.py] def generate_add_uint_operation_dataset( ... ): ... # 入力桁数 input_digit = digits * 2 + 1 # 123+456 # 出力桁数 output_digit = digits + 1 # 500+500=1000 のような桁上りのケースを考慮 # dat_x = [] dat_y = [] # 指定されたサンプル数ぶんループ処理 for i in range( n_samples ): uint_x = generate_number_uint( digits ) uint_y = generate_number_uint( digits ) train = "{}+{}".format( uint_x, uint_y ) train = padding( train, input_digit ) dat_x.append( train ) target = "{}".format( uint_x + uint_y ) target = padding( target, output_digit ) dat_y.append( target )
- 文字からインデックスへのディクショナリ
dict_str_to_idx
にもとづき、各文字を one-hot encode 処理する。
[MLPreProcess.py] def generate_add_uint_operation_dataset( ... ): ... map_str = "0123456789+ " # map 作成用の使用する文字列 # 文字からインデックスへの map dict_str_to_idx = { key: idx for (idx,key) in enumerate( map_str ) } # インデックスから文字への map dict_idx_to_str = { idx: key for (key,idx) in dict_str_to_idx.items() } # one-hot encode されたデータ shape = (n_sample, sequence, one-hot encodeed vector size) X_features = numpy.zeros( ( len(dat_x), input_digit, len(map_str) ), dtype = numpy.int ) y_labels = numpy.zeros( ( len(dat_x), output_digit, len(map_str) ), dtype = numpy.int ) for i in range( n_samples ): for (j, str) in enumerate( dat_x[i] ): X_features[ i, j, dict_str_to_idx[str] ] = 1 # one-hot encode の 1 の部分 for (j, str) in enumerate( dat_y[i] ): y_labels[ i, j, dict_str_to_idx[str] ] = 1 # one-hot encode の 1 の部分 return X_features, y_labels, dict_str_to_idx, dict_idx_to_str
- この関数の処理は、以下のコードのようになる。
- データセットを、トレーニング用データセットと、テスト用データセットに分割する。
- 分割割合は、トレーニング用データ 90%、テスト用データ 10%
X_train, X_test, y_train, y_test \ = MLPreProcess.dataTrainTestSplit( X_input = X_features, y_input = y_labels, ratio_test = 0.1, input_random_state = 1 )
- この自然言語処理(NLP)に対応した、RNN Encoder-Decoder モデルの各種パラメーターの設定を行う。
- この設定は、
RecurrectNNEncoderDecoderLSTM
クラスのインスタンス作成時の引数にて行う。
[main1.py] rnn1 = RecurrectNNEncoderDecoderLSTM( session = tf.Session(), n_inputLayer = 12, # 12 : "0123456789+ " の 12 文字 n_hiddenLayer = 128, # rnn の cell 数と同じ n_outputLayer = 12, # 12 : "0123456789+ " の 12 文字 n_in_sequence_encoder = 7, # エンコーダー側のシーケンス長 / 足し算の式のシーケンス長 : "123 " "+" "456 " の計 4+1+4=7 文字 n_in_sequence_decoder = 4, # デコーダー側のシーケンス長 / 足し算の式の結果のシーケンス長 : "1000" 計 4 文字 epochs = 20000, batch_size = 100, eval_step = 1 )
- この設定は、
- RNN Encoder-Decoder (LSTM使用) モデルの構造を定義する。
- この処理は、
RecurrectNNEncoderDecoderLSTM
クラスのmodel()
メソッドにて行う。 - まず、Encoder 側のモデルを RNN の再帰構造に従って構築していく。
- Encoder 側の最終的な出力は、次の Decoder の初期入力となる。
[RecurrectNNEncoderDecoderLSTM.py] def model(): ... #-------------------------------------------------------------- # Encoder #-------------------------------------------------------------- # tf.contrib.rnn.LSTMCell(...) : 時系列に沿った RNN 構造を提供するクラス `LSTMCell` のオブジェクト cell を返す。 # この cell は、内部(プロパティ)で state(隠れ層の状態)を保持しており、 # これを次の時間の隠れ層に順々に渡していくことで、時間軸の逆伝搬を実現する。 cell_encoder = tf.contrib.rnn.LSTMCell( num_units = self._n_hiddenLayer, # int, The number of units in the RNN cell. forget_bias = 1.0 # 忘却ゲートのバイアス項 / Default : 1.0 in order to reduce the scale of forgetting at the beginning of the training. ) #self._rnn_cells_encoder.append( cell_encoder ) # 後述の処理で同様の処理が入るので不要 # 最初の時間 t0 では、過去の隠れ層がないので、 # cell.zero_state(...) でゼロの状態を初期設定する。 initial_state_encoder_tsr = cell_encoder.zero_state( self._batch_size_holder, tf.float32 ) self._rnn_states_encoder.append( initial_state_encoder_tsr ) # Encoder の過去の隠れ層の再帰処理 with tf.variable_scope('Encoder'): for t in range( self._n_in_sequence_encoder ): if (t > 0): # tf.get_variable_scope() : 名前空間を設定した Variable にアクセス # reuse_variables() : reuse フラグを True にすることで、再利用できるようになる。 tf.get_variable_scope().reuse_variables() # LSTMCellクラスの `__call__(...)` を順次呼び出し、 # 各時刻 t における出力 cell_output, 及び状態 state を算出 cell_encoder_output, state_encoder_tsr = cell_encoder( inputs = self._X_holder[:, t, :], state = self._rnn_states_encoder[-1] ) # 過去の隠れ層の出力をリストに追加 self._rnn_cells_encoder.append( cell_encoder_output ) self._rnn_states_encoder.append( state_encoder_tsr ) # 最終的な Encoder の出力 #output_encoder = self._rnn_cells_encoder[-1]
- 次に、Decoder 側のモデルを RNN の再帰構造に従って構築していく。
- Decoder の初期状態は Encoder の最終出力なので、これに従って初期状態を定める。
- 尚、Decoder のモデルは、教師データの一部を使用するが、損失関数等の評価指数の計算時は、この教師データは使用しないので、モデルのトレーニング時と損失関数等のモデルに関連付けられた評価指数の計算時とで、処理を分ける。
[RecurrectNNEncoderDecoderLSTM.py] def model(): ... cell_decoder = tf.contrib.rnn.LSTMCell( num_units = self._n_hiddenLayer, # int, The number of units in the RNN cell. forget_bias = 1.0 # 忘却ゲートのバイアス項 / Default : 1.0 in order to reduce the scale of forgetting at the beginning of the training. ) # Decoder の初期状態は Encoder の最終出力 self._rnn_cells_decoder.append( self._rnn_cells_encoder[-1] ) # Decoder の初期状態は Encoder の最終出力 initial_state_decoder_tsr = self._rnn_states_encoder[-1] self._rnn_states_decoder.append( initial_state_decoder_tsr ) # 隠れ層 ~ 出力層の重みを事前に設定 self._weights.append( self.init_weight_variable( input_shape = [self._n_hiddenLayer, self._n_outputLayer] ) ) self._biases.append( self.init_bias_variable( input_shape = [self._n_outputLayer] ) ) eval_outputs = [] # Decoder の過去の隠れ層の再帰処理 with tf.variable_scope('Decoder'): # t = 1 ~ self._n_in_sequence_decoder 間のループ処理 (t != 0) # t = 0 を含まないのは、Decoder の t = 0 の初期状態は、Encoder の最終出力で処理済みのため for t in range( 1, self._n_in_sequence_decoder ): if (t > 1): # tf.get_variable_scope() : 名前空間を設定した Variable にアクセス # reuse_variables() : reuse フラグを True にすることで、再利用できるようになる。 tf.get_variable_scope().reuse_variables() # トレーニング処理中の場合のルート if ( self._bTraining_holder == True ): with tf.name_scope( "Traning_root" ): # LSTMCellクラスの `__call__(...)` を順次呼び出し、 # 各時刻 t における出力 cell_output, 及び状態 state を算出 cell_decoder_output, state_decoder_tsr = cell_decoder( inputs = self._t_holder[:, t-1, :], state = self._rnn_states_decoder[-1] ) # loss 値などの評価用の値の計算時のルート # デコーダーの次の step における出力計算時、self._t_holder[:, t-1, :] という正解データ(教師データ)を使用しないようにルート分岐させる。 else: with tf.name_scope( "Eval_root" ): # matmul 計算時、直前の出力 self._rnn_cells_decoder[-1] を入力に用いる cell_decoder_output = tf.matmul( self._rnn_cells_decoder[-1], self._weights[-1] ) + self._biases[-1] cell_decoder_output = tf.nn.softmax( cell_decoder_output ) eval_outputs.append( cell_decoder_output ) cell_decoder_output = tf.one_hot( tf.argmax(cell_decoder_output, -1), depth = self._n_in_sequence_decoder) cell_decoder_output, state_decoder_tsr = cell_decoder( cell_decoder_output, self._rnn_states_decoder[-1] ) # 過去の隠れ層の出力をリストに追加 self._rnn_cells_decoder.append( cell_decoder_output ) self._rnn_states_decoder.append( state_decoder_tsr )
- 最終的な出力層からの出力
self._y_out_op
を構築する。- まず、Decoder の出力を
tf.concat(...)
で結合し、tf.reshape(...)
で適切な形状に reshape する。 - そして、reshape した Tensor に対し、
tf.einsum(...)
ortf.matmul(...)
を用いてテンソル積 or 行列積をとる。 - 最終的なモデルの出力は、この線形和を softmax して出力する。
- まず、Decoder の出力を
[RecurrectNNEncoderDecoderLSTM.py] def model(): ... # トレーニング処理中の場合のルート if ( self._bTraining_holder == True ): with tf.name_scope( "Traning_root" ): #-------------------------------------------------------------- # 出力層への入力 #-------------------------------------------------------------- # まず、Decoder の出力を `tf.concat(...)` で結合し、`tf.reshape(...)` で適切な形状に reshape する。 # self._rnn_cells_decoder の形状を shape = ( データ数, デコーダーのシーケンス長, 隠れ層のノード数 ) に reshape # tf.concat(...) : Tensorを結合する。引数 axis で結合する dimension を決定 output = tf.reshape( tf.concat( self._rnn_cells_decoder, axis = 1 ), shape = [ -1, self._n_in_sequence_decoder, self._n_hiddenLayer ] ) # そして、reshape した Tensor に対し、`tf.einsum(...)` or `tf.matmul(...)` を用いてテンソル積 or 行列積をとる。 # 3 階の Tensorとの積を取る(2階なら行列なので matmul でよかった) # Σ_{ijk} の j 成分を残して、matmul する # tf.einsum(...) : Tensor の積の アインシュタインの縮約表現 # equation : the equation is obtained from the more familiar element-wise (要素毎の)equation by # 1. removing variable names, brackets, and commas, # 2. replacing "*" with ",", # 3. dropping summation signs, # and 4. moving the output to the right, and replacing "=" with "->". y_in_op = tf.einsum( "ijk,kl->ijl", output, self._weights[-1] ) + self._biases[-1] #-------------------------------------------------------------- # モデルの出力 #-------------------------------------------------------------- # softmax self._y_out_op = tf.nn.softmax( y_in_op ) # loss 値などの評価用の値の計算時のルート else: with tf.name_scope( "Eval_root" ): #-------------------------------------------------------------- # 出力層への入力 #-------------------------------------------------------------- y_in_op = tf.matmul( self._rnn_cells_decoder[-1], self._weights[-1] ) + self._biases[-1] #-------------------------------------------------------------- # モデルの出力 #-------------------------------------------------------------- # softmax self._y_out_op = tf.nn.softmax( y_in_op ) # モデルの最終的な出力を含める eval_outputs.append( self._y_out_op ) # Decoder の出力、及び モデルの最終的な出力を `tf.concat(...)` で結合し、`tf.reshape(...)` で適切な形状に reshape する。 # self._y_out_op の形状を shape = ( データ数, デコーダーのシーケンス長, 出力層ののノード数 ) に reshape # tf.concat(...) : Tensorを結合する。引数 axis で結合する dimension を決定 self._y_out_op = tf.reshape( tf.concat( eval_outputs, axis = 1 ), [-1, self._n_in_sequence_decoder, self._n_outputLayer ] ) return self._y_out_op
- この処理は、
- 損失関数として、ソフトマックス・エントロピー関数を使用する。
[main1.py] rnn1.loss( SoftmaxCrossEntropy() )
- 最適化アルゴリズム Optimizer として、Adam アルゴリズム を使用する。
- 学習率
learning_rate
は、0.001 で検証。減衰項はbeta1 = 0.9
,beta1 = 0.999
[main1.py] rnn1.optimizer( Adam( learning_rate = learning_rate1, beta1 = adam_beta1, beta2 = adam_beta2 ) )
- 学習率
- トレーニング用データ
X_train
,y_train
に対し、fitting 処理を行う。[main1.py] rnn1.fit( X_train, y_train )
- fitting 処理
fit(...)
後のモデル(学習済みモデル)で、予想を行い、正解率を算出する。- 正解率の算出は
accuracy(...)
メソッドを使用して行う。 - この際、one-hot encoding 要素方向 ( axis=2 ) で
numpy.argmax(...)
して、文字の数値インデックス取得する。(シーケンス長の dimension が追加されたため)
[main1.py] # 正解率を取得 accuracy_total1 = rnn1.accuracy( X_features, y_labels ) accuracy_train1 = rnn1.accuracy( X_train, y_train ) accuracy_test1 = rnn1.accuracy( X_test, y_test ) print( "accuracy_total1 : {} / n_sample : {}".format( accuracy_total1, len(X_features[:,0,0]) ) ) print( "accuracy_train1 : {} / n_sample : {}".format( accuracy_train1, len(X_train[:,0,0]) ) ) print( "accuracy_test1 : {} / n_sample : {}".format( accuracy_test1, len(X_test[:,0,0]) ) )
[RecurrectNNEncoderDecoderLSTM.py] def accuracy( ... ): # 予想ラベルを算出する。 predicts = self.predict( X_test ) # y_test の one-hot encode された箇所を argmax し、文字に対応した数値インデックスに変換 y_labels = numpy.argmax( y_test, axis = -1 ) # 正解数 n_corrects = 0 resluts = numpy.equal( predicts, y_labels ) # shape = (n_sample, n_in_sequence_decoder ) for i in range( len(X_test[:,0,0]) ): # 各サンプルのシーケンス内で全てで True : [True, True, True, True] なら 正解数を +1 カウント if ( all( resluts[i] ) == True ): n_corrects = n_corrects + 1 # 正解率 accuracy = n_corrects / len( X_test[:,0,0] ) return accuracy
[RecurrectNNEncoderDecoderLSTM.py] def predict( ... ): prob = self._session.run( self._y_out_op, feed_dict = { self._X_holder: X_test, self._batch_size_holder: len( X_test[:,0,0] ), self._bTraining_holder: False } ) # one-hot encoding 要素方向で argmax して、文字の数値インデックス取得 # numpy.argmax(...) : 多次元配列の中の最大値の要素を持つインデックスを返す # axis : 最大値を読み取る軸の方向 (-1 : 最後の次元数、この場合 i,j,k の k) predicts = numpy.argmax( prob, axis = -1 ) return predicts
- 正解率の算出は
- fitting 処理
fit(...)
後のモデル(学習済みモデル)で、幾つかの指定された質問文に対する応答文の予想値を確かめてみる。- この質問文に対する応答文の予想は
question_answer_responce(...)
メソッドを使用して行う。
[main1.py] #--------------------------------------------------------- # 質問&応答処理 #--------------------------------------------------------- # 質問文の数 n_questions = min( 100, len(X_test[:,0,0]) ) for q in range( n_questions ): answer = rnn1.question_answer_responce( question = X_test[q,:,:], dict_idx_to_str = dict_idx_to_str ) # one-hot encoding → 対応する数値インデックス → 対応する文字に変換 question = numpy.argmax( X_test[q,:,:], axis = -1 ) question = "".join( dict_idx_to_str[i] for i in question ) print( "-------------------------------" ) print( "n_questions = {}".format( q ) ) print( "Q : {}".format( question ) ) print( "A : {}".format( answer ) ) # 正解データ(教師データ)をone-hot encoding → 対応する数値インデックス → 対応する文字に変換 target = numpy.argmax( y_test[q,:,:], axis = -1 ) target = "".join( dict_idx_to_str[i] for i in target ) if ( answer == target ): print( "T/F : T" ) else: print( "T/F : F" ) print( "-------------------------------" )
[RecurrectNNEncoderDecoderLSTM.py] def question_answer_responce( ... ): if ( question.ndim == 2): # 3 次元に reshape / (7,12) → (1,7,12) question = [ question ] # question に対する予想値 prob = self._y_out_op.eval( session = self._session, feed_dict = { self._X_holder: question, self._batch_size_holder: 1, self._bTraining_holder: False } ) # one-hot encoding 要素方向で argmax して、文字の数値インデックス取得 answer = numpy.argmax( prob, axis = -1 ) # ディクショナリにもとづき、数値インデックスを文字の変換 answer = "".join( dict_idx_to_str[i] for i in answer[0] ) return answer
- この質問文に対する応答文の予想は
- 尚、このモデルの TensorBorad で描写した計算グラフは以下のようになる。(純粋なモデルの構築時の計算グラフ。損失関数等のモデルに関連付けられた評価指数の計算時の計算グラフではない)
- {入力層 : 12 ノード、隠れ層 : 128 ノード、出力層 : 12 ノード}の RNN Encoder-Decoder モデル
- エンコーダーのシーケンス長 : 7 個, デコーダーのシーケンス長 : 4 個
- 学習率 0.001, 最適化アルゴリズム : Adam ( 減衰項 : beta1 = 0.9, beta2 = 0.999 )
- サンプル数 20000 個(トレーニング用データ : 90 %、テスト用データ : 10% に分割)
- エポック数 20000 回, ミニバッチサイズ 100
エポック数 15000 回程度で 0 に収束しており、うまく学習出来ていることが見て取れる。
data | Acuraccy | サンプル数 |
---|---|---|
total | 0.969 | 20000 |
train data | 0.989 | 18000 (90.0%) |
test data | 0.794 | 2000 (10.0%) |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
質問 Q | 3+881 | 830+95 | 19+5 | 1+21 | 34+910 | 52+14 | 7+1 | 522+3 | 9+8 | 433+77 | 244+396 | 0+71 | 87+613 | 119+573 | 568+4 | 98+2 | 287+302 | 87+613 | 119+573 | 568+4 |
応答 A | 884 | 916 | 24 | 22 | 944 | 67 | 8 | 525 | 17 | 510 | 631 | 71 | 700 | 612 | 572 | 100 | 598 | 700 | 612 | 572 |
正誤 | ○ | × | ○ | ○ | ○ | × | ○ | ○ | ○ | ○ | × | ○ | ○ | × | ○ | ○ | × | ○ | × | ○ |
足し算の応答結果が誤っている場合でも、全く正解と離れた値ではなく、正解に近い値になっている傾向が見て取れる。
これは、このモデル (RNN Encoder-Decoder) が、足し算のルール(入出力パターン)をうまく学習出来ているためと考えられる。
尚、応答結果が誤っている場合において、足し算の値が大きい場合のほうが、正解との値の差は大きい傾向も見て取れる。
実装中...
many-to-many な構造を持つ多層 RNN による自然言語処理(NLP)の一例として、英文学作品のシェイクスピア作品のテキストデータ ( http://www.gutenberg.org/cache/epub/100/pg2265.txt ) を用いて、many-to-many な RNN(LSTM 使用)モデルで学習し、特定のワード("thus"(それ故), "more"(更には) 等)の後に続くワードを予想する処理を実装する。
tf.nn.rnn.BasicLSTMCell(...)
: LSTM セル
tf.nn.dynamic_rnn(...)
: 可変長サイズの動的な RNNhttps://www.tensorflow.org/api_docs/python/tf/nn/dynamic_rnn
記載中...
以下、コードの説明
- まず、The Project Gutenberg EBook にある、シェイクスピア作品のテキストデータの読み込み&抽出処理を行う。
- この処理は、
MLPreProcess.load_textdata_by_shakespeare_from_theProjectGutenbergEBook(...)
関数にて行う。- 具体的には、まず、指定されたテキストデータの各行を Unicode 形式で読み込み、データを格納する。
[MLPreProcess.py] def load_textdata_by_shakespeare_from_theProjectGutenbergEBook( ... ): text_data = [] #-------------------------------------------------------- # codecs.open() 関数と with 構文でテキストデータの読み込む # "r" : 文字のまま読み込み #-------------------------------------------------------- with codecs.open( path, "r", "utf-8" ) as file: # txt ファイルの各行に関してのループ処理 for row in file: # 各行の文字列全体(特殊文字、空白込 : \t \n)を格納 text_data.append( row )
- 読み込むテキストデータの内容上、本文とは関係ない説明文を除外する。
[MLPreProcess.py] def load_textdata_by_shakespeare_from_theProjectGutenbergEBook( ... ): ... # EBook のテキストファイルに含まれている、最初の説明文の段落部分を除外 text_data = text_data[ n_DeleteParagraph : ]
- 改行, 先頭に復帰の特殊文字 \n, \r を削除する
[MLPreProcess.py] def load_textdata_by_shakespeare_from_theProjectGutenbergEBook( ... ): ... text_data = [ str.replace( "\r\n", "" ) for str in text_data ] text_data = [ str.replace( "\n", "" ) for str in text_data ]
- クリーニング処理を実施する。
これは、文字量を減らすために、各種句読点、余分なホワイトスペースを削除する処理となる。
但し、ハイフン "-" と、アポストロフィ "'" は残す。(この作品文章が文章内容を繋ぐのに、頻繁に使用されているため)
[MLPreProcess.py] def load_textdata_by_shakespeare_from_theProjectGutenbergEBook( ... ): ... if ( bCleaning == True ): # string.punctuation : 文字と文字の間の句読点、括弧などをまとめたもの # 置換表現で「!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~」等 punctuation = string.punctuation # punctuation から ハイフン "-" と、アポストロフィ "'" を除外 # sep.join(seq) : sepを区切り文字として、seqを連結してひとつの文字列にする。 punctuation = ''.join( [ x for x in punctuation if x not in ['-', "'"] ] ) def clean_text( str ): # ハイフン "-" と、アポストロフィ "'" 以外の特殊文字をホワイトスペース " " に置換 # re.sub() : 正規表現で文字列を別の文字列で置換 str = re.sub( pattern = r"[{}]".format( punctuation ), # 正規表現 : []集合, |和集合(または)()グループ化 repl = " ", # 置換する文字列 : " " なのでホワイトスペースに置換 string = str # 置換される文字列 ) # 任意の空白文字 \s = [\t\n\r\f\v] をホワイトスペース " " に置換 # + : 1回以上の繰り返し(正規表現) str = re.sub( pattern = "\s+", # 正規表現 : []集合, |和集合(または)()グループ化 repl = " ", # 置換する文字列 : " " なのでホワイトスペースに置換 string = str # 置換される文字列 ) # ホワイトスペース " " に置換したものを一斉に除外 # str.strip() : 引数を指定しないとホワイトスペースを除去する str = str.strip() # リスト中の大文字→小文字に変換 str = str.lower() return str text_data = [ clean_text(str) for str in text_data ]
- この処理は、
- 抽出したテキストデータから、出現頻度の高い単語をディクショナリに登録する。
(出現頻度の高い単語のみ学習の対象とする。出現頻度の低い単語は除外)- この処理は
MLPreProcess.text_vocabulary_processing_without_tensorflow(...)
にて行う。- 前段階として、str からなる list → 1つの str に変換したものを、空白スペースで split し、単語単位の配列に変換しておく。
[MLPreProcess] def text_vocabulary_processing_without_tensorflow(...): ... # list<str> → 1つの str に変換 text_data = "".join( text_data ) # 空白スペースで split し、単語単位の配列に変換 text_data = text_data.split( " " )
collections
モジュールのcollections.Counter(...)
を用いて、抽出したテキストデータから、単語の出現頻度を数える
[MLPreProcess] def text_vocabulary_processing_without_tensorflow(...): ... word_counts = collections.Counter( text_data )
- 抽出した単語の出現頻度から、出現頻度の高い (
min_word_freq
値以上の) 単語をディクショナリに登録する。出現頻度の低い単語は除外
[MLPreProcess] def text_vocabulary_processing_without_tensorflow(...): ... word_counts = { key: count for (key,count) in word_counts.items() if count > min_word_freq } # ディクショナリの内包表現
- 語彙 "xxx" → インデックスへの map を作成する。
[MLPreProcess] def text_vocabulary_processing_without_tensorflow(...): ... # dict.keys() : ディクショナリから key を取り出し dict_keys_words = word_counts.keys() dict_vcab_to_idx = { key: (idx+1) for (idx,key) in enumerate( dict_keys_words ) } # 不明な key (=vocab) のインデックスとして 0 を登録 dict_vcab_to_idx[ "unknown" ] = 0
- インデックス → 語彙 "xxx" への map を作成する。
[MLPreProcess] def text_vocabulary_processing_without_tensorflow(...): ... dict_idx_to_vocab = { idx: key for (key,idx) in dict_vcab_to_idx.items() }
- 更に、抽出したテキストデータを、このディクショナリに基づき、数値インデックス情報に変換する。
[MLPreProcess] def text_vocabulary_processing_without_tensorflow(...): ... #--------------------------------------------------------- # テキストデータのインデックス配列 #--------------------------------------------------------- text_data_idx = [] # テキストから抽出した単語単位の配列 text_data に関してのループ for (idx,words) in enumerate( text_data ): try: text_data_idx.append( dict_vcab_to_idx[words] ) except: text_data_idx.append( 0 ) # list → ndarray に変換 text_data_idx = numpy.array( text_data_idx ) # 単語の数 n_vocab = len( dict_idx_to_vocab ) + 1
- そして、抽出したテキストデータを数値インデックス情報に変換したデータの形状を、
入力用 x と(1文字ずれた)出力用 y のバッチに変換する。
[main3.py]
- この処理は
- この自然言語処理(NLP)に対応した、RNN Encoder-Decoder モデルの各種パラメーターの設定を行う。
- この設定は、
Seq2SeqMultiRNNLSTM
クラスのインスタンス作成時の引数にて行う。 - xxx
[main3.py] rnn = Seq2SeqMultiRNNLSTM( 実装中... )
- この設定は、
- many-to-many な RNN(多層の LSTM)モデルの構造を定義する。
- この処理は、
Seq2SeqMultiRNNLSTM
クラスのmodel()
メソッドにて行う。 - 再帰構造に従って、モデルを構築していく。
- そのために、
tf.tf.rnn.BasicLSTMCell(...)
を用いて、時系列に沿った RNN 構造を提供するクラスBasicLSTMCell
の cell を取得する。 - この cell は、内部(プロパティ)で state(隠れ層の状態)を保持しており、 これを次の時間の隠れ層に順々に渡していくことで、時間軸の逆伝搬を実現する。
[Seq2SeqMultiRNNLSTM.py] def model(): ... 実装中
- 最初の時間 t0 では、過去の隠れ層がないので、
cell.zero_state(...)
でゼロの状態を初期設定する。 tf.nn.dynamic_rnn(...)
を用いて、可変長な RNN シーケンスを作成する。
- そのために、
- この処理は、
- xxx
- このモデルの計算グラフを TensorBoard で表示すると以下のようになる。
実装中...
- 隠れ層(LSTM): 128 ノード、の many-to-many な RNN モデル
- テキストコーパスの分割単位:単語単位
- テキストコーパスの総単語数:4920 個
- 学習率 0.001, 最適化アルゴリズム : Adam ( 減衰項 : beta1 = 0.9, beta2 = 0.999 )
- エポック数 100 回, ミニバッチサイズ 4、ミニバッチのイテレーション回数 400 回
- 隠れ層(LSTM): 128 ノード、の many-to-many な RNN モデル
- テキストコーパスの分割単位:文字単位
- テキストコーパスの総単語数:xxx 個
- 学習率 0.001, 最適化アルゴリズム : Adam ( 減衰項 : beta1 = 0.9, beta2 = 0.999 )
- エポック数 100 回, ミニバッチサイズ 25、ミニバッチのイテレーション回数 2500 回
The much thee much horatio liue much liue thee horatio unknown horatio liue liue horatio liue liue liue much much liue liue thee liue thee liue horatio horatio liue horatio thee horatio liue horatio thee liue thee horatio thee liue liue horatio liue liue thee horatio thee thee liue liue liue liue horatio liue liue horatio thee liue thee horatio liue liue liue horatio liue much unknown thee thee thee thee liue liue liue horatio liue unknown liue liue liue liue unknown horatio liue unknown thee thee unknown liue much thee much liue horatio thee thee liue horatio liue liue unknown horatio
-
星の本棚 : 自然言語処理(NLP)
-
TensorFlow(公式)
-
Neural Machine Translation (seq2seq)
-
Seq2Seq まとめ
-
もちもちしている
[18/04/29]
[main3_1.py]
text = text[0:10000]
Epoch 1/100 Iteration 1| Training loss: 9.1010
Epoch 1/100 Iteration 2| Training loss: 9.0976
Epoch 1/100 Iteration 3| Training loss: 9.0942
Epoch 1/100 Iteration 4| Training loss: 9.0902
Epoch 1/100 Iteration 5| Training loss: 9.0849
Epoch 1/100 Iteration 6| Training loss: 9.0783
Epoch 1/100 Iteration 7| Training loss: 9.0685
Epoch 1/100 Iteration 8| Training loss: 9.0543
Epoch 1/100 Iteration 9| Training loss: 9.0331
Epoch 2/100 Iteration 10| Training loss: 9.0011
Epoch 2/100 Iteration 11| Training loss: 8.9074
Epoch 2/100 Iteration 12| Training loss: 8.6036
Epoch 2/100 Iteration 13| Training loss: 8.1486
Epoch 2/100 Iteration 14| Training loss: 7.8879
Epoch 2/100 Iteration 15| Training loss: 7.6821
Epoch 2/100 Iteration 16| Training loss: 7.4822
Epoch 2/100 Iteration 17| Training loss: 7.2818
Epoch 2/100 Iteration 18| Training loss: 7.1040
Epoch 3/100 Iteration 19| Training loss: 7.1695
Epoch 3/100 Iteration 20| Training loss: 6.7276
Epoch 3/100 Iteration 21| Training loss: 6.5226
Epoch 3/100 Iteration 22| Training loss: 6.3341
Epoch 3/100 Iteration 23| Training loss: 6.1582
Epoch 3/100 Iteration 24| Training loss: 5.9937
Epoch 3/100 Iteration 25| Training loss: 5.8265
Epoch 3/100 Iteration 26| Training loss: 5.6644
Epoch 3/100 Iteration 27| Training loss: 5.5686
Epoch 4/100 Iteration 28| Training loss: 5.7141
Epoch 4/100 Iteration 29| Training loss: 5.3697
Epoch 4/100 Iteration 30| Training loss: 5.2486
Epoch 4/100 Iteration 31| Training loss: 5.1735
Epoch 4/100 Iteration 32| Training loss: 5.1055
Epoch 4/100 Iteration 33| Training loss: 5.0376
Epoch 4/100 Iteration 34| Training loss: 4.9538
...
...
...
Epoch 99/100 Iteration 889| Training loss: 4.5974
Epoch 99/100 Iteration 890| Training loss: 4.5539
Epoch 99/100 Iteration 891| Training loss: 4.6373
Epoch 100/100 Iteration 892| Training loss: 4.6482
Epoch 100/100 Iteration 893| Training loss: 4.6546
Epoch 100/100 Iteration 894| Training loss: 4.6130
Epoch 100/100 Iteration 895| Training loss: 4.6328
Epoch 100/100 Iteration 896| Training loss: 4.6214
Epoch 100/100 Iteration 897| Training loss: 4.6402
Epoch 100/100 Iteration 898| Training loss: 4.5956
Epoch 100/100 Iteration 899| Training loss: 4.5607
Epoch 100/100 Iteration 900| Training loss: 4.6327
train_x.shape : (64, 2540)
train_y.shape : (64, 2540)