- Python深度學習:模型、方法與實現
- (保加利亞)伊凡·瓦西列夫
- 2528字
- 2021-09-26 16:10:28
3.3 理解殘差網絡
殘差網絡(ResNet,“Deep Residual Learning for Image Recognition”,https://arxiv.org/abs/1512.03385)是在2015年發布的,當時它們贏得了那年ImageNet挑戰賽的所有五個類別。在第1章中,提到神經網絡的層不受順序的限制,而是形成一個圖。這是我們要學習的第一個架構,它利用了這種靈活性。這也是第一個成功訓練了深度超過100層的網絡架構。
由于更好的權重初始化、新的激活函數以及標準化層,現在可以訓練深度網絡。但是,論文作者進行了一些實驗,觀察到一個56層的網絡比一個20層的網絡有更多的訓練和測試錯誤,他們認為情況不應該如此。理論上,可以采用一個淺層網絡,并在其之上堆疊恒等層(這些恒等層的輸出只是在重復輸入),從而產生一個與淺層網絡完全相同的深層網絡。然而,他們的實驗的性能還不能匹配淺層網絡的性能。
為了解決這個問題,他們提出了一種殘差塊構成的網絡。殘差塊由兩個或三個順序卷積層和一個單獨的并行恒等、(中繼器)快捷連接組成,連接第一層的輸入和最后一層的輸出。在下面的截圖中可以看到三種類型的殘差塊。

從左到右:原始殘差塊、原始瓶頸殘差塊、預激活殘差塊、預激活瓶頸殘差塊
每個塊有兩條并行路徑。左邊的路徑與其他網絡相似,由順序卷積層+批標準化組成。右邊的路徑包含恒等快捷連接(也稱為跳躍連接)。這兩條路徑通過元素依次求和進行合并。也就是說,左右張量具有相同的形狀,第一個張量的一個元素被加到第二個張量中相同位置的元素上。輸出是一個與輸入形狀相同的張量。實際上,前向傳播塊學習的特征,同時也傳播原始的未修改的信號。這樣,可以更接近作者所描述的原始場景。由于有了跳躍連接,網絡可以決定跳過一些卷積層,這實際上減少了它自己的深度。殘差塊使用這樣的方式填充,使塊的輸入和輸出具有相同的尺寸。多虧了這一點,可以為任意深度的網絡堆疊任意數量的塊。
現在,看看圖中的塊有什么不同:
- 第一個塊包含兩個3×3的卷積層。這是原始的殘差塊,但如果層很寬,堆疊多個塊會使計算代價昂貴。
- 第二個塊與第一個塊等價,但它使用了瓶頸層。首先,使用一個1×1卷積來向下采樣輸入volume的深度(見第2章)。然后,對被減少的輸入應用一個3×3(瓶頸)卷積。最后,用另一個1×1的卷積將輸出擴展到所需的深度。這一層的計算開銷比第一層少。
- 第三個塊是由同一作者于2016年發布的最新版本(“Identity Mappings in Deep Residual Networks”,https://arxiv.org/abs/1603.05027)。它使用預激活,其中批標準化和激活函數位于卷積層之前。乍一看,這可能有些奇怪,但是由于這種設計,跳躍連接路徑可以在整個網絡中不間斷地運行。這與其他殘差塊相反,其中至少有一個激活函數位于跳躍連接的路徑上。堆疊的殘差塊的組合仍然具有順序正確的層。
- 第四個塊是第三個塊的瓶頸版本。其原理與瓶頸殘差層v1相同。
在下表中,可以看到論文作者提出的網絡家族。

最流行的殘差網絡家族。殘差塊用圓角矩形表示
它們的一些特性如下:
- 它們從一個步長為2的7×7卷積層開始,然后是3×3最大池化層。這一層也作為一個向下采樣的步驟——與輸入的224×224相比,網絡的其余部分開始于一個更小的56×56的切片。
- 網絡其余部分的下采樣采用改進的步長為2的殘差塊實現。
- 平均池化下采樣在所有殘差塊之后,1000單元全連接softmax層之前輸出。
ResNet家族網絡會流行,不僅因為它們的準確率,還因為它們相對簡單和殘差塊的通用性。如前所述,由于填充,殘差塊的輸入和輸出形狀可以是相同的。可以將殘差塊按不同的配置堆疊起來,以解決訓練集大小和輸入維數范圍廣泛的各種問題。由于這種普遍性,我們將在下一節中實現ResNet的一個示例。
實現殘差塊
本節使用PyTorch 1.3.1和torchvision
0.4.2實現一個預激活ResNet來對CIFAR-10圖像進行分類。讓我們開始吧。
1)和往常一樣,從導入開始。注意,使用簡短的F
表示PyTorch的功能模塊(https://pytorch.org/docs/stable/nn.html#torch-nn-functional):

2)定義預激活常規(非瓶頸)殘差塊。將其實現為nn.Module
——所有神經網絡模塊的基類。從類定義和__init__
方法開始:

在__init__
方法中,只定義可學習的塊組件——包括卷積和批標準化操作。另外,請注意實現shortcut
連接的方式。如果輸入維數和輸出維數相同,可以直接使用輸入張量實現快捷連接。但是,如果維度不同,必須在與主路徑相同的步長和輸出通道下,用1×1卷積來轉換輸入。維度可能因高度/寬度(stride !=1
)或深度(in_slices !=self.expansion * slices
)而不同。self.expansion
是一個超參數,它包含在原始ResNet實現中。它允許擴展殘差塊的輸出深度。
3)實際的數據傳播是通過forward
方法實現的(請注意縮進,因為它是PreActivationBlock
的成員):

