ichou1のブログ

主に音声認識、時々、データ分析のことを書く

Kerasメモ(seq2seqで足し算)

以下の書籍を参考に、seq2seqを使った足し算を試してみる。

https://www.oreilly.co.jp/books/images/picture978-4-87311-836-9.gif
O'Reilly Japan - ゼロから作るDeep Learning ❷

データの長さは固定で「12」、値がない部分はブランクで埋める。

  • 式部分の長さ : 「7」固定
  • 答え部分の長さ : 「5」固定(イコールにあたる"_"含む)
データ例(addition.txt)
16+75  _91  
52+607 _659 
75+22  _97  
63+22  _85  
795+3  _798 
706+796_1502
...

サンプル数は全体で「50,000」、学習用に90%、残り10%を検証用に分割する。

出現する文字は、数字の「0から9」、プラス記号、ブランク、アンダーバーの13種類。

「input_dim」が「13」、「output_dim」が「16」のEmbeddingレイヤを通して特徴ベクトルに変換する。

今回、Encoder側とDecoder側とでEmbeddingレイヤを「それぞれ持つ」パターンと「共有する」パターンを試してみる。

モデルはKerasで書き直した。

Embeddingレイヤをそれぞれ持つパターン

学習モデル

f:id:ichou1:20190215210929p:plain

推論モデル

f:id:ichou1:20190215213954p:plain



Embeddingレイヤを共有するパターン

学習モデル

f:id:ichou1:20190215215410p:plain

推論モデル

f:id:ichou1:20190215220343p:plain

学習時のパラメータを以下のとおり設定。

batch_size = 128  # Batch size for training.
epochs = 20  # Number of epochs to train for.
train_model.compile(optimizer='adam', loss='categorical_crossentropy')

学習後、検証データを推論モデルに渡した結果の一部。2番目は誤りパターン。

Input sentence:   ['3', '6', '7', '+', '5', '5', ' ']
Decoded sentence: ['4', '2', '2', ' ']
label sentence:   ['4', '2', '2', ' ']
-
Input sentence:   ['6', '0', '0', '+', '2', '5', '7']
Decoded sentence: ['8', '6', '7', ' ']
label sentence:   ['8', '5', '7', ' ']
-
Input sentence:   ['7', '6', '1', '+', '2', '9', '2']
Decoded sentence: ['1', '0', '5', '3']
label sentence:   ['1', '0', '5', '3']
-


今回、3種類のモデルを作成した。

  • 1番目のモデル : EmbeddingレイヤをEncoder / Decoderでそれぞれ保持、インプットは順方向
  • 2番目のモデル : EmbeddingレイヤをEncoder / Decoderでそれぞれ保持、インプットは逆方向
  • 3番目のモデル : EmbeddingレイヤをEncoder / Decoderで共有、インプットは順方向

各モデルの損失は以下のとおり推移。
f:id:ichou1:20190215203114p:plain

一番速く損失が減少したのはインプットを逆方向にしたモデルだった。
また、Embeddingレイヤを共有するモデルの方が、共有しないモデルよりも速く損失が減少した。

Kerasメモ(Embeddingレイヤ)

https://www.oreilly.co.jp/books/images/picture978-4-87311-857-4.gif
「RとKerasによるディープラーニング」より)

Kerasネットワークは、入力として数値テンソルだけを受け付ける。テキストはそのままの形では渡せないため、変換が必要になる。
テキストを数値テンソルに変換することをベクトル化(vectorizing)と呼ぶ。

全てのベクトル化プロセスは、テキストに何らかのトークン化(tokenization)の方法を適用し、生成されたトークン(token)を数値ベクトルに対応付ける。
(文字、単語、N-gramといった、テキストを分割して得られるさまざまな単位のことを集合的にトークンと呼ぶ)

トークンに数値ベクトルを対応付ける方法はいくつかあり、よく使われるのは以下の2つ。

  • one-hot encodeding(2値の疎ベクトル)
  • token embedding(一般的に単語に対して使われるので、word embeddingとも呼ぶ)

one-hot encodingを使った例

Kerasメモ(seq2seqで文字レベル機械翻訳) - ichou1のブログ

一般的にベクトル(緑の部分)は20,000次元以上になる、と書かれている。
(20,000次元の場合、20,000語を管理する)
f:id:ichou1:20190227205843p:plain
ベクトルの次元数に、文の長さ(青の部分)、サンプル数(赤の部分)を掛けた分だけメモリを消費する。

