天天看點

ResNet及其變種的結構梳理、有效性分析與代碼解讀

0、前言

何凱明等人在2015年提出的ResNet,在ImageNet比賽classification任務上獲得第一名,獲評CVPR2016最佳論文。因為它“簡單與實用”并存,之後許多目标檢測、圖像分類任務都是建立在ResNet的基礎上完成的,成為計算機視覺領域重要的基石結構。

  • 本文對ResNet的論文進行簡單梳理,并對其網絡結構進行分析,然後對Torchvision版的ResNet代碼進行解讀,最後對ResNet訓練自有網絡進行簡單介紹,相關參考連結附在文末;
  • 論文連接配接:Deep Residual Learning for Image Recognition
  • 代碼連結:https://github.com/pytorch/vision/blob/master/torchvision/models/resnet.py
  • 本文所有代碼解讀均基于PyTroch 1.0,Python3;
  • 本文為原創文章,初次完成于2019.01,最後更新于2019.03;

1、ResNet要解決什麼問題?

自從深度神經網絡在ImageNet大放異彩之後,後來問世的深度神經網絡就朝着網絡層數越來越深的方向發展。直覺上我們不難得出結論:增加網絡深度後,網絡可以進行更加複雜的特征提取,是以更深的模型可以取得更好的結果。

但事實并非如此,人們發現随着網絡深度的增加,模型精度并不總是提升,并且這個問題顯然不是由過拟合(overfitting)造成的,因為網絡加深後不僅測試誤差變高了,它的訓練誤差竟然也變高了。作者提出,這可能是因為更深的網絡會伴随梯度消失/爆炸問題,進而阻礙網絡的收斂。作者将這種加深網絡深度但網絡性能卻下降的現象稱為退化問題(degradation problem)。

Is learning better networks as easy as stacking more layers? An obstacle to answering this question was the notorious problem of vanishing/exploding gradients [1, 9], which hamper convergence from the beginning.

Unexpectedly, such degradation is not caused by overfitting, and adding more layers to a suitably deep model leads to higher training error.

文中給出的實驗結果進一步描述了這種退化問題:當傳統神經網絡的層數從20增加為56時,網絡的訓練誤差和測試誤差均出現了明顯的增長,也就是說,網絡的性能随着深度的增加出現了明顯的退化。ResNet就是為了解決這種退化問題而誕生的。

ResNet及其變種的結構梳理、有效性分析與代碼解讀

圖120層與56層傳統神經網絡在CIFAR上的訓練誤差和測試誤差

2、ResNet怎麼解決網絡退化問題

随着網絡層數的增加,梯度爆炸和梯度消失問題嚴重制約了神經網絡的性能,研究人員通過提出包括Batch normalization在内的方法,已經一定程度上緩解了這個問題,但依然不足以滿足需求。

This problem,however, has been largely addressed by normalized initialization [23, 9, 37, 13] and intermediate normalization layers[16], which enable networks with tens of layers to start converging for stochastic gradient descent (SGD) with backpropagation [22].

作者想到了建構恒等映射(Identity mapping)來解決這個問題,問題解決的标志是:增加網絡層數,但訓練誤差不增加。為什麼是恒等映射呢,我是這樣子想的:20層的網絡是56層網絡的一個子集,56層網絡的解空間包含着20層網絡的解空間。如果我們将56層網絡的最後36層全部短接,這些層進來是什麼出來也是什麼(也就是做一個恒等映射),那這個56層網絡不就等效于20層網絡了嗎,至少效果不會相比原先的20層網絡差吧。同樣是56層網絡,不引入恒等映射為什麼就不行呢?因為梯度消失現象使得網絡難以訓練,雖然網絡的深度加深了,但是實際上無法有效訓練網絡,訓練不充分的網絡不但無法提升性能,甚至降低了性能。

There exists a solution by construction to the deeper model: the added layers are identity mapping, and the other layers are copied from the learned shallower model. The existence of this constructed solution indicates that a deeper model should produce no higher training error than its shallower counterpart.
ResNet及其變種的結構梳理、有效性分析與代碼解讀

圖2 殘差學習基本單元

那怎麼建構恒等映射呢?簡單地說,原先的網絡輸入x,希望輸出H(x)。現在我們改一改,我們令H(x)=F(x)+x,那麼我們的網絡就隻需要學習輸出一個殘差F(x)=H(x)-x。作者提出,學習殘差F(x)=H(x)-x會比直接學習原始特征H(x)簡單的多。

ResNet及其變種的結構梳理、有效性分析與代碼解讀

3、ResNet網絡結構與代碼實作

ResNet主要有五種主要形式:Res18,Res34,Res50,Res101,Res152;

如下圖所示,每個網絡都包括三個主要部分:輸入部分、輸出部分和中間卷積部分(中間卷積部分包括如圖所示的Stage1到Stage4共計四個stage)。盡管ResNet的變種形式豐富,但都遵循上述的結構特點,網絡之間的不同主要在于中間卷積部分的block參數和個數存在差異。下面我們以ResNet18為例,看一下整個網絡的實作代碼是怎樣的。

ResNet及其變種的結構梳理、有效性分析與代碼解讀

圖3.1 ResNet結構總覽

  • 網絡整體結構

我們通過調用resnet18( )函數來生成一個具體的model,而resnet18函數則是借助ResNet類來建構網絡的。

class ResNet(nn.Module):
    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

# 生成一個res18網絡
def resnet18(pretrained=False, **kwargs):
    model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs)
    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet18']))
    return model
           

在ResNet類中的forward( )函數規定了網絡資料的流向:

(1)資料進入網絡後先經過輸入部分(conv1, bn1, relu, maxpool);

(2)然後進入中間卷積部分(layer1, layer2, layer3, layer4,這裡的layer對應我們之前所說的stage);

(3)最後資料經過一個平均池化和全連接配接層(avgpool, fc)輸出得到結果;

具體來說,resnet18和其他res系列網絡的差異主要在于layer1~layer4,其他的部件都是相似的。

  • 網絡輸入部分

所有的ResNet網絡輸入部分是一個size=7x7, stride=2的大卷積核,以及一個size=3x3, stride=2的最大池化組成,通過這一步,一個224x224的輸入圖像就會變56x56大小的特征圖,極大減少了存儲所需大小。

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)
           
ResNet及其變種的結構梳理、有效性分析與代碼解讀

輸入層特征圖資料變化

  • 網絡中間卷積部分

中間卷積部分主要是下圖中的藍框部分,通過3*3卷積的堆疊來實作資訊的提取。紅框中的[2, 2, 2, 2]和[3, 4, 6, 3]等則代表了bolck的重複堆疊次數。

ResNet及其變種的結構梳理、有效性分析與代碼解讀

ResNet結構細節

剛剛我們調用的resnet18( )函數中有一句 ResNet(BasicBlock, [2, 2, 2, 2], **kwargs),這裡的[2, 2, 2, 2]與圖中紅框是一緻的,如果你将這行代碼改為 ResNet(BasicBlock, [3, 4, 6, 3], **kwargs), 那你就會得到一個res34網絡。

  • 殘差塊實作

下面我們來具體看一下一個殘差塊是怎麼實作的,如下圖所示的basic-block,輸入資料分成兩條路,一條路經過兩個3*3卷積,另一條路直接短接,二者相加經過relu輸出,十分簡單。

ResNet及其變種的結構梳理、有效性分析與代碼解讀

basic_block

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
           
ResNet及其變種的結構梳理、有效性分析與代碼解讀

bascic_block 資料走向

代碼比較清晰,不做分析了,主要提一個點:downsample,它的作用是對輸入特征圖大小進行減半處理,每個stage都有且隻有一個downsample。後面我們再詳細介紹。

  • 網絡輸出部分

網絡輸出部分很簡單,通過全局自适應平滑池化,把所有的特征圖拉成1*1,對于res18來說,就是1x512x7x7 的輸入資料拉成 1x512x1x1,然後接全連接配接層輸出,輸出節點個數與預測類别個數一緻。

self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512 * block.expansion, num_classes)
           

至此,整體網絡結構代碼分析結束,更多細節,請看torchvision源碼。

4、Bottleneck結構和1*1卷積

ResNet50起,就采用Bottleneck結構,主要是引入1x1卷積。我們來看一下這裡的1x1卷積有什麼作用:

  • 對通道數進行升維和降維(跨通道資訊整合),實作了多個特征圖的線性組合,同時保持了原有的特征圖大小;
  • 相比于其他尺寸的卷積核,可以極大地降低運算複雜度;
  • 如果使用兩個3x3卷積堆疊,隻有一個relu,但使用1x1卷積就會有兩個relu,引入了更多的非線性映射;
ResNet及其變種的結構梳理、有效性分析與代碼解讀

Basicblock和Bottleneck結構

我們來計算一下1*1卷積的計算量優勢:首先看上圖右邊的bottleneck結構,對于256維的輸入特征,參數數目:1x1x256x64+3x3x64x64+1x1x64x256=69632,如果同樣的輸入輸出次元但不使用1x1卷積,而使用兩個3x3卷積的話,參數數目為(3x3x256x256)x2=1179648。簡單計算下就知道了,使用了1x1卷積的bottleneck将計算量簡化為原有的5.9%,收益超高。

5、ResNet的網絡設計規律

ResNet及其變種的結構梳理、有效性分析與代碼解讀

整個ResNet不使用dropout,全部使用BN。此外,回到最初的這張細節圖,我們不難發現一些規律和特點:

  • 受VGG的啟發,卷積層主要是3×3卷積;
  • 對于相同的輸出特征圖大小的層,即同一stage,具有相同數量的3x3濾波器;
  • 如果特征地圖大小減半,濾波器的數量加倍以保持每層的時間複雜度;(這句是論文和現場演講中的原話,雖然我并不了解是什麼意思)
  • 每個stage通過步長為2的卷積層執行下采樣,而卻這個下采樣隻會在每一個stage的第一個卷積完成,有且僅有一次。
  • 網絡以平均池化層和softmax的1000路全連接配接層結束,實際上工程上一般用自适應全局平均池化 (Adaptive Global Average Pooling);

從圖中的網絡結構來看,在卷積之後全連接配接層之前有一個全局平均池化 (Global Average Pooling, GAP) 的結構,這個結構最早出自經典論文: Network In Network

In this paper, we propose another strategy called global average pooling to replace the traditional fully connected layers in CNN. The idea is to generate one feature map for each corresponding category of the classification task in the last mlpconv layer. Instead of adding fully connected layers on top of the feature maps, we take the average of each feature map, and the resulting vector is fed directly into the softmax layer. One advantage of global average pooling over the fully connected layers is that it is more native to the convolution structure by enforcing correspondences between feature maps and categories. Thus the feature maps can be easily interpreted as categories confidence maps. Another advantage is that there is no parameter to optimize in the global average pooling thus overfitting is avoided at this layer. Futhermore, global average pooling sums out the spatial information, thus it is more robust to spatial translations of the input.

We can see global average pooling as a structural regularizer that explicitly enforces feature maps to be confidence maps of concepts (categories).This is made possible by the mlpconv layers, as they makes better approximation to the confidence maps than GLMs.

總結如下:

  1. 相比傳統的分類網絡,這裡接的是池化,而不是全連接配接層。池化是不需要參數的,相比于全連接配接層可以砍去大量的參數。對于一個7x7的特征圖,直接池化和改用全連接配接層相比,可以節省将近50倍的參數,作用有二:一是節省計算資源,二是防止模型過拟合,提升泛化能力;
  2. 這裡使用的是全局平均池化,但我覺得大家都有疑問吧,就是為什麼不用最大池化呢?這裡解釋很多,我查閱到的一些論文的實驗結果表明平均池化的效果略好于最大池化,但最大池化的效果也差不到哪裡去。實際使用過程中,可以根據自身需求做一些調整,比如多分類問題更适合使用全局最大池化(道聽途說,不作保證)。如果不确定話還有一個更保險的操作,就是最大池化和平均池化都做,然後把兩個張量拼接,讓後續的網絡自己學習權重使用。

6、如何改造得到自己的ResNet?

我舉一個簡單的例子

from torchvision.models.resnet import *
def get_net():
    model = resnet18(pretrained=True)
    model.avgpool = nn.AdaptiveAvgPool2d((1, 1))
    model.fc = nn.Sequential(
                nn.BatchNorm1d(512*1),
                nn.Linear(512*1, 你的分類類别數),
            )
    return model
           

代碼簡單解讀一下:

  • 首先,通過torchvision導入相關的函數
  • 通過resnet18( )執行個體化一個模型,并使用imagenet預訓練權重
  • 将平均池化修改為自适應全局平均池化,避免輸入特征尺寸不比對
  • 修改全連接配接層,主要是修改分類類别數,并加入BN1d

這樣子,不僅可以根據自己的需求改造網絡,還能最大限度的使用現成的預訓練權重。需要注意的是,這裡的nn.BatchNorm1d(512*1)是很必要的,初學者可以嘗試删除這個部件感受一下差別。在我曾經的實驗裡面,loss會直接爆炸。

7、ResNet的常見改進

  • 改進一:改進downsample部分,減少資訊流失。前面說過了,每個stage的第一個conv都有下采樣的步驟,我們看左邊第一張圖左側的通路,input資料進入後在會經曆一個stride=2的1*1卷積,将特征圖尺寸減小為原先的一半,請注意1x1卷積和stride=2會導緻輸入特征圖3/4的資訊不被利用,是以ResNet-B的改進就是就是将下采樣移到後面的3x3卷積裡面去做,避免了資訊的大量流失。ResNet-D則是在ResNet-B的基礎上将identity部分的下采樣交給avgpool去做,避免出現1x1卷積和stride同時出現造成資訊流失。ResNet-C則是另一種思路,将ResNet輸入部分的7x7大卷積核換成3個3x3卷積核,可以有效減小計算量,這種做法最早出現在Inception-v2中。其實這個ResNet-C 我比較疑惑,ResNet論文裡說它借鑒了VGG的思想,使用大量的小卷積核,既然這樣那為什麼第一部分依舊要放一個7x7的大卷積核呢,不知道是出于怎樣的考慮,但是現在的多數網絡都把這部分改成3個3x3卷積核級聯。