使用函數F.relu
表示激活函數,因為它沒有可學習的參數。然后,如果跳躍連接是一個卷積而不是恒等連接(也就是說,塊的輸入/輸出維數不同),將重用F.relu (self.bn_1(x))
為快捷連接添加非線性和批標準化。否則,我們只是重復輸入。
4)實現殘差塊的瓶頸版本。使用與非瓶頸實現相同的模板。從類定義和__init__
方法開始:

expansion
參數在原始實現之后為4。self.conv_1
卷積運算表示1×1下采樣瓶頸連接,self.conv_2
是真實的卷積,self.conv_3
是上采樣1×1卷積。快捷機制遵循與PreActivationBlock
中相同的邏輯。
5)實現PreActivationBottleneckBlock.forward
方法。同樣,它遵循與PreActivationBlock
相同的邏輯:

6)實現殘差網絡本身。從類定義(它繼承了nn. Module
)和__init__
方法開始:

網絡中包含四組殘差塊,就像最初的實現一樣。每組的塊數由num_blocks
參數指定。最初的卷積使用的是步長為1的3×3過濾器,而不是原來的步長為2的7×7過濾器。這是因為32×32的CIFAR-10圖像遠小于224×224的ImageNet圖像,沒有必要進行下采樣。
7)實現PreActivationResNet._make_group
方法,該方法創建一個殘差塊組。組中的所有塊的步長都為1,除了第一個塊,其中stride
作為參數提供:

8)實現PreActivationResNet.forward
方法,通過網絡傳播數據。可以看到在全連接的最后一層之前的下行采樣平均池化:

9)一旦完成網絡,可以實現幾種ResNet配置。以下為ResNet34
,它具有34個卷積層,分組在[3,4,6,3]
的非瓶頸殘差塊中:

10)最后,可以訓練網絡。從定義訓練和測試數據集開始。我們不會詳細討論實現的細節,因為已經在第2章中看到了一個類似的場景。用4個像素填充樣本來增加訓練集,然后從中隨機取出32×32的裁剪。具體實現如下:

11)然后,實例化網絡模型和訓練參數——交叉熵損失和Adam優化器:

12)可以為EPOCHS
個epoch訓練網絡。train_model
、test_model
和plot_accuracy
函數與2.2.1節中的一樣,在這里不再重復它們的實現。代碼如下:

并且在下圖中,可以看到15次迭代(訓練可能需要一段時間)的測試準確率。

15個epoch的測量準確率
本節中的代碼部分基于https://github.com/kuangliu/pytorch-cifar中的預激活ResNet實現。
本節討論了各種類型的ResNet,然后使用PyTorch實現一個ResNet。在下一節中,討論Inception網絡——另一個網絡家族,它將并行連接的使用提升到一個新的水平。