JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

過度配適、正則化與驗證

建模的首要大罪及其解方:偏差-變異權衡、訓練/測試切分、交叉驗證,以及讓模型保持謙遜的正則化。

只會死記的模型一無是處

想像兩個學生在準備期末考。第一個真正理解了教材:給她一道從沒見過的題目,她也能算出來。第二個只是把去年的考卷一字不漏地背了下來。如果把去年的考卷再考一次,這個死背的人能拿一百分;但碰上今年真正算數的新考卷,他就崩潰了。一個統計模型可以是這兩種學生中的任何一種,而這整篇指南,就是要確保你的模型是第一種。

幾乎每個模型存在的意義,都是為了對新資料做預測。我們用手邊已有的資料來配適(fit)模型,但真正要評斷它的,卻是我們還沒見過的資料——下個月的顧客、明天的交易、下一位病人。在全新、沒見過的資料上仍表現良好的能力,稱為「泛化」(generalization,又譯一般化),這也是最終唯一重要的事。一個只配適得了訓練資料、卻在新資料上失靈的模型,學到的是錯誤的教訓。

陷阱就在這裡。假設你蒐集了十個資料點——比方說十天的廣告花費與隨後的銷售額——然後你畫一條蜿蜒的曲線,恰好穿過每一個點。在這十天上,你的預測誤差是零。完美!但那條曲線並沒有學到廣告與銷售之間的關係;它只是把十個點背了起來,連同其中所有的隨機雜訊。一旦問它第十一天會如何,它就會劇烈失控。它在自己學過的資料上所犯的誤差(也就是訓練誤差,training error),是一個討喜的謊言。

過度配適與配適不足

每個模型都活在兩種失敗模式之間。過度配適(overfitting)就是那個畫蜿蜒曲線的學生:模型太有彈性,會去追訓練資料裡的每一個小起伏,連雜訊一起追,於是泛化得很差。配適不足(underfitting)則相反:模型太僵硬,捕捉不到真正的規律,於是連在訓練資料上都表現不好。兩者在新資料上都會失敗,只是失敗的原因恰好相反。

想像有一份資料,真正的趨勢是一條和緩向上的曲線,再加上一點隨機散布。如果你硬配一條水平直線,就是配適不足:這條線無視那道曲線,幾乎在每個地方都是錯的。如果你用一條九次多項式穿過十個點,就是過度配適:曲線蜿蜒地穿過每一個點,卻在點與點之間生出巨大的擺盪,而這些擺盪只存在於這一份樣本裡。介於兩者之間的某處——一條平緩的曲線——才是那個既抓住真正形狀、又忽略雜訊的模型。它才能把第十一個點預測得好。

同一份資料的三種配適:太簡單(配適不足)、剛剛好,以及太複雜(過度配適)。只有中間那個模型抓住了訊號,又沒去追雜訊。

三張相同散點的散布圖。左:一條直線,錯過了明顯的曲線。中:一條平滑曲線,順著趨勢走。右:一條劇烈擺盪的曲線,恰好穿過每一個點。

如果不靠眼睛看圖,你怎麼判斷自己落在哪一種模式?比較兩個數字:訓練資料上的誤差,以及模型從沒見過、被保留下來的資料上的誤差。配適不足會表現為兩者都高——模型就是太弱。過度配適則表現為訓練誤差很低、但保留資料上的誤差高出許多——兩者之間有一道很大的落差。這道落差,是你能擁有的、最有用的過度配適警報。

舉例來說,一個彈性很大的模型,在訓練集上的誤差可能小到 2%,但在保留資料上卻是慘痛的 25%;而一個比較簡單的模型,兩邊都是 12%。彈性大的那個在帳面上比較好看,實際上卻差得多。這就是過度配適的本質——而它的鏡像,配適不足,同樣是真實的危險,尤其當模型太過簡單、或有用的輸入太少時。

偏差-變異權衡

過度配適與配適不足並不是兩個互不相干的毛病;它們是同一個旋鈕的兩端,而這個旋鈕的名字,叫做偏差-變異權衡(bias-variance tradeoff)。兩個新詞,但一旦看懂就很白話。偏差(bias)是模型因為假設太簡單、捕捉不到真相而產生的誤差——一個直線模型去配適一個彎曲的世界,不管你給它多少資料,它都是有偏差的。變異(variance)則是當你換一份訓練樣本時,模型的配適會跳動多少——一個超有彈性的模型,配到另外十個點上,會畫出一條完全不同的蜿蜒曲線,所以它的變異很高。

用飛鏢靶來看就很生動。高偏差、低變異,是一叢飛鏢緊緊地全射在錯的位置——很一致,卻一致地偏掉。低偏差、高變異,是飛鏢散落在紅心四周——平均來說是對的,但任何單獨一擲都很狂野。你當然兩者都想要低,但降低其中一個,通常會抬高另一個。把模型變得更有彈性,偏差降了、變異卻升了;把它變得更簡單,變異降了、偏差卻升了。這就是所謂的權衡,而甜蜜點,就是那個能讓兩者對新資料的合併影響最小的模型。

我們可以把它寫下來。一個模型在新資料上的期望誤差,恰好可以拆成三塊:偏差的平方、變異,以及一個不可化約的雜訊項——這是世界本身的隨機性,任何模型都無法去除。

\text{Expected test error} \;=\; \underbrace{\text{Bias}^2}_{\text{too simple}} \;+\; \underbrace{\text{Variance}}_{\text{too sensitive}} \;+\; \underbrace{\sigma^2}_{\text{irreducible noise}}

新資料上的誤差可拆解為偏差、變異與雜訊。前兩者可以彼此交換,第三者則設下一個任何模型都打不破的下限。

從這條小小的方程式,可以得出兩個教訓。第一,有一個下限:不可化約的雜訊意味著,面對真正帶有雜訊的資料,再聰明的模型也無法把誤差壓到零——任何承諾能做到的人,賣的其實是過度配適。第二,正因為偏差與變異彼此權衡,最好的模型通常不是手上最強大的那個,而是針對你實際擁有的資料、調到恰當彈性程度的那個。

隨著模型複雜度增加,偏差下降、變異上升。新資料上的總誤差呈 U 形:先下降,在甜蜜點觸底,接著隨著過度配適接手而再度爬升。

一張折線圖,x 軸是模型複雜度。一條下降的偏差曲線與一條上升的變異曲線交叉;兩者之和是一條 U 形的總誤差曲線,在中間有一個明顯的最低點。

本指南接下來的一切,都是用來尋找那個 U 形最低點的工具:切分資料以量測真正的誤差、用交叉驗證可靠地量測它,以及用正則化把複雜度有控制地調低。歸根究柢,它們全都是在偏差-變異權衡之間航行的方法。

訓練、驗證與測試的切分

如果訓練誤差與新資料誤差之間的落差是警報,那你就需要新資料來觸發它——而且你需要在上線之前就有,而不是上線之後。解法很簡單,也帶點狠勁:一開始就保留一部分資料,絕不讓模型從中學習。如此一來,這些被保留的資料,就能充當未來的替身。

標準做法是把資料切成三份。訓練集(training set)是模型用來學習的部分。驗證集(validation set)是你用來比較不同模型、調整它們設定的部分——試幾個選項,看哪一個在它沒訓練過的資料上分數最好,然後挑出贏家。測試集(test set)則鎖在保險庫裡,到最最後才打開唯一一次,用來誠實地估計選定的模型在真實世界裡會表現如何。常見的切分大致是 70% 訓練、15% 驗證、15% 測試,不過確切的比例取決於你有多少資料。

一份資料、三種角色:一大塊訓練片段供學習、一塊驗證片段用來挑選與調整模型,以及一塊封存到最後才動的測試片段。

一條水平的資料長條,分成三段不同顏色,標示為訓練(最大)、驗證(較小)與測試(較小,標註為鎖定)。

為什麼要三份、而不是兩份?因為一旦你用某一組資料來挑選模型,你就已經開始對它配適了——只是透過你的選擇,悄悄地進行。在對驗證集調了十幾輪之後,你的模型對它也有了一點過度配適。那個完全沒被碰過的測試集,是唯一能給你誠實最終數字的東西。如果你偷看測試集、一路調到它好看,然後回報那個數字,你就是在欺騙自己;這種事太常見了,常見到有個名字,叫做資料洩漏(data leakage)。

這套紀律——嚴格遵守的訓練/驗證/測試切分——是值得信任的建模的脊梁。其餘的一切都只是精修。下一節要處理最常見的一種精修:當你的資料少到無法騰出一整個驗證集時,該怎麼辦。

把交叉驗證做對

當資料稀少時,單一個驗證集有兩個弱點。你花掉了寶貴的資料列,只為了評估、而不是學習;而且你得到的分數,取決於哪些列剛好落進驗證集的運氣——換一個隨機切分,答案可能明顯不同。交叉驗證(cross-validation)藉由讓每一列輪流身兼兩職,把這兩個問題都解掉。

最常見的做法是 k 折交叉驗證(k-fold cross-validation)。把資料打亂,切成 k 等份,每一份稱為一折(fold)——五折是很受歡迎的選擇。接著,用其中四折訓練,在第五折上評估。然後再做一次,這次保留另外一折,如此反覆,直到五折中的每一折都恰好輪過一次當評估集。最後你會得到五個誤差分數,把它們平均。因為每一列都被當作評估用過一次、當作訓練用過四次,你什麼都沒浪費,而且你的估計,比任何單一切分都穩定得多。

  1. 先把鎖定的測試集分出來;k 折只在其餘資料上做。
  2. 把剩下的資料打亂,切成 k 等份(例如 k = 5)。
  3. 讓每一折輪流:用其餘 k−1 折訓練,在這一折上評估。
  4. 把 k 個分數平均——這就是你對新資料誤差的交叉驗證估計。
  5. 用這個估計來挑選模型或設定,然後在所有非測試資料上重新配適,最後打開測試集唯一一次。
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import Ridge

# 5-fold CV: 5 scores, one per held-out fold
scores = cross_val_score(Ridge(alpha=1.0), X_train, y_train, cv=5,
                         scoring="neg_root_mean_squared_error")
print(scores.mean(), scores.std())
在 scikit-learn 中做五折交叉驗證。平均分數估計新資料誤差;標準差則告訴你這個估計有多不穩。

兩點告誡。第一,每一個會從資料中學習的前處理步驟——縮放、插補遺漏值、挑選特徵——都必須在每一折內部進行,只用那一折的訓練部分。如果你一次就把縮放器(scaler)配適在全部資料上,你就把測試資訊直接洩漏了回去,那個漂亮的交叉驗證分數也成了虛構。(管線 pipeline 的存在,正是為了把這件事自動化。)第二,k 越大,每一折的訓練資料越多、估計的偏差越小,但計算量也越大;五折或十折通常是甜蜜點。

只要誠實地使用,交叉驗證會是你一再倚賴的主力工具——用來比較候選模型、決定要保留多少特徵,以及在下一節裡,用來設定正則化中最重要的那一個旋鈕。

正則化:對複雜度課罰(L1/L2)

切分與交叉驗證量測過度配適;正則化(regularization,又譯正規化)則真正地預防它。這個想法簡單到令人卸下心防:與其讓模型為所欲為地把訓練誤差壓到最低,你改為對「變複雜」課一筆罰金,要它去最小化「訓練誤差加上這筆罰金」。如今模型必須用誤差的真正下降,來為它每一分複雜度辯護。當它得在一個狂野的配適、與一個幾乎一樣好卻平靜得多的配適之間選擇時,它會偏好平靜的那個。

在迴歸裡,複雜度長什麼樣子?又大又像翹翹板般擺盪的係數。一個過度配適的線性模型,常常有一個係數是 +800、下一個是 −790,彼此拉鋸,只為了穿過每一個點。所以我們對係數的大小課罰:把所有係數的平方加總起來,乘上一個我們稱為 lambda 的調節旋鈕,再把這個總和加進模型要最小化的目標裡。

\min_{\beta}\; \underbrace{\sum_{i=1}^{n}\big(y_i-\hat{y}_i\big)^2}_{\text{fit the data}} \;+\; \lambda\,\underbrace{\sum_{j=1}^{p}\beta_j^{2}}_{\text{L2 penalty (ridge)}}

脊迴歸(Ridge,L2):最小化一般的平方誤差,加上 lambda 乘以係數平方和。把平方換成絕對值 Σ|βj|,得到的就是套索迴歸(Lasso,L1)。

這兩種經典的罰項,行為方式有個值得知道的差異。L2 罰項(係數平方和)造就脊迴歸(ridge),它會把所有係數平滑地朝零收縮,卻很少讓任何一個恰好為零——它保留每一個特徵,只是把音量轉小。L1 罰項(絕對值之和)造就套索迴歸(lasso),它樂於把較弱的係數一路推到恰好為零,這等於把那些特徵刪掉。所以套索同時兼任自動的特徵選擇:它交給你一個用更少輸入的、更簡單的模型。

lambda 是設定你施力多重的那個旋鈕。當 lambda 等於零時沒有罰項,你就回到一般的最小平方法,可以盡情過度配適。把 lambda 開到非常高,每個係數都會被壓向零,直到模型配適不足、只會預測平均值為止。對的值在兩者之間——而你不是用猜的,是試一整排值、挑出交叉驗證誤差最好的那個。這正是把偏差-變異權衡,變成一個你真的轉得動的旋鈕:一點點正則化,用少許額外的偏差,換來變異的大幅下降,於是新資料上的誤差跟著下降。

from sklearn.linear_model import RidgeCV
import numpy as np

# try many lambdas (alpha); CV picks the best automatically
alphas = np.logspace(-3, 3, 25)
model = RidgeCV(alphas=alphas, cv=5).fit(X_train, y_train)
print("chosen lambda:", model.alpha_)
RidgeCV 掃過一整排 lambda 值,用交叉驗證挑出泛化最好的那一個——不必手動瞎猜。

正則化不是什麼冷門小技巧;同樣的罰項想法,馴服了從邏輯迴歸到龐大神經網路的一切,在那裡它以權重衰減(weight decay)、隨機失活(dropout)等名字出現。一個實務提醒:因為罰項作用在係數的大小上,你應該先把特徵標準化(放到共同的尺度上),否則罰項會不公平地懲罰那些只是剛好用很小單位來量測的特徵。把正則化學好,你就握住了整個建模領域中,用途最廣的工具之一。

誠實模型的檢查清單

退一步看,整篇指南可以收束成一句話:一個模型有多好,全看它在從沒見過的資料上的表現。其餘的一切——切分、折、罰項——存在的目的,都是為了誠實地估計那一個數字,並把它壓低。下面這份檢查清單,你可以拿來檢驗任何模型,無論是你自己的、還是別人的,在你信任它之前先過一遍。

  1. 你是否在動手之前就保留了測試集,並且只在最最後打開它一次?
  2. 訓練誤差與保留資料誤差之間,是否有很大的落差?若有,就懷疑過度配適。
  3. 每一個前處理步驟(縮放、插補、特徵選擇),是否都在交叉驗證內部進行,而非在它之前?
  4. 你是否用交叉驗證來調 lambda 之類的旋鈕,而不是靠偷看測試集?
  5. 一個更簡單的模型、或更多資料,是否能做得幾乎一樣好?優先選擇謙遜的那個選項。
  6. 這個改善是否大到在實務上真有意義,而不只是在「統計上」偵測得到?

從這裡出發,有兩個方向會有回報。磨利你的輸入:用心的特徵工程——從原始資料打造出更好的變數——往往勝過任何更花俏的演算法。以及拓寬你的工具箱:你這輩子會遇到的每一個模型,從謙卑的一條直線到深層網路,都靠著你現在已經理解的同一套偏差-變異邏輯而生、而死。你已經學會了那套紀律——它區分了「展示時很漂亮的模型」與「真正管用的模型」。