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