戰(zhàn):從作業(yè)到調(diào)參的完整指南)
1. 從一次作業(yè)說(shuō)起為什么CNN成了圖像分類的必修課如果你正在跟著某位老師的機(jī)器學(xué)習(xí)課程做作業(yè)做到第三次作業(yè)發(fā)現(xiàn)題目是“用卷積神經(jīng)網(wǎng)絡(luò)做圖像分類”那你大概率已經(jīng)意識(shí)到一件事前面兩次作業(yè)還在讓你手推線性回歸和邏輯回歸的梯度這次突然就要你搭一個(gè)能識(shí)別圖片的模型了。這個(gè)跨度其實(shí)挺大的但恰恰是這種跨度把很多人從“調(diào)包俠”逼成了真正理解模型結(jié)構(gòu)的人。這份作業(yè)的核心任務(wù)用一句話概括就是給定一批帶標(biāo)簽的小尺寸彩色圖片訓(xùn)練一個(gè)卷積神經(jīng)網(wǎng)絡(luò)讓它學(xué)會(huì)分辨這些圖片屬于哪一類。聽(tīng)起來(lái)簡(jiǎn)單但里面藏著卷積核怎么設(shè)計(jì)、池化層要不要加、數(shù)據(jù)增強(qiáng)怎么做、訓(xùn)練不收斂怎么辦這一連串問(wèn)題。我?guī)н^(guò)不少剛?cè)腴T的朋友做這個(gè)作業(yè)發(fā)現(xiàn)大家卡住的地方高度一致——不是不會(huì)寫代碼而是不知道每一層為什么這么設(shè)計(jì)參數(shù)為什么這么調(diào)。這篇文章就是把我自己踩過(guò)的坑、幫別人排查過(guò)的問(wèn)題以及那些文檔里不會(huì)寫的經(jīng)驗(yàn)完整地梳理一遍。不管你是正在做這份作業(yè)的學(xué)生還是想系統(tǒng)理解CNN實(shí)戰(zhàn)的轉(zhuǎn)行者都能從里面找到可以直接抄作業(yè)的東西。我會(huì)從整體設(shè)計(jì)思路講起然后拆解每一層的實(shí)現(xiàn)細(xì)節(jié)再給出完整的訓(xùn)練流程和參數(shù)選擇依據(jù)最后把常見(jiàn)報(bào)錯(cuò)和排查方法整理成速查表。全程用大白話配合生活化的類比保證你看完能自己動(dòng)手跑通一個(gè)像樣的模型。2. 整體設(shè)計(jì)思路為什么是CNN而不是全連接網(wǎng)絡(luò)2.1 圖像任務(wù)的特殊性決定了網(wǎng)絡(luò)結(jié)構(gòu)先想一個(gè)問(wèn)題為什么圖像分類不用全連接網(wǎng)絡(luò)假設(shè)輸入是一張32x32的彩色圖片展開(kāi)成一維向量就是32×32×33072個(gè)像素值。如果第一層隱藏層有1000個(gè)神經(jīng)元光這一層的參數(shù)量就是3072×1000≈300萬(wàn)。這還只是一層要是堆個(gè)五六層參數(shù)量直接爆炸。更關(guān)鍵的是全連接網(wǎng)絡(luò)把每個(gè)像素都當(dāng)成獨(dú)立特征完全忽略了像素之間的空間關(guān)系——一張貓的圖片你把它的像素打亂順序全連接網(wǎng)絡(luò)照樣能訓(xùn)練但人眼已經(jīng)看不出是貓了。這說(shuō)明全連接網(wǎng)絡(luò)沒(méi)有利用圖像最重要的特性局部相關(guān)性和平移不變性。卷積神經(jīng)網(wǎng)絡(luò)解決的就是這兩個(gè)問(wèn)題。卷積核在圖像上滑動(dòng)每次只看一個(gè)小區(qū)域比如3x3這就是局部感受野同一個(gè)卷積核在整個(gè)圖像上共享權(quán)重這就是參數(shù)共享。一個(gè)3x3的卷積核只有9個(gè)參數(shù)加上偏置是10個(gè)卻能掃描整張圖片參數(shù)量比全連接少了幾個(gè)數(shù)量級(jí)。而且因?yàn)闄?quán)重共享貓?jiān)趫D片左上角還是右下角卷積核都能檢測(cè)到同樣的特征這就是平移不變性的來(lái)源。提示很多人做這個(gè)作業(yè)時(shí)第一反應(yīng)是用全連接網(wǎng)絡(luò)先跑個(gè)baseline。我的建議是別浪費(fèi)時(shí)間直接上CNN。全連接網(wǎng)絡(luò)在這個(gè)任務(wù)上不僅參數(shù)多、訓(xùn)練慢準(zhǔn)確率還很難看容易打擊信心。2.2 作業(yè)場(chǎng)景下的網(wǎng)絡(luò)深度取舍這份作業(yè)用的數(shù)據(jù)集通常是CIFAR-10或者類似的小尺寸圖片集圖片大小32x32共10個(gè)類別。這種規(guī)模的數(shù)據(jù)集網(wǎng)絡(luò)太深容易過(guò)擬合太淺又學(xué)不到足夠特征。我試過(guò)從3層卷積到10層卷積的各種配置最后發(fā)現(xiàn)對(duì)于32x32的輸入4到6個(gè)卷積層是比較合適的區(qū)間。為什么不是越深越好因?yàn)?2x32的圖片經(jīng)過(guò)幾次池化之后空間維度降得很快。比如每經(jīng)過(guò)一次2x2池化長(zhǎng)寬各減半32→16→8→4→2。如果做5次池化最后只剩1x1再深的卷積層已經(jīng)沒(méi)有空間信息可提取了。所以網(wǎng)絡(luò)深度要和輸入尺寸匹配不能盲目堆層數(shù)。另一個(gè)考慮是訓(xùn)練時(shí)間。這個(gè)作業(yè)通常要求在有限的計(jì)算資源下完成比如用免費(fèi)GPU或者個(gè)人筆記本。太深的網(wǎng)絡(luò)訓(xùn)練一輪就要很久調(diào)參周期拉長(zhǎng)不利于快速迭代。我一般建議先用一個(gè)輕量級(jí)的4層卷積網(wǎng)絡(luò)跑通流程確認(rèn)數(shù)據(jù)加載、損失計(jì)算、反向傳播都沒(méi)問(wèn)題再逐步加深或加寬。2.3 批歸一化和Dropout的引入時(shí)機(jī)批歸一化Batch Normalization和Dropout是兩個(gè)經(jīng)常被濫用的技術(shù)。批歸一化的作用是讓每一層的輸入分布更穩(wěn)定加速訓(xùn)練收斂Dropout則是隨機(jī)丟棄一部分神經(jīng)元防止過(guò)擬合。但在作業(yè)場(chǎng)景下這兩個(gè)東西不是越多越好。我的經(jīng)驗(yàn)是批歸一化可以加在每一個(gè)卷積層之后、激活函數(shù)之前基本上沒(méi)有副作用還能讓你用更大的學(xué)習(xí)率。Dropout則要謹(jǐn)慎一般只在全連接層之前加一個(gè)丟棄率設(shè)0.5左右。如果在卷積層后面也加Dropout容易導(dǎo)致欠擬合尤其是網(wǎng)絡(luò)本身就不深的時(shí)候。注意批歸一化在訓(xùn)練和測(cè)試時(shí)的行為不一樣。訓(xùn)練時(shí)用當(dāng)前batch的均值和方差測(cè)試時(shí)用訓(xùn)練階段累積的滑動(dòng)平均值。如果你自己實(shí)現(xiàn)批歸一化別忘了切換模式否則測(cè)試準(zhǔn)確率會(huì)莫名其妙地低一截。3. 核心細(xì)節(jié)解析每一層到底在做什么3.1 卷積層特征提取的基本單元卷積層是CNN的核心但很多人對(duì)它的理解停留在“滑動(dòng)窗口”這個(gè)層面。實(shí)際上卷積核的每個(gè)參數(shù)都是可學(xué)習(xí)的權(quán)重訓(xùn)練過(guò)程就是讓這些權(quán)重逐漸變成能夠檢測(cè)特定特征的濾波器。比如淺層的卷積核可能學(xué)會(huì)檢測(cè)邊緣、顏色斑塊深層的卷積核則能檢測(cè)更復(fù)雜的紋理和形狀。在實(shí)現(xiàn)時(shí)有幾個(gè)參數(shù)需要你手動(dòng)指定卷積核大小、步長(zhǎng)、填充。對(duì)于3x3的卷積核步長(zhǎng)通常設(shè)為1填充設(shè)為1這樣輸入輸出的空間尺寸不變。為什么用3x3而不是5x5或7x7因?yàn)閮蓚€(gè)3x3卷積層的感受野等于一個(gè)5x5卷積層但參數(shù)更少2×918 vs 25非線性更強(qiáng)多了一次激活函數(shù)。所以現(xiàn)代CNN基本都用3x3作為默認(rèn)選擇。代碼實(shí)現(xiàn)上如果用PyTorch一行就能搞定nn.Conv2d(in_channels3, out_channels64, kernel_size3, stride1, padding1)這里的in_channels是輸入通道數(shù)第一層是3RGB后續(xù)層等于上一層的輸出通道數(shù)。out_channels是卷積核的數(shù)量也就是輸出特征圖的通道數(shù)。這個(gè)值通常從32或64開(kāi)始每經(jīng)過(guò)一次池化翻倍因?yàn)榭臻g尺寸減半了通道數(shù)翻倍可以保持信息容量大致不變。3.2 池化層降維與平移不變性池化層的作用是降低空間維度減少計(jì)算量同時(shí)提供一定程度的平移不變性。最常用的是2x2最大池化步長(zhǎng)為2這樣每次操作后長(zhǎng)寬各減半。最大池化取的是每個(gè)2x2區(qū)域的最大值相當(dāng)于保留了最顯著的特征。為什么用最大池化而不是平均池化因?yàn)樽畲蟪鼗A袅藚^(qū)域內(nèi)的最強(qiáng)響應(yīng)對(duì)于紋理和邊緣特征更敏感。平均池化會(huì)平滑掉這些細(xì)節(jié)在淺層網(wǎng)絡(luò)中效果不如最大池化。當(dāng)然在某些深層網(wǎng)絡(luò)中平均池化也有它的用武之地但作業(yè)場(chǎng)景下用最大池化基本不會(huì)錯(cuò)。池化層沒(méi)有可學(xué)習(xí)的參數(shù)所以不會(huì)增加模型復(fù)雜度。但要注意池化會(huì)丟失空間信息如果圖片本身很小比如32x32池化次數(shù)不宜過(guò)多。我一般做3到4次池化把32x32降到4x4或2x2然后接全連接層。3.3 激活函數(shù)ReLU為什么成了默認(rèn)選擇激活函數(shù)給網(wǎng)絡(luò)引入非線性沒(méi)有它再多層疊加也等價(jià)于一層線性變換。早期用Sigmoid和Tanh但它們有個(gè)致命問(wèn)題當(dāng)輸入很大或很小時(shí)梯度接近零導(dǎo)致反向傳播時(shí)梯度消失深層網(wǎng)絡(luò)訓(xùn)練不動(dòng)。ReLURectified Linear Unit解決了這個(gè)問(wèn)題它在正區(qū)間梯度恒為1負(fù)區(qū)間梯度為0計(jì)算簡(jiǎn)單收斂快。但ReLU也有缺點(diǎn)負(fù)區(qū)間的梯度為零可能導(dǎo)致某些神經(jīng)元“死亡”永遠(yuǎn)不被激活。為了解決這個(gè)問(wèn)題后續(xù)出現(xiàn)了Leaky ReLU、ELU等變體。不過(guò)在作業(yè)場(chǎng)景下標(biāo)準(zhǔn)ReLU已經(jīng)足夠好了沒(méi)必要追求最新的激活函數(shù)。我試過(guò)把ReLU換成Leaky ReLU準(zhǔn)確率提升不到0.5%但代碼復(fù)雜度增加了不劃算。實(shí)操心得如果你發(fā)現(xiàn)訓(xùn)練過(guò)程中損失一直不下降先檢查激活函數(shù)是不是用錯(cuò)了。我見(jiàn)過(guò)有人把Sigmoid用在深層卷積網(wǎng)絡(luò)里結(jié)果訓(xùn)練了20輪損失還在2.3左右徘徊換成ReLU后第三輪就降到1.5以下。3.4 全連接層與輸出層從特征到類別經(jīng)過(guò)幾次卷積和池化之后特征圖的空間尺寸已經(jīng)很小了比如4x4通道數(shù)可能達(dá)到256或512。這時(shí)候需要把特征圖展平成一維向量接入全連接層。展平操作就是把每個(gè)通道的每個(gè)位置的值按順序排成一列長(zhǎng)度等于通道數(shù)×高×寬。全連接層通常只有一到兩層第一層的神經(jīng)元數(shù)量可以是512或1024最后一層的神經(jīng)元數(shù)量等于類別數(shù)比如10。最后一層后面接Softmax函數(shù)把輸出轉(zhuǎn)換成概率分布。損失函數(shù)用交叉熵?fù)p失它衡量的是預(yù)測(cè)概率分布和真實(shí)標(biāo)簽的差異。這里有個(gè)細(xì)節(jié)Softmax和交叉熵?fù)p失在數(shù)學(xué)上可以合并PyTorch的CrossEntropyLoss就包含了Softmax所以你在網(wǎng)絡(luò)最后一層不需要手動(dòng)加Softmax直接輸出logits就行。如果加了Softmax再用CrossEntropyLoss相當(dāng)于做了兩次Softmax會(huì)導(dǎo)致梯度計(jì)算錯(cuò)誤訓(xùn)練效果大打折扣。4. 實(shí)操過(guò)程從數(shù)據(jù)加載到模型訓(xùn)練4.1 數(shù)據(jù)預(yù)處理與增強(qiáng)策略數(shù)據(jù)預(yù)處理是很多人容易忽略的一步但它對(duì)最終準(zhǔn)確率的影響可能比網(wǎng)絡(luò)結(jié)構(gòu)還大。對(duì)于彩色圖片最基本的預(yù)處理是歸一化把像素值從0-255縮放到0-1再減去均值、除以標(biāo)準(zhǔn)差。均值和標(biāo)準(zhǔn)差可以按通道計(jì)算比如RGB三個(gè)通道分別算。這樣做的目的是讓輸入分布更接近標(biāo)準(zhǔn)正態(tài)分布加速訓(xùn)練收斂。數(shù)據(jù)增強(qiáng)是防止過(guò)擬合的利器。對(duì)于圖像分類任務(wù)常用的增強(qiáng)手段包括隨機(jī)水平翻轉(zhuǎn)、隨機(jī)裁剪、顏色抖動(dòng)等。但要注意增強(qiáng)手段要和任務(wù)匹配。比如水平翻轉(zhuǎn)對(duì)于貓狗分類是合理的因?yàn)樨埛D(zhuǎn)后還是貓但對(duì)于數(shù)字識(shí)別翻轉(zhuǎn)后的數(shù)字可能就不是合法數(shù)字了這時(shí)候就不能用水平翻轉(zhuǎn)。在作業(yè)場(chǎng)景下我建議至少加上隨機(jī)水平翻轉(zhuǎn)和隨機(jī)裁剪。隨機(jī)裁剪可以從圖片中隨機(jī)截取一個(gè)區(qū)域再縮放到原尺寸相當(dāng)于讓網(wǎng)絡(luò)看到不同視角的同一物體。顏色抖動(dòng)可以輕微調(diào)整亮度、對(duì)比度、飽和度讓網(wǎng)絡(luò)對(duì)光照變化更魯棒。transform_train transforms.Compose([ transforms.RandomHorizontalFlip(), transforms.RandomCrop(32, padding4), transforms.ToTensor(), transforms.Normalize(mean[0.491, 0.482, 0.447], std[0.247, 0.243, 0.261]) ])這里的mean和std是CIFAR-10數(shù)據(jù)集的統(tǒng)計(jì)值如果你用的是其他數(shù)據(jù)集需要自己算一下。計(jì)算方法是把所有訓(xùn)練圖片的像素值加起來(lái)求平均再求標(biāo)準(zhǔn)差。4.2 網(wǎng)絡(luò)結(jié)構(gòu)的具體搭建基于前面的分析我給出一個(gè)在作業(yè)場(chǎng)景下表現(xiàn)穩(wěn)定的網(wǎng)絡(luò)結(jié)構(gòu)。這個(gè)結(jié)構(gòu)有4個(gè)卷積塊每個(gè)塊包含兩個(gè)3x3卷積層、批歸一化、ReLU激活然后接一個(gè)2x2最大池化。通道數(shù)從64開(kāi)始每次池化后翻倍最后接一個(gè)全連接層輸出10個(gè)類別。class CNN(nn.Module): def __init__(self, num_classes10): super(CNN, self).__init__() self.block1 nn.Sequential( nn.Conv2d(3, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.Conv2d(64, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2) ) self.block2 nn.Sequential( nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.Conv2d(128, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2) ) self.block3 nn.Sequential( nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(), nn.Conv2d(256, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d(2) ) self.block4 nn.Sequential( nn.Conv2d(256, 512, 3, padding1), nn.BatchNorm2d(512), nn.ReLU(), nn.Conv2d(512, 512, 3, padding1), nn.BatchNorm2d(512), nn.ReLU(), nn.MaxPool2d(2) ) self.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(512 * 2 * 2, num_classes) ) def forward(self, x): x self.block1(x) x self.block2(x) x self.block3(x) x self.block4(x) x x.view(x.size(0), -1) x self.classifier(x) return x這個(gè)網(wǎng)絡(luò)大約有1100萬(wàn)個(gè)參數(shù)在CIFAR-10上訓(xùn)練50輪左右可以達(dá)到90%以上的測(cè)試準(zhǔn)確率。如果你覺(jué)得訓(xùn)練太慢可以把每個(gè)塊的第二個(gè)卷積層去掉參數(shù)量減半準(zhǔn)確率大概降2到3個(gè)百分點(diǎn)。4.3 訓(xùn)練循環(huán)與超參數(shù)選擇訓(xùn)練循環(huán)的代碼框架很固定前向傳播、計(jì)算損失、反向傳播、更新參數(shù)。但超參數(shù)的選擇需要一些經(jīng)驗(yàn)。我給出一個(gè)經(jīng)過(guò)驗(yàn)證的配置超參數(shù)推薦值說(shuō)明優(yōu)化器SGD Momentum動(dòng)量0.9比Adam泛化更好初始學(xué)習(xí)率0.1配合批歸一化使用學(xué)習(xí)率調(diào)度Cosine Annealing從0.1余弦衰減到0批大小128顯存不夠可以降到64訓(xùn)練輪數(shù)50通常30輪后準(zhǔn)確率趨于穩(wěn)定權(quán)重衰減5e-4防止過(guò)擬合為什么用SGD而不是AdamAdam收斂快但最終泛化性能往往不如調(diào)好學(xué)習(xí)率的SGD。在作業(yè)場(chǎng)景下SGD配合余弦退火學(xué)習(xí)率調(diào)度能穩(wěn)定達(dá)到較高的準(zhǔn)確率。學(xué)習(xí)率從0.1開(kāi)始是因?yàn)榕鷼w一化讓網(wǎng)絡(luò)對(duì)學(xué)習(xí)率不那么敏感0.1是一個(gè)比較激進(jìn)的起點(diǎn)配合余弦退火可以在后期精細(xì)調(diào)整。optimizer optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay5e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) criterion nn.CrossEntropyLoss()訓(xùn)練過(guò)程中要監(jiān)控訓(xùn)練損失和驗(yàn)證準(zhǔn)確率。如果訓(xùn)練損失持續(xù)下降但驗(yàn)證準(zhǔn)確率停滯甚至下降說(shuō)明過(guò)擬合了可以增加Dropout率或加強(qiáng)數(shù)據(jù)增強(qiáng)。如果訓(xùn)練損失都不下降說(shuō)明欠擬合需要增加網(wǎng)絡(luò)容量或提高學(xué)習(xí)率。實(shí)操心得我習(xí)慣每訓(xùn)練5輪就保存一次模型權(quán)重同時(shí)記錄驗(yàn)證準(zhǔn)確率。這樣即使訓(xùn)練中途出現(xiàn)意外中斷也能從最近的檢查點(diǎn)恢復(fù)。另外驗(yàn)證集不要用測(cè)試集代替否則你調(diào)參調(diào)到最后測(cè)試集就被“偷看”了最終評(píng)估結(jié)果不可信。4.4 學(xué)習(xí)率調(diào)度的具體實(shí)現(xiàn)與效果對(duì)比學(xué)習(xí)率調(diào)度是提升最終準(zhǔn)確率的關(guān)鍵技巧但很多人只是設(shè)一個(gè)固定學(xué)習(xí)率從頭跑到尾。我做過(guò)對(duì)比實(shí)驗(yàn)固定學(xué)習(xí)率0.01訓(xùn)練50輪測(cè)試準(zhǔn)確率約87%用余弦退火從0.1降到0測(cè)試準(zhǔn)確率約92%。差距非常明顯。余弦退火的原理是讓學(xué)習(xí)率按照余弦曲線從初始值平滑降到零。前期學(xué)習(xí)率大快速探索參數(shù)空間后期學(xué)習(xí)率小精細(xì)調(diào)整。實(shí)現(xiàn)起來(lái)很簡(jiǎn)單PyTorch的CosineAnnealingLR就是干這個(gè)的。你只需要在每個(gè)epoch結(jié)束后調(diào)用scheduler.step()。還有一種更激進(jìn)的策略叫熱重啟Warm Restarts就是讓學(xué)習(xí)率周期性回升再下降。這種方法在某些任務(wù)上能跳出局部最優(yōu)但在作業(yè)場(chǎng)景下不一定比余弦退火好而且調(diào)參更麻煩。我的建議是先用余弦退火穩(wěn)定可靠。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄5.1 訓(xùn)練不收斂的排查路徑訓(xùn)練不收斂是新手最常遇到的問(wèn)題表現(xiàn)是損失值居高不下或者震蕩劇烈。排查思路應(yīng)該從數(shù)據(jù)到模型逐層檢查。先看數(shù)據(jù)有沒(méi)有問(wèn)題圖片是否正常加載、標(biāo)簽是否對(duì)應(yīng)正確、歸一化是否做了。我見(jiàn)過(guò)有人把標(biāo)簽和圖片搞混了訓(xùn)練了半天準(zhǔn)確率一直在10%左右隨機(jī)猜測(cè)水平。如果數(shù)據(jù)沒(méi)問(wèn)題再看模型初始化。PyTorch默認(rèn)用Kaiming初始化一般不需要改。但如果你自己寫了初始化邏輯檢查一下有沒(méi)有把權(quán)重初始化成全零或者過(guò)大的值。全零初始化會(huì)導(dǎo)致所有神經(jīng)元輸出相同反向傳播時(shí)梯度也相同網(wǎng)絡(luò)永遠(yuǎn)學(xué)不到東西。最后檢查學(xué)習(xí)率。學(xué)習(xí)率太大會(huì)導(dǎo)致?lián)p失震蕩甚至發(fā)散太小則收斂極慢。可以做一個(gè)簡(jiǎn)單的學(xué)習(xí)率掃描從1e-5到1每個(gè)量級(jí)跑幾個(gè)batch看哪個(gè)學(xué)習(xí)率下?lián)p失下降最快。這個(gè)技巧叫LR Range Test能幫你快速找到合適的學(xué)習(xí)率區(qū)間。5.2 過(guò)擬合與欠擬合的判斷與應(yīng)對(duì)過(guò)擬合和欠擬合是兩種相反的問(wèn)題判斷方法很簡(jiǎn)單看訓(xùn)練準(zhǔn)確率和驗(yàn)證準(zhǔn)確率的差距。如果訓(xùn)練準(zhǔn)確率很高比如99%但驗(yàn)證準(zhǔn)確率低很多比如80%就是過(guò)擬合如果兩者都低比如60%就是欠擬合。過(guò)擬合的應(yīng)對(duì)手段包括增加數(shù)據(jù)增強(qiáng)、提高Dropout率、增加權(quán)重衰減、減少網(wǎng)絡(luò)參數(shù)量。我一般先加數(shù)據(jù)增強(qiáng)因?yàn)檫@是最根本的解決辦法——讓網(wǎng)絡(luò)看到更多樣的數(shù)據(jù)。如果增強(qiáng)已經(jīng)很強(qiáng)了還過(guò)擬合再考慮加Dropout或減參數(shù)量。欠擬合的應(yīng)對(duì)手段包括增加網(wǎng)絡(luò)深度或?qū)挾?、降低Dropout率、提高學(xué)習(xí)率、訓(xùn)練更長(zhǎng)時(shí)間。有時(shí)候欠擬合是因?yàn)榕鷼w一化沒(méi)加對(duì)位置比如加在了激活函數(shù)之后效果會(huì)打折扣。正確的順序是卷積→批歸一化→激活函數(shù)。5.3 顯存不足與訓(xùn)練速度優(yōu)化顯存不足是作業(yè)場(chǎng)景下的常見(jiàn)問(wèn)題尤其是用免費(fèi)GPU的時(shí)候。解決辦法有幾個(gè)減小批大小、用梯度累積模擬大批量、用混合精度訓(xùn)練、減少網(wǎng)絡(luò)參數(shù)量。批大小從128降到64顯存占用大概減半但訓(xùn)練速度會(huì)慢一些因?yàn)镚PU并行度降低了。梯度累積是個(gè)好技巧用小的批大小跑多次前向傳播把梯度累加起來(lái)再一次性更新參數(shù)。這樣等效于大批量訓(xùn)練但顯存占用不變。實(shí)現(xiàn)起來(lái)就是在每次反向傳播后不立即更新參數(shù)而是等累積了N次之后再更新。accumulation_steps 4 for i, (inputs, labels) in enumerate(train_loader): outputs model(inputs) loss criterion(outputs, labels) / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()混合精度訓(xùn)練是另一個(gè)加速技巧用半精度浮點(diǎn)數(shù)float16做前向和反向傳播用單精度f(wàn)loat32保存模型權(quán)重。PyTorch的amp模塊可以自動(dòng)處理代碼改動(dòng)很小訓(xùn)練速度能提升30%到50%。5.4 常見(jiàn)報(bào)錯(cuò)速查表報(bào)錯(cuò)信息可能原因解決方法RuntimeError: size mismatch全連接層輸入維度算錯(cuò)打印展平后的維度調(diào)整Linear輸入CUDA out of memory顯存不足減小批大小用梯度累積Loss is nan學(xué)習(xí)率太大或數(shù)據(jù)有異常值降低學(xué)習(xí)率檢查數(shù)據(jù)歸一化Accuracy stuck at 10%標(biāo)簽錯(cuò)亂或模型輸出未接Softmax檢查標(biāo)簽對(duì)應(yīng)關(guān)系確認(rèn)損失函數(shù)Validation accuracy fluctuates批歸一化統(tǒng)計(jì)量不穩(wěn)定增大批大小或改用GroupNorm注意如果你在測(cè)試時(shí)發(fā)現(xiàn)準(zhǔn)確率比驗(yàn)證時(shí)低很多檢查一下模型是否處于eval模式。PyTorch中要用model.eval()切換否則Dropout和批歸一化會(huì)繼續(xù)用訓(xùn)練模式導(dǎo)致結(jié)果不穩(wěn)定。6. 從作業(yè)到實(shí)戰(zhàn)可以繼續(xù)深挖的方向做完這份作業(yè)你其實(shí)已經(jīng)掌握了CNN的基本功。但如果想繼續(xù)深入有幾個(gè)方向值得探索。第一個(gè)是殘差連接Residual Connection它讓網(wǎng)絡(luò)可以做得非常深而不退化ResNet就是靠這個(gè)在ImageNet上拿了冠軍。實(shí)現(xiàn)起來(lái)就是在卷積塊之間加一條跳躍連接把輸入直接加到輸出上。第二個(gè)是注意力機(jī)制比如SE模塊Squeeze-and-Excitation它讓網(wǎng)絡(luò)自動(dòng)學(xué)習(xí)每個(gè)通道的重要性權(quán)重。加在卷積塊后面參數(shù)量增加很少但準(zhǔn)確率能提升1到2個(gè)百分點(diǎn)。第三個(gè)是遷移學(xué)習(xí)用在大數(shù)據(jù)集上預(yù)訓(xùn)練好的模型在自己的小數(shù)據(jù)集上微調(diào)。這在數(shù)據(jù)量不足時(shí)特別有效通常只需要訓(xùn)練最后幾層就能達(dá)到不錯(cuò)的效果。我自己在做完這個(gè)作業(yè)之后花了一個(gè)周末把ResNet-18在CIFAR-10上復(fù)現(xiàn)了一遍準(zhǔn)確率從92%提升到95%。過(guò)程并不復(fù)雜主要是把基本的卷積塊換成殘差塊再調(diào)整一下學(xué)習(xí)率調(diào)度。如果你也想試試建議先從小的殘差塊開(kāi)始比如兩個(gè)3x3卷積加一個(gè)跳躍連接跑通了再加深。最后分享一個(gè)我踩過(guò)的坑有一次我為了追求更高的準(zhǔn)確率把網(wǎng)絡(luò)加深到10層結(jié)果訓(xùn)練了100輪準(zhǔn)確率反而下降了。后來(lái)發(fā)現(xiàn)是梯度消失導(dǎo)致的加了殘差連接才解決。所以網(wǎng)絡(luò)深度不是越深越好關(guān)鍵是要有讓梯度順暢傳播的機(jī)制。這個(gè)教訓(xùn)讓我明白結(jié)構(gòu)設(shè)計(jì)比盲目堆層數(shù)重要得多。