【読書メモ】ゼロから作るDeep Learning 6章

f:id:taigok:20160908173332j:plain

ゼロから作るDeep Learningの読書メモとしてまとめました。

本を読む目的

Coursera Machine Learningを受講してニューラルネットワークを理解したので、次はディープラーニングについて深く理解をしたいため。

前章までのまとめ

本記事で利用しているソースコードGitHub - oreilly-japan/deep-learning-from-scratch: 『ゼロから作る Deep Learning』のリポジトリを参考にしています。

最適化とは

最適化とは、損失関数の値を出来るだけ小さくするパラメータを見つけるような問題を解くこと。前章まではパラメータの勾配(微分)を使って勾配方向にパラメータを更新するステップを何回も繰り返す、確率的勾配降下法(Stochastic Gradient Descent:SGD)を最適化の手法として利用していたが、ここではSGDの欠点を改善する手法について説明。

SGDの欠点

目的とする関数の形状が等方的でないと、非効率な経路で探索してしまうため、学習に時間がかかってしまう。

SGDに代わる手法

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

AdaGrad

パラメータの要素の中で大きく更新された要素は学習係数が小さくなるような更新方法。

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}

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)

Adam

MomentumとAdaGradの融合。ソースコードは割愛。

どの更新手法を用いるか

多くの研究では今でもSGDを使っている。最近はAdamを好んで使っている人も増えた。全ての問題で優れた手法は無いので、色々試す必要がある。


重みの初期値

重みの初期値によって、ニューラルネットワークの学習がうまくいくかが決定することが多い。 結論からいうと、活性化関数にsigmoid、tanhなどのS字カーブを用いる場合はXavierの初期値、ReLUを用いる場合は、Heの初期値を用いるとよい。

Weight decay(荷重減衰)

重みパラメータの値が小さくなるように学習を行うことを目的とした手法。重みパラメータを小さくするのに小さい初期値から始める。単純に初期値を0(均一な値)にすればいいのではと思うが、それはダメ。理由としては、すべての重みの値が均一に更新されてしまい、ニューラルネットワークの表現力がなくなるため。よって、ランダムな初期値が必要。

勾配消失問題

重みの初期値に標準偏差が1のガウス分布を用いると、活性化関数がシグモイド関数の場合、各層のアクティベーションの結果は0と1に偏った分布になってしまう。こうなると、逆伝播で微分(勾配)の値が0に近くため、勾配の値が伝播するにつれ小さくなってしまい、層が深いほど0に近づいてしまうこと。よって、初期値を適切な値にして上げる必要があり、XavierやHeの初期値が考えられた。

Xavierの初期値

Xaivierの初期値は一般的なディープラーニングフレームワークで使われている。各層のアクティベーションを同じ広がりのある分布にすることを目的として、前層のノードの個数をnとして、{1\sqrt{n}}標準偏差をもつ分布を使う。Xavierの初期値は活性化関数が線形の場合に有効。

num = 100
# random.randn(num, num):標準正規(ガウシアン)分布による num × num の行列
w = np.random.randn(num, num) / np.sqrt(1.0 / num)

Heの初期値

活性化関数がReLUの場合に推奨されるのがHeの初期値。前層のノードの個数をnとして、{2\sqrt{n}}標準偏差をもつ分布を使う。

num = 100
w = np.random.randn(num, num) / np.sqrt(2.0 / num)

コード

初期値や活性化関数を変えて動かしてみる。

# coding: utf-8
import numpy as np
import matplotlib.pyplot as plt


def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def ReLU(x):
    return np.maximum(0, x)

def tanh(x):
    return np.tanh(x)
    
input_data = np.random.randn(1000, 100)  
node_num = 100 
hidden_layer_size = 5
activations = {}

x = input_data

for i in range(hidden_layer_size):
    if i != 0:
        x = activations[i-1]

    # 初期値の値を変える
    w = np.random.randn(node_num, node_num) * 1
    # w = np.random.randn(node_num, node_num) * 0.01
    # w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num) # 
    # w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)

    a = np.dot(x, w)

    # 活性化関数
    z = sigmoid(a)
    # z = ReLU(a)
    # z = tanh(a)

    activations[i] = z

for i, a in activations.items():
    plt.subplot(1, len(activations), i+1)
    plt.title(str(i+1) + "-layer")
    if i != 0: plt.yticks([], [])
    # plt.xlim(0.1, 1)
    # plt.ylim(0, 7000)
    plt.hist(a.flatten(), 30, range=(0,1))
plt.show()


Batch Normalization

重みの初期値によって、各層のアクティベーションの結果が変わり適切な重みの初期値を設定することで各層のアクティベーションの分布は適度な広がりを持ち、学習がスムーズに進んだことを確認した。Batch Normalizationは、アクティベーションの分布を強制的に適度な広がりを持つように調整しようというアイディアのことアクティベーションの分布が適度な広がりを持たないと、ニューラルネットワークが表現力を無くす、つまりたくさんのニューロンを使用している意味がなくなる。

