習(xí)訓(xùn)練實(shí)戰(zhàn):優(yōu)化器、學(xué)習(xí)率調(diào)度與過擬合排查全攻略)
前兩篇筆記我寫的是正經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)和數(shù)學(xué)推導(dǎo)反向傳播怎么推、卷積怎么算、注意力機(jī)制的內(nèi)積為什么除根號d。那會(huì)兒覺得自己什么都懂了直到開始在真實(shí)數(shù)據(jù)集上訓(xùn)練模型才發(fā)現(xiàn)真正折磨人的不是公式而是那些訓(xùn)練過程里“玄學(xué)”一樣的細(xì)節(jié)。這篇筆記換個(gè)角度不聊網(wǎng)絡(luò)設(shè)計(jì)專門記錄我這幾個(gè)月調(diào)模型攢下來的實(shí)戰(zhàn)經(jīng)驗(yàn)包括優(yōu)化器選型、學(xué)習(xí)率調(diào)度、過擬合排查、訓(xùn)練不收斂的解決思路還有遷移學(xué)習(xí)里容易被忽略的坑。內(nèi)容主要來自我自己跑實(shí)驗(yàn)的訓(xùn)練日志和踩坑記錄適合已經(jīng)掌握深度學(xué)習(xí)基礎(chǔ)、正在動(dòng)手訓(xùn)練自己模型的讀者也適合那些理論看了不少但一上手就loss亂飄的人。1. 先從優(yōu)化器說起不是所有任務(wù)都適合默認(rèn)配置1.1 三個(gè)常用優(yōu)化器的本質(zhì)區(qū)別很多人拿到一個(gè)模型就默認(rèn)“優(yōu)化器用Adam就行”這沒有錯(cuò)但不夠聰明。我自己在這個(gè)問題上吃過虧所以先搞清楚三個(gè)主流優(yōu)化器到底差在哪。第一是帶動(dòng)量的SGD。它的核心思想很簡單在梯度下降的基礎(chǔ)上引入一個(gè)速度項(xiàng)讓更新方向不只依賴當(dāng)前梯度還參考之前幾步的運(yùn)動(dòng)方向。這么做能抑制震蕩尤其在loss曲面比較狹長的場景里效果很明顯。缺點(diǎn)是學(xué)習(xí)率要自己調(diào)得很小心而且對參數(shù)的每個(gè)維度用同一個(gè)學(xué)習(xí)率適應(yīng)性差一些。第二是Adam。它在SGD動(dòng)量思路上更進(jìn)一步給每個(gè)參數(shù)單獨(dú)維護(hù)一階動(dòng)量梯度均值和二階動(dòng)量梯度平方的均值相當(dāng)于每個(gè)參數(shù)都有自己的自適應(yīng)學(xué)習(xí)率。這個(gè)特性讓它對初始學(xué)習(xí)率的敏感度低很多很多新手上來設(shè)1e-3也能跑起來。但Adam有個(gè)問題它在訓(xùn)練后期容易出現(xiàn)極端的學(xué)習(xí)率泛化能力理論上不如調(diào)好的帶動(dòng)量SGD。第三是AdamW本質(zhì)上是把權(quán)重衰減從梯度更新中解耦出來。在傳統(tǒng)Adam里權(quán)重衰減會(huì)被除以二階動(dòng)量的平方根導(dǎo)致大梯度參數(shù)衰減得多、小梯度參數(shù)衰減得少這其實(shí)是錯(cuò)的。AdamW直接把權(quán)重衰減單獨(dú)作用在參數(shù)上不參與動(dòng)量的歸一化計(jì)算行為更可控。幾乎所有Transformer類模型都用它不是沒道理的。這里有個(gè)我在實(shí)際項(xiàng)目里體會(huì)很深的例子。有一回我訓(xùn)練一個(gè)圖像分類模型主干網(wǎng)絡(luò)用的是預(yù)訓(xùn)練的卷積網(wǎng)絡(luò)剛開始圖省事直接上了Adam初始學(xué)習(xí)率1e-3訓(xùn)練50個(gè)epoch驗(yàn)證集準(zhǔn)確率卡在86%左右上不去。后來把優(yōu)化器換成AdamW權(quán)重衰減設(shè)5e-2同樣的epoch數(shù)準(zhǔn)確率漲到了89%。原因就是AdamW的權(quán)重衰減作用得更干凈沒有和自適應(yīng)學(xué)習(xí)率攪在一起。1.2 學(xué)習(xí)率調(diào)度才是重頭戲優(yōu)化器決定了參數(shù)往哪個(gè)方向走、走多快但怎么走完全程要看學(xué)習(xí)率調(diào)度。我目前用得最多的是兩個(gè)階段組合先把學(xué)習(xí)率從一個(gè)很小的值線性升到目標(biāo)值warmup階段再用余弦退火慢慢降下來。為什么要warmup因?yàn)橛?xùn)練初期模型參數(shù)是隨機(jī)狀態(tài)梯度方向噪聲很大如果一上來就用大學(xué)習(xí)率很容易把參數(shù)推到某個(gè)不合適的區(qū)域后面很難收回來。尤其是在用Adam類優(yōu)化器時(shí)初期二階動(dòng)量估計(jì)不夠準(zhǔn)確學(xué)習(xí)率偏大容易產(chǎn)生震蕩。warmup相當(dāng)于讓模型先試探性地走幾步等梯度方向穩(wěn)定了再加速。余弦退火的理由比較直觀訓(xùn)練后期需要更細(xì)膩的步長來落入好的局部最優(yōu)解余弦曲線前半段緩慢下降、后半段快速下降正好符合訓(xùn)練由粗到細(xì)的節(jié)奏。我也試過Step Decay每30個(gè)epoch減半效果其實(shí)也不差但它需要手動(dòng)設(shè)定衰減節(jié)點(diǎn)不同數(shù)據(jù)集的最佳節(jié)點(diǎn)還不一樣不夠省心。warmup加余弦退火的實(shí)現(xiàn)非常簡單用PyTorch可以這樣寫import math from torch.optim import AdamW from torch.optim.lr_scheduler import LambdaLR def linear_warmup_cosine_decay(step, warmup_steps, total_steps): if step warmup_steps: return float(step 1) / float(warmup_steps) progress float(step - warmup_steps) / max(1, float(total_steps - warmup_steps)) return 0.5 * (1.0 math.cos(math.pi * progress)) total_steps 10000 warmup_steps 500 optimizer AdamW(model.parameters(), lr2e-4, weight_decay0.05) scheduler LambdaLR( optimizer, lr_lambdalambda step: linear_warmup_cosine_decay(step, warmup_steps, total_steps) )這段代碼配合一個(gè)逐step調(diào)用的循環(huán)就行注意這里lambda接收的是step計(jì)數(shù)而不是epoch因?yàn)椴煌琤atch size下每個(gè)epoch的步數(shù)不一樣按step調(diào)度更均勻。1.3 我常用的參數(shù)參考表下面是我在幾種常見任務(wù)上試出來比較穩(wěn)的初始配置可以作為起點(diǎn)再細(xì)調(diào)任務(wù)類型優(yōu)化器初始學(xué)習(xí)率調(diào)度器權(quán)重衰減CNN圖像分類SGD(動(dòng)量0.9)0.1配合batch size 256余弦退火1e-4CNN圖像分類AdamW1e-4warmup余弦0.05Transformer文本分類AdamW2e-5到5e-5warmup線性衰減0.01目標(biāo)檢測微調(diào)SGD(動(dòng)量0.9)0.001Step Decay1e-4生成對抗網(wǎng)絡(luò)Adam2e-4不衰減或固定0需要注意一個(gè)關(guān)聯(lián)變量初始學(xué)習(xí)率不是孤立設(shè)定的它和batch size成正比關(guān)系。經(jīng)驗(yàn)法則是batch size翻倍學(xué)習(xí)率大致也要翻倍因?yàn)樘荻裙烙?jì)更穩(wěn)定了可以承擔(dān)更大的更新步長。我一開始不懂這個(gè)batch size從32調(diào)到128的時(shí)候沒動(dòng)學(xué)習(xí)率結(jié)果訓(xùn)練非常慢白白浪費(fèi)了一個(gè)下午。2. 過擬合訓(xùn)練集漲、驗(yàn)證集跌最讓人頭疼的問題2.1 先判斷是不是真的過擬合過擬合這個(gè)事判斷錯(cuò)了比不判斷還麻煩。我之前有過一次經(jīng)歷看到驗(yàn)證集準(zhǔn)確率停下來就急著加各種正則手段結(jié)果把模型壓得太狠訓(xùn)練集都不收斂了。后來認(rèn)真看曲線才發(fā)現(xiàn)訓(xùn)練集loss還在下降驗(yàn)證集loss已經(jīng)反彈這才是典型的過擬合信號。判斷過擬合的幾條標(biāo)準(zhǔn)訓(xùn)練集loss持續(xù)降低驗(yàn)證集loss先降后升中間出現(xiàn)拐點(diǎn)。訓(xùn)練集準(zhǔn)確率接近甚至超過驗(yàn)證集準(zhǔn)確率并且差距越來越大。模型權(quán)重的L2范數(shù)在訓(xùn)練后期明顯增大說明部分參數(shù)被“撐”得很大。過擬合的根本原因是模型容量相對于數(shù)據(jù)量偏大模型把訓(xùn)練集里的噪聲也當(dāng)成規(guī)律學(xué)進(jìn)去了。所以對策有兩個(gè)方向一是減小模型有效容量二是增加數(shù)據(jù)的多樣性和數(shù)量。正則化手段的本質(zhì)都是在限制模型對訓(xùn)練數(shù)據(jù)的過度擬合程度。2.2 正則化手段怎么組合正則化不是堆得越多越好關(guān)鍵要看任務(wù)類型。Dropout是注意力模型里幾乎必用的手段一般加在Feed-Forward層和注意力輸出的殘差連接之后dropout率取0.1或0.2就夠。但在CNN主干網(wǎng)絡(luò)里Dropout的效果遠(yuǎn)沒有在Transformer里明顯因?yàn)榫矸e本身帶有空間共享的天然正則效果。我見過有人給ResNet50強(qiáng)行加了很多Dropout準(zhǔn)確率不升反降就是這個(gè)道理。如果要在CNN里用建議只加在全連接層之前而且概率不要超過0.5。權(quán)重衰減是最省心的正則手段它的對偶理解就是給參數(shù)空間畫了一個(gè)約束球防止參數(shù)變得過大。權(quán)重衰減系數(shù)的選擇我建議做一次快速小規(guī)模搜索比如在1e-5、1e-4、1e-3、0.01、0.05這幾個(gè)量級里對比。我踩過的一個(gè)坑是在AdamW里權(quán)重衰減設(shè)得太小比如1e-4幾乎不起作用設(shè)得太大又會(huì)欠擬合所以這個(gè)參數(shù)一定要放在和任務(wù)類型相關(guān)的上下文里調(diào)。標(biāo)簽平滑是另一個(gè)容易被忽視的正則技巧。傳統(tǒng)交叉熵在訓(xùn)練時(shí)要求模型對正確類別輸出接近1的置信度模型容易變得過度自信。標(biāo)簽平滑把正確類別的目標(biāo)概率從1降到比如0.9剩下的0.1分?jǐn)偨o其他類別相當(dāng)于告訴模型“不要那么絕對”。這在分類任務(wù)里基本沒有副作用我一般設(shè)0.1。早停是性價(jià)比最高的止損失控手段。驗(yàn)證集loss在連續(xù)N個(gè)epoch不下降時(shí)直接停掉保存歷史最優(yōu)的模型。這里有個(gè)細(xì)節(jié)早停不是看驗(yàn)證集loss本身而是看它有沒有打破歷史最低記錄比如連續(xù)7個(gè)epoch沒有新低就停止。我用的是保存最優(yōu)權(quán)重加記錄“最優(yōu)epoch序號”的方式而不是簡單淘汰模型文件。這幾種手段可以組合但要注意主次手段適用場景常見系數(shù)/參數(shù)備注DropoutTransformer、MLP0.1-0.2CNN主干慎用權(quán)重衰減所有任務(wù)1e-4到0.05AdamW里不要設(shè)太小標(biāo)簽平滑分類任務(wù)0.05-0.1對噪聲標(biāo)簽也有好處早停所有任務(wù)patience 5-10配合模型保存梯度裁剪訓(xùn)練不穩(wěn)定時(shí)裁剪閾值1.0不是正則但防爆炸2.3 數(shù)據(jù)增強(qiáng)的正確姿勢數(shù)據(jù)增強(qiáng)是隱形的正則化它通過改變訓(xùn)練樣本的形態(tài)讓模型學(xué)到更本質(zhì)的特征而不是輸入表面的統(tǒng)計(jì)模式。對圖像任務(wù)我常用隨機(jī)水平翻轉(zhuǎn)、隨機(jī)裁剪、顏色抖動(dòng)亮度、對比度、飽和度的組合。顏色抖動(dòng)對光照變化明顯的場景很有效但也別把顏色變到失真否則模型學(xué)不到真實(shí)分布的紋理。增強(qiáng)強(qiáng)度是個(gè)需要把握的度。過度增強(qiáng)會(huì)導(dǎo)致訓(xùn)練集樣本和真實(shí)數(shù)據(jù)分布差距過大模型在驗(yàn)證集上表現(xiàn)反而下降。我的經(jīng)驗(yàn)是先從標(biāo)準(zhǔn)增強(qiáng)開始比如水平翻轉(zhuǎn)加隨機(jī)裁剪觀察曲線形態(tài)如果過擬合明顯再逐步加入顏色抖動(dòng)、旋轉(zhuǎn)、尺度變換每一步都記錄驗(yàn)證集的變化。對Transformer類模型我現(xiàn)在用得比較多的是隨機(jī)掩碼。具體做法是對輸入嵌入的序列在時(shí)間維度上隨機(jī)mask掉一部分這能增強(qiáng)模型對缺失輸入的魯棒性。注意mask的比例不要超過15%否則訓(xùn)練信號太稀疏收斂很慢。3. 訓(xùn)練不收斂怎么辦一套可復(fù)制的排查流程3.1 先看loss曲線形態(tài)模型訓(xùn)練不收斂的時(shí)候最忌諱的就是盲目改參數(shù)。我現(xiàn)在的做法是先看loss曲線長什么樣不同形態(tài)對應(yīng)完全不同的病因。loss曲線形態(tài)可能原因排查方向一直不降在初始值附近震蕩學(xué)習(xí)率太低或數(shù)據(jù)沒配對好檢查數(shù)據(jù)、提高學(xué)習(xí)率loss直接跳NaN梯度爆炸、除零、數(shù)值溢出檢查模型數(shù)值穩(wěn)定性、用梯度裁剪快速下降后反彈并且震蕩劇烈學(xué)習(xí)率過高降低學(xué)習(xí)率或加warmup訓(xùn)練loss正常驗(yàn)證loss亂竄overfitting或驗(yàn)證集太小檢查數(shù)據(jù)劃分、加正則降到某個(gè)平臺(tái)就是不動(dòng)陷入局部極值或?qū)W習(xí)率太小換優(yōu)化器、重啟訓(xùn)練、cosine退火我印象最深的一次排查是loss一直不降試了很多辦法都沒用。后來抱著懷疑的心態(tài)打印了一批訓(xùn)練樣本發(fā)現(xiàn)輸入x和標(biāo)簽y在數(shù)據(jù)加載時(shí)對齊錯(cuò)了x還是第一版的索引y已經(jīng)換成了新版本。這種低級錯(cuò)誤最耗時(shí)間因?yàn)樗粓?bào)錯(cuò)損失只是不降而已。所以排查不收斂時(shí)第一件事永遠(yuǎn)是檢查數(shù)據(jù)。3.2 從數(shù)據(jù)側(cè)排查數(shù)據(jù)問題排在模型問題前面因?yàn)閿?shù)據(jù)比模型更容易出錯(cuò)。我整理了自己常用的排查清單確認(rèn)x和y的對應(yīng)關(guān)系隨機(jī)打印幾個(gè)樣本人工核對標(biāo)簽。檢查數(shù)據(jù)是否每個(gè)epoch都進(jìn)行了shuffle。如果不shuffle模型會(huì)學(xué)到樣本順序的假規(guī)律驗(yàn)證集表現(xiàn)會(huì)很不穩(wěn)定。檢查樣本分布是否極端不均衡。某個(gè)類占比超過90%時(shí)模型會(huì)傾向全部預(yù)測成這個(gè)類看起來loss在降但實(shí)際沒學(xué)到東西這時(shí)候需要class weight或重采樣。檢查歸一化方式。圖像一般用均值0.5、標(biāo)準(zhǔn)差0.5歸一化到[-1,1]或者用數(shù)據(jù)集自身的均值方差。文本任務(wù)里padding mask有沒有正確傳給損失函數(shù)也容易被忽略。這里有個(gè)容易踩的坑在驗(yàn)證集上也使用了訓(xùn)練時(shí)的數(shù)據(jù)增強(qiáng)。增強(qiáng)會(huì)在驗(yàn)證時(shí)給同一個(gè)樣本引入隨機(jī)性導(dǎo)致每次驗(yàn)證結(jié)果都不一致你根本分不清模型是變好了還是變壞了。驗(yàn)證階段必須關(guān)閉所有隨機(jī)增強(qiáng)只保留確定性預(yù)處理。3.3 從模型側(cè)排查數(shù)據(jù)檢查完沒問題就該看模型內(nèi)部的數(shù)值情況。我最常用的手段是打印每一步的梯度范數(shù)。訓(xùn)練不收斂的模型梯度范數(shù)要么小到接近0要么大到幾千。梯度接近0說明梯度在反向傳播過程中消失了常見原因是網(wǎng)絡(luò)太深、激活函數(shù)選擇不當(dāng)或者初始化不當(dāng)導(dǎo)致輸出飽和梯度很大說明反向傳播時(shí)梯度越積越大層數(shù)多的時(shí)候尤其容易發(fā)生梯度爆炸。具體檢查方法是在每個(gè)step的backward之后、optimizer.step之前把各層參數(shù)的梯度范數(shù)打印出來total_norm 0.0 for p in model.parameters(): if p.grad is not None: param_norm p.grad.data.norm(2) total_norm param_norm.item() ** 2 total_norm total_norm ** 0.5 print(fgrad norm: {total_norm:.4f})如果發(fā)現(xiàn)某一層的梯度和別層差了好幾個(gè)數(shù)量級基本就是數(shù)值不穩(wěn)定的根源。解決方案通常是梯度裁剪把整體梯度范數(shù)限制在1.0或5.0以內(nèi)torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)初始化方式也值得檢查?,F(xiàn)代網(wǎng)絡(luò)結(jié)構(gòu)一般自帶默認(rèn)初始化但如果是自己搭的非常規(guī)結(jié)構(gòu)要用Kaiming或Xavier初始化來匹配激活函數(shù)的尺度。我見過一個(gè)自己寫的MLP用了全零初始化結(jié)果訓(xùn)練了200步loss紋絲不動(dòng)換成Kaiming初始化后馬上開始下降這種問題從數(shù)值上查是最快的。3.4 關(guān)于隨機(jī)種子的執(zhí)念排查不收斂的時(shí)候我還有一個(gè)習(xí)慣固定隨機(jī)種子。不固定的種子會(huì)讓每次運(yùn)行結(jié)果都有細(xì)微不同排查問題的時(shí)候很難判斷改動(dòng)到底有沒有效果。建議在訓(xùn)練腳本開頭固定所有相關(guān)種子import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)但我也要提醒一點(diǎn)固定種子只是排查和復(fù)現(xiàn)實(shí)驗(yàn)的手段不是解決不收斂的方案。如果模型在好幾個(gè)不同的種子下訓(xùn)練結(jié)果波動(dòng)巨大那說明訓(xùn)練本身不穩(wěn)定光靠固定種子是在掩蓋問題真正的病因可能是學(xué)習(xí)率太高或者數(shù)據(jù)太少。4. 遷移學(xué)習(xí)與微調(diào)小數(shù)據(jù)集救星但不是無腦加載權(quán)重4.1 什么時(shí)候適合微調(diào)手上數(shù)據(jù)量少的時(shí)候從頭訓(xùn)練一個(gè)大模型基本是死路。預(yù)訓(xùn)練模型已經(jīng)在海量數(shù)據(jù)上學(xué)到了通用的底層特征比如邊緣、紋理、形狀結(jié)構(gòu)這些知識(shí)對小數(shù)據(jù)集很有價(jià)值。我的經(jīng)驗(yàn)判斷標(biāo)準(zhǔn)是如果訓(xùn)練集只有幾千張圖或幾萬條文本從頭訓(xùn)練很難達(dá)到滿意的效果微調(diào)幾乎必然優(yōu)于從頭訓(xùn)練。當(dāng)數(shù)據(jù)量達(dá)到百萬級時(shí)從頭訓(xùn)練才可能和微調(diào)打平。在數(shù)據(jù)量為中等規(guī)模比如幾萬到幾十萬微調(diào)依然是最省時(shí)間的方案。4.2 凍結(jié)與解凍策略微調(diào)最忌諱的是直接用一個(gè)大學(xué)習(xí)率去更新全部參數(shù)尤其是預(yù)訓(xùn)練模型的底層特征已經(jīng)足夠通用大更新會(huì)把它們破壞掉。我常用的策略分兩步走第一步凍結(jié)backbone部分參數(shù)只訓(xùn)練新加的任務(wù)頭。比如圖像分類時(shí)凍結(jié)卷積網(wǎng)絡(luò)的所有層只訓(xùn)練global pooling和最后的全連接層。這一步讓任務(wù)頭先適應(yīng)預(yù)訓(xùn)練特征輸出大約訓(xùn)練5到10個(gè)epoch。凍結(jié)的實(shí)現(xiàn)很簡單for param in model.backbone.parameters(): param.requires_grad False optimizer AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3)第二步解凍backbone但用一個(gè)小得多的學(xué)習(xí)率比如1e-5到3e-5和任務(wù)頭一起繼續(xù)訓(xùn)練。這么做是為了讓預(yù)訓(xùn)練特征在目標(biāo)任務(wù)數(shù)據(jù)上做細(xì)微調(diào)整又不至于偏離太遠(yuǎn)。有些任務(wù)甚至全程都只訓(xùn)任務(wù)頭就夠了尤其是任務(wù)和預(yù)訓(xùn)練任務(wù)比較接近、數(shù)據(jù)量很小的時(shí)候。我還試過一種變體把參數(shù)分成兩個(gè)組backbone一個(gè)組用低學(xué)習(xí)率任務(wù)頭一個(gè)組用正常學(xué)習(xí)率用一個(gè)optimizer就能管理optimizer AdamW([ {params: model.backbone.parameters(), lr: 1e-5}, {params: model.head.parameters(), lr: 1e-3}, ], weight_decay0.05)這種分層學(xué)習(xí)率的方法比手動(dòng)凍結(jié)解凍更方便適合一次跑完整個(gè)流程。4.3 經(jīng)驗(yàn)坑BN層在微調(diào)時(shí)的行為微調(diào)時(shí)最容易被忽略的是BatchNorm層。BN層在訓(xùn)練時(shí)維護(hù)一組running mean和running variance這是從訓(xùn)練數(shù)據(jù)上積累出來的統(tǒng)計(jì)量。如果你的batch size比較小比如8或16BN統(tǒng)計(jì)量會(huì)很不穩(wěn)定因?yàn)樗挥卯?dāng)前batch的均值和方差來歸一化小batch本身噪聲就大。剛才說的“先凍結(jié)backbone只訓(xùn)分類頭”對含BN的網(wǎng)絡(luò)要格外小心。凍結(jié)參數(shù)只是不再更新scale和shift但BN的running stats在訓(xùn)練模式下還是在更新的這可能導(dǎo)致預(yù)訓(xùn)練模型統(tǒng)計(jì)量被小batch小數(shù)據(jù)帶偏。我踩過這個(gè)坑微調(diào)一個(gè)帶BN的卷積網(wǎng)絡(luò)時(shí)凍結(jié)backbone解凍head驗(yàn)證集準(zhǔn)確率反而下降就是因?yàn)锽N統(tǒng)計(jì)量被污染了。解決辦法有兩個(gè)方向。一個(gè)是把BN層也凍結(jié)通過設(shè)置requires_grad之外再維護(hù)一個(gè)“只跑前向不更新統(tǒng)計(jì)量”的模式另一個(gè)是batch size盡量保持適中不要太小讓BN統(tǒng)計(jì)量有足夠的樣本支撐。如果顯存不允許大batch建議干脆用LayerNorm替代BN結(jié)構(gòu)的模型或者使用GroupNorm這類不依賴batch大小的歸一化方式。還有個(gè)更典型的坑是解凍全部層之后驗(yàn)證集準(zhǔn)確率反而下降。這往往發(fā)生在目標(biāo)數(shù)據(jù)和預(yù)訓(xùn)練數(shù)據(jù)分布差異比較大的場景。比如預(yù)訓(xùn)練模型是從通用圖像數(shù)據(jù)上學(xué)的你的任務(wù)是某種特定的醫(yī)學(xué)影像淺層特征雖然通用但高層特征差異很大。此時(shí)要謹(jǐn)慎解凍最后幾層或者學(xué)習(xí)率設(shè)得更低比如1e-6量級同時(shí)加上早停在驗(yàn)證集最優(yōu)的時(shí)候及時(shí)停止。5. 實(shí)驗(yàn)管理別讓結(jié)果無法復(fù)現(xiàn)5.1 日志記錄與模型保存訓(xùn)練深度學(xué)習(xí)模型實(shí)驗(yàn)管理做得越好后期效率越高。我最早訓(xùn)練模型時(shí)只在控制臺(tái)打印日志模型只保存最后一個(gè)checkpoint結(jié)果想分析某個(gè)epoch為什么效果最好時(shí)根本沒有數(shù)據(jù)可用?,F(xiàn)在我的習(xí)慣是至少記錄三樣?xùn)|西每個(gè)epoch的loss和指標(biāo)、模型的歷史最優(yōu)權(quán)重、完整的配置參數(shù)。模型保存方面我建議保存歷史最好的幾個(gè)checkpoint而不是只保留最后一個(gè)。可以用類似這樣一個(gè)簡單邏輯每個(gè)epoch結(jié)束后在驗(yàn)證集上評估如果指標(biāo)超過歷史最優(yōu)就把當(dāng)前權(quán)重存成best_model.pth同時(shí)把epoch號寫在文件名里。這樣即使后面訓(xùn)練崩了也還有最優(yōu)模型兜底。配置參數(shù)一定要保存成獨(dú)立文件和模型權(quán)重放在同一個(gè)目錄下。很多復(fù)現(xiàn)不了實(shí)驗(yàn)的問題都是因?yàn)檫^了一周自己都忘了當(dāng)時(shí)用了什么學(xué)習(xí)率、什么batch size、什么增強(qiáng)方式。用PyTorch的話可以在啟動(dòng)訓(xùn)練時(shí)把所有超參數(shù)整理成字典存成JSONimport json config { optimizer: adamw, lr: 2e-4, weight_decay: 0.05, batch_size: 64, epochs: 30, warmup_steps: 500, loss: cross_entropy, seed: 42, model_name: resnet50, } with open(config.json, w) as f: json.dump(config, f, indent2)5.2 對比實(shí)驗(yàn)的變量控制做對比實(shí)驗(yàn)時(shí)最大的敵人是變量不干凈。我見過別人滿懷信心地跟我說某技巧提升了兩個(gè)點(diǎn)但仔細(xì)一問他同時(shí)改了學(xué)習(xí)率調(diào)度、改了數(shù)據(jù)增強(qiáng)、還換了優(yōu)化器這根本沒法歸因。正確的打開方式是一次只改一個(gè)變量其余全部保持一致。具體執(zhí)行時(shí)我習(xí)慣給每輪實(shí)驗(yàn)編一個(gè)簡短的名字比如baseline_v1、aug_v2、adamw_v3然后把每輪實(shí)驗(yàn)對應(yīng)的config文件和日志文件都存好。橫向比較時(shí)固定評價(jià)指標(biāo)和評估數(shù)據(jù)集比如只用驗(yàn)證集上的top-1準(zhǔn)確率做比較。不要一會(huì)兒看準(zhǔn)確率一會(huì)兒看loss這兩個(gè)指標(biāo)關(guān)心的問題不一樣。這里有個(gè)我自己的小習(xí)慣每次實(shí)驗(yàn)前先在表格里記下“這次要改的變量是什么、預(yù)期效果是什么、對照組是哪個(gè)”。雖然看起來有點(diǎn)繁瑣但它能逼著你想清楚每個(gè)實(shí)驗(yàn)的目的避免白白浪費(fèi)算力在無意義的試錯(cuò)上。實(shí)驗(yàn)編號改動(dòng)變量固定不變的因素驗(yàn)證集指標(biāo)baseline_v1無全部默認(rèn)86.2%aug_v2增加顏色抖動(dòng)默認(rèn)其余全部87.1%adamw_v3換成AdamW默認(rèn)增強(qiáng)調(diào)度89.0%lr_v4學(xué)習(xí)率降到1e-4同上88.4%這張表看起來簡單但堅(jiān)持記錄下來你會(huì)發(fā)現(xiàn)幾輪實(shí)驗(yàn)之后完成了一套很有價(jià)值的“經(jīng)驗(yàn)地圖”哪些改動(dòng)對當(dāng)前任務(wù)有效、哪些無效一目了然。結(jié)束語這篇筆記寫到這里其實(shí)還沒有把我想記錄的都寫完比如分布式訓(xùn)練、混合精度、模型量化這些話題都還沒展開以后有機(jī)會(huì)再單獨(dú)寫。但從我自己這半年的實(shí)戰(zhàn)感受來說深度學(xué)習(xí)訓(xùn)練中真正拉開差距的往往不是誰看了更多論文而是誰能在loss不降的時(shí)候冷靜地一步步排查在過擬合出現(xiàn)的時(shí)候找到正確的組合拳在微調(diào)的時(shí)候尊重預(yù)訓(xùn)練模型的“體質(zhì)”。如果你是正在自己動(dòng)手跑模型的讀者我建議把優(yōu)化器、學(xué)習(xí)率調(diào)度、早期停止、種子固定這幾件事先踏踏實(shí)實(shí)做扎實(shí)大部分訓(xùn)練問題都能在這些基礎(chǔ)設(shè)置里找到答案。最后再分享一個(gè)小技巧每次訓(xùn)練前先在驗(yàn)證集上跑一個(gè)epoch看看模型能不能快速用少數(shù)樣本過擬合這一步看起來多余但它能幫你快速確認(rèn)代碼鏈路沒有低級錯(cuò)誤省下來的時(shí)間遠(yuǎn)比訓(xùn)練一次多。