戰(zhàn):從理論到PyTorch完整訓(xùn)練流程)
從書桌抽屜里翻出那本已經(jīng)翻得卷了邊的深度學(xué)習(xí)教材時(shí)我習(xí)慣性地在目錄頁(yè)寫下了一個(gè)編號(hào)deeplearningbook_007。這是我給自己布置的第七個(gè)讀書實(shí)踐任務(wù)——圖像分類。而這篇博文要聊的是這個(gè)編號(hào)的后半段也就是 007-2把卷積神經(jīng)網(wǎng)絡(luò)CNN從理論書里搬進(jìn)真實(shí)代碼用一套完整流程訓(xùn)練出一個(gè)能用的分類模型。很多朋友讀 CNN 章節(jié)時(shí)的狀態(tài)我太熟悉了卷積是滑動(dòng)窗口池化是降采樣反向傳播公式也能手推一遍可真要打開編輯器寫第一個(gè)模型光標(biāo)停在第一行就不知道往哪兒落了。007-2 就是為這個(gè)尷尬期準(zhǔn)備的。我會(huì)把從數(shù)據(jù)準(zhǔn)備、模型搭建、訓(xùn)練配置到調(diào)優(yōu)排錯(cuò)的整個(gè)過程完整走一遍并且說清楚每個(gè)設(shè)計(jì)決策背后的原因。這篇內(nèi)容適合兩類人一類是剛讀完 CNN 理論、正準(zhǔn)備動(dòng)手實(shí)踐的新手另一類是已經(jīng)能跑通基礎(chǔ)模型、但總覺得哪里沒搞透的進(jìn)階學(xué)習(xí)者。1. 項(xiàng)目定位007-2 到底要解決什么問題1.1 從讀書到動(dòng)手為什么需要第二部分理論學(xué)習(xí)和實(shí)際落地之間有一條深深的溝這條溝不是靠再讀一遍書能填平的。我在 007-1 里做的事是手動(dòng)實(shí)現(xiàn)卷積運(yùn)算、推導(dǎo)感受野計(jì)算公式、逐層統(tǒng)計(jì)參數(shù)量這些工作幫我建立了對(duì) CNN 底層機(jī)制的直觀理解。但到了真要訓(xùn)練一個(gè)能分類圖片的模型時(shí)我發(fā)現(xiàn)自己仍然不知道從哪兒開始數(shù)據(jù)怎么加載損失函數(shù)選哪個(gè)學(xué)習(xí)率定多少007-2 就是用來填這條溝的。它能解決三個(gè)具體問題第一把書上抽象的概念映射到框架里的真實(shí) API比如把卷積層對(duì)應(yīng)到 Conv2d把批歸一化對(duì)應(yīng)到 BatchNorm2d讓知識(shí)真正落地第二把數(shù)據(jù)預(yù)處理、模型設(shè)計(jì)、訓(xùn)練、評(píng)估這條鏈路完整地走通一遍以后遇到任何圖像任務(wù)都可以復(fù)用這一套方法論第三把訓(xùn)練過程中那些書上不會(huì)寫的問題暴露出來——過擬合、不收斂、顯存不夠——這些問題只有親手踩一遍才知道怎么處理。我自己的體會(huì)是這種先手動(dòng)實(shí)現(xiàn)底層、再用框架快速搭建的兩段式學(xué)習(xí)法特別適合深度學(xué)習(xí)。手動(dòng)實(shí)現(xiàn)讓你理解原理框架搭建讓你專注解決問題本身兩者互補(bǔ)缺一個(gè)都會(huì)讓能力樹長(zhǎng)歪。1.2 數(shù)據(jù)與任務(wù)設(shè)定007-2 選的任務(wù)很樸素對(duì)一組公開的花卉圖片做分類。一共 5 個(gè)類別每類大概 600 張圖片圖片尺寸不太統(tǒng)一大部分在 200x200 到 500x500 像素之間。選這個(gè)任務(wù)有三個(gè)原因5 分類問題是展示 CNN 能力的合理規(guī)模既不會(huì)像千分類那樣需要龐大訓(xùn)練資源和很長(zhǎng)時(shí)間又能清楚展現(xiàn)特征提取、分類決策的完整過程?;ɑ軋D片在顏色、紋理、形狀上差異明顯非常便于觀察模型到底學(xué)到了什么特征也方便用熱力圖做可視化分析。數(shù)據(jù)量不大即使用 CPU 訓(xùn)練簡(jiǎn)單模型也能在合理時(shí)間內(nèi)跑完一個(gè) epoch適合反復(fù)調(diào)試。任務(wù)目標(biāo)我定得很明確在測(cè)試集上達(dá)到 90% 以上的準(zhǔn)確率同時(shí)記錄清楚訓(xùn)練過程中 loss 和 accuracy 的變化曲線能夠解釋每一次調(diào)參操作帶來的影響。這個(gè)目標(biāo)比單純跑通代碼高一個(gè)層次——不只是讓模型動(dòng)起來而是要建立改動(dòng)到結(jié)果的因果鏈條這種能力在真正的工作中比背下來幾個(gè)網(wǎng)絡(luò)結(jié)構(gòu)值錢得多。注意我這里用花卉數(shù)據(jù)集舉例是因?yàn)樗悇e清晰、容易上手。如果你有更貼合自己業(yè)務(wù)的數(shù)據(jù)直接把路徑和標(biāo)簽替換掉即可整套流程完全通用。2. 核心組件拆解CNN 里的每一個(gè)設(shè)計(jì)決策2.1 卷積核大小與通道數(shù)的選擇依據(jù)搭建 CNN 時(shí)第一個(gè)要拍板的是卷積核大小。我見過不少新手直接照搬經(jīng)典網(wǎng)絡(luò)的 3x3 卷積卻不清楚這個(gè)尺寸為什么流行。答案有兩層從感受野的角度看兩個(gè) 3x3 卷積堆疊等效于一個(gè) 5x5 卷積的感受野但參數(shù)更少。計(jì)算一下就知道兩個(gè) 3x3 卷積的參數(shù)是 2 x 3 x 3 18 個(gè)而一個(gè) 5x5 卷積是 5 x 5 25 個(gè)后者多出近 40%。更關(guān)鍵的是兩個(gè) 3x3 中間夾了一次非線性激活網(wǎng)絡(luò)表達(dá)能力反而更強(qiáng)。從工程角度看3x3 是眾多硬件和深度學(xué)習(xí)框架深度優(yōu)化的尺寸計(jì)算效率高。但 3x3 不是唯一答案。輸入圖片很小的場(chǎng)景比如常見的小圖數(shù)據(jù)集那種 32x32 規(guī)格第一層直接用 5x5 或 7x7 卷積可以更快擴(kuò)大感受野減少網(wǎng)絡(luò)層數(shù)輸入圖片很大時(shí)比如 500x500 以上第一層用大卷積核配合步長(zhǎng) 2 做快速降采樣也很常見可以省掉一層池化。通道數(shù)的設(shè)計(jì)我習(xí)慣用一個(gè)從窄到寬再收窄的規(guī)律來理解。淺層用較少通道比如 32、64提取邊緣、顏色等基礎(chǔ)特征中層逐漸加寬128、256提取紋理、局部形狀等組合特征最后通過全局池化或全連接層把特征映射到類別空間。每經(jīng)過一個(gè)降采樣層通道數(shù)翻倍這是一個(gè)經(jīng)典的經(jīng)驗(yàn)法則特征圖尺寸減半通道數(shù)就加倍既控制參數(shù)量又保證信息不丟失。007-2 里的具體配置是輸入圖片統(tǒng)一縮放到 128x128像素值歸一化到 [0,1]。卷積塊 1Conv2d(3, 32, kernel_size3, padding1) BatchNorm ReLU MaxPool2d(2)卷積塊 2Conv2d(32, 64, kernel_size3, padding1) BatchNorm ReLU MaxPool2d(2)卷積塊 3Conv2d(64, 128, kernel_size3, padding1) BatchNorm ReLU MaxPool2d(2)分類頭GlobalAvgPool Dense(128, 5)這個(gè)結(jié)構(gòu)總參數(shù)量不到 50 萬非常輕量但在小型數(shù)據(jù)集上足以跑出不錯(cuò)的準(zhǔn)確率。設(shè)計(jì)它時(shí)我刻意避開了那些動(dòng)輒上千萬參數(shù)的經(jīng)典網(wǎng)絡(luò)因?yàn)閷?duì)數(shù)據(jù)和算力都有限的學(xué)習(xí)項(xiàng)目來說理解架構(gòu)設(shè)計(jì)的思路比復(fù)現(xiàn)一個(gè)大模型更有價(jià)值。2.2 池化層的存在價(jià)值與替代方案很多初學(xué)者把池化看作簡(jiǎn)單的圖片縮小操作這其實(shí)低估了它。池化層有兩個(gè)核心作用降維壓縮特征圖尺寸減少后續(xù)層計(jì)算量同時(shí)成倍擴(kuò)大下一層的感受野。平移不變性池化讓模型對(duì)輸入的小幅平移、局部變化不那么敏感這對(duì)圖像分類很有利因?yàn)樵谡鎸?shí)圖片里目標(biāo)和鏡頭相對(duì)位置總在變。不過我想說清楚一件事池化不是卷積網(wǎng)絡(luò)里必不可少的組件。我在 007-1 手動(dòng)實(shí)現(xiàn)時(shí)發(fā)現(xiàn)帶步長(zhǎng)的卷積完全可以完成降采樣任務(wù)所以現(xiàn)代網(wǎng)絡(luò)里出現(xiàn)了兩種派別。經(jīng)典派是卷積分支后用池化下采樣結(jié)構(gòu)直白容易調(diào)試現(xiàn)代派用 stride2 的卷積替代池化信息保留更完整殘差網(wǎng)絡(luò)里就是這么干的。007-2 中考慮到調(diào)試方便我選擇了經(jīng)典的 MaxPool2d。MaxPool 和 AvgPool 的選擇也有講究。MaxPool 保留區(qū)域內(nèi)最強(qiáng)的響應(yīng)適合中間層提取邊緣紋理這類強(qiáng)特征AvgPool 對(duì)區(qū)域整體求平均適合最后做全局特征聚合。我自己的經(jīng)驗(yàn)是中間層用 MaxPool最后一層用 GlobalAvgPool 替代 Flatten 加 Dense后者的優(yōu)勢(shì)非常明顯——它會(huì)強(qiáng)制讓每個(gè)通道對(duì)應(yīng)一種語義特征同時(shí)參數(shù)量幾乎降為原來的十分之一還天然帶有正則化效果。這個(gè)改動(dòng)在 007-2 里讓驗(yàn)證準(zhǔn)確率穩(wěn)定提升了將近 2 個(gè)百分點(diǎn)而且模型對(duì)輸入尺寸的變化也更魯棒了。2.3 激活函數(shù)與歸一化的組合拳激活函數(shù)的選擇直接決定訓(xùn)練過程的穩(wěn)定性。ReLU 是目前默認(rèn)選項(xiàng)但我在實(shí)踐里遇到過一個(gè)非常典型的問題學(xué)習(xí)率稍微調(diào)大一點(diǎn)網(wǎng)絡(luò)里就有一批神經(jīng)元的輸出永遠(yuǎn)變成 0梯度也永遠(yuǎn)為 0模型徹底死掉一部分表達(dá)能力。這個(gè)現(xiàn)象叫 Dead ReLU本質(zhì)是負(fù)輸入?yún)^(qū)間沒有梯度通路一旦神經(jīng)元落入負(fù)區(qū)就再也回不來。應(yīng)對(duì) Dead ReLU 的常用手段有三個(gè)換用 LeakyReLU 或 ELU給負(fù)半軸一點(diǎn)斜率讓梯度有機(jī)會(huì)回傳。用合理的權(quán)重初始化比如 He 初始化讓卷積輸出的分布不過于集中避免大量神經(jīng)元一開始就落入負(fù)區(qū)??刂瞥跏紝W(xué)習(xí)率不要為了追求快速收斂而設(shè)得太大。007-2 里我最終用了 ReLU 加 He 初始化初始學(xué)習(xí)率設(shè)為 0.001配合余弦退火調(diào)度。這里要特別強(qiáng)調(diào)批歸一化BatchNormalization的放置位置它應(yīng)當(dāng)放在卷積層之后、激活函數(shù)之前而不是很多人以為的激活之后。原因是 BN 的作用是穩(wěn)定卷積輸出的分布讓激活函數(shù)的輸入落在合適區(qū)間如果先過了非線性激活分布已經(jīng)被打亂BN 的效果會(huì)大打折扣訓(xùn)練時(shí)還會(huì)出現(xiàn)奇怪的震蕩。注意BN 在小 batch size 下效果會(huì)明顯變差。如果顯存只允許 batch size 設(shè)為 8 甚至更小我建議改用 GroupNorm或者干脆去掉 BN、靠調(diào)整學(xué)習(xí)率來維持穩(wěn)定。不要盲目迷信 BN它也是有適用條件的。3. 實(shí)操全流程從數(shù)據(jù)到模型的完整落地3.1 數(shù)據(jù)準(zhǔn)備數(shù)據(jù)集劃分與預(yù)處理細(xì)節(jié)寫模型代碼之前數(shù)據(jù)準(zhǔn)備環(huán)節(jié)直接決定后續(xù)調(diào)試的體驗(yàn)。我把處理流程分成四步每一步都有明確的坑要避。第一步是劃分?jǐn)?shù)據(jù)集。按照 8:1:1 的比例切成訓(xùn)練集、驗(yàn)證集、測(cè)試集并且用分層抽樣保證每個(gè)類別的樣本比例在三個(gè)集合里一致。這一步用訓(xùn)練測(cè)試分割工具加 stratify 參數(shù)就能輕松實(shí)現(xiàn)。為什么單獨(dú)留測(cè)試集因?yàn)轵?yàn)證集在調(diào)參過程中被反復(fù)使用模型已經(jīng)間接見過它的信息了如果拿驗(yàn)證集當(dāng)最終成績(jī)會(huì)高估模型的真實(shí)泛化能力。測(cè)試集只能最后用一次這是鐵律。第二步是統(tǒng)一尺寸。我選的是 128x128。這個(gè)尺寸的確定背后有考量太小比如 64x64會(huì)丟失花瓣紋理等關(guān)鍵細(xì)節(jié)太大比如 256x256計(jì)算量會(huì)翻好幾倍。128 對(duì)這個(gè)小數(shù)據(jù)集來說是精度和速度的平衡點(diǎn)訓(xùn)練一輪的時(shí)間也剛好在可接受范圍內(nèi)。第三步是歸一化。把像素值從 [0,255] 縮放到 [0,1]。這個(gè)操作看起來簡(jiǎn)單很多人卻會(huì)漏掉。不歸一化的話輸入數(shù)值范圍過大會(huì)讓損失函數(shù)對(duì)權(quán)重初始化極其敏感訓(xùn)練初期非常容易出現(xiàn) loss 震蕩甚至直接發(fā)散。歸一化之后模型對(duì)初始化的容忍度會(huì)高很多。第四步是數(shù)據(jù)增強(qiáng)。訓(xùn)練集上我做了幾種隨機(jī)變換水平翻轉(zhuǎn)、隨機(jī)旋轉(zhuǎn) 15 度、隨機(jī)裁剪到 112x112 再縮放回 128x128、調(diào)整亮度和對(duì)比度。驗(yàn)證集和測(cè)試集只做縮放和歸一化絕不做增強(qiáng)。增強(qiáng)的目的是讓模型看到更多變體相當(dāng)于免費(fèi)擴(kuò)充數(shù)據(jù)集對(duì)防止過擬合有立竿見影的效果。這里踩過一個(gè)坑增強(qiáng)到底在歸一化之前還是之后做我的結(jié)論是先做幾何變換翻轉(zhuǎn)、旋轉(zhuǎn)、裁剪再做歸一化。因?yàn)閹缀巫儞Q涉及像素插值先歸一化再插值會(huì)產(chǎn)生細(xì)微數(shù)值偏差雖然通常不影響最終精度但養(yǎng)成熟練的先后順序能省掉很多不必要的疑惑。3.2 模型搭建用框架寫出一個(gè)可訓(xùn)練的 CNN下面就是 007-2 里實(shí)際使用的模型代碼結(jié)構(gòu)非常簡(jiǎn)潔核心邏輯全部在這。import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes5): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x))我解釋一下這個(gè)模型里的幾個(gè)關(guān)鍵設(shè)計(jì)點(diǎn)AdaptiveAvgPool2d(1) 是個(gè)很省心的操作。無論前面特征圖的尺寸是多大它都會(huì)輸出 1x1 的向量省去了手動(dòng)計(jì)算全連接層輸入維度的麻煩。如果以后想把輸入分辨率從 128 改成 96模型代碼完全不用動(dòng)。BatchNorm 的位置嚴(yán)格按照卷積之后、激活之前排列這也是前面反復(fù)強(qiáng)調(diào)過的請(qǐng)務(wù)必養(yǎng)成這個(gè)習(xí)慣。最后一層沒有加 Softmax。這是個(gè)非常典型的看著對(duì)其實(shí)是多余的操作。框架里的交叉熵?fù)p失函數(shù)內(nèi)部已經(jīng)包含了 Softmax 和 log 運(yùn)算如果在前向里提前做了 Softmax損失函數(shù)會(huì)再次計(jì)算導(dǎo)致數(shù)值不穩(wěn)定甚至梯度錯(cuò)誤。模型參數(shù)量用sum(p.numel() for p in model.parameters())可以查出來我這個(gè)結(jié)構(gòu)大概在 43 萬左右。對(duì) 3000 張級(jí)別的小數(shù)據(jù)集來說這個(gè)規(guī)模很合適容量夠用又不會(huì)因?yàn)閰?shù)過多而輕松過擬合。3.3 訓(xùn)練配置優(yōu)化器、學(xué)習(xí)率與訓(xùn)練循環(huán)模型搭好之后訓(xùn)練配置是另一塊容易出問題的地方。007-2 的配置如下優(yōu)化器AdamW初始學(xué)習(xí)率 0.001權(quán)重衰減設(shè)為 0.0001。損失函數(shù)交叉熵?fù)p失。批次大小32。訓(xùn)練輪數(shù)50。學(xué)習(xí)率調(diào)度余弦退火總周期 50 輪。為什么選 AdamW 而不選經(jīng)典的 SGD 加動(dòng)量這兩個(gè)的取舍值得說清楚。SGD 加動(dòng)量在超參數(shù)調(diào)好后往往能收斂到更好的結(jié)果但它對(duì)學(xué)習(xí)率太敏感手動(dòng)調(diào)整的成本很高而且訓(xùn)練過程的穩(wěn)定性差一些。AdamW 是 Adam 的改進(jìn)版把權(quán)重衰減從梯度更新里分離出來既保留了 Adam 快速收斂的優(yōu)點(diǎn)又緩解了 L2 正則與 Adam 相互作用導(dǎo)致的過擬合問題。對(duì)于 007-2 這種需要快速驗(yàn)證想法、反復(fù)迭代的場(chǎng)景AdamW 是更省心的選擇。訓(xùn)練循環(huán)的骨架長(zhǎng)這樣for epoch in range(epochs): model.train() train_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) model.eval() val_loss 0.0 val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) val_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) val_correct (preds labels).sum().item() val_total labels.size(0) scheduler.step() train_acc 100.0 * correct / total val_acc 100.0 * val_correct / val_total print(fEpoch {epoch1:02d} | fTrain Loss {train_loss / total:.4f} | Train Acc {train_acc:.2f}% | fVal Loss {val_loss / val_total:.4f} | Val Acc {val_acc:.2f}%)這段代碼有兩個(gè)細(xì)節(jié)是新手的常見盲區(qū)model.train() 和 model.eval() 必須成對(duì)出現(xiàn)。train 模式下批歸一化層會(huì)統(tǒng)計(jì)當(dāng)前 batch 的均值和方差eval 模式下它才使用訓(xùn)練階段積累的全局統(tǒng)計(jì)量。如果漏了 model.eval()驗(yàn)證集的 loss 會(huì)忽高忽低甚至出現(xiàn)驗(yàn)證準(zhǔn)確率像是隨機(jī)猜測(cè)的假象之前的調(diào)試工作全都白費(fèi)。驗(yàn)證階段必須用 torch.no_grad() 包起來。這個(gè)作用有兩個(gè)節(jié)省顯存因?yàn)椴辉俦4嬷虚g計(jì)算圖加快驗(yàn)證速度因?yàn)椴恍枰?jì)算梯度。我還故意把驗(yàn)證集的 loss 除以樣本總數(shù)而不是 batch 數(shù)這樣得到的是每個(gè)樣本的平均損失和訓(xùn)練集對(duì)比起來才公平。3.4 評(píng)估與結(jié)果分析訓(xùn)練 50 輪之后我拿到的曲線大致是這樣的訓(xùn)練準(zhǔn)確率從初始的 60% 左右快速爬升到第 10 輪已經(jīng)接近 92%驗(yàn)證準(zhǔn)確率在第 20 輪左右達(dá)到峰值大概 91%之后訓(xùn)練準(zhǔn)確率還在緩慢上升驗(yàn)證準(zhǔn)確率則穩(wěn)定在 90% 到 92% 之間沒有明顯掉頭。最終測(cè)試集準(zhǔn)確率 91.6%。這個(gè)結(jié)果告訴我兩件事模型沒有明顯過擬合泛化能力符合預(yù)期訓(xùn)練曲線的走勢(shì)整體健康沒有大幅震蕩。如果訓(xùn)練準(zhǔn)確率 97%、驗(yàn)證準(zhǔn)確率只有 88%說明過擬合已經(jīng)出現(xiàn)了需要加數(shù)據(jù)增強(qiáng)、加正則化或者降低模型容量。除準(zhǔn)確率外我還多算了一步混淆矩陣。分析后發(fā)現(xiàn)模型最容易把玫瑰和月季搞混這兩類在視覺上確實(shí)高度相似。這種細(xì)顆粒度的分析在真實(shí)業(yè)務(wù)里非常有用——如果分類錯(cuò)誤的代價(jià)很高你就知道該優(yōu)先收集哪些易混淆類別的數(shù)據(jù)或者給損失函數(shù)里的稀有類別加上權(quán)重。4. 訓(xùn)練調(diào)優(yōu)的實(shí)戰(zhàn)心得與踩坑記錄4.1 過擬合的判斷與應(yīng)對(duì)策略判斷過擬合我從來不只看 loss 曲線而是盯住訓(xùn)練準(zhǔn)確率和驗(yàn)證準(zhǔn)確率的差距。差距超過 5 個(gè)百分點(diǎn)就要開始警惕超過 10 個(gè)點(diǎn)基本可以確診。007-2 的模型差距始終控制在 3 個(gè)點(diǎn)以內(nèi)所以沒有太擔(dān)心。如果確實(shí)過擬合了我的應(yīng)對(duì)策略按優(yōu)先級(jí)排序第一優(yōu)先加強(qiáng)數(shù)據(jù)增強(qiáng)。比如隨機(jī)旋轉(zhuǎn)從 10 度加到 20 度或者引入隨機(jī)遮擋。這個(gè)手段成本最低效果卻常常最好因?yàn)樗谋举|(zhì)是擴(kuò)大訓(xùn)練集的有效多樣性。第二優(yōu)先在全連接層前加一個(gè) Dropout比率 0.3 左右。我見過不少模型靠這一招把驗(yàn)證準(zhǔn)確率拉回 2 到 3 個(gè)點(diǎn)。第三優(yōu)先降低模型容量。比如去掉 128 通道的那個(gè)卷積塊讓模型沒有能力去背下訓(xùn)練集。第四優(yōu)先調(diào)大權(quán)重衰減從 0.0001 試到 0.0005。這里想特別提醒一句數(shù)據(jù)增強(qiáng)不是越狠越好。我貪心過一次把翻轉(zhuǎn)、旋轉(zhuǎn) 30 度、隨機(jī)裁剪、顏色抖動(dòng)、隨機(jī)遮擋全部疊加驗(yàn)證準(zhǔn)確率反而掉了。原因很直白增強(qiáng)過猛會(huì)讓訓(xùn)練樣本的分布偏離真實(shí)數(shù)據(jù)分布模型學(xué)到的特征被增強(qiáng)痕跡帶偏了。正確做法是每次只加一種增強(qiáng)觀察驗(yàn)證集曲線變化再?zèng)Q定要不要繼續(xù)加。4.2 損失不下降的排查清單訓(xùn)練中損失卡著不掉是讓新手最崩潰的場(chǎng)景。這里分享一份我反復(fù)用到的排查清單按順序檢查能省掉大量瞎試的時(shí)間檢查數(shù)據(jù)預(yù)處理歸一化做了沒有標(biāo)簽是不是從 0 開始連續(xù)編號(hào)有沒有混入損壞的圖片文件檢查損失函數(shù)分類任務(wù)用的是交叉熵?fù)p失而不是均方誤差前者才適合離散類別。檢查學(xué)習(xí)率太小了損失下降會(huì)慢得讓你懷疑人生太大了初期會(huì)震蕩然后卡住。我建議做一次學(xué)習(xí)率掃描實(shí)驗(yàn)快速找到合適區(qū)間。檢查模型輸出單獨(dú)跑一個(gè) batch 的前向確認(rèn)輸出形狀是 (batch_size, num_classes)別搞成 (batch_size,) 或者 (num_classes,)。檢查梯度每隔幾個(gè) step 打印參數(shù)梯度的范數(shù)如果始終為 0說明梯度根本沒有回傳問題大概率出在權(quán)重初始化、激活函數(shù)或者模型結(jié)構(gòu)上。我印象最深的一次排錯(cuò)經(jīng)歷是這樣的某個(gè)自定義數(shù)據(jù)類里忘了歸一化結(jié)果損失一直停在 2.3 附近模型輸出的概率幾乎是均勻分布怎么看都不對(duì)。排查了一下午最后才發(fā)現(xiàn)是數(shù)據(jù)值太大數(shù)值計(jì)算把梯度搞崩了。所以檢查數(shù)據(jù)預(yù)處理這條一定要放在第一位它不是走過場(chǎng)。4.3 顯存優(yōu)化與訓(xùn)練速度提升技巧如果顯存不夠我的處理順序是這樣的調(diào)小 batch size同時(shí)把學(xué)習(xí)率也相應(yīng)調(diào)低一點(diǎn)因?yàn)樾?batch 的梯度噪聲更大維持原來的學(xué)習(xí)率容易震蕩。開啟混合精度訓(xùn)練AMP讓部分計(jì)算自動(dòng)切換到半精度顯存占用通常能直接降三分之一左右。檢查代碼里有沒有多余的顯存占用比如驗(yàn)證階段忘了開 torch.no_grad()或者訓(xùn)練循環(huán)里保留了用不到的中間張量。以上都不夠就降低輸入分辨率從 128 降到 96。分辨率降低對(duì)準(zhǔn)確率的影響往往小于 1 個(gè)百分點(diǎn)但顯存節(jié)省非常顯著。訓(xùn)練速度方面一個(gè)容易被忽視的細(xì)節(jié)是數(shù)據(jù)加載的并行度。我剛開始把加載器參數(shù)留成默認(rèn)值 0結(jié)果每個(gè) epoch 的數(shù)據(jù)加載時(shí)間比模型訓(xùn)練時(shí)間還長(zhǎng)。改成并行數(shù)為 4 并且打開內(nèi)存釘住之后整個(gè)訓(xùn)練時(shí)間縮短了將近一半。如果你的機(jī)器核心多還可以繼續(xù)調(diào)大但要注意不同操作系統(tǒng)下并行加載可能和主程序進(jìn)程相關(guān)遇到奇怪報(bào)錯(cuò)就調(diào)回 0 試試。4.4 常見問題速查表現(xiàn)象可能原因排查方向損失一直不降數(shù)據(jù)未歸一化 / 學(xué)習(xí)率不當(dāng) / 標(biāo)簽錯(cuò)位檢查預(yù)處理、損失曲線、梯度范數(shù)訓(xùn)練準(zhǔn)確率高、驗(yàn)證準(zhǔn)確率低過擬合增強(qiáng)數(shù)據(jù)增強(qiáng)、加 Dropout、降低容量驗(yàn)證損失波動(dòng)大忘了 eval 模式 / 批次太小 / 批歸一化失效切換模型模式、增加批次、考慮分組歸一化顯存溢出批次過大 / 未開混合精度 / 多余中間變量調(diào)小批次、開 AMP、檢查代碼某類別準(zhǔn)確率特別低類別不均衡 / 類別特征相似計(jì)算類別權(quán)重、收集更多該類數(shù)據(jù)訓(xùn)練時(shí)間過長(zhǎng)加載并行度為零 / 未用 GPU / 分辨率過高開多進(jìn)程加載、升級(jí)硬件、降低分辨率這張表是我自己調(diào)試時(shí)反復(fù)翻看的總結(jié)里面很多現(xiàn)象其實(shí)是同一個(gè)根因。你可以把表存下來遇到問題時(shí)按行去對(duì)照比從零開始猜要高效得多。5. 從 007-2 到下一個(gè)實(shí)踐的擴(kuò)展思路5.1 把單模型變成可解釋的系統(tǒng)007-2 跑通一個(gè) 91% 準(zhǔn)確率的模型之后我做的第一件事并不是拼命刷準(zhǔn)確率而是回頭研究模型到底學(xué)到了什么。用類激活映射CAM生成熱力圖觀察分類向日葵時(shí)模型關(guān)注的是花盤的哪個(gè)區(qū)域、分類玫瑰時(shí)又聚焦在哪些結(jié)構(gòu)上。這個(gè)步驟讓我對(duì) CNN 的理解又上了一個(gè)臺(tái)階它不是簡(jiǎn)單記住模板而是在提取顏色、紋理、形狀的組合特征。如果你也在做類似的圖像分類項(xiàng)目我強(qiáng)烈建議把可視化當(dāng)成必做項(xiàng)它會(huì)幫你快速驗(yàn)證模型學(xué)到的特征是否合理也能在結(jié)果異常時(shí)提供最直觀的線索。5.2 后續(xù)可以嘗試的三個(gè)方向遷移學(xué)習(xí)把預(yù)訓(xùn)練模型拿來做特征提取只訓(xùn)練自己數(shù)據(jù)的分類頭。在數(shù)據(jù)量只有一兩千張的小數(shù)據(jù)集上遷移學(xué)習(xí)往往能把準(zhǔn)確率從 90% 直接拉到 96% 以上省時(shí)省力是性價(jià)比極高的升級(jí)路徑。目標(biāo)檢測(cè)把這張圖是什么擴(kuò)展為圖里有什么、在哪里用檢測(cè)框架把每朵花的位置和類別都框出來。這個(gè)方向邏輯遞進(jìn)非常自然也是從學(xué)術(shù)練習(xí)走向?qū)嶋H應(yīng)用的常見一步。模型壓縮用剪枝、量化等手段把模型從幾十萬參數(shù)壓到幾萬參數(shù)在準(zhǔn)確率下降可接受的前提下?lián)Q一個(gè)更輕量、更適合部署的版本。對(duì)真實(shí)產(chǎn)品落地來說這一步幾乎是必經(jīng)之路。5.3 一個(gè)值得養(yǎng)成的習(xí)慣實(shí)驗(yàn)記錄最后分享一個(gè)這段時(shí)間讓我受益非常大的習(xí)慣給每次實(shí)驗(yàn)做記錄。我會(huì)把每次改動(dòng)的內(nèi)容、最終準(zhǔn)確率、訓(xùn)練耗時(shí)、顯存占用全部寫進(jìn)一個(gè)簡(jiǎn)單的表格里。這個(gè)習(xí)慣在調(diào)參階段的價(jià)值最明顯——你不用反復(fù)嘗試已經(jīng)試過的配置也能清晰地知道到底哪個(gè)改動(dòng)帶來了真實(shí)的提升。記錄模板通常包含這幾項(xiàng)實(shí)驗(yàn)編號(hào)與日期、模型結(jié)構(gòu)改動(dòng)點(diǎn)、數(shù)據(jù)增強(qiáng)策略、優(yōu)化器與學(xué)習(xí)率等超參數(shù)、最終的訓(xùn)練與驗(yàn)證與測(cè)試準(zhǔn)確率、訓(xùn)練耗時(shí)與顯存占用、備注這次實(shí)驗(yàn)想驗(yàn)證什么假設(shè)。有了這個(gè)表你會(huì)發(fā)現(xiàn)憑感覺調(diào)參和有依據(jù)地調(diào)參差距巨大后者得到的模型更穩(wěn)定也更容易復(fù)現(xiàn)。007-2 只是整個(gè)深度學(xué)習(xí)讀書實(shí)踐系列里的一個(gè)節(jié)點(diǎn)但這個(gè)記錄習(xí)慣會(huì)陪你走完后面所有項(xiàng)目。個(gè)人經(jīng)驗(yàn)角度再說一句深度學(xué)習(xí)教材里的每個(gè)模塊只有親手放到一個(gè)具體項(xiàng)目里跑通一遍才能真正體會(huì)到設(shè)計(jì)的精妙和那些藏在代碼縫隙里的坑。如果你現(xiàn)在正卡在理論階段看不進(jìn)去別再硬啃找一個(gè)像 007-2 這樣的小項(xiàng)目動(dòng)手跑起來等回頭再看書里那些公式你會(huì)體驗(yàn)到一種完全不同的豁然開朗。