JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

讓訓練更快的最佳化器:動量、Adam 與排程

純梯度下降又慢又不穩——認識動量、Adam 與學習率排程,讓訓練又快又穩。

純 SGD 的困境:峽谷、平原與來回震盪

在上一篇你認識了訓練迴圈:前向傳播、損失、梯度,然後往下坡走一步。負責走這一步的引擎就是 隨機梯度下降——每一步它看一個 mini-batch,算出梯度(最陡上升的方向),再往反方向移動一小段。這一小段的大小由 學習率 決定。它簡單、誠實、而且有效。但在真實神經網路那種顛簸又蜿蜒的地形上,它可能慢得令人痛苦、抖得令人不安。這一節要讓你親身感受到原因,這樣本篇後面的內容讀起來就會像是解藥,而不是更多公式。

把損失曲面想成一片實體地景:權重的每一組設定就是一個位置,高度就是損失。訓練就是讓一顆球滾下坡,去找最低的山谷。麻煩出在這個山谷的形狀。一個非常常見的形狀是峽谷:一條又長又窄的峽道,橫切方向(兩側谷壁)很陡,但沿著谷底(你真正想前進的方向)幾乎是平的。純 SGD 永遠沿著梯度走,而在峽谷裡梯度大多指向最近的谷壁,而不是指向那條平緩的谷底。

結果會怎樣?球衝過谷底、撞上對面的谷壁、被彈回來、又衝過頭——它在兩側谷壁之間來回 Z 字形震盪,橫向亂彈,沿著谷底卻只往前蠕動一點點。要止住彈跳,你得把學習率調小,但那樣沿谷底的前進就會慢得像冰河。你被迫在「抖且衝過頭」和「穩但龜速」之間二選一。這就是峽谷問題。

還有另外兩種困境雪上加霜。在平原——一片又寬又幾乎全平的區域——每個方向的梯度都極小,於是每一步都微乎其微,球幾乎不動;訓練會停滯很多次迭代。而且因為每一步只用一個帶雜訊的 mini-batch、而非整個資料集,梯度本身會在步與步之間抖動,在所有問題之上再加一層顫動。陡峭的 Z 字震盪+平地龜爬+雜訊顫動,正是讓純 SGD 訓練緩慢的組合。解法是給這顆球一點記憶與一點自適應能力——這正是本篇後續要打造的東西。

在峽谷狀曲面上的梯度下降:純粹的步伐指向兩側谷壁,因此路徑在峽谷裡來回 Z 字形震盪,而不是沿著平緩的谷底筆直下行。

一張狹長山谷的等高線圖,箭頭路徑在陡峭谷壁之間來回彈跳,沿著谷底卻只緩慢前進。

動量:賦予梯度下降慣性

這是第一帖解藥。我們不再用此刻看到的梯度直接走,而是維護一個不斷累積過去梯度的「速度」,並用它來走一步。這正是一顆重球滾下坡時所做的事:它不會在每顆小石子前停下來重新瞄準。它會在一直被推的那個方向上累積速度,而它的慣性會帶著它直接穿過小凸起。這個累積出來的速度,就是我們所說的 動量

為什麼這能修好峽谷的 Z 字震盪?把兩個方向分開想。橫切峽谷時,梯度每一步都變號(先指右、再指左、再指右……),因為球一直在來回穿越谷底。當你把這些交替的推力加進一個累積速度裡,它們大致會互相抵消——左和右平均下來約等於零。但沿著谷底,梯度永遠指向同一個方向(下坡)。這些方向一致的推力會疊加起來,於是有用方向上的速度越長越大。結果就是:橫向彈跳變少、前進速度變穩。在平原上,同樣的累積會讓球即使在局部幾乎全平時仍能滑行向前。

v_{t+1} = \beta\, v_t + \nabla L(\theta_t), \qquad \theta_{t+1} = \theta_t - \eta\, v_{t+1}

動量更新:先把舊速度與新梯度混合,再依速度走一步。

