訓(xùn)練模型遷移學習:特征提取與部分微調(diào)怎么選)
1. 先把兩個概念掰開誰在訓(xùn)練誰被凍住做遷移學習的時候模型部分微調(diào)和特征提取這兩個詞幾乎天天出現(xiàn)在討論里但很多人第一次接觸時會把它們當成一回事。我在帶新人做圖像分類任務(wù)時最常聽到的一句話就是“我把預(yù)訓(xùn)練模型加載進來了然后呢”。這個問題看著簡單背后其實是一個關(guān)于“信任程度”的決策你到底有多信任預(yù)訓(xùn)練權(quán)重學到的表示愿意讓它在新數(shù)據(jù)上改動多少。特征提取的做法是把主干網(wǎng)絡(luò)當成一個固定的、不可修改的特征變換器參數(shù)全部凍死只訓(xùn)練后面新加的那一小截分類頭部分微調(diào)則是把主干的一部分層解開讓它們跟著新任務(wù)一起更新通常是解凍靠后的幾個stage或者最后一個block前面保留凍結(jié)。這個區(qū)別在定義上只有一句話但落到實驗結(jié)果、訓(xùn)練時長、顯存占用、可復(fù)現(xiàn)性上差異相當明顯。我先把結(jié)論擺在這里特征提取本質(zhì)上是在做“換頭”部分微調(diào)本質(zhì)上是在做“換頭加局部改造”。前者假設(shè)預(yù)訓(xùn)練模型已經(jīng)學到了足夠通用的表示你只需要一個線性分類器把它映射到新標簽空間后者假設(shè)預(yù)訓(xùn)練表示大體可用但和新任務(wù)之間存在語義偏移需要讓高層特征重新適應(yīng)。這個假設(shè)成立與否跟你的數(shù)據(jù)量、數(shù)據(jù)和新任務(wù)的距離、以及算力預(yù)算直接相關(guān)。寫這篇文章我想把這兩條路從原理到代碼到踩坑都拆一遍盡量讓剛?cè)腴T的朋友看完能直接上手也讓已經(jīng)在做項目的人對選型有個清晰的判斷依據(jù)。不管你現(xiàn)在是在做工業(yè)質(zhì)檢的小樣本分類、還是做文本多分類、還是給一個小數(shù)據(jù)集做快速基線這兩個方案都是你繞不開的起點。1.1 一句話區(qū)分代碼層面到底差在哪幾行如果只看代碼兩者的差異小到讓人意外核心就是requires_grad和優(yōu)化器收哪些參數(shù)。特征提取的典型寫法是這樣加載預(yù)訓(xùn)練權(quán)重把主干所有參數(shù)的requires_grad設(shè)成False然后替換掉最后的全連接層新建的層默認requires_gradTrue優(yōu)化器只接收新層的參數(shù)。部分微調(diào)則是在這個基礎(chǔ)上把指定的若干層重新設(shè)回True再把這些解凍的參數(shù)以更小的學習率交給優(yōu)化器。注意這里有個很多新手會掉的坑優(yōu)化器是在凍結(jié)之前構(gòu)造的那么即使你后續(xù)把requires_grad改成False那些參數(shù)雖然不會更新因為梯度是None但仍然會掛在優(yōu)化器里占著狀態(tài)內(nèi)存還可能被 weight decay 之類的邏輯誤傷。所以正確順序永遠是先凍結(jié)、再建優(yōu)化器。再往細里說特征提取還有一個更徹底的加速版本既然主干完全不更新那它對同一張輸入永遠輸出同一個向量那就干脆不要在每個 epoch 重復(fù)跑前向直接一次性把所有樣本的特征算出來存起來后面只訓(xùn)練一個線性層或者甚至用邏輯回歸。這個做法在數(shù)據(jù)量不大幾萬張以內(nèi)、增強策略比較溫和的場景下非常好用原本要跑幾個小時的訓(xùn)練幾分鐘就出結(jié)果而且能讓你快速判斷“這套預(yù)訓(xùn)練特征到底有沒有信息量”。當然它也有代價后面會詳細說。部分微調(diào)的代碼差異主要是參數(shù)分組。你需要把參數(shù)按層級拆成幾組分別給不同的學習率通常主干靠后的層給1e-5到1e-4新加的頭給1e-3到1e-2形成一種從“幾乎不動”到“放開訓(xùn)練”的梯度。這個做法在業(yè)內(nèi)叫判別式微調(diào)思路來源于一個很樸素的觀察靠前的層學的是邊緣、顏色、紋理這種通用東西基本不需要改靠后的層學的是語義組合最需要適配新任務(wù)。所以給它們不同的步長比一視同仁地設(shè)一個學習率更穩(wěn)。1.2 從反向傳播看本質(zhì)梯度流經(jīng)的范圍決定了成本要理解兩者為什么性能和成本都不同得從反向傳播的路徑上看。特征提取時因為主干參數(shù)requires_gradFalse并且輸入也不需要梯度PyTorch 的自動微分機制在主干部分根本不會記錄中間激活值前向算完就釋放。這意味著顯存占用里最大的一塊激活值幾乎不占只有新加的頭部會保存中間結(jié)果。部分微調(diào)則不同只要某一層參數(shù)需要梯度它前面的所有計算圖節(jié)點都得保留直到反向傳播走完。哪怕你只解凍最后一個 block前面所有層的激活仍然要存著因為梯度要一路傳回去鏈式求導(dǎo)。這是很多人沒意識到的點解凍最后一層省下的顯存遠比你想象的少省下的是計算量而不是顯存。我做過一個粗略的對比以常用的 50 層殘差網(wǎng)絡(luò)為例輸入 224×224全量微調(diào)在單卡上大概能開 batch size 64 左右就頂?shù)斤@存上限換成只訓(xùn)練分類頭同樣顯存能開到 256 甚至更高。但如果你只是解凍最后一個 stagebatch size 大概只能提到 80 到 96提升有限。這個實測結(jié)果和很多人的直覺相反他們以為“我只解凍一點點顯存應(yīng)該接近特征提取”實際不是。所以如果你的瓶頸是顯存特征提取才是真正的解法如果你只是想省訓(xùn)練時間部分微調(diào)的加速效果也就那樣。另一個從反向傳播角度看的重要差異是梯度尺度。新加的分類頭是隨機初始化的訓(xùn)練初期梯度很大如果主干也一起放開這些大梯度會順著鏈式法則一路污染主干權(quán)重把預(yù)訓(xùn)練學到的表示沖垮。這就是為什么微調(diào)必須用很小的學習率或者先凍結(jié)主干訓(xùn)幾輪頭再解凍業(yè)內(nèi)管這個叫預(yù)熱。特征提取天然沒有這個問題因為主干壓根不接收梯度。1.3 為什么會有這兩種做法遷移學習的譜系位置把視野拉遠一點看特征提取和部分微調(diào)其實是“遷移學習”這條譜系上的兩個點而不是兩個孤立的技術(shù)。譜系的一端是完全凍結(jié)、只訓(xùn)頭另一端是全量微調(diào)中間還有逐層解凍、分層學習率、加輕量適配層等一串變體。選擇哪個點本質(zhì)上是在回答三個問題新數(shù)據(jù)和預(yù)訓(xùn)練數(shù)據(jù)的分布差多少、新數(shù)據(jù)有多少、你能接受多大的訓(xùn)練成本。分布差得遠、數(shù)據(jù)又少就往凍結(jié)那頭靠因為放開訓(xùn)練容易過擬合分布差得遠、數(shù)據(jù)挺多反而應(yīng)該多解凍因為舊的高層語義確實不管用了分布接近、數(shù)據(jù)少凍結(jié)就夠分布接近、數(shù)據(jù)多全量微調(diào)最省心。這個譜系思維很關(guān)鍵因為它讓你不必在“提特征”和“部分微調(diào)”之間做二選一而是可以設(shè)計一條漸進路線先用特征提取跑一個基線看看模型到底能到什么水平如果欠擬合明顯再逐步解凍。這個流程我在實際項目里用了很多次比一上來就調(diào)微調(diào)超參要高效得多因為基線跑得快你能在半天內(nèi)摸清數(shù)據(jù)的難度上限。2. 凍結(jié)參數(shù)到底改變了什么原理與幾個隱藏坑2.1 requires_grad 與優(yōu)化器參數(shù)組一個必須理清的關(guān)系很多人以為設(shè)了requires_gradFalse就等于“這層不參與訓(xùn)練”這個說法大方向沒錯但細節(jié)上有幾層含義需要分清。第一層是計算圖層面參數(shù)不需要梯度前向過程中不會為該參數(shù)的操作保存反向所需的中間量所以顯存和計算都省。第二層是優(yōu)化器層面如果這個參數(shù)被傳進了優(yōu)化器即使梯度是None優(yōu)化器仍然會在step()時跳過它PyTorch 里p.grad is None時直接 continue但優(yōu)化器內(nèi)部為它分配的動量緩沖、二階矩緩沖依然占顯存對大的主干網(wǎng)絡(luò)來說這筆開銷不小。第三層是權(quán)重衰減層面有些實現(xiàn)會在外部手動做參數(shù)衰減如果不加過濾凍結(jié)的層也會被緩慢改動雖然幅度小但會讓“凍結(jié)”名不副實。所以標準做法是寫一個過濾函數(shù)在構(gòu)造優(yōu)化器之前明確收集需要訓(xùn)練的參數(shù)def trainable_params(model): return [p for p in model.parameters() if p.requires_grad] optimizer torch.optim.AdamW(trainable_params(model), lr1e-3, weight_decay1e-4)如果想驗證凍結(jié)是否生效可以在第一個 epoch 結(jié)束后打印一下凍結(jié)層權(quán)重的范數(shù)和初始值對比差異應(yīng)該是零。這個小檢查我在調(diào)試時經(jīng)常做比看 loss 曲線更直接。還有一種情況是加載預(yù)訓(xùn)練權(quán)重之后requires_grad被重置了比如某些封裝庫在load_state_dict之后會重建模塊這時候要重新設(shè)置一遍。2.2 train 與 evalBN 統(tǒng)計量和 Dropout 的隱形漂移這是我認為最容易翻車、但文檔里往往一筆帶過的地方。批歸一化層有兩類東西一類是可學習的縮放和平移參數(shù)weight、bias屬于參數(shù)另一類是滑動平均的均值和方差running_mean、running_var屬于緩沖區(qū)不是參數(shù)。你把參數(shù)requires_grad設(shè)成False緩沖區(qū)依然會在model.train()模式下根據(jù)當前 batch 更新。也就是說你以為自己“凍結(jié)了整個主干”實際上主干里的歸一化統(tǒng)計量正在被新數(shù)據(jù)慢慢改寫。這個行為在兩種情況下會帶來麻煩。第一種是新數(shù)據(jù)的分布和預(yù)訓(xùn)練數(shù)據(jù)差得比較遠比如預(yù)訓(xùn)練用的是自然圖像你的是灰度工業(yè)圖統(tǒng)計量被新數(shù)據(jù)改寫后原本適配的歸一化就偏了特征質(zhì)量下降。第二種是 batch size 很小比如只有 8 或 16用這么小的 batch 估計出來的均值和方差噪聲很大會污染滑動平均導(dǎo)致驗證時表現(xiàn)抖動。這兩種情況我都在項目里遇到過現(xiàn)象都是“訓(xùn)練 loss 在降但驗證集不漲甚至掉”排查半天最后發(fā)現(xiàn)是歸一化統(tǒng)計量在作祟。解決辦法是訓(xùn)練時把歸一化層強制留在評估模式def freeze_bn(module): if isinstance(module, torch.nn.modules.batchnorm._BatchNorm): module.eval() model.apply(freeze_bn)但要注意model.train()會遞歸地把所有子模塊設(shè)回訓(xùn)練模式所以如果你在每個 epoch 開頭調(diào)用model.train()這個凍結(jié)就失效了。常見的處理是每個 epoch 訓(xùn)練前先model.train()然后立刻再調(diào)用一次上面的凍結(jié)函數(shù)或者干脆重寫模型的train方法。Dropout 也是類似的邏輯不過 Dropout 在推理時本來就關(guān)閉訓(xùn)練時開啟是正常行為一般不需要特殊處理除非你想讓凍結(jié)主干的輸出完全確定。2.3 特征分布偏移為什么越靠后的層越“認死理”深度網(wǎng)絡(luò)有一個被反復(fù)驗證的現(xiàn)象淺層學的東西通用性強深層學的東西任務(wù)相關(guān)性強。淺層提取的是邊緣、角點、顏色對比這類東西無論你識別貓還是識別電路板缺陷這些基元都在用深層組合出來的可能是“貓耳朵”“車輪”這種具體概念換到新領(lǐng)域就完全對不上。這就是為什么特征提取在小樣本、域差異大的場景下常常表現(xiàn)平庸——你用的恰恰是最不適配的那一層特征。我用過一個比較直觀的診斷方法把主干最后幾層的輸出特征做一次降維可視化看新數(shù)據(jù)的兩類樣本在這上面是否可分。如果特征提取的線性探針準確率明顯低于隨機猜測加一點說明高層特征幾乎完全失效此時只訓(xùn)頭就是徒勞必須解凍。反過來如果線性探針能到八成以上說明特征本身攜帶信息問題出在分類頭的容量或者正則上這時候繼續(xù)加深微調(diào)收益不大。這個判斷比盲目調(diào)參有用得多而且成本極低。3. 兩套可復(fù)現(xiàn)的實現(xiàn)骨架3.1 特征提取把主干當成固定變換器最基礎(chǔ)的實現(xiàn)方式是每個 epoch 跑一遍完整前向。這種寫法簡單但浪費算力因為主干輸出對同一張圖是恒定的增強隨機性除外。更實用的寫法是先做一次全量特征緩存然后用緩存訓(xùn)練分類頭。這樣做有個前提你的數(shù)據(jù)增強不能是強隨機的否則同一張圖在不同 epoch 應(yīng)該是不同視圖緩存就把增強固定住了。實踐中我的做法是緩存階段只用 resize 加中心裁剪這種確定性變換訓(xùn)練頭的時候不加增強或者只加很輕的噪聲。import torch import torch.nn as nn from torch.utils.data import DataLoader torch.no_grad() def extract_features(backbone, loader, device): backbone.eval() feats, labels [], [] for images, targets in loader: images images.to(device, non_blockingTrue) out backbone(images) feats.append(out.cpu()) labels.append(targets) return torch.cat(feats), torch.cat(labels) # 主干只保留到池化層去掉原始分類頭 backbone torchvision.models.resnet50(weightsDEFAULT) backbone.fc nn.Identity() backbone backbone.to(device) train_feats, train_labels extract_features(backbone, train_loader, device) val_feats, val_labels extract_features(backbone, val_loader, device)緩存下來之后訓(xùn)練分類頭就是純矩陣運算一個 epoch 幾百毫秒可以把學習率、正則強度、頭結(jié)構(gòu)掃個遍。存儲上算一筆賬50 層殘差網(wǎng)絡(luò)池化后是 2048 維float32 每個樣本 8KB十萬張圖就是 800MB用 float16 降到 400MB內(nèi)存里放得下如果是百萬級數(shù)據(jù)就得落盤或者用分塊訓(xùn)練。這個成本換來的收益是數(shù)量級的迭代速度提升我個人覺得非常值。如果任務(wù)需要空間信息比如分割、檢測、或者細粒度定位那緩存的池化向量就不夠用了因為它把空間結(jié)構(gòu)壓沒了。這時候你需要中間層的特征圖尺寸可能是原圖的 1/32一個 512×7×7 的張量。這種情況下“特征提取”實際上退化成“凍結(jié)主干 訓(xùn)練下游結(jié)構(gòu)”仍然省計算但沒法緩存成緊湊向量顯存占用也會回升因為下游結(jié)構(gòu)要讀取大張量。3.2 部分微調(diào)分層解凍與差異學習率部分微調(diào)的關(guān)鍵在于“解凍哪些、給多大學習率”。按名字匹配是最省事的做法model torchvision.models.resnet50(weightsDEFAULT) model.fc nn.Linear(model.fc.in_features, num_classes) # 先全部凍結(jié) for p in model.parameters(): p.requires_grad False # 只解凍最后一段和分類頭 for name, p in model.named_parameters(): if name.startswith(layer4) or name.startswith(fc): p.requires_grad True groups [ {params: [p for n, p in model.named_parameters() if n.startswith(layer4)], lr: 1e-5}, {params: [p for n, p in model.named_parameters() if n.startswith(fc)], lr: 1e-3}, ] optimizer torch.optim.AdamW(groups, weight_decay1e-4)學習率的比例關(guān)系有個經(jīng)驗值主干解凍部分的學習率是新頭部的十分之一到百分之一。這個比例的來由是梯度尺度差異頭是隨機初始化的梯度大需要大步長主干是預(yù)訓(xùn)練好的只需要小步長微調(diào)步長太大會把已有表示沖掉。我一般在數(shù)據(jù)量幾千到幾萬的場景下用 1:100數(shù)據(jù)量上萬之后可以放寬到 1:30 甚至 1:10。另一個細節(jié)是解凍的粒度。按殘差網(wǎng)絡(luò)的四個 stage 來解凍比較自然只解凍layer4是穩(wěn)妥起點效果不夠再解凍layer3。從后往前逐層放開配合更小的學習率這就是逐層解凍的樸素版本。它比一次性解凍全部要穩(wěn)因為每一步的改動都比較小你能觀察到驗證集在哪一步開始變差從而確定解凍的深度上限。3.3 算力與顯存把賬算清楚再動手以常用的 50 層殘差網(wǎng)絡(luò)為例總參數(shù)量約 2560 萬。如果做十類分類新的全連接層是 2048×10 加 10 個偏置約 2 萬個參數(shù)占總量的萬分之八。這個數(shù)字放在一起看很直觀特征提取方案實際訓(xùn)練的參數(shù)量只有全量微調(diào)的萬分之八訓(xùn)練速度快一個數(shù)量級是理所當然的。前向計算量方面單張 224×224 輸入的前向大約 4 GFLOPs特征提取每個樣本只需要一次前向全量微調(diào)需要前向加反向反向大約是前向的兩倍所以約 12 GFLOPs。顯存方面激活值是大頭粗略估算按每層輸出張量的大小累加輸入分辨率和 batch size 都成正比。實際的數(shù)字跟實現(xiàn)框架的優(yōu)化策略有關(guān)我建議你在自己的環(huán)境里跑一次實測用torch.cuda.max_memory_allocated()打點比自己推導(dǎo)更靠譜。實測的方法是固定 batch size 逐步增大看什么時候拋顯存不足兩個方案各測一次把結(jié)論記在項目筆記里下次換任務(wù)直接復(fù)用。還有一點容易被忽略訓(xùn)練時間不只是計算量決定的。特征提取能通過緩存把 IO 和數(shù)據(jù)增強的開銷也省掉實際提速往往超過理論值而部分微調(diào)時數(shù)據(jù)加載如果跟不上GPU 利用率會掉到五六成這時候提升 dataloader 的 worker 數(shù)量比優(yōu)化模型更有效。3.4 兩套方案的訓(xùn)練配置對照表維度特征提取部分微調(diào)可訓(xùn)練參數(shù)僅新分類頭約萬分級頭部加主干后段百萬級主干梯度無激活不保存有全部前向激活需保存典型顯存占用低可開大 batch中高受分辨率影響大單輪訓(xùn)練耗時秒級緩存后分鐘級過擬合風險低參數(shù)少中高需要早停與正則對域差異的適應(yīng)弱中到強超參敏感度低主要是正則強度高學習率比例影響大復(fù)現(xiàn)難度極低中隨機種子與凍結(jié)順序都要固定適合的數(shù)據(jù)量幾百到幾萬幾千到幾十萬適合的域差異小到中中到大這張表不是教條但能幫你在項目開始時快速定一個起點。我的習慣是無論什么任務(wù)都先跑特征提取拿到基線數(shù)字再決定是否要往表的下半部分走。4. 選型不同場景下怎么定4.1 決策依據(jù)數(shù)據(jù)量、域差異、算力與交付周期選型不該憑感覺我一般看四個量。第一個是標注數(shù)據(jù)量幾百到幾千條的時候可訓(xùn)練參數(shù)越少越穩(wěn)特征提取幾乎是唯一選擇幾萬條可以開始嘗試解凍最后一兩個 stage十萬條以上才值得考慮全量微調(diào)。第二個是域差異用公開自然圖像預(yù)訓(xùn)練的權(quán)重去做醫(yī)學影像或者遙感影像低層特征還能用高層基本失效這時候只訓(xùn)頭往往卡在七成準確率上不去需要解凍中后段。第三個是算力如果只有一張消費級顯卡、還要和別的任務(wù)共享訓(xùn)練時長直接影響你能做多少輪實驗特征提取那種十幾分鐘出一版結(jié)果的節(jié)奏對小團隊更友好。第四個是交付周期如果一周內(nèi)要出可用模型先特征提取交付第一版再慢慢迭代微調(diào)版本這個策略在項目排期緊的時候特別實用。還有一個容易被忽視的維度是驗證集的可信度。小樣本場景下驗證集本身就只有幾百條微調(diào)版的方差會很大同一個配置換個隨機種子可能差三四個點這時候你很難判斷微調(diào)到底有沒有收益。特征提取的參數(shù)少、方差小結(jié)論更可靠。我在數(shù)據(jù)少于兩千條的項目里基本不看微調(diào)結(jié)果因為噪聲比信號大。4.2 典型場景拆解小樣本工業(yè)缺陷分類是很典型的例子。缺陷樣本往往只有幾百張而且圖像是灰度的、高對比度的和預(yù)訓(xùn)練用的自然圖像差異極大。這種場景下很多人第一反應(yīng)是解凍整個網(wǎng)絡(luò)微調(diào)結(jié)果過擬合嚴重驗證準確率來回震蕩。我的做法是特征提取先跑同時把輸入預(yù)處理改成三通道復(fù)制加歸一化調(diào)整讓統(tǒng)計量盡量貼近預(yù)訓(xùn)練分布如果線性探針只能到六成說明高層特征確實不管用再解凍最后一個 stage學習率壓到1e-6級別配合強增強和早停。這套組合在我手上比直接全量微調(diào)穩(wěn)定得多。文本分類是另一個常見場景。用預(yù)訓(xùn)練語言模型做下游任務(wù)時特征提取對應(yīng)的是取池化輸出接一個線性層部分微調(diào)對應(yīng)的是解凍最后幾層 Transformer block。經(jīng)驗上情感分析這類和預(yù)訓(xùn)練目標比較接近的任務(wù)凍結(jié)后幾層就能拿到很好的效果而領(lǐng)域術(shù)語密集的任務(wù)比如專業(yè)文檔分類高層語義偏移明顯解凍兩到四個 block 收益顯著。這里還要注意歸一化層的位置Transformer 里的 LayerNorm 不涉及跨樣本統(tǒng)計凍結(jié)與否的影響比卷積網(wǎng)絡(luò)里的 BatchNorm 小很多這一點常被混淆。分割和檢測這類密集預(yù)測任務(wù)是特征提取的“例外區(qū)”。因為輸出需要空間分辨率你不能只用池化向量必須用中間特征圖而中間特征圖接上采樣和解碼器之后梯度雖然不回傳到主干但主干前向的激活要保留給解碼器讀取顯存優(yōu)勢就沒了。這類任務(wù)我基本直接上凍結(jié)主干的完整微調(diào)框架或者只解凍解碼器把“特征提取”這個詞換成“固定編碼器”更準確。搞清楚這個區(qū)別能避免你在檢測任務(wù)上照著分類的教程去緩存特征白折騰半天。4.3 折中路線先提特征再解凍、逐層解凍、輕量適配層實際項目里我更常用的是漸進路線而不是一開始就二選一。第一步凍結(jié)主干訓(xùn)練頭跑到收斂記下驗證指標這是下限。第二步解凍最后一個 stage學習率設(shè)成頭部的百分之一再訓(xùn)若干輪看指標有沒有超過下限一個閾值比如兩個點以上。如果沒有說明瓶頸不在主干表示可能是數(shù)據(jù)質(zhì)量問題或者標簽噪聲繼續(xù)解凍只會更糟。第三步如果確有提升再往前解凍一層重復(fù)判斷。這個流程的好處是每一步都有對照你能清楚知道收益來自哪一層而不是一鍋端地調(diào)參。另一個折中思路是加輕量適配結(jié)構(gòu)比如在凍結(jié)主干之后插入一個小的瓶頸層或者低秩適配矩陣只訓(xùn)練這部分參數(shù)。這種做法的參數(shù)量介于純特征提取和小范圍微調(diào)之間顯存占用接近特征提取但表達能力比純線性頭強。實現(xiàn)上就是在主干輸出后接一個降維再升維的小模塊維度壓縮比取 8 到 32參數(shù)量控制在幾萬到幾十萬。我在顯存吃緊又要提升精度的場景下用過幾次收益比純線性頭穩(wěn)定高出一到三個點。5. 踩坑實錄與排查清單5.1 訓(xùn)不動或掉點時的排查順序遇到微調(diào)后驗證指標不如特征提取基線我按這個順序查。先看學習率主干學習率是不是設(shè)太大了把預(yù)訓(xùn)練權(quán)重沖掉了直接現(xiàn)象是訓(xùn)練 loss 快速下降但驗證集在第二三個 epoch 就開始惡化。然后看歸一化層是不是沒凍結(jié)統(tǒng)計量batch 又小。接著看數(shù)據(jù)預(yù)處理訓(xùn)練和驗證的歸一化參數(shù)是否一致這一點在手工實現(xiàn)時經(jīng)常出錯。最后看解凍范圍是不是一次解凍太多層。這個順序不是隨便排的是按出現(xiàn)頻率排的我遇到的案例里學習率和歸一化兩類占了絕大多數(shù)。還有一個隱蔽的原因是類別不平衡下的采樣策略。特征提取時因為只訓(xùn)線性頭模型容量小對不平衡的敏感度低換成微調(diào)后容量變大模型會迅速偏向多數(shù)類看起來像“掉點”實際是閾值問題。這時候調(diào)損失函數(shù)的類別權(quán)重比調(diào)解凍范圍更有效。5.2 特征提取效果上不去的常見原因線性探針準確率卡住先懷疑預(yù)處理不匹配。預(yù)訓(xùn)練模型對輸入的均值方差、通道順序、顏色空間是有預(yù)期的你把灰度圖直接塞進去或者歸一化用了另一套參數(shù)特征質(zhì)量會明顯下降。其次是分辨率預(yù)訓(xùn)練用 224 但你把圖縮到 128高層感受野對不上特征會失真。第三是池化方式全局平均池化對小目標不友好如果任務(wù)關(guān)注局部細節(jié)換成最大值池化或者拼接多種池化結(jié)果可能有改善。第四是特征層選錯用最后一層的輸出未必最好中間層的特征有時線性可分性更強值得把幾個 stage 的輸出都取出來做一次探針對比。我做過一次系統(tǒng)的探針實驗把四個 stage 的輸出分別接線性分類器結(jié)果中間層的準確率比最后一層高出五個點原因就是最后一層的語義太偏向原始任務(wù)的類別體系。這個實驗成本很低但能幫你選到更合適的特征層比盲目加深網(wǎng)絡(luò)有效。5.3 顯存、速度與復(fù)現(xiàn)性問題顯存爆掉的時候先確認有沒有多余的梯度記錄。特征提取階段忘了加torch.no_grad()或者在評估時沒切到eval()都會讓顯存翻倍。部分微調(diào)階段可以試試梯度檢查點用時間換顯存代價是訓(xùn)練變慢兩三成。速度慢的話先看數(shù)據(jù)加載是不是瓶頸用nvidia-smi看 GPU 利用率低于七成基本就是數(shù)據(jù)管道拖后腿增加 worker、預(yù)取、把增強放到 GPU 上做都能改善。復(fù)現(xiàn)性問題主要來自三處隨機種子沒固定全包括數(shù)據(jù)增強的隨機數(shù)生成器、凍結(jié)順序在不同運行里不一致、以及非確定性算子。前兩個靠代碼規(guī)范解決把凍結(jié)邏輯寫成一個函數(shù)在入口調(diào)用一次第三個在調(diào)試階段可以開啟確定性模式雖然會慢一點但至少能保證兩次運行結(jié)果一致。5.4 問題速查表現(xiàn)象可能原因處理方式微調(diào)后驗證指標低于特征提取基線主干學習率過大降到頭部學習率的百分之一訓(xùn)練 loss 降但驗證抖動大歸一化統(tǒng)計量被小 batch 污染訓(xùn)練時凍結(jié)歸一化層為評估模式特征提取探針準確率很低預(yù)處理與預(yù)訓(xùn)練預(yù)期不匹配對齊均值方差、通道數(shù)、分辨率顯存占用遠超預(yù)期主干激活被記錄檢查是否遺漏無梯度上下文同一配置兩次結(jié)果差異大種子未固定或凍結(jié)順序不一致統(tǒng)一入口凍結(jié)并固定全部隨機源微調(diào)收斂極慢頭部初始化尺度過小或?qū)W習率過低檢查頭部初始化頭部學習率單獨放大解凍多層后性能反而下降解凍范圍超出數(shù)據(jù)支撐能力回退到只解凍最后一層這張表我貼在項目筆記首頁新同事上手時照著查能省掉大量重復(fù)的排查時間。表格里的每一條都是從實際報錯和曲線異常里總結(jié)出來的不是理論推演。最后分享一點個人體會。這幾年做下來我發(fā)現(xiàn)選型的難點不在技術(shù)本身而在于你對自己數(shù)據(jù)的判斷有多準。同樣是小樣本任務(wù)有的數(shù)據(jù)雖然少但標注質(zhì)量高、類間差異明顯特征提取就能到九成有的數(shù)據(jù)量不小但標簽噪聲大、類間混淆嚴重微調(diào)再怎么調(diào)也就七成出頭。所以在動手調(diào)參之前花半天時間看看錯樣本、算算類間距離、跑一次線性探針比后面幾天的調(diào)參都值。我現(xiàn)在的習慣是任何新任務(wù)先花兩小時做這三件事基本就能判斷該走哪條路剩下的時間都花在數(shù)據(jù)清洗和驗證集構(gòu)建上。