2.10 MatMulMean

「ゼロから作るDeep Learning ❷」3章のword2vecでは、2つのMatMulレイヤが重みを共有しながら、両者の出力を平均を出力していました。ここでは、形状確認と勾配確認の手法を使って一つのレイヤとして実装します。

まずは、形状を定義します。

N, I, L, M = 2, 3, 4, 5

[2] 2019-06-12 20:01:35 (19.8ms) python3 (207ms)

ここでNはバッチ数、Iは入力数、Lは入力の次元、Mは出力の次元です。

通常のMatMulレイヤの順伝搬、逆伝搬は以下の通りです。

\mathbf{Y}=\mathbf{X}\cdot\mathbf{W}
\frac{\partial L}{\partial \mathbf{X}}=\frac{\partial L}{\partial \mathbf{Y}}\cdot \mathbf{W}^\mathrm{T}
\frac{\partial L}{\partial \mathbf{W}}=\mathbf{X}^\mathrm{T}\cdot \frac{\partial L}{\partial \mathbf{Y}}

実験のために、乱数行列を作成します。

import numpy as np

x = np.random.randn(N, I, L)
w = np.random.randn(L, M)

[12] 2019-06-12 20:01:36 (8.00ms) python3 (327ms)

まずは、入力を別々にして、MatMulレイヤの計算をひとつずつ行った場合を示します。

xs = x.transpose(1, 0, 2)
sum(xi @ w for xi in xs) / len(xs)

[13] 2019-06-12 20:01:36 (7.00ms) python3 (334ms)

array([[ 0.37747743,  0.62237493, -0.67779153,  0.17521082,  0.39120128],
       [ 0.31547654, -3.08890347,  0.28518512,  1.49883203,  1.34925693]])

各パラメータの形状を確認しておきます。

パラメータ 形状 具体例
\mathbf X (N, I, L) (2, 3, 4)
\mathbf W (L, M) (4, 5)
\mathbf X\cdot\mathbf W (N, I, M) (2, 3, 5)
\mathbf Y (N, M) (2, 5)

\mathbf{X}\mathbf{W}の内積をとった後に\mathbf{Y}の形状に合わせるには、軸1で和を取ります。

np.sum(x @ w, axis=1) / x.shape[1]

[27] 2019-06-12 20:01:36 (22.8ms) python3 (519ms)

array([[ 0.37747743,  0.62237493, -0.67779153,  0.17521082,  0.39120128],
       [ 0.31547654, -3.08890347,  0.28518512,  1.49883203,  1.34925693]])

順伝搬は以上です。ひとまず、ここまでをMatMulMeanレイヤとして実装します。イニシャライザに与える形状は、(I, L, M)とします。

from ivory.core.layer import Layer

class MatMulMean(Layer):
    def init(self):
        self.W = self.add_weight(self.shape[1:]).randn()

    def forward(self):
        self.y.d = np.sum(self.x.d @ self.W.d, axis=1) / self.shape[0]

[28] 2019-06-12 20:01:36 (7.07ms) python3 (526ms)

動作を確認しておきます。

layer = MatMulMean((I, L, M))
print(layer)
print(layer.x)
print(layer.W)
print(layer.y)

[29] 2019-06-12 20:01:36 (9.56ms) python3 (536ms)

<MatMulMean('MatMulMean.1', (3, 4, 5)) at 0x22c1f150710>
<Input('MatMulMean.1.x', (3, 4)) at 0x22c1d7d8588>
<Weight('MatMulMean.1.W', (4, 5)) at 0x22c0ab6e2e8>
<Output('MatMulMean.1.y', (5,)) at 0x22c1df36a20>
vs = layer.set_variables()
vs[0].data = x
vs[2].data = w
layer.forward()
layer.y.d

[30] 2019-06-12 20:01:36 (8.00ms) python3 (544ms)

array([[ 0.37747743,  0.62237493, -0.67779153,  0.17521082,  0.39120128],
       [ 0.31547654, -3.08890347,  0.28518512,  1.49883203,  1.34925693]])

次に逆伝搬を実装します。ここで勾配確認のテクニックを使います。今作ったMatMulMeanレイヤに損失関数を繋げます。

from ivory.core.model import Model
from ivory.layers.loss import SoftmaxCrossEntropy

loss_layer = SoftmaxCrossEntropy((M,))
loss_layer.set_input_layer(layer)
model = Model([loss_layer.loss]).build()

[31] 2019-06-12 20:01:36 (18.7ms) python3 (563ms)

ターゲットを乱数で生成します。

loss_layer.t.variable.data = np.random.randint(0, M, N)

[32] 2019-06-12 20:01:36 (15.7ms) python3 (578ms)

数値微分による勾配を求めます。まずは、\partial L/\partial \mathbf{X}です。

model.numerical_gradient(layer.x.variable)

[34] 2019-06-12 20:01:36 (12.0ms) python3 (607ms)

array([[[ 0.00071336, -0.27242102, -0.08968653, -0.02660314],
        [ 0.00071336, -0.27242102, -0.08968653, -0.02660314],
        [ 0.00071336, -0.27242102, -0.08968653, -0.02660314]],

       [[-0.25583178,  0.49819441,  0.22915949,  0.03552404],
        [-0.25583178,  0.49819441,  0.22915949,  0.03552404],
        [-0.25583178,  0.49819441,  0.22915949,  0.03552404]]])

入力数の数だけ、同じ勾配が分配されていることが分かります。

MatMulMeanレイヤの出力の勾配を求めます。

try:
    model.backward()
except AttributeError:  # まだ、backwardメソッドを定義していないため
    pass
layer.y.g

[35] 2019-06-12 20:01:37 (13.1ms) python3 (620ms)

array([[ 0.11220109,  0.14333581,  0.03905706,  0.0916545 , -0.38624847],
       [ 0.06187598, -0.49794393,  0.06002869,  0.20205965,  0.17397961]])

これを使ってとにかく単純なMatMulレイヤと同様の計算をしてみます。

dx = (layer.y.g @ layer.W.d.T) / layer.shape[0]
dx

[36] 2019-06-12 20:01:37 (15.6ms) python3 (635ms)

array([[ 0.00071336, -0.27242114, -0.08968657, -0.02660315],
       [-0.25583625,  0.49820791,  0.22916519,  0.03551903]])

軸0でrepeatした後、reshapeするのがよさそうです。

np.repeat(dx, layer.shape[0], axis=0).reshape(*layer.x.d.shape)

[37] 2019-06-12 20:01:37 (17.9ms) python3 (653ms)

array([[[ 0.00071336, -0.27242114, -0.08968657, -0.02660315],
        [ 0.00071336, -0.27242114, -0.08968657, -0.02660315],
        [ 0.00071336, -0.27242114, -0.08968657, -0.02660315]],

       [[-0.25583625,  0.49820791,  0.22916519,  0.03551903],
        [-0.25583625,  0.49820791,  0.22916519,  0.03551903],
        [-0.25583625,  0.49820791,  0.22916519,  0.03551903]]])

数値微分と一致しました。次は、\partial L/\partial \mathbf{W}です。

model.numerical_gradient(layer.W.variable)

[39] 2019-06-12 20:01:37 (12.1ms) python3 (674ms)

array([[-0.01558251,  0.35705042, -0.03057175, -0.11170919, -0.19918698],
       [ 0.06852379, -0.31887347,  0.05096554,  0.16268992,  0.03669422],
       [-0.00880439, -0.5160213 ,  0.03060959,  0.12534984,  0.36886626],
       [-0.02559231,  0.20198423, -0.02456447, -0.08252685, -0.06930061]])

MatMulレイヤと同様の計算をする前に、形状を確認しておきます。

パラメータ 形状 具体例
\mathbf{Y} (N, M) (2, 5)
\mathbf X (N, I, L) (2, 3, 4)
\mathbf{X}^\mathrm{T} (L, I, N) (4, 3, 2)
\mathbf{X}^\mathrm{T}\cdot \partial L/\partial \mathbf{Y} (L, I, M) (4, 3, 5)
\mathbf W (L, M) (4, 5)

これより、必要な計算が明らかになります。

np.sum(layer.x.d.T @ layer.y.g, axis=1) / layer.shape[0]

[58] 2019-06-12 20:01:37 (7.03ms) python3 (901ms)

array([[-0.01558197,  0.35705834, -0.03057052, -0.1117174 , -0.19918845],
       [ 0.06852323, -0.31888197,  0.05096423,  0.16269879,  0.03669573],
       [-0.00880513, -0.51603192,  0.03060795,  0.12536083,  0.36886827],
       [-0.02559198,  0.20198915, -0.02456371, -0.08253197, -0.0693015 ]])

数値微分と一致しました。backwardメソッドを追加した実装は以下の通りです。

Code 2.13 MatMulMeanクラスの実装

class MatMulMean(Layer):
    def init(self):
        self.W = self.add_weight(self.shape[1:]).randn()

    def forward(self):
        self.y.d = np.sum(self.x.d @ self.W.d, axis=1) / self.shape[0]

    def backward(self):
        dx = (self.y.g @ self.W.d.T) / self.shape[0]
        self.x.g = np.repeat(dx, self.shape[0], axis=0).reshape(*self.x.d.shape)
        self.W.g = np.sum(self.x.d.T @ self.y.g, axis=1) / self.shape[0]

動作の確認を行います。

from ivory.core.model import sequential

net = [("input", I, L), ("matmulmean", M, "softmax_cross_entropy")]
model = sequential(net)
mat = model.layers[0]
mat.x

model.set_data(np.random.randn(N, I, L), np.random.randint(0, M, N))
model.forward()
model.backward()

for var in model.grad_variables:
    error = model.gradient_error(var)
    print(var.parameters[0].name, f"{error:.04e}")

[61] 2019-06-12 20:01:37 (22.0ms) python3 (971ms)

x 1.7170e-07
W 1.7841e-05

以上のように、形状確認と勾配確認によって、新規にレイヤを実装することができました。