機械学習の基礎【誤差逆伝播法でニューラルネットワークのいろんなレイヤを実装する】

この記事を読むのに必要な時間は約 23 分です。

\(\require{cancel}\)

今回はニューラルネットワークのいろんなレイヤの逆伝播を計算グラフで求めて、さらに実装します。

 

扱うレイヤはRelu、Sigmoid、Affine(行列の積)、Softmaxです。

 

誤差逆伝播法の仕組みをより詳しく理解することを目的としています。

 

 

この記事を書いている僕はシステムエンジニア6年目

 

普段はJavaでWebアプリを作ったりSQL書いたり・・・、

なので最近流行りのPython、数学、人工知能、デープラーニングができる人には正直憧れています。。。。

 

 

自分も一から勉強してこの辺りできるようになりたい、、画像認識モデルを作ったりして、アプリに組み込みたい!

これが機械学習、深層学習の勉強を始めたきっかけでした。

 

 

体系的に、この分野の基礎から学ぼうとJDLAのG検定の勉強をして合格するところまでいきました。

次のステップとして、

実際にPythonでコードを書きながら機械学習や深層学習の知識を深めているところです。。。

 

 

前回は誤差逆伝播法の仕組みついて確認しました。

機械学習の基礎【誤差逆伝播法の仕組みを計算グラフで理解する】

 

今回は、

ニューラルネットワークの各レイヤの逆伝播を計算グラフで求め、

さらに実装しながら各レイヤにおける逆伝播の理解を深めます。

 

 

加算・乗算ノードの逆伝播

 

 

まず、簡単な加算・乗算ノードの逆伝播について見ていきます。

 

加算ノード

ここでは\(z = x + y\)という数式の逆伝播を見ていきます。

この数式の微分を解析的に求めると、

$$ \frac{ \partial z }{ \partial x } = 1 $$

$$ \frac{ \partial z }{ \partial y } = 1 $$

 

そのため、計算グラフで順伝播と逆伝播を表すと、

 

 

これは、逆伝播の際には、上流からきた値に1を乗算して次のノードに流すことを表しています。

つまり、加算ノードの逆伝播は次のノードにそのまま値を流します。

 

乗算ノード

次は、乗算ノードの逆伝播を見ていきます。

ここでは\(z = xy\)という数式の逆伝播を見ていきます。

この数式の微分を解析的に求めると、

$$ \frac{ \partial z }{ \partial x } = y $$

$$ \frac{ \partial z }{ \partial y } = x $$

 

そのため、計算グラフで順伝播と逆伝播を表すと、

 

 

これは、逆伝播の際には、

上流からきた値に、反対側の順伝播の値を乗算して次のノードに流すことを表しています。

 

活性化関数レイヤの逆伝播

 

 

まず、計算グラフの考え方をニューラルネットワークを構成する活性化レイヤ(ReLU、Sigmoid)に適用させます。

 

 

ReLUレイヤ

ReLUは、次の式で表されます。

$$ \begin{eqnarray}y = \begin{cases} x & ( x \gt  0) \\ 0 & ( x \leq  0) \end{cases}\end{eqnarray} $$

 

また、\(x\)に関する\(y\)の微分は、

$$ \begin{eqnarray}\frac{ \partial y }{ \partial x } = \begin{cases} 1 & ( x \gt  0) \\ 0 & ( x \leq  0) \end{cases}\end{eqnarray} $$

 

 

実装すると、

Python
class Relu:
    def __init__(self):
        self.mask = None

    def forward(self, x):
        self.mask = (x <= 0)
        out = x.copy()
        out[self.mask] = 0

        return out

    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout

        return dx

 

Reluクラスは、インスタンス変数としてmaskという変数を持ち、

順伝播(forward)の際、x(NumPy配列)の0以下の箇所を保持しておきます。

逆伝播(backward)の際、maskを使って0で値を更新して伝播します。

 

Sigmoidレイヤ

Sigmoidは、次の式で表されます。

$$y = \frac{1}{1 + exp(-x)}$$

 

計算グラフで表すと、

 

 

それでは逆伝播を求めていきます。

 

ステップ1

「/」ノードは\(y=\frac{1}{x}\)を表し、この微分は解析的に次の式で表されます。

$$ \frac{ \partial y }{ \partial x } = – \frac{1}{x^{2}} = -y^{2} $$

