SGD vs Momentum vs Adam オプティマイザ比較
同じ「細長い谷」の損失関数上で3種類のオプティマイザをそれぞれ実行し、収束経路と速度の違いを可視化します。
このケースを学ぶと、次のことが理解できます:SGD の振動、Momentum の慣性による平滑化、Adam の適応的ステップサイズ調整——3つの戦略の数学的原理と直感的効果。
生活からの導入
曲がった山道を下る——3つの戦略
SGD 戦略:足元だけを見て、毎回最も急な方向へ進む。狭い溝に当たると行ったり来たり弾む。
Momentum 戦略:雪だるま式——以前の速度方向を覚えていて、慣性で小さな窪みを乗り越える。
Adam 戦略:慣性だけでなく、路面の状況に応じて歩幅を調整する——急な所は小さく歩き、平らな所は大きくまたぐ。
直感的理解
SGD
theta -= lr * g
純粋な勾配方向のみ、履歴情報なし、急な方向で振動する
Momentum
v = beta*v + (1-beta)*g
theta -= lr * v
theta -= lr * v
履歴の勾配を累積し、慣性で振動を抑える
Adam
m = beta1*m + (1-beta1)*g
v = beta2*v + (1-beta2)*g^2
theta -= lr * m_hat/(sqrt(v_hat)+eps)
v = beta2*v + (1-beta2)*g^2
theta -= lr * m_hat/(sqrt(v_hat)+eps)
モーメンタム + 適応的学習率、各パラメータごとに独立してステップサイズを調整
テスト関数:\(f(x, y) = 0.5x^2 + 5y^2\)——x 方向は緩やかで、y 方向は急峻な細長い谷。
Python で手を動かす実践
実例
import numpy as np
def f(pos):
x, y = pos
return 0.5 * x**2 + 5 * y**2
def grad(pos):
x, y = pos
return np.array([x, 10 * y])
def sgd(start, lr=0.1, steps=60):
pos = np.array(start, dtype=float)
history = [pos.copy()]
for _ in range(steps):
pos = pos - lr * grad(pos)
history.append(pos.copy())
return np.array(history)
def momentum(start, lr=0.1, beta=0.9, steps=60):
pos = np.array(start, dtype=float)
v = np.zeros_like(pos)
history = [pos.copy()]
for _ in range(steps):
g = grad(pos)
v = beta * v + (1 - beta) * g
pos = pos - lr * v
history.append(pos.copy())
return np.array(history)
def adam(start, lr=0.3, beta1=0.9, beta2=0.999, eps=1e-8, steps=60):
pos = np.array(start, dtype=float)
m = np.zeros_like(pos)
v = np.zeros_like(pos)
history = [pos.copy()]
for t in range(1, steps + 1):
g = grad(pos)
m = beta1 * m + (1 - beta1) * g
v = beta2 * v + (1 - beta2) * (g ** 2)
m_hat = m / (1 - beta1 ** t)
v_hat = v / (1 - beta2 ** t)
pos = pos - lr * m_hat / (np.sqrt(v_hat) + eps)
history.append(pos.copy())
return np.array(history)
start = [4.0, 4.0]
results = {
"SGD": sgd(start, lr=0.1),
"Momentum": momentum(start, lr=0.1),
"Adam": adam(start, lr=0.3),
}
print("サンプル 3つのオプティマイザ比較 (開始点 [4,4]):\n")
for name, hist in results.items():
dists = np.linalg.norm(hist, axis=1)
mask = dists < 0.05
conv = (np.argmax(mask) if mask.any() else "未収束")
n_osc = np.sum(np.diff(np.sign(hist[:, 1])) != 0)
print(f" {name:10s}: {str(conv):>6s} ステップで収束, y振動{n_osc}回, 損失={f(hist[-1]):.6f}")
def f(pos):
x, y = pos
return 0.5 * x**2 + 5 * y**2
def grad(pos):
x, y = pos
return np.array([x, 10 * y])
def sgd(start, lr=0.1, steps=60):
pos = np.array(start, dtype=float)
history = [pos.copy()]
for _ in range(steps):
pos = pos - lr * grad(pos)
history.append(pos.copy())
return np.array(history)
def momentum(start, lr=0.1, beta=0.9, steps=60):
pos = np.array(start, dtype=float)
v = np.zeros_like(pos)
history = [pos.copy()]
for _ in range(steps):
g = grad(pos)
v = beta * v + (1 - beta) * g
pos = pos - lr * v
history.append(pos.copy())
return np.array(history)
def adam(start, lr=0.3, beta1=0.9, beta2=0.999, eps=1e-8, steps=60):
pos = np.array(start, dtype=float)
m = np.zeros_like(pos)
v = np.zeros_like(pos)
history = [pos.copy()]
for t in range(1, steps + 1):
g = grad(pos)
m = beta1 * m + (1 - beta1) * g
v = beta2 * v + (1 - beta2) * (g ** 2)
m_hat = m / (1 - beta1 ** t)
v_hat = v / (1 - beta2 ** t)
pos = pos - lr * m_hat / (np.sqrt(v_hat) + eps)
history.append(pos.copy())
return np.array(history)
start = [4.0, 4.0]
results = {
"SGD": sgd(start, lr=0.1),
"Momentum": momentum(start, lr=0.1),
"Adam": adam(start, lr=0.3),
}
print("サンプル 3つのオプティマイザ比較 (開始点 [4,4]):\n")
for name, hist in results.items():
dists = np.linalg.norm(hist, axis=1)
mask = dists < 0.05
conv = (np.argmax(mask) if mask.any() else "未収束")
n_osc = np.sum(np.diff(np.sign(hist[:, 1])) != 0)
print(f" {name:10s}: {str(conv):>6s} ステップで収束, y振動{n_osc}回, 損失={f(hist[-1]):.6f}")
サンプル 三种优化器对比 (起点 [4,4]): SGD : 未收敛 步收敛, y震荡36次, 损失=0.000035 Momentum : 21 步收敛, y震荡6次, 损失=0.000000 Adam : 16 步收敛, y震荡0次, 损失=0.000000
AI での応用シーン
| シナリオ | 推奨オプティマイザ | 理由 |
|---|---|---|
| ほとんどのタスク | Adam/AdamW | 適応的、収束が速い、チューニングが少ない |
| Transformer トレーニング | AdamW | GPT/BERT もこれを使う |
| 極限の精度が必要な場合 | SGD+Momentum | 汎化能力が時には向上する |