5.2 動量與 Adam#
概念說明#
樸素梯度下降有兩大毛病:在狹長山谷裡之字折返,在平坦高原上龜速爬行。動量法(momentum)用物理直覺修補:把更新視為球在坡面上滾動,速度 \(\mathbf{v}\) 累積歷史梯度:
\(\beta \approx 0.9\) 表示「新速度 = 九成舊速度 + 一成新梯度」。效果有二:峽谷底部來回正負抵消的梯度分量被平均掉(之字形消失);同方向的持續梯度被累積放大(過平坦區加速)。理論上,對條件數 \(\kappa\) 的二次函數,動量法把迭代次數從 \(O(\kappa \log(1/\varepsilon))\) 降到 \(O(\sqrt{\kappa} \log(1/\varepsilon))\) 。Nesterov 動量是精修版:先沿速度預跳一步、再看該處梯度,好比「先探頭再看路」,收斂證明更乾淨、實務常略快。
Adam(Adaptive Moment Estimation)把兩個自適應概念打包:
- 一階動量 \(\mathbf{m}_k\) (梯度的指數移動平均)≈ 動量;
- 二階動量 \(\mathbf{v}_k\) (梯度平方的移動平均)≈ 每個座標的歷史尺度。
更新規則:
偏置修正(除以 \(1-\beta^k\) )補償初期平均的偏小。除以 \(\sqrt{\hat{v}}\) 實現逐參數步長:梯度一直很大的座標步長被自動縮小、梯度稀疏的座標保有較大步長——這正是它對深度學習中尺度懸殊的參數特別好用的原因。典型超參數: \(\alpha = 0.001\) 、 \(\beta_1 = 0.9\) 、 \(\beta_2 = 0.999\) 、 \(\epsilon = 10^{-8}\) ,幾乎不用調,故 Adam 成為深度學習的預設選擇。代價是每參數多兩份狀態(記憶體 ×3),且泛化表現有時不如仔細調過的 SGD+動量。
生活類比#
動量:重車下坡,踩油門的力度會疊加,方向上顛簸被車重抹平。Adam:一位替每個員工個別調整步調的主管——衝太猛的(梯度大)被提醒放慢,長期沉默的(梯度小)被鼓勵多走,整體仍朝平均方向前進。
演算法步驟#
Adam 完整流程:
- 初始化 \(\mathbf{x}_0\) 、 \(\mathbf{m}_0 = \mathbf{0}\) 、 \(\mathbf{v}_0 = \mathbf{0}\) 、時間步 \(t = 0\) 。
- 每輪: \(t \mathrel{+}= 1\) ;算 \(\mathbf{g}_t = \nabla f(\mathbf{x}_{t-1})\) 。
- 更新 \(\mathbf{m}_t\) 、 \(\mathbf{v}_t\) (指數移動平均)。
- 偏置修正得 \(\hat{\mathbf{m}}_t\) 、 \(\hat{\mathbf{v}}_t\) 。
- 更新 \(\mathbf{x}_t = \mathbf{x}_{t-1} - \alpha \hat{\mathbf{m}}_t / (\sqrt{\hat{\mathbf{v}}_t} + \epsilon)\) 。
- 收斂或達上限則停。
Python 範例#
import numpy as np
def gd_momentum(grad, x0, alpha=0.01, beta=0.9, n=300):
x, v = np.array(x0, float), np.zeros(len(x0))
for _ in range(n):
v = beta * v + grad(x)
x = x - alpha * v
return x
def adam(grad, x0, alpha=0.05, b1=0.9, b2=0.999, eps=1e-8, n=300):
x = np.array(x0, float)
m, v = np.zeros(len(x0)), np.zeros(len(x0))
for t in range(1, n + 1):
g = grad(x)
m = b1 * m + (1 - b1) * g
v = b2 * v + (1 - b2) * g * g
mh = m / (1 - b1**t)
vh = v / (1 - b2**t)
x = x - alpha * mh / (np.sqrt(vh) + eps)
return x
H = np.diag([100.0, 1.0]) # 病態二次函數
grad = lambda x: H @ x
x0 = [10.0, 10.0]
x_plain = np.zeros(2) # 樸素 GD 基準
for _ in range(300):
x_plain -= 0.018 * grad(x_plain)
print(f"樸素 GD 誤差: {np.linalg.norm(x_plain):.3e}")
print(f"動量法 誤差: {np.linalg.norm(gd_momentum(grad, x0)):.3e}")
print(f"Adam 誤差: {np.linalg.norm(adam(grad, x0)):.3e}")
在 \(\kappa = 100\) 的病態問題上,動量法與 Adam 的終點誤差通常比樸素 GD 低數個數量級。
複雜度與收斂性分析#
三者每步皆為 \(O(n)\) 向量運算(不含梯度本身),記憶體:GD \(O(n)\) 、動量 \(O(n)\) 、Adam \(O(3n)\) 。凸光滑情形:GD \(O(1/k)\) 、重球動量 \(O(1/k)\) 、Nesterov \(O(1/k^2)\) ;強凸二次函數收斂率分別約 \(O(\kappa)\) 、 \(O(\sqrt{\kappa})\) 。Adam 對一般凸問題有 \(O(1/\sqrt{k})\) 保證,且對梯度尺度不敏感。實務要訣:深度學習起步用 Adam,追求最佳泛化再試 SGD+動量+學習率排程。
練習#
- 把 5.1 節的 Rosenbrock 函數分別用樸素 GD、動量、Adam 各跑 5000 步,比較抵達 \(f < 10^{-8}\) 所需步數。
- 實作 Nesterov 動量,與普通動量在二次函數上比較迭代次數。
- 解釋偏置修正:若拿掉( \(\hat{m} = m\) ),初期步長會發生什麼?用 \(t=1\) 、 \(\mathbf{g}=1\) 的數值實驗驗證。