こんにちは、日沢です。
今回は「6.1パラメータの更新」についてまとめました。
パラメータの更新の手法を紹介していきます。
最適化について
これまでで、誤差逆伝播法を使ってニューラルネットワークを学習させるところまで勉強してきました。今回は、パラメータをうまく更新していく手法について見ていきます。
ニューラルネットワークは損失関数をできるだけ小さくするパラメータを見つけることが目的です。最適なパラメータを見つけることを最適化(optimization)といいます。
勾配降下法
今までやってきた最適化は、パラメータの勾配を使って勾配方向にパラメータを更新するという方法です。この手法を勾配降下法といいます。
勾配降下法にはいくつか種類があります。
・最急降下法(Gradient Descent):学習データすべての誤差を計算した後にパラメータを更新する。
・確率的勾配降下法(Stochastic Gradient Descent, SGD):ランダムに一つの学習データを選び、誤差を計算し、パラメータを更新する。
・ミニバッチ確率的勾配降下法(Minibatch SGD, MSGD):ランダムにいくつかの学習データを選び、誤差を計算し、パラメータを更新する。
確率的勾配降下法(SGD)
これまでの学習は、無作為に選んだデータ(ミニバッチ)を用いて学習しているので、確率的勾配降下法(以下、SGD)で最適化しています。ここでSGDについて復習しておきましょう。
SGDは数式で書くと、次のように表せます。
$$ W = W – \eta\frac{\partial L}{\partial W} $$
$$W: 更新するパラメータ、\eta: 学習率、\frac{\partial L}{\partial W}: Wに関する損失関数の勾配$$
つまり、もとのパラメータを勾配方向にずらしたものを新しいパラメータとするのです。ここで、左辺の2項目がマイナスであるのは、勾配の正負とパラメータをずらす方向は逆になるためです。学習率はどのくらい大きく勾配方向にずらすかを決めるハイパーパラメータです。
SGDの実装
では、SGDをクラスとして実装してみましょう。
#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]
lrが学習率(学習係数:learning rate)、paramsがパラメータ、gradが勾配です。keyはどのパラメータかを表しています。一番下は先ほどの式と同じ計算を行っています。
かなり単純なつくりであることがわかると思います。
SGDの欠点
単純で実装も簡単ですが、問題によっては非効率な場合があります。
ここで、次の関数の最小値を求める問題を考えます。
$$f(x, y) = \frac{1}{20}x^2 + y^2$$
この関数をグラフにする図1のようになります。

y方向に比べ、x方向の傾きが非常に小さいです。
この関数に対してSGDを適応させてみます。
(x, y)=(-7.0, 2.0)から探索を始めます。

緑、青、紫の線は先ほどの関数の等高線です。赤い点と線が最適化の経路です。
ジグザグしていて非効率です。
なぜこうなってしまうのでしょうか。
SGDは学習率×勾配をもとのパラメータに加えることでパラメータを更新しています。つまり、パラメータ毎の勾配の大きさに差があると、パラメータの更新速度に大きな差がついてしまいます。勾配が小さいパラメータに合わせた学習率にすると、勾配が大きいパラメータで図2のようにジグザグになってしまいます。逆に勾配が大きいパラメータに合わせるとx軸方向への進みが遅くなります(図3)。

以下ではSGDを改善した手法を紹介していきます。
Momentum
ジグザグで非効率的な更新経路を改善するために考えられたのが、次に説明するMomentum(モーメンタム)です。Momentumでは、もとのパラメータと新しく計算されたパラメータを足し合わせたパラメータが学習されます。
早速、式を見てみましょう。
$$v = \alpha v – \eta \frac{\partial L}{\partial W}$$
$$W = W + v$$
変数は基本的にSGDと同じですが、新しくvという変数が出てきました。これは、SGDにおける更新されたパラメータWにあたります。つまり、Momentumは、SGDで出したパラメータをさらに、もとのパラメータに足したものを学習しているのです。
αは勾配がなかった際に、徐々にパラメータを収束させるための変数です。0.9などの値を入れます。
物理を勉強した人なら知っているかもしれませんが、Momentumは「運動量」のことです。「運動量=質量×速度」と定義されています。Momentumでは、新しい変数vを速度に対応させています。αvの項は慣性を表していると考えるとわかりやすいかもしれません(実際はαがあるので慣性ではないですが)。
では、実装してみましょう。
#Momentumの実装
class Momentum:
"""Momentum SGD"""
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) #各パラメータのvに0を入れて初期化
for key in params.keys():
self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]
params[key] += self.v[key]
それでは、SGDと同様の問題を解いてもらいます。