token embeddingを使った例

Kerasメモ(seq2seqで足し算) - ichou1のブログ

f:id:ichou1:20190227214449p:plain

一般的にベクトル(オレンジの部分)は256次元か512次元、大規模な語彙を扱うときは1024次元ほど、と書かれている。
ベクトルの次元数に、文の長さ(青の部分)、サンプル数(赤の部分)を掛けた分だけメモリを消費する。


Embeddingレイヤは、整数のインデックス(特定の単語に対応)をベクトルにマッピングする辞書にあたる。
入力として整数を受け付け、内蔵辞書で整数をルックアップし、対応するベクトルを返す。


具体的に見てみる。
取り込むトークン数が「50,000」、ベクトルの次元が「128」のEmbeddingレイヤを定義する。

シーケンス長が固定のパターン

1sequence=1wordの場合、input_lengthの長さは"1"になる。

model = Sequential()
model.add(Embedding(input_dim=50000, output_dim=128, input_length=1))

バージョン「1.12」での引数説明より抜粋。
Flattenレイヤに出力を渡す場合は、"input_length"の指定が必要。
tf.keras.layers.Embedding  |  TensorFlow Core r2.1

input_dim: Size of the vocabulary

output_dim: Dimension of the dense embedding

input_length: Length of input sequences, when it is constant. This argument is required if you are going to connect Flatten then Dense layers upstream.

レイヤ内部の重み(トークンベクトルの内部辞書)。トレーニングにより調整される。

# model.layers[0].weights
# [<tf.Variable 'embedding_1/embeddings:0' shape=(50000, 128) dtype=float32_ref>]

f:id:ichou1:20190211173236p:plain

シーケンス長が可変のパターン

ただし、バッチごとに長さを揃える必要がある。

model = Sequential()
model.add(Embedding(input_dim=50000, output_dim=128))

f:id:ichou1:20190211175103p:plain

Kerasメモ(seq2seqで文字レベル機械翻訳)その2

前回のモデル。

f:id:ichou1:20190210092043p:plain
f:id:ichou1:20190210091920p:plain

one-hot表現の入力テキストをEncoderに渡し、モデルの隠れ状態(h)および記憶セル(c)をDecoderのインプットとした。
ここで、入力テキストは順方向でEncoderに渡している。

今回、入力テキストを逆方向にしたものをEncoderに渡し、その出力をDecoderのインプットとして追加してみる。

Bidirectionalレイヤを使おうとしたが、

encoder = Bidirectional(LSTM(units=256, return_state=True), merge_mode='concat')

以下のソース箇所でエラー。

tensorflow/python/keras/_impl/keras/layers/wrappers.py
class Bidirectional(Wrapper):
    ....
    def call(self, inputs, training=None, mask=None):
        ...
        y = self.forward_layer.call(inputs, **kwargs)    # y is list, not tensor
        ...
        if self.merge_mode == 'concat':
            output = K.concatenate([y, y_rev])  #  y.get_shape()  --> Error
        ...

Encoder用のLSTMレイヤを2つ用意し、出力をconcatenateレイヤで結合する。

encoder_fwd = LSTM(units=256, return_state=True)
encoder_bwd = LSTM(units=256, return_state=True, go_backwards=True)

f:id:ichou1:20190210092729p:plain

「epochs」を「50」に設定し、トレーニングを終えたモデルを使ったdecode結果。

Input sentence: Go.
Decoded sentence: 行け。
-
Input sentence: Hi.
Decoded sentence: やっほー。
-
...
-
Input sentence: She is mad at me.
Decoded sentence: 彼女は僕をついている。
-
Input sentence: She is obstinate.
Decoded sentence: 彼女はタイピストです。
-
Input sentence: She is on a diet.
Decoded sentence: 彼女はダイエット中だ。
-
...

"She is on a diet."という文は、"彼女はダイエットをしている。"という教師データを与えたが、今回、"彼女はダイエット中だ。"としてdecodeされた。
意味レベル、品詞レベルでも妥当。

一方、今回は文字レベルの機械翻訳なので、品詞レベルでおかしな結果もあった。

Input sentence: She kept working.
Decoded sentence: 彼女は歌を歌がめた。

レーニング時のlossとaccuracyの遷移は以下のとおり。
f:id:ichou1:20190210105216p:plain
f:id:ichou1:20190210105231p:plain

