ゼロから作るDeepLearning 8章メモ（その１）

# ゼロから作るDeepLearning 8章メモ（その１）

### ソースコード：deep-learning-from-scratch/ch08/deep_convnet.py

```python
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import pickle
import numpy as np
from collections import OrderedDict
from common.layers import *


class DeepConvNet:
    """認識率99%以上の高精度なConvNet

    ネットワーク構成は下記の通り
        conv - relu - conv- relu - pool -
        conv - relu - conv- relu - pool -
        conv - relu - conv- relu - pool -
        affine - relu - dropout - affine - dropout - softmax
    """
    def __init__(self, input_dim=(1, 28, 28),
                 conv_param_1 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_2 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_3 = {'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_4 = {'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1},
                 conv_param_5 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_6 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
                 hidden_size=50, output_size=10):
        # 重みの初期化===========
        # 各層のニューロンひとつあたりが、前層のニューロンといくつのつながりがあるか（TODO:自動で計算する）
        pre_node_nums = np.array([1*3*3, 16*3*3, 16*3*3, 32*3*3, 32*3*3, 64*3*3, 64*4*4, hidden_size])
        weight_init_scales = np.sqrt(2.0 / pre_node_nums)  # ReLUを使う場合に推奨される初期値
        
        self.params = {}
        pre_channel_num = input_dim[0]
        for idx, conv_param in enumerate([conv_param_1, conv_param_2, conv_param_3, conv_param_4, conv_param_5, conv_param_6]):
            self.params['W' + str(idx+1)] = weight_init_scales[idx] * np.random.randn(conv_param['filter_num'], pre_channel_num, conv_param['filter_size'], conv_param['filter_size'])
            self.params['b' + str(idx+1)] = np.zeros(conv_param['filter_num'])
            pre_channel_num = conv_param['filter_num']
        self.params['W7'] = weight_init_scales[6] * np.random.randn(64*4*4, hidden_size)
        self.params['b7'] = np.zeros(hidden_size)
        self.params['W8'] = weight_init_scales[7] * np.random.randn(hidden_size, output_size)
        self.params['b8'] = np.zeros(output_size)

        # レイヤの生成===========
        self.layers = []
        self.layers.append(Convolution(self.params['W1'], self.params['b1'], 
                           conv_param_1['stride'], conv_param_1['pad']))
        self.layers.append(Relu())
        self.layers.append(Convolution(self.params['W2'], self.params['b2'], 
                           conv_param_2['stride'], conv_param_2['pad']))
        self.layers.append(Relu())
        self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
        self.layers.append(Convolution(self.params['W3'], self.params['b3'], 
                           conv_param_3['stride'], conv_param_3['pad']))
        self.layers.append(Relu())
        self.layers.append(Convolution(self.params['W4'], self.params['b4'],
                           conv_param_4['stride'], conv_param_4['pad']))
        self.layers.append(Relu())
        self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
        self.layers.append(Convolution(self.params['W5'], self.params['b5'],
                           conv_param_5['stride'], conv_param_5['pad']))
        self.layers.append(Relu())
        self.layers.append(Convolution(self.params['W6'], self.params['b6'],
                           conv_param_6['stride'], conv_param_6['pad']))
        self.layers.append(Relu())
        self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
        self.layers.append(Affine(self.params['W7'], self.params['b7']))
        self.layers.append(Relu())
        self.layers.append(Dropout(0.5))
        self.layers.append(Affine(self.params['W8'], self.params['b8']))
        self.layers.append(Dropout(0.5))
        
        self.last_layer = SoftmaxWithLoss()

    def predict(self, x, train_flg=False):
        for layer in self.layers:
            if isinstance(layer, Dropout):
                x = layer.forward(x, train_flg)
            else:
                x = layer.forward(x)
        return x

    def loss(self, x, t):
        y = self.predict(x, train_flg=True)
        return self.last_layer.forward(y, t)

    def accuracy(self, x, t, batch_size=100):
        if t.ndim != 1 : t = np.argmax(t, axis=1)

        acc = 0.0

        for i in range(int(x.shape[0] / batch_size)):
            tx = x[i*batch_size:(i+1)*batch_size]
            tt = t[i*batch_size:(i+1)*batch_size]
            y = self.predict(tx, train_flg=False)
            y = np.argmax(y, axis=1)
            acc += np.sum(y == tt)

        return acc / x.shape[0]

    def gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.last_layer.backward(dout)

        tmp_layers = self.layers.copy()
        tmp_layers.reverse()
        for layer in tmp_layers:
            dout = layer.backward(dout)

        # 設定
        grads = {}
        for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
            grads['W' + str(i+1)] = self.layers[layer_idx].dW
            grads['b' + str(i+1)] = self.layers[layer_idx].db

        return grads

    def save_params(self, file_name="params.pkl"):
        params = {}
        for key, val in self.params.items():
            params[key] = val
        with open(file_name, 'wb') as f:
            pickle.dump(params, f)

    def load_params(self, file_name="params.pkl"):
        with open(file_name, 'rb') as f:
            params = pickle.load(f)
        for key, val in params.items():
            self.params[key] = val

        for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
            self.layers[layer_idx].W = self.params['W' + str(i+1)]
            self.layers[layer_idx].b = self.params['b' + str(i+1)]

```

