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