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

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

#2.最適化手法を試すコード
　最適化手法を実際に試してみるために、`ch06/optimizer_compare_mnist.py`を一部修正・追加して使います。ネットワークは、100×4層で、MNISTを分類させるものです。下記のコードの `optimizer の key 設定`で、使わない optimizer だけコメントアウトして実行すればOKです。 

```python
import os
import sys
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.util import smooth_curve  # smooth_curve (ロス値の推移を滑らかにする関数)　インポート
from common.multi_layer_net import MultiLayerNet  # MultiLayerNet インポート
from common.optimizer import *  # optimizer　インポート

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

# 初期設定
train_size = x_train.shape[0]
batch_size = 128
max_iterations = 2001

# optimizer の key 設定
optimizers = {}
optimizers['SGD'] = SGD()
optimizers['Momentum'] = Momentum()
optimizers['Nesterov'] = Nesterov()
optimizers['AdaGrad'] = AdaGrad()
optimizers['RMSprop'] = RMSprop() 
optimizers['Adam'] = Adam()

# network と train_loss を optimizer の key 毎に設定
networks = {}
train_loss = {}
for key in optimizers.keys():
    networks[key] = MultiLayerNet(
        input_size=784, hidden_size_list=[100, 100, 100, 100],
        output_size=10)
    train_loss[key] = []    


# 学習ループ
for i in range(max_iterations):
    
    # ミニバッチのデータを抽出
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # optimizer の key 毎に、勾配をアップデートし、ロスを記録
    for key in optimizers.keys():
        grads = networks[key].gradient(x_batch, t_batch)
        optimizers[key].update(networks[key].params, grads)
    
        loss = networks[key].loss(x_batch, t_batch)
        train_loss[key].append(loss)
    
    # ロス表示 (500 iter 毎)
    if i % 500 == 0:
        print( "===========" + "iteration:" + str(i) + "===========")
        for key in optimizers.keys():
            loss = networks[key].loss(x_batch, t_batch)
            print(key + ":" + str(loss))


# グラフの描画
fig = plt.figure(figsize=(8,6))  # グラフサイズ指定
markers = {"SGD": "o", "Momentum": "x", "Nesterov": "^", "AdaGrad": "s", "RMSprop":"*", "Adam": "D", } 
x = np.arange(max_iterations)
for key in optimizers.keys():
    plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 1)
plt.legend()
plt.show()
```

#3.SGD
　最適化手法のベースモデルは、第５章まで使っていた`SGD`です。
![スクリーンショット 2020-05-06 10.20.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/bc50d7bc-56ab-81cf-3e2b-502e017fb4a1.png)

　`common/optimizer.py` にある `SGD` の実装を見ると、

```python
class SGD:

    def __init__(self, lr=0.01):
        self.lr = lr
        
    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key] 
```

#4.Momentum
　`SGD`は、勾配の更新が常に一定なので、特に初期段階において最適化するための時間が掛かります。そこで、`Momentum` の登場です。

![スクリーンショット 2020-05-06 10.22.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/03d8c136-5268-b564-6b90-79f622d3e8e6.png)

　`Momentum` は、勾配の方向が変わらないうちは、勾配の更新度合いをだんだん大きくして行く手法です。それは、まさに地面の傾斜に合わせてボールが転がって行くイメージで、$\alpha=0.9$ は、地面の摩擦や空気抵抗と考えることが出来ます。

　もうちょっとイメージを具体的に表現すると、例えば４回の勾配計算結果がいずれも$\frac{\partial L}{\partial W}$で同じだと仮定するとvは、
![スクリーンショット 2020-05-04 14.24.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/fc53a650-2fac-3c6b-af09-0a9ef57fcf1a.png)
　勾配を更新する度合いが、-1.0, -1.9, -2.71, -3.439 とだんだん大きくなって行くことが分かります。`common/optimizer.py` にある `Momentum` の実装を見ると、


```python
class Momentum:

    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None
        
    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():                                
                self.v[key] = np.zeros_like(val)
                
        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] - self.lr*grads[key] 
            params[key] += self.v[key]
```

