NEWSIO+ 教育系列01 自己訓練一個 AI02 從照片還原 3D03 深度學習
主線 A · 模組 A3動手做 // 五個實驗台

Optimizer 賽跑

同一個坡,為什麼 Adam 比 SGD 快到底,又為什麼大家後來改用 AdamW?

A2 學會了往下坡走:算梯度,往反方向走一小步。但「走一步」有很多種走法。SGD 只看腳下;Momentum 記得剛才往哪走;RMSProp 讓每個權重有自己的步長;Adam 把兩者合起來。這個模組讓四顆球在同一個坡上比賽,再看兩件後來才發現的事:weight decay 放錯地方會失效,以及大模型的 optimizer 本身就吃掉大量記憶體。

SEC.01 / THE RACE

四顆球,同一個起點,同一個坡

A2 的球只有一個權重。現在有兩個權重,loss 變成一張地形圖,顏色越深越高。講義說純 SGD 有兩個典型麻煩:狹長山谷(一個方向很陡、另一個方向很平,球左右彈來彈去,往谷底的那個方向卻走得很慢),和鞍點(某些方向往上、某些往下,中間的梯度幾乎是 0,球停在那裡不動)。下面四顆球每一步都用真的公式在算,看誰先到門檻。

PROBE A / Optimizer 賽跑動手做 // 拖起點,換地形,按「播放」
SGD—
Momentum—
RMSProp—
Adam—

Loss 對步數(對數刻度,虛線是門檻)

現在顯示到第幾步 / 門檻—
怎麼讀。四顆球共用同一個 η,但意思不一樣:SGD 和 Momentum 的一步是「η × 梯度」,梯度大就走得大;RMSProp 和 Adam 先把每個方向的梯度除以它自己最近的大小,所以每一步大約就是 η。在狹長山谷裡,SGD 被陡的方向限制住 η(η 大了就彈出去),平的方向只能慢慢挪;RMSProp 和 Adam 把平的方向放大、陡的方向壓小,直接往谷底走。換到鞍點,起點刻意放在幾乎正中間:SGD 看到的側向梯度非常小,要很多步才滑下去;自適應的方法把小梯度放大,很快就離開。Momentum 在這幾個地形上常常也很快,這不是 bug:講義的寫法 v ← βv + g 在方向一致時等於把步長放大約 1/(1−β) 倍。換到彎曲山谷,Adam 反而最慢:η 小的時候它每步只走大約 η,沿著長長的彎道要走很多步,這正是講義說的「Adam 不是永遠比較好」。打開雜訊看看誰比較穩。地形、起點、門檻都是示意用的數字;Momentum 用講義 slide 10 的寫法,Adam 的 β₂ 固定 0.999、RMSProp 的 ρ 固定 0.9。
SGD 每一步都重新決定方向只看當下這一批資料的梯度。簡單、不用額外記憶體,但在山谷裡會左右彈。
Momentum 是有重量的購物車講義的比喻:短暫的顛簸不會立刻改變整體方向。左右震盪互相抵消,一致的方向越推越快。
Adam = 方向 + 尺度一階 moment m 管「往哪走」,二階 moment v 管「每個權重走多大步」。講義一句話:Momentum 解方向,RMSProp 解尺度,Adam 合併兩者。
細看 · Nesterov、bias correction、講義裡的視覺化觀察

Nesterov momentum:普通 momentum 是「在現在的位置算梯度,再加上慣性」。Nesterov 先假裝照慣性走一步,在預測的位置算梯度,再做真正的更新。講義的比喻是開車:不是到了彎道才煞車,而是轉彎前先估計煞車距離。實驗台裡打開「Nesterov」,在 Rosenbrock 類的彎谷特別看得出它比較少衝過頭。

gₜ = ∇L(θₜ − ηβvₜ₋₁) vₜ = βvₜ₋₁ + gₜ θₜ₊₁ = θₜ − ηvₜ

Adam 的完整一步:

