§01學習重點
- 說出訓練誤差為什麼不能當成表現估計,並指出「訓練誤差降到零」與「過擬合」不是同一件事
- 用「誰有決定權」講清楚訓練集、驗證集、測試集的分工,並判定一段工作流程有沒有違反切分紀律
- 親手跑出樂觀偏誤:在完全沒有訊號的資料上,光靠反覆挑選就能把分數推高多少
- 說出測試誤差的三塊組成各自從哪裡來、哪一塊你動得了、哪一塊動不了
- 寫出平方誤差損失下的變異—偏誤—雜訊分解,並指出這條式子在什麼條件下才成立
- 親手畫出一條雙下降曲線,說出尖峰落在哪裡、越過之後為什麼還會再降一次
- 精確指出雙下降推翻的是哪一句話——不是分解式本身,而是「變異隨容量單調上升」這個附加假設
- 說明超參數為什麼不能用梯度下降來調,並列出測試集通過之後仍可能失準的三種偏移
§02課程內容
一、訓練成績為什麼不能當成績單
先講一件會讓人不太舒服的事:只要模型容量夠大,把訓練資料擬合到誤差幾乎為零,是很容易做到的。就算你把標籤全部隨機打亂、讓輸入與答案之間完全沒有關係,一個夠大的網路照樣背得起來。所以「訓練誤差很低」這個事實本身幾乎不帶訊息——它可能表示模型抓到了規律,也可能只表示模型記性好,而這兩種情況在訓練集上長得一模一樣。
要把它們分開,只有一個辦法:拿一批模型沒看過的資料去問它。這批資料叫保留測試集(held-out test set),而模型在沒見過的資料上維持表現的程度,就叫泛化(generalization)。本章從頭到尾在處理同一件事——怎麼把泛化能力量出來,以及量出來的數字該怎麼讀。
先把兩個誤差寫下來,後面的討論才有共同的符號:
逐項拆解:\(\boldsymbol\phi\) 是模型的參數;\(f[\cdot,\boldsymbol\phi]\) 是模型;\(\ell[\cdot,\cdot]\) 是單筆資料的損失,第 5 章談過怎麼從機率分布導出它。上式左邊對 \(I\) 筆訓練資料 \((x_i,y_i)\) 取平均,右邊對 \(J\) 筆測試資料 \((\tilde{x}_j,\tilde{y}_j)\) 取平均,波浪號只是提醒你那是另一批資料。兩邊都除以自己的筆數,這一點很要緊:訓練集與測試集的大小通常差很多,只有取平均才能把兩個數字直接並排看。有些教材把損失寫成總和、不除以筆數,那跟這裡只差一個正的常數倍,最小值落在同一組參數上,但兩個數字就不能互相比較了。
訓練誤差降到零之後,還有一個常被誤讀的現象。分類任務通常同時盯兩個數字:錯了幾筆(錯誤率)與損失值。訓練到某個階段,測試錯誤率可能已經停住不動,測試損失卻還在往上爬。這兩件事並不矛盾。錯誤率只問模型有沒有把最高分給對的類別,損失還在乎它給了多少把握。當模型繼續把 softmax 之前的分數往兩極推,答對的那些筆損失只能再降一點點(本來就已經很接近零),答錯的那幾筆損失卻可以無上限地漲——整體平均因此被少數幾筆「愈錯愈有把握」的樣本拉高。這個現象叫過度自信(overconfidence)。
它的實務意義是:只盯錯誤率,你會漏看模型的機率輸出已經開始不可信。而下游只要有任何一個環節在用那個機率做決定(例如設一個門檻,把把握不足的案例送人工複核),這個模型其實已經壞了,錯誤率卻不會告訴你。
二、三套資料的分工:一條不能違反的紀律
有了測試集還不夠。真正的問題是:一個專案裡有一大堆決定要做——網路要幾層、每層多寬、學習率取多少、訓練多久——這些決定不是靠梯度算出來的,得靠實際跑一遍看結果。而只要你「看結果來做決定」,被你拿來看的那批資料就開始被消耗。
所以資料要切成三份,各自握有不同的決定權:
- 訓練集(training set) 決定參數(parameter),也就是被梯度下降調整的那些數字。
- 驗證集(validation set) 決定超參數(hyperparameter),也就是所有不由梯度決定的選擇。
- 測試集(test set) 什麼都不決定。它只做一件事:對已經完全定案的模型,估計它的表現。
這個切法背後只有一個原則,但值得單獨寫成一行:選擇的動作和估計的動作,不能用同一批資料完成。 一旦某批資料被你拿來「挑」過,它給出的分數就不再是無偏的估計,而是被挑選過程推高過的數字。
比喻: 一款新藥要上市,得依序走完幾期臨床試驗。第一期在少數人身上找安全的劑量範圍,怎麼調、調幾輪都可以,因為這一期的任務就是「調」。第二期換一批人,比較幾種給藥方案,挑出最有希望的那一種——它同樣可以反覆做,但它挑出來的成績不能當成療效證據,因為那個成績是被挑出來的,挑的過程本身就會偏向剛好走運的那一個方案。第三期試驗招募全新的一群受試者,方案在開始之前就凍結,執行完只回答一個問題:這個已經定案的東西,效果有多大。三套資料的分工就是這個順序。
這個比喻在一個地方明確失準:臨床試驗做壞了可以重來——換一群人、重新登記、重新執行,新的一期依然有效。測試集不行。你只要根據測試集的分數改過任何一個決定,那份資料就永久失去了「沒被看過」的身分,而且沒有任何辦法讓它恢復;你只能去弄一批真正全新的資料。可回復與不可回復,是這兩件事最大的差別。
這個「挑」到底能把分數推高多少? 這不是修辭問題,可以直接量。下面這段程式造一批完全沒有訊號的資料——受試者的療效標籤是丟銅板決定的,跟他們的任何一項量測都無關——然後假裝我們有一大堆候選模型,每次都挑驗證分數最高的那一個,看分數會被推到哪裡去:
import numpy as np
rng = np.random.default_rng(0)
# 假想情境:受試者的療效標籤是丟銅板決定的,資料裡完全沒有可學的訊號。
# 我們仍然拿 K 個候選模型去「挑最好的」,看驗證分數會被挑高多少。
n_val, n_test, K, R = 200, 200, 400, 300
best_v = np.zeros((R, K))
paired_t = np.zeros((R, K))
for r in range(R):
pred = rng.integers(0, 2, (K, n_val + n_test)) * 2 - 1 # K 個候選模型的預測
truth = rng.integers(0, 2, n_val + n_test) * 2 - 1 # 與預測無關的真標籤
hit = (pred == truth)
acc_v = hit[:, :n_val].mean(axis=1) # 驗證集分數
acc_t = hit[:, n_val:].mean(axis=1) # 測試集分數
win = np.maximum.accumulate(acc_v) # 挑過前 k 個的最佳值
idx = np.maximum.accumulate(np.where(acc_v >= win, np.arange(K), -1))
best_v[r], paired_t[r] = win, acc_t[idx]
print("挑選次數 k 驗證集最佳(平均) 同一模型的測試集分數(平均)")
for k in (1, 5, 20, 100, 400):
print(f"{k:>9} {best_v[:, k-1].mean():.4f}"
f" {paired_t[:, k-1].mean():.4f}")實跑輸出:挑 1 次時驗證集最佳是 0.5033、該模型的測試集分數 0.5009;挑 5 次變成 0.5416 對 0.5017;挑 20 次是 0.5648 對 0.5041;挑 100 次是 0.5896 對 0.4988;挑 400 次是 0.6053 對 0.4989。
請把這兩欄並排讀。左欄從 0.5033 一路爬到 0.6053,看起來像是「我們找到了一個好模型」;右欄始終貼在 0.50 附近不動,而 0.50 正是這批資料真正的上限——標籤是丟銅板決定的,任何模型都不可能比擲銅板更準。左欄那十個百分點是純粹被挑出來的,資料裡一絲一毫的訊號都沒有。這個高估叫樂觀偏誤(optimistic bias),它的大小只取決於你挑過幾次,跟模型好不好無關。
這就是「測試集只能用一次」這條紀律的全部理由。要說清楚的是:這是本課要你養成的工作紀律,不是什麼數學定理,也沒有哪條理論規定你只准看一次。它的正當性完全來自上面那段程式量出來的膨脹量——你每多看一次、多改一個決定,那個數字就多虛一點,而虛掉多少沒有人替你記帳。
比喻: 一份臨床試驗的資料收完了,主要終點沒有達標。這時候如果回頭翻資料,總翻得出某個小群體看起來有效——六十歲以上、女性、帶某個基因型的那一小撮人。問題在於,你是看過結果才決定要看這一群的,而不是動手前就寫在計畫書上。這種事後挑出來的「有效」,跟上面那段程式在純雜訊裡挑出
0.6053是同一回事。臨床研究界為此發明了預先登記:主要終點、分析方法、次族群的定義,全部要在收資料之前公開寫死,事後才有辦法檢查你改過沒有。這個比喻的失準之處在稽核。臨床試驗的登記紀錄是公開的,別人可以逐條核對你有沒有中途換過終點;而一個機器學習專案的驗證集被看過幾次、換過幾種架構、調過幾輪學習率,通常沒有任何紀錄——連你自己都數不清。所以這裡的紀律不能靠事後查核,只能靠一開始就把測試集鎖起來,並且在動手之前就寫下它什麼時候才准打開。
切分本身也會出錯。 有一類問題叫資料洩漏(data leakage):測試集的資訊在你沒察覺的情況下滲進了訓練流程。常見的三種:
第一種是切分前做全域前處理。你先對整批資料算平均值與標準差再標準化,然後才切分——測試集的統計量已經進了訓練資料的每一個數字裡。正確順序是先切,再只用訓練集算出的統計量去換算其餘兩套。
第二種是去重不完全。同一個受試者在資料庫裡出現兩次、同一張影像有兩個略微不同的版本,切分時一份落在訓練、一份落在測試,模型只要記住第一份就能答對第二份。要切的單位是「來源」而不是「筆數」。
第三種是時間順序被打亂。資料若帶有時間性,隨機切分會讓模型用未來的資料預測過去。正確做法是按時間切,測試集永遠取最晚的那一段。
還有一個相關但不同的問題:類別不均時的切分。如果某一類只佔全體百分之二,隨機切分很可能讓測試集裡一筆都沒有。分層抽樣(stratified split)的做法是先按類別分組,再在每一組內部各自按比例切,保證三套資料的類別比例一致。
資料很少的時候怎麼辦? 切三份會讓訓練集小到不堪用。標準做法是 k 折交叉驗證(k-fold cross-validation):把「訓練+驗證」這一大塊平均切成 k 份,輪流拿其中一份當驗證、其餘 k−1 份當訓練,跑 k 次取平均。每一筆資料都當過驗證,也都當過訓練,估計的抖動因此小很多。但請注意一條不變量:交叉驗證只在訓練與驗證之間輪替,測試集從頭到尾不參與。把測試集也丟進去輪,等於把整批資料都變成了驗證集。
三、測試誤差是由什麼堆出來的
紀律建立好之後,我們手上有一個乾淨的測試誤差數字。接下來的問題是:這個數字為什麼降不下去?它其實由三塊性質完全不同的東西堆成,而你能施力的只有其中兩塊。
第一塊是雜訊(noise)。 資料生成的過程本身就帶隨機性:同樣的輸入不見得對應同樣的輸出。看到這種情形,先問一個問題——那個「同樣的輸入」,真的是同樣的嗎?
很多時候不是。兩位受試者在你的表格上每一欄都相同,療效卻差一大截;追下去常會發現有一欄你根本沒收——其中一位長期服用另一種藥,而你的資料表裡沒有這個欄位。這一類的雜訊看起來像隨機,本質上卻是可解釋、只是沒被你量進來的東西。它也是最容易被誤判的一類,因為它有個別種雜訊沒有的性質:多收一欄,它就從雜訊變成訊號。
問完這一題,剩下的才是真的沒得救,可以分成兩種。一種出在記錄環節:判讀的人看走眼、數字打錯行,標籤本身就是壞的。另一種出在世界本身:同一個人、同一天、同一劑量,生理反應本來就有起伏,任何欄位都補不回來。這幾種雜訊的處理方式完全不同,混在一起談會讓你把可以補救的當成不能補救的。
雜訊有兩個關鍵性質。第一,在你手上這組欄位固定不動的前提下,它是測試誤差的下界:不管模型多好,這一塊都留在那裡,這就是圖 08-2 裡那條水平的虛線。
第二個性質常被誤推,值得慢慢講。既然雜訊擋在那裡,很多人會順手推論訓練誤差也降不到零——這是錯的。想一想模型要在什麼情況下才會被雜訊卡住:它得同時看到兩筆輸入完全相同、標籤卻不同的資料,那時候不管吐出什麼數字都會錯一邊,這才叫「被迫取捨」。可是只要輸入裡有一維是連續值,訓練集裡就幾乎不可能撞出兩筆一模一樣的輸入。既然那個取捨的場面從頭到尾沒發生過,模型也就沒有任何理由停手,它可以順著每一筆給定的標籤一路把誤差壓到零。雜訊是測試誤差的地板,卻不是訓練誤差的地板——第一節那句「訓練誤差幾乎不帶訊息」,根就在這裡。
第二塊是偏誤(bias)。 你挑的那一族函數可能根本就畫不出真實的形狀。用一條直線去描述一條彎曲的曲線,不管參數怎麼調都貼不上去,剩下的那個系統性偏移就是偏誤。它是「取到這一族裡最好的那組參數之後,仍然存在的誤差」。
這裡有一個中文與英文都會撞名的坑要先擋掉:統計學裡的「估計偏誤」講的是估計量的期望值偏離真值,跟這裡的「模型偏誤」是同名不同義的兩件事。看到 bias 這個字時,先確認上下文在講哪一個。
第三塊是變異(variance)。 你的訓練集只是從真實世界抽出來的一小把樣本,換一把就會擬合出一條不一樣的曲線。這些曲線彼此之間的散布程度就是變異。它還有第二個來源常被忽略:最佳化演算法本身帶隨機性——初始化不同、批次順序不同,同一批資料也會訓練出不同的模型。
三者的可控性差很多:雜訊你動不了(除非回頭去改資料收集流程),偏誤靠加容量降,變異靠加資料降。
把它寫成式子。 在迴歸任務、平方誤差損失下,這三塊剛好可以乾淨地相加。先定義兩個量:\(\mu[x]\) 是給定輸入 \(x\) 時真實輸出的平均值(也就是拿掉雜訊之後那條「真正的」曲線),\(\sigma^2\) 是輸出繞著 \(\mu[x]\) 的變異數。再定義 \(\bar{f}[x] = \mathbb{E}_{\mathcal{D}}\bigl[f[x,\hat{\boldsymbol\phi}]\bigr]\),讀作「把訓練集 \(\mathcal{D}\) 換過無數次、每次都重訓一個模型,這些模型在 \(x\) 這一點的預測值取平均」。於是:
逐項拆解:等號左邊是期望測試誤差——外層 \(\mathbb{E}_{\mathcal{D}}\) 對「抽到哪一個訓練集」取平均,內層 \(\mathbb{E}_{y}\) 對「同一個 \(x\) 底下真實輸出的隨機性」取平均,\(\hat{\boldsymbol\phi}\) 是在那個訓練集上訓練出來的參數。右邊第一項是變異:模型的預測繞著自己的平均散開多少。第二項是偏誤的平方:那個平均本身離真曲線多遠。第三項就是雜訊。推導的技巧只有一個——在括號裡「加一項再減掉同一項」,把 \(\bar{f}[x]\) 塞進去,展開後那個交叉項因為期望值的定義剛好等於零。完整的代數展開本課不寫,思路就是這一句。
兩個邊界條件要記住,第五節會用到。第一,這個乾淨的三項相加只保證在迴歸加平方誤差損失下成立;換成分類任務或別的損失,三者依然存在,但不見得這樣相加。第二,分類問題還有一個反直覺的地方:如果平均而言模型已經偏向錯的類別,那麼適度的變異反而有幫助——它讓一部分模型偶然跳回正確類別。所以「變異越小越好」在分類上不成立。
最後一句提醒,很多人在這裡卡住:可分解不等於可量測。 上面那條式子需要你知道真實的 \(\mu[x]\)(你不知道,那正是要學的東西),而且需要很多組獨立的訓練集(你只有一組)。所以在真實專案裡,你量不到偏誤和變異各自是多少,只量得到它們的總和。下面這段程式之所以能把三塊分開列出來,是因為那批資料是我們自己造的——真實函數是我們寫的,訓練集要幾組有幾組。
import numpy as np
rng = np.random.default_rng(0)
def mu(x): # 假想的劑量—反應真實關係
return np.tanh(10 * (x - 0.4))
sigma, N, R = 0.15, 60, 500 # 雜訊標準差、每次試驗人數、重複次數
grid = np.linspace(0.02, 0.98, 200) # 評估用的劑量格點
truth = mu(grid)
xs = rng.uniform(0, 1, (R, N)) # R 次獨立試驗
ys = mu(xs) + rng.normal(0, sigma, (R, N))
def design(x, K): # 容量 K=用 K 個餘弦基底
return np.cos(np.pi * np.outer(x, np.arange(K)))
print(" K 偏誤平方 變異 雜訊 合計")
for K in (1, 2, 4, 6, 8, 12, 16, 20, 26):
preds = np.empty((R, grid.size))
for r in range(R):
w, *_ = np.linalg.lstsq(design(xs[r], K), ys[r], rcond=None)
preds[r] = design(grid, K) @ w
b2 = np.mean((preds.mean(0) - truth) ** 2)
v = np.mean(preds.var(0))
print(f"{K:>2} {b2:10.4f} {v:10.4f} {sigma ** 2:10.4f} "
f"{b2 + v + sigma ** 2:10.4f}")這段程式假想一款試驗用藥的劑量與某項生理指標之間有一條固定的真實關係,每次試驗招募 60 位受試者、量到的指標帶有標準差 0.15 的雜訊,整個流程重複 500 次。容量 \(K\) 就是模型用了幾個餘弦基底函數。實跑輸出的「偏誤平方/變異/合計」三欄依 \(K\) 是:\(K=1\) 為 0.7499、0.0126、0.7850;\(K=2\) 為 0.0842、0.0032、0.1098;\(K=4\) 為 0.0168、0.0028、0.0421;\(K=6\) 為 0.0024、0.0027、0.0276;\(K=8\) 為 0.0002、0.0034、0.0261;\(K=12\) 為 0.0000、0.0061、0.0286;\(K=16\) 為 0.0001、0.0250、0.0476;\(K=20\) 為 0.0000、0.0277、0.0502;\(K=26\) 為 0.0150、9.7379、9.7753。雜訊那一欄從頭到尾都是 0.0225,也就是 0.15 的平方。
四、兩條施力點,與那條古典的 U 形曲線
上一節的表已經把兩條施力點攤開了。加容量會讓偏誤下降:從 \(K=1\) 到 \(K=8\),偏誤平方從 0.7499 掉到 0.0002,模型終於畫得出那條曲線的形狀。加資料會讓變異下降:這一點作業二會請你實測。
先把「容量」講清楚。容量(capacity)沒有唯一的定義,日常用法是拿參數數量或隱藏單元數當代理指標。要精確一點的話,得分成兩層:表徵容量(representational capacity)指的是這一族函數原則上畫得出哪些形狀;有效容量(effective capacity)指的是你的最佳化演算法實際上搆得到的那個子集合。兩者常常差很多——一個網路能表示的函數裡,有一大堆是梯度下降從你的初始點出發永遠走不到的。統計學習理論還有兩個更形式化的容量度量,VC 維度(VC dimension)與 Rademacher 複雜度(Rademacher complexity);它們需要的前置知識超出本課,這裡只點名,想深入請看參考資料。
現在把兩條施力點放在一起。在資料量固定的前提下,容量往上加會同時發生兩件方向相反的事:偏誤下降、變異上升。兩者相加就形成一條 U 形曲線——一開始總誤差跟著偏誤一起降,降到某個容量之後變異接手主導,總誤差轉頭往上。這條曲線在古典統計裡叫偏誤—變異權衡(bias-variance trade-off),U 形的谷底就是「最佳容量」。
圖上的谷底落在 \(K=8\),合計 0.0261。往左是欠擬合:容量不夠,模型連訓練集都貼不上去,訓練誤差與測試誤差同時很高。往右是過擬合(overfitting):多出來的表達力被拿去描述訓練集裡的雜訊,於是訓練誤差繼續降、測試誤差反而升。這個「訓練誤差與測試誤差的落差持續拉大」就是過擬合最常用的操作型徵狀。
順帶擋掉一個很常見的誤解:訓練誤差降到零不等於過擬合。 過擬合說的是測試誤差開始上升,不是訓練誤差降到零。這兩件事在古典圖像裡常常同時發生,久了就被當成同一件事——第五節會用一條實測曲線把它們拆開。
最後把這條古典圖像的適用前提寫出來,因為下一節要正面挑戰它。U 形曲線來自一個容量遠小於樣本數的年代:模型有幾十個參數、資料有幾百筆,「參數量比樣本數還多」在那時候不是可行的選項,而是明顯的錯誤。在這個前提裡,「參數量大於樣本數 ⇒ 解不唯一 ⇒ 必然過擬合」是一條合理的推論。深度學習做的事情,剛好整個落在這個前提之外。
五、雙下降:古典直覺斷在哪裡
現在把容量一路往上加,加到超過訓練樣本數,看看會發生什麼。
下面這段程式做一個最小可執行的示範。假想的試驗設定是這樣:每位受試者有 16 項基線量測(把它想成一個 16 維的向量),真實的療效沿著某個固定方向變化;訓練集只有 60 位受試者,標籤帶雜訊;測試則用 2000 位受試者的無雜訊真值。模型是一個單隱藏層網路,寫成:
逐項拆解:\(\mathbf{x}\) 是輸入向量;\(\boldsymbol\Omega\) 是第一層的權重矩陣,\(\mathbf{b}\) 是第一層的偏置向量(本課全站把偏置寫成粗體的 \(\mathbf{b}\);有些教材用希臘字母 \(\boldsymbol\beta\),但本課的 \(\beta\) 已經留給第 6 章的動量衰減率,為了跨章不撞名,這裡一律用 \(\mathbf{b}\));\(a[\cdot]\) 是激活函數,這裡取 ReLU;\(\boldsymbol\omega\) 是最後一層的權重,\(\mathsf{T}\) 表示轉置。為了讓「容量」這個變數乾淨地只有一個,我把 \(\boldsymbol\Omega\) 與 \(\mathbf{b}\) 隨機抽出來之後就固定不動,只求解 \(\boldsymbol\omega\)。這樣一來,隱藏單元數 \(P\) 就是唯一的容量旋鈕,而且求解 \(\boldsymbol\omega\) 是一個線性最小平方問題,有閉式解,不會混進最佳化演算法自己的隨機性。
當 \(P\) 超過訓練樣本數,能把訓練誤差降到零的 \(\boldsymbol\omega\) 有無限多個,得挑一個。這裡挑的是最小範數解:
逐項拆解:\(\boldsymbol\Phi\) 是把 60 位受試者各自算出的 \(P\) 個隱藏單元輸出排成的矩陣,\(\mathbf{y}\) 是他們的標籤;橫線下面那一行的意思是「在所有能完美擬合訓練集的 \(\boldsymbol\omega\) 之中」,而 \(\|\boldsymbol\omega\|_{2}\) 是向量的長度。所以這行式子讀作:在所有零訓練誤差的解裡,挑長度最短的那一個。np.linalg.lstsq 在方程式數少於未知數時交出的就是這個解——請記住這個選擇,本節後半解釋第二次下降時它是關鍵。
import numpy as np
rng = np.random.default_rng(0)
d, N, M, REP = 16, 60, 2000, 20 # 輸入維度、訓練人數、測試人數、重複次數
u = rng.normal(0, 1, d)
u /= np.linalg.norm(u)
def sample(n): # 高維球面上的樣本;真實反應沿固定方向 u
x = rng.normal(0, 1, (n, d))
x /= np.linalg.norm(x, axis=1, keepdims=True)
return x, np.tanh(2 * np.sqrt(d) * (x @ u))
Ps = [2, 4, 8, 12, 16, 20, 26, 32, 40, 48, 54, 58, 60, 62,
66, 72, 84, 100, 140, 200, 320, 500, 800, 1200]
tr = np.zeros(len(Ps))
te = np.zeros(len(Ps))
for _ in range(REP):
Xtr, ytr = sample(N)
Xte, yte = sample(M)
ytr = ytr + rng.normal(0, 0.15, N) # 訓練標籤帶雜訊、測試標籤是真值
W = rng.normal(0, 1, (max(Ps), d)) # 隨機的第一層,不訓練
b = rng.uniform(-0.5, 0.5, max(Ps))
Htr, Hte = np.maximum(Xtr @ W.T + b, 0), np.maximum(Xte @ W.T + b, 0)
for i, P in enumerate(Ps):
w, *_ = np.linalg.lstsq(Htr[:, :P], ytr, rcond=None) # 最小範數解
tr[i] += np.mean((Htr[:, :P] @ w - ytr) ** 2) / REP
te[i] += np.mean((Hte[:, :P] @ w - yte) ** 2) / REP
print(f"訓練樣本數 N = {N}")
print(" P 訓練誤差 測試誤差")
for P, a, c in zip(Ps, tr, te):
print(f"{P:>5} {a:9.5f} {c:12.4f}")實跑輸出的測試誤差依 \(P\) 是:0.6271(P=2)、0.5998(4)、0.5329(8)、0.5055(12)、0.4663(16)、0.4391(20)、0.4537(26)、0.4746(32)、0.5958(40)、0.9028(48)、1.6581(54)、23.3681(58)、83.3478(60)、6.8910(62)、2.0519(66)、1.1119(72)、0.6344(84)、0.4468(100)、0.3084(140)、0.2528(200)、0.2195(320)、0.2044(500)、0.1935(800)、0.1878(1200)。訓練誤差則從 0.59824(P=2)一路降到 0.00526(P=58),並在 \(P=60\) 之後恆為 0.00000。
這條曲線分成三段,各有名字:
古典區(classical regime),也叫欠參數化區。從 \(P=2\) 到 \(P=20\),測試誤差從 0.6271 降到 0.4391,然後轉頭往上——就是第四節那條 U 形曲線,谷底在 \(P=20\)。
臨界區(critical regime),容量剛好夠記住訓練集的那一帶。誤差在這裡不是溫和地上升,而是暴衝:0.9028(P=48)→ 23.3681(P=58)→ 83.3478(P=60)。尖峰的位置正好落在 \(P=N=60\),也就是特徵數等於訓練樣本數的那一點。這個位置在文獻上叫插值閾值(interpolation threshold)——順帶說明,這是這個領域通用的講法(見參考資料的兩篇論文),不是本課或任何一本教科書獨有的用語。
尖峰為什麼這麼高?直覺是這樣:在 \(P\) 接近 \(N\) 的那一帶,模型的自由度剛好勉強夠穿過每一個訓練點,一點餘裕都沒有。它只能把自己扭曲到極致去命中每一筆——包括那些純粹是雜訊的偏移。方程式數與未知數數量相當時,解對資料的微小擾動極度敏感,於是那條扭曲的曲線在訓練點之間亂竄。這一段的訓練誤差已經很低(\(P=58\) 時是 0.00526),測試誤差卻高到不能看,是「訓練誤差趨近零」與「過擬合」被拆開來看得最清楚的地方。
過參數化區(over-parameterized regime)。越過閾值之後,測試誤差開始第二次下降:6.8910(P=62)→ 0.4468(P=100)→ 0.2528(P=200)→ 0.1878(P=1200)。請注意最後這個數字:0.1878 比古典區的谷底 0.4391 還要低一半以上。U 形曲線給出的那個「最佳容量」,在這裡根本不是最佳。
而且整個過參數化區的訓練誤差都是 0.00000。同樣是完美擬合訓練集,\(P=60\) 的測試誤差是 83.3478,\(P=1200\) 是 0.1878,差了四百多倍。這就把上一節那句提醒推到極致:訓練誤差告訴你的資訊,比你以為的還要少。
這條曲線推翻了什麼? 這是本章最需要講精確的一句話,因為它最常被講錯。
被推翻的不是第三節那條分解式。那是一條恆等式——只要在平方誤差損失下對兩層期望做代數展開,它就必然成立,跟模型多大、資料多少都沒有關係。恆等式不會被實驗推翻。
被推翻的是一條附加的經驗規律:變異隨容量單調上升。這條規律在容量遠小於樣本數的年代成立得很好,好到大家忘了它只是一個經驗觀察而不是定理,把它跟分解式當成同一件事。
要說清楚它是怎麼被推翻的,得先擋掉一個很順口、卻推不出來的講法。上面那組實驗的測試標籤是無雜訊的真值,所以測試誤差就等於偏誤平方加變異;於是有人會接著說:「越過閾值之後測試誤差一路下降,偏誤平方又不可能是負的,所以變異必定也在下降。」這一步無效。 第三節那條式子裡的偏誤平方項 \(\bigl(\bar{f}[x] - \mu[x]\bigr)^{2}\ge 0\),給你的只是一個上界——變異不會超過測試誤差——它對變異往哪個方向動一個字都沒說。偏誤平方大幅下降的時候,變異完全可以同時上升,兩者的和照樣在降。
這不是紙上談兵,反例就落在同一組設定的左半段。要看見它,必須把偏誤與變異分別量出來。第三節說過「可分解不等於可量測」,那句話講的是真實世界;在這個模擬裡我們自己就是資料生成過程,所以量得出來。做法是把測試集固定成同一批人不再更動,然後反覆重抽訓練集與隨機第一層,看那一群預測彼此散得多開:
import numpy as np
rng = np.random.default_rng(0)
d, N, M, REP = 16, 60, 2000, 60 # 分解需要更多次重複,變異才量得穩
u = rng.normal(0, 1, d)
u /= np.linalg.norm(u)
def sample(n):
x = rng.normal(0, 1, (n, d))
x /= np.linalg.norm(x, axis=1, keepdims=True)
return x, np.tanh(2 * np.sqrt(d) * (x @ u))
Ps = [2, 8, 20, 32, 60, 62, 100, 200, 1200]
Xte, yte = sample(M) # 關鍵:測試集只抽一次,之後固定不動
pred = np.zeros((REP, len(Ps), M)) # 每一次重跑、每一個 P 的預測都留著
for r in range(REP):
Xtr, ytr = sample(N)
ytr = ytr + rng.normal(0, 0.15, N)
W = rng.normal(0, 1, (max(Ps), d))
b = rng.uniform(-0.5, 0.5, max(Ps))
Htr, Hte = np.maximum(Xtr @ W.T + b, 0), np.maximum(Xte @ W.T + b, 0)
for i, P in enumerate(Ps):
w, *_ = np.linalg.lstsq(Htr[:, :P], ytr, rcond=None)
pred[r, i] = Hte[:, :P] @ w
bias2 = ((pred.mean(0) - yte) ** 2).mean(1) # 平均預測離真值多遠
var = pred.var(0).mean(1) # 各次預測彼此散得多開
mse = ((pred - yte) ** 2).mean(axis=(0, 2))
print(" P 偏誤平方 變異 偏誤平方+變異 測試 MSE")
for P, b2, v, e in zip(Ps, bias2, var, mse):
print(f"{P:>5} {b2:10.4f} {v:12.4f} {b2 + v:14.4f} {e:14.4f}")實跑輸出:\(P=2\) 是 0.5784 / 0.0502 / 0.6286(偏誤平方 / 變異 / 測試 MSE,下同)、\(P=8\) 是 0.3639 / 0.1964 / 0.5603、\(P=20\) 是 0.1733 / 0.3033 / 0.4766、\(P=32\) 是 0.1224 / 0.3834 / 0.5059、\(P=60\) 是 17.1505 / 846.8283 / 863.9788、\(P=62\) 是 0.1971 / 6.4503 / 6.6474、\(P=100\) 是 0.1049 / 0.3180 / 0.4229、\(P=200\) 是 0.1015 / 0.1529 / 0.2545、\(P=1200\) 是 0.1009 / 0.0891 / 0.1900。每一列的第三欄與第四欄完全相等,這是分解式在無雜訊測試標籤下的直接驗證。
(這些測試誤差跟上面那張主表不會一模一樣——重複次數從 20 改成 60、測試集也從「每次重抽」改成「固定一組」,是另一組抽樣。曲線的形狀與三段的位置完全一樣。唯一差很多的是尖峰那一格:863.9788 對上面的 83.3478。這不是筆誤,\(P=N\) 那一點在數值上近乎奇異,換一個亂數流就可能差一個數量級——作業三會再碰到同一件事。)
現在看 \(P=2\) 到 \(P=20\) 這一段:測試誤差從 0.6286 降到 0.4766,確實在下降;偏誤平方從 0.5784 降到 0.1733;而變異從 0.0502 升到 0.3033。測試誤差下降與變異上升同時發生——前面那條推理若成立,這幾格就不該存在。順帶把另一條救援路線也堵掉:尖峰 \(P=60\) 處的偏誤平方是 17.1505,比別處大兩個數量級,所以「偏誤平方小到可以忽略、於是變異約等於測試誤差」也不成立。
那過參數化區到底怎麼了?看右半段的變異欄:846.8283(P=60)→ 6.4503(62)→ 0.3180(100)→ 0.1529(200)→ 0.0891(1200)。變異確實一路在降,結論成立——但它是量出來的,不是從分解式推出來的。 這個分別值得你記住:恆等式告訴你幾塊東西加起來等於什麼,它從來不會告訴你其中任何一塊往哪裡動。想知道方向,只能去量。
分解式照樣成立,只是它三項裡的那一項不照老規矩走了。
分清楚這兩者為什麼重要?因為它決定你該丟掉什麼。如果你以為被推翻的是分解式,你會把「測試誤差可以拆成三塊」這個很好用的診斷框架也一起丟掉;而該丟掉的其實只有「容量越大越危險」這條經驗法則。
還有兩個延伸事實值得記住。第一,同一個現象也可以用訓練步數當容量軸:固定模型大小、把訓練時間拉長,測試誤差同樣可能先降、再升出一個峰、再降,這叫世代雙下降(epoch-wise double descent)。第二,雙下降不是每次都看得見。它的可見度取決於資料集、模型、損失函數、訓練長度,尤其取決於標籤裡有多少雜訊——雜訊越多,臨界區的尖峰越明顯;標籤很乾淨時,那個峰可能小到看不出來。上面那段程式如果把雜訊標準差調小,尖峰就會塌下去。
還有一個更反直覺的推論。 既然尖峰落在「容量與樣本數相當」的地方,那麼在容量固定的情況下增加訓練資料,就等於把模型從過參數化區往回推向臨界區——表現反而會變差。這聽起來荒謬,但作業三會請你親手量出來。
比喻: 假設有一款試驗用藥,它的不良反應率在低劑量時很低,在中劑量時衝到最高,再往上加反而又降下來。第一次看到這種劑量—反應曲線的人幾乎都會先懷疑資料出錯,因為我們對「劑量」的預設是單調的:越多越強。雙下降給人的衝擊是同一個形狀——我們對「容量」的預設也是單調的:越大越容易過擬合。曲線在中間隆起一個峰、越過之後反而變好,於是直覺搶在證據前面說「這不可能」。
這個比喻的失準之處必須說清楚:藥理上的非單調反應通常找得到明確的生理機制去解釋,例如某條代謝路徑在中劑量就飽和了。雙下降沒有這樣一個公認的機制。下面會給幾個候選解釋,但它們到今天都還是推測,不是定論。
為什麼會有第二次下降? 老實說,這仍是開放問題。目前的主流直覺大致是這樣一條線。
當 \(P\) 遠大於 \(N\),能把訓練誤差降到零的解有無限多個,而它們的形狀天差地遠——每一個都完美穿過那 60 個訓練點,在點與點之間卻可以長成完全不同的樣子。既然訓練損失完全分不出它們的好壞,真正決定測試表現的就變成「你挑了哪一個」。任何讓模型在這些等價解裡偏好某一類的因素,都叫正則器(regularizer);它偏好的方向叫歸納偏好(inductive bias)。(怎麼主動加入正則化來改善泛化,是第 9 章的整章主題,這裡只用到它的定義。)
在上面那段程式裡,這個偏好是明擺著的:lstsq 挑的是最小範數解,也就是所有完美擬合裡最「平緩」的那一個。容量越大,可選的解越多,其中最平緩的那一個就越平緩——這就是平滑插值假說。真實的深度網路沒有人明著寫下這個偏好,但初始化的位置、梯度下降走的路徑,似乎讓它偏向類似的方向,這叫隱式正則化(implicit regularization)。
要誠實指出這個解釋的漏洞:「有能力挑到平滑的解」不等於「一定會挑到」。目前沒有人能證明梯度下降在真實網路上必然收斂到那類解,也沒有公認的答案說明是什麼在鼓勵這種平滑。
不過有一件事可以量給你看:為什麼「在資料點之間平滑地補」這件事會這麼重要。答案是高維空間裡的空隙大得驚人。下面這段程式在維度逐步升高的立方體裡各丟 500 個點,量每個點到最近鄰居的距離、以及到最遠那個點的距離:
import numpy as np
rng = np.random.default_rng(0)
n = 500 # 每種維度都抽同樣多的點
print(" 維度d 最近鄰距離 最遠點距離 最近/最遠")
for d in (2, 4, 8, 16, 32, 64, 256, 1024):
x = rng.uniform(0, 1, (n, d))
diff = x[:, None, :] - x[None, :, :]
dist = np.sqrt((diff ** 2).sum(-1))
np.fill_diagonal(dist, np.inf)
near = dist.min(axis=1).mean()
np.fill_diagonal(dist, -np.inf)
far = dist.max(axis=1).mean()
print(f"{d:>6} {near:10.4f} {far:10.4f} {near / far:9.3f}")實跑輸出的「最近/最遠」比值依維度是:0.022(d=2)、0.101(4)、0.241(8)、0.399(16)、0.537(32)、0.654(64)、0.812(256)、0.902(1024)。二維時最近的鄰居只有最遠距離的 2.2%,一千零二十四維時卻是 90.2%——在高維空間裡,「最近的鄰居」跟「最遠的那個點」幾乎一樣遠,距離這個概念本身失去了鑑別力。這就是維度詛咒(curse of dimensionality)的一個面向。
換個說法:你的訓練資料不是鋪滿了空間,而是幾根孤零零的針,中間全是空的。模型在這些空隙裡怎麼補,訓練損失一個字都管不著——那裡沒有任何一筆資料在說話。「零訓練損失的解有無限多個」聽起來抽象,這就是它的幾何版本。
六、超參數怎麼挑,以及測試集之後的世界
驗證集的必要性其實在前面已經論證完了,只是分散在兩處:在古典區間裡,你量不到偏誤與變異各自是多少(第三節那句「可分解不等於可量測」);在過參數化區間裡,你不知道容量該加到哪裡才停(第五節那條曲線的右半段沒有理論告訴你何時該停)。兩邊都沒有可以直接計算的答案,所以只剩下一條路:把每一組候選設定實際跑一遍,用一批不參與訓練的資料量結果——這就是驗證集。
超參數大致分兩類。模型超參數決定函數族長什麼樣:層數、每層寬度、用哪種激活函數。演算法超參數決定你怎麼在這一族裡搜尋:學習率、批次大小、訓練多久。兩類的共通點是它們都不由梯度決定。
為什麼不能拿梯度下降去調它們?三個理由疊在一起。第一,很多超參數根本不連續——層數是整數,「用哪種激活函數」連數線都排不上,沒有導數可言。第二,就算是連續的超參數,你也拿不到它對測試表現的導數:中間隔著一整趟訓練過程,那不是一個你能直接微分的東西。第三,就算前兩關過了,超參數空間裡到處是局部極小,順著坡走下去多半停在一個不好的地方。
還有兩個實務上的麻煩。一是每次評估都很貴:超參數沒辦法「試一半」——想知道某一組值好不好,唯一的途徑是拿它從頭把模型訓練到底,再去驗證集上量一個數字。換句話說,你在這個空間裡每問一個問題,付出的代價就是一整趟訓練。二是評估本身帶雜訊:同一組超參數換個隨機種子重跑,結果就不一樣,所以兩組設定分數差一點點,很可能什麼都不代表。
再加上超參數之間會互相牽連——例如「用不用某個模組」為真時才有「那個模組要幾維」這個選項——整個搜尋空間是離散、條件相依、每次取樣都很貴而且還帶雜訊的。這種問題有幾條標準路線:
隨機搜尋(random search) 是最該先試的基準:在每個超參數的合理範圍裡隨機抽組合。它比格點搜尋更有效率,因為實際上通常只有少數幾個超參數真的重要,而隨機抽樣在那幾個重要的維度上會試到比較多不同的值。
貝氏最佳化(Bayesian optimization) 則對「超參數 → 表現」這個未知關係本身建一個模型,每次挑下一個要試的點時,同時考慮兩件事:這個點預測起來會不會好(利用),以及這個點附近我們有多不確定(探索)。這正是第 1 章談強化學習時出現過的那個取捨,換了個場合。
測試集通過了,就結束了嗎? 沒有。測試集只保證了一件事:在跟測試集同一個分布抽出來的資料上,模型的表現大概是那個數字。真實世界不保證維持同一個分布,這件事叫資料漂移(data drift),細分成三種:
- 共變量偏移(covariate shift):輸入的分布變了。假設你的模型是用某幾家醫學中心的資料訓練的,部署到基層診所,人口組成整個不一樣。
- 先驗偏移(prior shift):輸出的分布變了。假設原本每一百個人裡有三個陽性,換了季節變成十五個。
- 概念偏移(concept shift):輸入與輸出之間的關係本身變了。同樣的量測值,因為新的治療準則上路,對應的結果已經不同了。
前兩種還可以靠監控輸入與輸出的統計量抓出來;第三種最難,因為輸入看起來完全正常,只有實際結果會慢慢背離預測。所以部署不是終點,是另一段量測工作的起點:持續回收真實結果、持續跟預測比對。
新藥上市之後還有第四期,也就是上市後監測:受試規模與追蹤時間都遠大於前面幾期,專門找那些在前三期裡因為人數太少或觀察期太短而沒現形的問題。機器學習模型部署之後需要的是同一件事。這一章講了一整套紀律,走到最後仍然只能得到一個結論:紀律做滿,也只是讓你的估計值誠實,並不能讓它永遠成立。
§03原書對照
本課這一章把原書第 8 章的順序重排過:先立資料切分的紀律,再拆誤差來源,最後才進雙下降。原書的鋪陳與此不同,有幾處值得進階讀者按頁翻回去。
第一處是那個貫穿全章的實驗。原書 pp.118–119 用一個一維化的手寫數字資料集當基準,把樣本數、輸入維度、隱藏層寬度、批次大小、學習率與訓練步數全部寫死,讓後面每一張圖都能互相對得上;p.120 還指出一個容易被忽略的現象——測試錯誤率早已持平,測試損失卻繼續往上爬,並把成因追到 softmax 讓模型對錯誤答案愈來愈有把握。本課的示範資料與數字全為自造,想看一組固定設定下的完整診斷曲線,那三頁最直接。
第二處是誤差分解的完整代數。原書 p.122 先用三張並排的小圖,把雜訊、偏誤、變異各自畫成一塊灰色區域,讓讀者在碰到符號之前先看見這三件事長什麼樣;接著 pp.123–124 用兩頁把平方誤差一路拆到變異、偏誤、雜訊三項,中間反覆使用「加一項再減掉同一項」的配方手法,先對測試輸出取期望、再對訓練集抽樣取期望,分兩層走完;頁邊還指向附錄 C.2 的期望值運算規則供讀者補課。本課只給結果與思路骨架,這段推導的每一步都在那兩頁。
第三處是畫圖用的那個簡化網路。原書 pp.120–121 說明它為什麼把第一層權重固定、折點等距排開,因而可以閉式求解、保證取到全域最小,省掉隨機最佳化帶來的額外變異;pp.125–128 接著用它掃過資料量與容量兩個軸,畫出偏誤與變異各自的變化曲線,並標出兩者相加最小的那個容量值。
第四處是雙下降的原始證據。原書 pp.127–130 給了兩組對照曲線:乾淨標籤與刻意打亂一部分標籤,後者才讓臨界區的尖峰真正浮現;同一處還轉載了另外兩篇論文在別的資料集與架構上的結果。pp.129–132 是解釋的部分,包括高維空間有多空曠的量化估計,以及三條訓練損失都為零、形狀卻完全不同的曲線。原書在這裡的態度值得注意:它把平滑插值列為推測,並明說仍不清楚是什麼在鼓勵這種平滑。
第五處是超參數。原書 pp.132–133 先講清楚一件事——古典區間裡偏誤與變異其實都量不到,現代區間又無從預知容量該加到哪,所以只剩下經驗性的搜尋;接著才引入驗證集,並逐條說明超參數空間為什麼不能用梯度下降處理。
第六處是註記與習題。原書 pp.134–136 的註記區密度極高:分類問題裡變異反而有利的反例、k 折交叉驗證、表徵容量與有效容量之分、VC 維度、以訓練步數為軸的世代雙下降,以及「加資料反而讓表現變差」這個推論,全部收在這三頁;同一段還列出資料漂移的三種型態,以及一整排超參數搜尋演算法。想把超參數搜尋的選項一次看齊,那一段列得最全。p.135 把高維空間的反直覺性質整理成五條,p.137 再配上典型集的圖示,以及幾道要讀者自己動手驗一遍的習題。原書第 8 章對應印刷頁 pp.118–137。
§04作業和解答
作業一:判定六段工作流程有沒有違反切分紀律
下面是六段假想的工作流程。請逐段判定它有沒有違反本章第二節的切分紀律;若有,指出被破壞的是哪一條,以及正確做法。
(a)把全部資料算出平均值與標準差做標準化,然後切成訓練、驗證、測試三份。 (b)用訓練集訓練五種架構,在驗證集上挑最好的一種,最後在測試集上報告一個數字。 (c)在測試集上跑了一次,覺得分數比預期低,於是回頭把學習率調小重訓,再跑一次測試集,報告後者。 (d)資料量太少,改用 5 折交叉驗證:把全部資料切成五份,輪流當驗證集,回報五次的平均分數當作最終表現。 (e)某罕見類別只佔全體 2%,隨機切分後測試集裡剛好一筆都沒有,於是把訓練集裡的幾筆搬過去補上。 (f)用訓練集訓練,每個 epoch 在測試集上量一次,看到測試損失開始上升就停止訓練,報告停止時的測試分數。
解答 SOLUTION
(a)違反,屬於資料洩漏。標準化用到的平均值與標準差是從整批資料算的,測試集的統計量已經滲進了訓練資料的每一個數字。正確做法:先切分,再只用訓練集算統計量,並把同一組統計量套用到驗證集與測試集。
(b)沒有違反。這正是三套資料的標準用法:訓練集調參數、驗證集做選擇、測試集做一次估計。唯一要補的是「最後那個數字只能報一次」。
(c)違反,破壞的是「測試集只做估計、不參與選擇」。「調小學習率」這個決定是看著測試分數做出來的,測試集因此退化成第二個驗證集,第二次報告的分數帶有樂觀偏誤。正確做法:那個決定應該在驗證集上做;若測試集已經被看過,誠實的處理是回報「測試集已被使用過一次」,或另外取得一批全新資料。
(d)違反。交叉驗證的不變量是「只在訓練與驗證之間輪替」。把全部資料都拿去輪,等於沒有任何一份資料是從未參與過選擇的,那個平均分數是驗證分數,不是表現估計。正確做法:先切出測試集鎖起來,再在剩下的資料裡做 5 折交叉驗證。
(e)違反,而且是兩層違反。第一,「切完之後看了測試集的內容才動手調整」本身就是在用測試集做決定。第二,被搬過去的那幾筆曾經在訓練集裡,模型見過它們,測試集不再是未見資料。正確做法:一開始就用分層抽樣,讓三套資料的類別比例一致。
(f)違反。用測試損失決定何時停止訓練,就是拿測試集在挑一個超參數(訓練步數),而且報告的還是被挑中的那個最高分——樂觀偏誤最強的一種用法。正確做法:拿驗證集決定停止時機,停完之後才碰測試集。(用早停來改善泛化本身是合理的手法,屬於第 9 章的主題;這裡違反的是它用錯了資料。)
作業二:資料量怎麼移動「最佳容量」
把第三節那段偏誤—變異程式的訓練樣本數 \(N\) 改成 15、60、240 三種,其餘不動,容量掃 \(K = 2, 4, 6, 8, 12\)。(a)預測三種資料量下最佳容量會往哪個方向移動,並說明理由;(b)實際跑一遍驗證;(c)解釋為什麼同一個容量在不同資料量下的評價會反過來。
解答 SOLUTION
(a)預測:資料越多,最佳容量越大。理由來自兩條施力點的分工——偏誤只跟函數族的表達力有關,不會因為資料變多而改變;變異則隨資料量增加而下降。既然加容量的代價(變異上升)變小、好處(偏誤下降)不變,最划算的那個容量就會往右移,而且 U 形的右半邊會變平。
(b)把 N 改成迴圈變數,每個 \(N\) 各自重新建立 rng = np.random.default_rng(0) 與資料集之後,實跑得到的「偏誤平方/變異/合計」是:
- \(N=15\):\(K=2\) 為
0.0841、0.0154、0.1220;\(K=4\) 為0.0171、0.0416、0.0812;\(K=6\) 為0.0034、0.0834、0.1092;\(K=8\) 為0.1653、89.8902、90.0780(\(K=12\) 已經逼近樣本數,解完全發散,合計衝到 10 的 12 次方量級——那是第五節臨界區的預演)。 - \(N=60\):\(K=2\) 為
0.0842、0.0032、0.1098;\(K=4\) 為0.0168、0.0028、0.0421;\(K=6\) 為0.0024、0.0027、0.0276;\(K=8\) 為0.0002、0.0034、0.0261;\(K=12\) 為0.0000、0.0061、0.0286。 - \(N=240\):\(K=2\) 為
0.0841、0.0007、0.1074;\(K=4\) 為0.0168、0.0006、0.0399;\(K=6\) 為0.0024、0.0006、0.0255;\(K=8\) 為0.0002、0.0007、0.0235;\(K=12\) 為0.0000、0.0011、0.0236。
最佳容量從 \(N=15\) 的 \(K=4\)(0.0812)移到 \(N=60\) 的 \(K=8\)(0.0261)。\(N=240\) 時最低點仍在 \(K=8\)(0.0235),但 \(K=12\) 只差 0.0001——曲線已經幾乎沒有右半邊了,這就是預測裡「U 形變平」的部分。
(c)把三個 \(N\) 底下的偏誤平方欄由上往下讀,會發現它們幾乎完全一樣(\(K=4\) 都是 0.0168 上下)——偏誤確實不隨資料量改變。變異欄則從 0.0416 掉到 0.0028 再掉到 0.0006,掉了大約兩個數量級。所以同一個 \(K=8\) 在 \(N=15\) 時是災難(合計 90.0780),在 \(N=60\) 時卻是最佳(0.0261)。容量本身沒有好壞,只有「相對於資料量」的好壞,這也是為什麼第四節那條 U 形曲線一定要加上「資料量固定」這個前提才成立。
作業三:讓「更多資料」把模型弄壞
第五節提到一個反直覺的推論:容量固定時增加訓練資料,可能把模型從過參數化區推回臨界區,表現反而變差。請把第五節的程式改成固定特徵數 \(P = 70\)、掃訓練樣本數 \(N\),實測這個現象。(a)預測測試誤差對 \(N\) 的曲線長什麼形狀;(b)實跑驗證;(c)這個結果是不是表示「應該少收一點資料」?
解答 SOLUTION
(a)預測:曲線先隨 \(N\) 增加而變差,在 \(N \approx P = 70\) 附近衝到最高,越過之後又開始變好。理由是尖峰的位置由「容量與樣本數相當」決定;\(P\) 固定時,把 \(N\) 往上加就是把系統往那個點推。\(N\) 遠小於 \(P\) 時模型在過參數化區(好),\(N\) 遠大於 \(P\) 時回到古典區(也好),中間那一帶最糟。
(b)這一題的迴圈順序與亂數抽取次序會影響結果,所以完整程式直接附上,照著跑就能對到同樣的數字:
import numpy as np
rng = np.random.default_rng(0)
d, P, M, REP = 16, 70, 2000, 20 # 特徵數 P 固定,改掃訓練人數
u = rng.normal(0, 1, d)
u /= np.linalg.norm(u)
def sample(n):
x = rng.normal(0, 1, (n, d))
x /= np.linalg.norm(x, axis=1, keepdims=True)
return x, np.tanh(2 * np.sqrt(d) * (x @ u))
Ns = [20, 30, 40, 50, 60, 66, 70, 74, 80, 100, 140, 220]
te = np.zeros(len(Ns))
for _ in range(REP): # 外層重跑,內層掃 N:每一輪共用同一組測試集與第一層
Xte, yte = sample(M)
W = rng.normal(0, 1, (P, d))
b = rng.uniform(-0.5, 0.5, P)
Hte = np.maximum(Xte @ W.T + b, 0)
for i, N in enumerate(Ns):
Xtr, ytr = sample(N)
ytr = ytr + rng.normal(0, 0.15, N)
Htr = np.maximum(Xtr @ W.T + b, 0)
w, *_ = np.linalg.lstsq(Htr, ytr, rcond=None)
te[i] += np.mean((Hte @ w - yte) ** 2) / REP
print(f"特徵數 P = {P}")
for N, v in zip(Ns, te):
print(f" N={N:>4} 測試誤差 {v:12.4f}")實跑輸出的測試誤差是:0.3885(N=20)、0.3740(30)、0.4780(40)、0.6788(50)、1.0999(60)、3.0734(66)、819.2125(70)、2.3497(74)、1.2746(80)、0.4983(100)、0.3028(140)、0.1965(220)。曲線形狀與預測一致:從 \(N=30\) 起一路惡化,在 \(N=70\) 衝到最高,越過之後又一路變好。從 30 位受試者加到 70 位,測試誤差從 0.3740 惡化到 10 的 3 次方量級——資料多收了一倍出頭,模型爛了三個數量級。(\(N=20\) 到 30 那一小段還微幅變好,不算例外:20 離 70 還夠遠,仍穩穩待在過參數化區的舒適位置。)
但尖峰那個數字不要背。 \(N=P\) 這一點在數值上近乎奇異:訓練矩陣剛好接近方陣,最小平方解對資料的一丁點擾動極度敏感。只把上面程式的 default_rng(0) 換成 1、2、3、4,\(N=70\) 那一格分別是 1645.4594、424.4334、109.4048、127.2528——量級可以差上一個數量級,而其他每一格幾乎都不動。這件事本身就是教學重點的一部分:臨界區之所以叫臨界區,正是因為那裡的數字站不穩。你該記住的是形狀(先變差、尖峰落在 \(N \approx P\)、然後變好),不是尖峰的高度。
(c)不是。要注意兩件事。第一,\(N=220\) 時的 0.1965 比 \(N=20\) 時的 0.3885 好——資料一路加下去終究是有利的,變差的只是中間那一段。第二,這裡真正錯的是「容量固定不動」這個前提:如果你在加資料的同時也把 \(P\) 一起放大(維持在遠大於 \(N\) 的位置),就完全不會經過那個尖峰。所以這題的結論不是「少收資料」,而是容量與資料量必須一起考慮,不能只動一個。順帶一提,如果你在真實專案裡遇到「加了資料反而變差」,這個現象值得列進待查清單,但更常見的原因是新資料的分布跟舊的不一樣——也就是第六節的資料漂移。
§05參考資料
- NumPy:numpy.linalg.lstsq 官方文件 — 本章雙下降示範的核心工具,說明了未知數多於方程式時它交出的是最小範數解
- Reconciling modern machine learning practice and the bias-variance trade-off — 提出「雙下降」這個說法與插值閾值圖像的論文,本章第五節的曲線形狀就是在重現它的定性結果
- Deep Double Descent: Where Bigger Models and More Data Hurt — 在真實深度網路上把世代雙下降與「加資料反而變差」量出來,作業三的靈感來源
- Understanding deep learning requires rethinking generalization — 用隨機標籤實驗顯示大網路可以把任意資料背下來,第一節那句「訓練誤差幾乎不帶訊息」最常被引用的證據
- Random Search for Hyper-Parameter Optimization(JMLR) — 說明為什麼隨機搜尋通常比格點搜尋划算,第六節那條基準做法的依據
- scikit-learn:交叉驗證與常見的洩漏陷阱 — 把 k 折、分層抽樣、群組切分的差別整理得很清楚,作業一的延伸閱讀
- Understanding Deep Learning(MIT Press) — 本課課綱主題所本的原書出版頁(ISBN 9780262048644,2023-12 出版)
- udlbook 官方網站(作者釋出的 PDF、投影片與習題) — 原書作者維護的免費資源站(udlbook.com 會轉址到此)