## 解説

### 【1】初期化

```python
def __init__(self, input_dim=(1, 28, 28),
             conv_param_1 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
             conv_param_2 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
             conv_param_3 = {'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1},
             conv_param_4 = {'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1},
             conv_param_5 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
             conv_param_6 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
             hidden_size=50, output_size=10):
```

* 入力データ：チャネル数1, サイズ28×28
* 畳込み層①：フィルター数16, サイズ3×3, パディング1, スライド1
* 畳込み層②：フィルター数16, サイズ3×3, パディング1, スライド1
* 畳込み層③：フィルター数32, サイズ3×3, パディング1, スライド1
* 畳込み層④：フィルター数32, サイズ3×3, パディング1, スライド1
* 畳込み層⑤：フィルター数64, サイズ3×3, パディング1, スライド1
* 畳込み層⑥：フィルター数64, サイズ3×3, パディング1, スライド1
* 全結合層：ノード数50
* 出力層：ノード数10
<br>

```python
pre_node_nums = np.array([1*3*3, 16*3*3, 16*3*3, 32*3*3, 32*3*3, 64*3*3, 64*4*4, hidden_size])
```
各層における前層ノード数（チャネル数×フィルターサイズ縦×フィルターサイズ横）を計算し、リストに格納する。
ここでいう前層ノード数とは、フィルターを通される側のデータをブロックで考えた際の1回分の適応領域を指す。
例えば、畳込み層①であれば、
フィルター適応領域：チャネル数1×フィルターサイズ縦3×フィルターサイズ3
畳込み層②であれば、
フィルター適応領域：チャネル数16×フィルターサイズ縦3×フィルターサイズ3

＜畳込み層②のフィルター適応領域算出＞
畳込み層①の、
input:(1, 28, 28)
フィルター：(16, 1, 3, 3)
OH = (28 + -3) / 1 + 1 = 26
OW = (28 + -3) / 1 + 1 = 26
であるため、
| inputの行列サイズ | im2col後の行列のサイズ |
| ---------------- | -------------------- |
| (1, 28, 28)      | (26×26, 9)           |

im2col後の行列 × フィルターを縦方向に１列に展開して並べた行列
(26×26, 9) × (9, 16) 
= (26×26, 16)
reshape((26×26, 16)) = (16, 26, 26)

畳込み層①の出力データは、(16, 26, 26)の行列となる。
よって、畳込み層②のフィルター適応領域は
チャネル数16×フィルターサイズ縦3×フィルターサイズ3 となる。
<br>

```python
weight_init_scales = np.sqrt(2.0 / pre_node_nums)  # ReLUを使う場合に推奨される初期値
```
ReLuに特化した初期値「Heの初期値」の計算に必要な標準偏差の値を求める。
「Heの初期値」は前層ノードをn個としたとき、√(2/n)を標準偏差とするガウス分布。
<br>

```python
for idx, conv_param in enumerate([conv_param_1, conv_param_2, conv_param_3, conv_param_4, conv_param_5, conv_param_6]):
    self.params['W' + str(idx+1)] = weight_init_scales[idx] * np.random.randn(conv_param['filter_num'], pre_channel_num, conv_param['filter_size'], conv_param['filter_size'])
    self.params['b' + str(idx+1)] = np.zeros(conv_param['filter_num'])
    pre_channel_num = conv_param['filter_num']
```
以下を、畳込み層の数だけループ処理する。

1. フィルターの重み`self.params['W' + str(idx+1)]`の算出し、辞書`params`に格納する。
`np.random.randn()`はガウス分布からランダムに値を出力する関数。
よって、`self.params['W' + str(idx+1)]`=「Heの初期値」となる。

2. バイアス`self.params['b' + str(idx+1)]`の算出し、辞書`params`に格納する。
`np.zeros(conv_param['filter_num'])`はフィルター数の大きさでゼロベクトルを作成している。

3. 前層ノードのチャネル数を格納している変数`pre_channel_num`の値を、フィルター数で上書きする。
<br>

