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

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

#2.２層ニューラルネットワークの学習
　第４章の最後に、2層ニューラルネットワークを数値微分によってパラメータの勾配を計算する方法で学習するコード ( `train_neuralnet.py` ) があります。今回は、このコードを実行してから、細部を確認して行きます。但し、Githubのコードそのままではなく、下記の様に一部修正・追加します。

**１）実行速度が遅いことへの対応**
　`数値微分`を使うと莫大な時間が掛かるのにも関わらず精度表示が 600 iter 毎(1 epoch 毎)では、2回目の精度計算の表示が数時間後くらいになってしまいます。そこで、1時間ちょっとで結果が分かるように、`精度表示を 1 iter 毎`にし、`実行回数を10000から100`にし、`学習率を0.1から1.0`にします。

**２）外部呼び出しコードを減らして見通しよく**
　ゼロ作の特徴は、既に説明したコードは極力外部呼び出しにして、一度に表示するコードを出来るかぎり簡潔に表すことです。しかし、`train_neuralnet.py` から `two_layer_net.py` を呼び出し、`two_layer_net.py` から `common フォルダー`内の `gradient.py` を呼び出すというのは、とても分かり難いです。なので、このあたりは、`train_neuralnet.py`　に追加して見通しをよくします。

　但し、`commonフォルダー`にある `function.py` だけは外部コードとして使っています（sigmoid, sofytmax, cross_entropy_errorなど)。

　では、まずは下記コードを実行してみましょう。

```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 numerical_gradient(self, x, t):   
        loss_W = lambda W: self.loss(x, t)        
        grads = {}
        grads['W1'] = numerical_gradient2(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient2(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient2(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient2(loss_W, self.params['b2'])        
        return grads
    
    # 数値微分
    def numerical_gradient2(f, x):   
        h = 1e-4 # 0.0001
        grad = np.zeros_like(x)
    
        it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
        while not it.finished:
            idx = it.multi_index
            tmp_val = x[idx]
            x[idx] = tmp_val + h
            fxh1 = f(x) # f(x+h)
        
            x[idx] = tmp_val - h 
            fxh2 = f(x) # f(x-h)
            grad[idx] = (fxh1 - fxh2) / (2*h)
        
            x[idx] = tmp_val # 値を元に戻す
            it.iternext()           
        return grad  
            
# データの読み込み
(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 = 100  # 実行回数は 10000 → 100 に変更
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 1   # 学習率は 0.1 → 1.0 に変更
train_loss_list, train_acc_list, test_acc_list = [], [], []
iter_per_epoch = 1  # 精度表示は 1 epoch 毎から 1 iter 毎に変更

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.numerical_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)
        
        # 表示 (iter と train_loss を追加)
        print('[iter='+str(i)+'] '+'train_loss='+str(loss)+', '+'train_acc='+str(train_acc)+', '+'test_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("iter")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
```
![スクリーンショット 2020-04-29 16.40.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/e4a9afb9-10f9-1dd6-bfda-4867468e02fe.png)
　私の `Macbook air` で 100iter 実行するのに 75分掛かりました。数値微分の場合でも 16epoch (= 9600iter) を実行した精度グラフをテキストに載せるのは一体どうなの？とは思いますが、それはさておき、数値微分には膨大な時間が掛かるということですね。

#3.クラス TwoLayerNet

```python
    # パラメータの初期化
    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)
```
　class　をインスタンス化する時に1度だけ実行される部分で、ここでは各パラメータの初期化をしています。`np.random.randn()` は、平均0, 分散1 の正規分布の乱数生成、`np.zeros()`は　ゼロ行列の生成です。各行列の大きさは、下記の様です。
![スクリーンショット 2020-04-29 17.32.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/7e946b24-b367-5949-684c-1be551c71511.png)

```python
    # 順伝播
    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
```
　順伝播の部分です。辞書 params に保存されている 重み'W1', 'W2' とバイアス 'b1', 'b2' の行列を読み出し、行列演算で順伝播させます。

```python
    # ロス計算
    def loss(self, x, t):
        y = self.predict(x)        
        return cross_entropy_error(y, t)
```
　ロス計算をする部分です。先程の順伝播を呼び出して求めた y と教師データ t のクロスエントロピーを求めます。クロスエントロピーは、`common フォルダー`内の `function.py` の関数を使っています。一応みておくと、

![スクリーンショット 2020-04-30 08.44.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/38359903-f186-9060-754a-f502b310fa9b.png)

　`y[np.arange(batch_size), t]` は、tに順番に入っている正解ラベルに従って、yの該当する要素をスライシングするという意味です。


```python
    # 精度計算
    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
```
　精度計算の部分です。入力データxからyを推論し、yと正解ラベルtのインデックスをそれぞれ取り出し、2つのインデクスが同じ場合の数をxのデータ数で割って精度を計算しています。

