からっぽのしょこ

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

5.6.2:Affineレイヤの実装【ゼロつく1のノート(実装)】

はじめに

 「プログラミング」初学者のための『ゼロから作るDeep Learning』攻略ノートです。『ゼロつくシリーズ』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ実行結果を見ながら処理の意図を確認していきます。

 この記事は、5.6.2項「バッチ版Affineレイヤ」の内容です。AffineレイヤをPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

5.6.1 Affineレイヤ

 これまで「重み付き和」や「重み付き信号とバイアスの総和」と呼んできた計算を全結合(fully conected)とも呼びます。この資料では、重み付き和を計算するレイヤをAffineレイヤと呼ぶことにします。Affine変換が名前の由来ですが、これは行列の積の計算がAffine変換と共通しているからで、AffineレイヤではAffine変換を行いません。

5.6.2 Affineレイヤの実装

 重み付き和の順伝播と逆伝播の計算を行うAffineレイヤを実装します。重み付き和(順伝播)については「3.3-4:ニューラルネットワークの順伝播【ゼロつく1のノート(実装)】 - からっぽのしょこ」、Affineレイヤの微分(逆伝播)については「1データ版Affineレイヤの逆伝播の導出【ゼロつく1のノート(数学)】 - からっぽのしょこ」または「バッチデータ版Affineレイヤの逆伝播の導出【ゼロつく1のノート(数学)】 - からっぽのしょこ」を参照してください。

 利用するライブラリを読み込みます。

# 5.6.2項で利用するライブラリ
import numpy as np
import matplotlib.pyplot as plt


・数式の確認

 まずは、Affineレイヤの定義式を確認します。

・順伝播の計算

 順伝播の入力$\mathbf{X}$、重み$\mathbf{W}$、バイアス$\mathbf{b}$をそれぞれ次の形状とします。

$$ \mathbf{X} = \begin{pmatrix} x_{0,0} & x_{0,1} & \cdots & x_{0,D-1} \\ x_{1,0} & x_{1,1} & \cdots & x_{1,D-1} \\ \vdots & \vdots & \ddots & \vdots \\ x_{N-1,0} & x_{N-1,1} & \cdots & x_{N-1,D-1} \end{pmatrix} ,\ \mathbf{W} = \begin{pmatrix} w_{0,0} & w_{0,1} & \cdots & w_{0,H-1} \\ w_{1,0} & w_{1,1} & \cdots & w_{1,H-1} \\ \vdots & \vdots & \ddots & \vdots \\ w_{D-1,0} & w_{D-1,1} & \cdots & w_{D-1,H-1} \end{pmatrix} ,\ \mathbf{b} = \begin{pmatrix} b_1 & b_2 & \cdots & b_{H-1} \end{pmatrix} $$

 ここで、$N$はバッチサイズ(1試行当たりのデータ数)、$D$は1データの要素数または前の層のニューロン数、$H$は次の層のニューロン数です。Pythonのインデックスに合わせて添字を0から割り当てています。

 順伝播では、次の式で出力$\mathbf{Y}$を計算します。

$$ \begin{aligned} \mathbf{Y} &= \mathbf{X} \mathbf{W} + \mathbf{B} \\ &= \begin{pmatrix} \sum_{d=0}^{D-1} x_{0,d} w_{d,0} + b_0 & \sum_{d=0}^{D-1} x_{0,d} w_{d,1} + b_1 & \cdots & \sum_{d=0}^{D-1} x_{0,d} w_{d,H-1} + b_{H-1} \\ \sum_{d=0}^{D-1} x_{1,d} w_{d,0} + b_0 & \sum_{d=0}^{D-1} x_{1,d} w_{d,1} + b_1 & \cdots & \sum_{d=0}^{D-1} x_{1,d} w_{d,H-1} + b_{H-1} \\ \vdots & \vdots & \ddots & \vdots \\ \sum_{d=0}^{D-1} x_{N-1,d} w_{d,0} + b_0 & \sum_{d=0}^{D-1} x_{N-1,d} w_{d,1} + b_1 & \cdots & \sum_{d=0}^{D-1} x_{N-1,d} w_{d,H-1} + b_{H-1} \\ \end{pmatrix} \\ &= \begin{pmatrix} y_{0,0} & y_{0,1} & \cdots & y_{0,H-1} \\ y_{1,0} & y_{1,1} & \cdots & y_{1,H-1} \\ \vdots & \vdots & \ddots & \vdots \\ y_{N-1,0} & y_{N-1,1} & \cdots & y_{N-1,H-1} \end{pmatrix} \end{aligned} $$

 $N \times D$と$D \times H$の行列の積なので、$\mathbf{Y}$は$N \times H$の行列になります。
 また、バイアス$\mathbf{b}$は行方向に複製した行列$\mathbf{B}$として扱います。