そのため、逆伝播のときは、上流の値に対して、\(-y^{2}\)(順伝播の出力の2乗にマイナスを付けた値)を乗算して下流へ伝播します。

 

 

 

ステップ2

「+」ノードは上流の値を下流にそのまま流すだけです。

 

 

ステップ3

「exp」ノードは\(y = exp(x)\)を表し、その微分は、

$$ \frac{ \partial y }{ \partial x } = exp(x) $$

上流の値に対して\(exp(x)\)を乗算して下流へ伝播します。(例では順伝播の値が\(-x\)なので\(exp(-x)を乗算\))

 

 

ステップ4

「×」ノードは順伝播の値をひっくり返して乗算します。

そのためここでは-1を乗算します。

 

 

以上で、計算グラフでSigmoidの逆伝播の出力\( \frac{ \partial L }{ \partial y } y^{2}exp(-x) \)

を求めることができました。

また、式を変形すると、

\(y = y = \frac{1}{1 + exp(-x)}\)より、

 

$$ \frac{ \partial L }{ \partial y } y^{2}exp(-x) = \frac{ \partial L }{ \partial y } \frac{1}{(1 + exp(-x))^{2}}exp(-x)$$

$$ = \frac{ \partial L }{ \partial y } \frac{1}{(1 + exp(-x))}\frac{exp(-x)}{(1 + exp(-x))}$$

 

\(1-y = \frac{1 + exp(-x)}{1 + exp(-x)} – \frac{1}{1 + exp(-x)} = \frac{exp(-x)}{1 + exp(-x)}\)より、

 

$$ = \frac{ \partial L }{ \partial y } y(1-y)$$

 

と整理することができます。

 

逆伝播は順伝播の入力\(x\)と出力\(y\)だけで計算できるので、

以下のように計算グラフのノードをSigmoidにまとめることもできます。

 

 

実装すると、

Python
class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x):
        out = sigmoid(x)
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out

        return dx

 

Sigmoidの実装では、

逆伝播の時に順伝播の出力を利用するのでインスタンス変数のoutに保持しています。

 

 

Affine・Softmaxレイヤの逆伝播

 

 

次はAffineレイヤとSoftmaxレイヤの逆伝播です。

 

Affineレイヤ

Affineレイヤは、

ニューラルネットワークの重み付き信号の総和を計算する行列の積です。

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

\(\mathbf{X}\)は入力、\(\mathbf{W}\)は重み、\(\mathbf{B}\)はバイアス、

\(\mathbf{Y}\)は出力の行列をそれぞれ表しています。

 

ここでは、以下のようなAffineレイヤについて考えます。

 

 

これを計算グラフにすると、、

(かっこ内は行列の形状を記載しています。また(2,)は(1, 2)のことです。)

 

 

まず、\(W^{T}\)や\(X^{T}\)は転置を表します。

数式で表すと、

$$ \mathbf{W} = \begin{pmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \end{pmatrix} $$

$$ \mathbf{W}^{T} = \begin{pmatrix} w_{11} & w_{21} \\ w_{12} & w_{22} \\ w_{13} & w_{23} \end{pmatrix} $$

 

また、上の計算グラフでは、各変数の形状(かっこ内に記載)に注意する必要があり、

特に、\( \mathbf{X}\)と\( \frac{ \partial L }{ \partial \mathbf{X} }\)、\( \mathbf{W}\)と\( \frac{ \partial L }{ \partial \mathbf{W} }\)

は同じ形状になります。

$$\mathbf{X} = (x_0, x_1, x_2, \cdot \cdot \cdot , x_n)$$

$$\frac{ \partial L }{ \partial \mathbf{X} } = (\frac{ \partial L }{ \partial x_0 }, \frac{ \partial L }{ \partial x_1 }, \frac{ \partial L }{ \partial x_2 }, \cdot \cdot \cdot , \frac{ \partial L }{ \partial x_n })$$

 

各変数に注目することで、

例えば、\(\frac{ \partial L }{ \partial \mathbf{Y} }\)の(3,)と\(\mathbf{X}\)の(2,)から、

(3,)(?, ?) = (2,)つまり、(3,2)の形状で\(\mathbf{W}\)を掛けないといけない。

\(\mathbf{W}\)は(2, 3)の形状なので転置の\(\mathbf{W}^{T}\)と、

\(\frac{ \partial L }{ \partial \mathbf{X} }\)の値を考えることもできるのです。

 

 

先ほど考えたAffineレイヤは入力の\(\mathbf{X}\)は1つのデータを対象としたものでしたが、

N個のデータをまとめて伝播するバッチ版を考えると、

\(\mathbf{X}\)の形状は(N, 2)となり、計算グラフは以下のようになります。

 

 

データをN個扱うので流れるデータの形状が異なる点が、先ほどの計算グラフと異なります。

 

ただ、バイアスの逆伝播のところは分かりにくいと思いますので、、、

例えば、上の計算グラフのN=2の順伝播で、

\(\mathbf{X} \cdot \mathbf{W} = \begin{pmatrix} 0 & 0 & 0 \\ 10 & 10 & 10 \end{pmatrix}\)、\(\mathbf{B} = \begin{pmatrix} 1 & 2 & 3 \end{pmatrix} \)を考えます。

すると、

\(\mathbf{X} \cdot \mathbf{W} + \mathbf{B} = \begin{pmatrix} 1 & 2 & 3 \\ 11 & 12 & 13 \end{pmatrix}\)

となります。つまりN個分バイアスが足されるのです。

 

 

そのため逆伝播ではN個分のバイアスの要素を足し合わせる必要があるのです。

具体的な例で考えると、

\(\frac{ \partial L }{ \partial \mathbf{Y} } = \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}\)