mₜ = β₁mₜ₋₁ + (1−β₁)gₜ vₜ = β₂vₜ₋₁ + (1−β₂)gₜ² θₜ₊₁ = θₜ − η·m̂ₜ / (√v̂ₜ + ε)

典型預設 β₁ ≈ 0.9、β₂ ≈ 0.999,ε 只是防止除以 0。講義筆記提到:β₁ = 0 時 Adam 基本上就退化成 RMSProp。

為什麼要 bias correction:m 和 v 都是指數移動平均(EMA),從 0 開始。第一步 m₁ = (1−β₁)g₁ = 0.1·g₁,只有真正梯度的十分之一,被起始值 0 拉小了。所以要除以 (1−β₁ᵗ):m̂₁ = 0.1·g₁ / 0.1 = g₁,剛好補回來。隨著 t 變大,β₁ᵗ 趨近 0,修正就自動消失。v 的 β₂ = 0.999 更慢:第 1000 步時 1−β₂ᵗ 還只有約 0.63,要到幾千步之後修正才幾乎消失。講義提醒兩件事:這也是 warmup 和訓練初期行為互相糾纏的原因之一;實作上不要自己刪掉 bias correction 再拿預設的 learning rate 去比。

講義引用的視覺化觀察:在等高線地形上,Adagrad、Adadelta、RMSprop 幾乎一開始就朝對的方向走;Momentum 和 NAG 會先被帶偏、像球滾下山一樣衝過頭,但 NAG 因為「往前看」很快修正。在鞍點上,SGD、Momentum、NAG 很難打破對稱,後兩者最後才逃出來;自適應的方法很快就往負斜率的方向下去。

Full batch 和 mini-batch:每一步用全部資料,梯度雜訊小,但一步很貴,大資料幾乎不可行。mini-batch 矩陣運算效率高、容易平行,但梯度帶雜訊。講義說這個雜訊既是缺點,也可能形成 implicit regularization。實驗台的「mini-batch 雜訊」就是在每一步的梯度上加一點隨機擾動。

SEC.02 / PER-PARAMETER STEP SIZE

每個權重自己的步長:AdaGrad 會累積,RMSProp 會忘記

SGD 讓所有權重共用同一個 learning rate。講義指出這對「出現頻率不同的特徵」很不利:常出現的特徵每一步都有梯度,很少出現的特徵難得才有一次,卻需要比較大的更新。AdaGrad 的想法是記下每個權重梯度平方的總和 r,梯度一直很大的就走小步、很少更新的就走大步。問題是 r 只會增加,步長最後小到幾乎不動。RMSProp 把「一直加總」改成指數移動平均 v,舊的歷史會被忘掉。

PROBE B / 頻繁特徵與稀疏特徵動手做 // 拉時間軸 t,切換參數
這個參數每一步的梯度 |g|AdaGrad 有效步長 η/(√r+ε)RMSProp 有效步長 η/(√v+ε)
第 t 步這個參數的梯度 gₜ—
AdaGrad 累積 rₜ = rₜ₋₁ + gₜ²(只增不減)—
RMSProp 的 EMA vₜ = ρvₜ₋₁ + (1−ρ)gₜ²—
AdaGrad 有效步長—
RMSProp 有效步長—
兩者相差—
頻繁特徵每一步梯度大約是 1;稀疏特徵大部分時候是 0,每 20 步才出現一次,出現時也是 1。曲線用對數刻度,η = 0.1、ε = 0.01(ε 刻意放大是為了讓圖畫得下),這些都是示意用的數字。看兩件事:(1)AdaGrad 的藍線只會往下,因為 r 只會變大;到了第 100 步之後頻繁特徵變安靜了,它需要大一點的步長,AdaGrad 卻回不來。RMSProp 的橘線幾十步內就忘記舊的大梯度,步長回升。(2)切到稀疏特徵:AdaGrad 給它的步長比頻繁特徵大(這正是 AdaGrad 想要的),但一樣一路往下。把 ρ 拉到 0.99,RMSProp 記得比較久、回升得比較慢。
最近梯度大,下一步就保守一點把梯度除以它自己的大小,每個權重的有效 learning rate 就各自不同。這叫逐參數(per-parameter)縮放。
AdaGrad 的分母只會長大r 是所有過去梯度平方的總和。講義的註記:learning rate 最後會趨近 0。
EMA 讓舊歷史退場RMSProp 只看「最近一段」的梯度大小,分母不會無限長大。講義的說法:把一直累加改成 EMA,舊歷史會被忘掉。
細看 · 兩個公式、AdaDelta、為什麼 Transformer 特別依賴 Adam
AdaGrad:rₜ = rₜ₋₁ + gₜ² θ ← θ − η·gₜ / (√rₜ + ε)
RMSProp:vₜ = ρvₜ₋₁ + (1−ρ)gₜ² θ ← θ − η·gₜ / (√vₜ + ε)

