LoginSignup
76
70

More than 5 years have passed since last update.

【学習メモ】ゼロから作るDeep Learning【6章】

Last updated at Posted at 2017-09-09

ゼロから作るDeep Learning 6章

Deep Learningと言わず
Machine Learningでかなり大切な事を扱っているのがこの6章になります。

パラメータの更新

ニューラルネットワークの目的=損失関数の値をできるだけ小さくするパラメータを見つけること
そのような問題を解くことを「最適化」と言う

SGD:確率的勾配降下法


W \leftarrow  W - \eta\frac{\partial L}{\partial W} 

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]

    network = TwoLayerNet(...)



#optimizer:最適化を行う者
optimizer = SGD()

for i in range(10000) :
        ...
    x_bath, t_bath = get_mini_batch(...)
    grads = network.gradient(x_batch, t_batch)
    params = network.params
    optimizer.update(params, grads)
    ...

パラメータの更新はopetimizerが行う
実施するのはoptimizerにパラメータと勾配の情報を渡すだけ

上記のように最適化を行うクラスを分離して実装することで、機能のモジュール化が容易になる

例えば次に出てくるMomentumという別の最適化手法を実装する際にも、同じくupdate(params, grads)という共通メソッドを持つように実装する
するとoptimizer = SGD()をoptimizer = Momentum()に変更するだけで、SGDをMomentumに切り替えることが可能になる

SGDの欠点

SGDの欠点は、関数の形状が等方的出ないと出ないと(伸びた形の関数だと)非効率的な経路で検索することになる

なお、上記の欠点の根本的な原因は、勾配の方向が本来の最小値ではない方向を指していることに起因します

Kobito.a81EO2.png

このSGDの欠点を改善するために、SGDに代わる手法として3つの手法が紹介
・Momentum
・AdaGrad
・Adam

Momentum

モーメンタン(Momentum)とは「運動量」のこと
地面を転がるボールが、何も力を受けない時に徐々に摩擦や空気抵抗で減速するようなイメージ

v \leftarrow  \alpha v - \eta\frac{\partial L}{\partial W}\\

W \leftarrow  W + v 

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]
            param[key] += self.v[key]

Kobito.g9jOgk.png

x軸方向:受ける力は小さいが、常に同じ方向の力を向けるため、同じ方向へ一定して加速することになる
y軸方向:受ける力は大きいが、正と負の方向の力を交互に受け歌め、互いに打ち消し合い、y軸への速度は安定しない
→SGDよりx軸方向へ早く近づけ、ジグザグの動きを軽減できる

AdaGrad

この手法は学習係数の低減(learning rate decay)行う
これは学習が進むにつれて学習係数を小さくするという手法です
Ada:適応的のAdaptiveに由来

h \leftarrow  h + \frac{\partial L}{\partial W}\odot\frac{\partial L}{\partial W}\\

W \leftarrow  W - \eta \frac{1}{\sqrt{h}} \frac{\partial L}{\partial W}