Batch Normalizationのメリット

  • 学習を早くできる
  • 初期値にあまり依存しない
  • Overfitしない

Batch Normalizationレイヤの挿入

活性化関数レイヤの前(or後)にBatch Normalizationレイヤを挿入する。

Batch Normalizationの詳細

ミニバッチを単位として、ミニバッチごとにaデータの分布が平均が0で分散が1になるように正規化すること。

Batch Normalizationの評価

重みの初期値の標準偏差を様々な値に変えた時の学習経過が下記。Batch Normalizationを使用することで、重みの初期値にロバストになる。

f:id:taigok:20190202120733p:plain


過学習(Overfitting)

過学習とは訓練データだけに適応しすぎて、他のデータに適応できない状態のこと。過学習の原因としては、下記の2つが考えられる。

  • パラメータをたくさん持っている
  • 訓練データが少ない

Weight decay

過学習は重みパラメータが大きな値を取ることで起こることがしばしばある。そこで学習において大きな重みを取ることにペナルティを与えることで過学習を抑制することができる。この手法をWeight decay(荷重減衰)という。 具体的には損失関数に重みの二乗ノルム(L2ノルム)を加算する。 (ノルムとはベクトル空間における長さを示すもの。二乗ノルムはベクトルの長さ)

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

Dropout

過学習を抑制するためにニューロンを削除しながら学習する手法。訓練時にデータが流れるたびに消去するニューロンをランダムに選択する。 テスト時には全てのニューロンの信号を伝達するが、各ニューロンの出力に対して訓練時に削除した割合を乗算して出力。 Dropoutは学習時にランダムにニューロンを削除することによって、毎回別のモデルを学習させていることになるので、アンサンブル学習に近い発想。


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

ハイパーパラメータとは、重みやバイアスといった訓練の結果取得できるパラメータとは異なり、人が試行錯誤をして決定しなければいけないパラメータのこと。例えば下記のパラメータがハイパーパラメータ

検証データ(Validation data)

テストデータを使ってハイパーパラメータを調整するとテストデータに対して過学習してしまう。よって別途、検証データを用意してハイパーパラメータの調整を行う。まとめると下記になる。

  • 訓練データは重みやバイアスの学習に使用
  • 検証データはハイパーパラメータの性能評価のため使用
  • テストデータは汎化性能をチェックするために使用

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

ハイパーパラメータの最適化のポイントは、いい値が存在する範囲を徐々に絞り込んでいくこと。

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.multi_layer_net import MultiLayerNet
from common.util import shuffle_dataset
from common.trainer import Trainer

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

# 高速化のため訓練データの削減
x_train = x_train[:500]
t_train = t_train[:500]

# 検証データの分離
validation_rate = 0.20
validation_num = int(x_train.shape[0] * validation_rate)
x_train, t_train = shuffle_dataset(x_train, t_train)
x_val = x_train[:validation_num]
t_val = t_train[:validation_num]
x_train = x_train[validation_num:]
t_train = t_train[validation_num:]


def __train(lr, weight_decay, epocs=50):
    network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100],
                            output_size=10, weight_decay_lambda=weight_decay)
    trainer = Trainer(network, x_train, t_train, x_val, t_val,
                      epochs=epocs, mini_batch_size=100,
                      optimizer='sgd', optimizer_param={'lr': lr}, verbose=False)
    trainer.train()

    return trainer.test_acc_list, trainer.train_acc_list


# ハイパーパラメータのランダム探索======================================
optimization_trial = 100
results_val = {}
results_train = {}
for _ in range(optimization_trial):
    # 探索したハイパーパラメータの範囲を指定===============
    weight_decay = 10 ** np.random.uniform(-8, -4)
    lr = 10 ** np.random.uniform(-6, -2)
    # ================================================

    val_acc_list, train_acc_list = __train(lr, weight_decay)
    print("val acc:" + str(val_acc_list[-1]) + " | lr:" + str(lr) + ", weight decay:" + str(weight_decay))
    key = "lr:" + str(lr) + ", weight decay:" + str(weight_decay)
    results_val[key] = val_acc_list
    results_train[key] = train_acc_list

# グラフの描画========================================================
print("=========== Hyper-Parameter Optimization Result ===========")
graph_draw_num = 20
col_num = 5
row_num = int(np.ceil(graph_draw_num / col_num))
i = 0

for key, val_acc_list in sorted(results_val.items(), key=lambda x:x[1][-1], reverse=True):
    print("Best-" + str(i+1) + "(val acc:" + str(val_acc_list[-1]) + ") | " + key)

    plt.subplot(row_num, col_num, i+1)
    plt.title("Best-" + str(i+1))
    plt.ylim(0.0, 1.0)
    if i % 5: plt.yticks([])
    plt.xticks([])
    x = np.arange(len(val_acc_list))
    plt.plot(x, val_acc_list)
    plt.plot(x, results_train[key], "--")
    i += 1

    if i >= graph_draw_num:
        break

plt.show()

f:id:taigok:20190202133532p:plain