絡(luò)原理與PyTorch實(shí)現(xiàn))
1. 從零手寫ResNet18為什么我不建議你直接調(diào)庫剛?cè)腴T深度學(xué)習(xí)那會兒我總覺得調(diào)包是理所當(dāng)然的事。torchvision.models.resnet18(pretrainedTrue)一行代碼就能拿到一個現(xiàn)成的殘差網(wǎng)絡(luò)何必自己費(fèi)勁去寫直到有一次我需要把ResNet18的BasicBlock改成帶注意力機(jī)制的結(jié)構(gòu)才發(fā)現(xiàn)如果連殘差塊內(nèi)部長什么樣都說不清楚根本無從下手。從那次之后我就養(yǎng)成了一個習(xí)慣任何一個經(jīng)典網(wǎng)絡(luò)我都要自己手寫一遍不追求性能超越官方實(shí)現(xiàn)只求把每一層的來龍去脈搞清楚。這篇文章要聊的就是用PyTorch從零實(shí)現(xiàn)ResNet18的完整過程。不是簡單地把官方代碼復(fù)制粘貼過來而是把每個設(shè)計(jì)決策背后的邏輯講透——為什么殘差塊要設(shè)計(jì)成兩層卷積而不是一層shortcut連接什么時(shí)候需要做下采樣BatchNorm放在卷積前面還是后面這些細(xì)節(jié)官方代碼不會告訴你但恰恰是面試和實(shí)際項(xiàng)目中真正拉開差距的地方。適合誰來讀如果你已經(jīng)了解卷積神經(jīng)網(wǎng)絡(luò)的基本概念知道卷積層、池化層、全連接層是干什么的但還沒有自己完整搭建過一個深層網(wǎng)絡(luò)那這篇文章就是為你準(zhǔn)備的。如果你已經(jīng)用過ResNet但說不清楚它為什么能解決退化問題這篇文章也能幫你把知識盲區(qū)補(bǔ)上。我會盡量用大白話把原理講清楚同時(shí)給出可以直接運(yùn)行的完整代碼你跟著敲一遍對殘差網(wǎng)絡(luò)的理解會上一個臺階。2. 殘差網(wǎng)絡(luò)到底解決了什么問題2.1 深層網(wǎng)絡(luò)的退化困境在ResNet出現(xiàn)之前深度學(xué)習(xí)領(lǐng)域有一個讓人困惑的現(xiàn)象網(wǎng)絡(luò)層數(shù)增加到一定程度后訓(xùn)練誤差反而上升了。注意這里說的不是過擬合導(dǎo)致的驗(yàn)證誤差上升而是訓(xùn)練集上的誤差本身就變大了。這意味著模型連訓(xùn)練數(shù)據(jù)都擬合不好顯然不是過擬合的問題。這個問題被稱為退化問題。按常理推斷一個56層的網(wǎng)絡(luò)應(yīng)該至少能表現(xiàn)得和20層網(wǎng)絡(luò)一樣好——因?yàn)槎喑鰜淼?6層哪怕什么都不做只是恒等映射也不應(yīng)該讓效果變差。但實(shí)際訓(xùn)練中讓多層非線性變換去逼近一個恒等映射難度遠(yuǎn)超想象。梯度在反向傳播過程中經(jīng)過層層衰減到了淺層幾乎消失殆盡參數(shù)更新變得極其緩慢。我做過一個簡單的對比實(shí)驗(yàn)用同樣的數(shù)據(jù)集分別訓(xùn)練一個20層的普通卷積網(wǎng)絡(luò)和一個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í)恒等映射很難那就換個思路讓網(wǎng)絡(luò)學(xué)習(xí)殘差。具體來說假設(shè)我們希望某幾層學(xué)到的映射是H(x)傳統(tǒng)做法是讓這幾層直接擬合H(x)。ResNet的做法是讓這幾層擬合F(x) H(x) - x然后最終的輸出變成F(x) x。這個改動的精妙之處在于如果最優(yōu)的映射就是恒等映射那么網(wǎng)絡(luò)只需要把F(x)的權(quán)重全部推向零就行了這比讓一堆非線性層去逼近恒等映射容易得多。用生活化的類比來說傳統(tǒng)網(wǎng)絡(luò)像是讓你從零開始畫一幅畫而殘差網(wǎng)絡(luò)像是給你一張底稿你只需要在上面做修改。修改到什么都不改就是恒等映射這顯然更容易。從梯度傳播的角度看殘差連接提供了一條“高速公路”。反向傳播時(shí)梯度可以通過shortcut連接直接傳到淺層不需要經(jīng)過中間那些卷積層的層層衰減。這就是為什么ResNet可以訓(xùn)練到上百層甚至上千層而普通網(wǎng)絡(luò)幾十層就訓(xùn)不動了。2.3 ResNet18的網(wǎng)絡(luò)結(jié)構(gòu)總覽ResNet18是ResNet家族中最淺的成員之一18這個數(shù)字指的是網(wǎng)絡(luò)中帶有權(quán)重的層數(shù)——包括卷積層和全連接層不包括池化層和BatchNorm層。具體來說它包含1個7x7的初始卷積層、8個BasicBlock每個Block含2個3x3卷積層共16個卷積層、以及1個全連接層加起來正好18層。整個網(wǎng)絡(luò)可以分成五個階段初始卷積階段、四個殘差階段。初始階段用一個7x7的大卷積核快速降低特征圖尺寸然后每個殘差階段由若干個BasicBlock組成特征圖的空間尺寸逐階段減半通道數(shù)逐階段翻倍。最后通過全局平均池化和全連接層輸出分類結(jié)果。這個結(jié)構(gòu)設(shè)計(jì)有幾個關(guān)鍵考量。7x7卷積核雖然參數(shù)量大但在網(wǎng)絡(luò)最前面輸入通道只有3計(jì)算量其實(shí)可控而且大感受野有助于捕捉圖像的全局信息。四個階段的通道數(shù)分別是64、128、256、512每次翻倍是為了在空間信息減少的同時(shí)增加通道容量保持信息的總體表達(dá)能力。全局平均池化替代了傳統(tǒng)的多層全連接大幅減少了參數(shù)量也降低了過擬合風(fēng)險(xiǎn)。3. 手寫ResNet18的核心組件拆解3.1 BasicBlock的設(shè)計(jì)細(xì)節(jié)BasicBlock是ResNet18的基本構(gòu)建單元結(jié)構(gòu)并不復(fù)雜但有幾個容易踩坑的地方。它的主路徑是3x3卷積 → BatchNorm → ReLU → 3x3卷積 → BatchNorm然后在輸出處與shortcut相加再過一次ReLU。第一個坑是卷積的padding。3x3卷積要保持特征圖尺寸不變padding必須設(shè)為1。這個不用死記公式是輸出尺寸 (輸入尺寸 2×padding - kernel_size) / stride 1。代入kernel_size3、stride1、padding1輸出尺寸等于輸入尺寸。如果padding設(shè)成0每經(jīng)過一個卷積特征圖就縮小2網(wǎng)絡(luò)還沒到深處就沒了。第二個坑是BatchNorm的位置。標(biāo)準(zhǔn)做法是卷積之后、ReLU之前。BatchNorm放在卷積后面可以對卷積輸出做歸一化讓數(shù)據(jù)分布更穩(wěn)定。如果放在ReLU后面被ReLU截?cái)嗟呢?fù)值部分就無法被歸一化修正了。另外BatchNorm的momentum參數(shù)默認(rèn)是0.1但在小批量訓(xùn)練時(shí)可能需要調(diào)小這個后面會細(xì)說。第三個坑是shortcut的處理。當(dāng)輸入輸出通道數(shù)相同且特征圖尺寸不變時(shí)shortcut可以直接用恒等映射。但當(dāng)通道數(shù)需要翻倍或者特征圖需要減半時(shí)shortcut必須做一個1x1卷積來匹配維度同時(shí)用stride2來同步降采樣。這個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ù)會起到偏置的作用再加一個卷積偏置是冗余的只會浪費(fèi)參數(shù)。3.2 下采樣與通道匹配的邏輯什么時(shí)候需要給shortcut加卷積判斷條件就兩個stride不等于1或者輸入通道數(shù)不等于輸出通道數(shù)。在ResNet18中每個殘差階段的第一塊需要下采樣stride設(shè)為2同時(shí)通道數(shù)翻倍。后面的塊保持stride1、通道數(shù)不變shortcut就是純恒等映射。這里有一個細(xì)節(jié)值得展開為什么下采樣用stride2的1x1卷積而不是池化因?yàn)槌鼗僮鲿G失信息而且池化后的特征圖與主路徑經(jīng)過stride2卷積后的特征圖在空間位置上可能對不齊。用1x1卷積配合stride2既能完成降采樣又能學(xué)習(xí)到一個線性變換來匹配通道數(shù)信息損失更小。還有一個容易忽略的點(diǎn)主路徑的第一個3x3卷積也用了stride2這樣主路徑和shortcut路徑的空間降采樣是同步的相加時(shí)特征圖尺寸才能對齊。如果主路徑降采樣了而shortcut沒降相加就會報(bào)維度錯誤。3.3 網(wǎng)絡(luò)整體架構(gòu)的搭建有了BasicBlock搭建ResNet18就是按部就班的事了。先定義一個_make_layer方法負(fù)責(zé)堆疊指定數(shù)量的BasicBlock然后按照[2, 2, 2, 2]的配置構(gòu)建四個階段。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然后四個殘差階段分別輸出56、28、14、7的空間尺寸。這個尺寸變化鏈條是固定的輸入不是224x224的話最后全局平均池化之前會得到不同的尺寸但AdaptiveAvgPool2d會自動適配所以網(wǎng)絡(luò)對輸入尺寸有一定靈活性。4. 訓(xùn)練配置與實(shí)操全流程4.1 數(shù)據(jù)準(zhǔn)備與增強(qiáng)策略ResNet18原論文是在ImageNet上訓(xùn)練的輸入尺寸224x224。如果你用自己的數(shù)據(jù)集不一定非要縮放到224但建議至少保持128以上否則經(jīng)過五次降采樣后特征圖太小信息損失嚴(yán)重。數(shù)據(jù)增強(qiáng)方面訓(xùn)練階段我通常用隨機(jī)裁剪加隨機(jī)水平翻轉(zhuǎn)。隨機(jī)裁剪的scale范圍設(shè)在0.08到1.0之間ratio在3/4到4/3之間這是比較通用的配置。顏色抖動可以加但強(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ù)集的均值和方差替換掉這兩個值。這個操作看起來不起眼但對訓(xùn)練穩(wěn)定性的影響不小。4.2 學(xué)習(xí)率與優(yōu)化器的選擇ResNet18的訓(xùn)練我一般用SGD加動量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))。這個調(diào)度方式在訓(xùn)練后期學(xué)習(xí)率下降得很平滑不會像階梯下降那樣突然跳變導(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)練會更穩(wěn)定。但大batch通常需要配合更大的學(xué)習(xí)率有一個經(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ù)雜但有幾個監(jiān)控指標(biāo)必須盯著。第一個是訓(xùn)練loss和驗(yàn)證loss的走勢如果訓(xùn)練loss持續(xù)下降但驗(yàn)證loss開始上升說明過擬合了需要加正則化或者早停。第二個是學(xué)習(xí)率的變化曲線確保調(diào)度器按預(yù)期工作。第三個是梯度范數(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()不然會白白消耗顯存。另外如果用了BatchNorm驗(yàn)證時(shí)的batch size不能太小至少要有8以上否則統(tǒng)計(jì)量估計(jì)不準(zhǔn)驗(yàn)證精度會異常偏低。這個問題我踩過坑當(dāng)時(shí)驗(yàn)證集batch size設(shè)成1結(jié)果驗(yàn)證精度比訓(xùn)練精度低了十幾個百分點(diǎn)排查了半天才發(fā)現(xiàn)是BatchNorm的鍋。4.4 模型保存與恢復(fù)訓(xùn)練保存模型時(shí)我習(xí)慣同時(shí)保存模型參數(shù)、優(yōu)化器狀態(tài)、當(dāng)前epoch和學(xué)習(xí)率。這樣萬一訓(xùn)練中斷可以無縫恢復(fù)不用從頭再來。只保存state_dict而不是整個模型對象是因?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í)率會從頭開始影響訓(xùn)練連續(xù)性。5. 常見問題與排查技巧實(shí)錄5.1 訓(xùn)練不收斂的排查思路訓(xùn)練不收斂是新手最常遇到的問題表現(xiàn)是loss居高不下或者震蕩劇烈。排查順序我一般是這樣先檢查數(shù)據(jù)標(biāo)簽有沒有對錯這個聽起來很蠢但確實(shí)經(jīng)常發(fā)生然后看學(xué)習(xí)率是不是太大了把學(xué)習(xí)率縮小10倍試試再檢查BatchNorm的momentum和batch size是否匹配最后看權(quán)重初始化有沒有問題。ResNet的權(quán)重初始化PyTorch默認(rèn)用Kaiming初始化對ReLU激活函數(shù)是合適的。但如果你自己改了激活函數(shù)比如換成GELU初始化方式也要相應(yīng)調(diào)整。另外shortcut上的1x1卷積如果初始化不當(dāng)可能導(dǎo)致殘差分支的輸出過大或過小影響訓(xùn)練穩(wěn)定性。5.2 顯存不足的優(yōu)化手段ResNet18本身參數(shù)量不大大概1100萬左右但訓(xùn)練時(shí)的顯存占用主要來自中間激活值。如果顯存不夠有幾個立竿見影的手段減小batch size、用混合精度訓(xùn)練、用梯度累積模擬大batch?;旌暇扔?xùn)練在PyTorch里用torch.cuda.amp就能實(shí)現(xiàn)代碼改動很小顯存占用能降低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 過擬合的應(yīng)對策略ResNet18在小型數(shù)據(jù)集上很容易過擬合訓(xùn)練精度沖到99%但驗(yàn)證精度卡在70%上不去。應(yīng)對手段按優(yōu)先級排數(shù)據(jù)增強(qiáng)、權(quán)重衰減、Dropout、早停。數(shù)據(jù)增強(qiáng)是最有效的尤其是隨機(jī)裁剪和顏色抖動能顯著增加數(shù)據(jù)的多樣性。權(quán)重衰減我一般設(shè)1e-4如果過擬合嚴(yán)重可以加到5e-4。Dropout在ResNet里通常加在全連接層之前但原論文其實(shí)沒怎么用Dropout因?yàn)锽atchNorm本身就有正則化效果。如果加了Dropout發(fā)現(xiàn)訓(xùn)練變慢可以試試把Dropout率從0.5降到0.3。早停是最簡單粗暴的驗(yàn)證loss連續(xù)10個epoch不下降就停。但早停需要你有獨(dú)立的驗(yàn)證集而且驗(yàn)證集的分布要和測試集一致否則可能停早了或停晚了。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ù)集較大幾萬張以上或者和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í)測下來分層學(xué)習(xí)率比統(tǒng)一學(xué)習(xí)率能提升2到3個百分點(diǎn)的精度。5.5 常見問題速查表問題現(xiàn)象可能原因排查方法解決方案訓(xùn)練loss不下降學(xué)習(xí)率過大或過小打印每層梯度范數(shù)調(diào)整學(xué)習(xí)率加warmup驗(yàn)證精度遠(yuǎn)低于訓(xùn)練精度過擬合對比訓(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過大打印顯存占用減小batch size或混合精度梯度爆炸學(xué)習(xí)率過大或數(shù)據(jù)異常監(jiān)控梯度范數(shù)梯度裁剪、降低學(xué)習(xí)率訓(xùn)練速度慢數(shù)據(jù)加載瓶頸檢查GPU利用率增加num_workers、預(yù)取數(shù)據(jù)精度比官方低很多預(yù)處理不一致對比官方預(yù)處理流程統(tǒng)一Normalize參數(shù)和輸入尺寸6. 從ResNet18延伸出去的幾個方向ResNet18雖然結(jié)構(gòu)簡單但它是理解更復(fù)雜網(wǎng)絡(luò)的基石。搞懂了BasicBlock再去看BottleneckBlock就很容易了——無非是把兩個3x3卷積換成了1x1、3x3、1x1的瓶頸結(jié)構(gòu)目的是在更深的網(wǎng)絡(luò)里減少計(jì)算量。ResNet50、ResNet101用的都是BottleneckBlock。另一個方向是把殘差連接的思想用到其他任務(wù)上。比如目標(biāo)檢測里的FPN特征金字塔的橫向連接本質(zhì)上也是一種殘差式的信息融合。再比如Transformer里的殘差連接雖然結(jié)構(gòu)不同但核心思想是一致的給梯度提供一條直接通路讓深層網(wǎng)絡(luò)更容易訓(xùn)練。如果你想進(jìn)一步提升ResNet18的性能可以試試這幾個改動把ReLU換成GELU或SiLU在殘差塊里加入SE注意力模塊或者用GroupNorm替代BatchNorm來適應(yīng)小batch訓(xùn)練。這些改動我都試過SE模塊帶來的提升最明顯大概能漲1到2個百分點(diǎn)但參數(shù)量和計(jì)算量也會相應(yīng)增加。最后分享一個我調(diào)試網(wǎng)絡(luò)時(shí)的小習(xí)慣每次改完結(jié)構(gòu)先在一個很小的子集上跑幾個epoch確認(rèn)loss能正常下降、梯度沒有異常再放到全量數(shù)據(jù)上訓(xùn)練。這個習(xí)慣幫我省了很多時(shí)間避免在錯誤的結(jié)構(gòu)上浪費(fèi)算力。