はじめに
『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
本の内容を1つずつ確認しながらゆっくりと組んでいきます。
この記事は、6.3節「LSTMの実装」の始めの内容です。LSTMレイヤを解説して、Pythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
6.3.0 LSTMの実装
前節では、LSTMレイヤの処理を確認しました。この項では、LSTMレイヤを実装します。LSTMレイヤの順伝播の計算については前々回の記事、逆伝播の計算については前回の記事を参照してください。
# 6.3.0項で利用するライブラリ import numpy as np
LSTMレイヤの実装において、1.2.1項で実装したsigmoid()
を利用します。関数定義を再実行するか、次の方法でマスターデータから読み込む必要があります。
# 読み込み用の設定 import sys sys.path.append('C://Users//「ユーザ名」//Documents//・・・//deep-learning-from-scratch-2-master') # 実装済みの関数 from common.functions import sigmoid
・処理の確認
LSTMレイヤでは、入力データ$\mathbf{x}_t,\ \mathbf{h}_{t-1}$を用いて3つのゲート値と新しい情報を計算しました。パラメータを結合することで、この4つの計算を一度の処理で行えます。
ここでは効率化した処理を確認します。まずは、同じ形状の重みを複数個作成します。
# 重みの行数と列数を指定 row_num = 4 col_num = 3 # 重みを作成 W1 = np.ones((row_num, col_num)) W2 = np.ones((row_num, col_num)) * 2 W3 = np.ones((row_num, col_num)) * 3 print(W1) print(W2) print(W3)
[[1. 1. 1.]
[1. 1. 1.]
[1. 1. 1.]
[1. 1. 1.]]
[[2. 2. 2.]
[2. 2. 2.]
[2. 2. 2.]
[2. 2. 2.]]
[[3. 3. 3.]
[3. 3. 3.]
[3. 3. 3.]
[3. 3. 3.]]
次に、入力データを1つ作成します。
# データを作成 x = np.arange(5 * row_num).reshape((5, row_num)) print(x)
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]
[16 17 18 19]]
入力データと各重みとの行列の積を1つずつ確認します。
# 重み付き和を計算 print(np.dot(x, W1)) print(np.dot(x, W2)) print(np.dot(x, W2))
[[ 6. 6. 6.]
[22. 22. 22.]
[38. 38. 38.]
[54. 54. 54.]
[70. 70. 70.]]
[[ 12. 12. 12.]
[ 44. 44. 44.]
[ 76. 76. 76.]
[108. 108. 108.]
[140. 140. 140.]]
[[ 12. 12. 12.]
[ 44. 44. 44.]
[ 76. 76. 76.]
[108. 108. 108.]
[140. 140. 140.]]
np.hstack()
で重みを行方向に結合します。
# 重みを結合 W = np.hstack((W1, W2, W3)) print(W)
[[1. 1. 1. 2. 2. 2. 3. 3. 3.]
[1. 1. 1. 2. 2. 2. 3. 3. 3.]
[1. 1. 1. 2. 2. 2. 3. 3. 3.]
[1. 1. 1. 2. 2. 2. 3. 3. 3.]]
3つの重みは、それぞれ0列目からcol_num
列ずつ並んでいます。
結合した重みを使って1度の処理で行列の積を計算します。
# 重み付き和を計算 A = np.dot(x, W) print(A)
[[ 6. 6. 6. 12. 12. 12. 18. 18. 18.]
[ 22. 22. 22. 44. 44. 44. 66. 66. 66.]
[ 38. 38. 38. 76. 76. 76. 114. 114. 114.]
[ 54. 54. 54. 108. 108. 108. 162. 162. 162.]
[ 70. 70. 70. 140. 140. 140. 210. 210. 210.]]
計算結果もcol_num
列ずつ行方向に並んでいることが確認できます。勿論同じ値を計算できています。これは、x
から各行を取り出しW
から各列を取り出して計算するためです。後のオブジェクトの列数の変化は、出力するオブジェクトの列数に影響します。
スライス機能を使って、本来の形状のオブジェクトを取り出します。
# 計算結果を分割 print(A[:, :col_num]) print(A[:, col_num:2*col_num]) print(A[:, 2*col_num:3*col_num])
[[ 6. 6. 6.]
[22. 22. 22.]
[38. 38. 38.]
[54. 54. 54.]
[70. 70. 70.]]
[[ 12. 12. 12.]
[ 44. 44. 44.]
[ 76. 76. 76.]
[108. 108. 108.]
[140. 140. 140.]]
[[ 18. 18. 18.]
[ 66. 66. 66.]
[114. 114. 114.]
[162. 162. 162.]
[210. 210. 210.]]
先ほど確認した個別の計算結果と同じ出力が得られました。
・実装
処理の確認ができたので、LSTMレイヤをクラスとして実装します。
# LSTMレイヤの実装 class LSTM: # 初期化メソッドの定義 def __init__(self, Wx, Wh, b): self.params = [Wx, Wh, b] # パラメータ self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)] # 勾配 self.cache = None # 順伝播メソッドの定義 def forward(self, x, h_prev, c_prev): # パラメータと変数の形状に関する値を取得 Wx, Wh, b = self.params N, H = h_prev.shape # 結合したパラメータによる重み付き和の計算 A = np.dot(x, Wx) + np.dot(h_prev, Wh) + b # 各ゲートの重み付き和を取得 f = A[:, :H] # forgetゲート g = A[:, H:2*H] # 記憶セル i = A[:, 2*H:3*H] # inputゲート o = A[:, 3*H:] # outputゲート # ゲート値に変換 f = sigmoid(f) g = np.tanh(g) i = sigmoid(i) o = sigmoid(o) # 出力を計算 c_next = f * c_prev + g * i # 記憶セル h_next = o * np.tanh(c_next) # 出力データ # 逆伝播の計算用に変数を保存 self.cache = (x, h_prev, c_prev, i, f, g, o, c_next) return h_next, c_next # 逆伝播メソッドの定義 def backward(self, dh_next, dc_next): # 変数を取得 Wx, Wh, b = self.params x, h_prev, c_prev, i, f, g, o, c_next = self.cache # 計算用に活性化記憶セルを計算 tanh_c_next = np.tanh(c_next) # 現レイヤの記憶セルの勾配を計算 ds = dc_next + (dh_next * o) * (1 - tanh_c_next ** 2) # 前レイヤの記憶セルの勾配を計算 dc_prev = ds * f # 活性化後のゲートの勾配を計算 di = ds * g df = ds * c_prev do = dh_next * tanh_c_next dg = ds * i # 活性化前のゲートの勾配を計算 di *= i * (1 - i) df *= f * (1 - f) do *= o * (1 - o) dg *= (1 - g ** 2) # ゲートの勾配を結合 dA = np.hstack((df, dg, di, do)) # パラメータの勾配を計算 dWh = np.dot(h_prev.T, dA) dWx = np.dot(x.T, dA) db = dA.sum(axis=0) # パラメータの勾配を格納 self.grads[0][...] = dWx self.grads[1][...] = dWh self.grads[2][...] = db # 入力の勾配を計算 dx = np.dot(dA, Wx.T) dh_prev = np.dot(dA, Wh.T) return dx, dh_prev, dc_prev
前項で変数名をdc_next
としていたものが、このクラスではds
となっています(何由来のs?)。
実装したクラスを試してみましょう。LSTMレイヤで使う変数を作成します。
# データとパラメータの形状に関する値を指定 N = 5 # バッチサイズ D = 15 # 単語ベクトル(Embedレイヤの中間層)のサイズ H = 10 # 隠れ状態(Affineレイヤの中間層)のサイズ # (簡易的に)LSTMレイヤの入力データを作成 x = np.random.rand(N, D) * 2 # Embedレイヤの出力 h_prev = np.random.rand(N, H) * 2 # 1つ前のLSTMレイヤの出力 c_prev = np.random.rand(N, H) * 2 # 1つ前のLSTMレイヤの記憶セル print(np.round(h_prev, 2)) print(np.round(c_prev, 2)) # 結合したパラメータを生成 Wx = np.random.randn(D, H * 4) Wh = np.random.randn(H, H * 4) b = np.zeros(H * 4)
[[0.58 0.49 0.69 0.65 1.34 0.18 0.31 1.58 0.39 1.11]
[1.07 1.06 1.87 0.62 0.09 0.19 1.88 1.03 0.57 0.81]
[0.47 1.33 0.26 1.21 1.5 0.54 1.95 0.67 1.98 0.71]
[0.92 1.54 1.39 0.16 0.62 0.87 0.74 0.99 0.24 1.47]
[0.45 1.04 1.41 0.32 1.29 1.49 0.33 1.18 1.11 1.93]]
[[1.63 0.8 1.37 0.82 0.45 0.47 0.39 0.93 1.01 0.07]
[0.04 0.65 0.83 0.67 0.1 0.04 0.82 0.48 0.7 1.64]
[0.02 1.4 1.49 0.15 1.09 0.31 0.38 0.49 1.11 0. ]
[0.85 0.63 1.44 1.37 1.57 1.86 0.99 0.03 1.84 0.16]
[1.84 1.58 0.39 0.74 1.14 0.84 1.55 0.08 1.1 0.25]]
LSTMレイヤのインスタンスを作成して、順伝播メソッドを実行します。
# LSTMレイヤのインスタンスを作成 layer = LSTM(Wx, Wh, b) # 順伝播を計算 h_next, c_next = layer.forward(x, h_prev, c_prev) print(np.round(h_next, 2)) print(np.round(c_next, 2))
[[ 0.89 0.01 0.89 0.47 0.22 0.77 0. 0.08 0.11 0. ]
[ 0.61 0. -0.2 0.42 0. 0.38 0.01 0.38 0.02 0.8 ]
[-0.73 0. 0.95 0.68 0.31 0.39 0.27 0.05 -0.01 -0. ]
[ 0.65 0. 0.75 0.78 0.76 0.92 0.22 0. 0.01 -0.05]
[ 0.8 0. 0.58 0.53 0.02 0.32 0. -0. 0. 0. ]]
[[ 1.4 0.62 1.43 0.52 0.23 1.05 0.06 0.78 1.39 0. ]
[ 0.94 0.07 -0.2 0.65 0. 0.67 0.36 0.49 1.14 1.49]
[-0.99 0.91 2.06 0.84 0.39 0.49 0.35 0.42 -0.47 -0. ]
[ 1.64 0.49 0.99 1.94 0.99 1.98 0.42 0. 0.77 -0.05]
[ 1.1 0.87 0.69 0.85 0.07 0.35 0.31 -0. 0.19 0. ]]
h_next
とc_next
は、次のLSTMレイヤに入力します。ただしTime LSTMレイヤの実装上は、同じ変数名のオブジェクト繰り返し上書きします。
LSTMレイヤの逆伝播の入力を作成して、逆伝播メソッドを実行します。
# (簡易的に)LSTMレイヤの入力データを作成 dh_next = np.random.rand(N, H) * 2 # LSTMレイヤの出力 dc_next = np.random.rand(N, H) * 2 # LSTMレイヤの記憶セル # 逆伝播を計算 dx, dh_prev, dc_prev = layer.backward(dh_next, dc_next) print(np.round(dh_prev, 2)) print(np.round(dc_prev, 2))
[[-0.22 -1.34 0.5 -0.12 1.04 -0.98 -1.47 -1.31 1.82 1.81]
[ 0.6 -1.36 -0.76 0.71 0.62 -0.09 0.43 -1.25 0.96 0.31]
[ 0.24 -0.9 0.78 -2.77 3.11 -1.46 -0.45 -1.21 -2.87 -1.35]
[-0.77 -1.99 -0.54 -0.94 1.8 -0.06 -0.42 -1.8 0.63 1.28]
[ 0.25 1.46 1.74 -1.67 0.93 -1.39 0.03 2.01 -2.15 -0.89]]
[[0.3 0. 2.27 0.51 1.31 2.39 0.18 1.13 1.11 0.02]
[1.31 0. 0.46 1.24 0.04 0.89 0.36 1.22 0.94 1.32]
[0.58 0.01 1.55 0.31 0.86 1.03 0.98 0.4 0. 0.4 ]
[1.53 0. 1.23 0.47 0.77 1.81 0.92 0.18 0.47 0.04]
[0.52 0.01 0.79 1.19 0.06 1.99 0.06 0.16 1.16 0.01]]
dx
は、$T$個分の値をdxs
に格納してTime Embedレイヤに入力します。dh_prev
とdc_prev
は、順伝播と同様に再代入を繰り返すことで、簡単にTime LSTMレイヤの処理を行えます。
以上でLSTMレイヤを実装できました。次項では、時系列データに対応したTime LSTMレイヤを実装します。
参考文献
おわりに
前回と前々回で色々書いちゃったので、今回書くことがほとんどなかった。
【次節の内容】