讓我們逐個符號拆解。向量 v_t速度——我們對近期梯度的累積記憶(在第一步之前從零開始)。\nabla L(\theta_t) 是在目前權重 \theta_t 處的當前 mini-batch 梯度——球腳下新鮮的坡度。\beta(beta)是動量係數,一個介於 0 與 1 之間的數;它像摩擦力,決定有多少舊速度能存活到下一步。\eta(eta)是 學習率,整體的步幅大小。第一條式子說「新速度 =(舊速度的 \beta 比例)+(新鮮梯度)」。第二條說「把權重移動 \eta 乘以這個速度」。當 \beta = 0 時速度就只是今天的梯度,於是退化回純 SGD;把 \beta 調高就加入慣性。

現在來談那個著名的數字直覺。在係數 \beta 下,速度是一個指數加權平均,實際上記得約 1/(1-\beta) 個最近的梯度。標準選擇 \beta = 0.9 給出 1/(1-0.9) = 10:速度大致是最近十個梯度的平均。這個平均就是整個訣竅——把十個帶雜訊、左右交替的橫切峽谷梯度平均起來,它們幾乎抵消;把十個方向一致、沿谷底向下的梯度平均起來,你得到一股強而平滑的推力。具體來說,假設沿谷底的梯度每步都穩定為 +1。在 \beta=0.9 下速度會攀升 1, 1.9, 2.71, 3.44, \dots,朝向最高速度 1/(1-0.9)=10——大約是單一純步伐的十倍快。同時,橫切峽谷的梯度 +1, -1, +1, -1 會讓速度在零附近徘徊。記憶同時加速了好方向、抑制了壞方向。

加入動量後,橫切峽谷的交替推力在累積速度中互相抵消,而沿谷底一致的推力不斷累積,於是路徑變直、速度加快。

與先前相同的狹長山谷,但這次下降路徑平滑且幾乎沿谷底筆直向下,左右擺動已被抑制。

# One step of SGD with momentum (beta = 0.9)
# v starts as zeros, same shape as the parameters
def sgd_momentum_step(theta, v, grad, lr=0.01, beta=0.9):
    v = beta * v + grad          # blend old velocity with fresh gradient
    theta = theta - lr * v       # step the weights along the velocity
    return theta, v              # carry v into the next step
動量只是在 SGD 之上多兩行——但你必須把速度 v 從一步帶到下一步。

為每個參數調整步幅:從 RMSProp 到 Adam

動量修好了方向問題,但還有第二個、各自獨立的問題:尺度。一個神經網路有上百萬個權重,它們梯度的大小天差地遠。網路深處的某個權重可能一直看到很小的梯度,而靠近輸出的某個權重看到的卻很大。單一的全域 學習率 只能是個妥協——要小到不會炸掉大梯度的權重,這意味著它對小梯度的權重又太膽怯。如果每個參數都能有自己的步幅、自動依據它自身典型的梯度尺度來調整,那不是很好嗎?這就是自適應最佳化器的想法,我們分兩步來打造它。

第一步是 RMSProp。對每個參數,維護它平方梯度的移動平均。平方丟掉了正負號、只衡量大小,所以這個移動平均本質上是「這個參數的梯度通常有多大?」。然後在走一步時,把梯度除以這個平均的平方根。一個習慣性梯度很大的參數會被一個大數除,於是步幅縮小;一個梯度很小的參數會被一個小數除,於是步幅放大。每個參數最後都走出大致相當、合理的步幅——全域學習率不再是一視同仁的妥協。

第二步是 Adam(自適應動量估計,Adaptive Moment Estimation):就是把 RMSProp 的逐參數縮放,與上一節的 動量 想法結合,再加上一個叫偏差校正的技術修正。Adam 對每個參數維護兩個移動平均:一個類動量的梯度平均(讓它仍有慣性與方向平滑),以及 RMSProp 的平方梯度平均(讓它仍能自適應尺度)。以下是完整的更新式。

\begin{aligned} 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 &= \frac{m_t}{1-\beta_1^{\,t}}, \qquad \hat{v}_t = \frac{v_t}{1-\beta_2^{\,t}} \\ \theta_t &= \theta_{t-1} - \eta\, \frac{\hat{m}_t}{\sqrt{\hat{v}_t}+\epsilon} \end{aligned}

Adam 更新:一個動量平均、一個平方梯度平均、兩個偏差校正,以及一個經過縮放的步伐。

