絡(luò)原理與PyTorch實(shí)現(xiàn))
1. 從零手寫ResNet18為什么我不建議你直接調(diào)庫(kù)剛?cè)腴T深度學(xué)習(xí)那會(huì)兒我總覺得調(diào)包是理所當(dāng)然的事。torchvision.models.resnet18(pretrainedTrue)一行代碼就能拿到一個(gè)現(xiàn)成的殘差網(wǎng)絡(luò)何必自己費(fèi)勁去寫直到有一次我需要把ResNet18的BasicBlock改成帶注意力機(jī)制的結(jié)構(gòu)才發(fā)現(xiàn)如果連殘差塊內(nèi)部長(zhǎng)什么樣都說(shuō)不清楚根本無(wú)從下手。從那次之后我就養(yǎng)成了一個(gè)習(xí)慣任何一個(gè)經(jīng)典網(wǎng)絡(luò)我都要自己手寫一遍不追求性能超越官方實(shí)現(xiàn)只求把每一層的來(lái)龍去脈搞清楚。這篇文章要聊的就是用PyTorch從零實(shí)現(xiàn)ResNet18的完整過(guò)程。不是簡(jiǎn)單地把官方代碼復(fù)制粘貼過(guò)來(lái)而是把每個(gè)設(shè)計(jì)決策背后的邏輯講透——為什么殘差塊要設(shè)計(jì)成兩層卷積而不是一層shortcut連接什么時(shí)候需要做下采樣BatchNorm放在卷積前面還是后面這些細(xì)節(jié)官方代碼不會(huì)告訴你但恰恰是面試和實(shí)際項(xiàng)目中真正拉開差距的地方。適合誰(shuí)來(lái)讀如果你已經(jīng)了解卷積神經(jīng)網(wǎng)絡(luò)的基本概念知道卷積層、池化層、全連接層是干什么的但還沒有自己完整搭建過(guò)一個(gè)深層網(wǎng)絡(luò)那這篇文章就是為你準(zhǔn)備的。如果你已經(jīng)用過(guò)ResNet但說(shuō)不清楚它為什么能解決退化問題這篇文章也能幫你把知識(shí)盲區(qū)補(bǔ)上。我會(huì)盡量用大白話把原理講清楚同時(shí)給出可以直接運(yùn)行的完整代碼你跟著敲一遍對(duì)殘差網(wǎng)絡(luò)的理解會(huì)上一個(gè)臺(tái)階。2. 殘差網(wǎng)絡(luò)到底解決了什么問題2.1 深層網(wǎng)絡(luò)的退化困境在ResNet出現(xiàn)之前深度學(xué)習(xí)領(lǐng)域有一個(gè)讓人困惑的現(xiàn)象網(wǎng)絡(luò)層數(shù)增加到一定程度后訓(xùn)練誤差反而上升了。注意這里說(shuō)的不是過(guò)擬合導(dǎo)致的驗(yàn)證誤差上升而是訓(xùn)練集上的誤差本身就變大了。這意味著模型連訓(xùn)練數(shù)據(jù)都擬合不好顯然不是過(guò)擬合的問題。這個(gè)問題被稱為退化問題。按常理推斷一個(gè)56層的網(wǎng)絡(luò)應(yīng)該至少能表現(xiàn)得和20層網(wǎng)絡(luò)一樣好——因?yàn)槎喑鰜?lái)的36層哪怕什么都不做只是恒等映射也不應(yīng)該讓效果變差。但實(shí)際訓(xùn)練中讓多層非線性變換去逼近一個(gè)恒等映射難度遠(yuǎn)超想象。梯度在反向傳播過(guò)程中經(jīng)過(guò)層層衰減到了淺層幾乎消失殆盡參數(shù)更新變得極其緩慢。我做過(guò)一個(gè)簡(jiǎn)單的對(duì)比實(shí)驗(yàn)用同樣的數(shù)據(jù)集分別訓(xùn)練一個(gè)20層的普通卷積網(wǎng)絡(luò)和一個(gè)56層的普通卷積網(wǎng)絡(luò)前者訓(xùn)練loss能降到0.1以下后者在0.5左右就震蕩不降了。這不是因?yàn)?6層網(wǎng)絡(luò)容量不夠恰恰相反是優(yōu)化難度太大了。2.2 殘差學(xué)習(xí)的核心思想ResNet的解決方案非常優(yōu)雅既然讓網(wǎng)絡(luò)學(xué)習(xí)恒等映射很難那就換個(gè)思路讓網(wǎng)絡(luò)學(xué)習(xí)殘差。具體來(lái)說(shuō)假設(shè)我們希望某幾層學(xué)到的映射是H(x)傳統(tǒng)做法是讓這幾層直接擬合H(x)。ResNet的做法是讓這幾層擬合F(x) H(x) - x然后最終的輸出變成F(x) x。這個(gè)改動(dòng)的精妙之處在于如果最優(yōu)的映射就是恒等映射那么網(wǎng)絡(luò)只需要把F(x)的權(quán)重全部推向零就行了這比讓一堆非線性層去逼近恒等映射容易得多。用生活化的類比來(lái)說(shuō)傳統(tǒng)網(wǎng)絡(luò)像是讓你從零開始畫一幅畫而殘差網(wǎng)絡(luò)像是給你一張底稿你只需要在上面做修改。修改到什么都不改就是恒等映射這顯然更容易。從梯度傳播的角度看殘差連接提供了一條“高速公路”。反向傳播時(shí)梯度可以通過(guò)shortcut連接直接傳到淺層不需要經(jīng)過(guò)中間那些卷積層的層層衰減。這就是為什么ResNet可以訓(xùn)練到上百層甚至上千層而普通網(wǎng)絡(luò)幾十層就訓(xùn)不動(dòng)了。2.3 ResNet18的網(wǎng)絡(luò)結(jié)構(gòu)總覽ResNet18是ResNet家族中最淺的成員之一18這個(gè)數(shù)字指的是網(wǎng)絡(luò)中帶有權(quán)重的層數(shù)——包括卷積層和全連接層不包括池化層和BatchNorm層。具體來(lái)說(shuō)它包含1個(gè)7x7的初始卷積層、8個(gè)BasicBlock每個(gè)Block含2個(gè)3x3卷積層共16個(gè)卷積層、以及1個(gè)全連接層加起來(lái)正好18層。整個(gè)網(wǎng)絡(luò)可以分成五個(gè)階段初始卷積階段、四個(gè)殘差階段。初始階段用一個(gè)7x7的大卷積核快速降低特征圖尺寸然后每個(gè)殘差階段由若干個(gè)BasicBlock組成特征圖的空間尺寸逐階段減半通道數(shù)逐階段翻倍。最后通過(guò)全局平均池化和全連接層輸出分類結(jié)果。這個(gè)結(jié)構(gòu)設(shè)計(jì)有幾個(gè)關(guān)鍵考量。7x7卷積核雖然參數(shù)量大但在網(wǎng)絡(luò)最前面輸入通道只有3計(jì)算量其實(shí)可控而且大感受野有助于捕捉圖像的全局信息。四個(gè)階段的通道數(shù)分別是64、128、256、512每次翻倍是為了在空間信息減少的同時(shí)增加通道容量保持信息的總體表達(dá)能力。全局平均池化替代了傳統(tǒng)的多層全連接大幅減少了參數(shù)量也降低了過(guò)擬合風(fēng)險(xiǎn)。3. 手寫ResNet18的核心組件拆解3.1 BasicBlock的設(shè)計(jì)細(xì)節(jié)BasicBlock是ResNet18的基本構(gòu)建單元結(jié)構(gòu)并不復(fù)雜但有幾個(gè)容易踩坑的地方。它的主路徑是3x3卷積 → BatchNorm → ReLU → 3x3卷積 → BatchNorm然后在輸出處與shortcut相加再過(guò)一次ReLU。第一個(gè)坑是卷積的padding。3x3卷積要保持特征圖尺寸不變padding必須設(shè)為1。這個(gè)不用死記公式是輸出尺寸 (輸入尺寸 2×padding - kernel_size) / stride 1。代入kernel_size3、stride1、padding1輸出尺寸等于輸入尺寸。如果padding設(shè)成0每經(jīng)過(guò)一個(gè)卷積特征圖就縮小2網(wǎng)絡(luò)還沒到深處就沒了。第二個(gè)坑是BatchNorm的位置。標(biāo)準(zhǔn)做法是卷積之后、ReLU之前。BatchNorm放在卷積后面可以對(duì)卷積輸出做歸一化讓數(shù)據(jù)分布更穩(wěn)定。如果放在ReLU后面被ReLU截?cái)嗟呢?fù)值部分就無(wú)法被歸一化修正了。另外BatchNorm的momentum參數(shù)默認(rèn)是0.1但在小批量訓(xùn)練時(shí)可能需要調(diào)小這個(gè)后面會(huì)細(xì)說(shuō)。第三個(gè)坑是shortcut的處理。當(dāng)輸入輸出通道數(shù)相同且特征圖尺寸不變時(shí)shortcut可以直接用恒等映射。但當(dāng)通道數(shù)需要翻倍或者特征圖需要減半時(shí)shortcut必須做一個(gè)1x1卷積來(lái)匹配維度同時(shí)用stride2來(lái)同步降采樣。這個(gè)1x1卷積后面也要接BatchNorm但不需要ReLU。import torch import torch.nn as nn class BasicBlock(nn.Module): expansion 1 def __init__(self, in_channels, out_channels, stride1): super(BasicBlock, self).__init__() self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.shortcut nn.Sequential() if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels) ) def forward(self, x): out self.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out self.shortcut(x) out self.relu(out) return out注意卷積層都設(shè)置了biasFalse因?yàn)楹竺婢o跟著BatchNormBatchNorm的beta參數(shù)會(huì)起到偏置的作用再加一個(gè)卷積偏置是冗余的只會(huì)浪費(fèi)參數(shù)。3.2 下采樣與通道匹配的邏輯什么時(shí)候需要給shortcut加卷積判斷條件就兩個(gè)stride不等于1或者輸入通道數(shù)不等于輸出通道數(shù)。在ResNet18中每個(gè)殘差階段的第一塊需要下采樣stride設(shè)為2同時(shí)通道數(shù)翻倍。后面的塊保持stride1、通道數(shù)不變shortcut就是純恒等映射。這里有一個(gè)細(xì)節(jié)值得展開為什么下采樣用stride2的1x1卷積而不是池化因?yàn)槌鼗僮鲿?huì)丟失信息而且池化后的特征圖與主路徑經(jīng)過(guò)stride2卷積后的特征圖在空間位置上可能對(duì)不齊。用1x1卷積配合stride2既能完成降采樣又能學(xué)習(xí)到一個(gè)線性變換來(lái)匹配通道數(shù)信息損失更小。還有一個(gè)容易忽略的點(diǎn)主路徑的第一個(gè)3x3卷積也用了stride2這樣主路徑和shortcut路徑的空間降采樣是同步的相加時(shí)特征圖尺寸才能對(duì)齊。如果主路徑降采樣了而shortcut沒降相加就會(huì)報(bào)維度錯(cuò)誤。3.3 網(wǎng)絡(luò)整體架構(gòu)的搭建有了BasicBlock搭建ResNet18就是按部就班的事了。先定義一個(gè)_make_layer方法負(fù)責(zé)堆疊指定數(shù)量的BasicBlock然后按照[2, 2, 2, 2]的配置構(gòu)建四個(gè)階段。class ResNet18(nn.Module): def __init__(self, num_classes1000): super(ResNet18, self).__init__() self.in_channels 64 self.conv1 nn.Conv2d(3, 64, kernel_size7, stride2, padding3, biasFalse) self.bn1 nn.BatchNorm2d(64) self.relu nn.ReLU(inplaceTrue) self.maxpool nn.MaxPool2d(kernel_size3, stride2, padding1) self.layer1 self._make_layer(64, 2, stride1) self.layer2 self._make_layer(128, 2, stride2) self.layer3 self._make_layer(256, 2, stride2) self.layer4 self._make_layer(512, 2, stride2) self.avgpool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(512, num_classes) def _make_layer(self, out_channels, num_blocks, stride): strides [stride] [1] * (num_blocks - 1) layers [] for s in strides: layers.append(BasicBlock(self.in_channels, out_channels, s)) self.in_channels out_channels return nn.Sequential(*layers) def forward(self, x): x self.relu(self.bn1(self.conv1(x))) x self.maxpool(x) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) x self.avgpool(x) x torch.flatten(x, 1) x self.fc(x) return x初始卷積的padding設(shè)為3因?yàn)閗ernel_size7、stride2代入公式(224 6 - 7) / 2 1 112正好把224x224的輸入降到112x112。接著maxpool再降一半到56x56然后四個(gè)殘差階段分別輸出56、28、14、7的空間尺寸。這個(gè)尺寸變化鏈條是固定的輸入不是224x224的話最后全局平均池化之前會(huì)得到不同的尺寸但AdaptiveAvgPool2d會(huì)自動(dòng)適配所以網(wǎng)絡(luò)對(duì)輸入尺寸有一定靈活性。4. 訓(xùn)練配置與實(shí)操全流程4.1 數(shù)據(jù)準(zhǔn)備與增強(qiáng)策略ResNet18原論文是在ImageNet上訓(xùn)練的輸入尺寸224x224。如果你用自己的數(shù)據(jù)集不一定非要縮放到224但建議至少保持128以上否則經(jīng)過(guò)五次降采樣后特征圖太小信息損失嚴(yán)重。數(shù)據(jù)增強(qiáng)方面訓(xùn)練階段我通常用隨機(jī)裁剪加隨機(jī)水平翻轉(zhuǎn)。隨機(jī)裁剪的scale范圍設(shè)在0.08到1.0之間ratio在3/4到4/3之間這是比較通用的配置。顏色抖動(dòng)可以加但強(qiáng)度不要太大否則可能破壞圖像的關(guān)鍵特征。驗(yàn)證階段只做中心裁剪或者直接resize不做隨機(jī)變換。from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.08, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])Normalize的均值和標(biāo)準(zhǔn)差是ImageNet的統(tǒng)計(jì)值如果你在自己的數(shù)據(jù)集上訓(xùn)練建議先算一下自己數(shù)據(jù)集的均值和方差替換掉這兩個(gè)值。這個(gè)操作看起來(lái)不起眼但對(duì)訓(xùn)練穩(wěn)定性的影響不小。4.2 學(xué)習(xí)率與優(yōu)化器的選擇ResNet18的訓(xùn)練我一般用SGD加動(dòng)量momentum0.9weight_decay1e-4。學(xué)習(xí)率初始值設(shè)0.1配合余弦退火或者階梯下降。為什么不用AdamAdam在訓(xùn)練初期收斂快但在ResNet這類結(jié)構(gòu)上最終精度往往不如調(diào)好的SGD。SGD的噪聲更大反而有助于找到更平坦的極小值泛化性能更好。學(xué)習(xí)率調(diào)度我用余弦退火比較多公式是lr lr_min 0.5 × (lr_max - lr_min) × (1 cos(π × epoch / total_epochs))。這個(gè)調(diào)度方式在訓(xùn)練后期學(xué)習(xí)率下降得很平滑不會(huì)像階梯下降那樣突然跳變導(dǎo)致loss震蕩。optimizer torch.optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max200, eta_min1e-6)Batch size方面如果有條件用大batch比如256或512訓(xùn)練會(huì)更穩(wěn)定。但大batch通常需要配合更大的學(xué)習(xí)率有一個(gè)經(jīng)驗(yàn)公式是lr 0.1 × batch_size / 256。如果顯存不夠只能用32或64的小batch學(xué)習(xí)率要相應(yīng)調(diào)小同時(shí)BatchNorm的momentum可以適當(dāng)調(diào)低到0.01讓統(tǒng)計(jì)量更新更平滑。4.3 訓(xùn)練循環(huán)與關(guān)鍵監(jiān)控指標(biāo)訓(xùn)練循環(huán)本身不復(fù)雜但有幾個(gè)監(jiān)控指標(biāo)必須盯著。第一個(gè)是訓(xùn)練loss和驗(yàn)證loss的走勢(shì)如果訓(xùn)練loss持續(xù)下降但驗(yàn)證loss開始上升說(shuō)明過(guò)擬合了需要加正則化或者早停。第二個(gè)是學(xué)習(xí)率的變化曲線確保調(diào)度器按預(yù)期工作。第三個(gè)是梯度范數(shù)如果梯度范數(shù)突然變得很大可能是遇到了梯度爆炸需要檢查數(shù)據(jù)中是否有異常樣本。def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for inputs, labels in dataloader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * inputs.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_acc驗(yàn)證階段記得加torch.no_grad()不然會(huì)白白消耗顯存。另外如果用了BatchNorm驗(yàn)證時(shí)的batch size不能太小至少要有8以上否則統(tǒng)計(jì)量估計(jì)不準(zhǔn)驗(yàn)證精度會(huì)異常偏低。這個(gè)問題我踩過(guò)坑當(dāng)時(shí)驗(yàn)證集batch size設(shè)成1結(jié)果驗(yàn)證精度比訓(xùn)練精度低了十幾個(gè)百分點(diǎn)排查了半天才發(fā)現(xiàn)是BatchNorm的鍋。4.4 模型保存與恢復(fù)訓(xùn)練保存模型時(shí)我習(xí)慣同時(shí)保存模型參數(shù)、優(yōu)化器狀態(tài)、當(dāng)前epoch和學(xué)習(xí)率。這樣萬(wàn)一訓(xùn)練中斷可以無(wú)縫恢復(fù)不用從頭再來(lái)。只保存state_dict而不是整個(gè)模型對(duì)象是因?yàn)閟tate_dict更輕量也不依賴具體的類定義。checkpoint { epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), scheduler_state_dict: scheduler.state_dict(), best_acc: best_acc } torch.save(checkpoint, fresnet18_epoch{epoch}.pth)恢復(fù)訓(xùn)練時(shí)先創(chuàng)建模型和優(yōu)化器實(shí)例再加載state_dict。注意scheduler的狀態(tài)也要恢復(fù)否則學(xué)習(xí)率會(huì)從頭開始影響訓(xùn)練連續(xù)性。5. 常見問題與排查技巧實(shí)錄5.1 訓(xùn)練不收斂的排查思路訓(xùn)練不收斂是新手最常遇到的問題表現(xiàn)是loss居高不下或者震蕩劇烈。排查順序我一般是這樣先檢查數(shù)據(jù)標(biāo)簽有沒有對(duì)錯(cuò)這個(gè)聽起來(lái)很蠢但確實(shí)經(jīng)常發(fā)生然后看學(xué)習(xí)率是不是太大了把學(xué)習(xí)率縮小10倍試試再檢查BatchNorm的momentum和batch size是否匹配最后看權(quán)重初始化有沒有問題。ResNet的權(quán)重初始化PyTorch默認(rèn)用Kaiming初始化對(duì)ReLU激活函數(shù)是合適的。但如果你自己改了激活函數(shù)比如換成GELU初始化方式也要相應(yīng)調(diào)整。另外shortcut上的1x1卷積如果初始化不當(dāng)可能導(dǎo)致殘差分支的輸出過(guò)大或過(guò)小影響訓(xùn)練穩(wěn)定性。5.2 顯存不足的優(yōu)化手段ResNet18本身參數(shù)量不大大概1100萬(wàn)左右但訓(xùn)練時(shí)的顯存占用主要來(lái)自中間激活值。如果顯存不夠有幾個(gè)立竿見影的手段減小batch size、用混合精度訓(xùn)練、用梯度累積模擬大batch?;旌暇扔?xùn)練在PyTorch里用torch.cuda.amp就能實(shí)現(xiàn)代碼改動(dòng)很小顯存占用能降低30%到50%訓(xùn)練速度也能提升。但要注意混合精度下BatchNorm的統(tǒng)計(jì)量可能不夠精確如果發(fā)現(xiàn)精度下降可以把BatchNorm層強(qiáng)制轉(zhuǎn)回float32。from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for inputs, labels in dataloader: optimizer.zero_grad() with autocast(): outputs model(inputs) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()梯度累積的思路是多次前向傳播后再統(tǒng)一更新參數(shù)相當(dāng)于變相增大了batch size。比如實(shí)際batch size是32累積4次就等效于128。但要注意BatchNorm的統(tǒng)計(jì)量是按實(shí)際batch size算的梯度累積不能改變這一點(diǎn)所以如果BatchNorm效果不好梯度累積解決不了根本問題。5.3 過(guò)擬合的應(yīng)對(duì)策略ResNet18在小型數(shù)據(jù)集上很容易過(guò)擬合訓(xùn)練精度沖到99%但驗(yàn)證精度卡在70%上不去。應(yīng)對(duì)手段按優(yōu)先級(jí)排數(shù)據(jù)增強(qiáng)、權(quán)重衰減、Dropout、早停。數(shù)據(jù)增強(qiáng)是最有效的尤其是隨機(jī)裁剪和顏色抖動(dòng)能顯著增加數(shù)據(jù)的多樣性。權(quán)重衰減我一般設(shè)1e-4如果過(guò)擬合嚴(yán)重可以加到5e-4。Dropout在ResNet里通常加在全連接層之前但原論文其實(shí)沒怎么用Dropout因?yàn)锽atchNorm本身就有正則化效果。如果加了Dropout發(fā)現(xiàn)訓(xùn)練變慢可以試試把Dropout率從0.5降到0.3。早停是最簡(jiǎn)單粗暴的驗(yàn)證loss連續(xù)10個(gè)epoch不下降就停。但早停需要你有獨(dú)立的驗(yàn)證集而且驗(yàn)證集的分布要和測(cè)試集一致否則可能停早了或停晚了。5.4 遷移學(xué)習(xí)的正確打開方式用自己的數(shù)據(jù)集訓(xùn)練ResNet18時(shí)從零開始往往不如用預(yù)訓(xùn)練權(quán)重微調(diào)。微調(diào)有兩種策略只訓(xùn)練最后的全連接層或者全部層都微調(diào)但用較小的學(xué)習(xí)率。只訓(xùn)練全連接層適合數(shù)據(jù)集很小幾千張以內(nèi)且和ImageNet分布接近的情況。全部微調(diào)適合數(shù)據(jù)集較大幾萬(wàn)張以上或者和ImageNet差異較大的情況。全部微調(diào)時(shí)前面的層學(xué)習(xí)率要設(shè)小一些后面的層可以大一些因?yàn)闇\層學(xué)的是通用特征不需要大改。model ResNet18(num_classes10) pretrained_dict torch.load(resnet18_pretrained.pth) model_dict model.state_dict() pretrained_dict {k: v for k, v in pretrained_dict.items() if k in model_dict and fc not in k} model_dict.update(pretrained_dict) model.load_state_dict(model_dict) for param in model.parameters(): param.requires_grad True optimizer torch.optim.SGD([ {params: model.conv1.parameters(), lr: 0.001}, {params: model.layer1.parameters(), lr: 0.001}, {params: model.layer2.parameters(), lr: 0.005}, {params: model.layer3.parameters(), lr: 0.01}, {params: model.layer4.parameters(), lr: 0.01}, {params: model.fc.parameters(), lr: 0.05} ], momentum0.9, weight_decay1e-4)這種分層學(xué)習(xí)率的設(shè)置核心邏輯是淺層特征更通用不需要大改深層特征更任務(wù)相關(guān)需要更大調(diào)整。實(shí)測(cè)下來(lái)分層學(xué)習(xí)率比統(tǒng)一學(xué)習(xí)率能提升2到3個(gè)百分點(diǎn)的精度。5.5 常見問題速查表問題現(xiàn)象可能原因排查方法解決方案訓(xùn)練loss不下降學(xué)習(xí)率過(guò)大或過(guò)小打印每層梯度范數(shù)調(diào)整學(xué)習(xí)率加warmup驗(yàn)證精度遠(yuǎn)低于訓(xùn)練精度過(guò)擬合對(duì)比訓(xùn)練驗(yàn)證loss曲線加數(shù)據(jù)增強(qiáng)、權(quán)重衰減、早停驗(yàn)證精度異常低BatchNorm統(tǒng)計(jì)量不準(zhǔn)檢查驗(yàn)證batch size增大驗(yàn)證batch size顯存溢出batch size過(guò)大打印顯存占用減小batch size或混合精度梯度爆炸學(xué)習(xí)率過(guò)大或數(shù)據(jù)異常監(jiān)控梯度范數(shù)梯度裁剪、降低學(xué)習(xí)率訓(xùn)練速度慢數(shù)據(jù)加載瓶頸檢查GPU利用率增加num_workers、預(yù)取數(shù)據(jù)精度比官方低很多預(yù)處理不一致對(duì)比官方預(yù)處理流程統(tǒng)一Normalize參數(shù)和輸入尺寸6. 從ResNet18延伸出去的幾個(gè)方向ResNet18雖然結(jié)構(gòu)簡(jiǎn)單但它是理解更復(fù)雜網(wǎng)絡(luò)的基石。搞懂了BasicBlock再去看BottleneckBlock就很容易了——無(wú)非是把兩個(gè)3x3卷積換成了1x1、3x3、1x1的瓶頸結(jié)構(gòu)目的是在更深的網(wǎng)絡(luò)里減少計(jì)算量。ResNet50、ResNet101用的都是BottleneckBlock。另一個(gè)方向是把殘差連接的思想用到其他任務(wù)上。比如目標(biāo)檢測(cè)里的FPN特征金字塔的橫向連接本質(zhì)上也是一種殘差式的信息融合。再比如Transformer里的殘差連接雖然結(jié)構(gòu)不同但核心思想是一致的給梯度提供一條直接通路讓深層網(wǎng)絡(luò)更容易訓(xùn)練。如果你想進(jìn)一步提升ResNet18的性能可以試試這幾個(gè)改動(dòng)把ReLU換成GELU或SiLU在殘差塊里加入SE注意力模塊或者用GroupNorm替代BatchNorm來(lái)適應(yīng)小batch訓(xùn)練。這些改動(dòng)我都試過(guò)SE模塊帶來(lái)的提升最明顯大概能漲1到2個(gè)百分點(diǎn)但參數(shù)量和計(jì)算量也會(huì)相應(yīng)增加。最后分享一個(gè)我調(diào)試網(wǎng)絡(luò)時(shí)的小習(xí)慣每次改完結(jié)構(gòu)先在一個(gè)很小的子集上跑幾個(gè)epoch確認(rèn)loss能正常下降、梯度沒有異常再放到全量數(shù)據(jù)上訓(xùn)練。這個(gè)習(xí)慣幫我省了很多時(shí)間避免在錯(cuò)誤的結(jié)構(gòu)上浪費(fèi)算力。