1/√hを乗算することは、パラメータの更新の中でよく動いた(大きく更新された)要素は、学習係数が小さくなることを意味する

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]
            param[key] += self.lr * grads[key] / (np.sqrt(self.h[key] + le-7)

le-7という小さい値を加算しているのはself.h[key]の中に0があった場合、0で除算されてしまうからです

image.png

RMSProp

AdaGradは過去の勾配を2乗和として全て記録します
そのため学習を無限に進めれば更新料は0になり、全く更新されなくなリます

この問題がを解決したRMSPropと違う手法もあります
RMSPropはかこ全ての勾配を均一に加算するのではなく、過去の勾配を徐々に忘れて、新しい勾配情報が大きく反映させるように計算される手法です
これを「指数移動平均」と言います

本には式は載っていないがプログラムから逆算するにこんな計算式に見える

\begin{align}
初期値d&=0.99\\
h &\leftarrow  h * d + (1 - d)\frac{\partial L}{\partial W}\odot\frac{\partial L}{\partial W}\\

W &\leftarrow  W - \eta \frac{1}{\sqrt{h}} \frac{\partial L}{\partial W}
\end{align}
class RMSprop:

    """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)

Adam

Momentum:物理法則に準じる動き
AdaGrad:パラメータの要素ごとに、適応的に更新ステップを調整

Adam = Momentum + AdaGrad + ハイパーパラメータの「バイアス補正(偏りの補正)」

同じく本には数式は載っておらず
ググったら数式は出てきたのでそちらを確認

引用:http://postd.cc/optimizing-gradient-descent/#adam

Adam(Adaptive Moment Estimation)14はまた別の方式で、それぞれのパラメータに対し学習率を計算し適応させていきます。
AdadeltaやRMSpropでは、過去の勾配の二乗v_tの指数関数的減衰平均を蓄積していました。Adamではこれに加え、同じように過去の勾配m_tの指数関数的減数平均を保持します。Momentumと似た方法です。



\begin{align}
初期値\beta_1 &= 0.9,\beta_2=0.999,\epsilon=10^{-8}\\
\\
m_t &= \beta_1 m_{t-1} + (1 – \beta_1) g_t\\
v_t &= \beta_2 v_{t-1} + (1 – \beta_2) g_t^2\\
\\
\hat{m}_t &= \dfrac{m_t}{1 – \beta^t_1}\\
\hat{v}_t &= \dfrac{v_t}{1 – \beta^t_2}\\
\\
\theta_{t+1} &= \theta_{t} – \dfrac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t\\

\end{align}

image.png

class Adam:

    """Adam (http://arxiv.org/abs/1412.6980v8)"""

    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] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
            #self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
            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)

            #unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
            #unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
            #params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)

比較

下記画像ではAdaGradが良さそうです
しかし残念ながらどの手法が優れているということは(今の所)ありません
それぞれに特徴があり、得意な問題、不得意な問題があるそうです

image.png

image.png

重みの初期値

重みの初期値を0とした時の問題点

過学習を抑え、汎化性能を高めるテクニック、Weight decay(荷重減衰)
Weight decay:重みパラメータの値を小さくするように学習を行うことを目的とした手法
重みの値を小さくすることで、過学習が起きにくくなリマす。

重みを小さくしたいのであれば、初期値もできるだけ小さい値でスタートしたいと思うのが当然です
これまでの重みの初期値、0.01 * np.random.randn(10, 100)でした。
(標準偏差が0.01のガウス分布)

しかし重みが0の際には悪いアイディアだそうです。
(正確には重みの値を均一な値に設定してはいけない)
理由は、誤差伝播法において、全ての重みの値が均一の更新されてしまうからです。
そのためランダムな初期値が必要になります。

重みの初期値のベストプラクティス

結論を先に述べると以下になります。

  • 活性化関数にReLUを使う場合は「Heの初期値」
  • sigmoidやtanhなどのS字カーブの時は「Xavierの初期値」

前層のノードの個数がnの際に

Xavierの初期値:標準偏差が\frac{1}{\sqrt{n}}の標準偏差を持つガウス分布\\
Heの初期値:標準偏差が\sqrt\frac{2}{n}の標準偏差を持つガウス分布\\

隠れ層のアクティベーション分布(活性化関数の後の出力データ)

0と1に偏ったデータ
→逆伝播の勾配の値がどんどん小さくなって消えていく
この問題を「勾配消失」と言います

各ニューロンがほぼ同じ値を出力する
→アクティベーションの偏り
表現力の制限」という問題になる

そのため程よくばらけている結果が望ましい

  • 活性化関数にsigmoidを使用した際のアクティベーション分布の変化

image.png
重みの初期値として標準偏差1のガウス分布を用いた時の各層のアクティベーションの分布

image.png
重みの初期値として標準偏差0.01のガウス分布を用いた時の各層のアクティベーションの分布

image.png
重みの初期値としてXavierの初期値を用いた時の各層のアクティベーションの分布

image.png
重みの初期値としてHeの初期値を用いた時の各層のアクティベーションの分布

  • 活性化関数にReLUを使用した際のアクティベーション分布の変化

image.png
重みの初期値として標準偏差1のガウス分布を用いた時の各層のアクティベーションの分布

