からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

5.4.2.2:Time Affineレイヤの実装【ゼロつく2のノート(実装)】

はじめに

 『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 本の内容を1つずつ確認しながらゆっくりと組んでいきます。

 この記事は、5.4.2.2項「Time Affineレイヤの実装」の内容です。Time Affineレイヤを解説して、Pythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

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}}$の各項は、次の式で計算できます。

$$ \frac{\partial L}{\partial b_v} = \sum_{n=0}^{N-1} \sum_{t=0}^{T-1} \frac{\partial L}{\partial s_{ntv}} $$

 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}}$は、次の式で計算できます。

$$ \frac{\partial L}{\partial \mathbf{W}} = \mathbf{rh}^{\mathrm{T}} \frac{\partial L}{\partial \mathbf{rs}} $$

 ただし$\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}}$を、次の式で計算します。

$$ \frac{\partial L}{\partial \mathbf{rh}} = \frac{\partial L}{\partial \mathbf{rs}} \mathbf{W}^{\mathrm{T}} $$

 ただし$\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レイヤを実装します。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.

おわりに

 2020年12月27日は、Juice=Juiceの稲場愛香さんの23歳のお誕生日です!おめでとうございます!

 明日もがんばりまなかん🌸゜。\(*´ω ` *)/🌙

【次節の内容】

www.anarchive-beta.com