$$ \mathbf{B} = \begin{pmatrix} b_1 & b_2 & \cdots & b_{H-1} \\ b_1 & b_2 & \cdots & b_{H-1} \\ \vdots & \vdots & \ddots & \vdots \\ b_1 & b_2 & \cdots & b_{H-1} \end{pmatrix} $$

 この処理はブロードキャストによって行われます。

・逆伝播の計算

 逆伝播の入力$\frac{\partial L}{\partial \mathbf{Y}}$は、$\mathbf{Y}$と同じ形状で、次のレイヤから入力します。

$$ \frac{\partial L}{\partial \mathbf{Y}} = \begin{pmatrix} \frac{\partial L}{\partial y_{0,0}} & \frac{\partial L}{\partial y_{0,1}} & \cdots & \frac{\partial L}{\partial y_{0,H-1}} \\ \frac{\partial L}{\partial y_{1,0}} & \frac{\partial L}{\partial y_{1,1}} & \cdots & \frac{\partial L}{\partial y_{1,H-1}} \\ \vdots & \vdots & \ddots & \vdots \\ \frac{\partial L}{\partial y_{N-1,0}} & \frac{\partial L}{\partial y_{N-1,1}} & \cdots & \frac{\partial L}{\partial y_{N-1,H-1}} \end{pmatrix} $$

 逆伝播では、順伝播の入力$\mathbf{X}$とパラメータ$\mathbf{W},\ \mathbf{b}$それぞれに関する損失$L$の勾配$\frac{\partial L}{\partial \mathbf{X}},\ \frac{\partial L}{\partial \mathbf{W}},\ \frac{\partial L}{\partial \mathbf{b}}$を求めます。

 順伝播の入力$\mathbf{X}$の勾配$\frac{\partial L}{\partial \mathbf{X}}$は、次の式で計算します。

$$ \begin{align} \frac{\partial L}{\partial \mathbf{X}} &= \frac{\partial L}{\partial \mathbf{Y}} \mathbf{W}^{\mathrm{T}} \tag{5.13} \\ &= \begin{pmatrix} \sum_{h=0}^{H-1} \frac{\partial L}{\partial y_{0,h}} w_{0,h} & \sum_{h=0}^{H-1} \frac{\partial L}{\partial y_{0,h}} w_{1,h} & \cdots & \sum_{h=0}^{H-1} \frac{\partial L}{\partial y_{0,h}} w_{H-1,h} \\ \sum_{h=0}^{H-1} \frac{\partial L}{\partial y_{1,h}} w_{0,h} & \sum_{h=0}^{H-1} \frac{\partial L}{\partial y_{1,h}} w_{1,h} & \cdots & \sum_{h=0}^{H-1} \frac{\partial L}{\partial y_{1,h}} w_{H-1,h} \\ \vdots & \vdots & \ddots & \vdots \\ \sum_{h=0}^{H-1} \frac{\partial L}{\partial y_{N-1,h}} w_{0,h} & \sum_{h=0}^{H-1} \frac{\partial L}{\partial y_{N-1,h}} w_{1,h} & \cdots & \sum_{h=0}^{H-1} \frac{\partial L}{\partial y_{N-1,h}} w_{H-1,h} \end{pmatrix} \\ &= \begin{pmatrix} \frac{\partial L}{\partial x_{0,0}} & \frac{\partial L}{\partial x_{0,1}} & \cdots & \frac{\partial L}{\partial x_{0,H-1}} \\ \frac{\partial L}{\partial x_{1,0}} & \frac{\partial L}{\partial x_{1,1}} & \cdots & \frac{\partial L}{\partial x_{1,H-1}} \\ \vdots & \vdots & \ddots & \vdots \\ \frac{\partial L}{\partial x_{N-1,0}} & \frac{\partial L}{\partial x_{N-1,1}} & \cdots & \frac{\partial L}{\partial x_{N-1,H-1}} \end{pmatrix} \end{align} $$

 ここで、$\mathbf{W}^{\mathrm{T}}$は重み$\mathbf{W}$の転置行列です。転置行列とは、$d, h$要素と$h, d$要素を入れ替えた行列を言います。