image.png
重みの初期値として標準偏差0.01のガウス分布を用いた時の各層のアクティベーションの分布

image.png
重みの初期値としてXavierの初期値を用いた時の各層のアクティベーションの分布

image.png
重みの初期値としてHeの初期値を用いた時の各層のアクティベーションの分布

MNISTデータセットによる重みの比較

std=0.01の場合はほとんど学習が進んでいおらず
He、Xavierの際にはサクサク学習が進んでいる
→初期値の問題はとても重要ということがわかります

image.png

Batch Normalization

各層のアクティベーションの分布が適度な広がりを持つように”強制的”にアクティベーションの調整を行う

Batch Normalizationの利点

  • 学習を早く進行させることができる(学習係数を大きくすることができる)
  • 初期値にそれほど依存しない(初期値に対してそこまで神経質にならなくて良い)
  • 過学習を抑制する(Dropoutなどの必要性を減らす)

なんだか私見ですが料理の味の素みたいですね

Batch Normalizationアルゴリズム

各層のアクティベーションの分布が適度な広がりを持つように調整する
→つまりデータ分布の正規化(平均が0、分散が1の分布)を行うレイヤをニューラルネットワークに挿入する

image.png

\begin{align}
&ミニバッチとしてB=\{x_1, x_2, \cdots, x_m\}というm個の入力データの集合に対して、\\
&平均\mu_B、 分散\sigma_B^2を求める \\
&また\epsilonは10^{-7}などのとても小さな値
\end{align}
\begin{align}
\mu_B &\leftarrow \frac{1}{m} \sum_{i-1}^{m} x_i\\
\sigma_B^2 &\leftarrow \frac{1}{m} \sum_{i-1}^{m} (x_i - \mu_B)^2\\
\hat{x_i} &\leftarrow \frac{x_i-\mu_B}{\sqrt{\sigma_B^2 + \epsilon}}

\end{align}

さらにBath Normレイヤは、この正規化されたデータに対して、固有のスケールとシフトで変換を行います。
γ、βはパラメータで、最初はγ=1、β=0からスタートして、学習によって適した値に調整されていきます

y_i \leftarrow \gamma \hat{x_i} + \beta

逆伝播などは
Frederik Kratzertのブログを読めとのことでした。

Batch Normalizationの評価

image.png

正則化

過学習

過学習が起きる原因
・パラメータを大量に持ち、表現力の高いモデルである事
・訓練データが少ない事

過学習をわざと発生させた

image.png

Weight decay

Weight decay:荷重減衰

ニューラルネットの学習目的は損失関数の値を小さくする事
この際に重みの2重ノルム(L2ノルム)を加算してあげれば、重みが大きくなる事を抑えられる

重みWとすれば、L2ノルムのWeight decayは

\frac{1}{2}\lambda W^2

λは正則化の強さをコントロールするハイパーパラメータ
1/2はW^2を微分した結果をλWにするための調整用の定数

L2ノルム

\sqrt{w_1^2+w_2^2+\cdots+w_n^2}

L1ノルム

|w_1^2|+|w_2^2|+\cdots+|w_n^2|

正直本の説明よりも他を参照した方がわかりやすかった

引用:http://qiita.com/supersaiakujin/items/97f4c0017ef76e547976

deep neural networkではlayerが多層になるほど、そのモデルの表現能力が増します。
しかし、多層になるほどoverfittingのリスクも高くなります。
Modelの表現能力を維持したまま、parameterの自由度に制限を与えることでoverfittingのリスクを減らすことが行われます。
その手法の一つがweight decay(重み減衰)です。
weightの更新式は下記のように書かれます。

{w \leftarrow w -\eta \frac{\partial C(w)}{\partial w} - \eta \lambda w\\
}

上記式は何がしたいのか少しわかりづらいですが、実際はcost functionを下記のようにしたものから来ています。

{\tilde C(w) = C(w) + \frac{\lambda}{2}||w||^2
}

これはつまり、cost functionにL2 regularization項をつけたものです。
この項によりweightの値は小さくなります。
なので、実際に実装するときはL2 regularizationの項をcostに加えることになります。