この時、\(\frac{ \partial L }{ \partial \mathbf{B}} = \begin{pmatrix} 5 & 7 & 9 \end{pmatrix}\)

となります。つまり各行を列の方向に足し合わせています。

(上の計算グラフの図で最初の軸の和とか第0軸の和と表現していたのはこれのことです)

 

長くなりましたが、、、

このバッチ版Affineレイヤを実装すると、

Python

class Affine:
    def __init__(self, W, b):
        self.W =W
        self.b = b
        
        self.x = None
        self.original_x_shape = None
        # 重み・バイアスパラメータの微分
        self.dW = None
        self.db = None

    def forward(self, x):
        self.x = x
        out = np.dot(self.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

 

Softmax-with-Lossレイヤ

最後はニューラルネットワークの出力層であるソフトマックス関数です。

ソフトマックス関数についての詳細は以下にまとめてあります。

機械学習の基礎まとめ【活性化関数(ステップ、シグモイド、ReLU、ソフトマックス)】

 

今回はソフトマックスレイヤだけでなく、損失関数である交差エントロピー誤差も含めた、

Softmax-with-Lossレイヤを実装します。

交差エントロピー誤差についての詳細は以下にまとめてあります。

機械学習の基礎まとめ【損失関数(2乗和誤差、交差エントロピー誤差)】

 

計算グラフの詳細は複雑ですが、、

簡略化した以下のグラフで表すことができます。

 

 

上の図の例では、

まずSoftmax関数は3クラス分類を想定しています。

\((a_{1}, a_{2}, a_{3})\)は入力、\((y_{1}, y_{2}, y_{3})\)はSoftmax関数の出力、\((t_{1}, t_{2}, t_{3})\)は教師ラベル(正解の値)を表しています。

つまり、Softmaxレイヤからの逆伝播ではSoftmax関数の出力と教師ラベル(正解の値)との差(\((y_{1} – t_{1}, y_{2} – t_{1}, y_{3} – t_{1})\))が流れます。

 

ニューラルネットワークの学習の目的は、

ニューラルネットワークの出力(ここではSoftmax関数の出力)を教師ラベルに近づけるように重みパラメータを調整することです。

そのため、ニューラルネットワークの出力と教師ラベルとの誤差を効率良く前のレイヤに伝える必要がありますが、

まさに上の図のSoftmax-with-Lossレイヤでは逆伝播でそれを伝えています。

 

実装すると、

Python
class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None # softmaxの出力
        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

 

まとめ

 

 

今回はニューラルネットワークのいろんなレイヤの順伝播と逆伝播について仕組みを確認しました。

今回実装した各レイヤを組み合わせることで、

複雑なニューラルネットワークを作成することができ、

誤差逆伝播法で学習して推論できます。

 

次回は、

今回実装した色々なレイヤを組み合わせて、

勾配降下法で実装したようなニューラルネットワークを誤差逆伝播法バージョンで作成してみます。

 

参考にした資料

 

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください