1.はじめに
今回は、最適化手法についてまとめます。
2.SGD
更新する重みパラメータをW, Wに関する損失関数を$\frac{\partial L}{\partial W}$とし、学習率をηとすると、
もう少し厳密な書き方。抽出されたm個の訓練集合{${x^{(1)}, ... , x^{(m)}}$}とそれぞれに対応する目標$y^{(i)}$を元にパラメータθによって構成される推論を行うための関数fの勾配を推定し、誤差Lが小さくなる様にパラメータθの更新を繰り返し行う。
calss StocasticGradient:
def __init__(self, lr=0.01):
self.lr = lr
def update_params(self, params, grads):
for key in params.keys():
params[key] -= self.lr * grads[key]
ここで、引数 params, grads はそれぞれ更新対象のパラメータ、誤差関数の各パラメータに対する勾配を格納する辞書とします。
3.Momentum
SGDは、単に勾配方向へ進むだけで、勾配が急であると行ったり来たりでジグザクな動きになるし、勾配が緩慢な場合は中々最適化が進まないという問題点があります。これを改善しようと提案されたのが、Momentumです。
Momentumでは、vという変数を加えて最初はゆっくりした速度で動き、勾配が同じ方向ならば徐々に加速させる工夫をしています。
当初は、αvがブレーキの役目を果たし(α=0.9などの値を設定)、徐々にこのブレーキが緩んで行くイメージです。
class Momentum:
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update_params(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]
momentum = α として、先程同様コードを書くとこんな形です。
4.ネステロフのMomentum
モメンタムには、ロスの小さな点を行き過ぎてしまうオーバーシュートが起こる問題があります。そこで、現在の速度を使ってさらに一歩更新した点から求めた勾配を元に更新することで、このオーバーシュートを軽減しています。
5.AdaGrad
パラメータ毎に最適な学習率が存在するという考え方で、学習過程で個別に最適化を図ることを適用的学習率を持つと言います。これ以降は、適応的学習率を持つアルゴリズムを説明します。
Adagradは、パラメータ毎に異なる学習率を設定しますると共に、全てのパラメータの学習率の補正として、過去の学習率の二乗和の平方根に反比例させます。
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)
6.RMSProp
AdaGradは、パラメータの勾配の二乗和を使いますが、これだと学習率の減衰が過剰気味になってしまう場合があります。RMSpropは、勾配の累計を指数関数的な重みを付けた移動平均に変更することで、非凸の条件下でAdaGradの性能を改善しています。
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)
7.Adam
基本的には、RMSpropとMomentumの組み合わせとみることができますが、2つの違いがあります。1つ目は、モメンタムは勾配の(指数関数的な重み付けのある)一次モーメントの推定として直接導入される点。2つ目は、一次モーメント(モメンタム項)と二次モーメント(中心化されていない)両方の推定へのバイアス補正が含まれていて、原点の初期化が考慮されている点です。
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)
コードが少しわかり難いので、self.iter +=1以降の更新式を補足します。
これは、self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
に相当します。
これは、self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
に相当します。
赤枠の左側が、lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)
に相当します。
赤枠の右側が、params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
に相当します。