2.14 LSTM

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

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

from ivory.core.model import sequential

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

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

[1] 2019-06-19 12:31:21 (12.0ms) python3 (6.61s)

<LSTM('LSTM.4', (3, 4)) at 0x247e8c2bac8>

数値部分のためにビット精度を64ビットにします。パラメータはRNNレイヤと同じです。

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

[2] 2019-06-19 12:31:21 (11.0ms) python3 (6.62s)

<Input('LSTM.4.x', (3,)) at 0x247e8c2b630>
<Output('LSTM.4.y', (4,)) at 0x247e8c2b940>
<Weight('LSTM.4.W', (3, 16)) at 0x247e8c2b240>
<Weight('LSTM.4.U', (4, 16)) at 0x247e8c2b208>
<Weight('LSTM.4.b', (16,)) at 0x247e8c2b550>
<State('LSTM.4.h', (4,)) at 0x247e8c2b198>

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

from ivory.common.context import np

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

[3] 2019-06-19 12:31:21 (5.98ms) python3 (6.63s)

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

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

[4] 2019-06-19 12:31:21 (5.97ms) python3 (6.63s)

順伝搬します。

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

[5] 2019-06-19 12:31:21 (9.04ms) python3 (6.64s)

[[[ 0.02786736  0.24418579 -0.19244531  0.03590731]
  [ 0.00518608  0.03557678 -0.2358253   0.00400717]]

 [[ 0.00212776  0.03478492  0.16185796 -0.01194557]
  [ 0.01531401  0.48747157 -0.08520488  0.36584658]]

 [[-0.29622047  0.18068509 -0.33047082  0.5464813 ]
  [ 0.01409608  0.1266075  -0.37425125  0.0819055 ]]]

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

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

for i in range(2):
    y = np.zeros(M)
    c = 0
    for xt in x[i, :2]:
        a = xt @ w + y @ u + b
        f, i, o, g = a[:M], a[M : 2 * M], a[2 * M : 3 * M], a[3 * M :]
        f = sigmoid(f)
        i = sigmoid(i)
        o = sigmoid(o)
        g = np.tanh(g)
        c = f * c + g * i
        y = o * np.tanh(c)
        print(y)
    print()

[6] 2019-06-19 12:31:21 (15.0ms) python3 (6.66s)

[ 0.02786736  0.24418579 -0.19244531  0.03590731]
[ 0.00518608  0.03557678 -0.2358253   0.00400717]

[ 0.00212776  0.03478492  0.16185796 -0.01194557]
[ 0.01531401  0.48747157 -0.08520488  0.36584658]

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

print(lstm.h.d)

[7] 2019-06-19 12:31:21 (7.05ms) python3 (6.66s)

[[ 0.20556649 -0.33462179  0.06576778 -0.08725707]
 [ 0.0074724   0.29543943 -0.56057381  0.51566009]
 [-0.01590355  0.50526685 -0.359696    0.78310709]]

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

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

[8] 2019-06-19 12:31:21 (219ms) python3 (6.88s)

x 0.0012416276092712534
W 0.0009545032892601483
U 0.00020825901063260317
b 0.0013799114447504608
h 0.0003029476310785402

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

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

Code 2.18 LSTMクラス

class LSTM(Layer):
    def init(self):
        L, M = self.shape
        self.W = self.add_weight((L, 4 * M)).randn()
        self.U = self.add_weight((M, 4 * M)).randn()
        self.b = self.add_weight((4 * M,)).zeros()
        self.h = self.add_state((M,))  # (N, M)
        self.A = None  # (N, T, 4M)
        self.c = None  # (N, T, M)
        self.h_prev = None  # (N, M)
        self.c_prev = None  # (N, M)

    def forward(self):
        L, M = self.shape
        N, T, L = self.x.d.shape
        if self.h.d is None:
            self.A = np.empty((N, T, 4 * M), dtype=self.dtype)  # [f, i, o, g]
            self.c = np.empty((N, T, M), dtype=self.dtype)
            self.h_prev = np.zeros((N, M), dtype=self.dtype)
            self.c_prev = np.zeros((N, M), dtype=self.dtype)
        else:
            self.h_prev = self.h.d
            self.c_prev = self.c[:, -1]

        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]
            a = x[:, t] + h @ self.U.d + self.b.d
            a[:, : 3 * M] = sigmoid(a[:, : 3 * M])  # [f, i, o]
            a[:, 3 * M :] = np.tanh(a[:, 3 * M :])  # [g]
            self.A[:, t] = a
            f, i, o, g = a[:, :M], a[:, M : 2 * M], a[:, 2 * M : 3 * M], a[:, 3 * M :]
            c = self.c_prev if t == 0 else self.c[:, t - 1]
            self.c[:, t] = f * c + g * i
            y[:, t] = o * np.tanh(self.c[:, t])

        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
        dc = 0
        for t in reversed(range(T)):
            dy = self.y.g[:, t] + self.h.g
            tanh_c = np.tanh(self.c[:, t])
            a = self.A[:, t]
            f, i, o, g = a[:, :M], a[:, M : 2 * M], a[:, 2 * M : 3 * M], a[:, 3 * M :]
            ds = dc + (dy * o) * (1 - tanh_c ** 2)
            c = self.c_prev if t == 0 else self.c[:, t - 1]
            dc = ds * f
            df = ds * c
            di = ds * g
            do = dy * tanh_c
            dg = ds * i
            df *= f * (1 - f)
            di *= i * (1 - i)
            do *= o * (1 - o)
            dg *= 1 - g ** 2
            da = np.hstack((df, di, do, dg))
            self.b.g = np.sum(da, axis=0)
            self.W.g = self.x.d[:, t].T @ da
            h = self.h_prev if t == 0 else self.y.d[:, t - 1]
            self.U.g = h.T @ da
            self.h.g = da @ self.U.d.T
            dx[:, t] = da @ self.W.d.T
        self.x.g = dx