逐行來看。g_t 是第 t 步的當前 mini-batch 梯度。第一行建立 m_t,即一階動差——梯度的移動平均,就像動量;它攜帶方向。第二行建立 v_t,即二階動差——平方梯度 g_t^2 的移動平均,用來估計每個參數的梯度尺度(其變異數/大小),就像 RMSProp。衰減率為 \beta_1 \approx 0.9(梯度記憶持續多久,約 10 步)與 \beta_2 \approx 0.999(對尺度而言長得多的記憶,約 1000 步,使尺度估計平滑而穩定)。因子 (1-\beta_1)(1-\beta_2) 讓每一行都是恰當的加權平均,並維持在與原始梯度相同的尺度上。

現在來看第三行,偏差校正,這是大家容易卡住的地方。m_tv_t 在訓練前都從零開始。在最一開始那一步,m_1 = (1-\beta_1) g_1 = 0.1\,g_1——只有真實梯度的十分之一,因為這個平均裡大部分還是我們初始化用的那個零。早期的估計會偏向零。修正方法是除以 1-\beta_1^{\,t}1-\beta_2^{\,t},其中指數 t 是步數。在 t=1 時,1-\beta_1^1 = 1-0.9 = 0.1,所以 \hat{m}_1 = m_1/0.1 = g_1——恰好還原成完整大小。隨著 t 變大,\beta_1^{\,t} 縮向零,除數趨近 1,校正便悄悄自行關閉。所以偏差校正是一個暫時的加成,只在最初的幾十步才重要,用來防止一個錯誤地過小的起步。

最後是步伐:\theta_t = \theta_{t-1} - \eta\, \hat{m}_t / (\sqrt{\hat{v}_t}+\epsilon)。把這個分數讀成「沿動量方向 \hat{m}_t 移動,但對梯度大的參數(\hat{v}_t 大、分母平方根大)縮小移動、對梯度小的參數放大移動」。\eta 仍是整體的 學習率 旋鈕。\epsilon(epsilon,通常是 10^{-8})是一個極小的常數,只是加進去以免在某參數梯度幾乎為零時除以零——純粹的數值安全。小例子:若 \hat{m}=0.2\hat{v}=0.04,則 \sqrt{0.04}=0.2,於是步伐為 \eta \cdot 0.2/0.2 = \eta。逐參數縮放把一個大小為 0.2 的梯度規範成乾淨的單位大小步伐,正是我們想要的。這個組合——動量管方向、RMSProp 管尺度、偏差校正給公平的起步——就是為什麼 Adam 在許多問題上開箱即用。

# One step of Adam. m and v start as zeros; t is the step counter (1, 2, 3, ...)
def adam_step(theta, m, v, grad, t, lr=3e-4, b1=0.9, b2=0.999, eps=1e-8):
    m = b1 * m + (1 - b1) * grad          # first moment: momentum-like mean
    v = b2 * v + (1 - b2) * grad**2       # second moment: mean of squared grads
    m_hat = m / (1 - b1**t)               # bias-correct (matters only early)
    v_hat = v / (1 - b2**t)
    theta = theta - lr * m_hat / (v_hat**0.5 + eps)   # per-parameter step
    return theta, m, v
完整的 Adam。注意逐參數除以 sqrt(v_hat):每個權重實際上都得到自己的學習率。

何時用哪一種?SGD+動量 對上 Adam

你現在有兩個強力選擇:SGD 加動量Adam。該選哪一個?誠實的答案是「看情況」,但確實有一條清楚、立場鮮明的經驗法則,所以我直接告訴你,而不是含糊其辭。

預設就用 Adam(或 AdamW),尤其當你想要快速跑出個能動的東西、或不確定該如何設定 學習率 時。因為 Adam 逐參數自適應步幅,它對你挑的確切學習率寬容得多——很大一段範圍內的值通常都能順利訓練。它在前幾個 epoch 收斂得快,而且是 Transformer 與視覺 Transformer(ViT)的標準、預期選擇,那裡純 SGD 往往根本難以訓練。如果你在做原型、微調、或處理基於注意力的模型,Adam 是安全又有產出的起點。

