自動微分(Automatic Differentiation)は大規模なニューラルネットワークであるDeepLearningの学習における誤差逆伝播などに用いられる手法です。当記事ではAffine変換の自動微分とニューラルネットワーク(Neural Network)の実装について取りまとめました。
作成にあたっては「ゼロから作るDeep Learning」の第$5$章「誤差逆伝播法」の内容を主に参照しました。
・用語/公式解説
https://www.hello-statisticians.com/explain-terms
Contents
Affine変換の自動微分
Affine変換の式定義と転置
MLP(Multi Layer Perceptron)の順伝播の計算はAffine変換の式を元に定義することができる。たとえば入力層を$\mathbf{x}$、計算に用いるパラメータを$W$、出力層を$\mathbf{y}$、バイアス項を$\mathbf{b}$とおくとき、Affine変換の式は下記のように表せる。
$$
\large
\begin{align}
\mathbf{y} = W \mathbf{x} + \mathbf{b} \quad (1.1)
\end{align}
$$
$\mathbf{y} \in \mathbb{R}^{d_x}, \, \mathbf{y} \in \mathbb{R}^{d_y}$であるとき、$(1.1)$式は$W \in \mathbb{R}^{d_y \times d_x}, \, \mathbf{b} \in \mathbb{R}^{d_y}$を用いて下記のように表すこともできる。
$$
\large
\begin{align}
\mathbf{y} &= W \mathbf{x} + \mathbf{b} \quad (1.1) \\
\left( \begin{array}{c} y_1 \\ \vdots \\ y_{d_y} \end{array} \right) &= \left( \begin{array}{ccc} w_{11} & \cdots & w_{1, d_x} \\ \vdots & \ddots & \vdots \\ w_{d_y, 1} & \cdots & w_{d_y, d_x} \end{array} \right) \left( \begin{array}{c} x_1 \\ \vdots \\ x_{d_x} \end{array} \right) + \left( \begin{array}{c} b_1 \\ \vdots \\ b_{d_y} \end{array} \right) \quad (1.2)
\end{align}
$$
上記の式はオーソドックスな行列とベクトルの積の形式である一方で、統計学や機械学習では$\mathbf{x}$に複数サンプルを用いるにあたって、下記のような転置の形式を用いることが多い。
$$
\large
\begin{align}
\left( \begin{array}{c} y_1 \\ \vdots \\ y_{d_y} \end{array} \right)^{\mathrm{T}} &= \left[ \left( \begin{array}{ccc} w_{11} & \cdots & w_{1, d_x} \\ \vdots & \ddots & \vdots \\ w_{d_y, 1} & \cdots & w_{d_y, d_x} \end{array} \right) \left( \begin{array}{c} x_1 \\ \vdots \\ x_{d_x} \end{array} \right) + \left( \begin{array}{c} b_1 \\ \vdots \\ b_{d_y} \end{array} \right) \right]^{\mathrm{T}} \\
\left( \begin{array}{ccc} y_1 & \cdots & y_{d_y} \end{array} \right) &= \left( \begin{array}{ccc} x_1 & \cdots & x_{d_x} \end{array} \right) \left( \begin{array}{ccc} w_{11} & \cdots & w_{1, d_y} \\ \vdots & \ddots & \vdots \\ w_{d_x, 1} & \cdots & w_{d_x, d_y} \end{array} \right) + \left( \begin{array}{ccc} b_1 & \cdots & b_{d_y} \end{array} \right) \quad (1.2)’
\end{align}
$$
$(1.2)’$式を元に$n$個の複数サンプルの入力と出力の対応の式を下記のように表すことができる。
$$
\begin{align}
\left( \begin{array}{ccc} y_{11} & \cdots & y_{1,d_y} \\ \vdots & \ddots & \vdots \\ y_{n,1} & \cdots & y_{n,d_y} \end{array} \right) &= \left( \begin{array}{ccc} x_{11} & \cdots & x_{1,d_x} \\ \vdots & \ddots & \vdots \\ x_{n,1} & \cdots & x_{n,d_x} \end{array} \right) \left( \begin{array}{ccc} w_{11} & \cdots & w_{1, d_y} \\ \vdots & \ddots & \vdots \\ w_{d_x, 1} & \cdots & w_{d_x, d_y} \end{array} \right) + \left( \begin{array}{ccc} b_{1} & \cdots & b_{d_y} \\ \vdots & \ddots & \vdots \\ b_{1} & \cdots & b_{d_y} \end{array} \right) \quad (1.3) \\
Y &= XW + B
\end{align}
$$
上記の$(1.3)$式の入力とパラメータの積の順序はデザイン行列と同様に理解しておくと良い。
入力・パラメータ・バイアスによる行列微分
多変数スカラー関数$\displaystyle L(Y) = \sum_{i=1}^{n} f(y_{i1}, \cdots , y_{i,d_y}) = \sum_{i=1}^{n} f(\mathbf{y}_{i})$を定義する。このとき関数$f$を$(1.3)$の表記に基づいて入力やパラメータについて行列微分を行う際の式について以下、確認を行う。
入力行列による行列微分
下記のように入力による行列微分$\displaystyle \frac{\partial L}{\partial X}$を定義する。
$$
\large
\begin{align}
\frac{\partial L}{\partial X} = \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial x_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial x_{1,d_x}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial x_{n1}} & \cdots & \displaystyle \frac{\partial L}{\partial x_{n,d_x}} \end{array} \right)
\end{align}
$$
このとき、$\displaystyle \frac{\partial L}{\partial x_{ij}}$は微分の連鎖律に基づいて下記のように計算できる。
$$
\large
\begin{align}
\frac{\partial L}{\partial x_{ij}} &= \frac{\partial L}{\partial y_{i1}} \cdot \frac{\partial y_{i1}}{\partial x_{ij}} + \cdots + \frac{\partial L}{\partial y_{i,d_y}} \cdot \frac{\partial y_{i,d_y}}{\partial x_{ij}} \\
&= \sum_{k=1}^{d_y} \frac{\partial f(\mathbf{y}_{i})}{\partial y_{ik}} \cdot \frac{\partial y_{ik}}{\partial x_{ij}} = \sum_{k=1}^{d_y} \frac{\partial f(\mathbf{y}_{i})}{\partial y_{ik}} w_{jk} \\
&= \left( \begin{array}{ccc} \displaystyle \frac{\partial f(\mathbf{y}_{i})}{\partial y_{i1}} & \cdots & \displaystyle \frac{\partial f(\mathbf{y}_{i})}{\partial y_{i,d_y}} \end{array} \right) \left( \begin{array}{c} w_{j1} \\ \vdots \\ w_{j,d_y} \end{array} \right)
\end{align}
$$
上記より、$\displaystyle \frac{\partial L}{\partial X}$は下記のように表せる。
$$
\large
\begin{align}
\frac{\partial L}{\partial X} &= \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial x_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial x_{1,d_x}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial x_{n1}} & \cdots & \displaystyle \frac{\partial L}{\partial x_{n,d_x}} \end{array} \right) \\
&= \left( \begin{array}{ccc} \displaystyle \frac{\partial f(\mathbf{y}_{1})}{\partial y_{11}} & \cdots & \displaystyle \frac{\partial f(\mathbf{y}_{1})}{\partial y_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial f(\mathbf{y}_{n})}{\partial y_{n1}} & \cdots & \displaystyle \frac{\partial f(\mathbf{y}_{n})}{\partial y_{n,d_y}} \end{array} \right) \left( \begin{array}{ccc} w_{11} & \cdots & w_{d_x,1} \\ \vdots & \ddots & \vdots \\ w_{1,d_y} & \cdots & w_{d_x,d_y} \end{array} \right) \\
&= \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial y_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial y_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial y_{n1}} & \cdots & \displaystyle \frac{\partial L}{\partial y_{n,d_y}} \end{array} \right) \left( \begin{array}{ccc} w_{11} & \cdots & w_{1,d_y} \\ \vdots & \ddots & \vdots \\ w_{d_x,1} & \cdots & w_{d_x, d_y} \end{array} \right)^{\mathrm{T}} = \frac{\partial L}{\partial Y} W^{\mathrm{T}}
\end{align}
$$
パラメータ行列による行列微分
下記のようにパラメータ行列による行列微分$\displaystyle \frac{\partial L}{\partial W}$を定義する。
$$
\large
\begin{align}
\frac{\partial L}{\partial W} = \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial w_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial w_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial w_{d_x,1}} & \cdots & \displaystyle \frac{\partial f}{\partial w_{d_x,d_y}} \end{array} \right)
\end{align}
$$
このとき、$\displaystyle \frac{\partial L}{\partial w_{jk}}$は微分の連鎖律に基づいて下記のように計算できる。
$$
\large
\begin{align}
\frac{\partial L}{\partial w_{jk}} &= \sum_{i=1}^{n} \frac{\partial f(\mathbf{y}_{i})}{\partial w_{jk}} \\
&= \sum_{i=1}^{n} \frac{\partial f(\mathbf{y}_{i})}{\partial y_{ik}} \cdot \frac{\partial y_{ik}}{\partial w_{jk}} = \sum_{i=1}^{n} \frac{\partial f(\mathbf{y}_{i})}{\partial y_{ik}} x_{ij} \\
&= \left( \begin{array}{ccc} x_{1j} & \cdots & x_{nj} \end{array} \right) \left( \begin{array}{c} \displaystyle \frac{\partial f(\mathbf{y}_{1})}{\partial y_{1k}} \\ \vdots \\ \displaystyle \frac{\partial f(\mathbf{y}_{n})}{\partial y_{nk}} \end{array} \right)
\end{align}
$$
上記より、$\displaystyle \frac{\partial L}{\partial W}$は下記のように表せる。
$$
\large
\begin{align}
\frac{\partial L}{\partial W} &= \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial w_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial w_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial w_{d_x,1}} & \cdots & \displaystyle \frac{\partial L}{\partial w_{d_x,d_y}} \end{array} \right) \\
&= \left( \begin{array}{ccc} x_{11} & \cdots & x_{n1} \\ \vdots & \ddots & \vdots \\ x_{1,d_x} & \cdots & x_{n,d_x} \end{array} \right) \left( \begin{array}{ccc} \displaystyle \frac{\partial f(\mathbf{y}_{1})}{\partial y_{11}} & \cdots & \displaystyle \frac{\partial f(\mathbf{y}_{1})}{\partial y_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial f(\mathbf{y}_{n})}{\partial y_{n1}} & \cdots & \displaystyle \frac{\partial f(\mathbf{y}_{n})}{\partial y_{n,d_y}} \end{array} \right) \\
&= \left( \begin{array}{ccc} x_{11} & \cdots & x_{1,d_x} \\ \vdots & \ddots & \vdots \\ x_{n1} & \cdots & x_{n,d_x} \end{array} \right)^{\mathrm{T}} \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial y_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial y_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial y_{n1}} & \cdots & \displaystyle \frac{\partial L}{\partial y_{n,d_y}} \end{array} \right) = X^{\mathrm{T}} \frac{\partial L}{\partial Y}
\end{align}
$$
バイアス行列による行列微分
下記のようにパラメータ行列による行列微分$\displaystyle \frac{\partial L}{\partial B}$を定義する。
$$
\large
\begin{align}
\frac{\partial L}{\partial W} = \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial b_{1}} & \cdots & \displaystyle \frac{\partial L}{\partial b_{d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial b_{1}} & \cdots & \displaystyle \frac{\partial L}{\partial b_{d_y}} \end{array} \right)
\end{align}
$$
このとき、$i$行目の$\displaystyle \frac{\partial L}{\partial b_{k}}$は微分の連鎖律に基づいて下記のように計算できる。
$$
\large
\begin{align}
\frac{\partial L}{\partial b_{k}} &= \frac{\partial L}{\partial y_{ik}} \cdot \frac{\partial y_{ik}}{\partial b_{k}} \\
&= \frac{\partial L}{\partial y_{ik}} \cdot 1 \\
&= \frac{\partial L}{\partial y_{ik}}
\end{align}
$$
上記より、$\displaystyle \frac{\partial L}{\partial B}$は下記のように表せる。
$$
\large
\begin{align}
\frac{\partial L}{\partial B} &= \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial b_{1}} & \cdots & \displaystyle \frac{\partial L}{\partial b_{d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial b_{1}} & \cdots & \displaystyle \frac{\partial L}{\partial b_{d_y}} \end{array} \right) \\
&= \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial y_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial y_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial y_{n1}} & \cdots & \displaystyle \frac{\partial L}{\partial y_{n,d_y}} \end{array} \right) = \frac{\partial L}{\partial Y}
\end{align}
$$
バイアスパラメータはベクトルで定義されるので、勾配法の計算などの際は行方向に上記の和を計算し、パラメータのUpdateを行う。次項のAffine
クラスの17
行目のself.db = np.sum(dout, axis=0)
がバイアス行列の行方向の和の計算に対応する。
実装と使用例
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
上記のクラスは下記のように用いることができる。
# x = np.array([1., 2.]) # error
x = np.array([[1., 2.]])
# x = np.array([[1., 3.], [3., 5.], [5., 7.]]) # batch
W = np.array([[1., 2., 3.], [1., 2., 3.]])
b = np.array([1., 1., 1.])
mlp_layer = Affine(W, b)
# forward
y = mlp_layer.forward(x)
# backward
dout = np.ones(y.shape)
dx = mlp_layer.backward(dout)
print(y)
print(dx)
・実行結果
[[ 4. 7. 10.]]
[[6. 6.]]
入力に与えるx
を1D
ではなく2D
で与える必要があることに注意が必要である。また、バイアス項のb
はNumPy
のブロードキャストという機能に基づいて行われていることに着目しておくと良い。
ニューラルネットワーク
誤差関数と誤差逆伝播
ニューラルネットワークでは出力層のソフトマックスを計算したのちに、交差エントロピー(Cross Entropy)誤差関数を用いて学習を行う。中間層やパラメータなどの誤差関数の勾配を誤差逆伝播法に基づいて取得し、勾配法などを用いてパラメータの推定を行う。
実装と使用例
ソフトマックス関数と交差エントロピー誤差関数
ソフトマックス関数は下記のように実装できる。
def softmax(a): # a is 2D-Array
c = np.max(a, axis=1)
exp_a = np.exp(a.T-c)
sum_exp_a = np.sum(exp_a, axis=0)
y = (exp_a / sum_exp_a).T
return y
softmax
関数の実装は「ゼロから作るDeepLearning」の$3$章の実装は1D
の配列用であるので、2D
用に改変を行った。softmax
関数は下記のように実行することができる。
x1 = np.array([[1., 2., 3.]])
x2 = np.array([[1., 2., 3.], [1., 3., 5]])
print(softmax(x1))
print("---")
print(softmax(x2))
・実行結果
[[0.09003057 0.24472847 0.66524096]]
---
[[0.09003057 0.24472847 0.66524096]
[0.01587624 0.11731043 0.86681333]]
ソフトマックス関数の演算とクロスエントロピー誤差関数の計算などの処理は下記のように実装することができる。
def cross_entropy_error(y,t):
delta = 1e-7
return -np.sum(t * np.log(y+delta))
class SoftmaxWithLoss:
def __init__(self):
self.loss = None
self.y = None
self.t = None
def forward(self, x, t):
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_error(self.y, self.t)
return self.loss
def backward(self, dout=1.):
batch_size = self.t.shape[0]
dx = (self.y - self.t) / batch_size
return dx
上記のSoftmaxWithLoss
クラスは下記のように使用できる。
x = np.array([[1., 2., 3.]]) # x is 2D-Array
t = np.array([0., 1., 0.])
calc_loss = SoftmaxWithLoss()
# forward
loss = calc_loss.forward(x,t)
# backward
dx = calc_loss.backward()
print(loss)
print(dx)
・実行結果
1.407605555828337
[[ 0.03001019 -0.25175718 0.22174699]]
ニューラルネットワーク
二層のニューラルネットワークを取り扱うにあたって、下記のようにTwoLayerNet
クラスを定義する。
from collections import OrderedDict
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
self.params = {}
self.params["W1"] = weight_init_std * np.random.randn(input_size, hidden_size)
self.params["b1"] = np.zeros(hidden_size)
self.params["W2"] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params["b2"] = np.zeros(output_size)
# generate layers
self.layers = OrderedDict()
self.layers["Affine1"] = Affine(self.params["W1"], self.params["b1"])
self.layers["Relu1"] = Relu()
self.layers["Affine2"] = Affine(self.params["W2"], self.params["b2"])
self.lastLayer = SoftmaxWithLoss()
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss(self, x, t):
y = self.predict(x)
return self.lastLayer.forward(y, t)
def calc_gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = self.lastLayer.backward(1.)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# output
grads = {}
grads["W1"] = self.layers["Affine1"].dW
grads["b1"] = self.layers["Affine1"].db
grads["W2"] = self.layers["Affine2"].dW
grads["b2"] = self.layers["Affine2"].db
return grads
上記のTwoLayerNet
を元に下記のようにニューラルネットワークの学習を行える。
np.random.seed(10)
alpha = 0.1
x = np.array([[1., 1., 1.], [1., 3., 5.]])
t = np.array([[1., 0.], [0., 1.]])
network = TwoLayerNet(input_size=3, hidden_size=5, output_size=2)
for i in range(100):
grad = network.calc_gradient(x, t)
for key in ("W1", "b1", "W2", "b2"):
network.params[key] -= alpha * grad[key]
if (i+1)%20==0:
print(softmax(network.predict(x)))
・実行結果
[[0.48243772 0.51756228]
[0.38127236 0.61872764]]
[[0.58016734 0.41983266]
[0.1216153 0.8783847 ]]
[[0.75082177 0.24917823]
[0.05060992 0.94939008]]
[[0.81603073 0.18396927]
[0.01974114 0.98025886]]
[[0.85941286 0.14058714]
[0.01267276 0.98732724]]
[…] […]