```python
self.params['W7'] = weight_init_scales[6] * np.random.randn(64*4*4, hidden_size)
self.params['b7'] = np.zeros(hidden_size)
self.params['W8'] = weight_init_scales[7] * np.random.randn(hidden_size, output_size)
self.params['b8'] = np.zeros(output_size)
```
畳込み層以外の重みとバイアスを算出し、辞書`params`に格納する。
<br>

### 【2】レイヤの作成
ネットワーク構成は下記の通り

```
    conv - relu - conv- relu - pool -
    conv - relu - conv- relu - pool -
    conv - relu - conv- relu - pool -
    affine - relu - dropout - affine - dropout - softmax
```
説明省略
<br>

### 【3】推論

```python
def predict(self, x, train_flg=False):
    for layer in self.layers:
        if isinstance(layer, Dropout):
            x = layer.forward(x, train_flg)
        else:
            x = layer.forward(x)
    return x
```
`layer.forward`で前層の出力データを入力データとし、次の層に渡す出力データを作成している。
Dropoutレイヤのときは、引数として、`train_flg`を指定する。
※`train_flg=False`の時は、Dropoutしない
<br>

### 【4】損失関数を求める

```python
def loss(self, x, t):
    y = self.predict(x, train_flg=True)
    return self.last_layer.forward(y, t)
```
`predict(self, x, train_flg=False)`で導き出した値`y`と、正解ラベル`t`を引数として、
`self.last_layer.forward(y, t)`によって損失関数を導き出す。

ここで、`last_layer`は引数`y`をソフトマックス関数に通し、出力された値と正解ラベル`t`とで交差エントロピー誤差を計算する`SoftmaxWithLoss()`であることに注意する。
<br>

### 【5】正解率を求める

```python
def accuracy(self, x, t, batch_size=100):
        if t.ndim != 1 : t = np.argmax(t, axis=1)

        acc = 0.0

        for i in range(int(x.shape[0] / batch_size)):
            tx = x[i*batch_size:(i+1)*batch_size]
            tt = t[i*batch_size:(i+1)*batch_size]
            y = self.predict(tx, train_flg=False)
            y = np.argmax(y, axis=1)
            acc += np.sum(y == tt)

        return acc / x.shape[0]
```
1. `if t.ndim != 1 : t = np.argmax(t, axis=1)`で正解ラベルが1次元でなければ、軸に対して最も大きな値を取得し、1次元に成形する。

2. バッチサイズごとに正解数を計算し、合算する。
`int(x.shape[0] / batch_size))`：入力データ数 / バッチサイズ
`x[i*batch_size:(i+1)*batch_size]`：入力データのリストの「i×バッチサイズ～(i+1)×バッチサイズ」番目
`t[i*batch_size:(i+1)*batch_size]`：正解ラベルのリストの「i×バッチサイズ～(i+1)×バッチサイズ」番目
`acc`：正解数

3. 正解数を返す
`acc / x.shape[0]`：正解率
<br>

### 【6】勾配計算

```python
def gradient(self, x, t):
    # forward
    self.loss(x, t)

    # backward
    dout = 1
    dout = self.last_layer.backward(dout)

    tmp_layers = self.layers.copy()
    tmp_layers.reverse()
    for layer in tmp_layers:
        dout = layer.backward(dout)

    # 設定
    grads = {}
    for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
       grads['W' + str(i+1)] = self.layers[layer_idx].dW
       grads['b' + str(i+1)] = self.layers[layer_idx].db

    return grads
```

1. 順伝播し、損失関数を求める

2. 逆伝播し、各パラメータで損失関数を偏微分した値を求める
`dout = layer.backward(dout)`で局所的な微分の値が計算される。

3. 辞書`grads`勾配を格納する
`enumerate((0, 2, 5, 7, 10, 12, 15, 18))`は、`layers`リストの中で、パラメータが存在する層のインデックスを指定している。
<br>

### 【7】ハイパーパラメータの保存と取り出し
```python
    def save_params(self, file_name="params.pkl"):
        params = {}
        for key, val in self.params.items():
            params[key] = val
        with open(file_name, 'wb') as f:
            pickle.dump(params, f)

    def load_params(self, file_name="params.pkl"):
        with open(file_name, 'rb') as f:
            params = pickle.load(f)
        for key, val in params.items():
            self.params[key] = val

        for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
            self.layers[layer_idx].W = self.params['W' + str(i+1)]
            self.layers[layer_idx].b = self.params['b' + str(i+1)]
```
`save_params(self, file_name="params.pkl")`で学習後のハイパーパラメータをファイルに保存する。

`load_params(self, file_name="params.pkl")`でファイルに保存されているハイパーパラメータを取り出し初期値とする。
（学習後のパラメータをそのまま使って推論がすぐにできる）