はじめに
『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
本の内容を1つずつ確認しながらゆっくりと組んでいきます。
この記事は、5.4.2.2項「Time Affineレイヤの実装」の内容です。Time Affineレイヤを解説して、Pythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
5.4.2.2 Time Affineレイヤの実装
依存関係のある$T$個のデータ(時系列データ)に対してAffineレイヤの処理を行うTime Affineレイヤを実装します。Affineレイヤについては、「1.3.5.2項」または「1巻の5.6節」も参照してください。
# 5.4.2.2項で利用するライブラリ import numpy as np
・順伝播の処理の確認
Time Affineレイヤで行う順伝播の処理を確認していきます。
まずはこれまでと同様に、ハイパーパラメータを設定して簡易的なTime Affineレイヤの入力データ$\mathbf{hs} = (\mathbf{h}_0, \mathbf{h}_1, \cdots, \mathbf{h}_{T-1})$を用意します。これは、5.3.2項で実装したTime RNNレイヤの出力データです。また$\mathbf{hs}$は3次元配列で、各次元の要素数は$(N, T, H)$です。ここで$N$はバッチサイズ、$T$は時間サイズ、$H$はAffineレイヤの中間層のニューロン数です。
# バッチサイズを指定 batch_size = 3 # 時間サイズ(繰り返し回数)を指定 time_size = 4 # Affineレイヤの中間層のニューロン数を指定 hidden_size = 5 # (簡易的に)入力データを作成 hs = np.arange( batch_size * time_size * hidden_size ).reshape((batch_size, time_size, hidden_size)) print(hs) print(hs.shape)
[[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]
[15 16 17 18 19]]
[[20 21 22 23 24]
[25 26 27 28 29]
[30 31 32 33 34]
[35 36 37 38 39]]
[[40 41 42 43 44]
[45 46 47 48 49]
[50 51 52 53 54]
[55 56 57 58 59]]]
(3, 4, 5)
次に、Time Affineレイヤの重み$\mathbf{W}$とバイアス$\mathbf{b}$を生成します。$\mathbf{W}$は$H \times V$の行列、$\mathbf{b}$は$V$次元ベクトルです。ここで$V$は、語彙数(単語の種類数)です。これはTime Affineレイヤによって、各単語ベクトルを(観測された)全ての種類の単語に変換するのが目的です。
# 語彙数を指定 vocab_size = 7 # パラメータを生成 W = np.random.randn(hidden_size, vocab_size) b = np.zeros(vocab_size) print(W.shape) print(b.shape)
(5, 7)
(7,)
入力データとパラメータを作成できたので、順伝播の処理を行います。
素直に$T$回繰り返しAffineレイヤの処理を行うと次のようになります。
# 出力の受け皿を作成 ss = np.empty((batch_size, time_size, vocab_size), dtype='f') # 1期ずつ重み付き和を計算 for t in range(time_size): ss[:, t, :] = np.dot(hs[:, t, :], W) + b print(np.round(ss, 1)) print(ss.shape)
[[[ 2.7 6.2 -5.9 -1.4 1.9 -1.8 3. ]
[ -3.3 20.4 -20.6 -4.2 -7.2 -9.4 4.9]
[ -9.2 34.7 -35.4 -7. -16.2 -17. 6.8]
[ -15.2 48.9 -50.1 -9.8 -25.3 -24.7 8.7]]
[[ -21.2 63.2 -64.8 -12.6 -34.3 -32.3 10.6]
[ -27.1 77.5 -79.5 -15.3 -43.4 -40. 12.5]
[ -33.1 91.7 -94.3 -18.1 -52.4 -47.6 14.4]
[ -39.1 106. -109. -20.9 -61.5 -55.3 16.3]]
[[ -45.1 120.3 -123.7 -23.7 -70.5 -62.9 18.3]
[ -51. 134.5 -138.4 -26.5 -79.6 -70.5 20.2]
[ -57. 148.8 -153.2 -29.3 -88.6 -78.2 22.1]
[ -63. 163.1 -167.9 -32. -97.7 -85.8 24. ]]]
(3, 4, 7)
各時刻の入力データを取り出して、行列の積の計算を行っています。計算結果は、$(N, T, V)$の3次元配列になります。これを$\mathbf{ss} = (\mathbf{s}_0, \mathbf{s}_1, \cdots, \mathbf{s}_{V-1})$と表記することにします。
この繰り返しの処理は、入力データを次のように2次元配列に変換することで、全ての計算を一度に行うことができます。
# 2次元配列に変換 rh = hs.reshape(batch_size * time_size, -1) print(rh) print(rh.shape)
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]
[15 16 17 18 19]
[20 21 22 23 24]
[25 26 27 28 29]
[30 31 32 33 34]
[35 36 37 38 39]
[40 41 42 43 44]
[45 46 47 48 49]
[50 51 52 53 54]
[55 56 57 58 59]]
(12, 5)
3次元方向に並んでいた単語ベクトルを、行方向(2次元方向)に並べ替えています。
求めたいのは、単語ベクトルと対応する重みの列との内積と言えます。単語ベクトルを行として持つ2次元配列に変換することで、1度の行列の積の計算で求められます。
# 重み付き和を計算 rs = np.dot(rh, W) + b print(np.round(out, 1)) print(rs.shape)
[[[-0.2 0.8 -0.7 -0.3 1.7 1.1 0.9 -0.5 0.4 -2.3]
[-2.1 -1.1 -0.3 1.2 -0.1 1.3 -1.5 0.5 -1.7 -0.3]
[ 0.1 -1.2 2.4 1.8 0.1 -0.4 -0.4 -0.9 0.2 1.7]
[-1.2 1.5 0.5 0. 0.9 -0.7 0.6 -0.1 1.4 -0.4]
[-0.5 -0.1 0.6 -0.4 -1.4 -0.7 0.6 0.9 -0.3 -0. ]
[-2.1 -1.1 -0.3 1.2 -0.1 1.3 -1.5 0.5 -1.7 -0.3]
[ 0.5 1.2 -1.5 0.6 2.1 -1.5 1.1 0.2 1.8 -0.1]]]
(12, 7)
計算結果は、$N T \times V$の2次元配列になります。
これを本来の出力データの形状である$(N, T, V)$の3次元配列に整形する必要があります。
# 本来の形状に再変換 ss = rs.reshape((batch_size, time_size, vocab_size)) print(np.round(ss, 1)) print(ss.shape)
[[[ 2.7 6.2 -5.9 -1.4 1.9 -1.8 3. ]
[ -3.3 20.4 -20.6 -4.2 -7.2 -9.4 4.9]
[ -9.2 34.7 -35.4 -7. -16.2 -17. 6.8]
[ -15.2 48.9 -50.1 -9.8 -25.3 -24.7 8.7]]
[[ -21.2 63.2 -64.8 -12.6 -34.3 -32.3 10.6]
[ -27.1 77.5 -79.5 -15.3 -43.4 -40. 12.5]
[ -33.1 91.7 -94.3 -18.1 -52.4 -47.6 14.4]
[ -39.1 106. -109. -20.9 -61.5 -55.3 16.3]]
[[ -45.1 120.3 -123.7 -23.7 -70.5 -62.9 18.3]
[ -51. 134.5 -138.4 -26.5 -79.6 -70.5 20.2]
[ -57. 148.8 -153.2 -29.3 -88.6 -78.2 22.1]
[ -63. 163.1 -167.9 -32. -97.7 -85.8 24. ]]]
(3, 4, 7)
for
文による処理の結果と同じ値になっているのを確認できました。
ここまでが順伝播の処理です。続いて、逆伝播の処理を確認していきます。
・逆伝播の処理の確認
順伝播の入力データを$N T \times H$の行列$\mathbf{rh}$、重みを$H \times D$の行列$\mathbf{W}$、バイアスを$V$次元ベクトルと考えると、1巻の5.6.2項の計算式で各変数の勾配を求められます。
まずは、Time Affineレイヤの逆伝播における入力データ$\frac{\partial L}{\partial \mathbf{ss}}$を簡易的に作成します。$\frac{\partial L}{\partial \mathbf{ss}}$は、Time Softmax with Lossレイヤの逆伝播の出力データです。これは$\mathbf{ss}$と同じ形状の$(N, T, V)$の3次元配列です。
# (簡易的に)Time Affineレイヤの逆伝播の入力データを作成 dss = np.ones((batch_size, time_size, vocab_size)) print(dss) print(dss.shape)
[[[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]]
[[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]]
[[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]]]
(3, 4, 7)
順伝播のときと同様に、$N T \times V$の2次元配列に変換して計算に使います。これを$\frac{\partial L}{\partial \mathbf{rs}}$と表記することにします。
# 2次元配列に変換 drs = dss.reshape((batch_size * time_size, vocab_size)) print(drs) print(drs.shape)
[[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]]
(12, 7)
次に、各変数に関する勾配を求めていきます。
Time RNNレイヤのときと同様に、$v$番目のバイアス$b_v$は、全てのバッチデータ$n = 0, 1, \cdots, N - 1$かつ全ての時刻$t = 0, 1, \cdots, V - 1$の3次元方向に$v$番目の項に、分岐(1.3.4.3項「Repeatノード」)して、加算(1.3.4項「加算ノード」)されています。そのためその全ての勾配の和をとる必要があります。従ってバイアス$\mathbf{b}$の勾配$\frac{\partial L}{\partial \mathbf{b}}$の各項は、次の式で計算できます。
2次元配列に変換したdrs
は、バッチデータ$n$と時系列データ$t$に関する項は行方向に並んでいるので、axis=0
を指定して計算します。
# バイアスの勾配を計算 db = np.sum(drs, axis=0) print(db) print(db.shape)
[12. 12. 12. 12. 12. 12. 12.]
(7,)
計算結果のdb
は、b
と同じ形状になります。
行列の積の計算は、MatMalノード(1.3.4.5項)です。従って、重み$\mathbf{W}$の勾配$\frac{\partial L}{\partial \mathbf{W}}$は、次の式で計算できます。
ただし$\mathbf{rh}$は、順伝播の入力データ$\mathbf{hs}$を行列に変換したものとします。また$\mathrm{T}$は転置記号であり、$\mathbf{rh}^{\mathrm{T}}$は$\mathbf{rh}$の転置行列です。
# 重みの勾配を計算 dW = np.dot(rh.T, drs) print(dW) print(dW.shape)
[[330. 330. 330. 330. 330. 330. 330.]
[342. 342. 342. 342. 342. 342. 342.]
[354. 354. 354. 354. 354. 354. 354.]
[366. 366. 366. 366. 366. 366. 366.]
[378. 378. 378. 378. 378. 378. 378.]]
(5, 7)
計算結果のdW
は、W
と同じ形状になります。
入力データ$\mathbf{hs}$の勾配$\frac{\partial L}{\partial \mathbf{hs}}$を求めるために、まず$\mathbf{rh}$の勾配$\frac{\partial L}{\partial \mathbf{rh}}$を、次の式で計算します。
ただし$\mathbf{W}^{\mathrm{T}}$は、$\mathbf{W}$の転置行列です。
# 入力データの勾配を計算 drh = np.dot(drs, W.T) print(np.round(drh, 1)) print(dh.shape)
[[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]]
(1, 5)
これを本来の3次元配列に再度変換するすることで$\frac{\partial L}{\partial \mathbf{hs}}$となります。
# 本来の形状に戻す dhs = drh.reshape((batch_size, time_size, hidden_size)) print(np.round(dhs, 1)) print(dhs.shape)
[[[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]]
[[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]]
[[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]]]
(3, 4, 5)
計算結果のdhs
は、hs
と同じ形状になります。
ここまでが逆伝播の処理です。
・実装
処理の確認ができたので、Time Affineレイヤをクラスとして実装します。
# Time Affineレイヤの実装 class TimeAffine: # 初期化メソッドの定義 def __init__(self, W, b): self.params = [W, b] # パラメータ self.grads = [np.zeros_like(W), np.zeros_like(b)] # 勾配 self.x = None # 順伝播メソッドの定義 def forward(self, x): # 形状に関する値を取得 N, T, D = x.shape # パラメータを取得 W, b = self.params # 重み付き和の計算 rx = x.reshape(N * T, -1) # 整形 out = np.dot(rx, W) + b # 逆伝播の計算用に保存 self.x = x return out.reshape(N, T, -1) # 逆伝播メソッドの定義 def backward(self, dout): # 入力データを取得 x = self.x # 形状に関する値を取得 N, T, D = x.shape # パラメータを取得 W, b = self.params # 計算用に整形 dout = dout.reshape(N * T, -1) rx = x.reshape(N * T, -1) # 勾配を計算 db = np.sum(dout, axis=0) dW = np.dot(rx.T, dout) dx = np.dot(dout, W.T) dx = dx.reshape(*x.shape) # 元の形状に戻す # 結果を格納 self.grads[0][...] = dW self.grads[1][...] = db return dx
実装したクラスを試してみましょう。
# Time Affineレイヤのインスタンスを作成 layer = TimeAffine(W, b) # 順伝播の計算 ss = layer.forward(hs) print(np.round(ss, 1)) print(ss.shape) # 逆伝播の計算 dhs = layer.backward(dss) print(np.round(dhs, 1)) # 入力データの勾配 print(dhs.shape) print(layer.grads[0]) print(layer.grads[0].shape) # 重みの勾配 print(layer.grads[1]) print(layer.grads[1].shape) # バイアスの勾配
・出力(クリックで展開)
[[[ 2.7 6.2 -5.9 -1.4 1.9 -1.8 3. ]
[ -3.3 20.4 -20.6 -4.2 -7.2 -9.4 4.9]
[ -9.2 34.7 -35.4 -7. -16.2 -17. 6.8]
[ -15.2 48.9 -50.1 -9.8 -25.3 -24.7 8.7]]
[[ -21.2 63.2 -64.8 -12.6 -34.3 -32.3 10.6]
[ -27.1 77.5 -79.5 -15.3 -43.4 -40. 12.5]
[ -33.1 91.7 -94.3 -18.1 -52.4 -47.6 14.4]
[ -39.1 106. -109. -20.9 -61.5 -55.3 16.3]]
[[ -45.1 120.3 -123.7 -23.7 -70.5 -62.9 18.3]
[ -51. 134.5 -138.4 -26.5 -79.6 -70.5 20.2]
[ -57. 148.8 -153.2 -29.3 -88.6 -78.2 22.1]
[ -63. 163.1 -167.9 -32. -97.7 -85.8 24. ]]]
(3, 4, 7)
[[[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]]
[[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]]
[[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]
[-1.5 -4.8 -1.3 -1.1 3.8]]]
(3, 4, 5)
[[330. 330. 330. 330. 330. 330. 330.]
[342. 342. 342. 342. 342. 342. 342.]
[354. 354. 354. 354. 354. 354. 354.]
[366. 366. 366. 366. 366. 366. 366.]
[378. 378. 378. 378. 378. 378. 378.]]
(5, 7)
[12. 12. 12. 12. 12. 12. 12.]
(7,)
以上でTime Affineレイヤを実装できました。次項では、Time Softmax With Lossレイヤを実装します。
参考文献
おわりに
2020年12月27日は、Juice=Juiceの稲場愛香さんの23歳のお誕生日です!おめでとうございます!
明日もがんばりまなかん🌸゜。\(*´ω ` *)/🌙
【次節の内容】