ゼロから作るDeep Learning② word2vecコードで謎だった場所(その2)


謎だった場所

class CBOWの最後のW_in自体がword2vecで作成される単語の分散表現であるために、学習が進むごとに変化する値のはずだが、最後にいきなりself.word_vecsに代入されている。。このW_inの値はいつ変更されているのだろうか?

class CBOW:
    def __init__(self, vocab_size, hidden_size, window_size, corpus):
        V, H = vocab_size, hidden_size

        # 重みの初期化
        W_in = 0.01 * np.random.randn(V, H).astype('f')
        W_out = 0.01 * np.random.randn(V, H).astype('f')

        # レイヤの生成
        self.in_layers = []
        for i in range(2 * window_size):
            layer = Embedding(W_in)  # Embeddingレイヤを使用
            self.in_layers.append(layer)
        self.ns_loss = NegativeSamplingLoss(W_out, corpus, power=0.75, sample_size=5)

        # すべての重みと勾配をリストにまとめる
        layers = self.in_layers + [self.ns_loss]
        self.params, self.grads = [], []
        for layer in layers:
            self.params += layer.params
            self.grads += layer.grads

        # メンバ変数に単語の分散表現を設定
        self.word_vecs = W_in

これはW_inがミュータブルなものとして設定されているために、実際にはそれぞれ学習される時のオプティマイザーによってW_inの値が変更されていると理解しました。

つまり、train.pyの中の「trainer = Trainer(model, optimizer)」で作成されたインスタンスを、trainer.fitで学習する際に呼ばれるoptimizerの中にある「params[i] -= (オプティマイザーにより異なる)」の箇所。

ゼロつくではword2vecの学習時にオプティマイザーとしてAdamを指定しているので、具体的にはcommon.optimizer.pyの中の以下部分で値が変更されているようです。

            params[i] -= lr_t * self.m[i] / (np.sqrt(self.v[i]) + 1e-7)

【参考】
Pythonの引数における参照渡しと値渡しについてが分かりやすかったです!(ありがとうございます)



処理イメージを簡単に書くと、以下のように(メモリ上に展開されている?)直接W_inの値を変更しているという理解です。

import numpy as np

W_in = 0.01 * np.random.randn(7, 3).astype('f')
print(W_in)

params = W_in[2]   #W_inの2行目のみparamsに代入
print(params)
print(type(params))

params += np.array([0.3,0.3,0.3])   #paramsのすべての要素に0.3を足す。つまりW_inのミュータブルな値W_in[2]を書き換えている

print(W_in)

出力結果は以下の通り。

# W_in = 0.01 * np.random.randn(7, 3).astype('f')
# print(W_in)
[[-0.00701334 -0.0073347   0.01756549]
 [-0.00425539 -0.00826015 -0.00587883]
 [ 0.01375221  0.01287736  0.00347899]
 [-0.01656658  0.01144262  0.00432454]
 [ 0.01538134 -0.00216335 -0.00644081]
 [ 0.00757222 -0.00047482 -0.02197131]
 [ 0.01234352  0.01055192  0.00145422]]

# params = W_in[2]   #W_inの2行目のみparamsに代入
# print(params)
[0.01375221 0.01287736 0.00347899]
# print(type(params))
<class 'numpy.ndarray'>

# params += np.array([0.3,0.3,0.3])
# print(W_in)
[[-0.00701334 -0.0073347   0.01756549]
 [-0.00425539 -0.00826015 -0.00587883]
 [ 0.3137522   0.31287736  0.303479  ]   #W_inの2行目の値にそれぞれ0.3足されていることが分かる
 [-0.01656658  0.01144262  0.00432454]
 [ 0.01538134 -0.00216335 -0.00644081]
 [ 0.00757222 -0.00047482 -0.02197131]
 [ 0.01234352  0.01055192  0.00145422]]



【訂正履歴】
最初に記事を書いたときは、W_inの値の変更はCBOWクラスの中の以下部分で行われていると勘違いしていました。

        for layer in layers:
            self.params += layer.params

知り合いから「間違えてるのではー?」と指摘を受けて確認してみると、この部分ではリストに値を追加しているだけでした(スイマセン)。

具体的には、paramsというリストに、Embeddingされる回数分のW_inの値のnumpy配列を突っ込み、最後に(layersの最後に格納されている[ns_loss]部分の)NegativeSamplingLossのsample_size数分のnumpy配列を突っ込んでいるという動きをしており、W_inの値を変更するというような動きはしていませんでした。
(まだまだ上手くプログラムが読めない・・・日々精進せねば。)