Kerasメモ(seq2seqで文字レベル機械翻訳)

「Sequence to sequence example in Keras (character-level)」を試してみる。

環境

tensorflowに統合されたKerasを使用、tensorflowのバージョンは「1.5.1」

今回、試したソースコード
https://github.com/keras-team/keras/blob/master/examples/lstm_seq2seq.py

We apply it to translating short English sentences into short French sentences, character-by-character.
Note that it is fairly unusual to do character-level machine translation, as word-level models are more common in this domain.

文字レベル(character-level)での機械翻訳を行う(一般的な機械翻訳は単語レベル)

解説ページ
A ten-minute introduction to sequence-to-sequence learning in Keras

デフォルトから以下のとおり変更する。

  • データは英文と和文のペア(short English sentences into short Japanese sentences)
  • 「num_samples」は「10000」から「5000」に変更

データのフォーマットは以下のとおり(タブ区切り)

Go.	行け。
Go.	行きなさい。
Hi.	やっほー。
Hi.	こんにちは!
...
She is mad at me.	彼女は私に怒っています。
She is obstinate.	彼女は強情です。
She is on a diet.	彼女はダイエットをしている。
...

タブの前をencoder用、タブの後ろをdecoder用に読み込む。

# for encoder
(Pdb) input_texts[0]
u'Go.'
(Pdb) len(input_texts[0])
3

# for decoder
(Pdb) target_texts[0]
u'\t\u884c\u3051\u3002\n'
(Pdb) print(target_texts[0])
	行け。

(Pdb) len(target_texts[0])
5
Encoder用のデータ形式(encoder_input_data)

英文テキスト文字長は最大「18」、文字の種類は「70」。
f:id:ichou1:20190210115035p:plain

(Pdb) encoder_input_data.shape
# (len(input_texts), max_encoder_seq_length, len(input_characters))
(5000, 18, 70)
(Pdb) input_characters
[u' ', u'!', u'"', u'$', u"'", u',', u'-', u'.', u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9', u':', u'?', u'A', u'B', u'C', u'D', u'E', u'F', u'G', u'H', u'I', u'J', u'K', u'L', u'M', u'N', u'O', u'P', u'Q', u'R', u'S', u'T', u'U', u'V', u'W', u'Y', u'a', u'b', u'c', u'd', u'e', u'f', u'g', u'h', u'i', u'j', u'k', u'l', u'm', u'n', u'o', u'p', u'q', u'r', u's', u't', u'u', u'v', u'w', u'x', u'y', u'z']
Decoder用のデータ形式(decoder_input_data / decoder_target_data)

日本語文テキスト文字長は最大「28」(先頭のタブと末尾の改行も込みでカウント)、文字の種類は「1123」(漢字を使うので文字種が多くなる)
f:id:ichou1:20190210115847p:plain

(Pdb) decoder_input_data.shape
(5000, 28, 1123)
(Pdb) target_characters[:50]
[u'\t', u'\n', u'!', u'.', u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'8', u'9', u'?', u'B', u'C', u'D', u'F', u'I', u'V', u'e', u'f', u'i', u'o', u'r', u'x', u'\u2014', u'\u3000', u'\u3001', u'\u3002', u'\u3005', u'\u300c', u'\u300d', u'\u3041', u'\u3042', u'\u3044', u'\u3046', u'\u3048', u'\u3049', u'\u304a', u'\u304b', u'\u304c', u'\u304d', u'\u304e', u'\u304f', u'\u3050', u'\u3051', u'\u3052', u'\u3053', u'\u3054']

正解ラベル(decoder_target_data)については1つ後ろの文字を保持する。

(Pdb) decoder_target_data.shape
(5000, 28, 1123)
if t > 0:
    # decoder_target_data will be ahead by one timestep
    # and will not include the start character.
    decoder_target_data[i, t - 1, target_token_index[char]] = 1.

modelの最終的な出力。
f:id:ichou1:20190210115952p:plain

modelのサマリ

f:id:ichou1:20190205162852p:plain

modelのweight (lstm_1)

LSTMレイヤのunit数は256、4つの重みを一つにまとめている(1024=256x4)

[<tf.Variable 'lstm_1/lstm_cell/kernel:0' shape=(70, 1024)>,
 <tf.Variable 'lstm_1/lstm_cell/recurrent_kernel:0' shape=(256, 1024)>,
 <tf.Variable 'lstm_1/lstm_cell/bias:0' shape=(1024,)>]
modelのweight (lstm_2)
[<tf.Variable 'lstm_2/lstm_cell/kernel:0' shape=(1123, 1024)>,
 <tf.Variable 'lstm_2/lstm_cell/recurrent_kernel:0' shape=(256, 1024)>,
 <tf.Variable 'lstm_2/lstm_cell/bias:0' shape=(1024,)>]
modelのトレーニングオプション
# Run training
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
model.fit([encoder_input_data, decoder_input_data],
          decoder_target_data,
          batch_size=batch_size,
          epochs=epochs,
          validation_split=0.2)


「epochs」を「50」に設定し、トレーニングを終えたモデルを使ったdecode結果。

Input sentence: Go.
Decoded sentence: 行き。
-
Input sentence: Hi.
Decoded sentence: やっちー。
-
...
-
Input sentence: She is mad at me.
Decoded sentence: 彼女はダイエットをしている。
-
Input sentence: She is obstinate.
Decoded sentence: 彼女はダイエットをしている。
-
Input sentence: She is on a diet.
Decoded sentence: 彼女はダイエットをしている。
-
...

今回、使ったデータでは、"Go."という英文に対応する日本語の文は"行け。"と"行きなさい。"の2つ。
同様に、"Hi."という英文に対応する日本語の文は"やっほー。"と"こんにちは!"の2つ。
decode結果は、2種類の日本語の文を混合したような内容になっている。

また、"She is ..."はis以降が異なるのに、decode結果が同じになるケースがあった。
レーニングセットをどのように分割したかは未確認だが、Bidirectional LSTMに換えたらどうか試してみたい(試した結果


最後に、encoder_modelとdecoder_modelについてメモしておく。

encoder_model
encoder_outputs, state_h_enc, state_c_enc = model.layers[2].output   # lstm_1

encoder_model = Model(
    model.input[0],   # input_1
    [state_h_enc, state_c_enc])
_________________________________________________________________________________
Layer (type)       Output Shape                                                  
=================================================================================
input_1 (Input)    (None, None, 70)                                              
_________________________________________________________________________________
lstm_1 (LSTM)      [(None, 256), (None, 256), (None, 256)]   
=================================================================================
decoder_model
decoder_state_input_h = Input(shape=(256,), name='input_3')
decoder_state_input_c = Input(shape=(256,), name='input_4')
decoder_lstm = model.layers[3]   # lstm_2
decoder_outputs, state_h_dec, state_c_dec = decoder_lstm(
    model.input[1], initial_state=[decoder_state_input_h, decoder_state_input_c])
decoder_dense = model.layers[4]
decoder_outputs = decoder_dense(decoder_outputs)

decoder_model = Model(
    [model.input[1]] + [decoder_state_input_h, decoder_state_input_c],
    [decoder_outputs] + [state_h_dec, state_c_dec])
_________________________________________________________________________________
Layer (type)       Output Shape                                     Connected to 
=================================================================================
input_2 (Input)    (None, None, 1123)                                             
_________________________________________________________________________________
input_3 (Input)    (None, 256)                                                   
_________________________________________________________________________________
input_4 (Input)    (None, 256)                                      
_________________________________________________________________________________
lstm_2 (LSTM)      [(None, None, 256), (None, 256), (None, 256)]    input_2[0][0]
                                                                    input_3[0][0]
                                                                    input_4[0][0]
_________________________________________________________________________________
dense_1 (Dense)    (None, None, 1123)                                lstm_2[0][0]
=================================================================================

インプットとなる英文(one-hot表現へ変換済み)をencoder_modelに渡し、隠れ状態(state_h_enc)と記憶セル(state_c_enc)を得る。

# Encode the input as state vectors.
# input_seq.shape  --> (1, 18, 70)
states_value = encoder_model.predict(input_seq)

# states_value[0].shape  --> (1, 256)
# states_value[1].shape  --> (1, 256)

encoder_modelのアウトプットである隠れ状態および記憶セルを初期値としてdecoder_modelに渡し、predictionを実行する。

target_seq = np.zeros((1, 1, 1123))
# Populate the first character of target sequence with the start character.
target_seq[0, 0, target_token_index['\t']] = 1.

while not stop_condition:
    output_tokens, h, c = decoder_model.predict([target_seq] + states_value)

    # Sample a token
    sampled_token_index = np.argmax(output_tokens[0, -1, :])
    sampled_char = reverse_target_char_index[sampled_token_index]
    decoded_sentence += sampled_char

    # find stop character('\n')  --> stop_condition = True

    # Update the target sequence (of length 1).
    target_seq = np.zeros((1, 1, 1123))
    target_seq[0, 0, sampled_token_index] = 1.

    # Update states
    states_value = [h, c]

EncoceとDecodeに関するメモ

マルチバイト文字の扱いに関するメモ。

UTF-8として保存されたファイルがあったとする。

% file /tmp/hoge.txt
/tmp/hoge.txt: UTF-8 Unicode text

これをpythonのコードで読み込む。

with codecs.open('/tmp/hoge.txt', 'r', encoding='utf-8') as f:
    lines = f.read().split('\n')
for line in lines:
    ...

ここで、"line"には"行"という漢字が入っていたとして、pdbで表示させるとUnicode形式で表示される。
pdbのバージョンは「2.7」)

(Pdb) p line
u'\u884c'

(Pdb) isinstance(line, unicode)
True

日本語で表示させるには、pdbのprintコマンドではなく、pythonのprint関数を使えばよい。

標準出力のエンコード形式は"UTF-8"であるとする。

(Pdb) print sys.stdout.encoding
UTF-8
Unicode
(Pdb) isinstance(line, unicode)
True

(Pdb) print(line)
行
文字列型
(Pdb) isinstance(line.encode('utf-8'), str)
True

(Pdb) print(line.encode('utf-8'))
行

(Pdb) p line.encode('utf-8')
'\xe8\xa1\x8c'


encodeとdecodeの向きについては、一瞬迷う時があるのでメモしておく。

f:id:ichou1:20190202104539p:plain

tensorflowメモ(word2vec)

チュートリアルの「word2vec_basic.py」を試してみる。

www.tensorflow.org


出現頻度が低い単語は"unknown"(未知語)として扱い、全体として5万語の単語を特徴ベクトルで表現する。

vocabulary_size = 50000  # replace rare words with UNK token.

各単語の特徴は"embedding_size"次元のベクトルで表現する。

embedding_size = 128  # Dimension of the embedding vector.

# "embeddings" shape is [50000, 128]
embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
(参考) embeddingsの中身

f:id:ichou1:20190114093759p:plain

レーニングや検証では、ここから対象となる単語に該当する行を抽出する。
レーニング時のバッチサイズは「128」から「100」に変更して試してみる。
("embedding_size"と同じで、区別しにくいため)

batch_size = 100  # original 128
train_inputs = tf.placeholder(tf.int32, shape=[batch_size])

# "embed" shape is [100, 128]
embed = tf.nn.embedding_lookup(embeddings, train_inputs)

学習ラベルは、その単語の前あるいは後に出現する単語になる。

# "train_labels" shape is [100, 1]
train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])

損失計算は「tf.nn.nce_loss」、オプティマイザは「tf.train.GradientDescentOptimizer」を使う。

NCE(Noise Contrastive Estimation)については以下で説明されている。
https://www.tensorflow.org/extras/candidate_sampling.pdf

“Exhaustive” training methods such as softmax and logistic regression require us to compute probability for every class L for every training example.
Typically, the set of candidates C is the union of the target classes with a randomly chosen sample of (other) classes S.

出力クラスにあたる単語数は「50,000」(これを全出力クラス集合"L"とおく)
全出力クラスに対して各出力クラスごとの確率を計算するのは効率的ではないため、正解となる出力クラス"T"とランダムに選んだ出力クラス集合"S"の和集合"C"を使って計算する。


S \subset L \\
C = T \cup S

"num_sampled"が"S"に該当する。

num_sampled = 64  # Number of negative examples to sample.

"ネガティブサンプリング"は、「偽の入力」を選び、それが正解と判断される確率が下がるように学習する。
NCEは、"ネガティブサンプリング"に、確率(サンプル内で対象となる単語が出現する事前確率?)の引き算処理を追加したものである模様。


損失を求める以下のコードの内部処理を追ってみる。

loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_weights,
                                     biases=nce_biases,
                                     labels=train_labels,
                                     inputs=embed,
                                     num_sampled=num_sampled,
                                     num_classes=vocabulary_size))

引数で渡している"nce_weight"と"nce_biases"については以下のとおり。

# Construct the variables for the NCE loss
w_stddev = 1.0 / math.sqrt(embedding_size)  # 0.088 

# "nce_weight" shape is [50000, 128]
nce_weights = tf.Variable(tf.truncated_normal([vocabulary_size, embedding_size],
                                              stddev=w_stddev))

# "nce_biases" shape is [50000,]
nce_biases = tf.Variable(tf.zeros([vocabulary_size]))

まずはロジット計算。

# "logits" shape is [batch_size, num_sampled + num_true]  --> [100, 64+1]
# "labels" shape is [batch_size, num_sampled + num_true]  --> [100, 64+1]
logits, labels = nn_impl._compute_sampled_logits(weights=nce_weights,
                                                 biases=nce_biases,
                                                 labels=train_labels,
                                                 inputs=embed,
                                                 num_sampled=num_sampled,
                                                 num_classes=vocabulary_size,
                                                 num_true=1,
                                                 sampled_values=None,
                                                 subtract_log_q=True,
                                                 remove_accidental_hits=False,
                                                 partition_strategy="mod",
                                                 name="nce_loss")

「偽の入力」を準備する。

sampled_values = candidate_sampling_ops.log_uniform_candidate_sampler(
                     true_classes=labels,         # train_labels [100, 1]
                     num_true=num_true,           # 1
                     num_sampled=num_sampled,     # 64
                     unique=True,                 
                     range_max=num_classes)       # vocabulary_size

# "sampled" shape is : [num_sampled, ]  --> [64,]
# "true_expected_count" shape is :  [batch_size, 1]  --> [100, 1]
# "sampled_expected_count" shape is : [num_sampled,]  --> [64,]
sampled, true_expected_count, sampled_expected_count = (
    array_ops.stop_gradient(s) for s in sampled_values)
(参考) "true_expected_count"の中身
# shape is :  [batch_size, 1]  --> [100, 1]
array([[0.06667874],
       [0.21983916],
       [0.16749582],
       [0.84463996],
       [...],
       [0.9905478 ],
       [0.6892732 ]], dtype=float32)
"(参考) "sampled_expected_count"の中身
# shape is : [num_sampled,]  --> [64,]
array([0.02967292, 0.10759848, 0.02796301, 0.18194638, 0.17686114,
       0.06529876, 0.84463996, 0.9905478 , 0.0336272 , 0.92956346,
       ...
       0.11761837, 0.5279828 , 0.02912493, 0.03380741, 0.05318905,
       0.5735737 , 0.2901329 , 0.37532815, 0.04080481], dtype=float32)

インプットと重みの計算。

1つ目の計算。インプットと重み(「偽の入力」用)の内積

# X : "inputs" shape is [batch_size, dim]  --> [100, 128]
# W : "sampled_w" shape is [num_sampled, dim]  --> [64, 128]
# Apply X*W^T', which yields [batch_size, num_sampled]  --> [100, 64]
sampled_logits = math_ops.matmul(inputs, sampled_w, transpose_b=True)

# sampled_b is a [num_sampled] float tensor
sampled_logits += sampled_b

2つ目の計算。インプットと重み(トレーニングインプット用)の、要素ごとの積アダマール積)

# X : "inputs" shape is [batch_size, dim]  --> [100, 128]
# W : "true_w" shape is [batch_size, dim]  --> [100, 128]
# Apply X*W', which yields [batch_size, dim]  --> [100, 128]

# "dots_as_matrix" shape is [batch_size, dim] --> [100, 128]
dots_as_matrix = math_ops.multiply(inputs, true_w)

# "true_logits" shape is [batch_size, 1] --> [100, 1]
true_logits = _sum_rows(dots_as_matrix)

# true_b is a [batch_size * num_true] tensor
true_logits += true_b

事前確率を減算する。
これがネガティブサンプリングとNCEの違いを分ける処理にあたる。

# Subtract log of Q(l), prior probability that l appears in sampled.
true_logits -= math_ops.log(true_expected_count)
sampled_logits -= math_ops.log(sampled_expected_count)

2つの計算結果を結合する。

# "out_logits" shape is [batch_size, num_true + num_sampled]  --> [100, 1+64]
out_logits = array_ops.concat([true_logits, sampled_logits], 1)

# "out_labels" shape is [batch_size, num_true + num_sampled]  --> [100, 1+64]
out_labels = array_ops.concat([array_ops.ones_like(true_logits),
                               array_ops.zeros_like(sampled_logits)],
                              1)
"(参考) "out_logits"の中身
# shape is [batch_size, num_true + num_sampled]  --> [100, 1+64]
array([[ 0.75966  ,  1.3191822, ...,  3.09035  ,  2.6109805 ],
       [ 2.434282 ,  1.3191822, ...,  3.09035  ,  2.6109805 ],
       [ 2.1441886,  0.7031988, ...,  3.1732008,  2.9454582 ],
       ...,                           
       [ 1.2829933,  1.6065203, ...,  3.2463934,  2.426865  ],
       [-0.1432182,  0.5460663, ...,  2.9791045,  3.5856936 ],
       [-0.2673956,  0.5460663, ...,  2.9791045,  3.5856936 ]]
      , dtype=float32)
"(参考) "out_labels"の中身
# shape is [batch_size, num_true + num_sampled]  --> [100, 1+64]
array([[1., 0., 0., ..., 0., 0., 0.],
       [1., 0., 0., ..., 0., 0., 0.],
       [1., 0., 0., ..., 0., 0., 0.],
       ...,
       [1., 0., 0., ..., 0., 0., 0.],
       [1., 0., 0., ..., 0., 0., 0.],
       [1., 0., 0., ..., 0., 0., 0.]], dtype=float32)

計算結果と正解ラベルとのクロスエントロピーを計算。

# "sampled_losses" shape is [batch_size, num_true + num_sampled]  --> [100, 1+64]
sampled_losses = tf.nn.sigmoid_cross_entropy_with_logits(labels=labels,
                                                         logits=logits,
                                                         name="sampled_losses")

計算式は以下のとおり。

# x = out_logits[row_index][col_index]
# z = out_labels[row_index][col_index]
x - (x * z) + numpy.log(1 + numpy.exp(-x))
"(参考) "sampled_losses"の中身

# shape is [batch_size, num_true + num_sampled] --> [100, 1+64]

array([[0.38378218,  1.5561134, ...,  3.134832 ,  2.68187   ],
       [0.08402921,  1.5561134, ...,  3.134832 ,  2.68187   ],
       [0.1107925 ,  1.1053246, ...,  3.2142174,  2.9967005 ],
       ...,                           
       [0.24467511,  1.7893287, ...,  3.2845697,  2.5114942 ],
       [0.767318  ,  1.0029998, ...,  3.0286927,  3.6130338 ],
       [0.83575606,  1.0029998, ...,  3.0286927,  3.6130338 ]]
      , dtype=float32)

行の値を合計する。

# "loss_nce" shape is [batch_size, 1]  --> [100, 1]
loss_nce = nn_impl._sum_rows(sampled_losses)

この平均を求める。

# "loss" shape : scalar
loss = tf.reduce_mean(loss_nce)

この値を最小化する問題として、オプティマイザに渡す(ここでは「勾配降下法」を使う)

# Construct the SGD optimizer using a learning rate of 1.0.
optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)


学習済みの"embeddings"を使って単語のコサイン類似度が計算できる。

# Compute the cosine similarity between target embedding and all embeddings.
norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
normalized_embeddings = embeddings / norm
valid_embeddings = tf.nn.embedding_lookup(normalized_embeddings, valid_dataset)

# X : "valid_embeddings" shape is [valid_size, embedding_size]
# W : "normalized_embeddings" shape is [vocabulary_size, embedding_size]
# Apply X*W^T', which yields [valid_size, vocabulary_size]  --> [valid_size, 50000]
similarity = tf.matmul(valid_embeddings, normalized_embeddings, transpose_b=True)

例えば、"history"という単語にindex番号76が振られていたとする。

>>> dictionary['history']
76
>>> reverse_dictionary[76]
'history'

"history"と全単語の内積を求める。

昇順ソート後、末尾から5つ分のindex
>>> numpy.dot(final_embeddings[76], final_embeddings.T).argsort()[295:]
array([122,   7,  33,   1,  76])

indexに対応する単語と、その類似度は以下のとおりであった。

reverse_dictionary[122]	 --> 'but'        # similarity:  0.25302753
reverse_dictionary[7]	 --> 'that'       # similarity:  0.25326163
reverse_dictionary[33]	 --> 'first'      # similarity:  0.29976884
reverse_dictionary[1]	 --> 'the'	  # similarity:  0.3162361 
reverse_dictionary[76]	 --> 'history'    # similarity:  1.0000001 

