ディープラーニングを基本から学ぶ　Part3 誤差逆伝播法でニューラルネットワークの学習を高速化する

◆前章：[Part2 ニューラルネットワークの学習](http://qiita.com/nvtomo1029/items/70eab5c770811520784c)
◆次章：[Part4 学習に関するテクニック](http://qiita.com/nvtomo1029/items/6d57ff25fbddf76c7651)

---

##**■3.誤差逆伝播法でニューラルネットワークの学習を高速化する**
勾配の計算を効率よく行える手法として用いられる。
誤差逆伝播法を用いることで、損失関数の微分を高速に計算することができる。

層の値を入力層から出力層に向かって順番に計算していく過程は「順伝播」と呼ばれるが、それに対して、偏微分(局所的な微分)を出力層から入力層に向かって順番に計算していく過程を「逆伝播」と呼ぶ。
そして、損失関数を”誤差”と見立てて、逆伝播によって重みとバイアスを修正していくアルゴリズムが「誤差逆伝播法」である。

逆伝播のような局所的な微分を伝達する原理は連鎖率によるものである。

・連鎖率の原理
　ある関数が合成関数であらわされる場合、その合成関数の微分は、合成関数を構成するそれぞれの関数の微分の積によって表すことができる。

###**▼3.1.誤差逆伝播法をニューラルネットワークに実装する**

####**▽3.1.1.活性化関数レイヤ**

**[1]ReLuレイヤ**

```py3
# coding: utf-8
import numpy as np

# Reluレイヤクラス
class Relu:
    def __init__(self):
        self.mask = None # 順伝播の結果をmaskとして保持

    # 順伝播
    def forward(self, x):
        # 入力値が0以下:True/0より大きい:Falseとしてmask値を保持
        self.mask = (x <= 0)
        out = x.copy()
        # x <= 0の場合は出力値を0にする(x > 0なら入力値のまま)
        out[self.mask] = 0
        return out

    # 逆伝播
    def backward(self, dout):
        # 順伝播での入力値が0以下の場合、逆伝播の出力値を0にする
        # (順伝播での入力値 > 0なら逆伝播の入力値をそのまま出力する)
        dout[self.mask] = 0
        dx = dout
        return dx
```


**[2]Sigmoidレイヤ**

```py3
# coding: utf-8
import numpy as np

# Sigmoidレイヤクラス
class Sigmoid:
    def __init__(self):
        self.out = None

    # 順伝播
    def forward(self, x):
        out = 1 / (1 + np.exp(-x))
        self.out = out # 逆伝播で使うので出力値を保持
        return out

    # 逆伝播
    def backward(self, dout):
        dx = dout * self.out * (1.0 - self.out)

        return dx
```




####**▽3.1.2.Affineレイヤ**
Affineレイヤとは、これまでニューラルネットワークの順伝播で行ってきた重み付き信号の総和の計算のことである。

```py3
# coding: utf-8
import numpy as np

# 入力信号
X = np.array([1.0, 0.5])
# 重み
W = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
# バイアス
B = np.array([0.1, 0.2, 0.3])
# 計算
Y = np.dot(X, W) + B
```

ニューラルネットワークの順伝播で行っている行列の内積を幾何学の分野ではAffine変換と呼ぶ。

```py3
# coding: utf-8
import numpy as np

# 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(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
```




####**▽3.1.3.Softmax-with-Lossレイヤ**

出力層であるソフトマックス関数に損失関数 交差エントロピー誤差を含めたものである。

```py3
# coding: utf-8
import numpy as np

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]
        if self.t.size == self.y.size: # 教師データがone-hot-vector(ex. [0, 1, 0, 0, 0])の場合
            # 入力値の微分
            dx = (self.y - self.t) / batch_size
        else:
            # 入力値の微分
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size
        
        return dx

```

※ソフトマックス関数、交差エントロピー誤差関数の実装は、前章に記載

####**▽3.1.4.誤差逆伝播法で実装した2層ニューラルネットワーク**

この章のまとめとして、前の章で実装した2層ニューラルネットワークを誤差逆伝播法に対応させる。
ニューラルネットワークの構成要素を”レイヤ”として実装することで、”レイヤ”の伝播のみで認識処理や学習に必要な勾配を求めることができるようになる。

```py3
# coding: utf-8
import numpy as np
from collections import OrderedDict

# 2層ニューラルネットワークのクラス
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)

        # レイヤの生成
        self.layers = OrderedDict() # 順序付きディクショナリ
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1']) # 一層目(Affineレイヤ)
        self.layers['Relu1'] = Relu() # 活性化関数(Relu)
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2']) # 二層目(Affineレイヤ)

        self.lastLayer = SoftmaxWithLoss() # 出力層(Softmax with Lossレイヤ)
        
    # 認識(推論)を行う
    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        
        return x
        
    # 損失関数の値を求める
    # x:入力データ, t:教師データ
    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)
    
    # 認識精度を求める
    def accuracy(self, x, t):
        # 認識(推論)を行う
        y = self.predict(x)
        # 認識(推論)結果の正解を取得
        y = np.argmax(y, axis=1)
        # 教師データの正解を取得
        if t.ndim != 1 : t = np.argmax(t, axis=1)
        
        # 認識(推論)結果と教師データの正解が一致している率を求める
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
        
    # 重みパラメータに対する勾配を求める
    def gradient(self, x, t): # x:入力データ, t:教師データ
        # 順伝播
        self.loss(x, t)

        # 逆伝播
        dout = 1
        dout = self.lastLayer.backward(dout) # 出力層から逆伝播
        
        # 入力層を逆順にし、後ろから逆伝播していく
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 求められた勾配値を戻り値にセット
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads
```

クラス内で呼ばれている関数は、これまでの説明で出てきているので省略。

データを順伝播、逆伝播することで、重みパラメータの勾配を効率的に求めることができる。
これにより、前章で実装した数値微分によって勾配を求める方法とは比べ物にならないパフォーマンスが出る。

また、入力層、出力層、活性化関数と言ったニューラルネットワークの構成要素を”レイヤ”としてモジュール化したことで、そのモジュールの組み合わせ次第で様々なニューラルネットワークを作れるようになる。

##**■参考文献**
[ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装](https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E4%BD%9C%E3%82%8BDeep-Learning-%E2%80%95Python%E3%81%A7%E5%AD%A6%E3%81%B6%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%83%A9%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0%E3%81%AE%E7%90%86%E8%AB%96%E3%81%A8%E5%AE%9F%E8%A3%85-%E6%96%8E%E8%97%A4-%E5%BA%B7%E6%AF%85/dp/4873117585/ref=cm_cr_arp_d_product_top?ie=UTF8)