ResNet學習筆記
論文地址:
Deep Residual Learning for Image Recognition arxiv.org何凱明現場講解ResNet:
我曾經:【AI Talking】CVPR2016 最佳論文, ResNet 現場演講 zhuanlan.zhihu.com
PyTorch官方程式碼實現:
ResNet的PyTorch版本官方程式碼 github.com筆者讀論文的學習筆記,本人水平有限,如有錯誤,請指出。
碼字不易,如果您覺得寫得不錯,請點個贊,謝謝。
ResNet關鍵點:
- 利用殘差結構讓網路能夠更深、收斂速度更快、優化更容易,同時引數相對之前的模型更少、複雜度更低
- 解決深網路退化、難以訓練的問題
- 適用於多種計算機視覺任務
本文先根據論文的順序介紹ResNet,然後解釋PyTorch版本的程式碼實現。
一、背景介紹
ResNet是何凱明等人在2015年提出的模型,獲得了CVPR最佳論文獎,在ILSVRC和COCO上的比賽成績:(以下比賽專案都是第一)
- ImageNet Classification
- ImageNet Detection
- ImageNet Localization
- COCO Detection
- COCO Segmentation
深度學習的發展從LeNet到AlexNet,再到VGGNet和GoogLeNet,網路的深度在不斷加深,經驗表明,網路深度有著至關重要的影響,層數深的網路可以提取出圖片的低層、中層和高層特徵。但當網路足夠深時,僅僅在後面繼續堆疊更多層會帶來很多問題:第一個問題就是 梯度爆炸 / 消失(vanishing / exploding gradients) ,這可以通過BN和更好的網路初始化解決;第二個問題就是 退化(degradation) 問題,即當網路層數多得飽和了,加更多層進去會導致優化困難、且訓練誤差和預測誤差更大了, 注意這裡誤差更大並不是由過擬合導致的 (後面實驗細節部分會解釋)。作者通過加入殘差結構解決退化問題。

二、ResNet想法來源
- 為什麼想到使用殘差?
- 對於VLAD和Fisher Vector來說,對殘差向量編碼比對原始向量編碼效率更高
- 用Multigrid解偏微分方程(Partial Differential Equations,PDE)時,使用殘差向量對於優化更好,收斂速度更快
- 為什麼想到使用跨層連線?
- 在多層感知機(multi-layer perceptrons,MLP)中加一層從輸入到輸出的線性層
- GoogLeNet中使用輔助分類器防止梯度爆炸 / 消失
- 在此之前已有研究者使用跨層連線對響應和梯度中心化(center)處理
- inception結構本質也是跨層連線
- highway網路也使用到了跨層連線
三、殘差結構
- 殘差結構內部組成 (注意下面是用全連線層解釋,而卷積層與其類似)