TensorFlowメモ(RNNその2)

LSTM(Long short-term memory)を試してみる。

SimpleRNNについては下記ご参照。
tensorflowメモ(RNNその1) - ichou1のブログ

kerasで実装する場合は、単純にRecurrentレイヤーを置き換えるだけ済む。

SimpleRNN
model.add(SimpleRNN(n_hidden,
                    input_shape=(maxlen, 1),
                    kernel_initializer='random_normal'))
LSTM
model.add(LSTM(n_hidden,
               input_shape=(maxlen, 1),
               kernel_initializer='random_normal'))

隠れ層のユニット数は「20」とする。

n_hidden = 20

(参考)model.summary()で表示
f:id:ichou1:20190105183810p:plain

学習で更新されるパラメータ数の内訳(右側はソースコード内の変数)

# lstm_1 Layer
# input gate
weight(input) :  1row * 20col = 20    --> self.kernel_i
weight(state) : 20row * 20col = 400   --> self.recurrent_kernel_i
bias          : 20                    --> self.bias_i

# forget gate
weight(input) :  1row * 20col = 20    --> self.kernel_f
weight(state) : 20row * 20col = 400   --> self.recurrent_kernel_f
bias          : 20                    --> self.bias_f

# input modulation gate(?)
weight(input) :  1row * 20col = 20    --> self.kernel_z
weight(state) : 20row * 20col = 400   --> self.recurrent_kernel_z
bias          : 20                    --> self.bias_z

# output gate
weight(input) :  1row * 20col = 20    --> self.kernel_o
weight(state) : 20row * 20col = 400   --> self.recurrent_kernel_o
bias          : 20                    --> self.bias_o

# dense_1 Layer
weight        : 20row * 1col
bias          : 1
レイヤ構成

f:id:ichou1:20190105191025p:plain
論文「LSTM: A Search Space Odyssey」より引用加工

左側が「SimpleRNN」の説明。
囲み部分が、"input"と"previous timestepの出力"に重みを掛けてバイアスを加算する処理にあたる。
f:id:ichou1:20190105195109p:plain

右側がLSTMの説明、ソースと一緒に見た方が分かりやすいと思われる(tensorflowバージョン1.5を想定)
tensorflow/recurrent.py at r1.5 · tensorflow/tensorflow · GitHub
1816行目以降が該当。

class LSTMCell(Layer):
...
  def call(self, inputs, states, training=None):

f:id:ichou1:20190105201129p:plain

上図に該当するソースコード(説明用に加工)

ソースコードの"h"が図中の"y"に該当、図の表記に合わせている

# tm1 means "t minus one" as in "previous timestep"

# "inputs" shape                  : (None, 1) 
# "self.kernel_*" shape           : (1, units)
# "y_tm1" shape                   : (None, units)
# "self.recurrent_kernel_*" shape : (units, units)

 
# (1)  "x_i","i" shape : (None, units) 
x_i = K.dot(inputs, self.kernel_i)
x_i = K.bias_add(x_i, self.bias_i)
i = self.recurrent_activation(x_i + K.dot(y_tm1, self.recurrent_kernel_i))

# (2)  "x_f","f" shape : (None, units)
x_f = K.dot(inputs, self.kernel_f)
x_f = K.bias_add(x_f, self.bias_f)
f = self.recurrent_activation(x_f + K.dot(y_tm1, self.recurrent_kernel_f))

# (3)  "x_z","z" shape : (None, units)
x_z = K.dot(inputs, self.kernel_z)
x_z = K.bias_add(x_z, self.bias_z)
z = self.activation(x_z + K.dot(y_tm1, self.recurrent_kernel_z))

# (4)  "x_o","o" shape : (None, units)
x_o = K.dot(inputs, self.kernel_o)
x_o = K.bias_add(x_o, self.bias_o)
o = self.recurrent_activation(x_o + K.dot(y_tm1, self.recurrent_kernel_o))


f:id:ichou1:20190105203002p:plain

上図に該当するソースコード(説明用に加工)
#  "c" shape : (None, units)
c = (f * c_tm1) + (i * z)


f:id:ichou1:20190105203450p:plain

上図に該当するソースコード(説明用に加工)
#  "y" shape : (None, units)
y = o * self.activation(c)