からっぽのしょこ

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

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

はじめに

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

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ処理を確認しながらゆっくりと組んでいきます。

 この記事は、5.6.2項「バッチ版Affineレイヤ」の内容になります。バッチデータ対応版のAffineレイヤの順伝播と逆伝播をPythonで実装します。

【前節の内容】

https://www.anarchive-beta.com/entry/2020/07/31/180000www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

5.6.1 Affineレイヤ

 これまでは「重み付き和」や「重み付き信号とバイアスの総和」などと呼んできた計算を、ここからはAffineレイヤと呼ぶことにします。「Affineレイヤの逆伝播」の方で、Affineレイヤの順伝播(重み付き和の計算)の確認と逆伝播の導出を行います。

5.6.2 バッチ版Affineレイヤ

 5.6.1項では、簡単な例でAffineレイヤにおける順伝播と逆伝播の数学上の処理を確認しました。この項では、複数データを一度に処理できるように実装します。  

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

$$ \mathbf{X} = \begin{pmatrix} x_{11} & x_{12} & \cdots & x_{1L} \\ x_{21} & x_{22} & \cdots & x_{2L} \\ \vdots & \vdots & \ddots & \vdots \\ x_{N1} & x_{N2} & \cdots & x_{NL} \end{pmatrix} ,\ \mathbf{W} = \begin{pmatrix} w_{11} & w_{12} & \cdots & w_{1M} \\ w_{21} & w_{22} & \cdots & w_{2M} \\ \vdots & \vdots & \ddots & \vdots \\ w_{L1} & w_{L2} & \cdots & w_{LM} \end{pmatrix} $$

 ここで、$N$は(画像)データ数(バッチサイズ)、$M$は隠れ層のニューロン数または最終層であれば数字の数の10、$L$は1つ前の層のニューロン数または第1層であればピクセル数の784です。
 またバイアスは

$$ \mathbf{B} = \begin{pmatrix} b_1 & b_2 \cdots & b_{M} \end{pmatrix} $$

です。これをデータごとに加算します。

 順伝播の計算式は、(もう何度も見ましたが)次の式でしたね。

$$ \mathbf{Y} = \mathbf{X} \mathbf{W} + \mathbf{B} $$

 出力$\mathbf{Y}$は、$N \times M$の行列となり、$i,j$要素は

$$ y_{ij} = \sum_{j=1}^L x_{ij} w_{jk} + b_k $$

になります。

 逆伝播の入力を$\frac{\partial L}{\partial \mathbf{Y}}$とすると、逆伝播の計算式は次の式です。こちらは「バッチ版Affineレイヤの逆伝播」の方で導出します。

$$ \begin{align} \frac{\partial L}{\partial \mathbf{X}} &= \frac{\partial L}{\partial \mathbf{Y}} \mathbf{W}^{\mathrm{T}} \tag{5.13}\\ \frac{\partial L}{\partial \mathbf{W}} &= \mathbf{X}^{\mathrm{T}} \frac{\partial L}{\partial \mathbf{Y}} \tag{5.13}\\ \frac{\partial L}{\partial \mathbf{B}} &= \begin{pmatrix} \sum_{i=1}^N \frac{\partial L}{\partial y_{i1}} & \sum_{i=1}^N \frac{\partial L}{\partial y_{i2}} & \cdots & \sum_{i=1}^N \frac{\partial L}{\partial y_{iM}} \end{pmatrix} \end{align} $$

 ここで、$\mathbf{W}^{\mathrm{T}}$は重み$\mathbf{W}$の転置行列、$\mathbf{X}^{\mathrm{T}}$は順伝播の入力$\mathbf{X}$の転置行列です。転置行列とは、全ての要素で行と列を入れ替えた行列を言います。それぞれ要素は次のようになります。

$$ \mathbf{X}^{\mathrm{T}} = \begin{pmatrix} x_{11} & x_{21} & \cdots & x_{N1} \\ x_{12} & x_{22} & \cdots & x_{N2} \\ \vdots & \vdots & \ddots & \vdots \\ x_{1L} & x_{2L} & \cdots & x_{NL} \end{pmatrix} ,\ \mathbf{W}^{\mathrm{T}} = \begin{pmatrix} w_{11} & w_{21} & \cdots & w_{L1} \\ w_{12} & w_{22} & \cdots & w_{L2} \\ \vdots & \vdots & \ddots & \vdots \\ w_{1M} & w_{2M} & \cdots & w_{LM} \end{pmatrix} $$

 転置行列では、行方向に移動すると2つ目の添字が変化し、列号項に移動すると1つ目の添字が変化します。

 実装の前にまずは、転置メソッド.Tを確認します。