image.png

テキストではスルーされているが使っている部分のソースだけ抜粋:weight_decay_lambdaで検索すればわかりやすい
初期化、損失関数の計算、重みの設定の際に使用している

    def __init__(self, input_size, hidden_size_list, output_size,
                 activation='relu', weight_init_std='relu', weight_decay_lambda=0):
        self.input_size = input_size
        self.output_size = output_size
        self.hidden_size_list = hidden_size_list
        self.hidden_layer_num = len(hidden_size_list)
        self.weight_decay_lambda = weight_decay_lambda
        self.params = {}

        # 重みの初期化
        self.__init_weight(weight_init_std)

        # レイヤの生成
        activation_layer = {'sigmoid': Sigmoid, 'relu': Relu}
        self.layers = OrderedDict()
        for idx in range(1, self.hidden_layer_num+1):
            self.layers['Affine' + str(idx)] = Affine(self.params['W' + str(idx)],
                                                      self.params['b' + str(idx)])
            self.layers['Activation_function' + str(idx)] = activation_layer[activation]()

        idx = self.hidden_layer_num + 1
        self.layers['Affine' + str(idx)] = Affine(self.params['W' + str(idx)],
            self.params['b' + str(idx)])

        self.last_layer = SoftmaxWithLoss()

    def loss(self, x, t):
        """損失関数を求める

        Parameters
        ----------
        x : 入力データ
        t : 教師ラベル

        Returns
        -------
        損失関数の値
        """
        y = self.predict(x)

        weight_decay = 0
        for idx in range(1, self.hidden_layer_num + 2):
            W = self.params['W' + str(idx)]
            weight_decay += 0.5 * self.weight_decay_lambda * np.sum(W ** 2)

        return self.last_layer.forward(y, t) + weight_decay


    def gradient(self, x, t):
        """勾配を求める(誤差逆伝搬法)

        Parameters
        ----------
        x : 入力データ
        t : 教師ラベル

        Returns
        -------
        各層の勾配を持ったディクショナリ変数
            grads['W1']、grads['W2']、...は各層の重み
            grads['b1']、grads['b2']、...は各層のバイアス
        """
        # forward
        self.loss(x, t)

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

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 設定
        grads = {}
        for idx in range(1, self.hidden_layer_num+2):
            grads['W' + str(idx)] = self.layers['Affine' + str(idx)].dW + self.weight_decay_lambda * self.layers['Affine' + str(idx)].W
            grads['b' + str(idx)] = self.layers['Affine' + str(idx)].db

        return grads

Dropout

Dropout:ニューロンをランダムに消去しながら学習する手法

image.png


image.png

Chainerで実装されるDropout

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

        def forward(self, x, train_flg=True):
            if train_flg:
                self.mask = np.random.rand(*x.shape) > self.dropout_ratio
                return x * self.mask
            else
                return x * (1.0 - self.dropout_ratio)

        def backward(self, dout):
            return dout * self.mask

Dropoutを使用した際の結果

image.png

ハイパーパラメータの検証

これまでのハイパーパラメータ例
・各層のニューロンの数
・バッチサイズ
・学習係数
・Weight decay

検証データ

ハイパーパラメータはテストデータで性能を評価してはいけない
→過学習を起こす起こす事になるから

そのため、ハイパーパラメータ専用の確認データ、検証データ(validation data)を使用する

データの内容によってはユーザの手で作る必要がある
訓練データから20%程度検証データとして先に分離するコード

(x_train, t_train), (x_test, t_test) = load_mnist()

# 訓練データをシャッフル
x_train, t_train = shuffle_dataset(x_train, t_train)

# 検証データの分割
validation_rate = 0.20
validation_num = int(x_train.shape[0] * validation_rate)

x_val - x_train[:validation_num]
t_val - t_train[:validation_num]
x_train - x_train[validation_num:]
t_train - t_train[validation_num:]

ハイパーパラメータの最適化