運算都是逐元素(element-wise)的:每個權重有自己的 r 或 v。講義筆記的常用值:AdaGrad 的 η 約 0.01、ε 約 1e-8;RMSProp 的 ρ 約 0.9。

AdaDelta:另一個解決 AdaGrad 步長趨近 0 的方法,連初始 learning rate 都不用設,用之前的步長們估計下一步的步長。講義也指出這一類「固定時間窗口」的方法的風險:資料若突然變化,分母時大時小,訓練後期可能讓 learning rate 震盪、無法收斂。

為什麼 Transformer 特別依賴 Adam:講義列出 Transformer 內部各組參數的梯度尺度差異極大。Embedding table 稀疏、梯度變異大;Q/K/V projection 對數值尺度敏感;attention output 和 FFN 的量級隨深度累積;LayerNorm 的 scale 和 bias 維度很小卻影響很大。單一 learning rate 很難同時照顧這些群組。Adam 逐元素的正規化把跨層的尺度差異消掉,深層網路因此訓練得穩。這也是上面 PROBE A 狹長山谷的放大版:幾百萬個方向,每個方向陡度都不一樣。

SEC.03 / DECOUPLED WEIGHT DECAY

正則化放哪裡:為什麼 Adam + L2 不等於 AdamW

為了不讓模型過度擬合,常見做法是每一步把權重往 0 拉一點,叫 weight decay。另一種寫法是在 loss 加上 (λ/2)‖θ‖²,叫 L2 正則化。在 SGD 裡這兩件事完全一樣。但在 Adam 裡,L2 的那一項 λθ 會混進梯度,跟著梯度一起被 1/√v 除掉。結果是:歷史梯度大的權重幾乎沒被拉,歷史梯度小的權重被拉過頭。AdamW 的修正只有一件事:把 weight decay 從梯度裡拿出來,單獨做。

PROBE C / 兩個一樣大的權重,被拉回 0 的力道動手做 // 拉 λ 和兩個權重的 √v
Adam + L2:θ₁ 每步被拉回 ηλθ₁/√v₁—
Adam + L2:θ₂ 每步被拉回 ηλθ₂/√v₂—
AdamW:兩個都是 ηλθ—
θ₂ 被拉的力道是 θ₁ 的幾倍—
只看衰減,1000 步之後 θ 剩下多少(起點都是 1)—
兩個權重現在的值一樣大(θ₁ = θ₂ = 1),照理說應該被拉回 0 一樣多。θ₁ 是梯度一直很大的權重(√v 大),θ₂ 是梯度很小的權重(√v 小)。長條用對數刻度。Adam + L2 的橘色兩條差了好幾個數量級;AdamW 的兩條永遠一樣高,而且只由 η 和 λ 決定。這裡沿用講義的推導,假設 v 已經穩定、而且主要來自 loss 的梯度;√v 和 η、λ 的數字是示意。把兩個 √v 拉成一樣,兩種寫法的差距就消失:問題的根源正是「不同權重的 v 不一樣」。
SGD:L2 和 weight decay 是同一件事每個權重都乘上同一個 (1−ηλ),固定比例地縮小。
Adam:L2 被 adaptive scaling 扭曲λθ 混進梯度後也被 1/√v 除。講義原話:大梯度的權重罰太少、小梯度的權重罰太多。
AdamW 只改一個位置不改 Adam 的 m 和 v,只把 weight decay 移到梯度之外。講義的結論:用了 weight decay,就不必再用 L2。
細看 · 推導、AdamW 公式、Adam 的三大限制