$$ \mathbf{W}^{\mathrm{T}} = \begin{pmatrix} w_{0,0} & w_{1,0} & \cdots & w_{D-1,0} \\ w_{0,1} & w_{1,1} & \cdots & w_{D-1,1} \\ \vdots & \vdots & \ddots & \vdots \\ w_{0,H-1} & w_{1,H-1} & \cdots & w_{D-1,H-1} \end{pmatrix} $$

 $\mathbf{W}^{\mathrm{T}}$は$H \times D$の行列になります。

 重み$\mathbf{W}$の勾配$\frac{\partial L}{\partial \mathbf{W}}$は、次の式で計算します。

$$ \begin{align} \frac{\partial L}{\partial \mathbf{W}} &= \mathbf{X}^{\mathrm{T}} \frac{\partial L}{\partial \mathbf{Y}} \tag{5.13}\\ &= \begin{pmatrix} \sum_{n=0}^{N-1} x_{n,0} \frac{\partial L}{\partial y_{n,0}} & \sum_{n=0}^{N-1} x_{n,0} \frac{\partial L}{\partial y_{n,1}} & \cdots & \sum_{n=0}^{N-1} x_{n,0} \frac{\partial L}{\partial y_{n,H-1}} \\ \sum_{n=0}^{N-1} x_{n,1} \frac{\partial L}{\partial y_{n,0}} & \sum_{n=0}^{N-1} x_{n,1} \frac{\partial L}{\partial y_{n,1}} & \cdots & \sum_{n=0}^{N-1} x_{n,1} \frac{\partial L}{\partial y_{n,H-1}} \\ \vdots & \vdots & \ddots & \vdots \\ \sum_{n=0}^{N-1} x_{n,D-1} \frac{\partial L}{\partial y_{n,0}} & \sum_{n=0}^{N-1} x_{n,D-1} \frac{\partial L}{\partial y_{n,1}} & \cdots & \sum_{n=0}^{N-1} x_{n,D-1} \frac{\partial L}{\partial y_{n,H-1}} \end{pmatrix} \\ &= \begin{pmatrix} \frac{\partial L}{\partial w_{0,0}} & \frac{\partial L}{\partial w_{0,1}} & \cdots & \frac{\partial L}{\partial w_{0,H-1}} \\ \frac{\partial L}{\partial w_{1,0}} & \frac{\partial L}{\partial w_{1,1}} & \cdots & \frac{\partial L}{\partial w_{1,H-1}} \\ \vdots & \vdots & \ddots & \vdots \\ \frac{\partial L}{\partial w_{D-1,0}} & \frac{\partial L}{\partial w_{D-1,1}} & \cdots & \frac{\partial L}{\partial w_{D-1,H-1}} \end{pmatrix} \end{align} $$

 ここで、$\mathbf{X}^{\mathrm{T}}$は順伝播の入力$\mathbf{X}$の転置行列です。

$$ \mathbf{X}^{\mathrm{T}} = \begin{pmatrix} x_{0,0} & x_{1,0} & \cdots & x_{N-1,0} \\ x_{0,1} & x_{1,1} & \cdots & x_{N-1,1} \\ \vdots & \vdots & \ddots & \vdots \\ x_{0,D-1} & x_{1,D-1} & \cdots & x_{N-1,D-1} \end{pmatrix} $$

 $\mathbf{X}^{\mathrm{T}}$は、$\mathbf{X}$の$n, d$要素と$d, n$要素を入れ変えた行列で、$D \times N$の行列になります。

 バイアス$\mathbf{b}$の勾配$\frac{\partial L}{\partial \mathbf{b}}$は、次の式で計算します。

