Optimizers
最適化(Optimization)¶
機械学習では、モデルを定義し、重みパラメータを調整して損失関数の値を減少させることで、性能を高めます。
この時、「いつ」・「どのようにして」・「どのぐらい」重みパラメータを調整するのか、を決めるのが optimizers
の役割です。
ここでは、最適化の基本である勾配降下法(Gradient Decent)のパラメータ更新方法を見ていき、その後勾配降下法の欠点を修正した様々な最適化手法を紹介します。
勾配降下法(Gradient Decent)¶
勾配降下法は非常に単純なアイデアで、損失関数の勾配ベクトル(最も傾きが急な方向)に注目し、その逆方向に進めば関数の最小値にたどり着くことができるだろう、というものです。
更新式は以下のようになります。
$$\mathbf{w} \leftarrow \mathbf{w} - \eta \frac{\partial E}{\partial \mathbf{w}}$$この時、$\eta$ は学習率と呼ばれ、どの程度勾配を下るかを決めるパラメータです。
$\eta$ と学習の関係¶
$\eta$ の値を変化させた時にどのように値が変化するかを、二乗和誤差 $E(\mathbf{w}) = \left\|\mathbf{y} - \mathbf{X}\mathbf{w}\right\|^2$ に対する線形回帰の場合で見ていきます。
この時、損失関数の勾配ベクトルは以下によって求まります。
$$ \begin{aligned} \frac{\partial}{\partial \mathbf{w}}E(\mathbf{w}) &= \frac{\partial}{\partial \mathbf{w}}\left\|\mathbf{y} - \mathbf{X}\mathbf{w}\right\|^2\\ &= \frac{\partial}{\partial \mathbf{w}}\left( \left\|\mathbf{y}\right\|^2 -2\mathbf{w}^{\mathrm{T}}\mathbf{X}^{\mathrm{T}}\mathbf{y} + \|\mathbf{Xw}\|^2\right)\\ &= -2\mathbf{X}^{\mathrm{T}}\mathbf{y} + \frac{\partial}{\partial \mathbf{w}}\left(\left(\mathbf{Xw}\right)^{\mathrm{T}}\left(\mathbf{Xw}\right)\right)\\ &= -2\mathbf{X}^{\mathrm{T}}\mathbf{y} + \frac{\partial}{\partial \mathbf{w}}\left(\mathbf{w}^{\mathrm{T}}\mathbf{X}^{\mathrm{T}}\mathbf{Xw}\right)\\ &= -2\mathbf{X}^{\mathrm{T}}\mathbf{y} + \left(\mathbf{X}^{\mathrm{T}}\mathbf{X} + \left(\mathbf{X}^{\mathrm{T}}\mathbf{X}\right)^{\mathrm{T}}\right)\mathbf{w}\\ &= 2\mathbf{X}^{\mathrm{T}}\left(\mathbf{Xw} - \mathbf{y}\right) \end{aligned} $$import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
#=== パラメータ ===
xmin=0
xmax=1
ETAs = [0.01,0.1,0.5]
EPOCH = 100 # エポック数
N = 100 # データ数
M = 3 # 重みのパラメタ数
#=== 学習データの生成/重みの初期化 ===
X = np.random.uniform(xmin, xmax, (N,1)) # shape=(N,1)
y = 1 + 2*X + np.random.randn(N,1)/10 # shape=(N,1)
b = np.ones(shape=(N,1)) # shape=(N,1)
Xb = np.c_[X,b] # shape=(N,2)
#=== テストデータ ===
X_test = np.c_[
np.linspace(xmin,xmax,N),
np.ones(shape=(N))
] # shape=(N,2)
Loss = np.zeros(shape=(EPOCH))
fig = plt.figure(figsize=(14,4))
for i,eta in enumerate(ETAs):
ax = fig.add_subplot(1, 3, i+1)
ax.scatter(X,y,label="data")
#=== 重みの初期化 ===
np.random.seed(123)
w = np.random.randn(2,1) # shape=(2,1)
for epoch in range(EPOCH):
grad = (1/N) * 2 * Xb.T.dot(Xb.dot(w)-y)
w = w-eta * grad # .reshape(-1,1)
if (epoch<10):
y_pred = X_test.dot(w)
ax.plot(X_test[:,0],y_pred,color="blue",alpha=0.1*(epoch+1))
y_pred = X_test.dot(w)
ax.plot(X_test[:,0],y_pred,color="red",label="prediction")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title(f"$\eta={eta}$")
ax.legend()
plt.tight_layout()
plt.show()
上の図から、アルゴリズムだけでなく、パラメータによっても学習の仕方が大きく異なることがわかります。
特に、ニューラルネットワークなどの局所解がいくつも存在する関数の場合、パラメータによって最終的に取る値が大きく異なることは容易に想像できます。
ですが、パラメータについて触れるのはここまでとし、ここからは他の最適化手法について見ていきます。
その他の最適化手法¶
# | 手法 | Kerasy |
---|---|---|
1 | SGD(Momentum SGD) | kerasy.optimizers.SGD(lr=0.01, momentum=0.0, decay=0.0, nesterov=False) |
2 | Adagrad | kerasy.optimizers.Adagrad(lr=0.01, epsilon=None, decay=0.0) |
3 | RMSprop | kerasy.optimizers.RMSprop(lr=0.001, rho=0.9, epsilon=None, decay=0.0) |
4 | Adadelta | kerasy.optimizers.Adadelta(lr=1.0, rho=0.95, epsilon=None, decay=0.0) |
5 | Adam | kerasy.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0, amsgrad=False) |
6 | Adamax | kerasy.optimizers.Adamax(lr=0.002, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0) |
7 | Nadam | kerasy.optimizers.Nadam(lr=0.002, beta_1=0.9, beta_2=0.999, epsilon=None, schedule_decay=0.004) |
SGD
SGD(Stochastic Gradient Decent)は基本的には勾配降下法と同じですが、更新するタイミングが異なります。
先ほどは $1\mathrm{epoch}$ ごとにまとめて更新を行なっていましたが、SGDでは $1$ サンプルごと、もしくはミニバッチごとに更新を行います。こうすることで、局所解にはまり込み、抜け出せなくなったパラメータを揺さ振ることで、局所解から抜け出すことが期待できます。
ところが、SGDにも欠点があります。以下の関数の最小値を求める問題を考えます。
$$f(x,y) = \frac{1}{200}x^2 + y^2$$この関数の勾配は以下で表されます。
x = np.arange(-5,5,1)
y = np.arange(-5,5,1)
X,Y = np.meshgrid(x,y)
dX = - 1/100 * X # ∂f/∂x = (1/10)x
dY = - 2 * Y # ∂f/∂y = 2y
plt.quiver(X, Y, dX-X, dY-Y, label="gradient")
plt.scatter(0,0, color="r", label="Optimal")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.show()
この時、勾配の方向が本来の最小値を向いていないことがわかります。
したがって、SGDの探索はジグザグに動くことが予測され、これは非効率です。
そこで、これを改良したのが Momentum SGD です。
Momentum SGD
Momentum SGDは、SGDにモメンタム(Momentum,運動量)の概念を加えた手法で、以下のように表されます。
$$ \begin{aligned} \mathbf{m} &\leftarrow \gamma\mathbf{m} - \eta\frac{\partial E}{\partial \mathbf{w}}\\ \mathbf{w} &\leftarrow \mathbf{w} + \mathbf{m} \end{aligned} $$この $\mathbf{m}$ は「速度」に対応する値であり、「物体が勾配方向に力を受け、その力によって物体の速度が加算される」と言う物理法則を表しています。なお、$\gamma$ は $0.9$ などの値をとり、これは「物体が何も力を受けないときには徐々に減速する」性質を表しています。
Adagrad
AdaGradの基本はSGDですが、パラメータ $\mathbf{w}$ の各成分ごとに異なる学習率(learning rate)を与える点が異なります。
これは、「稀にしか観測されない特徴は、その軸方向の勾配がほとんどの場合ゼロになっているため、学習が上手く行き届かない可能性がある。」という問題に対処するための方法で、各軸方向の勾配の二乗和を記憶しておき、学習率をその平方根(+微小量 $\varepsilon$)で割ることで、稀な特徴に対して学習率を高めにします。
$$ \begin{aligned} v_{i}^{t} &=v_{i}^{t-1}+\left(g_{i}^{t}\right)^{2} & \left(v_i^0=0\right)\\ w_{i}^{t+1} &=w_{i}^{t}-\frac{\eta}{\sqrt{v_{i}^{t}+\varepsilon}} \left(g_{i}^{t}\right) \end{aligned} $$しかしこの手法では、逆に一度勾配の急な場所を通ると、それ以降その軸の学習率が小さくなってしまうという問題点もあります。
RMSprop
Adagradでは、更新が繰り返されるほど実質的な学習率が小さくなり、値の更新が止まってしまいます。
これだと学習が十分に行き届かない可能性があるため、過去の勾配をそのまま記憶するのではなく、「過去の累積と直近の勾配を $\gamma:1-\gamma$ の比率で足しながら記憶」します。
$$ \begin{aligned} v_{i}^{t} &=\gamma v_{i}^{t-1}+(1-\gamma)\left(g_{i}^{t}\right)^{2} & \left(v_i^0=0\right)\\ w_{i}^{t+1} &=w_{i}^{t}-\frac{\eta}{\sqrt{v_{i}^{t}+\varepsilon}} \left(g_{i}^{t}\right) \end{aligned} $$すると、$v_i^t$ の更新式が
$$ \begin{cases} \begin{aligned} v_{i}^{t}&=\left(g_{i}^{t}\right)^{2}+\left(g_{i}^{t-1}\right)^{2}+\left(g_{i}^{t-2}\right)^{2}+\cdots & \text{(Adagrad)}\\ v_{i}^{t}&=(1-\gamma) \sum_{l=1}^{t} \gamma^{t-l}\left(g_{i}^{l}\right)^{2} & \text{(RMSprop)} \end{aligned} \end{cases} $$となることから、過去の情報が指数的に減衰しながら累積されていることが確認できます。
Adadelta
AdadeltaもAdagradを改良した手法で、$w_i^t$ が大きく更新された場合、さらに探索距離が広がるようにRMSpropに補正が入っています。
$$ \begin{aligned} v_{i}^{t} &=\gamma v_{i}^{t-1}+(1-\gamma)\left(g_{i}^{t}\right)^{2} & \left(v_i^0=0\right) \\ s_{i}^{t} &=\gamma s_{i}^{t-1}+(1-\gamma)\left(\Delta w_{i}^{t-1}\right)^{2} & \left(s_i^0=0\right) \\ \Delta w_{i}^{t} &=-\frac{\sqrt{s_{i}^{t}+\epsilon}}{\sqrt{v_{i}^{t}+\epsilon}} g_{i}^{t} \\ w_{i}^{t+1} &=w_{i}^{t}+\Delta w_{i}^{t} \end{aligned} $$※ なお、学習率 $\eta$ をさらに加えることもあります。
Adam
Adamは、以下の2つを組み合わせた手法です。
- 勾配の1乗を利用したモメンタム($m$)
- 勾配の2乗を利用したAdagrad($v$)
更新式は以下のようになります。
$$ \begin{aligned} m_{i}^{t} &=\beta_{1} m_{i}^{t-1}+\left(1-\beta_{1}\right) g_{i}^{t} & \left(m_i^0=0\right)\\ v_{i}^{t} &=\beta_{2} v_{i}^{t-1}+\left(1-\beta_{2}\right)\left(g_{i}^{t}\right)^{2} & \left(v_i^0=0\right)\\ \hat{m}_{i}^{t} &=\frac{m_{i}^{t}}{1-\beta_{1}^{t}} \\ \hat{v}_{i}^{t} &=\frac{v_{i}^{t}}{1-\beta_{2}^{t}} \\ w_{i}^{t+1} &=w_{i}^{t}-\frac{\eta}{\sqrt{\hat{v}_{i}^{t}}+\varepsilon}\hat{m}_{i}^{t} \end{aligned} $$Adamax
Adamの $v_i^t$ の更新式に現れる $\beta_2$ を $\beta_2^p$ の形に拡張することを考えます。
$p$ の値が大きくなると不安定になるため、一般に $p=1,2$ つまり、$l_1$ ノルム, $l_2$ ノルムが好んで使われますが、$l_{\infty}$ ノルムも安定的な振る舞いを見せることが知られています。
Adamaxはこの $l_{\infty}$ ノルムを更新式に用いた手法で、以下の更新式を用いて重みの更新を行います。
$$ \begin{aligned} v_i^{t} &=\beta_{2}^{\infty} v_i^{t-1}+\left(1-\beta_{2}^{\infty}\right)\left|g_i^{t}\right|^{\infty} \\ &=\max \left(\beta_{2} \cdot v_i^{t-1},\left|g_i^{t}\right|\right)\\ w_{i}^{t+1} &=w_{i}^{t}-\frac{\eta}{v_i^t}\hat{m}_{i}^{t} \end{aligned} $$Nadam
Adamは、「勾配の1乗を利用したモメンタム($m$)」と「勾配の2乗を利用したAdagrad($v$)」の2つを組み合わせた手法ですが、Nadamでは、モメンタム法の代わりに、それを拡張させたネステロフの加速勾配法(Nesterov's Accelerated Gradient Method)を利用します。
ネステロフの加速勾配法は、モメンタム法とは異なり、慣性による更新の後に勾配による更新が行われます。
Classical Momentum | Nesterov's accelerated gradient |
---|---|
$$ |
$$|