假設輸入為 ,有兩層全連線層學習到的對映為 ,也就是說這兩層可以漸進(asymptotically)擬合
。假設
與 維度相同,那麼擬合
與擬合殘差函式
等價,令殘差函式
,則原函式變為
,於是直接在原網路的基礎上加上一個跨層連線,這裡的跨層連線也很簡單,就是 將 的 恆等對映(Identity Mapping) 傳遞過去。
本質也就是不改變目標函式 ,將網路結構拆成兩個分支,一個分支是殘差對映
,一個分支是恆等對映 ,於是網路僅需學習殘差對映
即可。至於為什麼這麼做,後面介紹殘差結構原理的時候會解釋。
整個殘差結構可以形式化定義為 ,這裡的
指擬合的殘差對映,如上圖中有兩層全連線層,即
,其中 指ReLU,注意這裡為了簡潔沒有寫上bias。當
與 維度相同時,可以直接逐元素相加;但如果不同,就必須給 再加一個線性對映,將其對映到一個與
維度相同的向量,此時整個殘差結構為
,
就是一個用於維度匹配的矩陣。其實當
與 維度相同時也可以用一個方陣
,但經過後面的實驗發現恆等對映更好, 尤其是在bottleneck結構中 。
如果殘差結構僅一層,也可以使用,但這樣做沒什麼好處。
注意卷積層和上面基本一樣,維度匹配直接用1 x 1卷積升維或降維即可,這樣做因為又加了一層ReLU所以增加了網路的非線性擬合性,求和的時候直接每個對應feature map的對應元素相加即可, 注意是先相加再ReLU啟用 。
- 殘差結構為什麼有效?
- 自適應深度: 網路退化問題就體現了多層網路難以擬合恆等對映這種情況,也就是說
難以擬合 ,但使用了殘差結構之後,擬合恆等對映變得很容易,直接把網路引數全學習到為0,只留下那個恆等對映的跨層連線即可。於是當網路不需要這麼深時,中間的恆等對映就可以多一點,反之就可以少一點。(當然網路中出現某些層僅僅擬合恆等對映的可能性很小,但根據下面的第二點也有其用武之地;另外關於為什麼多層網路難以擬合恆等對映,這涉及到訊號與系統的知識: https://www. zhihu.com/question/2932 43905/answer/484708047 )
- “差分放大器”: 假設最優
更接近恆等對映,那麼網路更容易發現除恆等對映之外微小的波動
- 模型整合: 整個ResNet類似於多個網路的整合,原因是刪除ResNet的部分網路結點不影響整個網路的效能,但VGGNet會崩潰,具體可以看這篇NIPS論文: Residual Networks Behave Like Ensembles of Relatively Shallow Networks
- 緩解梯度消失: 針對一個殘差結構對輸入 求導就可以知道,由於跨層連線的存在,總梯度在
對 的導數基礎上還會加1
四、ResNet網路結構及實現細節
- 網路結構
從上面對殘差結構的描述可以看出, 殘差結構既不增加計算複雜度(除了幾乎可以忽略的元素相加),又不增加模型的引數量,同時這也為模型間的比較提供了方便 。下圖的左邊是VGG-19;下圖的中間是作者仿照VGG19堆疊了34層的網路,記為plain-34,雖然更深了,但FLOPs(代表計算複雜度,multiply-adds)僅為VGG-19的18%(畢竟VGG-19兩層全連線層太耗時了);下圖的右邊是針對中間加入了跨層連線即殘差結構,注意實線就是直接恆等變換和後面的feature map相加,虛線就是由於維度不匹配需要先升維後相加。

升維有兩種方式: 第一種是直接全補0,這樣做優勢是不會增加網路的引數;第二種是1 x 1卷積升維,後面實驗部分會進行比較。
注意這裡除第一個stage之外都會在stage的第一層使用步長為2的卷積來進行下采樣 ,倒數第二層輸出的feature map後面是全域性平均池化(global average pooling,VGGNet預測時轉化成全卷積網路的時候也用到了),也就是每個feature map求平均值,因為ImageNet輸出是1000個類別,所以再連線一層1000個神經元的全連線層,最後再接上一個Softmax。
- 更深的ResNet
Res18和Res34採用上面的結構,當使用更深的網路結構時,殘差結構發生了變化。

Res50、Res101、Res152採用的是被稱為 bottleneck 的殘差結構:

bottleneck結構就是前面先用1 x 1卷積降維,後面再用1 x 1卷積升維以符合維度大小,這樣做可以大大減少計算量。 注意bottleneck中3 x 3的卷積層只有一個,而不是普通結構的兩個。 關於1 x 1卷積作用我在之前的VGGNet學習筆記已提到:VGGNet學習筆記
- 實現細節
- 訓練時,和AlexNet、VGGNet一樣先每張圖片減均值; 資料增強: 利用VGGNet的多尺度處理,從區間[256, 480]隨機取一個數
,將原圖resize到短邊長度為
,然後再從這張圖隨機裁剪出224 x 224大小的圖片以及其水平翻轉作為模型的輸入,除此之外還用了AlexNet的顏色增強;在卷積之後ReLU之前用了BN;網路初始化方法用這篇論文: https:// arxiv.org/abs/1502.0185 2 ;所有的網路都是從頭開始訓練;優化使用SGD,batch size = 256,學習率初始值0.1,每當驗證集誤差開始上升LR就除以10,總迭代次數達到60萬次,weight decay = 0.0001,momentum = 0.9;不使用dropout
- 預測時,和AlexNet一樣進行 TTA ,每張圖片有10個裁剪;並且和VGGNet一樣採用全卷積形式和多尺度TTA,最後對它們的模型輸出值取均值即可
五、實驗細節
- 驗證ResNet可以解決網路退化問題

上表可以看出plain-34相對於plain-18網路退化了,注意根據作者的發現,這個問題並不是梯度消失引起的,因為BN能保證前向傳播時,響應值方差不為0,同時觀測到反向梯度良好。
這裡的實驗增維用的是補0的方法,可以看出Res34相對於Res18錯誤率卻得到了提升。結合下圖可以得出結論:第一ResNet解決了退化問題;第二Res34效果很好;第三下圖比較左邊和右邊可以看出,雖然Res18的結果和plain-18差不多,但Res18的收斂速度更快。

- 跨層連線應該用恆等對映還是更復雜的投影變換(就是前面介紹的矩陣
)?

這裡比較了三種選擇:選擇A為增維直接補0,其它還是恆等對映;選擇B為增維用投影變換,其它還是恆等對映;選擇C為所有都用投影變換。
從上面的實驗結果可以看出選擇C是相對最好的,但實際還是使用選擇B,因為對於bottleneck來說,如果不用恆等對映而用投影變換,整個網路的時間複雜度和模型大小會加倍,所以為了效率還是使用選擇B。
- Res101和Res152的實驗結果

上表更驗證了ResNet解決了網路退化問題, 注意即使是Res152,也比VGG-16的複雜度低很多。 (Res152:11.3 billion FLOPs,VGG-16:15.3 billion FLOPs,VGG-19:19.6 billion FLOPs)
- CIFAR-10上的實驗結果
這裡作者主要是研究很深的網路,並不是為了得到更好的結果,所以這裡重點說幾點:
- 初始學習率為0.1對於Res110太大,幾個epoch之後才開始收斂,所以先用小學習率0.01 warm up 網路,等到訓練誤差低於80%再恢復正常
- 作者分析了每一層的響應值的方差 (注意響應值和feature map不一樣,響應值是BN之後,ReLU或求和之前的值) ,結果如下圖中所示。從圖中可以得出:第一殘差函式
相對於非殘差函式更接近0;第二當層數加深,中間部分殘差結構對其輸入的更改更少。這兩點驗證了前面對殘差結構原理的闡述
- 作者還嘗試了超過1000層的網路,但發現實驗結果也還行,訓練集錯誤率和Res110差不多,測試集錯誤率比Res110略高一點,由於CIFAR-10資料集比較小,所以可能這裡過擬合了。這一點說明殘差結構可以適用於非常深的網路,也不會產生退化問題,或者說退化問題不明顯


- PASCAL和MS COCO資料集上目標檢測任務
使用Faster R-CNN檢測,將VGG-16替換成ResNet-101,發現有很大的提升

六、PyTorch官方程式碼解析
再看一次ResNet的五種基本形式:

這五種形式的中間卷積部分雖然都有四個stage,但卻各不相同,而網路的其它部分都是相同的,所以下面以Res18為例介紹其原始碼:
- resnet18函式
def resnet18(pretrained=False, **kwargs): """Constructs a ResNet-18 model. Args: pretrained (bool): If True, returns a model pre-trained on ImageNet """ model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs) if pretrained: model.load_state_dict(model_zoo.load_url(model_urls['resnet18'])) return model
呼叫這個函式可以直接獲得Res18;注意還可以使用在ImageNet預訓練過的Res18;[2, 2, 2, 2]指每個stage殘差塊的個數,如果是Res34那麼就應該是[3, 4, 6, 3]
- ResNet類(僅展示程式碼核心部分)
class ResNet(nn.Module): def __init__(self, block, layers, num_classes=1000, zero_init_residual=False): super(ResNet, self).__init__() self.inplanes = 64 self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False) self.bn1 = nn.BatchNorm2d(64) self.relu = nn.ReLU(inplace=True) self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) self.layer1 = self._make_layer(block, 64, layers[0]) self.layer2 = self._make_layer(block, 128, layers[1], stride=2) self.layer3 = self._make_layer(block, 256, layers[2], stride=2) self.layer4 = self._make_layer(block, 512, layers[3], stride=2) self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) self.fc = nn.Linear(512 * block.expansion, num_classes) def _make_layer(self, block, planes, blocks, stride=1): downsample = None if stride != 1 or self.inplanes != planes * block.expansion: downsample = nn.Sequential( conv1x1(self.inplanes, planes * block.expansion, stride), nn.BatchNorm2d(planes * block.expansion), ) layers = [] layers.append(block(self.inplanes, planes, stride, downsample)) self.inplanes = planes * block.expansion for _ in range(1, blocks): layers.append(block(self.inplanes, planes)) return nn.Sequential(*layers) def forward(self, x): x = self.conv1(x) x = self.bn1(x) x = self.relu(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 = x.view(x.size(0), -1) x = self.fc(x) return x
幾個關鍵點:
- 在殘差結構之前,先對原始224 x 224的圖片處理,在經過7 x 7的大卷積核、BN、ReLU、最大池化之後得到56 x 56 x 64的feature map
- 從layer1、layer2、layer3、layer4的定義可以看出,第一個stage不會減小feature map,其餘都會在stage的第一層用步長2的3 x 3卷積進行feature map長和寬減半
- _make_layer函式中downsample對殘差結構的輸入進行升維,直接1 x 1卷積再加上BN即可,後面BasicBlock類和Bottleneck類用得到
- 最後的池化層使用的是 自適應平均池化 ,而非論文中的全域性平均池化
- BasicBlock類和Bottleneck類
注意Res18、Res34用的是BasicBlock,其餘用的是Bottleneck
class BasicBlock(nn.Module): expansion = 1 def __init__(self, inplanes, planes, stride=1, downsample=None): super(BasicBlock, self).__init__() self.conv1 = conv3x3(inplanes, planes, stride) self.bn1 = nn.BatchNorm2d(planes) self.relu = nn.ReLU(inplace=True) self.conv2 = conv3x3(planes, planes) self.bn2 = nn.BatchNorm2d(planes) self.downsample = downsample self.stride = stride def forward(self, x): identity = x out = self.conv1(x) out = self.bn1(out) out = self.relu(out) out = self.conv2(out) out = self.bn2(out) if self.downsample is not None: identity = self.downsample(x) out += identity out = self.relu(out) return out
- expansion是殘差結構中輸出維度是輸入維度的多少倍,BasicBlock沒有升維,所以expansion = 1
- 殘差結構是在求和之後才經過ReLU層
class Bottleneck(nn.Module): expansion = 4 def __init__(self, inplanes, planes, stride=1, downsample=None): super(Bottleneck, self).__init__() self.conv1 = conv1x1(inplanes, planes) self.bn1 = nn.BatchNorm2d(planes) self.conv2 = conv3x3(planes, planes, stride) self.bn2 = nn.BatchNorm2d(planes) self.conv3 = conv1x1(planes, planes * self.expansion) self.bn3 = nn.BatchNorm2d(planes * self.expansion) self.relu = nn.ReLU(inplace=True) self.downsample = downsample self.stride = stride def forward(self, x): identity = x out = self.conv1(x) out = self.bn1(out) out = self.relu(out) out = self.conv2(out) out = self.bn2(out) out = self.relu(out) out = self.conv3(out) out = self.bn3(out) if self.downsample is not None: identity = self.downsample(x) out += identity out = self.relu(out) return out
- expansion = 4,因為Bottleneck中每個殘差結構輸出維度都是輸入維度的4倍
- 得到自己的ResNet
from torchvision.models.resnet import * def get_net(): model = resnet18(pretrained=True)# ImageNet上預訓練 model.fc = nn.Sequential( nn.BatchNorm1d(512 * 1), nn.Linear(512 * 1, num),# num為自己的資料集的類別數 ) return model