何必從零開始?遷移學習的理由
在這個系列中,你學會了模型如何學習:訓練迴圈、像 Adam 這樣的優化器、讓梯度保持健康的正規化,以及對抗過度擬合的各種正則化手段。有了這些,你「確實」可以拿一個隨機初始化的全新網路,直接在自己的影像上訓練。但幾乎總有更聰明的做法。很可能已經有人用數百萬張影像訓練過一個巨大的模型,而那個模型已經學會用一種通用的方式「看見」世界。我們可以借用它的眼睛。
打個比方。從隨機權重訓練,就像從出生開始養育一個嬰兒,期望他長大成為畫家:他得先學會什麼是邊緣與顏色,再學紋理、形狀,最後才是物件——這要花上好幾年。重用預訓練模型,則像是聘請一位經驗豐富的畫家。他早已懂得邊緣、明暗、透視,也知道一張臉或一個輪子長什麼樣;你只需要教他你這個特定的主題。在 ImageNet(120 萬張有標註的照片)這類龐大資料集上訓練、或在網路規模影像上以自監督方式訓練出來的模型,正是在它們的前段與中段網路層裡學到了這些通用視覺特徵——而這些特徵能遷移到遠超原始任務的地方。
卷積網路示意圖,從輸入影像經過層層堆疊的特徵萃取層,到達分類頭;前段層標示為通用,分類頭標示為任務特定。
重用這些學到的特徵,就是我們所說的 遷移學習,它帶來三重好處:成本大幅降低(在單張 GPU 上花幾小時,而不是用上百張 GPU 跑好幾週)、需要的自有標註資料少得多,而且通常能達到比從零訓練「更高」的準確率——因為預訓練特徵比你那個小資料集能從頭教出來的任何東西都更好。接下來有兩個具體策略,本指南其餘部分都圍繞它們展開:特徵萃取(凍結借來的模型,只訓練一個新的分類頭)與 微調(也溫和地繼續訓練借來的權重)。
特徵萃取:凍結骨幹、只訓練新分類頭
最簡單的策略,是把預訓練網路當成一台固定的「特徵機器」。我們保留它的主體——也就是把影像轉成豐富特徵向量的骨幹——然後把它凍結起來。凍結有個精確的機械意義:對骨幹裡的每一個權重,我們設定 `requires_grad = False`,於是在反向傳播時,優化器不會為它計算梯度,也永遠不會更新它。骨幹就變成了一個常數函數:丟一張影像進去,每一個 epoch 都得到同樣高品質的特徵輸出。真正在學習的,只有接在尾端的新零件。
那個新零件,是一個全新的、小巧的分類頭:我們刪掉原本的分類器(也就是和來源任務類別綁死的那一段),換上自己的。因為只有分類頭在訓練,這種做法的可訓練參數非常少,所以又快又省,而且——關鍵在於——很難過度擬合。回想指南四:相對於資料量,模型的自由參數越多,過度擬合就越嚴重;凍結骨幹幾乎把這個數目砍到趨近於零,這正是為什麼當你的資料集「小」、而且和來源領域「相似」時,特徵萃取會大放異彩。通用特徵依然適用,而能用來死背雜訊的容量又所剩無幾。
影像分類流程,顯示一張輸入照片、一個凍結的特徵萃取器、一個特徵向量,以及一個輸出類別機率的小型可訓練分類器。
舉個具體例子:你有 5 個類別(比如五種鳥),每類只有幾百張照片——遠遠不足以從零訓練一個深度網路。那就拿一個在 ImageNet 1000 類別上預訓練好的 ResNet-50,把它整個凍結,丟掉它那個 1000 路的分類器,再接上一個全新的線性層,把 2048 維的特徵向量對應到你的 5 個類別。現在你「只」訓練那一層。下面的程式碼展示了每一個步驟。
import torch, torch.nn as nn
from torchvision import models
# 1. Load a backbone pretrained on ImageNet (1000 classes)
model = models.resnet50(weights="IMAGENET1K_V2")
# 2. FREEZE every backbone parameter: no gradient, no update
for param in model.parameters():
param.requires_grad = False # frozen -> autograd skips it
# 3. Replace the 1000-class head with a fresh 5-class head.
# A newly created layer has requires_grad=True by default.
num_features = model.fc.in_features # 2048 for ResNet-50
model.fc = nn.Linear(num_features, 5) # our 5 classes
# 4. Only the new head's parameters are trainable
trainable = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.Adam(trainable, lr=1e-3)
# ...a standard training loop now updates ONLY model.fc微調:溫和地重塑預訓練權重
特徵萃取是原封不動地借用預訓練特徵。微調則更進一步:我們解凍部分或全部骨幹,繼續在自己的資料上訓練它,好讓借來的特徵能「重新塑形」以貼合我們的任務。問題在於:那些權重本來就已經很好了。如果我們用對待隨機權重時那種大刀闊斧的步伐去更新它們,來自小資料集的最初幾個帶雜訊的梯度,就會把它們從費心學來的數值狠狠推開——我們會把自己原本想借的那份知識給覆蓋掉。這種失敗有個名字:災難性遺忘。
解法是用一個小得多的 學習率。從零訓練可能用大約 1e-1(SGD)或 1e-3(Adam)的基礎學習率,而微調通常用「小上 10 到 100 倍」的數值——剛好足以「輕推」權重,而不是改寫它們。可以把它想成在調校一把已經做得很好的樂器:旋鈕只能轉一點點,絕不會整個鬆開重來。小學習率讓每一步都停留在那個良好預訓練解的鄰近區域,於是模型一邊適應你的任務,一邊保住它原本就會的東西。
實務上會同時操作好幾根槓桿。(1) 如上所述,採用更小的基礎學習率。(2) 逐層(差異化)學習率:前段網路層偵測幾乎永遠不需要改變的通用邊緣與紋理,所以給它們極小的學習率;後段那些任務特定的層,則給較大的學習率。(3) 漸進式解凍:先訓練分類頭,再解凍最上面的區塊,接著下一塊,一路往下——讓網路先穩定下來,那些脆弱的前段層才開始移動。(4) 短排程:微調只需要幾個 epoch,通常先暖身再衰減(見本系列關於 學習率排程的指南)。這幾根槓桿裡最精準的,就是逐層學習率,我們可以把它寫成一條公式。
逐層(差異化)學習率:越靠近輸入端,每一層的學習率衰減得越多。
我們逐個符號來讀。l 是層的索引,編號方式是 l=1 為最前面的層(最靠近輸入像素),l=L 為最後一層(最靠近輸出)。L 是層的總數。\eta_{\text{base}}(希臘字母 eta)是我們希望「最後一層」使用的學習率——也就是最大、適應最快的那個。\xi(xi)是一個小於 1 的衰減因子,比如 0.8。指數是 L-l:對最後一層 l=L,指數為 0,所以 \xi^0=1,這一層拿到完整的 \eta_{\text{base}};對較前面的層,指數變大,因子被升到更高次方,學習率就縮小。具體代入 \eta_{\text{base}}=10^{-3}、\xi=0.8、L=5:最後一層拿 10^{-3},接著 8\times10^{-4}、6.4\times10^{-4}、5.1\times10^{-4},而最前面那一層拿到 10^{-3}\cdot0.8^{4}=4.1\times10^{-4}——還不到分類頭的一半。直覺是:保護預訓練學到的通用低階特徵(幾乎不動它們),同時讓高階、任務特定的層自由地去適應。
# Discriminative / layer-wise learning rates.
# Order groups from early (generic) to late (task-specific),
# then give group l the rate eta_base * xi^(L - l).
eta_base, xi = 1e-3, 0.8
layer_groups = [model.layer1, model.layer2,
model.layer3, model.layer4, model.fc]
L = len(layer_groups)
param_groups = []
for l, group in enumerate(layer_groups, start=1): # l = 1..L
lr_l = eta_base * (xi ** (L - l)) # earlier -> smaller
param_groups.append({"params": group.parameters(), "lr": lr_l})
optimizer = torch.optim.AdamW(param_groups, weight_decay=1e-4)要微調到什麼程度?依資料挑策略
你該多激進地去微調,不是憑喜好決定的——它取決於你的資料。有兩個軸要權衡。第一個是資料集大小:例子很少時,你只能安全地訓練少量參數;例子很多時,你才負擔得起去移動整個網路。第二個是與來源領域的相似度:如果你的影像長得像預訓練模型看過的影像(日常照片,像 ImageNet),它的特徵幾乎開箱即用;如果完全不像(X 光片、衛星影像方塊、顯微鏡切片),那麼較深的特徵可能需要真正地重新塑形。把這兩個軸交叉,就得到四個象限,而 遷移學習對每個象限都有一套標準建議。
- 小 + 相似:凍結骨幹,只訓練新的分類頭(純特徵萃取)。你的資料太少,無法安全地移動數百萬個權重,但特徵本來就合用——所以別去冒險動它們。
- 大 + 相似:用小學習率微調更多層、甚至全部層。你的資料足以安全地調整整個網路,而剩下那一點點領域落差,最好用溫和的全面微調來彌平。
- 小 + 不同:凍結前段的通用層,只小心地微調後段的層,並且大量做資料增強。低階的邊緣/紋理仍可遷移,但高階特徵必須改變——可是資料少,意味著你得謹慎地移動它們,並大量倚賴資料增強。
- 大 + 不同:微調整個網路(這種情況下你甚至可以考慮從零訓練)。這是風險最高、回報也最高的情形:資料充足讓你能重塑一切,而巨大的領域落差意味著有很多可賺——但也最容易出岔子。
注意這條貫穿的主線:資料「越少」、領域「越不同」,你就該「凍結越多」、正則化下手越重。領域偏移是真實存在的,而且很容易被低估——醫學掃描和衛星影像,真的和 ImageNet 那些貓狗照片長得毫無相似之處,所以別假設 ImageNet 骨幹的深層特徵對它們也合用;前段的邊緣/紋理偵測器通常還是有幫助,但後段那些「這是一隻黃金獵犬」的特徵就往往派不上用場了。拿不準時,就多凍結一些、漸進地解凍,並緊盯你的驗證曲線。
避開陷阱:災難性遺忘、學習率與 BatchNorm 統計量
微調看起來簡單得叫人鬆懈——改一個旗標、把學習率調低、按下開始——但有幾個具體的陷阱,幾乎人人都會踩到。每一個都直接連回本系列前面的指南,而且都最適合用一條鏈來記:你觀察到的症狀 → 底下的成因 → 有效的修法。
(a) 災難性遺忘。 症狀:微調的第一個 epoch 裡,訓練與驗證準確率「崩跌」,遠低於凍結分類頭時就已經達到的水準。成因:學習率太高,最初帶雜訊的梯度在好不容易穩定下來之前,就把優良的預訓練權重炸開了——正是第三節提到的覆蓋問題。修法:採用低的基礎學習率(比從零訓練小 10 到 100 倍),再加上一段短短的暖身:學習率從接近零開始,在最初幾百步內逐步爬升(指南二)。暖身意味著最初、也最不可靠的那些更新都極小,於是模型永遠不會猛地離開它的預訓練起點。
(b) 過時的 BatchNorm 統計量。 回想指南三:批次正規化在訓練模式與推論模式下的行為不同:在「訓練」時,它用每個批次自己的平均值與變異數來正規化,同時更新一組「移動」平均值/變異數;在「推論」時,它凍結下來,改用那組儲存的移動統計量。陷阱在於:預訓練模型的移動統計量,是在「來源」領域上蒐集的(ImageNet 的亮度、色彩、對比)。症狀:在訓練模式下損失看起來沒問題,但在你的新領域上準確率卻奇差。成因:那些被凍結的來源領域統計量,和你資料的分佈對不上。修法:微調時讓 BatchNorm 維持在訓練模式,好讓它在你的目標資料上重新計算統計量;或者乾脆把 BN 層整個凍結(當你的批次很小、很吵時,這往往最好)。
(c) 正則化不會消失。 症狀:驗證損失開始上升,而訓練損失卻持續下降——正是指南四裡那個經典的過度擬合分叉——而且在微調時它出現得「更快」,因為模型本來就強大,而你的目標集又小。成因:一個高容量的預訓練網路,能很快把一個小目標集死背下來。修法:保持 權重衰減開啟,把權重拉向小而簡單的數值;並用 提前停止在驗證損失的最低點喊停,還原表現最好的那個檢查點。在小目標集上,正則化要比從零訓練時「下得更重」——模型的強大,正是它在這裡容易過度擬合的原因。
完整食譜:大師級的訓練手冊
現在,本系列的每一樣材料你都齊備了。讓我們把它們組裝成一份連貫、現代的食譜——也就是一位高手實際從頭到尾訓練視覺模型時,所遵循的順序。把它當成一張有序的檢查清單來讀,並留意每一步都是你早已從前面某篇指南理解過的東西;這份手冊不過是把它們扣合在一起的順序罷了。
- 挑一個預訓練骨幹(指南五)。別從隨機權重開始——借一個早已懂得「看」的模型。這是你最大的一筆先行優勢。
- 建立一條帶有強力 資料增強的資料管線(指南四)。隨機裁切、翻轉、色彩抖動與混合,能製造出多樣性,讓模型學會泛化、而不是死背——這至關重要,因為你的目標集通常很小。
- 選一個優化器:要快速、穩健地收斂就用 AdamW,要榨出最後一滴準確率就用 SGD+動量(指南二)。微調時,AdamW 是穩妥的預設選項。
- 設定「暖身 + 餘弦」的 學習率排程(指南二)。暖身在最初那些帶雜訊的步驟裡保護預訓練權重;餘弦衰減則把學習率平滑地退火,收尾收得謹慎。
- 倚靠架構本身的正規化與殘差連接(指南三)來讓梯度保持健康——但跨領域時,要留意第五節提到的 BatchNorm 統計量陷阱。
- 加上權重衰減,必要時再加 dropout(指南四)。小目標集正則化下得重一點,大目標集則輕一點。
- 緊盯驗證曲線,並使用 提前停止(指南一與四)。告訴你何時該停、該保留哪個檢查點的,是驗證損失,而不是訓練損失。
- 用低學習率或逐層學習率做微調(指南五)。可選擇先以凍結分類頭暖身,再漸進解凍,給前段層比後段層更小的學習率。
# End-to-end transfer-learning recipe (sketch)
model = load_pretrained_backbone() # guide 5
model.head = new_head(num_classes) # guide 5
train_loader = DataLoader(dataset, transforms=[ # guide 4: augmentation
random_crop, horizontal_flip, color_jitter, mixup])
optimizer = AdamW(model.parameters(), # guides 2 & 4
lr=3e-4, weight_decay=1e-4)
scheduler = warmup_then_cosine(optimizer, # guide 2
warmup_epochs=2, total_epochs=30)
best, patience, bad = float("inf"), 5, 0
for epoch in range(30):
train_one_epoch(model, train_loader, # guides 1-3
optimizer, scheduler)
val = evaluate(model, val_loader) # guide 1
if val < best: # checkpoint the best
best, bad = val, 0
save(model)
else:
bad += 1
if bad >= patience: # guide 4: early stopping
break機器學習訓練迴圈的循環示意圖:資料餵入模型、計算損失、梯度回流、更新權重,如此反覆。
這就是完整的旅程。你能讀懂學習曲線、能推理訓練迴圈;能挑選並調校優化器與排程;能用正規化讓梯度保持穩定;能讓模型學會泛化;而現在,你還能站在巨大預訓練模型的肩膀上,把它們塑造成屬於你自己的東西。把這五篇指南合在一起,正好就是從頭到尾訓練出一個強大視覺模型所需要的一切——去動手做一個吧。