SGD 裡 L2 = weight decay。設 loss 是 f(θ),L2 版本是 f̃(θ) = f(θ) + (λ/2)‖θ‖²,梯度多一項 λθ:

θₜ₊₁ = θₜ − η(∇f(θₜ) + λθₜ) = (1−ηλ)θₜ − η∇f(θₜ)

直接做 weight decay:梯度只來自 f,更新時另外乘上 (1−ηλ)。寫出來是同一條式子。

θₜ₊₁ = (1−ηλ)θₜ − η∇f(θₜ)

Adam 裡不等價。Adam + L2 把 g̃ = ∇f + λθ 送進 m 和 v,更新變成 η(gₜ + λθₜ)/(√vₜ+ε)。拆開看,衰減的那一項是 ηλθₜ/(√vₜ+ε):每個權重被除以不同的數,所以不再是固定比例。更糟的是 λθ 本身也污染了 v。

AdamW:θₜ₊₁ = (1−ηλ)θₜ − η·m̂ₜ/(√v̂ₜ+ε)

梯度和 v 保持乾淨,衰減量恆為 ηλθₜ,回到真正和權重大小成正比的縮小。這已經是框架的預設:PyTorch 的 torch.optim.AdamW 明確寫著 decoupled。講義筆記的實務規則:LayerNorm、BatchNorm 的 scale/shift 和 bias 通常排除在 weight decay 之外;大型公開設定(講義舉 Llama 2)是 AdamW + warmup 加 cosine、wd = 0.1、global-norm clip = 1.0。

Adam 的三大限制(講義):

  • Optimizer state 記憶體沉重:以 BF16 權重訓練時,還要 FP32 master weight、一階 moment、二階 moment,連同權重和梯度每個參數約 16 bytes,遠超過推論的成本。SEC.04 會算給你看。
  • 理論上的收斂缺陷(Reddi 等人):二階 moment 的 EMA 會讓有效步長忽大忽小、不單調,某些情況下會完全不收斂。AMSGrad 的修正是讓 v 只能變大:用 max(vₜ₋₁, vₜ)。
  • Training loss 和泛化不一致:自適應方法能很快把 training loss 壓低,但它偏好的解(implicit bias)和 SGD 不同,某些視覺任務上最終的 test accuracy 略輸 SGD + momentum。講義也提醒這不是普遍定律。

即使如此,講義的結論是 AdamW 仍是現代 Transformer / LLM 的成熟 baseline,Llama 3、OLMo 2 等大型公開 recipe 仍以 AdamW 為主,所以新的 optimizer 必須和一個公平、調好的 AdamW 比。

SEC.04 / LLM ERA

大模型時代:optimizer 本身就是記憶體帳單

SGD 不用存任何額外的東西。Momentum 每個參數多存一份速度。Adam 多存 m 和 v 兩份。模型小的時候這無所謂;到了幾十億、幾百億個參數,optimizer 的狀態比模型本身還大。講義的帳:用 BF16 權重訓練,加上 FP32 master weight、一階 moment、二階 moment,每個參數約 16 bytes。拆開來大約是:BF16 權重 2 bytes、FP32 master 4 bytes、m 4 bytes、v 4 bytes,再加上 BF16 的 gradient 2 bytes(這個拆法是我們的說明,講義只給總數)。拉參數數量看看。