當你要把卷積網路訓練到最佳的最終準確率、且願意細心調參時,就用 SGD 加動量。 經典的 ImageNet 配方(ResNet 與同類)就是 SGD+動量、\beta = 0.9、精心挑選的排程,加上權重衰減。坊間經驗(有大量實證支持)是:對 CNN 而言,SGD+動量往往泛化得稍好一些,能達到比 Adam 略高的測試準確率,即使 Adam 可能更快達到較低的訓練損失。代價是敏感:SGD+動量需要你找到好的學習率與排程,否則表現會打折。

現在再埋下一個名字:AdamW。它是 Adam 的現代預設變體,差別在於它如何處理 權重衰減——一種輕輕把權重拉向零、以防止過擬合的正則化技術。純 Adam 把權重衰減折進梯度裡,逐參數縮放會扭曲它;AdamW 把它「解耦」,當作一個乾淨、獨立的收縮來施加。你現在還不需要懂權重衰減——本軌道的第 4 篇專門講泛化,會好好解釋它。目前只要知道:「AdamW」是你在真實設定檔裡會看到的名字,而它是 Adam 那個更乖巧的表親。

學習率排程:暖身、衰減與餘弦退火

即使有了很棒的最佳化器,把 學習率 整段訓練都固定在同一個值仍是次佳的。訓練初期權重離任何好解都很遠,所以你想要大步,快速跨過地面。訓練後期你已接近谷底,大步只會讓你在最小值附近彈來彈去而無法安頓;此時你想要小步,精準地收斂進去。解法是 學習率排程:一份在訓練過程中改變學習率的計畫。比喻:你在漆黑房間裡朝一扇門衝刺,然後在感覺快到時放慢成小心翼翼的碎步,這樣才不會一頭撞上去。

有三個值得認識的部分。(1) 暖身(warmup):在最初的幾百到幾千步,把學習率從接近零線性地拉升到完整值。最一開始權重是隨機的、梯度估計很不穩定;在那之上再加一個完整大小的步伐可能會炸掉訓練。暖身保護這些脆弱的早期權重,而且對大 batch 與 Transformer 來說基本上是必備的。(2) 階梯衰減(step decay):先讓學習率維持固定,然後在選定的里程碑把它乘上一個固定因子(例如 ÷10)下降——經典的 ResNet 排程在第 30、60、90 個 epoch 砍學習率。(3) 餘弦退火(cosine annealing):不用突兀的下降,而是讓學習率沿一條餘弦曲線從最大值平滑滑落到接近零。餘弦是現代的最愛,因為平滑衰減傾向落入稍微更好的最小值。

\eta_t = \eta_{\min} + \tfrac{1}{2}\,(\eta_{\max} - \eta_{\min})\left(1 + \cos\!\left(\frac{\pi\, t}{T}\right)\right)

餘弦退火:學習率沿半個餘弦波,從 eta_max 滑落到 eta_min。

逐個符號:t 是當前步數,T 是總步數(餘弦階段的完整長度),而 \eta_{\max}\eta_{\min} 是學習率的高、低界。引擎是 \cos(\pi t / T) 這一項。在開始 t=0 時,角度為 0\cos 0 = +1;在結束 t=T 時,角度為 \pi\cos\pi = -1。所以餘弦從 +1 平滑掃到 -1,而 (1+\cos) 因子從 2 掃到 0,再由 \tfrac{1}{2} 重新縮放成從 1 滑到 0 的過程。乘上範圍再加上底值:\eta_t 從開始的 \eta_{\max} 滑到結束的 \eta_{\min},一開始下降快、接近尾聲時平緩趨平(因為餘弦在其極值處是平的)。

一個小例子讓它變得具體。設 \eta_{\max}=0.001\eta_{\min}=0T=100 個 epoch。在 t=0\cos(0)=1,所以 \eta = \tfrac{1}{2}(0.001)(1+1) = 0.001——完整學習率。在中點 t=50\cos(\pi/2)=0,所以 \eta = \tfrac{1}{2}(0.001)(1+0) = 0.0005——恰好一半。在 t=75\cos(3\pi/4)\approx-0.707,所以 \eta \approx \tfrac{1}{2}(0.001)(0.293) \approx 0.00015。在 t=100\cos(\pi)=-1,所以 \eta = 0。注意它到中點時就已是半速,接著在後段衰減得更快、再趨平到接近零——正是「靠近門口時小心碎步」的行為。