ResNet及其變種的結構梳理、有效性分析與代碼解讀

ResNet的三種改進

  • 改進二:ResNet V2。這是由ResNet原班人馬打造的,主要是對ResNet部分元件的順序進行了調整。各種魔改中常見的預激活ResNet就是出自這裡。
ResNet及其變種的結構梳理、有效性分析與代碼解讀

ResNet V2

原始的resnet是上圖中的a的模式,我們可以看到相加後需要進入ReLU做一個非線性激活,這裡一個改進就是砍掉了這個非線性激活,不難了解,如果将ReLU放在原先的位置,那麼殘差塊輸出永遠是非負的,這制約了模型的表達能力,是以我們需要做一些調整,我們将這個ReLU移入了殘差塊内部,也就是圖e的模式。這裡的細節比較多,建議直接閱讀原文:Identity Mappings in Deep Residual Networks ,就先介紹這麼多。

8.1、從模型內建角度了解ResNet的有效性

ResNet 中其實是存在着很多路徑的集合,整個ResNet類似于多個網絡的內建學習,證據是删除部分ResNet的網絡結點,不影響整個網絡的性能,但是在VGG上做同樣的事請網絡立刻崩潰,由此可見相比其他網絡ResNet對于部分路徑的缺失不敏感。更多細節具體可參見NIPS論文: Residual Networks Behave Like Ensembles of Relatively Shallow Networks ;

ResNet及其變種的結構梳理、有效性分析與代碼解讀

模型內建假說

ResNet及其變種的結構梳理、有效性分析與代碼解讀

破壞性實驗

8.2、從梯度反向傳播角度了解ResNet的有效性

殘差結構使得梯度反向傳播時,更不易出現梯度消失等問題,由于Skip Connection的存在,梯度能暢通無阻地通過各個Res blocks,下面我們來推導一下 ResNet v2 的反向傳播過程。

ResNet及其變種的結構梳理、有效性分析與代碼解讀

原始的殘差公式是這樣子的,函數F表示一個殘差函數,函數f表示激活函數,:

ResNet及其變種的結構梳理、有效性分析與代碼解讀

ResNet v2 使用恒等映射,且相加後不使用激活函數,是以可得到:

ResNet及其變種的結構梳理、有效性分析與代碼解讀

遞歸得到第L層的表達式:

ResNet及其變種的結構梳理、有效性分析與代碼解讀

反向傳播求第l層梯度:

ResNet及其變種的結構梳理、有效性分析與代碼解讀

我們從這個表達式可以看出來:第l層的梯度裡,包含了第L層的梯度,通俗的說就是第L層的梯度直接傳遞給了第l層。因為梯度消失問題主要是發生在淺層,這種将深層梯度直接傳遞給淺層的做法,有效緩解了深度神經網絡梯度消失的問題。

8.3、其他假說彙總

(1)差分放大器假說

殘差結構可以放大輸入中微小的擾動,是以更加靈敏;

(2)自适應深度(https://www.zhihu.com/question/293243905/answer/484708047)

傳統的conv子產品是很難通過學習變成恒等的,因為大家學過信号與系統都知道,恒等的話filter的沖激響應要為一個沖激函數,而神經網絡本質是學機率分布 局部一層不太容易變成恒等,而resnet加入了這種子產品給了神經網絡學習恒等映射的能力。是以我個人了解resnet除了減弱梯度消失外,我還了解為這是一種自适應深度,也就是網絡可以自己調節層數的深淺,不需要太深時,中間恒等映射就多,需要時恒等映射就少。當然了,實際的神經網絡并不會有這麼強的局部特性,它的訓練結果基本是一個整體,并不一定會出現我說的某些block就是恒等的情況

9、總結

ResNet是目前計算機視覺領域的基石結構,是初學者無法繞開的網絡模型,仔細閱讀論文和源碼并進行實驗是極有必要的。

最後,強烈推薦大家看一下何凱明的現場演講,有助于更好地了解ResNet。

轉載自:ResNet及其變種的結構梳理、有效性分析與代碼解讀

繼續閱讀