# NumPを読み込む
import numpy as np

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(A)
print(A.T)

B = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]])
print(B)
print(B.T)
[[1 2 3]
 [4 5 6]
 [7 8 9]]
[[1 4 7]
 [2 5 8]
 [3 6 9]]
[[ 1  2  3  4  5]
 [ 6  7  8  9 10]]
[[ 1  6]
 [ 2  7]
 [ 3  8]
 [ 4  9]
 [ 5 10]]


 ニューラルネットワークで用いる変数を作成します。

# バッチサイズ(N)
batch_size = 50

# 入力サイズ(L)
input_size = 784

# 出力サイズ(M)
output_size = 10

# 仮の入力の値を生成
X = np.random.rand(batch_size, input_size)
print(X.shape)

# 仮の重みの値を生成
W = np.random.rand(input_size, output_size)
print(W.shape)

# 仮のバイアスの値を生成
B = np.random.rand(output_size)
print(B.shape)
(50, 784)
(784, 10)
(10,)


 作成した変数を使って逆伝播の計算をします。逆伝播の入力は、順伝播の出力と同じ形状である必要があります。

# 仮の逆伝播の入力の値を生成
dout = np.random.rand(batch_size, output_size)

# 式(5.13.1)
dX = np.dot(dout, W.T) 
print(dX)

# 式(5.13.2)
dW = np.dot(X.T, dout)
print(dW)

# バイアスの微分
dB = np.sum(dout, axis=0)
print(dB)
[[1.79253131 2.05496205 2.9512666  ... 2.00908433 2.51731899 3.08935345]
 [1.80756113 2.07803887 2.32485795 ... 2.07492706 2.9155429  2.40053809]
 [1.6091882  1.7147844  2.16380273 ... 1.6846666  2.28510009 2.06453882]
 ...
 [2.23015796 2.2490529  2.40153028 ... 1.99048408 2.18247045 2.24576519]
 [1.92064384 1.42331331 2.6470193  ... 1.54243858 2.31169577 2.20297621]
 [2.39369459 2.44902945 3.0523484  ... 2.46352849 3.22989678 3.34763237]]
[[10.94723046 12.80814804 12.13150857 ... 13.89768034 11.46870832
  12.72507754]
 [12.36765099 11.90348319 12.03358054 ... 14.48536631 12.98602586
  13.38990508]
 [11.96456821 11.0094984  11.85154449 ... 13.16323672 11.31928422
  10.00812163]
 ...
 [10.42788398 10.77632209 11.59803885 ... 11.92821797 10.68620892
  11.52054554]
 [12.3641733  12.70255228 13.50156816 ... 14.0772642  12.92251533
  13.50256496]
 [13.13685806 12.33475481 12.73440142 ... 14.26143683 13.84251376
  13.8191765 ]]
[24.04527559 23.90163631 24.68365705 22.83245759 23.96076624 21.85210336
 24.82636976 26.98662933 24.98976743 24.98538026]

 np.sum()の引数axis0を指定することで、列ごとに和をとります。

 では、順伝播と逆伝播のメソッドを持つ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) # 式(5.13.1)
        self.dW = np.dot(self.x.T, dout) # 式(5.13.2)
        self.db = np.sum(dout, axis=0)
        
        return dx

 重みとバイアスは、順伝播でも逆伝播でも用いるためインスタンスの作成時に引数に指定して、インスタンス変数として保存しておきます。

 順伝播と逆伝播の入力は、それぞれのメソッドの使用時に引数に指定します。順伝播の入力については、逆伝播の計算にも使うので、インスタンス変数として保存します。

 逆伝播の計算時に求める重みとバイアスの微分は、インスタンス変数として保存します。

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

 最初に指定した値を使って、動作の確認をしましょう。まずはインスタンスを作成します。

# インスタンスを作成
network = Affine(W, B)
print(network.x)
print(network.W.shape)
None
(784, 10)

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

 順伝播の計算をします。

# 順伝播
Y = network.forward(X)
print(Y.shape)
(50, 10)

 出力の形状は、行数がbatch_size列数がoutput_sizeの2次元配列になります。

 次は、逆伝播の計算をします。

# 逆伝播
dX = network.backward(dout)
print(dX.shape)
print(network.dW.shape)
print(network.db.shape)
(50, 784)
(784, 10)
(10,)

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

 以上でバッチデータ対応版のAffineレイヤを実装できました!次は、Affineレイヤの出力の活性化に用いるソフトマックス関数を実装します。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.

おわりに

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

【次節の内容】

https://www.anarchive-beta.com/entry/2020/08/05/180000www.anarchive-beta.com