```python
    # 勾配計算
    def numerical_gradient(self, x, t):   
        loss_W = lambda W: self.loss(x, t)        
        grads = {}
        grads['W1'] = numerical_gradient2(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient2(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient2(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient2(loss_W, self.params['b2'])        
        return grads
```
　勾配計算の部分です。この後出て来る `numerical_gradient2` 関数を使って、勾配の計算結果を辞書形式にまとめます。引数は、xと正解ラベルtを元にクロスエントロピーを求める先程の `loss関数式`、そして`パラメータの指定`です。

　ちなみに、オリジナルは、`grads['W1'] = numerical_gradient(loss_W, self.params['W1'])`となっていて、あれ？再帰利用かな？と最初思ったりしたのですが、そうではありません。

　オリジナルでは、`TwoLayerNet.py` の冒頭に、`from common.gradient import numerical_gradient` と記載があり、`common フォルダー`内の `gradient.py` にある `numerical_gradient 関数`がインポートされているので、使っているのはこちらです。ここは名前を変えた方が間違いが少ないので、`numerical_gradient2`に名称変更しています。

```python
    # 数値微分
    def numerical_gradient2(f, x):   
        h = 1e-4 # 0.0001

        # 勾配の計算結果を保管するゼロ行列 grad(サイズはxで指定)を準備
        grad = np.zeros_like(x)

        # 行列 x を順次インデックス指定(行と列を指定)する
        it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
        
        # 行列 x の全ての行と列を指定するまで継続
        while not it.finished:
            
            idx = it.multi_index  # idxに(行,列)を代入            
            tmp_val = x[idx]  # idxで指定したxの値をtmp_valに退避
            
            # 微小数hを加えて順伝播しロスを計算
            x[idx] = tmp_val + h
            fxh1 = f(x) # f(x+h)

            # 微小数hを引いて順伝播しロスを計算
            x[idx] = tmp_val - h 
            fxh2 = f(x) # f(x-h)

            # 該当インデックスの勾配を計算
            grad[idx] = (fxh1 - fxh2) / (2*h)

            x[idx] = tmp_val # 退避した値を元に戻す
            it.iternext()  # 次のインデックスを実行          
        return grad  
```
　数値微分をする部分です。簡単に言うと、`パラメータの１つ１つについて、小さな値hを足して順伝播・ロス計算、小さな値hを引いて順伝播・ロス計算し、２つのロス計算がどう変化したかで勾配を決めています。`

　今回、パラメータ数は、W1が784*50=39,200個、W2が50*10=500個、b1が50個、b2が10個で、合計39,760個あります。パラメータ1個あたり、順伝播・ロス計算を2回行いますので、ネットワークの1回のパラメータ更新に、なんと79,520回の順伝播・ロス計算をすることになります。動作が激遅なわけです。

　さて、コードに見慣れない、`np.nditer` があります。通常、行列のインデッックス指定は行指定にforループ＋列指定にforループと2重ループを使うわけですが、それを1回で済ませるのが、この`np.nditer`です。

　なお、先程も説明しましたが、このコードはオリジナルでは、`commonフォルダー`の`gradient.py`に入っています。オリジナルの関数名は、`numerical_gradient` と紛らわしい名前(勾配計算のところの関数名と同一）をしているので、今回は `numerical_gradient2` に変更しています。
#4.本体部分

```python
# データの読み込み
(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 = 100  # 実行回数は 10000 → 100 に変更
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 1   # 学習率は 0.1 → 1.0 に変更
train_loss_list, train_acc_list, test_acc_list = [], [], []
iter_per_epoch = 1  # 精度表示は 1 epoch 毎から 1 iter 毎に変更
```
　データを読み込んだら、クラス`TwoLayerNet`をインスタンス化します。input_size=784, hidden_size=50, output_size=10なので、モデルと行列演算は下記の様です。
![スクリーンショット 2020-04-30 12.56.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/fd7fa6a1-9b55-5592-b2ae-d02a23b0d25c.png)

```python
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.numerical_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)

        # 表示 (iter と train_loss を追加)
        print('[iter='+str(i)+'] '+'train_loss='+str(loss)+', '+'train_acc='+str(train_acc)+', '+'test_acc='+str(test_acc))
```
　まず、ミニバッチ学習用データの準備です。`np.random.choice(train_size, batch_size)` は 60,000個あるtrainデータの中からランダムに100個を選んだ結果（何番目を選んだか）を `batch_mask` に代入し、それを使って学習データと正解ラベルを取得します。

　次に、勾配計算。`TwoLayerNet クラスの numerical_gradient関数`を呼び出し、先程説明した様に、パラメータ１つ１つについて、数値微分で勾配を求めます。

　そして、求めた勾配を使ってパラメータを更新し、ロスを計算して記録し、精度を表示します。

```python
# グラフの描画
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("iter")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
```
　学習完了後に、精度グラフを表示します。これは、特に説明は必要ないでしょう。