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

L2.4 同一張地圖從不同地方出發:損失曲面與初始化

本篇重用M0.0霧中下山:Gradient 與方向·M0.2導航的三十秒:Taylor 展開與「假設直線」

起點:同一張地圖,兩個登山口

L2 前面四篇的每一條結論都建立在同一個圖像上:損失曲面是一個碗。霧中下山,一步該走多大:梯度下降的一頁 的門檻 2/λmax2/\lambda_{\max}一個 η 不夠用:動量與 Adam 各買到什麼κ\sqrt\kappa民調只問一千人:小批次與噪聲 的噪聲地板、限制筆記頁數,或考前一天停止補習:正則化與提早停止 的逐方向縮放——全部都是在二次問題上算的。

線性模型加平方損失確實是一個碗(Hessian 是常數,見 L0.3)。但只要模型裡有一層非線性,這件事就不成立了:同一張地圖,從不同的登山口出發,可能下到不同的谷底。

這在實務上表現成一件每個人都遇過的事:同一份程式、只換 random seed,跑出來的分數不一樣。

問題是這件事有多嚴重。常見的說法是「高維空間裡到處都是局部極小」,聽起來像是每次都在賭運氣。這一篇要把它量出來——結果比那個說法溫和得多,而真正該花時間調的是另一個東西。

同一個架構、同一份資料,只換隨機起點,
會下到同一個谷底嗎?
如果不會,那我該調的是哪個旋鈕?

課堂提問Q1

把「同一張地圖、不同登山口」翻成數學:非凸到底讓哪一個保證失效了?初始化在這件事裡扮演什麼角色——它只是「隨機起點」,還是一個要調的超參數?

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

失效的是「局部訊息足以判斷全域」這個保證。 凸函數有一個很強的性質:L(θ)=0\nabla L(\theta)=0 就是全域最小。梯度是局部的量(霧中下山:Gradient 與方向),而凸性讓這個局部的量帶著全域的資訊。非凸之後這個橋斷了——L=0\nabla L=0 只告訴你「這裡是平的」,它可能是局部極小、鞍點、或一片平原。

於是 霧中下山,一步該走多大:梯度下降的一頁 的 Taylor 論證仍然成立(每一小步仍然下降),但「走到哪裡」不再由損失函數單獨決定,而是由損失函數加上起點共同決定。梯度下降變成一個由起點決定終點的映射。

初始化不只是「隨機起點」,它至少決定三件事。

  1. 落在哪個盆地。 這是上面說的那件事,它給了 seed 之間的散佈。
  2. 一開始的梯度有多大。 權重的尺度直接決定前向訊號的尺度,而前向訊號的尺度又決定反向梯度的尺度。太小 → 幾乎沒有梯度可走;太大 → 落在飽和區(tanh\tanh 的導數趨近零)或直接越過穩定門檻。
  3. 每一層拿到的梯度比例。 深層網路裡,第一層的梯度是一連串因子的乘積;那些因子的大小由初始化的尺度決定。這是「梯度消失/爆炸」真正的來源——它是尺度的問題,不是深度本身的問題,本篇最後會把這件事量出來。

所以 gain(初始化的標準差乘數)是一個和學習率同等地位的超參數,而不是一個實作細節。

兩件要分開的事:散佈與尺度

散佈是「同一個設定跑很多次會差多少」。它決定你該怎麼報告結果(同一個實驗跑三次結果都不同,該報哪一個? 會處理報告的部分),以及「A 比 B 好 2%2\%」這句話有沒有意義。

尺度是「初始化的權重該多大」。它決定訓練能不能開始。這是一個可以先驗算出來的東西,而經典的答案是讓每一層的前向訊號變異數大致守恆:權重取標準差 1/fan-in\propto 1/\sqrt{\text{fan-in}}(Glorot/He 初始化,參考文獻 1、2)。本篇用的 gain 就是在那個基準上再乘一個倍數,gain=1\text{gain}=1 表示照基準。

兩件事的診斷方式不同:散佈要跑很多個 seed 看分布;尺度要在初始化時量前向與反向的統計量,不必訓練。

深度會不會讓梯度消失

先把「梯度消失」寫成一個可以量的東西。深層網路裡第一層的梯度是一串因子的乘積(鏈式法則):

