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
です。
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
の登場です。
Momentum
は、勾配の方向が変わらないうちは、勾配の更新度合いをだんだん大きくして行く手法です。それは、まさに地面の傾斜に合わせてボールが転がって行くイメージで、$\alpha=0.9$ は、地面の摩擦や空気抵抗と考えることが出来ます。
もうちょっとイメージを具体的に表現すると、例えば4回の勾配計算結果がいずれも$\frac{\partial L}{\partial W}$で同じだと仮定するとvは、
勾配を更新する度合いが、-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を求めることで代用します。これによって、オーバーシュートの抑制が期待できます。
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
を比較してみましょう。
Momentum
とNesterov
は、SGD
と比べて初期段階のロス低減スピード、最終ロス率ともに圧倒的に改善されています。Nesterov
はMomentum
より、さらにもう1段改善され、ロスのバラツキも若干小さくなっている様です。
6.AdaGrad
AdaGrad
からは、2つの重要なアイディアが導入されます。
1つ目は、膨大にあるパラメータを一括で最適化を図るのではなく、パラメータに応じて最適化を図るべきだという適応的な学習率
(Adaptive)という考え方です。
2つ目は、学習の初期は学習率を上げ、学習が進むにつれて学習率を下げて学習を効率よく進めるための学習係数の減衰
という考え方です。
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}$を掛けることで、学習率を補正します。
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
は、Momentum
と AdaGrad
の良いとこ取りをするアイディアから生まれたものです。
m
は Momentum
の指数移動平均みたいな形、v
はAdaGrad
そのものですね。その後に m
とv
を iterが小さい内は大きく利かせ、iterが大きくなるに従って利かせ具合を弱める補正をしています。実装は、下記の様に式を少し変形して行っています。
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
を比較してみます。
どんなタスクにもベストな結果を出す最適化手法はありません。今回は、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を掛ける。
具体的なイメージで言えば、こんな感じ、
また推論時は、maskを掛けずに信号全体に(1 - dropout_ratio)を掛けて、信号全体の大きさのみ調整しています。
11.Batch Normalization
Batch Normalization
は、2015年に発表された手法で、ミニバッチ毎に平均0, 分散1になるように正規化することで、学習収束速度の向上、Dropout
の必要性の低下、重み初期化の必要性低下(重みの初期化にロバスト)などの効果が得られます。下記が、アルゴリズムです。
12.Weight decay
Weight decay は、学習過程において、大きな重みを持つことにペナルティを課すことによって、過学習を抑制する手法です。
重みをWとするとき損失関数に、$\frac{1}{2}\lambda W^2$を加えることで、重みWが大きくなることが抑制でき、この方法をL2正則化と言います。ここで$\lambda$を大きくすれば、よりペナルティを大きく出来ます。
ちなみに、損失関数に$\lambda |W|$に加えたものを、L1正則化と言います。