§01學習重點
- 說出「正則化」在嚴格與寬鬆兩種用法下各指什麼,並解釋早停為什麼會被算進來
- 把任何一個正則化手法回答成同一個問題:它在一群同樣能把訓練損失壓低的解裡,偏好哪一種
- 寫出損失加懲罰項的一般形式,說明係數 \(\lambda\) 為什麼是相對權重而不是開關
- 從「參數的先驗」推出平方懲罰項,並說出資料量變大時同一個先驗的影響為什麼會變小
- 親手掃一遍懲罰係數,看見訓練誤差與還原誤差往相反方向走,並指出懲罰過強時模型壞在哪裡
- 解釋隱式正則不是「有人偷偷加了一項」,並用同一個地形、兩種步長跑出兩個不同的終點
- 說出丟棄在訓練期與推論期為什麼不一樣,並算出推論期該怎麼補償
- 判斷一個資料增強的變換有沒有把標籤弄壞,並說明增強為什麼不是在製造新資訊
§02課程內容
一、先把「正則化」這個詞拆成兩半
正則化(regularization)這個詞有兩種用法,而且兩種都很常見,第一次讀到的人幾乎一定會在這裡卡住。
嚴格的那一種指的是一個很具體的動作:在原本的損失上外加一項,這一項只看參數、不看資料。寬鬆的那一種指的是任何能縮小「訓練時看起來很好、上線之後沒那麼好」這個落差的做法——早停算、資料增強算、隨機關掉一半的隱藏單元也算。
兩種用法並存會造成一個實際的困擾。你讀到「早停是一種正則化」,會忍不住反問:早停沒有在損失上加任何東西,它憑什麼算?這個反問是對的,而答案不是「寬鬆義比較隨便」,是兩者底下有同一個機制。把那個機制找出來,這一章就不會變成一份手法清單。
先接上一件前面剛講完的事。第 8 章處理的是怎麼量測與診斷模型的表現;本章處理的是量出問題之後怎麼改善。那一章末尾留下一個結論:當模型的參數比資料多,能把訓練損失壓到極低的參數組合不只一組,而是一整片。訓練損失完全分不出這一片裡哪一組比較好——它們的訓練成績一模一樣。
於是問題的形狀就變了。既然訓練損失分不出來,真正決定測試表現的是「你最後停在這一片裡的哪一個位置」。而本章所有手法,做的都是同一件事:
在一群同樣能把訓練損失壓得很低的解裡,偏好其中某一類。
差別只在偏好是從哪裡進來的。你自己在損失上寫了一項,那叫顯式正則化(explicit regularization);你什麼都沒寫,偏好卻從訓練流程、網路結構、資料處理、甚至最佳化演算法本身冒出來,那叫隱式正則化(implicit regularization)。這條軸就是本章的骨架,接下來每個手法都要回答同一句話:你站在這條軸的哪一端,你偏好什麼樣的解。
把場景固定下來會比較好談。整章我們待在同一個地方:一間盆栽園。園裡每一盆樹都有一份紀錄——主幹粗細、去年留下的枝條數、葉面積、盆器容積、日照時數、澆水頻率,一共十項數字。你要用這些數字預測一件事:下一季新枝的總生長量是幾公分。
麻煩在於,量得完整的樹只有六盆,而你想用的欄位有十項。十個權重、六條方程式——這種情況叫欠定(underdetermined):能把六筆訓練資料全部算得分毫不差的權重組合有無限多組。底下這段程式從這無限多組裡挑三組出來看:
import numpy as np
rng = np.random.default_rng(0)
# 盆栽園:量了 10 項數字 → 預測下一季新枝的總生長量(公分)
# 只量了 6 盆,卻有 10 個權重要定 → 方程式不夠用
N_TRAIN, D = 6, 10
truth = np.zeros(D)
truth[[1, 4]] = [2.5, -1.2] # 真正有影響的只有兩項量測
X = rng.normal(0, 1, (N_TRAIN, D))
y = X @ truth
w_min, *_ = np.linalg.lstsq(X, y, rcond=None) # 偏好「權重整體最短」
_, _, Vt = np.linalg.svd(X) # 零空間:沿它走,訓練誤差不動
w_far = w_min + 4.0 * Vt[N_TRAIN]
X_new = rng.normal(0, 1, (400, D)) # 400 盆沒看過的
y_new = X_new @ truth
for name, w in (("生成資料的那一組", truth),
("最小範數解 ", w_min),
("零空間偏移解 ", w_far)):
tr = np.sqrt(np.mean((X @ w - y) ** 2))
te = np.sqrt(np.mean((X_new @ w - y_new) ** 2))
print(f"{name} 權重長度={np.linalg.norm(w):5.3f} "
f"訓練RMSE={tr:.1e} 測試RMSE={te:5.3f}")這裡用到一個線性代數的名詞:零空間(null space)。它指的是所有「乘上 \(X\) 之後變成零向量」的方向;沿著這種方向移動權重,每一筆訓練資料的預測值都不會變,所以訓練誤差原地不動。程式就是靠它,從一個完美擬合的解走到另一個完美擬合的解。
實跑輸出:三組權重的訓練 RMSE 依序是 0.0e+00、2.0e-15、2.4e-15,在浮點數的精度內全部是零;但在四百盆沒看過的樹上,測試 RMSE 是 0.000、1.872、5.602。權重長度則是 2.773、2.150、4.541。
三件事值得記住。第一,產生資料的那一組權重就在這片集合裡,資料沒有騙人,只是六盆樹不足以把它指認出來。第二,「偏好權重整體最短」把你帶到長度 2.150 那一組——它比真解的 2.773 還短,測試 RMSE 是 1.872。偏好不保證挑中對的那一個,它只是把範圍縮小、把最離譜的排除掉。第三,如果完全不表態,你可能拿到長度 4.541 那一組,測試誤差是前者的三倍。
第二點請特別記牢,本章後面每一個手法都受它約束:偏好會縮小範圍,但不會變出資訊。 六盆樹裡沒有的東西,任何懲罰項都補不出來。
現在講全章最重要的一個澄清。很多人第一次聽到正則化,腦中的畫面是「模型太大了,所以要把它變小、變簡單」。上面這個例子直接推翻了這個畫面:十個權重從頭到尾都在,一個也沒有被拿掉,模型的大小完全沒動。改變的只是你在那片解集合裡站在哪個位置。正則化限制的是方向,不是規模。 它甚至常常是在模型大得離譜的前提下運作的——那正是解集合最大、最需要有人來挑的時候。
二、顯式懲罰:把偏好寫進損失裡
顯式的做法最直接:把偏好寫成一個函數,加到損失上。
逐項拆解:\(\boldsymbol\phi\) 是模型的全部參數;\(\mathcal{L}[\boldsymbol\phi]\) 是原本的損失,也就是訓練資料上每一筆損失的平均(有些教材寫成總和,差一個正的常數倍,最小值落在同一個地方);\(g[\boldsymbol\phi]\) 是懲罰函數,它只看參數、不看任何一筆資料——這是它跟損失最根本的差別;\(\lambda\) 是懲罰係數,一個不會被訓練改動的正數;\(\mathcal{L}_{\lambda}\) 就是訓練時真正拿去最小化的那個目標,下標提醒你它會隨 \(\lambda\) 改變。
最常用的 \(g\) 是權重的平方和:
逐項拆解:\(\boldsymbol\Omega_k\) 是第 \(k\) 層的權重矩陣,\(\Omega_{k,ij}\) 是它第 \(i\) 列第 \(j\) 行那個數;外層對 \(k\) 求和是把每一層都算進去,內層對 \(i,j\) 求和是把該層每一個權重都算進去。平方讓正負權重一視同仁,也讓「大權重」被罰得特別重——權重從 1 變成 2,這一項多的不是 1 而是 3。
\(\lambda\) 是相對權重,不是開關。 訓練要同時壓低兩個東西,而它們通常互相拉扯:把資料配得更貼,權重就得長大;把權重壓小,資料就配得沒那麼貼。\(\lambda\) 決定的是拉鋸中哪一邊出力比較大。\(\lambda = 0\) 等於完全不表態,\(\lambda\) 大到極端則等於完全不管資料。它自己是一個超參數(hyperparameter),必須用驗證集挑——這是第 8 章那條紀律的直接應用,而且代價不小:每換一個 \(\lambda\) 就得重訓一次。
為什麼「權重小」會讓函數變平緩?看一層在做什麼:\(\mathbf{z}_{k} = \boldsymbol\Omega_{k}\mathbf{h}_{k} + \mathbf{b}_{k}\),其中 \(\mathbf{h}_k\) 是這一層收到的活化、\(\mathbf{z}_k\) 是送出去的預活化。輸入動一小步 \(\delta\),輸出就動 \(\boldsymbol\Omega_k\delta\)。權重越小,同樣的輸入變化引起的輸出變化越小。每一層都是這樣,一層接一層乘起來,整個網路對輸入的敏感度就被壓下來了——輸出隨輸入變化緩慢,畫成曲線就是平滑的那種。
比喻: 盆栽園裡有一種基本功叫綁線定型:拿一條軟金屬線順著枝條纏上去,讓它每天都往你要的方向偏一點點。線的力道不大,也不會把枝條硬扳到定位;樹照自己的節奏長,只是每一天都被同一個方向偏了一些,一季下來形狀就出來了。懲罰項就是這條線——它不指定任何一個權重該等於多少,它只是在每一步都讓「往某個方向走」變得比較划算。這個比喻在一處失準:線是外力,鬆手就少一分力,樹本來要長成什麼樣是它自己的事;而懲罰項是被寫進目標函數裡的,它跟原本的損失是同一場拉鋸的兩邊,不是外面附加上去的東西。這個差別在下一段會變得很重要——正因為兩邊在同一個目標裡搶份量,罰過頭就會直接把擬合弄壞。
把它跑出來看。這次換一個更貼近日常的設定:日照時數對新枝生長量的影響,真實關係是一條和緩的曲線,但我們刻意用一個九次多項式當函數族,而訓練資料只有十二盆:
import numpy as np
rng = np.random.default_rng(0)
# 盆栽園:日照時數 → 下一季新枝生長量(公分)。真實關係是一條和緩的曲線
def true_curve(s):
return 6.0 + 3.2 * s - 0.38 * s ** 2
def design(s, deg=9): # 刻意取一個比需要更大的函數族
u = (s - 5.0) / 4.0 # 置中縮放,免得高次項數值爆掉
return np.stack([u ** k for k in range(deg + 1)], axis=1)
s_tr = np.sort(rng.uniform(1.0, 9.0, 12))
y_tr = true_curve(s_tr) + rng.normal(0, 1.0, s_tr.size)
s_ck = np.linspace(1.0, 9.0, 300) # 密集取點,量「整條曲線」還原得多準
y_ck = true_curve(s_ck)
A, A_ck = design(s_tr), design(s_ck)
P = np.eye(A.shape[1]); P[0, 0] = 0.0 # 常數項不罰(它只把曲線整體上下平移)
print(" λ 受罰權重長度 訓練RMSE 曲線RMSE")
for lam in (0.0, 1e-6, 1e-4, 1e-3, 1e-2, 1e-1, 1.0, 1e2):
w = np.linalg.solve(A.T @ A + lam * len(s_tr) * P, A.T @ y_tr)
tr = np.sqrt(np.mean((A @ w - y_tr) ** 2))
ck = np.sqrt(np.mean((A_ck @ w - y_ck) ** 2))
print(f"{lam:9.0e} {np.linalg.norm(w[1:]):10.2f} {tr:10.3f} {ck:10.3f}")實跑輸出的八行,受罰權重長度從 714.80 一路降到 0.01;訓練 RMSE 從 0.321 一路升到 2.184;而「曲線 RMSE」——拿三百個密集取樣點量整條真實曲線還原得多準——走的是一條 U 形:4.521、2.542、0.872、0.624、0.683、0.995、2.135、2.561,最低點落在 \(\lambda = 10^{-3}\)。
這張表要讀出三件事。左邊那一欄證實了懲罰確實在做它宣稱的事:權重被壓下去了。中間那一欄告訴你這件事有代價:訓練成績單調變差。右邊那一欄才是重點——懲罰不是越多越好。\(\lambda = 0\) 時曲線 RMSE 是 4.521,模型死記了十二個點、點與點之間亂長一氣;\(\lambda = 100\) 時是 2.561,曲線被壓得太平、連真實的彎曲都不見了,這叫欠擬合。兩頭都糟,中間才對。
順帶把一個小設定講清楚:程式裡的 P[0, 0] = 0.0 是在說「常數項不罰」,對應到網路裡就是懲罰項一般不作用在偏置上。理由很單純:偏置只把整條輸出曲線上下平移,不改變它有多陡。把偏置壓向零,等於你在假設「輸出的整體高度本來就該接近零」,而那個高度該是多少完全由任務決定——盆栽的生長量平均是十公分還是一百公分,跟模型該不該平滑毫無關係。
懲罰項的另一個身分:參數的先驗。 第 5 章示範過怎麼從一個機率假設把損失導出來,現在只多加一樣東西——對參數本身也給一個機率分布。這叫最大後驗(maximum a posteriori)準則:
逐項拆解:連乘的那一塊是「在這組參數下,看到手上這批資料的機率」,也就是第 5 章的概似;\(Pr(\boldsymbol\phi)\) 是先驗(prior),你在看資料之前對參數的合理範圍的陳述;兩者相乘之後取最大,挑的就是「既能解釋資料、又符合事前偏好」的那一組。取負對數把乘積拆成加總,再除以 \(I\) 換成平均口徑:
前半段就是原本的損失。後半段是新東西。給每個參數一個平均為零、標準差為 \(\sigma_p\) 的常態先驗:
逐項拆解:\(Z\) 是讓機率積分等於一的正規化常數,它與 \(\boldsymbol\phi\) 無關,所以在找最小值時可以整項丟掉;剩下的就是我們熟悉的平方和,前面掛著一個係數。對照本節開頭的一般形式,得到 \(\lambda = 1/(2I\sigma_p^{2})\)。
這條式子一次交代三件事。第一,平方懲罰不是誰隨手挑的,它對應的是常態先驗;換成拉普拉斯先驗(Laplace prior),長出來的就是絕對值和。第二,\(\sigma_p\) 越小、\(\lambda\) 越大——你越確定參數應該接近零,罰得越重。第三也最有意思:\(\lambda\) 與資料量 \(I\) 成反比。同一個先驗,資料多十倍,它的相對份量就少十倍。事前的偏好會隨著證據累積而自動退讓,這正是一個合理的偏好該有的行為。
如果你對「先驗」有心理抗拒,覺得那是主觀猜測——請注意:不寫先驗其實也是一個先驗,它說的是「任何大小的權重一樣可信」,包括大到荒謬的那些。那個陳述本身也不客觀,只是沒有被寫出來而已。
這條路還可以再往前走一步,走到底就是貝葉斯取徑(Bayesian approach)。最大後驗雖然用上了先驗,最後還是只挑一組參數交差;貝葉斯的做法是連挑都不挑,把參數的不確定性整個留著,預測時一併算進去。原理上它比挑一組乾淨——你不必假裝自己確定參數是多少。代價出在計算:要把不確定性算進去,得對整個參數空間做積分,而深度網路有幾百萬個參數,那個積分做不出來,只能用各種近似手段逼近。本章就在最大後驗這裡停住,再往下是另一門課的份量。
剩下幾個相關的東西,各給一段就夠。改罰絕對值和會偏好稀疏(sparsity)——大部分權重恰好是零。原因在導數:絕對值的導數是常數,權重再小它推的力氣都一樣大,所以推得動最後那一段;平方項的導數是 \(2\lambda\phi\),權重越接近零它越使不上力。直接罰非零權重的個數是最直白的稀疏化,但那個函數不可微,最佳化很難做。稀疏做到極致就接上了剪枝(pruning):某個單元的進出權重全變成零,這個單元就可以整個從網路裡拿掉,模型真的變小、推論真的變快。
最後是權重衰減(weight decay),一個很容易被誤解的名字。它的定義是直接改寫更新規則:每一步更新之前,先把權重乘上一個略小於一的數。在固定學習率的普通梯度下降上,平方懲罰算出來的更新式是
逐項拆解:\(\alpha\) 是學習率;\(\nabla\mathcal{L}\) 是原損失的梯度;\(2\lambda\boldsymbol\phi\) 是平方懲罰的梯度。整理之後,權重確實在每一步先被乘上 \((1-2\alpha\lambda)\)——形式跟權重衰減一模一樣。所以很多教材直接把兩個名字互換使用。但形式相同不等於是同一個東西的兩個名字。 它們的定義從一開始就不同:一個是加在損失上的項,一個是寫死在更新規則裡的縮放。一旦最佳化器會逐座標調整步長(第 6 章的 Adam 就是這樣),懲罰項的梯度會跟著被那個縮放除過一遍,而寫在更新式裡的衰減不會,兩者立刻分家。把「權重衰減」講成「平方懲罰的別名」是一個很順口、也很常見的說法,但它只在特定條件下成立。
還有一句誠實話要說在這裡。顯式懲罰在小資料、小模型上效果很明顯——上面那張表就是證據。但在大型深度網路上,它對測試表現到底貢獻了多少,是一個仍有爭議的問題:有實驗把平方懲罰整個關掉,模型照樣泛化得不差。所以請不要把它當成「加了就會變好」的開關。它是各種偏好來源裡你控制得最直接的那一個,不見得是最強的那一個。真正扛下大部分工作的,可能是下一節那些從來沒有人寫下來的偏好。
三、隱式正則:沒有人加的那一項
隱式正則最容易被誤讀成「有人偷偷替你加了一項」。不是的。沒有人加任何東西——是最佳化演算法本身就帶著偏好。
先把一件事釘住:地形沒有變。損失函數由模型結構、資料和損失定義決定,梯度為零的點在哪裡,跟你每一步走多遠完全無關(第 7 章談初始化時已經強調過同一件事)。步長改變的不是零梯度點的位置,而是你最後停在哪一個零梯度點上。
為什麼步長會挑?有一個機械的理由最好懂。把一個谷的底部附近當成二次曲面來看,它有一個曲率 \(c\)(曲率越大表示谷壁越陡)。固定步長 \(\alpha\) 的梯度下降只有在 \(\alpha < 2/c\) 的時候才停得住;步長超過這條線,你每走一步離谷底更遠,最後被彈出去。所以步長越大,能容納你的谷就必須越寬——窄而陡的谷放不下大步子,不管它有多深。底下這段程式把這件事演一次:
import numpy as np
# 自造的一維地形:左邊一個窄而陡的谷,右邊一個寬而緩的谷
NARROW, WIDE = 0.20, 1.30
def loss(p):
return (-np.exp(-(p + 1.5) ** 2 / (2 * NARROW ** 2))
- np.exp(-(p - 2.0) ** 2 / (2 * WIDE ** 2)) + 0.02 * p ** 2)
def grad(p):
return ((p + 1.5) / NARROW ** 2 * np.exp(-(p + 1.5) ** 2 / (2 * NARROW ** 2))
+ (p - 2.0) / WIDE ** 2 * np.exp(-(p - 2.0) ** 2 / (2 * WIDE ** 2))
+ 0.04 * p)
def curvature(p):
return float((grad(p + 1e-5) - grad(p - 1e-5)) / 2e-5)
def run(alpha, steps, p):
for _ in range(steps):
p = p - alpha * grad(p)
return float(p)
for p0 in (-1.5, 2.0): # 極小步長:逼出兩個谷底的真實位置
b = run(1e-3, 60000, p0)
print(f"谷底 φ={b:+.4f} 損失={loss(b):+.4f} 曲率={curvature(b):6.2f}"
f" 穩定上限 2/曲率={2 / curvature(b):.3f}")
print("---- 同一個起點 φ=-1.10,只換步長 ----")
for alpha in (0.005, 0.120, 0.300):
a_end = run(alpha, 6000, -1.10)
b_end = run(alpha, 6001, -1.10)
print(f"步長 {alpha:.3f} → 最後兩步 φ={a_end:+.4f} / {b_end:+.4f}"
f" 損失={loss(a_end):+.4f}")實跑輸出的前兩行給出兩個谷底:\(\phi = -1.4954\),損失 -0.9819、曲率 24.92、穩定上限 0.080;\(\phi = +1.8728\),損失 -0.9251、曲率 0.62、穩定上限 3.209。請注意左邊那個谷比較深——它的訓練損失比較低。
後三行從同一個起點 \(\phi = -1.10\) 出發,只換步長:步長 0.005 停在 -1.4954,那個深而窄的谷;步長 0.300 停在 +1.8728,那個淺而寬的谷;步長 0.120 則在 -1.6457 與 -1.2975 之間來回跳,永遠停不下來。
第三種情況是額外的收穫:離散化不只會換一個終點,它也可能讓你落在「兩點之間反覆橫跳」的狀態——真實訓練裡看起來就是損失卡在某個值附近上下抖動。
第二種情況才是本節的重點。大步長主動放棄了比較低的訓練損失,換到一個比較平坦的位置。沒有人在損失上加任何一項,這個偏好純粹是「每步走 0.3」這個決定的副產品。用一句話總結有限步長的偏好:它躲開梯度變化劇烈的地方。
隨機取批次帶來第二重偏好。第 6 章的隨機梯度下降(stochastic gradient descent)每次只拿一小批資料估梯度,同一組參數換一批資料,算出來的方向就不一樣。如果你站的位置上各批次的梯度彼此差很多,隨機性會把你推來推去、待不住;如果各批次大致指向同一邊,你就留得下來。所以它偏好的是不同資料子集都同意的位置。
這兩個偏好合起來,解釋了一個實務上常被觀察到的現象:學習率調大一點、批次調小一點,訓練損失往往收得比較差,測試表現卻反而好一些。要留意這只是一個傾向,不是規律——調得太過,模型就直接訓練不動了。
把離散的步伐反推成一個修正過的連續問題、看清多出來的那一項長得像什麼樣的懲罰,是一段紮實的代數推導。本課只給結論,想看它怎麼一步步導出來,參考資料裡有一篇論文專門處理這件事。
比喻: 同一批盆栽交給兩位園丁,用同一套教材、同一個目標形狀,長出來的樹還是不會一樣。原因不在他們懂的東西不同,而在手法:一位習慣用小剪刀、每週修一點;另一位用大剪刀、隔一個月大修一次。用大剪刀的那位下不了細手,凡是需要精修才維持得住的造型,他做著做著就放棄了,最後留下的一定是那些「大概修修也不會走樣」的形狀。沒有人在教材上多寫一條規則,偏好卻已經從工具和節奏裡長出來——這正是有限步長在做的事。這個比喻在一處失準:園丁知道自己有這個習慣,也可以換一把剪刀來改變結果;梯度下降的偏好是離散化的數學副產品,沒有人選過它,你也不能單獨把它拆下來——要改,就得動整個最佳化流程,而那會連帶改掉別的東西。
四、不加項的四個施力點
接下來四個手法都沒有在損失上加東西。歸類的方式不只一種,這裡按「你動了什麼」排:訓練流程、網路、資料、模型數量。每一項只回答同一個問題——它讓解偏向什麼性質?
動訓練流程:早停。 早停(early stopping)就是在模型完全收斂之前把訓練停掉。
它憑什麼算正則化?回到第 7 章的初始化:權重是從很小的隨機值出發的,訓練每走一步才長大一點。你在第八百步停下來,權重就只有八百步的成長量——它還沒有時間長到足以做出那些劇烈的彎折。所以早停偏好的解跟平方懲罰偏好的解是同一類:輸出隨輸入變化緩慢的那一類。兩者從完全不同的入口進場,落腳處卻很接近。
早停的超參數是「停在第幾步」,而它有一個平方懲罰沒有的好處:只要訓練一次就能挑完。 每隔固定步數在驗證集上量一次,把當下的參數存起來;訓練跑完之後回頭看,留下驗證表現最好的那一組就行。相比之下,上一節那張 \(\lambda\) 的表跑了八次完整的求解。
有個誤解要正面反駁:早停不是「訓練沒做完的偷懶」。停在第幾步是一個要被有紀律地挑出來的超參數,跟學習率、層數同一個地位。至於「拿哪一套資料來挑」,第 8 章已經把紀律講死了——用驗證集,不能用測試集。
動網路:丟棄。 丟棄(dropout)的做法是:每一次更新,隨機把一部分隱藏單元的輸出設成零,其餘照常運算;下一次更新重抽一次,被關掉的換一批。
它偏好什麼?如果任何一個單元都有可能在任何一步缺席,網路就不能把某個判斷完全押在它身上——那個判斷會有相當比例的機會直接消失。可行的辦法只剩下讓好幾個單元各自承擔一部分。結果是責任分散,而且每個權重都不必太大。
現在講這一章最常被搞錯的一件事:推論的時候不丟。
import numpy as np
rng = np.random.default_rng(0)
# 一層 6 個隱藏單元的活化值,以及它們接到下一層的權重
h = np.array([1.4, 0.0, 2.2, 0.7, 3.1, 0.5])
omega = np.array([0.9, -1.3, 0.4, 2.0, -0.6, 1.1])
RHO = 0.3 # 丟棄機率
full = float(omega @ h)
T = 200_000
keep = rng.random((T, h.size)) >= RHO # 每次更新重抽一次遮罩
out_train = (keep * h) @ omega # 訓練期:被關掉的單元輸出 0
print(f"完全不丟棄的輸出 {full:8.4f}")
print(f"訓練期輸出的平均({T} 次) {out_train.mean():8.4f}")
print(f"保留比例 × 不丟棄的輸出 {(1 - RHO) * full:8.4f}")
print(f"訓練期輸出的標準差 {out_train.std():8.4f}")
print("---- 推論期的兩種寫法 ----")
print(f"把權重乘上保留比例 {float((omega * (1 - RHO)) @ h):8.4f}")
print(f"訓練期先除以保留比例,測試期原樣 {(out_train / (1 - RHO)).mean():8.4f}")
print("---- 如果測試時也照丟 ----")
single = out_train[:8]
print("連續 8 次的輸出:", " ".join(f"{v:.2f}" for v in single))這裡用 \(\rho\) 表示丟棄機率(程式裡的 RHO),保留比例就是 \(1-\rho\)。實跑輸出:完全不丟棄時輸出是 2.2300;訓練期二十萬次隨機遮罩的平均是 1.5655,而「保留比例乘上不丟棄的輸出」是 1.5610——兩者在抽樣誤差內相等。這就是不一致的來源:訓練期看到的輸出,期望值只有原本的 \(1-\rho\) 倍。
補償有兩種等價的寫法。第一種是推論期把權重乘上保留比例,輸出 1.5610,正好對上訓練期的平均。第二種更常見:訓練期就先把留下來的活化除以保留比例,推論期什麼都不必做——實跑平均 2.2365,對上不丟棄的 2.2300。
那如果測試時照丟會怎樣?看那八次連續輸出:-0.05、1.68、0.83、-1.31、2.23、1.35、2.23、4.09。同一筆輸入、同一組參數,答案從負的一路跳到四以上,標準差 1.3012,超過輸出本身的一半。測試時丟棄不是「差一點點」,是每問一次得到一個不同的答案。
反過來用也有價值:推論時故意多跑幾次隨機版本再平均,你等於免費拿到一組結構略有差異的模型——那是本節最後「集成」那一段的窮人版。
比喻: 盆栽園裡的摘芽,是在新芽還小的時候用手把一部分捻掉。被摘掉的那些不是長得不好,純粹是為了不讓養分全部集中在少數幾個生長點上;剩下的芽被迫多分擔一些,整棵樹的枝條分布因此均勻起來。丟棄做的是同一件事:不讓任何一個單元變成不可替代的那一個。這個比喻有兩處失準。第一,摘掉的芽不會回來,摘芽是一次性的、永久的;丟棄是每一步重新抽一次,這一步被關掉的單元下一步就回來了——真正被削弱的是「依賴」,不是單元本身。第二,也是整章最需要記住的一點:盆栽園藝的目的是把樹養小、養成一個好看的形狀,正則化的目的不是。它常常在模型大得離譜的前提下運作,改的從來不是模型有多大,而是在一堆同樣合身的解裡挑哪一個。
動資料:增強。 資料增強(data augmentation)是對輸入做一種變換,變換後的樣本沿用原來的標籤,一起丟進訓練集。
它到底在宣告什麼?不是「我變出了新資料」——變換後的樣本沒有帶進任何一個新的量測值,它的資訊全部來自原樣本。你真正在宣告的是一條約束:我要的模型,對這一種變化應該給出相同的答案。 這條約束原本不在資料裡,是你放進去的。所以增強不是憑空生資訊,是把你的知識寫進訓練集。
回到盆栽園。你替每一盆樹拍照,訓練一個模型判斷這盆需不需要修剪。你可以把每張照片左右鏡射一份、旋轉幾度一份、亮度調高調低各一份。這些變換都不改變「需不需要修剪」這個答案,標籤照用。
但有一個前提,而且非常容易被違反:變換必須保住標籤仍然正確。 同一個園子換一個任務——這次要判斷的是「這盆樹的主枝往左偏還是往右偏」——左右鏡射立刻把答案弄反了。鏡射本身沒有錯,錯在把它套到一個對左右敏感的任務上,而標籤沒有跟著換。這一類問題特別難抓:程式不會報錯,訓練照跑,你只會看到模型「就是學不好左右」。
在輸入上加一點隨機擾動也屬於這一類,只是變換換成了雜訊。它的效果可以直接算出來:擾動的平均為零、變異數為 \(\sigma_\varepsilon^{2}\) 時,平均而言損失會多出一項正比於「模型輸出對輸入的斜率平方」的東西。也就是說,在輸入上加雜訊,等於偷偷附掛了一個懲罰函數陡峭程度的項。作業三會請你把它推出來並驗證。
同一個方向還有幾個變體,各給一句。在權重上加雜訊,偏好的是「權重值稍微變一點也不影響表現」的解,那種解位在寬平的區域。標籤平滑(label smoothing)把訓練目標從「正確類別的機率是一」改成留一小塊機率給其他類別,治的是最大概似會把正確類別的分數往極端推、讓模型變得過度自信;它不是隨機把標籤改掉——標籤還是原來那個,只是目標值不再是滿分。對抗訓練(adversarial training)則不隨機加擾動,而是主動去找最能讓模型出錯的那個微小擾動,再把它加進訓練。
動模型數量:集成。 集成(ensemble)是訓練好幾個模型、把預測合起來:迴歸取平均或中位數,分類取平均分數或多數決。
它為什麼有用,關鍵不在「模型多」,在個別模型的誤差要不相關:
逐項拆解:\(e_m\) 是第 \(m\) 個模型在某筆輸入上的誤差,這裡假設每個模型的誤差平均為零;\(M\) 是模型數;\(\sigma^{2}\) 是單一模型誤差的變異數;\(c\) 是任兩個模型誤差之間的相關係數。看右邊那個括號:第二項 \((1-c)/M\) 會隨模型數變多而消失,第一項 \(c\) 完全不含 \(M\)。所以模型再多,變異數也只能降到 \(c\sigma^{2}\) 為止。誤差完全不相關(\(c=0\))時它一路降成 \(\sigma^{2}/M\);誤差高度相關時,集成幾乎白做。作業二會請你把這件事量出來。
造出差異的辦法有幾種:換初始化、換超參數、對訓練資料重抽樣、乾脆換一族模型。代價很直接——訓練、儲存、推論的成本全部乘上 \(M\)。省成本的變體是把訓練途中不同時點的參數存下來,當成一組互有差異的模型用。
動起點:把參數放在一個比較好的地方。 最後一組手法動的是起點。遷移學習(transfer learning)先在一個資料充足的相關任務上訓練,再換掉最後幾層接到你的任務上;多任務學習(multi-task learning)同時學幾個任務、共用同一組內部表示;自監督學習(self-supervised learning)從沒有標籤的資料裡自己造出訓練訊號,拿來當預訓練。它們為什麼算正則化?因為絕大部分參數的起點不再是隨機的,而是落在一個「已經被證明走得到好解」的區域裡——第 7 章講過,起點決定你腳下的坡長什麼樣,也就決定了你走得到哪裡。這三者各自都夠寫一章,本章只把它們放進地圖。
五、歸位:每個手法把解推向哪裡
這一章列了不少手法,但如果你記下來的是一份清單,那就白讀了。真正要記住的是那條軸——每個手法都在同一片解裡挑一個位置。所以最後不按「動了什麼」排,改按「被偏好的解具有什麼性質」排。同一個手法在兩種排法下會落在不同的格子裡,這很正常,因為它們問的問題不同。
一、輸出隨輸入變化緩慢。 平方懲罰、早停、在輸入上加雜訊。三個入口完全不同——一個改損失、一個改流程、一個改資料——落腳處卻是同一類解。這是本章最值得記住的一組對應。
二、權重稀疏,單元可以整個拿掉。 絕對值和懲罰、非零個數懲罰。稀疏到一定程度,某個單元的進出權重全變成零,就可以把它從網路裡剪掉。請注意這是本章唯一一組真的會讓模型變小的手法,而且變小是稀疏的結果,不是目的。
三、責任分散,不押在單一單元上。 丟棄、在權重上加雜訊。後者順帶偏好寬平的區域——參數稍微被推一下也不影響表現的那種。
四、對指定的變換不敏感。 資料增強。這一類的特別之處在於:偏好的內容是你指定的,不是通用的。你說鏡射不該改變答案,模型就朝那個方向被拉;你指定錯了,它就被拉往錯的方向。
五、誤差互相抵消。 集成。它跟前四類的性質不同:它不改變任何一個模型挑到哪個解,改的是「最後交出去的預測由誰組成」。
圖的最下面那一條單獨列出來,是因為它跟上面五類的性質不同:上面五類你可以決定要不要用,最佳化演算法自帶的那組偏好卻是你不用它也在。有限步長偏好平坦的位置,隨機取批次偏好各批次意見一致的位置,而你唯一動得到的旋鈕是步長和批次大小。
如果只能帶走一句話,帶走這一句:這些手法沒有一個在讓模型變笨。 它們全部是在同一片「訓練成績一樣好」的解裡替你做選擇,差別只在偏好從哪裡進來、偏好什麼。順著這個視角再走一步就會發現,換一個更適合任務的網路結構其實也在做同一件事——它把整片解集合換掉了,換成一片本來就比較合理的。那是第 10 章之後幾章的主題。
§03原書對照
原書第 9 章排在量測表現之後,二十來頁裡塞進了從嚴格定義到各種實務手法的一整個光譜,密度很高,有好幾處值得進階讀者翻回去看。
第一處是懲罰項的機率解讀。原書 pp.139–140 用一小節把損失上的懲罰項和參數的先驗機率對應起來,寫出最大後驗準則的式子,並點明取對數再變號之後,懲罰項就等於先驗機率的負對數。想弄清楚「加一項懲罰」與「事先偏好某些參數」為什麼是同一件事,那兩頁是最短的路徑。同一段脈絡裡,p.140 交代了平方範數懲罰在神經網路上通常只加在權重、不加在偏置,並列出這個懲罰項在其他領域裡的另外幾個名字;p.141 則用一組面板圖展示同一個模型在懲罰係數由小逐步增大時,擬合曲線如何從穿過每一個資料點變得平滑,最後平滑過頭。
第二處是隱式正則化的推導。原書 pp.141–143 先拿步長無限小的連續版梯度下降當參照,說明有限步長會讓實際軌跡偏離連續軌跡,再給出一個修正後的損失,指出離散步驟等效於在原損失上多加了一項與梯度大小有關的懲罰;接著把同樣的分析推廣到隨機梯度下降,得到另一項與各批次梯度彼此差異有關的懲罰。這兩條結論在正文裡只給結果,完整的反向誤差分析與泰勒展開放在章末註記 pp.156–157,逐行寫了出來。想看它怎麼一步步導出來的人,翻那兩頁。
第三處是各手法的配圖。pp.145–154 依序處理早停與集成、丟棄、對輸入與標籤加擾動、貝葉斯取徑、遷移與多任務學習、自監督,以及資料增強,每一項都配了自己的示意圖或實驗圖:p.145 用一組實驗比較不同學習率與批次大小的測試表現;p.146 展示訓練過程中函數先抓到粗略形狀、後來才貼上雜訊;p.149 用一張圖說明幾個隱藏單元如何合力在曲線上做出一個對訓練損失無害、卻明顯不合理的轉折;p.153 與 p.154 分別是遷移與自監督、以及影像增強的示意。p.155 另有一張總覽圖,把全章手法按作用方式歸成幾大類。
第四處是章末註記,pp.155–160 的資訊密度比正文還高:權重衰減與平方懲罰在什麼條件下等價、在自適應學習率下為什麼不等價;改用其他範數如何帶來稀疏、進而讓網路可以被剪枝;早停在二次近似下與平方懲罰的對應關係;集成、丟棄、標籤平滑、尋找較寬極小值各自的來源文獻;以及影像、語音、文字三類資料各有哪些增強手法。要找延伸閱讀,這幾頁比正文更好用。
還有一點記法上的事值得留意:原書在本章開頭把整體損失寫成各筆損失的總和,到了討論批次時才改寫成平均,兩種寫法只差一個正的常數倍,最小值的位置相同。章末 p.160 收了六道習題,其中兩道要你自己把先驗與懲罰項、把權重衰減與平方懲罰各推導成等價形式。原書第 9 章對應印刷頁 pp.138–160。
§04作業和解答
作業一:把先驗換成懲罰項
你在盆栽園的資料上訓練一個模型,損失採平均負對數概似 \(\mathcal{L}[\boldsymbol\phi] = -\frac{1}{I}\sum_{i=1}^{I}\log Pr(y_i \mid x_i, \boldsymbol\phi)\),並替每一個參數 \(\phi_j\) 假設一個彼此獨立、平均為零、標準差為 \(\sigma_p\) 的常態先驗。(a)寫出最大後驗準則取負對數並除以 \(I\) 之後的目標函數,指出懲罰係數 \(\lambda\) 等於什麼。(b)\(\sigma_p = 0.5\)、\(I = 200\) 時 \(\lambda\) 是多少?資料量增加到 \(I = 2000\) 呢?(c)用一句話說明(b)的變化代表什麼。(d)如果把偏置也放進同一個先驗裡,你其實對模型下了什麼假設?為什麼那通常不是你要的?
解答 SOLUTION
(a)目標函數是 \(\mathcal{L}[\boldsymbol\phi] + \frac{1}{2I\sigma_p^{2}}\sum_j \phi_j^{2}\),所以 \(\lambda = 1/(2I\sigma_p^{2})\)。過程:最大後驗要最大化「概似乘先驗」;取負對數把乘積變成加總、把最大化變成最小化;除以 \(I\) 讓第一項成為平均負對數概似,也就是 \(\mathcal{L}\)。第二項是 \(-\frac{1}{I}\log Pr(\boldsymbol\phi)\),把常態密度代進去得到 \(\frac{1}{2I\sigma_p^{2}}\sum_j\phi_j^{2}\) 再加上一個與 \(\boldsymbol\phi\) 無關的常數,那個常數不影響最小值的位置,可以丟掉。
(b)\(I = 200\) 時 \(\lambda = 1/(2 \times 200 \times 0.25) = 0.010000\);\(I = 2000\) 時 \(\lambda = 0.001000\)。
(c)同一個先驗,資料多十倍,它的相對份量就少十倍——事前的偏好會隨著證據累積而退讓。這正是一個合理的偏好該有的行為。反過來也對稱:\(I\) 保持 200、把 \(\sigma_p\) 從 0.5 收緊到 0.2,\(\lambda\) 會升到 \(0.062500\);你越確定參數應該接近零,罰得越重。
(d)那等於在假設「輸出的整體高度本來就該接近零」。偏置只把整條輸出曲線上下平移,不改變它有多陡;把偏置壓向零,你壓的是預測值的平均水準,而那個水準該落在哪裡完全由任務決定——新枝平均長十公分還是一百公分,跟模型該不該平滑毫無關係。(本題數值以 numpy 重算核對過。)
作業二:集成什麼時候會失效
有 \(M\) 個模型,第 \(m\) 個在某筆輸入上的誤差是 \(e_m\),每個誤差的平均為零、變異數為 \(\sigma^{2}\),任兩個之間的相關係數都是 \(c\)。(a)推出平均預測的誤差變異數。(b)\(c = 0\) 與 \(c = 0.6\) 時,\(M\) 從 1 增到 256,變異數各降到多少?(c)有人說「集成只要模型夠多就一定會變好」,用(b)反駁他。(d)舉一個會讓 \(c\) 變得很高的實務做法。
解答 SOLUTION
(a)\(\mathrm{Var}\bigl[\frac{1}{M}\sum_m e_m\bigr] = \frac{1}{M^{2}}\bigl[\sum_m \mathrm{Var}(e_m) + \sum_{m \neq n}\mathrm{Cov}(e_m, e_n)\bigr]\)。單項變異數共 \(M\) 個、每個是 \(\sigma^{2}\);交叉項共 \(M(M-1)\) 個、每個是 \(c\sigma^{2}\)。合起來得 \(\frac{1}{M^{2}}\bigl[M\sigma^{2} + M(M-1)c\sigma^{2}\bigr] = \sigma^{2}\bigl[c + \frac{1-c}{M}\bigr]\)。
(b)取 \(\sigma = 1\),每組跑四十萬次抽樣的實測結果如下。\(c = 0\):\(M = 1, 4, 16, 64, 256\) 的變異數是 0.9990、0.2502、0.0625、0.0156、0.0039,公式給的是 1.0000、0.2500、0.0625、0.0156、0.0039,走的正是 \(1/M\)。\(c = 0.6\):實測是 0.9975、0.7019、0.6257、0.6059、0.6030,公式是 1.0000、0.7000、0.6250、0.6062、0.6016。
(c)\(M\) 從 4 增到 256——模型數變成六十四倍——變異數只從 0.70 壓到 0.60。公式裡的 \(c\) 這一項完全不含 \(M\),它是地板;\(M\) 再大也只能把 \((1-c)/M\) 消掉。所以集成買的是「誤差裡不相關的那一部分」,不是模型的數量。
(d)用同一份訓練資料、同一種架構、同一組超參數,只換隨機種子。這樣造出來的模型會犯很相似的錯,因為錯誤大多來自資料本身的偏斜與架構的限制,而這兩樣完全沒變。要壓低 \(c\),得動的是差異的來源:換架構、對訓練資料重抽樣、換特徵處理方式。(本題數值以 numpy 重算核對過。)
作業三:在輸入上加雜訊,等於懲罰什麼
把一筆訓練資料的輸入 \(x\) 加上一個平均為零、變異數為 \(\sigma_\varepsilon^{2}\) 的小擾動 \(\varepsilon\),損失取平方誤差 \((f[x+\varepsilon] - y)^{2}\)。(a)把 \(f[x+\varepsilon]\) 對 \(\varepsilon\) 做二階泰勒展開,求這個損失對 \(\varepsilon\) 取期望之後多出來的項。(b)指出主項是什麼,並說明它為什麼是一個「平滑懲罰」。(c)取 \(f[x] = 6.0 + 3.2x - 0.38x^{2}\)、\(\sigma_\varepsilon = 0.3\),在 \(x = 2, 5, 8\) 三處驗證你的式子;三處的殘差 \(f[x] - y\) 依序取 \(-0.5\)、\(+0.4\)、\(-0.3\)。
解答 SOLUTION
(a)展開 \(f[x+\varepsilon] \approx f[x] + \varepsilon f'[x] + \frac{\varepsilon^{2}}{2}f''[x]\),平方後保留到 \(\varepsilon^{2}\) 這一階:
對 \(\varepsilon\) 取期望,用 \(\mathbb{E}[\varepsilon] = 0\)、\(\mathbb{E}[\varepsilon^{2}] = \sigma_\varepsilon^{2}\),一次項整個消失:
(b)主項是 \(\sigma_\varepsilon^{2}(f'[x])^{2}\)。它跟模型輸出對輸入的斜率平方成正比:函數在這一點越陡,它就越大。最小化這個目標等於同時要求函數平緩,所以在輸入上加雜訊,效果就是在原損失上附掛一個斜率懲罰,強度由 \(\sigma_\varepsilon^{2}\) 決定。另一項 \((f[x]-y)f''[x]\) 帶著殘差當因子,模型擬合得好時它很小。
(c)\(f'[x] = 3.2 - 0.76x\),三處是 \(+1.680\)、\(-0.600\)、\(-2.880\);\(f''[x] = -0.76\)(常數)。取 \(\sigma_\varepsilon = 0.3\),兩百萬次蒙地卡羅的平均損失是 0.54035、0.16843、0.85932;(a)的兩項近似給出 0.53822、0.16504、0.85702,三處都對到小數第二位。若只留斜率項,得到的是 0.50402、0.19240、0.83650——在 \(x = 5\) 偏得最多,因為那裡斜率最小(\(-0.600\)),殘差項的相對份量最大。這也順帶說明了為什麼「加雜訊等於罰斜率」是一個近似而不是等式。(本題數值以 numpy 重算核對過。)
§05參考資料
- Deep Learning(Goodfellow 等著)第 7 章:Regularization for Deep Learning — 另一本經典教材對同一主題的完整處理,本章略過的貝葉斯取徑在那裡有較長的鋪陳
- Dropout: A Simple Way to Prevent Neural Networks from Overfitting — 丟棄的原始論文,訓練期與推論期怎麼對齊的討論就出自這裡
- Decoupled Weight Decay Regularization(arXiv:1711.05101) — 專門處理「權重衰減與平方懲罰在自適應最佳化器上為什麼不等價」,第二小節那個保留條件的完整版
- Implicit Gradient Regularization(arXiv:2009.11162) — 把有限步長反推成一個修正過的連續問題,第三小節說「本課只給結論」的那段推導
- Understanding deep learning requires rethinking generalization(arXiv:1611.03530) — 用實驗說明顯式正則不是泛化的必要條件,第二小節那句「不要當成開關」的來源
- CS231n:神經網路訓練(第二部分) — 史丹佛課程講義,把各種正則化手法的實務設定與常見比例整理得很具體
- Understanding Deep Learning(MIT Press) — 本課課綱主題所本的原書出版頁(ISBN 9780262048644,2023-12 出版)
- udlbook 官方網站(作者釋出的 PDF、投影片與習題) — 原書作者維護的免費資源站(udlbook.com 會轉址到此)