自動微分・誤差逆伝播の理論と応用②:Affine変換の自動微分とニューラルネットワークの実装

自動微分(Automatic Differentiation)は大規模なニューラルネットワークであるDeepLearningの学習における誤差逆伝播などに用いられる手法です。当記事ではAffine変換の自動微分とニューラルネットワーク(Neural Network)の実装について取りまとめました。
作成にあたっては「ゼロから作るDeep Learning」の第$5$章「誤差逆伝播法」の内容を主に参照しました。

・用語/公式解説
https://www.hello-statisticians.com/explain-terms

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.]]

入力に与えるx1Dではなく2Dで与える必要があることに注意が必要である。また、バイアス項のbNumPyのブロードキャストという機能に基づいて行われていることに着目しておくと良い。

ニューラルネットワーク

誤差関数と誤差逆伝播

ニューラルネットワークでは出力層のソフトマックスを計算したのちに、交差エントロピー(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]]

「自動微分・誤差逆伝播の理論と応用②:Affine変換の自動微分とニューラルネットワークの実装」への1件の返信

コメントは受け付けていません。