LW1=LhD最後一層=2D(Wσ(z1))每一層一個因子 \frac{\partial L}{\partial W_1} =\underbrace{\frac{\partial L}{\partial h_D}}_{\text{最後一層}} \cdot\prod_{\ell=2}^{D}\underbrace{\big(W_\ell^\top \odot \sigma'(z_{\ell-1})\big)}_{\text{每一層一個因子}} \cdot\ \cdots

每個因子的「典型大小」記為 cc。那麼第一層相對於最後一層的梯度大小大約是 cD1c^{D-1}

  • c<1c<1:指數衰減 → 梯度消失,深層的前段學不動。
  • c>1c>1:指數放大 → 梯度爆炸
  • c1c\approx1:不隨深度變化。

關鍵是 cc 由什麼決定。 它是「權重的尺度」乘上「激發函數導數的典型值」——兩個都由初始化的 gain 控制1/fan-in1/\sqrt{\text{fan-in}} 這個選擇的整個目的就是把 cc 調到 11 附近。

「梯度消失」不是深度的性質,是初始化尺度沒調好的性質。

這句話可以直接檢驗:固定 gain 掃深度,看那個比值有沒有隨深度衰減。本篇最後會量,答案是 gain=1=1平的

注意殘差連接把問題換成了另一個

殘差連接 h+1=h+σ(Wh)h_{\ell+1}=h_\ell+\sigma(W_\ell h_\ell) 常被說成「解決了梯度消失」。它確實把每個因子從「一個乘法」變成「1+1+ 一個乘法」,於是梯度有一條不衰減的通路。

但那條通路也不會縮小:每一層都把訊號加上一份自己的輸出,所以前向與反向的量級都隨深度成長。本篇最後量到的比值是 0.230.521.3613.03280.23\to0.52\to1.36\to13.0\to328(深度 223232)——從「平的」變成「指數爆炸」。

修法是把那一份縮小:h+1=h+ασ(Wh)h_{\ell+1}=h_\ell+\alpha\,\sigma(W_\ell h_\ell)α=1/D\alpha=1/\sqrt{D}α\alpha 可學(初始化為 00),或者在每個 block 裡放一個正規化層把量級拉回來。這就是為什麼殘差幾乎總是和正規化層一起出現——它們修的是相反方向的兩個病。一張圖裡同時有紋理、物件和整個場景,一個固定大小的核怎麼辦? 會回到這件事。

回到情境:兩個旋鈕各怎麼調

尺度先調,而且不用訓練就能調。 做法:初始化之後跑一個 batch 的前向,看每一層輸出的標準差。理想是各層大致相同(不隨深度衰減或放大);同時反向一次,看第一層與最後一層的梯度大小比。這兩個數字五分鐘就量得到,而它們決定訓練能不能開始。

散佈用來決定報告方式,不是用來挑 seed。 跑 5–10 個 seed,報中位數與範圍。挑最好的那個 seed 來報告,就是 老師用考古題教學又出題:為什麼要切資料 講的選擇偏差再來一次——只是這次選的是隨機性而不是超參數。

這一切依賴什麼。 一,本篇的網路很小(兩層 32 單元)。散佈的大小隨規模改變,而且方向不是單調的:更大的網路在很多任務上散佈更小(過參數化讓不同盆地的品質趨近),但更深的網路對初始化更敏感。二,本篇量的是訓練損失的散佈;泛化的散佈是另一個量,通常更大。三,用 Adam 訓練會壓掉一部分初始化的影響(它的逐座標尺度會自我調整),所以同一組實驗用 SGD 跑,尺度那條曲線會更陡。

回到情境:把散佈與尺度量出來

模型是一個 1323211\to32\to32\to1tanh\tanh 網路,Adam η=0.01\eta=0.01 跑 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%

散佈存在,但它不是「每次都在賭運氣」。 中位數 0.00340.0034、第 90 百分位 0.00460.0046——九成的 seed 落在中位數的 1.351.35 倍以內。最大值 0.00850.0085 是中位數的 2.52.5 倍,而落在那個尾巴裡的只有 3%3\%

這給了兩個具體的實務結論。,「跑一次就下結論」在這個規模上大約有 3%3\% 的機率踩到一個爛 seed,而那會讓你以為某個改動有害。,兩個設定的差距如果小於中位數的 35%35\%,用單一 seed 是分辨不出來的——這正是 同一個實驗跑三次結果都不同,該報哪一個? 要處理的事。

也請注意最小值 0.00230.0023 與最大值 0.00850.0085 之間有一個連續的分布,不是幾個離散的谷底。實務上這比「有 kk 個局部極小」的圖像更接近真相:高維裡的極小點多到形成一個連通的低損失區域,而它們的品質分布是連續的。

第二段:初始化尺度的掃描。 同一個架構,只改 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=1=1(也就是 1/fan-in1/\sqrt{\text{fan-in}} 那個基準)。兩端壞得方式不同:gain=0.01=0.01 時權重太小,前向訊號幾乎是零、反向梯度也幾乎是零,4000 步還沒走到(0.00640.0064,而且最差的那個 seed 是 0.01350.0135);gain=30=30 時大部分單元一開始就落在 tanh\tanh 的飽和區(導數趨近零),0.01560.0156,而且最差的那個 seed 是 0.05230.0523——尺度太大時 seed 之間的散佈也跟著變大

注意這個掃描的範圍:從谷底往兩邊各差 3030 倍,損失只變差 2255 倍。它比學習率溫和霧中下山,一步該走多大:梯度下降的一頁η\eta 跨過門檻是 1012210^{122}),但它是一個實實在在的旋鈕,而且因為它在訓練開始前就決定,調錯了不會給你任何錯誤訊息

第三段:深度與梯度的比值。 量初始化時 L/W1\lVert\partial L/\partial W_1\rVertL/Wlast\lVert\partial L/\partial W_{\text{last}}\rVert 的比(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=0.5=0.511 那兩欄從深度 2 到 32 幾乎是平的0.160.220.16\to0.22)。也就是說:在 1/fan-in1/\sqrt{\text{fan-in}} 這個初始化下,32 層的網路沒有梯度消失。這直接否證了「深就會梯度消失」這個說法的一般形式。

而 gain=2=2 那一欄從 0.190.19 爬到 7.637.63——同一個深度、只把初始化放大兩倍,就從「平的」變成「放大 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 時比值是 328328,而無殘差是 0.220.22。原因寫在上面的 <Remark> 裡——每一層都把訊號加上一份自己的輸出,所以量級隨深度累積。

這不是說殘差沒用。它是說殘差不是一個可以單獨加上去的零件:它把訊號的量級從「守恆」推到「成長」,所以必須配一個把量級拉回來的東西(1/D1/\sqrt D 的縮放、可學的、初始化為零的門,或一個正規化層)。兩個修法方向相反,這就是為什麼它們總是成對出現。

補充本篇用的三十行 numpy MLP

為了讓上面的實驗可以逐行檢查(而不是呼叫一個框架),mlp.py 只做三件事:init(sizes, gain, rng)gain/fan-in\text{gain}/\sqrt{\text{fan-in}} 初始化每一層;grads(P, X, y, skip) 手寫前向與反向(tanh\tanh 的導數是 1tanh21-\tanh^2,殘差那一支在反向時多加一個恆等項);adam_train(P, X, y, T, eta)一個 η 不夠用:動量與 Adam 各買到什麼 那個更新式,含偏差修正。

刻意沒有做的事:沒有 minibatch(全批次,30 筆資料不需要)、沒有正規化層、沒有 weight decay。這樣一來每一個實驗只有一個變數在動——這是能把 gain 的效果和其他東西分開的原因,也是為什麼這裡的數字比在一個完整框架上跑出來的乾淨。

換 seed、換 gain、換深度

互動 demo:散佈與尺度是兩件事:seed 的散佈量的是前者,梯度比量的是後者。

先消化一下

想一想

一個團隊比較兩個架構,各跑一次:A 得到訓練 MSE 0.00320.0032、B 得到 0.00410.0041,結論是「A 比較好」。根據本篇的散佈實測,最合理的評論是:

想一想

一個 32 層的網路訓練不動,前幾層的權重幾乎不變。同事說「深層網路本來就會梯度消失,要加殘差」。根據本篇的實測,更準確的診斷順序是:

想一想

在 gain 的掃描裡,gain=30=30 的中位數是 0.01560.0156 而最差的 seed 是 0.05230.0523;gain=0.1=0.1 的中位數是 0.00570.0057 而最差是 0.00590.0059。這個對比說明:

想一想

下列哪一句不對

參考文獻

  1. Glorot, X., Bengio, Y. Understanding the difficulty of training deep feedforward neural networks. AISTATS 2010.(把「讓每層訊號變異數守恆」寫成初始化條件,1/fan-in1/\sqrt{\text{fan-in}} 的來源。)
  2. He, K. et al. Delving Deep into Rectifiers. ICCV 2015.(ReLU 下的對應版本,以及深度與初始化尺度如何一起決定訓練成不成立。)
  3. Li, H. et al. Visualizing the Loss Landscape of Neural Nets. NeurIPS 2018.(損失曲面的視覺化方法,以及殘差與寬度如何改變曲面的樣子。)