跳轉到

5.2 動量與 Adam#

概念說明#

樸素梯度下降有兩大毛病:在狹長山谷裡之字折返,在平坦高原上龜速爬行。動量法(momentum)用物理直覺修補:把更新視為球在坡面上滾動,速度 \(\mathbf{v}\) 累積歷史梯度:

\[ \mathbf{v}_{k+1} = \beta \mathbf{v}_k + \nabla f(\mathbf{x}_k), \qquad \mathbf{x}_{k+1} = \mathbf{x}_k - \alpha\, \mathbf{v}_{k+1} \]

\(\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\) (梯度平方的移動平均)≈ 每個座標的歷史尺度。

更新規則:

\[ \mathbf{m}_k = \beta_1 \mathbf{m}_{k-1} + (1-\beta_1)\mathbf{g}_k, \quad \mathbf{v}_k = \beta_2 \mathbf{v}_{k-1} + (1-\beta_2)\mathbf{g}_k^2 \]
\[ \hat{\mathbf{m}}_k = \frac{\mathbf{m}_k}{1-\beta_1^k}, \quad \hat{\mathbf{v}}_k = \frac{\mathbf{v}_k}{1-\beta_2^k}, \quad \mathbf{x}_{k+1} = \mathbf{x}_k - \alpha \frac{\hat{\mathbf{m}}_k}{\sqrt{\hat{\mathbf{v}}_k} + \epsilon} \]

偏置修正(除以 \(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 完整流程:

  1. 初始化 \(\mathbf{x}_0\) 、 \(\mathbf{m}_0 = \mathbf{0}\) 、 \(\mathbf{v}_0 = \mathbf{0}\) 、時間步 \(t = 0\) 。
  2. 每輪: \(t \mathrel{+}= 1\) ;算 \(\mathbf{g}_t = \nabla f(\mathbf{x}_{t-1})\) 。
  3. 更新 \(\mathbf{m}_t\) 、 \(\mathbf{v}_t\) (指數移動平均)。
  4. 偏置修正得 \(\hat{\mathbf{m}}_t\) 、 \(\hat{\mathbf{v}}_t\) 。
  5. 更新 \(\mathbf{x}_t = \mathbf{x}_{t-1} - \alpha \hat{\mathbf{m}}_t / (\sqrt{\hat{\mathbf{v}}_t} + \epsilon)\) 。
  6. 收斂或達上限則停。

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+動量+學習率排程。

練習#

  1. 把 5.1 節的 Rosenbrock 函數分別用樸素 GD、動量、Adam 各跑 5000 步,比較抵達 \(f < 10^{-8}\) 所需步數。
  2. 實作 Nesterov 動量,與普通動量在二次函數上比較迭代次數。
  3. 解釋偏置修正:若拿掉( \(\hat{m} = m\) ),初期步長會發生什麼?用 \(t=1\) 、 \(\mathbf{g}=1\) 的數值實驗驗證。