PROBE D / Optimizer state 要多少 GB動手做 // 拉參數數量,按 7B / 70B
BF16 權重 2 BFP32 master 4 Boptimizer stategradient ≈ 2 B
參數數量—
AdamW 權重 + master + m + v + gradient(約 16 B / 參數)—
其中只是 m + v(optimizer state)—
換成 8-bit AdamW 省下—
AdamW 這些東西要塞幾張 80 GB 的卡(不含 activation)—
每參數的 bytes(不含 gradient):SGD 6(權重 + master,沒有 state);Momentum 10(多一份 FP32 速度);AdamW 14(加上 gradient 就是講義的 16);Adafactor 約 6(預設不存 m,v 只存矩陣的行和列,這裡假設 4096 × 4096 的方陣,state 幾乎是 0);Lion 10(只存一份 momentum);8-bit AdamW 8(m 和 v 各壓成 1 byte,忽略 block 的縮放係數)。這是概念上的帳:講義強調實際成本取決於 dtype、sharding 和實作,activation 也沒算進來;80 GB 一張卡是示意的單位。
PROBE D′ / batch 變大,learning rate 要跟著變多少動手做 // 輸入原本的設定
batch 變成幾倍 N—
線性縮放:η × N(同樣看過的樣本,更新量相同)—
√N 縮放:η × √N(權重更新的變異數不變)—
提醒—
講義引了兩條規則:batch 變 N 倍時,要讓「看過同樣多樣本後權重更新量相同」,learning rate 也乘 N(線性縮放,Goyal 等人 2017 在 ImageNet 一小時訓練完的論文用的就是它);要讓「權重的變異數不變」,則乘 √N(Hoffer 等人 2017)。講義也提醒:大 batch 不只是改 learning rate,warmup、critical batch size、梯度雜訊都會跟著變。預設數字是示意。
訓練記憶體 ≠ 推論記憶體推論只要權重。訓練還要 master weight、m、v、梯度,optimizer state 是訓練獨有的大成本。
省記憶體的三條路壓縮 v(Adafactor 只存行和列)、壓縮位元(8-bit)、少存一份(Lion 只留 momentum)。
下一代看的是整個矩陣Adam 把每個權重當獨立的數字。Muon 之後的方法把權重當矩陣、當 layer、當整個架構來決定怎麼更新。
細看 · Adafactor、8-bit、Lion、四代 optimizer、SAM / Lookahead、實務 baseline、診斷與對策

Adafactor:只存行和列。一個 R × C 的權重矩陣,Adam 的 v 也是 R × C。講義的例子:10240 × 10240 的矩陣,光是 v 就要存一億多個數。Adafactor 只存兩個向量:每一列梯度平方的 EMA(長度 R)和每一行的 EMA(長度 C),需要時用外積 (R ⊗ C) / mean(R) 近似回原本的 v。記憶體從 O(R × C) 降到 O(R + C)。講義的比喻是圖片壓縮:不存整張高解析度圖,只存水平和垂直的投影,用的時候再乘回去。代價是 v 是近似的,行為和 Adam 不完全一樣;對 embedding 和 Transformer 的大矩陣特別自然。

8-bit optimizer:把 m 和 v 量化成 8 位元,通常用 block-wise 的縮放、對離群值特別處理、小的 tensor 不量化。優點是記憶體和通訊都下降,很適合微調大模型;限制是量化的額外運算、離群值、長期累積的誤差需要驗證。

Lion:只留 momentum 的符號。透過程式搜尋找到的 optimizer:捨棄二階 moment,更新只用 momentum 的 sign(每個方向只走 +1 或 −1 乘上步長)。比 AdamW 少一半額外記憶體,計算也簡單。講義的注意事項:要重新調 learning rate 和 weight decay,不能直接套 AdamW 的參數;batch 小於 64 時效果不如 AdamW(sign 本身帶雜訊,小 batch 又加雜訊);優勢依任務而異,不是全面取代。