SGDと比べてジグザグが軽減されています。
x軸では、小さいけれど同じ方向の勾配が何度もvに加えられるため、だんだん加速していきます。一方、y軸では、正負が交互に出てくるため、一つ前のvに打ち消され、なかなか加速しません。
Momentumと似た手法で、Nesterov(ネステロフ)というものがあります。Nesterovの更新式はMomentumとほぼ変わりませんが、Nesterovではもとのパラメータが正しいかどうかを確認してからMomentumと同様の計算を行うようです。
AdaGrad
Momentumは損失関数上で今までの動きを考慮することでSGDを改良していました。AdaGradは学習率を調節することでSGDの効率を上げています。
AdaGradの式を見てみましょう。
$$h = h + (\frac{\partial L}{\partial W})^2$$
$$W = W – \eta \frac{1}{\sqrt{h}} \frac{\partial L}{\partial W}$$
ほとんどSGDと変数は同じで、新たにhという変数が出てきました。これが学習率に影響します。hは勾配の2乗を足すことで更新します。ここで注意するのは、2乗は行列の要素ごとで行うという点です。この2乗は、単に正負を無くすためのものなので、気を付けてください。
下の式では、hの平方根の逆数が学習率となっています。それ以外は、SGDと変わりません。
では、実装してみましょう。
#AdaGradの実装
class AdaGrad:
"""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) #各パラメータのhを初期化
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)
複雑な点はないですが、ポイントとして学習率に1e-7という小さな値が加えられています。これは、hが0であった場合に0で除算することを防ぐためのものです。
では、上の2つの手法同様、問題を解いてもらいます。

はじめは勾配に合わせて大きく動いています。学習が進むにつれ、hにどんどん加算されていくので、学習率は小さくなっていきます。また、勾配の小さいx軸のほうが学習率が大きいことが、はじめの方からわかると思います。
AdaGradは学習を進めるほど学習率が小さくなるので、無限に学習した場合、全く更新されなくなります。これを改善したのがRMSPropです。だんだん過去の勾配を忘れ、新しい勾配が大きく反映されるようになっているようです。
Adam
Momentumでは今までの動きを反映させることで効率を上げました。AdaGradは学習率を変化させることで効率を上げました。Adamはこの二つを組み合わせた手法で、ハイパーパラメータのバイアス補正も組み込まれています。
式にすれば、以下のようになります。
$$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.$$
では、実装してみます。
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)
上の3手法と同様に、問題を解いてもらいます。

ちょうど、MomentumとAdaGradの中間のような結果になりました。
どの手法を用いるか?
これまで4つの手法を紹介してきました。
SGDから改善されていったと説明しましたが、SGDが優れていないわけではありません。問題によって、SGDがいいとか、AdaGradがいいとかがあります。
例えば、『ゼロから作るディープラーニング』ではMNISTのデータを使って比較しています。図7は損失関数の値をグラフにしています。

ここではAdaGradが一番良かったようです。二番はAdamのように見えます。
せっかくなので、他のデータではどうなるかを比較してみました。
まずは、SGDが得意そうなf(x, y) = x^2 /4 +y^2 /4です。




ここではAdaGradが一番よさそうです。二番がSDGですね。
おそらくMomentumがこの問題を苦手としているようなので、Adamはそれにつられていますね。
次は、f(x, y) = xsinx + ysinyです。これには特に理由はないです。




ここでもAdaGradが一番いいですね。(どこが最小値なのかはわかりませんが。)二番目はAdamです。SGDはx軸方向に揺れてるので、もう少し学習率を小さくすれば、効率は悪いかもしれませんが、上の等高線の真ん中にたどり着ける気がします。
あまりいい関数を知らないので、この程度しかできませんでしたが、もっと面白い形の関数でやってみたいです。
私調べでは、今のところAdaGradが一番いいですね。今度はAdaGradが苦手な関数を見つけてきたいです。
『ゼロから作るディープラーニング』では主にSGDやAdamを使うようですが、皆さんも好きな手法で試してみてほしいと思います。
また、Lasagneというディープラーニングのフレームワークに最適化手法がまとめて関数として実装されています。下記のURLから見れます。
https://github.com/Lasagne/Lasagne
読んでいただきありがとうございました。
参考
・【2020決定版 】スーパーわかりやすい最適化アルゴリズム -損失関数からAdamとニュートン法- – Qiita https://qiita.com/omiita/items/1735c1d048fe5f611f80
・Python/matplotlib3Dプロット!面と散布図を作成 | WATLAB -Python, 信号処理, AI- https://watlab-blog.com/2019/12/01/matplotlib-3dplot/
・勾配降下法の最適化アルゴリズムを概観する | POSTD https://postd.cc/optimizing-gradient-descent/