L2.4 同一張地圖從不同地方出發:損失曲面與初始化
本篇重用M0.0霧中下山:Gradient 與方向·M0.2導航的三十秒:Taylor 展開與「假設直線」
起點:同一張地圖,兩個登山口
L2 前面四篇的每一條結論都建立在同一個圖像上:損失曲面是一個碗。霧中下山,一步該走多大:梯度下降的一頁 的門檻 、一個 η 不夠用:動量與 Adam 各買到什麼 的 、民調只問一千人:小批次與噪聲 的噪聲地板、限制筆記頁數,或考前一天停止補習:正則化與提早停止 的逐方向縮放——全部都是在二次問題上算的。
線性模型加平方損失確實是一個碗(Hessian 是常數,見 L0.3)。但只要模型裡有一層非線性,這件事就不成立了:同一張地圖,從不同的登山口出發,可能下到不同的谷底。
這在實務上表現成一件每個人都遇過的事:同一份程式、只換 random seed,跑出來的分數不一樣。
問題是這件事有多嚴重。常見的說法是「高維空間裡到處都是局部極小」,聽起來像是每次都在賭運氣。這一篇要把它量出來——結果比那個說法溫和得多,而真正該花時間調的是另一個東西。
同一個架構、同一份資料,只換隨機起點,
會下到同一個谷底嗎?
如果不會,那我該調的是哪個旋鈕?
課堂提問Q1
把「同一張地圖、不同登山口」翻成數學:非凸到底讓哪一個保證失效了?初始化在這件事裡扮演什麼角色——它只是「隨機起點」,還是一個要調的超參數?
先想一想,再展開看整理後的答案
失效的是「局部訊息足以判斷全域」這個保證。 凸函數有一個很強的性質: 就是全域最小。梯度是局部的量(霧中下山:Gradient 與方向),而凸性讓這個局部的量帶著全域的資訊。非凸之後這個橋斷了—— 只告訴你「這裡是平的」,它可能是局部極小、鞍點、或一片平原。
於是 霧中下山,一步該走多大:梯度下降的一頁 的 Taylor 論證仍然成立(每一小步仍然下降),但「走到哪裡」不再由損失函數單獨決定,而是由損失函數加上起點共同決定。梯度下降變成一個由起點決定終點的映射。
初始化不只是「隨機起點」,它至少決定三件事。
- 落在哪個盆地。 這是上面說的那件事,它給了 seed 之間的散佈。
- 一開始的梯度有多大。 權重的尺度直接決定前向訊號的尺度,而前向訊號的尺度又決定反向梯度的尺度。太小 → 幾乎沒有梯度可走;太大 → 落在飽和區( 的導數趨近零)或直接越過穩定門檻。
- 每一層拿到的梯度比例。 深層網路裡,第一層的梯度是一連串因子的乘積;那些因子的大小由初始化的尺度決定。這是「梯度消失/爆炸」真正的來源——它是尺度的問題,不是深度本身的問題,本篇最後會把這件事量出來。
所以 gain(初始化的標準差乘數)是一個和學習率同等地位的超參數,而不是一個實作細節。
兩件要分開的事:散佈與尺度
散佈是「同一個設定跑很多次會差多少」。它決定你該怎麼報告結果(同一個實驗跑三次結果都不同,該報哪一個? 會處理報告的部分),以及「A 比 B 好 」這句話有沒有意義。
尺度是「初始化的權重該多大」。它決定訓練能不能開始。這是一個可以先驗算出來的東西,而經典的答案是讓每一層的前向訊號變異數大致守恆:權重取標準差 (Glorot/He 初始化,參考文獻 1、2)。本篇用的 gain 就是在那個基準上再乘一個倍數, 表示照基準。
兩件事的診斷方式不同:散佈要跑很多個 seed 看分布;尺度要在初始化時量前向與反向的統計量,不必訓練。
深度會不會讓梯度消失
先把「梯度消失」寫成一個可以量的東西。深層網路裡第一層的梯度是一串因子的乘積(鏈式法則):
每個因子的「典型大小」記為 。那麼第一層相對於最後一層的梯度大小大約是 :
- :指數衰減 → 梯度消失,深層的前段學不動。
- :指數放大 → 梯度爆炸。
- :不隨深度變化。
關鍵是 由什麼決定。 它是「權重的尺度」乘上「激發函數導數的典型值」——兩個都由初始化的 gain 控制。 這個選擇的整個目的就是把 調到 附近。
「梯度消失」不是深度的性質,是初始化尺度沒調好的性質。
這句話可以直接檢驗:固定 gain 掃深度,看那個比值有沒有隨深度衰減。本篇最後會量,答案是 gain 時平的。
注意殘差連接把問題換成了另一個
殘差連接 常被說成「解決了梯度消失」。它確實把每個因子從「一個乘法」變成「 一個乘法」,於是梯度有一條不衰減的通路。
但那條通路也不會縮小:每一層都把訊號加上一份自己的輸出,所以前向與反向的量級都隨深度成長。本篇最後量到的比值是 (深度 到 )——從「平的」變成「指數爆炸」。
修法是把那一份縮小: 取 或 可學(初始化為 ),或者在每個 block 裡放一個正規化層把量級拉回來。這就是為什麼殘差幾乎總是和正規化層一起出現——它們修的是相反方向的兩個病。一張圖裡同時有紋理、物件和整個場景,一個固定大小的核怎麼辦? 會回到這件事。
回到情境:兩個旋鈕各怎麼調
尺度先調,而且不用訓練就能調。 做法:初始化之後跑一個 batch 的前向,看每一層輸出的標準差。理想是各層大致相同(不隨深度衰減或放大);同時反向一次,看第一層與最後一層的梯度大小比。這兩個數字五分鐘就量得到,而它們決定訓練能不能開始。
散佈用來決定報告方式,不是用來挑 seed。 跑 5–10 個 seed,報中位數與範圍。挑最好的那個 seed 來報告,就是 老師用考古題教學又出題:為什麼要切資料 講的選擇偏差再來一次——只是這次選的是隨機性而不是超參數。
這一切依賴什麼。 一,本篇的網路很小(兩層 32 單元)。散佈的大小隨規模改變,而且方向不是單調的:更大的網路在很多任務上散佈更小(過參數化讓不同盆地的品質趨近),但更深的網路對初始化更敏感。二,本篇量的是訓練損失的散佈;泛化的散佈是另一個量,通常更大。三,用 Adam 訓練會壓掉一部分初始化的影響(它的逐座標尺度會自我調整),所以同一組實驗用 SGD 跑,尺度那條曲線會更陡。
回到情境:把散佈與尺度量出來
模型是一個 的 網路,Adam 跑 4000 步,資料是共用 toy 的 30 個點。
第一段:200 個隨機起點。
import numpy as np, mlp # mlp.py:三十行的 numpy MLP,見本節末
from ml_toy import toy_1d
x, y = toy_1d(n=30, seed=0); X = x[:,None]
fin = []
for s in range(200):
P = mlp.init([1,32,32,1], gain=1.0, rng=np.random.default_rng(s))
P, L = mlp.adam_train(P, X, y, T=4000, eta=0.01)
fin.append(L)
fin = np.array(fin)
print(f"最小 {fin.min():.4f} 中位數 {np.median(fin):.4f} 第90百分位 {np.percentile(fin,90):.4f} 最大 {fin.max():.4f}")
print(f"比中位數差兩倍以上的比例:{(fin > 2*np.median(fin)).mean()*100:.1f}%")
最小 0.0023 中位數 0.0034 第90百分位 0.0046 最大 0.0085
比中位數差兩倍以上的比例:3.0%
散佈存在,但它不是「每次都在賭運氣」。 中位數 、第 90 百分位 ——九成的 seed 落在中位數的 倍以內。最大值 是中位數的 倍,而落在那個尾巴裡的只有 。
這給了兩個具體的實務結論。一,「跑一次就下結論」在這個規模上大約有 的機率踩到一個爛 seed,而那會讓你以為某個改動有害。二,兩個設定的差距如果小於中位數的 ,用單一 seed 是分辨不出來的——這正是 同一個實驗跑三次結果都不同,該報哪一個? 要處理的事。
也請注意最小值 與最大值 之間有一個連續的分布,不是幾個離散的谷底。實務上這比「有 個局部極小」的圖像更接近真相:高維裡的極小點多到形成一個連通的低損失區域,而它們的品質分布是連續的。
第二段:初始化尺度的掃描。 同一個架構,只改 gain(每個 gain 跑 12 個 seed 取中位數):
gain=0.01 最終訓練 MSE 中位數 0.0064 最差 0.0135
gain=0.1 最終訓練 MSE 中位數 0.0057 最差 0.0059
gain=1 最終訓練 MSE 中位數 0.0031 最差 0.0061
gain=3 最終訓練 MSE 中位數 0.0066 最差 0.0089
gain=10 最終訓練 MSE 中位數 0.0108 最差 0.0133
gain=30 最終訓練 MSE 中位數 0.0156 最差 0.0523
又是一個 U 形,谷底正好在 gain(也就是 那個基準)。兩端壞得方式不同:gain 時權重太小,前向訊號幾乎是零、反向梯度也幾乎是零,4000 步還沒走到(,而且最差的那個 seed 是 );gain 時大部分單元一開始就落在 的飽和區(導數趨近零),,而且最差的那個 seed 是 ——尺度太大時 seed 之間的散佈也跟著變大。
注意這個掃描的範圍:從谷底往兩邊各差 倍,損失只變差 到 倍。它比學習率溫和(霧中下山,一步該走多大:梯度下降的一頁 的 跨過門檻是 ),但它是一個實實在在的旋鈕,而且因為它在訓練開始前就決定,調錯了不會給你任何錯誤訊息。
第三段:深度與梯度的比值。 量初始化時 與 的比(8 個 seed 的中位數):
(tanh,每層 32 單元,無殘差)
depth gain=0.5 gain=1 gain=2
2 1.61e-01 1.64e-01 1.90e-01
8 1.75e-01 2.26e-01 4.41e-01
16 1.87e-01 2.28e-01 7.99e-01
32 1.86e-01 2.19e-01 7.63e+00
gain 與 那兩欄從深度 2 到 32 幾乎是平的()。也就是說:在 這個初始化下,32 層的網路沒有梯度消失。這直接否證了「深就會梯度消失」這個說法的一般形式。
而 gain 那一欄從 爬到 ——同一個深度、只把初始化放大兩倍,就從「平的」變成「放大 40 倍」。第三個結論因此是:
會不會消失或爆炸,是 gain 的函數,不是深度的函數。
刻意違反:加上未縮放的殘差連接。
(同上,加 h_{ℓ+1} = h_ℓ + tanh(W_ℓ h_ℓ))
depth 無殘差 有殘差
2 1.64e-01 2.26e-01
4 1.93e-01 5.20e-01
8 2.26e-01 1.36e+00
16 2.28e-01 1.30e+01
32 2.19e-01 3.28e+02
殘差把「平的」變成「指數爆炸」:深度 32 時比值是 ,而無殘差是 。原因寫在上面的 <Remark> 裡——每一層都把訊號加上一份自己的輸出,所以量級隨深度累積。
這不是說殘差沒用。它是說殘差不是一個可以單獨加上去的零件:它把訊號的量級從「守恆」推到「成長」,所以必須配一個把量級拉回來的東西( 的縮放、可學的、初始化為零的門,或一個正規化層)。兩個修法方向相反,這就是為什麼它們總是成對出現。
補充本篇用的三十行 numpy MLP
為了讓上面的實驗可以逐行檢查(而不是呼叫一個框架),mlp.py 只做三件事:init(sizes, gain, rng) 用 初始化每一層;grads(P, X, y, skip) 手寫前向與反向( 的導數是 ,殘差那一支在反向時多加一個恆等項);adam_train(P, X, y, T, eta) 是 一個 η 不夠用:動量與 Adam 各買到什麼 那個更新式,含偏差修正。
刻意沒有做的事:沒有 minibatch(全批次,30 筆資料不需要)、沒有正規化層、沒有 weight decay。這樣一來每一個實驗只有一個變數在動——這是能把 gain 的效果和其他東西分開的原因,也是為什麼這裡的數字比在一個完整框架上跑出來的乾淨。
換 seed、換 gain、換深度
互動 demo:散佈與尺度是兩件事:seed 的散佈量的是前者,梯度比量的是後者。
先消化一下
參考文獻
- Glorot, X., Bengio, Y. Understanding the difficulty of training deep feedforward neural networks. AISTATS 2010.(把「讓每層訊號變異數守恆」寫成初始化條件, 的來源。)
- He, K. et al. Delving Deep into Rectifiers. ICCV 2015.(ReLU 下的對應版本,以及深度與初始化尺度如何一起決定訓練成不成立。)
- Li, H. et al. Visualizing the Loss Landscape of Neural Nets. NeurIPS 2018.(損失曲面的視覺化方法,以及殘差與寬度如何改變曲面的樣子。)