$$ \begin{aligned} \frac{\partial L}{\partial \mathbf{b}} &= \begin{pmatrix} \sum_{n=0}^{N-1} \frac{\partial L}{\partial y_{n,0}} & \sum_{n=0}^{N-1} \frac{\partial L}{\partial y_{n,1}} & \cdots & \sum_{n=0}^{N-1} \frac{\partial L}{\partial y_{n,H-1}} \end{pmatrix} \\ &= \begin{pmatrix} \frac{\partial L}{\partial b_0} & \frac{\partial L}{\partial b_1} & \cdots & \frac{\partial L}{\partial b_{H-1}} \end{pmatrix} \end{aligned} $$

 以上がAffineレイヤで行う計算です。

・処理の確認

 次に、Affineレイヤで行う処理を確認します。

 その前に、NumPyの転置メソッドTを確認しておきます。

# 変数を作成
A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(A)
print(A.shape)

# 転置
print(A.T)
print(A.T.shape)
[[1 2 3]
 [4 5 6]
 [7 8 9]]
(3, 3)
[[1 4 7]
 [2 5 8]
 [3 6 9]]
(3, 3)

 行数と列数が同じ2次元配列(正方行列)の場合は、転置しても形状は変わりません。

# 変数を作成
B = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]])
print(B)
print(B.shape)

# 転置
print(B.T)
print(B.T.shape)
[[ 1  2  3  4  5]
 [ 6  7  8  9 10]]
(2, 5)
[[ 1  6]
 [ 2  7]
 [ 3  8]
 [ 4  9]
 [ 5 10]]
(5, 2)

 行数と列数が異なる場合は、行数と列数が入れ替わります。

 逆伝播の計算でこの機能を利用します。

・順伝播の計算

 バッチサイズ$N$、前の層のニューロン数$N$、次の層のニューロン数$H$を指定します。

# バッチサイズを指定
N = 20

# 前の層のニューロン数を指定
D = 50

# 次の層のニューロン数を指定
H = 10


 順伝播の入力$\mathbf{X}$とパラメータ$\mathbf{W},\ \mathbf{b}$を作成します。

# (仮の)入力を作成
X = np.random.rand(N, D)
print(X.shape)

# (仮の)重みを作成
W = np.random.rand(D, H)
print(W.shape)

# (仮の)バイアスを作成
b = np.random.rand(H)
print(b.shape)
(20, 50)
(50, 10)
(10,)

 Xの列数とWの行数、Wの列数とbの要素数が一致している必要があります。

 順伝播の出力$\mathbf{Y}$を計算します。

# 重み付き和を計算
Y = np.dot(X, W) + b
print(Y.shape)
(20, 10)

 順伝播の出力が得られました。

 ここまでは、順伝播の処理を確認しました。続いて、逆伝播の処理を確認します。

・逆伝播の計算

 逆伝播の入力$\frac{\partial L}{\partial \mathbf{Y}}$を作成します。

# (仮の)逆伝播の入力を作成
dY = np.random.rand(N, H)
print(dY.shape)
(20, 10)

 $\frac{\partial L}{\partial \mathbf{Y}}$は$\mathbf{Y}$と同じ形状です。

 各変数の勾配$\frac{\partial L}{\partial \mathbf{X}},\ \frac{\partial L}{\partial \mathbf{W}},\ \frac{\partial L}{\partial \mathbf{b}}$を計算します。

# 順伝播の入力の勾配を計算
dX = np.dot(dY, W.T) 
print(dX.shape)

# 重みの勾配を計算
dW = np.dot(X.T, dY)
print(dW.shape)

# バイアスの勾配を計算
db = np.sum(dY, axis=0)
print(db.shape)
(20, 50)
(50, 10)
(10,)

 逆伝播の出力$\frac{\partial L}{\partial \mathbf{X}}$と各パラメータの勾配$\frac{\partial L}{\partial \mathbf{W}},\ \frac{\partial L}{\partial \mathbf{b}}$が得られました。
 $\frac{\partial L}{\partial \mathbf{X}}$は1つ前のレイヤに入力します。$\frac{\partial L}{\partial \mathbf{W}},\ \frac{\partial L}{\partial \mathbf{b}}$はパラメータ$\mathbf{W},\ \mathbf{b}$の更新に利用します。

 以上がAffineレイヤで行う処理です。

