L2.2 18 分鐘閱讀 2026年9月

L2.2 追一隻自己也在跑的狗:目標由自己產生的時候

本篇重用M5.2追一隻自己也在跑的狗:移動目標、EMA 與 Stop-Gradient·M1.1從鞋子猜身高:Conditional Expectation

起點:目標自己也在動

到目前為止,每一個訓練都有一個固定的標籤:yiy_i 寫在資料裡,不會因為模型變好而改變。

但有一大類做法不是這樣:

  • 自我蒸餾/mean teacher:用模型自己(或它的一個平均版本)對未標註資料的預測當標籤。
  • 自我一致性:要求模型在兩個「應該給同樣答案」的輸入上一致(同一張圖的兩種裁切、同一段話的兩種寫法、相鄰的兩個時刻)。
  • 任何 bootstrapping:目標的一部分是模型自己現在的輸出。

這些做法有一個共同的形狀:損失裡出現了兩次同一個模型。而這立刻帶來一個危險——

L(θ)=E[fθ(x)fθ(x)2]\mathcal L(\theta)=\mathbb E\big[\lVert f_\theta(x)-f_\theta(x')\rVert^2\big]

這個損失有一個完美的解:ff 變成常數。任何常數函數都讓它等於零,而且是全域最小。追一隻自己也在跑的狗,最省力的結局是兩個都停下來。

目標由模型自己產生的時候,
要靠什麼防止它塌成一個常數?
stop-gradient 與 EMA 各在防哪一件事?

課堂提問Q1

把「自己產生目標」翻成數學:平凡解為什麼是全域最小?stop-gradient 把哪一項從梯度裡拿掉了?拿掉之後那個更新式還是某個函數的梯度嗎?

先想一想,再展開看整理後的答案

平凡解。fθcf_\theta\equiv c,那麼對任何 x,xx,x' 都有 fθ(x)fθ(x)=0f_\theta(x)-f_\theta(x')=0,所以 L=0\mathcal L=0。損失非負,所以那是全域最小。而且不是唯一的——每一個常數都是全域最小,這一整個平坦的谷底就是問題所在。

stop-gradient 拿掉哪一項。 完整的梯度有兩項(記 r=fθ(x)fθ(x)r=f_\theta(x)-f_\theta(x')):

θL=2E[rθfθ(x)]2E[rθfθ(x)].\nabla_\theta\mathcal L=2\,\mathbb E\big[r\,\nabla_\theta f_\theta(x)\big]-2\,\mathbb E\big[r\,\nabla_\theta f_\theta(x')\big].

第一項是「把預測往目標拉」,第二項是「把目標往預測拉」。stop-gradient 把 fθ(x)f_\theta(x') 當成常數,也就是只保留第一項

gsg=2E[rθfθ(x)].g_{\text{sg}}=2\,\mathbb E\big[r\,\nabla_\theta f_\theta(x)\big].

它還是某個函數的梯度嗎?一般來說不是。 這件事很重要,因為它決定了「損失下降」這個保證還在不在。θL\nabla_\theta\mathcal L 是一個真正的梯度場,所以照它走一定會讓 L\mathcal L 下降(霧中下山,一步該走多大:梯度下降的一頁 的 Taylor 一階項)。gsgg_{\text{sg}} 是一個半梯度:它是一個向量場,但沒有一個純量函數以它為梯度,所以「每一步都讓某個東西變小」這個保證消失了。

代價是實實在在的:一個沒有位能的向量場可以有旋轉的成分,於是動力系統可以繞圈、可以發散。本篇的實測會看到,把 stop-gradient 加到沒有錨的自我一致性上,模型不是塌掉——是炸掉(振幅 1.3×1071.3\times10^7)。

所以三個東西必須分開問:誰讓非平凡解存在(錨)、誰讓動力系統穩定(EMA)、stop-gradient 到底改了什麼(動力系統的形狀)。

三個部件,三件不同的工作

錨(anchor):一個不由模型自己產生的目標。可以是少量真標籤、一個固定的預訓練模型、一個資料本身給的約束。它的工作是把平坦的谷底切掉——有了錨,常數函數不再是最小值,因為它在錨點上錯了。

EMA:把目標網路的權重換成當前權重的移動平均 θk+1=τθk+(1τ)θk\theta^-_{k+1}=\tau\theta^-_k+(1-\tau)\theta_k追一隻自己也在跑的狗:移動目標、EMA 與 Stop-Gradient)。它的工作是讓目標動得比預測慢。狗跑得慢,追的人才追得上;τ\tau 越接近 1,狗越慢。這買到的是穩定,不是正確。

stop-gradient:把目標那一支從計算圖上切開。它的工作不是防止塌陷,而是改變了更新的向量場——通常也讓實作變便宜(不必反向傳播兩支)。它常常和 EMA 一起出現,於是被誤認為是同一件事的一部分。

為什麼 stop-gradient 不是防塌陷的機制

把上一節的三件事寫成一個可以檢查的判準。

非平凡解存不存在,是損失的性質,不是最佳化器的性質。L\mathcal L 的全域最小集合包含常數函數,那麼任何忠實地最小化 L\mathcal L 的演算法都可能走到那裡去。stop-gradient 不改 L\mathcal L——它改的是你用什麼向量場去走。所以:

要讓平凡解不是最小值,唯一的辦法是改損失(加錨、加對比項、加去相關的約束),不是改梯度怎麼算。

stop-gradient 改的是動力系統。 只保留一項之後,更新式在線性情形下長成 θk+1=θkηAθk+ηb\theta_{k+1}=\theta_k-\eta A\theta_k+\eta b,而那個 AA 不對稱。不對稱矩陣的特徵值可以是複數、可以有正實部——前者給旋轉,後者給發散。對稱的情形(完整梯度)不可能發生這兩件事,因為那時 AA 是一個 Hessian,特徵值全實。

EMA 讓那個動力系統慢下來。 目標用 θ\theta^- 而不是 θ\theta,等於在系統裡插入一個低通濾波器:高頻的來回被抹掉,τ1\tau\to1 時目標幾乎不動,於是每一輪都近似成一個固定目標的普通回歸。這解釋了它為什麼有效,也解釋了它的極限——它把不穩壓下來,但如果那個系統的平衡點本身就是平凡解,慢慢走還是走到那裡。

注意「BYOL 沒有負樣本也不會塌」是怎麼回事

自監督學習裡有一組著名的方法(BYOL、SimSiam 一類)宣稱不需要負樣本、只靠 stop-gradient 加上一個 EMA 的目標網路就不會塌陷。這和本篇的結論看起來衝突。

差別在於那些方法的損失不只有一致性項:預測支上多了一個 predictor 網路(一個不對稱的多餘層),而目標支沒有。這個不對稱讓「兩支都輸出常數」不再是那個動力系統的穩定點——後續的分析(例如 Tian et al. 2021 對 SimSiam 的特徵值分析)指出,predictor 加上 stop-gradient 的組合在特徵空間裡產生了一個把方差撐開的效應。

換句話說:它們不是靠 stop-gradient 本身,而是靠一個被 stop-gradient 切開的不對稱結構。 這符合本篇的判準——防塌陷的機制在目標的構造裡,不在「梯度怎麼算」裡。這個機制到現在仍然沒有一個完全乾淨的理論說明,所以實務上那類方法對超參數(τ\tau、predictor 的大小)比一般監督式訓練敏感得多。

回到情境:什麼時候該用,怎麼設

先問有沒有錨。 打算用自我一致性或自我蒸餾之前,第一件事是找出「哪一部分的訊號不是模型自己給的」。半監督學習裡是那些真標籤;自監督裡是資料增強所定義的不變性加上某個防塌陷的結構;模型壓縮裡是那個固定的大模型。找不到錨的時候,先不要開始調 τ\tau

τ\tau 怎麼設。 記憶長度大約 1/(1τ)1/(1-\tau) 步,所以 τ\tau 要對照「目標要多久才跟上一次真正的變化」。實務上 τ=0.99\tau=0.990.99990.9999;訓練初期模型變化快,常見做法是讓 τ\tau 從小值開始慢慢升到接近 1。

監控什麼。 損失下降完全不能當作進度——平凡解的損失是零。要監控的是輸出的散佈:預測的變異數、特徵的秩、或本篇用的「振幅」。塌陷的第一個徵狀是散佈開始單調縮小,而那時損失曲線看起來一切正常。

這一切依賴什麼。 一,錨真的獨立於模型;用模型自己挑出來的「高信心樣本」當錨,那不是錨。二,τ\tau 夠大,讓每一輪近似成固定目標的回歸。三,下面實驗裡的線性模型讓我們能把振幅算得很乾淨;真實網路的塌陷有更多形式(特徵維度塌成低秩,而不是整個輸出變常數)。

回到情境:四種組合,四個結局

第一段:平凡解、發散、與錨。 共用 toy 的網格上取相鄰的一對 (x,x+0.02)(x,x+0.02),損失是「兩點的預測要一致」。模型是五次多項式(振幅上限足夠)。錨是三個真標籤點 x{0.05,0.5,0.95}x\in\{0.05,0.5,0.95\},權重 3.03.0。真值 sin(2πx)\sin(2\pi x)[0,1][0,1] 上的振幅是 2.002.00

import numpy as np
from ml_toy import truth
xs, ys = truth(); D = 5
mx = np.abs(np.vander(np.linspace(0,1,60), D+1, increasing=True)).max(0)
B  = lambda X: np.vander(X, D+1, increasing=True)/mx
xa = np.array([0.05, 0.5, 0.95]); ya = np.sin(2*np.pi*xa)          # 三個錨
xp = np.linspace(0, 1, 200); xq = np.clip(xp + 0.02, 0, 1)         # 相鄰的一對
Ap, Aq, Aa, As = B(xp), B(xq), B(xa), B(xs)

def train(stopgrad, anchor, tau, eta, T=20000, seed=0):
    rng = np.random.default_rng(seed)
    w = rng.standard_normal(D+1)*0.5; wt = w.copy()
    for k in range(T):
        ref = wt if tau is not None else w
        r = Ap@w - Aq@ref
        g = (Ap.T@r)/len(xp) if stopgrad else (Ap.T@r - Aq.T@r)/len(xp)   # 半梯度 vs 完整梯度
        if anchor: g = g + 3.0*(Aa.T@(Aa@w - ya))/len(xa)
        w -= eta*g
        if tau is not None: wt = tau*wt + (1-tau)*w
        if not np.isfinite(w).all(): return None
    f = As@w
    return f.max() - f.min(), ((f - ys)**2).mean()
真值的振幅 2.00;「誤差」是與真值的均方差(η=0.02,兩萬步)
  不 stop-grad、沒有 anchor        振幅       0.077   誤差       0.5374
  stop-grad、沒有 anchor           振幅 13017377.180   誤差 1.13e+14
  stop-grad + EMA(0.99)、沒有 anchor 振幅     34.187   誤差     569.6434
  不 stop-grad + anchor            振幅       0.676   誤差       0.3160
  stop-grad + anchor               振幅       0.671   誤差       0.3136
  stop-grad + EMA + anchor         振幅       0.672   誤差       0.3141

四件事,一件一件讀。

第一列是平凡解。 完整梯度忠實地最小化那個損失,於是它找到了全域最小——振幅 0.0770.077,模型幾乎是一條水平線。真值的振幅是 2.002.00損失確實掉到接近零,而學到的東西是零。

第二列是發散,不是塌陷。 加上 stop-gradient 之後,振幅衝到 1.3×1071.3\times10^7。這是上一節那個「半梯度不是任何函數的梯度」的直接後果:那個不對稱的動力系統有正實部的特徵值。如果有人告訴你 stop-gradient 是用來防止塌陷的,這一列就是反例——它把一個會塌的問題變成一個會炸的問題。

第三列是 EMA 的真正作用。 振幅從 1.3×1071.3\times10^7 降到 34.234.2——六個數量級的改善,但仍然是錯的(真值 2.002.00)。EMA 把發散放慢了,沒有把它變對。

後三列是錨的作用。 三個真標籤點一加進來,三種變體全部收在同一個地方(誤差 0.31360.31360.31600.3160,振幅 0.6710.6710.6760.676)。有了錨之後,stop-gradient 與 EMA 幾乎不影響最終結果——它們影響的是路,不是終點。

錨決定非平凡解存不存在;EMA 決定路上穩不穩;stop-gradient 兩件都不決定。

(順帶一提,有錨的那三列振幅只有 0.6720.672,遠低於真值 2.002.00:兩百對一致性項對三個錨點,一致性的權重太大,把函數壓平了。一致性項的權重本身就是一個要調的旋鈕,太大就變成一個溫和版的塌陷。)

第二段:EMA 完全不同的第二個用途。 上面 EMA 用在目標上;它還有一個和塌陷無關的用途——用在最終模型上。民調只問一千人:小批次與噪聲 說固定 η\eta 的 SGD 會在最小點附近繞著一個地板打轉,那麼「最後一步停在哪裡」有一半是運氣。把權重的移動平均拿出來用,就把那個抖動抹掉了:

wema = 0.99*wema + 0.01*w        # 每一步更新一次;最後拿 wema 去評估
(正交化特徵、真實風險;直接解 0.02744,不可約誤差 0.0225)
   η     B    最後一步的權重    EMA(0.99) 的權重
  0.05   1       0.02908           0.02854
  0.05   8       0.02831           0.02739
  0.15   1       0.03940           0.02934
  0.15   8       0.02893           0.02752

噪聲越大,EMA 賺越多。 η=0.15\eta=0.15B=1B=1 那一列:最後一步的權重是 0.039400.03940,超出直接解 0.011960.01196;EMA 權重是 0.029340.02934,只超出 0.001900.00190——拿掉了 84%84\% 的噪聲地板,而且完全不需要改訓練。η=0.05\eta=0.05B=8B=8 那一列(噪聲本來就小)只從 0.028310.02831 改善到 0.027390.02739,因為那裡幾乎沒有地板可以拿掉。

這個用法有一個嚴格的理論依據(Polyak–Juditsky averaging,參考文獻 1):在適當條件下,平均後的估計量達到最佳的漸近變異數。兩個用途共用同一個 EMA 公式,但它們在解決完全不同的問題——一個在對付「目標在動」,一個在對付「軌跡在抖」。

錨、EMA、stop-gradient:三個開關,四個結局

互動 demo:損失一路掉,表示的東西卻塌成常數——要監看的是表示的振幅,不是損失。

先消化一下

想一想

一個自監督訓練的損失從 0.80.8 平滑地降到 0.0020.002,看起來非常順利。要判斷它是不是在學東西,最該看的是:

想一想

根據本篇的實測,關於 stop-gradient 正確的說法是:

想一想

在有錨的三列裡,振幅都只有 0.6720.672(真值 2.002.00)。最合理的處方是:

想一想

下列哪一句不對

參考文獻

  1. Polyak, B. T., Juditsky, A. Acceleration of Stochastic Approximation by Averaging. SIAM Journal on Control and Optimization 1992.(權重平均為什麼能達到最佳漸近變異數——本篇第二段實驗的理論依據。)
  2. Tarvainen, A., Valpola, H. Mean teachers are better role models. NeurIPS 2017.(EMA 目標網路在半監督學習裡的標準做法,以及 τ\tau 的實務範圍。)
  3. Grill, J.-B. et al. Bootstrap Your Own Latent. NeurIPS 2020.(沒有負樣本的自監督學習;本篇 Remark 討論的那個「不對稱結構才是防塌陷機制」的案例。)