LoginSignup
6
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-06

1.はじめに

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

2.最適化手法を試すコード

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

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

 最適化手法のベースモデルは、第5章まで使っていたSGDです。
スクリーンショット 2020-05-06 10.20.23.png

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

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

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

 もうちょっとイメージを具体的に表現すると、例えば4回の勾配計算結果がいずれも$\frac{\partial L}{\partial W}$で同じだと仮定するとvは、
スクリーンショット 2020-05-04 14.24.01.png
 勾配を更新する度合いが、-1.0, -1.9, -2.71, -3.439 とだんだん大きくなって行くことが分かります。common/optimizer.py にある Momentum の実装を見ると、

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
 common/optimizer.py にある実装を見てみると、

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

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

6.AdaGrad

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

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

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

スクリーンショット 2020-05-06 10.25.44.png

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

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

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

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 は、MomentumAdaGrad の良いとこ取りをするアイディアから生まれたものです。

スクリーンショット 2020-05-06 10.31.16.png

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

スクリーンショット 2020-05-06 10.02.53.png

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

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

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

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

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

9.重みの初期化

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

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

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

10.Dropout

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

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

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

11.Batch Normalization

 Batch Normalization は、2015年に発表された手法で、ミニバッチ毎に平均0, 分散1になるように正規化することで、学習収束速度の向上、Dropout の必要性の低下、重み初期化の必要性低下(重みの初期化にロバスト)などの効果が得られます。下記が、アルゴリズムです。
スクリーンショット 2020-05-06 14.05.00.png

12.Weight decay

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

 重みをWとするとき損失関数に、$\frac{1}{2}\lambda W^2$を加えることで、重みWが大きくなることが抑制でき、この方法をL2正則化と言います。ここで$\lambda$を大きくすれば、よりペナルティを大きく出来ます。
スクリーンショット 2020-05-06 14.31.12.png

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

  

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5