暖身有它自己的小公式。在最初的 t_{\text{warm}} 步,設 \eta_t = \eta_{\max}\cdot t / t_{\text{warm}}。直白地讀:在第 0 步學習率為 0;在第 t_{\text{warm}} 步它已線性爬升到完整的 \eta_{\max};中間則是完整值的直線比例 t/t_{\text{warm}}。例如 t_{\text{warm}}=1000\eta_{\max}=0.001 時,在第 500 步學習率為 0.001\times 500/1000 = 0.0005。這道緩坡讓最初幾次更新保持很小,好讓隨機的初始權重與帶雜訊的早期梯度先安定下來,於是訓練不會還沒開始就引爆。實務上你會把兩者縫在一起:前 t_{\text{warm}} 步線性暖身,其餘用餘弦退火。

組合起來:一次穩定又快速的訓練

讓我們把所有東西組裝成一份具體的配方。三個決定——最佳化器、基礎學習率、排程——並非各自獨立;你要把它們當作一組來選,好讓它們互相配合。以下是一份用於訓練現代視覺模型、看起來完全正常的設定,並附上每一行的理由。

# A sane, fast, stable starting recipe for a vision model
optimizer    = AdamW(lr=3e-4, betas=(0.9, 0.999), weight_decay=0.05)
batch_size   = 256
epochs       = 100
warmup       = 5          # epochs of linear warmup
schedule     = cosine_annealing(eta_max=3e-4, eta_min=0, total=epochs - warmup)
# Per step: if epoch < warmup: lr = 3e-4 * progress_through_warmup
#           else:              lr = cosine value for the remaining epochs
AdamW + 基礎學習率 3e-4 + 5 個 epoch 線性暖身 + 餘弦衰減到零,batch size 256。
  1. 最佳化器 = AdamW。它收斂快、能容忍很寬的學習率範圍,所以你花在調參的時間更少、用來迭代的時間更多。(只有在這是 CNN、且你要榨出最後一點準確率時,才換成 SGD+動量。)
  2. 基礎學習率 = 3e-4。這是廣為人知的「Adam 預設值」,適用於極大範圍的模型;它高到能讓前期快速進展,又不會高到讓 AdamW 的自適應步伐失穩。
  3. 暖身 = 5 個 epoch,從 0 線性升到 3e-4。在梯度還不可信之前,保護隨機的初始權重免於一個完整大小的步伐——在 batch size 256 以上是必備的。
  4. 排程 = 在其餘 95 個 epoch 內餘弦衰減到約 0。前期大步以探索,再溫和地縮小成小心翼翼的爬行,讓模型精準地安頓進一個好的最小值。
  5. Batch size = 256,選來填滿 GPU。它決定每個梯度是用多少樣本平均出來(每步雜訊更少),並與學習率互動——較大的 batch 通常需要較高的學習率與較長的暖身。

其中兩個旋鈕值得附上術語連結,方便你之後深入:AdamW 是最佳化器、學習率排程 是暖身加餘弦的計畫、而 256 的 batch size 控制每一步背後有多少真實資料。三者合在一起構成單一連貫的訓練策略,而非三個孤立的設定。

你怎麼知道它有沒有在運作?用第 1 篇的工具:損失曲線。用這份配方的健康訓練會顯示:訓練損失在暖身期間與剛結束時快速下降,接著彎成穩定、平滑的下滑,並隨餘弦把學習率拉向零而溫和趨平。如果損失在最初幾百步就尖刺暴衝或變成 NaN,代表你的暖身太短或基礎學習率太高。如果損失太早就停滯不動,那大概是學習率太低、或排程衰減得太快。讀曲線、調一個旋鈕、再跑一次。

與第 1 篇相同的訓練迴圈——但現在「更新權重」這一格由帶暖身與餘弦衰減的 AdamW 驅動,把又慢又抖的下降,變成又快又穩的下降。

訓練迴圈的循環圖:前向傳播、損失、反向傳播,然後是現在標註了最佳化器與學習率排程的權重更新步驟。