深層学習／ゼロから作るDeep Learning 第５章メモ

#1.はじめに
　名著、**「ゼロから作るDeep Learning」**を読んでいます。今回は第５章のメモ。
　コードの実行はGithubからコード全体をダウンロードし、ch05の中で jupyter notebook にて行っています。

#2.誤差逆伝播の実装（レイヤー生成なし）
　第５章の最後に、レイヤーを生成して誤差逆伝播を行うコード( twoLayerNet.py & train_neuralnet.py )がありますが、その前にレイヤーを生成せず誤差伝播を行うコードが既に第４章にある(同じく twoLayerNet.py & train_neuralnet.py )ので、そちらからやってみます。

　第４章でやった様に、見通しを良くするために、1つのコードにまとめてみると、前回とほとんど同じで、--- 勾配計算 --- のところが異なるだけです。とりあえず、動かしてみます。

```python
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
from common.functions import *  # common フォルダーの function.py にある関数を全て使えるように設定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist

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)

    # 順伝播    
    def predict(self, x):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
    
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)        
        return y
        
    # ロス計算
    def loss(self, x, t):
        y = self.predict(x)
        
        return cross_entropy_error(y, t)

    # 精度計算
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)
        
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

    # -----------------  勾配計算  -------------------
    def gradient(self, x, t):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}
        
        batch_num = x.shape[0]
        
        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)
        
        dz1 = np.dot(dy, W2.T)
        da1 = sigmoid_grad(a1) * dz1
        grads['W1'] = np.dot(x.T, da1)
        grads['b1'] = np.sum(da1, axis=0)
        return grads
    # ------------------------------------------------

# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

# TwoLayerNet をインスタンス化
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

# 初期設定
iters_num = 10000  # 実行回数
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
train_loss_list, train_acc_list, test_acc_list = [], [], []
iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 勾配の計算
    grad = network.gradient(x_batch, t_batch)
    
    # パラメータの更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]
    
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)

    # 精度表示
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

# グラフの描画
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
```
![スクリーンショット 2020-04-30 18.23.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/6f4216f5-620f-cdd6-0ebc-2a6242b1a5c4.png)
　素晴らしい！数値微分と比べると、実行速度が天と地ほども違います。激早です！（というか、数値微分が激遅か）

　コードの内容は、--- 勾配計算 --- 以外は、第４章メモで説明済みですので、それをご覧になりたい方は[第4章メモ](https://qiita.com/jun40vn/items/be171ff7626d370072d1)をご参照下さい。この章では、勾配計算の部分のみ説明します。

#3.勾配計算
　勾配計算のコードの一部を計算グラフで説明すると、こんな感じです。
![スクリーンショット 2020-05-02 19.34.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/1e01b058-f53d-a907-b4d7-c634ee9eb4b8.png)
　`grads['b2'] = np.sum(dy, axis=0)`とsumを取っているのは、バッチ処理への対応です。それにしても、一見複雑に思える誤差逆伝播が行列演算に置き換えられることに感動を覚えますね。

#4.誤差逆伝播式の導出
　まず、$\frac{\partial L}{\partial W2}=z1^{T}*dy$ を導出します。連鎖率によって、
![スクリーンショット 2020-05-02 16.39.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/f38377f8-2965-8eb0-2e5f-6b1d884e8f85.png)

　次に、$\frac{\partial y}{\partial Z1}=\frac{\partial y}{\partial a2}W2^T$ を導出します。

![スクリーンショット 2020-05-02 18.45.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/d4bdadb5-863a-59e9-e62a-9797c029b494.png)
　数値微分の世界から行列演算による誤差逆伝播の世界への転換は正に革新的なことですよね。

#5.レイヤーの生成
　先程の様に、順伝播・誤差逆伝播とも行列演算式をダイレクトにコードに書く方法でも実用的な実行速度が得られますが、コードを書くのがちょっと面倒ですよね。そこで、レイヤーを生成して、もっと簡略にコードを記載する方法があります。

　レイヤーを生成するためには、まず `from collections import OrderedDict`で、 OrderedDictをインポートします。

```python
class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size, 
        # ................
        # レイヤの生成
        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()
```
　クラスTwoLayerNetの初期設定時に、`self.layers = OrderedDict()`で OrderedDictをインスタンス化します。

　OrderedDict は順番を含めて覚えるので、辞書 `self.layers` に、`Affine1`, `Relu1`,`Affine2`とレイヤー名と処理を順次登録すると、その順番も含めて記憶します。

　最終層だけは、誤差逆伝播式が異なるので、`self.lastLayer` として別に扱います。

```Python
    # 順伝播
    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)
```
　そうすると、順伝播 `predict` は、辞書 `self.layers` からレイヤー名を順番に１つづつ読み出して、`forward` 処理を実行することを繰り返すだけなので、レイヤーの数がいくつであろうと、このコードだけで済みます。

　順伝播・ロス計算 `loss` も、順伝播して `self.lastLayer.forward(y, t)`で最終層の `forward` 処理をするだけなので、最終層の処理内容が変わっても、このコードのままです。

```python
   # 勾配計算 
   def gradient(self, x, t):
        # 順伝播・ロス計算
        self.loss(x, t)

        # 誤差逆伝播
        dout = 1
        dout = self.lastLayer.backward(dout)  # 最終層のbackwardの結果をdoutへ代入        
        layers = list(self.layers.values())  # 辞書self.layersからレイヤー名を読む込む
        layers.reverse()  # レイヤー名の順番を逆転
        for layer in layers:  # 逆転したレイヤー名を読み込む
            dout = layer.backward(dout)  # レイヤー名のbackwardを実行

        # ..................
        return grads
```
　そして、勾配計算も同様に、レイヤーの組み合わせが変わっても同じで済みます。これは便利ですよね。

　それでは、レイヤー生成ありのコードを実行してみましょう。

#6.誤差逆伝播の実装（レイヤー生成あり）
　先程と同様、コードの見通しを良くするために、`twoLayerNet.py` と `train_neuralnet.py` を合体した形にします。また、グラフの描画部分を追加しています。

```python
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
from common.layers import *  # common フォルダーのlayers.py にある関数を全て使えるように設定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from collections import OrderedDict  # 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)

        # レイヤの生成
        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 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):
        # forward
        self.loss(x, t)

        # backward
        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


# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

# TwoLayerNetをインスタンス化
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

# 初期設定
iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
train_loss_list, train_acc_list, test_acc_list = [], [], []
iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 勾配の計算
    grad = network.gradient(x_batch, t_batch)
    
    # パラメータの更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]
    
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
    # 精度表示
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print(train_acc, test_acc)

# グラフの描画
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epoch")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
```
![スクリーンショット 2020-05-03 16.05.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/d33c7ffe-72a7-9348-8d8e-044f001e0656.png)
　先程のレイヤー生成無しの場合と比べ精度が＋３ポイントほど改善しています。この理由は、もちろんレイヤー生成したからではありません。活性化関数が `sigmoid` から `ReLU` に変更されたためです。