ichou1のブログ

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

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