2.12 EmbeddingDot

「ゼロから作るDeep Learning ❷」に従ったEmbeddingDotレイヤの実装は以下のようになります。

Code 2.15 EmbeddingDotクラス

class EmbeddingDot(Layer):
    def init(self):
        self.W = self.add_weight(self.shape[::-1]).randn()
        self.t = self.add_input()
        self.y.shape = ()

    def forward(self):
        self.t_W = self.W.d[self.t.d]
        self.y.d = np.sum(self.x.d * self.t_W, axis=1)

    def backward(self):
        dy = self.y.g.reshape(self.y.g.shape[0], 1)
        grad = np.zeros_like(self.W.d)
        np.scatter_add(grad, self.t.d, dy * self.x.d)  # np.add.at
        self.W.g = grad
        self.x.g = dy * self.t_W

動作を確かめます。形状(バッチ数、単語ベクトルの次元、ターゲットラベルの次元)を以下の通りとします。

N, L, M = 10, 4, 5

from ivory.layers.embedding import EmbeddingDot

dot = EmbeddingDot((L, M))
print(dot)
print(dot.x)
print(dot.t)
print(dot.W)
print(dot.y)

[3] 2019-06-12 20:09:42 (31.3ms) python3 (227ms)

<EmbeddingDot('EmbeddingDot.1', (4, 5)) at 0x20166f43be0>
<Input('EmbeddingDot.1.x', (4,)) at 0x201661836a0>
<Input('EmbeddingDot.1.t', ()) at 0x20166f43c88>
<Weight('EmbeddingDot.1.W', (5, 4)) at 0x20166f43c50>
<Output('EmbeddingDot.1.y', ()) at 0x20166f43ba8>

これまでのレイヤと違い、レイヤの形状と重みの形状が逆転していることに注意します。また、出力は内積の結果なので、損失関数と同様にスカラー値となります。2つ目の入力tがEmbeddingレイヤへの入力でこちらもラベル表現なのでスカラー値となります。(バッチ数は除きます。)

ランダムな入力他を作成します。

import numpy as np

x = np.random.randn(N, L)
t = np.random.randint(0, M, N)
w = np.random.randn(M, L)

[4] 2019-06-12 20:09:42 (15.8ms) python3 (242ms)

レイヤに入力し、順伝搬します。

dot.set_variables()
dot.x.variable.data = x
dot.t.variable.data = t
dot.W.variable.data = w
dot.forward()
dot.y.d[:3]

[5] 2019-06-12 20:09:42 (8.11ms) python3 (250ms)

array([1.7249044 , 0.01588949, 1.82722988])

バッチ数分の内積値が出力されました。上記が正しいか、確かめます。

[xi @ w[ti] for xi, ti in zip(x, t)][:3]

[6] 2019-06-12 20:09:42 (9.02ms) python3 (259ms)

[1.724904397256743, 0.01588949090515246, 1.827229877596224]

逆伝搬を検証するために、数値微分による勾配確認を行います。これまでと違い、SigmoidCrossEntropyレイヤを用います。

from ivory.core.model import sequential

net = [("input", L), ('embeddingdot', M), ('sigmoid_cross_entropy')]
model = sequential(net)
model.data_input_variables

[7] 2019-06-12 20:09:42 (9.02ms) python3 (268ms)

[<Variable(['EmbeddingDot.2.x'], (4,)) at 0x20166f67cc0>,
 <Variable(['EmbeddingDot.2.t'], ()) at 0x20166f67cf8>,
 <Variable(['SigmoidCrossEntropy.1.t'], ()) at 0x20166f67d30>]

正解ラベルを乱数で生成します。

t2 = np.random.randint(0, 2, N)

[8] 2019-06-12 20:09:42 (8.04ms) python3 (277ms)

データを入力します。

model.set_data(x, t, t2)

[9] 2019-06-12 20:09:42 (18.0ms) python3 (294ms)

数値微分による勾配を求めます。

W = model.layers[0].W
model.numerical_gradient(W.variable)

[10] 2019-06-12 20:09:42 (30.8ms) python3 (325ms)

array([[-0.07671048, -0.23686358, -0.06632242,  0.26851696],
       [ 0.        ,  0.        ,  0.        ,  0.        ],
       [ 0.03158904,  0.11123896,  0.31113186, -0.10000519],
       [-0.00457926,  0.00334235,  0.23325554, -0.06364331],
       [-0.03226531, -0.01438602,  0.01871012, -0.0163209 ]],
      dtype=float32)

逆伝搬による勾配を求めます。

model.forward()
model.backward()
W.g

[11] 2019-06-12 20:09:42 (9.96ms) python3 (335ms)

array([[-0.07672066, -0.236895  , -0.06633119,  0.26851252],
       [ 0.        ,  0.        ,  0.        ,  0.        ],
       [ 0.03158861,  0.11125373,  0.3111739 , -0.10001861],
       [-0.00457919,  0.00334229,  0.23325166, -0.06364226],
       [-0.03226477, -0.01438364,  0.01870981, -0.01631819]],
      dtype=float32)

一致した結果が得られました。

ここでbackwardメソッドを再掲します。

Code 2.16 EmbeddingDot.backwardメソッド

    def backward(self):
        dy = self.y.g.reshape(self.y.g.shape[0], 1)
        grad = np.zeros_like(self.W.d)
        np.scatter_add(grad, self.t.d, dy * self.x.d)  # np.add.at
        self.W.g = grad
        self.x.g = dy * self.t_W

Affineレイヤと同じように出力の勾配に内積の相手側を掛けた行列を入力側に逆伝搬しています。ただし、内積なので、要素ごとの積になっています。