・実装

 処理を確認ができたので、Affineレイヤをクラスとして実装します。

# Affineレイヤの実装
class Affine:
    # 初期化メソッド
    def __init__(self, W, b):
        # パラメータを保存
        self.W = W # 重み
        self.b = b # バイアス
        
        # 入力と勾配を初期化
        self.x = None  # 順伝播の入力
        self.dW = None # 重みの勾配
        self.db = None # バイアスの勾配
    
    # 順伝播メソッド
    def forward(self, x):
        # 順伝播の入力を保存
        self.x = x
        
        # 重み付き和を計算
        out = np.dot(x, self.W) + self.b
        return out
    
    # 逆伝播メソッド
    def backward(self, dout):
        # 勾配を計算
        dx = np.dot(dout, self.W.T)      # 順伝播の入力の勾配
        self.dW = np.dot(self.x.T, dout) # 重みの勾配
        self.db = np.sum(dout, axis=0)   # バイアスの勾配
        
        # 順伝播の入力の勾配を出力
        return dx

 重み$\mathbf{W}$とバイアス$\mathbf{b}$は、順伝播でも逆伝播でも計算に用いるため、インスタンスの作成時にそれぞれインスタンス変数W, bとして保存しておきます。逆伝播メソッドでは、保存した値を使って計算します。
 インスタンスの作成時にパラメータを指定するのは、ニューラルネットワークのクラス内でパラメータの初期化とレイヤの初期化を同時に行うためです(5.7節)。

 順伝播の入力$\mathbf{X}$は、順伝播メソッドの実行時に引数に指定します。逆伝播の計算にも使うので、インスタンス変数xとして保存します。
 逆伝播の入力$\frac{\partial L}{\partial \mathbf{Y}}$は、逆伝播メソッドの実行時に引数に指定します。こちらはここでだけ使うので、値を保存する必要はありません。
 順伝播の入力と逆伝播の入力をそれぞれメソッドの引数に指定するのは、学習を繰り返す度に入力が変わるためです。

 パラメータの勾配$\frac{\partial L}{\partial \mathbf{W}},\ \frac{\partial L}{\partial \mathbf{b}}$は、パラメータの更新に利用するため、逆伝播の計算時に(逆伝播メソッドの実行時に)インスタンス変数dW, dbとして保存しておきます。

 後で値を代入するインスタンス変数については、初期値をNoneにしておくことで、値を持たない状態で変数だけ作成しておきます。

 実装したクラスを試してみましょう。

 Affineクラスのインスタンスを作成します。

# Affineレイヤのインスタンスを作成
layer = Affine(W, b)
print(layer.x)
print(layer.W.shape)
print(layer.b.shape)
None
(50, 10)
(10,)

 インスタンス変数は、用途に応じて初期値を持つものと持たないものがあります。

 順伝播の計算をします。

# 順伝播を計算
Y = layer.forward(X)
print(Y.shape)
(20, 10)


 逆伝播の計算をします。

# 逆伝播を計算
dX = layer.backward(dY)
print(dX.shape)
print(layer.dW.shape)
print(layer.db.shape)
(20, 50)
(50, 10)
(10,)

 それぞれ元の変数の形状と同じ形状になります。

・グラフの確認

 最後に、Affineレイヤの順伝播と逆伝播の処理をグラフで確認します。

 思いの外長くなったので別の記事にしました。興味があれば読んでみてください。

www.anarchive-beta.com

www.anarchive-beta.com


 以上で、Affineレイヤを実装できました。次は、分類問題における出力層の活性化に用いるSoftmaxレイヤと交差エントロピー誤差を計算するCross Entropy Errorレイヤをまとめて実装します。

参考文献

github.com

おわりに

 ところで逆伝播の導出を読みましたか?私は1年は見たくないです。この本で一番キツいので、避けていいと思います。というか1つ解けませんでした、、、

  • 2021.09.30:加筆修正しました。

 逆伝播については暫く前に導出できて今はどうまとめたものか悩んでます。

【次節の内容】

www.anarchive-beta.com