はじめに
『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
本の内容を1つずつ確認しながらゆっくりと組んでいきます。
この記事は、3.5節「word2vecに関する補足」の内容です。確率の基礎・負の対数尤度とCBOWモデル・skip-gramとの対応関係を説明して、skip-gramをPythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
3.5.1 CBOWモデルと確率
・同時確率と条件付き確率
簡単に同時確率と条件付き確率について確認しておきましょう。
6面のサイコロを振ったときの出目について考えます。$P(\cdot)$で、括弧の中となる確率を表すことにします。例えば出目が「1」の確率は、$P(1) = \frac{1}{6}$と表記します。
サイコロを2回振ったとき、次の2つの事象から同時確率を考えます。
- $A$:1回目が1である
- $B$:2回目が3である
「$A$」の確率は$P(A) = \frac{1}{6}$、「$B$」の確率も$P(B) = \frac{1}{6}$ですね。
では「1回目が1で2回目が3」となる確率はどうでしょうか。つまり「$A$かつ$B$」の確率です。このような確率を同時確率と呼び、$P(A, B)$と表記することにします。サイコロを2回振ったときの出目の組み合わせは$6^2$通りあることから、$P(A, B) = \frac{1}{36}$ですね。
$A$と$B$の同時確率$P(A, B)$は、次のように$A$の確率と$B$の確率の積でも計算できます。
ただしこの関係が成り立つのは、それぞれの事象が独立に生じる場合に限ります。サイコロの出目は、振ったタイミングなどに影響されずに決まるため、このように求められます。
続いて、条件付き確率について考えます。条件付き確率とは、例えばサイコロを1回振り出目を自分で見る前に「偶数」であるという情報が与えられたとき、「3以下」の目である確率です。「偶数」の目は3通りで、その内「3以下」の目は1つなので、「偶数」という条件の下で「3以下」となる確率は$\frac{1}{3}$になります。
この2つの事象を次のように表し整理しましょう。
- $C$:3以下である
- $D$:偶数である
「$C$」の確率は$P(C) = \frac{3}{6} = \frac{1}{2}$、「$D$」の確率も$P(D) = \frac{3}{6} = \frac{1}{2}$ですね。「$C$かつ$D$」つまり「3以下の偶数」の確率は、$P(C, D) = \frac{1}{6}$です。$C$と$D$は独立ではないため、$P(C, D) \neq P(C) P(D)$です。
では例として上げた「$D$」の情報が与えられた下での「$C$」の確率を、$P(C | D) = \frac{1}{3}$と表すことにします。$D$という条件のあるなしによって、$C$の確率が$\frac{1}{2}$から$\frac{1}{3}$に変わることが分かりました。
この条件付き確率$P(C | D)$は、次の関係が成り立ちます。
つまり$C$と$D$の同時確率$P(C, D)$と、条件$D$の確率$P(D)$から求められます。
また1行目の式から、次の関係式が成り立つことも分かります。
$D$を条件とした$C$の確率と、条件$D$の確率の積は、$C$と$D$の同時確率になります。
ところで、同時確率の式について次のように変形すると
$B$という条件に関わらず$A$の確率が決まることが分かります。言い換えると、$A$は$B$の影響を受けないということです。この関係が成り立つとき、$A$と$B$は独立であると言えます。
その他条件付き独立などについては、確率の入門書などを参照してください。
それでは、これらの確率の定義をCBOWモデルに当てはめて考えていきます。
・負の対数尤度
これまで扱ってきた「You say goodbye and I say hello.」を使って考えます。このテキストを(ピリオドも1語として)単語に分解したものをコーパスとしました。この例では、語彙数(単語の種類数)が7、単語数が8でコーパスが構成されています。単語を$w$、出現した順番を下付き添字で示すこととし、それぞれ$w_1, w_2, \cdots, w_8$で表すします。つまりテキストの最初の単語「you」は$w_1$です。
注目する単語をターゲット、その前後の単語をコンテキストと呼ぶのでした。3番目の単語$w_3$をターゲットとするのであれば、コンテキストは$w_2$と$w_4$ですね。また「コンテキストの単語」の情報を得た上で、確率が最大となる「ターゲットの単語」を求めることを推論と呼ぶのでした。つまり「コンテキスト$w_2,\ w_4$」の情報が与えられた下での「未知のターゲット$w_2$」の確率です。これは条件付き確率
で表せます。
3.2.1項で確認したように順伝播では、コンテキスト$\mathbf{c}$を入力として2層の全結合層によってスコア$\mathbf{s} = (s_1, s_2, \cdots, s_7)$を計算します。この添字は単語IDです。このスコアをSoftmax関数によって正規化(確率に変換)したものを$\mathbf{y} = (y_1, y_2, \cdots, y_7)$とします。この出力$\mathbf{y}$とone-hot表現のターゲット(教師ラベル)$\mathbf{t} = (t_1, t_2, \cdots, t_7)$から、次の式により交差エントロピー誤差を計算します。交差エントロピー誤差についての詳細は「4.2.1:2乗和誤差の実装【ゼロつく1のノート(実装)】 - からっぽのしょこ」を参照してください。
この値を損失とするのでした。
3番目の単語$w_3 = \mathrm{you}$の単語IDは3なので、ターゲット$\mathbf{t}$は3番目の要素が1で他の要素は0です。すると式(1.7)は
正解ラベル(ターゲット)に関する確率のみに注目することになります。また$y_3$は「you」のコンテキスト$\mathbf{c}$から求めた確率なので、式(3.1)のことです。従って置き換えると、損失は
で表せます。またこの式を負の対数尤度と呼びます。
ここまでは1つの単語$w_3$について考えました。複数の単語$w_1, \cdots, w_8$を同時に計算する場合は、バッチデータ版の交差エントロピー誤差
を用います。
$(t_{11}, \cdots, t_{87})$はtarget
に対応します。つまり各行が$w_1, \cdots, w_8$に、各列が単語ID順に並んだ単語に対応します。$(y_1, \cdots, y_{87})$も同様です。
従って先ほどと同様に、正解ラベルつまり実際の単語$w_1, \cdots, w_8$に関する確率の項のみが残るので
となります。
以上の考え方は次のskip-gramでも同様です。
3.5.2 skip-gram
skip-gramをクラスとして実装し、学習を行います。
# 読み込み用の設定 import sys sys.path.append('C://Users//「ユーザー名」//Documents//・・・//deep-learning-from-scratch-2-master') # 実装済みのクラスをインポート #from common.trainer import Trainer from common.optimizer import Adam # その他必要なライブラリ import numpy as np import matplotlib.pyplot as plt
基本的な実装はCBOWモデルと同じです。
skip-gramでは入力層が1つのため、重み付き和の平均をとる必要がなくなります。逆に出力が2つになるため、交差エントロピー誤差の和を損失とします。それに伴い、出力層とSoftmax with Lossレイヤの間に分岐ノード(1.3.4.2項)の処理(和をとる)を行います。
# skip-gramモデルの実装 class SimpleSkipGram: # 初期化メソッドの定義 def __init__(self, vocab_size, hidden_size): # ニューロン数を保存 V = vocab_size # 入力層と出力層 H = hidden_size # 中間層 # 重みの初期値を生成 W_in = 0.01 * np.random.randn(V, H).astype('f') W_out = 0.01 * np.random.randn(H, V).astype('f') # レイヤを生成 self.in_layer = MatMul(W_in) # 入力層 self.out_layer = MatMul(W_out) # 出力層 self.loss_layer1 = SoftmaxWithLoss() # 損失層1 self.loss_layer2 = SoftmaxWithLoss() # 損失層2 # レイヤをリストに格納 layers = [self.in_layer, self.out_layer] # 各レイヤのパラメータと勾配をリストに格納 self.params = [] # パラメータ self.grads = [] # 勾配 for layer in layers: self.params += layer.params self.grads += layer.grads # 単語の分散表現を保存 self.word_vecs = W_in # 順伝播メソッドの定義 def forward(self, contexts, target): # 重み付き和を計算 h = self.in_layer.forward(target) # スコアを計算 s = self.out_layer.forward(h) # 損失を計算 l1 = self.loss_layer1.forward(s, contexts[:, 0]) l2 = self.loss_layer2.forward(s, contexts[:, 1]) loss = l1 + l2 return loss # 逆伝播メソッドの定義 def backward(self, dout=1): # Lossレイヤの勾配を計算 dl1 = self.loss_layer1.backward(dout) dl2 = self.loss_layer2.backward(dout) ds = dl1 + dl2 # 出力層の勾配を計算 dh = self.out_layer.backward(ds) # 入力層の勾配を計算 self.in_layer.backward(dh) return None
実装したクラスを試してみましょう。やることは3.4.1項と同じです。
# テキストを設定 text = 'You say goodbye and I say hello.' # 前処理 corpus, word_to_id, id_to_word = preprocess(text) print(word_to_id) print(id_to_word) print(corpus) # ウインドウサイズ window_size = 1 # 単語の種類数を取得 vocab_size = len(word_to_id) print(vocab_size) # コンテキストとターゲットを作成 contexts, target = create_contexts_target(corpus, window_size) print(contexts.shape) print(target.shape) # one-hot表現に変換 contexts = convert_one_hot(contexts, vocab_size) target = convert_one_hot(target, vocab_size) print(contexts.shape) print(target.shape)
{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
[0, 1, 2, 3, 4, 1, 5, 6]
7
(6, 2)
(6,)
(6, 2, 7)
(6, 7)
# バッチサイズを指定 batch_size = 2 # 2層のskypeのインスタンスを作成 model = SimpleSkipGram(vocab_size, hidden_size) # 最適化手法のインスタンスを作成 optimizer = Adam() # 学習処理のインスタンスを作成 trainer = Trainer(model, optimizer) # 学習 trainer.fit(contexts, target, max_epoch, batch_size, eval_interval=2)
| epoch 1 | iter 2 / 3 | time 0[s] | loss 3.89
| epoch 2 | iter 2 / 3 | time 0[s] | loss 3.89
| epoch 3 | iter 2 / 3 | time 0[s] | loss 3.89
(省略)
| epoch 999 | iter 2 / 3 | time 0[s] | loss 2.14
| epoch 1000 | iter 2 / 3 | time 0[s] | loss 1.66
損失の推移を見ましょう。
# 損失の推移をグラフ化
trainer.plot()
単語の類似度を確認しましょう。
# 単語ベクトルを表示 word_vecs = model.word_vecs for word_id, word in id_to_word.items(): print(word, word_vecs[word_id]) # クエリを指定 query = 'you' # 共起行列を用いた類似度の上位単語を表示 most_similar(query, word_to_id, id_to_word, model.word_vecs, top=6)
you [-0.00399093 -0.01740843 0.01117007 0.00167495 -0.00330359]
say [-1.4924841 -0.9265496 -0.06612136 -0.85639507 0.21594335]
goodbye [1.2914288 0.8205651 1.0868554 0.64892346 0.73790425]
and [-0.7078796 -1.2045625 -1.3328174 -1.2473851 -1.4656099]
i [1.2976283 0.7901488 1.0781299 0.6454606 0.77316916]
hello [-0.74642414 1.1192015 -0.25634232 1.364689 1.1631694 ]
. [-0.01523393 0.00943153 0.00037401 0.00478988 0.00889456]
[query] you
say: 0.4562843441963196
and: 0.1999540627002716
i: -0.18465915322303772
goodbye: -0.1908864974975586
.: -0.27402305603027344
hello: -0.4335971176624298
データが少ないのでこの結果からは詳しいことは言えませんが、「I」との類似度が低いことからCBOWモデルよりも上手く表現できていないように思います。skip-gramの方がCBOWモデルよりも難易度の高い問題を解くため、必要なデータも多くなるのでしょう。
以上でword2vecの基本的な仕組みを学びました。次章では、改良を加え本物のword2vecを実装します。
参考文献
おわりに
3章完了!特に問題なし。
【次節の内容】