四代 optimizer:coordinate → matrix → norm → architecture(講義的整理):

  • Adam(coordinate-aware):逐個參數看。穩定,但忽略矩陣結構;Q/K/V、FFN 本來是大矩陣,攤平之後就丟掉了它們「把輸入放大多少」的幾何。
  • Muon(matrix-aware):把 hidden weight 當矩陣。先算 momentum M,再把它的 singular value 拉平:M = UΣVᵀ → UVᵀ。講義的 2 × 2 例子:SGD 的更新正比於 diag(100, 1),第一個方向壓過第二個;Muon 變成 diag(1, 1)。實作用 Newton–Schulz 只要幾次矩陣乘法,不用做完整的 SVD。注意它不是把權重 W 變成正交,是把更新方向正交化;bias、norm、embedding 通常仍用 AdamW 類的更新。
  • Scion(norm-aware):先替每種 layer 選一個合適的 norm,再用 LMO(linear minimization oracle)找「一步最多走 ρ,在限制內最逆梯度的方向」。用 Euclidean norm 得到 normalized SGD,用 max norm 得到 sign 類更新,用 spectral norm 得到 Muon 類更新。
  • Modular Norm(architecture-aware):連整個網路的組合方式都納入尺度,目標是模型變寬、變深之後,小模型調好的 learning rate 還能直接轉移過去。

SAM 和 Lookahead:不只看 training loss。Lookahead 維護一組快的權重和一組慢的權重,快的走 k 步後,慢的往快的內插,降低軌跡的變異、讓超參數比較不敏感。SAM(sharpness-aware minimization)先往梯度方向試探最糟的擾動,強迫找平坦的最低點,代價是接近兩倍的 forward / backward 時間。講義提醒:flatness 會受參數化方式影響,不能簡化成唯一的泛化解釋。

實務 baseline(講義):

情境合理的起點理由
CNN 監督式SGD + momentum + cosine泛化強、記憶體低
ViT / TransformerAdamW + warmup + decay參數尺度異質,自適應比較穩
LLM pretrainingAdamW baseline,再測 Muon / SOAP / Sophia要公平比較 token 數和時間
記憶體受限Adafactor、8-bit Adam、GaLore、Adam-mini降低 optimizer state
超大 batchLARS / LAMB / 調好的 AdamW逐層的 trust ratio
Fine-tuningAdamW,必要時 8-bit穩定、好調、省顯存

評估準則:loss 對 token 數、loss 對實際時間、optimizer state 記憶體、通訊量、下游任務品質。先建立強 baseline,再看新 optimizer 是否真的縮短「到達同樣品質的時間」。

三個「診斷 → 對策」(講義):

  • 早期不穩、loss 突然暴衝(spike)→ 加長 warmup、降低 peak learning rate、global-norm clip = 1.0,必要時把 ε 稍微放大。很多大模型都這樣設定(講義舉 Llama 2)。
  • 欠擬合、學不動→ 降低 weight decay、放鬆資料增強;檢查是不是誤把 Adam 當 AdamW 用了(SEC.03 的問題)。
  • 顯存吃緊→ 用 8-bit AdamW 或 8-bit Lion,或改用 Adafactor。
SEC.05 / MISCONCEPTIONS

常見誤解

講義最後列了六個認知偏差,這裡挑和本模組最相關的四個。共同的錯是把訓練 loss、泛化、速度和記憶體混為一談。

Adam 永遠比 SGD 好。Adam 通常比較好優化(training loss 降得快),但 SGD 有時泛化更好。在 CNN 上 SGD + momentum 仍是成熟的 recipe。
Adam + L2 就等於 AdamW。在自適應 optimizer 裡通常不等價:L2 那一項會被 1/√v 縮放(PROBE C)。要的是 decoupled weight decay。
batch 變大,只要把 learning rate 改一改。warmup、critical batch size、梯度雜訊都會跟著變。線性和 √N 只是起點。
換一個好 optimizer 能救所有問題。loss 寫錯、mask 錯、數值精度、多卡同步出錯,換什麼 optimizer 都會毀掉訓練。比較 optimizer 時也要公平:看 token 數、FLOPs、實際時間和調參預算,不是只看 epoch 或步數,因為每一步的成本可能不同(SAM 一步要接近兩倍的 forward / backward)。