2.13 RNN

「ゼロから作るDeep Learning ❷」ではT個のRNNレイヤをTime RNNレイヤとして実装しますが、Ivoryライブラリでは、これをまとめてRNNレイヤとして実装します。

RNNレイヤの入力と出力の関係は以下のようになります。

\mathbf{Y}_t = \mathbf{X}_t\cdot\mathbf{W} + \mathbf{Y}_{t-1}\cdot\mathbf{U} + \mathbf{B}

動作を確かめます。形状(バッチ数、タイムステップ数、入力の次元、出力の次元)を以下の通りとします。

from ivory.core.model import sequential

N, T, L, M = 2, 10, 3, 4

net = [("input", L), ("rnn", M), ("softmax_cross_entropy")]
model = sequential(net)
rnn = model.layers[0]
print(rnn)

[9] 2019-06-19 12:26:44 (207ms) python3 (207ms)

<RNN('RNN.1', (3, 4)) at 0x247e6641ba8>

数値部分のためにビット精度を64ビットにします。

rnn.dtype = 'float64'
for p in rnn.parameters:
    print(p)

[10] 2019-06-19 12:26:44 (7.00ms) python3 (214ms)

<Input('RNN.1.x', (3,)) at 0x247e6641b70>
<Output('RNN.1.y', (4,)) at 0x247e6641be0>
<Weight('RNN.1.W', (3, 4)) at 0x247e6641b38>
<Weight('RNN.1.U', (4, 4)) at 0x247e6641c18>
<Weight('RNN.1.b', (4,)) at 0x247e6641c50>
<State('RNN.1.h', (4,)) at 0x247e6641c88>

レイヤパラメータを設定します。

from ivory.common.context import np

w = rnn.W.variable.data = np.random.randn(L, M)
u = rnn.U.variable.data = np.random.randn(M, M)
b = rnn.b.variable.data = np.random.randn(M)

[11] 2019-06-19 12:26:44 (9.00ms) python3 (223ms)

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

x = np.random.randn(N, T, L)
t = np.random.randint(0, M, (N, T))
model.set_data(x, t)

[12] 2019-06-19 12:26:44 (7.04ms) python3 (230ms)

順伝搬します。

model.forward()
print(rnn.y.d[:, :2])

[13] 2019-06-19 12:26:44 (7.02ms) python3 (237ms)

[[[ 0.67942201  0.92316021  0.92755244  0.67532329]
  [ 0.64782003 -0.90267257  0.92316364  0.36894766]]

 [[ 0.74078478 -0.99777283 -0.21203909 -0.9937645 ]
  [-0.29704364  0.99997487 -0.97511937 -0.01264683]]]

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

y = np.zeros(M)
for xt in x[0, :2]:
    y = np.tanh(xt @ w + y @ u + b)
    print(y)
print()
y = np.zeros(M)
for xt in x[1, :2]:
    y = np.tanh(xt @ w + y @ u + b)
    print(y)

[14] 2019-06-19 12:26:44 (10.00ms) python3 (247ms)

[0.67942201 0.92316021 0.92755244 0.67532329]
[ 0.64782003 -0.90267257  0.92316364  0.36894766]

[ 0.74078478 -0.99777283 -0.21203909 -0.9937645 ]
[-0.29704364  0.99997487 -0.97511937 -0.01264683]

隠れ状態\mathbf{h}は最後の出力を保持します。

print(rnn.h.d)

[15] 2019-06-19 12:26:44 (6.00ms) python3 (253ms)

[[-0.12219022  0.9819797   0.99488455  0.38738626]
 [-0.68594901  0.86778244 -0.98897499 -0.96731999]]

逆伝搬を検証するために、数値微分による勾配確認を行います。

model.forward()
model.backward()
for v in model.grad_variables:
    print(v.parameters[0].name, model.gradient_error(v))

[16] 2019-06-19 12:26:44 (49.0ms) python3 (302ms)

x 2.958226730972167e-08
W 7.499132052383283e-08
U 1.3688863295901977e-07
b 1.0700991801742069e-07
h 3.067465995025922e-08

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

実装は以下のとおりです。

Code 2.17 RNNクラス

class RNN(Layer):
    def init(self):
        L, M = self.shape
        self.W = self.add_weight((L, M)).randn()
        self.U = self.add_weight((M, M)).randn()
        self.b = self.add_weight((M,)).zeros()
        self.h = self.add_state((M,))
        self.h_prev = None

    def forward(self):
        L, M = self.shape
        N, T, L = self.x.d.shape
        if self.h.d is None:
            self.h_prev = np.zeros((N, M), dtype=self.dtype)
        else:
            self.h_prev = self.h.d
        x = self.x.d @ self.W.d
        y = np.empty((N, T, M), dtype=self.dtype)
        for t in range(T):
            h = self.h_prev if t == 0 else y[:, t - 1]
            y[:, t] = np.tanh(x[:, t] + h @ self.U.d + self.b.d)
        self.y.d = y
        self.h.d = y[:, -1]

    def backward(self):
        L, M = self.shape
        N, T, L = self.x.d.shape
        dx = np.empty((N, T, L), dtype=self.dtype)
        self.h.g = 0
        for t in reversed(range(T)):
            dy = self.y.g[:, t] + self.h.g
            dt = dy * (1 - self.y.d[:, t] ** 2)
            self.b.g = np.sum(dt, axis=0)
            self.W.g = self.x.d[:, t].T @ dt
            h = self.h_prev if t == 0 else self.y.d[:, t - 1]
            self.U.g = h.T @ dt
            self.h.g = dt @ self.U.d.T
            dx[:, t] = dt @ self.W.d.T
        self.x.g = dx