L2.2 追一隻自己也在跑的狗:目標由自己產生的時候
本篇重用M5.2追一隻自己也在跑的狗:移動目標、EMA 與 Stop-Gradient·M1.1從鞋子猜身高:Conditional Expectation
起點:目標自己也在動
到目前為止,每一個訓練都有一個固定的標籤: 寫在資料裡,不會因為模型變好而改變。
但有一大類做法不是這樣:
- 自我蒸餾/mean teacher:用模型自己(或它的一個平均版本)對未標註資料的預測當標籤。
- 自我一致性:要求模型在兩個「應該給同樣答案」的輸入上一致(同一張圖的兩種裁切、同一段話的兩種寫法、相鄰的兩個時刻)。
- 任何 bootstrapping:目標的一部分是模型自己現在的輸出。
這些做法有一個共同的形狀:損失裡出現了兩次同一個模型。而這立刻帶來一個危險——
這個損失有一個完美的解:讓 變成常數。任何常數函數都讓它等於零,而且是全域最小。追一隻自己也在跑的狗,最省力的結局是兩個都停下來。
目標由模型自己產生的時候,
要靠什麼防止它塌成一個常數?
stop-gradient 與 EMA 各在防哪一件事?
課堂提問Q1
把「自己產生目標」翻成數學:平凡解為什麼是全域最小?stop-gradient 把哪一項從梯度裡拿掉了?拿掉之後那個更新式還是某個函數的梯度嗎?
先想一想,再展開看整理後的答案
平凡解。 若 ,那麼對任何 都有 ,所以 。損失非負,所以那是全域最小。而且不是唯一的——每一個常數都是全域最小,這一整個平坦的谷底就是問題所在。
stop-gradient 拿掉哪一項。 完整的梯度有兩項(記 ):
第一項是「把預測往目標拉」,第二項是「把目標往預測拉」。stop-gradient 把 當成常數,也就是只保留第一項:
它還是某個函數的梯度嗎?一般來說不是。 這件事很重要,因為它決定了「損失下降」這個保證還在不在。 是一個真正的梯度場,所以照它走一定會讓 下降(霧中下山,一步該走多大:梯度下降的一頁 的 Taylor 一階項)。 是一個半梯度:它是一個向量場,但沒有一個純量函數以它為梯度,所以「每一步都讓某個東西變小」這個保證消失了。
代價是實實在在的:一個沒有位能的向量場可以有旋轉的成分,於是動力系統可以繞圈、可以發散。本篇的實測會看到,把 stop-gradient 加到沒有錨的自我一致性上,模型不是塌掉——是炸掉(振幅 )。
所以三個東西必須分開問:誰讓非平凡解存在(錨)、誰讓動力系統穩定(EMA)、stop-gradient 到底改了什麼(動力系統的形狀)。
三個部件,三件不同的工作
錨(anchor):一個不由模型自己產生的目標。可以是少量真標籤、一個固定的預訓練模型、一個資料本身給的約束。它的工作是把平坦的谷底切掉——有了錨,常數函數不再是最小值,因為它在錨點上錯了。
EMA:把目標網路的權重換成當前權重的移動平均 (追一隻自己也在跑的狗:移動目標、EMA 與 Stop-Gradient)。它的工作是讓目標動得比預測慢。狗跑得慢,追的人才追得上; 越接近 1,狗越慢。這買到的是穩定,不是正確。
stop-gradient:把目標那一支從計算圖上切開。它的工作不是防止塌陷,而是改變了更新的向量場——通常也讓實作變便宜(不必反向傳播兩支)。它常常和 EMA 一起出現,於是被誤認為是同一件事的一部分。
為什麼 stop-gradient 不是防塌陷的機制
把上一節的三件事寫成一個可以檢查的判準。
非平凡解存不存在,是損失的性質,不是最佳化器的性質。 若 的全域最小集合包含常數函數,那麼任何忠實地最小化 的演算法都可能走到那裡去。stop-gradient 不改 ——它改的是你用什麼向量場去走。所以:
要讓平凡解不是最小值,唯一的辦法是改損失(加錨、加對比項、加去相關的約束),不是改梯度怎麼算。
stop-gradient 改的是動力系統。 只保留一項之後,更新式在線性情形下長成 ,而那個 不對稱。不對稱矩陣的特徵值可以是複數、可以有正實部——前者給旋轉,後者給發散。對稱的情形(完整梯度)不可能發生這兩件事,因為那時 是一個 Hessian,特徵值全實。
EMA 讓那個動力系統慢下來。 目標用 而不是 ,等於在系統裡插入一個低通濾波器:高頻的來回被抹掉, 時目標幾乎不動,於是每一輪都近似成一個固定目標的普通回歸。這解釋了它為什麼有效,也解釋了它的極限——它把不穩壓下來,但如果那個系統的平衡點本身就是平凡解,慢慢走還是走到那裡。
注意「BYOL 沒有負樣本也不會塌」是怎麼回事
自監督學習裡有一組著名的方法(BYOL、SimSiam 一類)宣稱不需要負樣本、只靠 stop-gradient 加上一個 EMA 的目標網路就不會塌陷。這和本篇的結論看起來衝突。
差別在於那些方法的損失不只有一致性項:預測支上多了一個 predictor 網路(一個不對稱的多餘層),而目標支沒有。這個不對稱讓「兩支都輸出常數」不再是那個動力系統的穩定點——後續的分析(例如 Tian et al. 2021 對 SimSiam 的特徵值分析)指出,predictor 加上 stop-gradient 的組合在特徵空間裡產生了一個把方差撐開的效應。
換句話說:它們不是靠 stop-gradient 本身,而是靠一個被 stop-gradient 切開的不對稱結構。 這符合本篇的判準——防塌陷的機制在目標的構造裡,不在「梯度怎麼算」裡。這個機制到現在仍然沒有一個完全乾淨的理論說明,所以實務上那類方法對超參數(、predictor 的大小)比一般監督式訓練敏感得多。
回到情境:什麼時候該用,怎麼設
先問有沒有錨。 打算用自我一致性或自我蒸餾之前,第一件事是找出「哪一部分的訊號不是模型自己給的」。半監督學習裡是那些真標籤;自監督裡是資料增強所定義的不變性加上某個防塌陷的結構;模型壓縮裡是那個固定的大模型。找不到錨的時候,先不要開始調 。
怎麼設。 記憶長度大約 步,所以 要對照「目標要多久才跟上一次真正的變化」。實務上 到 ;訓練初期模型變化快,常見做法是讓 從小值開始慢慢升到接近 1。
監控什麼。 損失下降完全不能當作進度——平凡解的損失是零。要監控的是輸出的散佈:預測的變異數、特徵的秩、或本篇用的「振幅」。塌陷的第一個徵狀是散佈開始單調縮小,而那時損失曲線看起來一切正常。
這一切依賴什麼。 一,錨真的獨立於模型;用模型自己挑出來的「高信心樣本」當錨,那不是錨。二, 夠大,讓每一輪近似成固定目標的回歸。三,下面實驗裡的線性模型讓我們能把振幅算得很乾淨;真實網路的塌陷有更多形式(特徵維度塌成低秩,而不是整個輸出變常數)。
回到情境:四種組合,四個結局
第一段:平凡解、發散、與錨。 共用 toy 的網格上取相鄰的一對 ,損失是「兩點的預測要一致」。模型是五次多項式(振幅上限足夠)。錨是三個真標籤點 ,權重 。真值 在 上的振幅是 :
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
四件事,一件一件讀。
第一列是平凡解。 完整梯度忠實地最小化那個損失,於是它找到了全域最小——振幅 ,模型幾乎是一條水平線。真值的振幅是 。損失確實掉到接近零,而學到的東西是零。
第二列是發散,不是塌陷。 加上 stop-gradient 之後,振幅衝到 。這是上一節那個「半梯度不是任何函數的梯度」的直接後果:那個不對稱的動力系統有正實部的特徵值。如果有人告訴你 stop-gradient 是用來防止塌陷的,這一列就是反例——它把一個會塌的問題變成一個會炸的問題。
第三列是 EMA 的真正作用。 振幅從 降到 ——六個數量級的改善,但仍然是錯的(真值 )。EMA 把發散放慢了,沒有把它變對。
後三列是錨的作用。 三個真標籤點一加進來,三種變體全部收在同一個地方(誤差 –,振幅 –)。有了錨之後,stop-gradient 與 EMA 幾乎不影響最終結果——它們影響的是路,不是終點。
錨決定非平凡解存不存在;EMA 決定路上穩不穩;stop-gradient 兩件都不決定。
(順帶一提,有錨的那三列振幅只有 ,遠低於真值 :兩百對一致性項對三個錨點,一致性的權重太大,把函數壓平了。一致性項的權重本身就是一個要調的旋鈕,太大就變成一個溫和版的塌陷。)
第二段:EMA 完全不同的第二個用途。 上面 EMA 用在目標上;它還有一個和塌陷無關的用途——用在最終模型上。民調只問一千人:小批次與噪聲 說固定 的 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 賺越多。 、 那一列:最後一步的權重是 ,超出直接解 ;EMA 權重是 ,只超出 ——拿掉了 的噪聲地板,而且完全不需要改訓練。、 那一列(噪聲本來就小)只從 改善到 ,因為那裡幾乎沒有地板可以拿掉。
這個用法有一個嚴格的理論依據(Polyak–Juditsky averaging,參考文獻 1):在適當條件下,平均後的估計量達到最佳的漸近變異數。兩個用途共用同一個 EMA 公式,但它們在解決完全不同的問題——一個在對付「目標在動」,一個在對付「軌跡在抖」。
錨、EMA、stop-gradient:三個開關,四個結局
互動 demo:損失一路掉,表示的東西卻塌成常數——要監看的是表示的振幅,不是損失。
先消化一下
參考文獻
- Polyak, B. T., Juditsky, A. Acceleration of Stochastic Approximation by Averaging. SIAM Journal on Control and Optimization 1992.(權重平均為什麼能達到最佳漸近變異數——本篇第二段實驗的理論依據。)
- Tarvainen, A., Valpola, H. Mean teachers are better role models. NeurIPS 2017.(EMA 目標網路在半監督學習裡的標準做法,以及 的實務範圍。)
- Grill, J.-B. et al. Bootstrap Your Own Latent. NeurIPS 2020.(沒有負樣本的自監督學習;本篇 Remark 討論的那個「不對稱結構才是防塌陷機制」的案例。)