ハイパーパラメータの最適化には次のステップを繰り返す

  • STEP0
    ハイパーパラメータの範囲を指定する:最初はざっくりと指定でOK

  • STEP1
    設定されたハイパーパラメータの範囲からランダムにサンプリングする

  • STEP2
    STEP1でサンプリングされたハイパーパラメータの値を使用して学習を行い、
    喧騒データの認識精度を評価する
    (ただし、エポックは小さく設定)

  • STEP3
    STEP21とSTEP2wpある回数(100回など)繰り返し、それらの認識精度の結果から
    ハイパーパラメータの範囲を狭める

ランダムサンプリングの実装

wight_decay = 10 ** np.random.uniform(-8, -4)
lr = 10 ** np.random.uniform(-6, -2)

image.png

Best-1(val acc:0.84) | lr:0.008596628403945712, weight decay:3.075068633526172e-06
Best-2(val acc:0.83) | lr:0.009688160706596694, weight decay:5.876005684736357e-08
Best-3(val acc:0.78) | lr:0.007897858091143213, weight decay:3.792675246120474e-08
Best-4(val acc:0.77) | lr:0.008962267845301249, weight decay:4.0961888275354916e-07
Best-5(val acc:0.74) | lr:0.009453193380059509, weight decay:1.5625175027026464e-08
Best-6(val acc:0.73) | lr:0.0066257479672272536, weight decay:4.6591905625864734e-05
Best-7(val acc:0.72) | lr:0.007814005955583136, weight decay:4.9330072714643424e-06
Best-8(val acc:0.72) | lr:0.008895526423573389, weight decay:4.297901358238285e-06
Best-9(val acc:0.71) | lr:0.006419577071135049, weight decay:1.0848308972057103e-08
Best-10(val acc:0.69) | lr:0.006304961469167366, weight decay:1.6652787617252613e-07

上記の結果をみると次ぐらいか?
wight_decay:10^-5-10^-8
lr:0.01-0,0001

絞ってからもう一度実行

image.png

Best-1(val acc:0.82) | lr:0.009567378324697062, weight decay:8.329914422037397e-07
Best-2(val acc:0.81) | lr:0.009548817455702163, weight decay:1.9982550859731867e-08
Best-3(val acc:0.8) | lr:0.009291306660458992, weight decay:2.2402127139457002e-07
Best-4(val acc:0.8) | lr:0.008381207344259718, weight decay:8.66434339086022e-08
Best-5(val acc:0.8) | lr:0.009034895918329205, weight decay:1.2694550788849033e-08
Best-6(val acc:0.78) | lr:0.0057717685490679006, weight decay:5.933415739833589e-08
Best-7(val acc:0.77) | lr:0.005287013083466725, weight decay:5.585759633899539e-06
Best-8(val acc:0.77) | lr:0.006997138970399023, weight decay:3.1968420191793365e-06
Best-9(val acc:0.77) | lr:0.007756581950864435, weight decay:1.0281187459919625e-08
Best-10(val acc:0.77) | lr:0.008298200180190944, weight decay:7.389218444784364e-06

もう一回
wight_decay:10^-6-10^-8
lr:0.01-0.001

image.png

Best-1(val acc:0.84) | lr:0.00971135118325034, weight decay:1.0394539789935165e-07
Best-2(val acc:0.83) | lr:0.009584343636422769, weight decay:3.1009381429608424e-07
Best-3(val acc:0.8) | lr:0.00832916652339643, weight decay:6.618592237280191e-07
Best-4(val acc:0.8) | lr:0.00959218016681805, weight decay:1.6405007969017657e-07
Best-5(val acc:0.78) | lr:0.006451172600874767, weight decay:4.0323875599954127e-07
Best-6(val acc:0.77) | lr:0.008024291255610844, weight decay:2.0050763243482884e-07
Best-7(val acc:0.77) | lr:0.009809009860349643, weight decay:4.934310445408953e-07
Best-8(val acc:0.77) | lr:0.009275309843754197, weight decay:5.343909279054936e-08
Best-9(val acc:0.76) | lr:0.00741122584285725, weight decay:1.588771824270857e-07
Best-10(val acc:0.75) | lr:0.006528687212003595, weight decay:1.3251120646717308e-07

76
70
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
76
70