#5.Nesterov
　Momentum は、勾配の更新度合いをだんだん大きくした後に、勾配の方向が逆転するとオーバーシュートが起きやすくなります。そこで、momentum　を一部修正した、`Nesterov`(ネストロフのMomentumとも呼ばれます）の登場です。

　`Nesterov` は、勾配を計算する位置を現在の位置ではなく、1歩先の勾配更新後の位置に変更します。勾配更新後の正確な位置はもちろん分かりませんが、現在の勾配を使って概算のvを求めることで代用します。これによって、オーバーシュートの抑制が期待できます。
![スクリーンショット 2020-05-04 19.34.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/386ef587-532f-45ec-88ff-6c5e12f4e4ca.png)
　`common/optimizer.py` にある実装を見てみると、

```python
class Nesterov:

    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None
        
    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zeros_like(val)
            
        for key in params.keys():
            self.v[key] *= self.momentum
            self.v[key] -= self.lr * grads[key]
            params[key] += self.momentum * self.momentum * self.v[key]
            params[key] -= (1 + self.momentum) * self.lr * grads[key]
```
　それでは、`SGD`, `Momentum`, `Nesterov`を比較してみましょう。

![スクリーンショット 2020-05-05 19.14.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/2c6ba712-2aaf-d67a-058e-511c38ed65ac.png)

　`Momentum`と`Nesterov`は、`SGD`と比べて初期段階のロス低減スピード、最終ロス率ともに圧倒的に改善されています。`Nesterov`は`Momentum`より、さらにもう１段改善され、ロスのバラツキも若干小さくなっている様です。

#6.AdaGrad
　`AdaGrad`からは、2つの重要なアイディアが導入されます。

　1つ目は、膨大にあるパラメータを一括で最適化を図るのではなく、パラメータに応じて最適化を図るべきだという`適応的な学習率`(Adaptive)という考え方です。

　２つ目は、学習の初期は学習率を上げ、学習が進むにつれて学習率を下げて学習を効率よく進めるための`学習係数の減衰`という考え方です。 

![スクリーンショット 2020-05-06 10.25.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/12ce04bb-3cf9-165b-ea0a-a5c0bc1672d7.png)

　hに勾配の二乗和を累積し、勾配を更新する際に$\frac{1}{\sqrt{h}+\epsilon}$を掛けることで、学習率を補正します。つまり、大きく更新されたパラメータの学習率を次第に小さくします。
　ちなみに、$\epsilon$ は極めて小さな数値(ゼロ除算防止用)です。`common/optimizer.py` の実装を見ると、

```python
class AdaGrad:

    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None
        
    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)
            
        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
```

#7.RMSprop
　`RMSprop` は、`AdaGrad` の改良版で、hに勾配の二乗の**指数移動平均**（過去の計算結果は徐々に忘れ、新たな計算結果を組み込む）を保管し、勾配を更新する際に$\frac{1}{\sqrt{h}+\epsilon}$を掛けることで、学習率を補正します。

![スクリーンショット 2020-05-06 10.28.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/53af95cf-aa07-f421-903f-2480d1c9cba8.png)


　`common/optimizer.py` の実装を見ると、

```python
class RMSprop:

    def __init__(self, lr=0.01, decay_rate = 0.99):
        self.lr = lr
        self.decay_rate = decay_rate
        self.h = None
        
    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)
            
        for key in params.keys():
            self.h[key] *= self.decay_rate
            self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
```

#8.Adam
　`Adam` は、`Momentum` と `AdaGrad` の良いとこ取りをするアイディアから生まれたものです。

![スクリーンショット 2020-05-06 10.31.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/e014bf1d-c477-e592-ec86-5e004167b939.png)

　`m` は `Momentum` の指数移動平均みたいな形、`v`は`AdaGrad`そのものですね。その後に `m` と`v` を iterが小さい内は大きく利かせ、iterが大きくなるに従って利かせ具合を弱める補正をしています。実装は、下記の様に式を少し変形して行っています。

![スクリーンショット 2020-05-06 10.02.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/c6e275fa-b574-8b1b-80f7-665fb685fe94.png)


　`common/optimizer.py` の実装を見ると、

```python
class Adam:

    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None
        
    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)
        
        self.iter += 1
        lr_t  = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)         
        
        for key in params.keys():
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])            
            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
```
　それでは、`AdaGrad`, `RMSprop`, `Adam` を比較してみます。

　![スクリーンショット 2020-05-05 19.06.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/4ba770be-3cfe-15a1-16bb-030899994296.png)

　どんなタスクにもベストな結果を出す最適化手法はありません。今回は、`AdaGrad`が一番良い結果になりました。

　`RMSprop`の勾配の二乗の指数移動平均による学習率補正は、今回のタスクにはやり過ぎとなったせいでしょうか、ロスの振幅が非常に大きくなり、最終ロス率も高くなってしまいました。

　`Adam`は世間では、とりあえず最初に使ってみるべき最適化手法で常に安定的なパフォーマンスを示すと言われています。今回のタスクでは、一番良いというわけではありませんが、さすがに優等生的なロス低減推移を見せています。

#9.重みの初期化
　重みの初期値がゼロだと、全ての重みの値が均一に更新されてしまうため、表現力が失われてしまいます。どういう初期化が良いかは、活性化関数のタイプによって異なります。

　sigmoid関数やtanh関数など左右対称で中央付近が線形関数と見なせる場合は `Xavier の初期化`という$\sqrt{\frac{1}{n}}$を標準偏差とするガウス分布が最適と言われています。

　ReULを用いる場合は`He の初期化`という$\sqrt{\frac{2}{n}}$を標準偏差とするガウス分布が最適だと言われています。

#10.Dropout
　`Dropout` は、学習の際、iter毎にランダムに選んだニューロンの接続を切ることによって、表現力の高いネットワークであっても過学習を抑制する手法です。実装コードを見てみると、

```python
class Dropout:

    def __init__(self, dropout_ratio=0.5):
        self.dropout_ratio = dropout_ratio
        self.mask = None

    # 順伝播 
    def forward(self, x, train_flg=True):
        # 学習時は、接続可否を決める mask を作成し、順伝播する信号に mask を掛ける
        if train_flg:
            self.mask = np.random.rand(*x.shape) > self.dropout_ratio
            return x * self.mask

        # 推論時は maskを掛けず、信号全体に(1 - dropout_ratio)を掛ける
        else:
            return x * (1.0 - self.dropout_ratio)
    
    # 逆伝播する信号に mask を掛ける
    def backward(self, dout):
        return dout * self.mask
```
　学習時は、毎回、一様乱数と閾値(dropout_ratio)を使って、接続可否を決める mask(接続可はTrue, 不可はFalse)を作成する。そして、順伝播する信号にマスクを掛ける(x * self.mask )。逆伝播の時も同様に信号にmaskを掛ける。

　具体的なイメージで言えば、こんな感じ、

![スクリーンショット 2020-05-06 18.49.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/ec809228-0466-e36c-fe5f-8f39019b8dae.png)

　また推論時は、maskを掛けずに信号全体に(1 - dropout_ratio)を掛けて、信号全体の大きさのみ調整しています。

#11.Batch Normalization
　`Batch Normalization` は、2015年に発表された手法で、ミニバッチ毎に平均0, 分散1になるように正規化することで、学習収束速度の向上、`Dropout` の必要性の低下、重み初期化の必要性低下(重みの初期化にロバスト）などの効果が得られます。下記が、アルゴリズムです。
![スクリーンショット 2020-05-06 14.05.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/0d5b58fc-8427-70d9-9b97-2f42f19789d9.png)

#12.Weight decay
　Weight decay は、学習過程において、大きな重みを持つことにペナルティを課すことによって、過学習を抑制する手法です。

　重みをWとするとき損失関数に、$\frac{1}{2}\lambda W^2$を加えることで、重みWが大きくなることが抑制でき、この方法を**L2正則化**と言います。ここで$\lambda$を大きくすれば、よりペナルティを大きく出来ます。
 ![スクリーンショット 2020-05-06 14.31.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/ddbf678e-4b99-e2f8-a182-76a1329147c5.png)

　ちなみに、損失関数に$\lambda |W|$に加えたものを、**L1正則化**と言います。