# 5.11 殘差網絡(ResNet)
讓我們先思考一個問題:對神經網絡模型添加新的層,充分訓練后的模型是否只可能更有效地降低訓練誤差?理論上,原模型解的空間只是新模型解的空間的子空間。也就是說,如果我們能將新添加的層訓練成恒等映射`$ f(x) = x $`,新模型和原模型將同樣有效。由于新模型可能得出更優的解來擬合訓練數據集,因此添加層似乎更容易降低訓練誤差。然而在實踐中,添加過多的層后訓練誤差往往不降反升。即使利用批量歸一化帶來的數值穩定性使訓練深層模型更加容易,該問題仍然存在。針對這一問題,何愷明等人提出了殘差網絡(ResNet) [1]。它在2015年的ImageNet圖像識別挑戰賽奪魁,并深刻影響了后來的深度神經網絡的設計。
## 5.11.2 殘差塊
讓我們聚焦于神經網絡局部。如圖5.9所示,設輸入為`$ \boldsymbol{x} $`。假設我們希望學出的理想映射為`$ f(\boldsymbol{x}) $`,從而作為圖5.9上方激活函數的輸入。左圖虛線框中的部分需要直接擬合出該映射`$ f(\boldsymbol{x}) $`,而右圖虛線框中的部分則需要擬合出有關恒等映射的殘差映射`$ f(\boldsymbol{x})-\boldsymbol{x} $`。殘差映射在實際中往往更容易優化。以本節開頭提到的恒等映射作為我們希望學出的理想映射`$ f(\boldsymbol{x}) $`。我們只需將圖5.9中右圖虛線框內上方的加權運算(如仿射)的權重和偏差參數學成0,那么`$ f(\boldsymbol{x}) $`即為恒等映射。實際中,當理想映射`$ f(\boldsymbol{x}) $`極接近于恒等映射時,殘差映射也易于捕捉恒等映射的細微波動。圖5.9右圖也是ResNet的基礎塊,即殘差塊(residual block)。在殘差塊中,輸入可通過跨層的數據線路更快地向前傳播。
:-: 
<div align=center>圖5.9 普通的網絡結構(左)與加入殘差連接的網絡結構(右)</div>
ResNet沿用了VGG全`$ 3\times 3 $`卷積層的設計。殘差塊里首先有2個有相同輸出通道數的`$ 3\times 3 $`卷積層。每個卷積層后接一個批量歸一化層和ReLU激活函數。然后我們將輸入跳過這兩個卷積運算后直接加在最后的ReLU激活函數前。這樣的設計要求兩個卷積層的輸出與輸入形狀一樣,從而可以相加。如果想改變通道數,就需要引入一個額外的`$ 1\times 1 $`卷積層來將輸入變換成需要的形狀后再做相加運算。
殘差塊的實現如下。它可以設定輸出通道數、是否使用額外的`$ 1\times 1 $`卷積層來修改通道數以及卷積層的步幅。
``` python
import time
import torch
from torch import nn, optim
import torch.nn.functional as F
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
class Residual(nn.Module): # 本類已保存在d2lzh_pytorch包中方便以后使用
def __init__(self, in_channels, out_channels, use_1x1conv=False, stride=1):
super(Residual, self).__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, stride=stride)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(out_channels)
self.bn2 = nn.BatchNorm2d(out_channels)
def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
return F.relu(Y + X)
```
下面我們來查看輸入和輸出形狀一致的情況。
``` python
blk = Residual(3, 3)
X = torch.rand((4, 3, 6, 6))
blk(X).shape # torch.Size([4, 3, 6, 6])
```
我們也可以在增加輸出通道數的同時減半輸出的高和寬。
``` python
blk = Residual(3, 6, use_1x1conv=True, stride=2)
blk(X).shape # torch.Size([4, 6, 3, 3])
```
## 5.11.2 ResNet模型
ResNet的前兩層跟之前介紹的GoogLeNet中的一樣:在輸出通道數為64、步幅為2的`$ 7\times 7 $`卷積層后接步幅為2的`$ 3\times 3 $`的最大池化層。不同之處在于ResNet每個卷積層后增加的批量歸一化層。
``` python
net = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
```
GoogLeNet在后面接了4個由Inception塊組成的模塊。ResNet則使用4個由殘差塊組成的模塊,每個模塊使用若干個同樣輸出通道數的殘差塊。第一個模塊的通道數同輸入通道數一致。由于之前已經使用了步幅為2的最大池化層,所以無須減小高和寬。之后的每個模塊在第一個殘差塊里將上一個模塊的通道數翻倍,并將高和寬減半。
下面我們來實現這個模塊。注意,這里對第一個模塊做了特別處理。
``` python
def resnet_block(in_channels, out_channels, num_residuals, first_block=False):
if first_block:
assert in_channels == out_channels # 第一個模塊的通道數同輸入通道數一致
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(in_channels, out_channels, use_1x1conv=True, stride=2))
else:
blk.append(Residual(out_channels, out_channels))
return nn.Sequential(*blk)
```
接著我們為ResNet加入所有殘差塊。這里每個模塊使用兩個殘差塊。
``` python
net.add_module("resnet_block1", resnet_block(64, 64, 2, first_block=True))
net.add_module("resnet_block2", resnet_block(64, 128, 2))
net.add_module("resnet_block3", resnet_block(128, 256, 2))
net.add_module("resnet_block4", resnet_block(256, 512, 2))
```
最后,與GoogLeNet一樣,加入全局平均池化層后接上全連接層輸出。
``` python
net.add_module("global_avg_pool", d2l.GlobalAvgPool2d()) # GlobalAvgPool2d的輸出: (Batch, 512, 1, 1)
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(), nn.Linear(512, 10)))
```
這里每個模塊里有4個卷積層(不計算`$ 1\times 1 $`卷積層),加上最開始的卷積層和最后的全連接層,共計18層。這個模型通常也被稱為ResNet-18。通過配置不同的通道數和模塊里的殘差塊數可以得到不同的ResNet模型,例如更深的含152層的ResNet-152。雖然ResNet的主體架構跟GoogLeNet的類似,但ResNet結構更簡單,修改也更方便。這些因素都導致了ResNet迅速被廣泛使用。
在訓練ResNet之前,我們來觀察一下輸入形狀在ResNet不同模塊之間的變化。
``` python
X = torch.rand((1, 1, 224, 224))
for name, layer in net.named_children():
X = layer(X)
print(name, ' output shape:\t', X.shape)
```
輸出:
```
0 output shape: torch.Size([1, 64, 112, 112])
1 output shape: torch.Size([1, 64, 112, 112])
2 output shape: torch.Size([1, 64, 112, 112])
3 output shape: torch.Size([1, 64, 56, 56])
resnet_block1 output shape: torch.Size([1, 64, 56, 56])
resnet_block2 output shape: torch.Size([1, 128, 28, 28])
resnet_block3 output shape: torch.Size([1, 256, 14, 14])
resnet_block4 output shape: torch.Size([1, 512, 7, 7])
global_avg_pool output shape: torch.Size([1, 512, 1, 1])
fc output shape: torch.Size([1, 10])
```
## 5.11.3 獲取數據和訓練模型
下面我們在Fashion-MNIST數據集上訓練ResNet。
``` python
batch_size = 256
# 如出現“out of memory”的報錯信息,可減小batch_size或resize
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)
```
輸出:
```
training on cuda
epoch 1, loss 0.0015, train acc 0.853, test acc 0.885, time 31.0 sec
epoch 2, loss 0.0010, train acc 0.910, test acc 0.899, time 31.8 sec
epoch 3, loss 0.0008, train acc 0.926, test acc 0.911, time 31.6 sec
epoch 4, loss 0.0007, train acc 0.936, test acc 0.916, time 31.8 sec
epoch 5, loss 0.0006, train acc 0.944, test acc 0.926, time 31.5 sec
```
## 小結
* 殘差塊通過跨層的數據通道從而能夠訓練出有效的深度神經網絡。
* ResNet深刻影響了后來的深度神經網絡的設計。
## 參考文獻
[1] He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep residual learning for image recognition. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 770-778).
[2] He, K., Zhang, X., Ren, S., & Sun, J. (2016, October). Identity mappings in deep residual networks. In European Conference on Computer Vision (pp. 630-645). Springer, Cham.
-----------
> 注:除代碼外本節與原書此節基本相同,[原書傳送門](https://zh.d2l.ai/chapter_convolutional-neural-networks/googlenet.html)
- Home
- Introduce
- 1.深度學習簡介
- 深度學習簡介
- 2.預備知識
- 2.1環境配置
- 2.2數據操作
- 2.3自動求梯度
- 3.深度學習基礎
- 3.1 線性回歸
- 3.2 線性回歸的從零開始實現
- 3.3 線性回歸的簡潔實現
- 3.4 softmax回歸
- 3.5 圖像分類數據集(Fashion-MINST)
- 3.6 softmax回歸的從零開始實現
- 3.7 softmax回歸的簡潔實現
- 3.8 多層感知機
- 3.9 多層感知機的從零開始實現
- 3.10 多層感知機的簡潔實現
- 3.11 模型選擇、反向傳播和計算圖
- 3.12 權重衰減
- 3.13 丟棄法
- 3.14 正向傳播、反向傳播和計算圖
- 3.15 數值穩定性和模型初始化
- 3.16 實戰kaggle比賽:房價預測
- 4 深度學習計算
- 4.1 模型構造
- 4.2 模型參數的訪問、初始化和共享
- 4.3 模型參數的延后初始化
- 4.4 自定義層
- 4.5 讀取和存儲
- 4.6 GPU計算
- 5 卷積神經網絡
- 5.1 二維卷積層
- 5.2 填充和步幅
- 5.3 多輸入通道和多輸出通道
- 5.4 池化層
- 5.5 卷積神經網絡(LeNet)
- 5.6 深度卷積神經網絡(AlexNet)
- 5.7 使用重復元素的網絡(VGG)
- 5.8 網絡中的網絡(NiN)
- 5.9 含并行連結的網絡(GoogLeNet)
- 5.10 批量歸一化
- 5.11 殘差網絡(ResNet)
- 5.12 稠密連接網絡(DenseNet)
- 6 循環神經網絡
- 6.1 語言模型
- 6.2 循環神經網絡
- 6.3 語言模型數據集(周杰倫專輯歌詞)
- 6.4 循環神經網絡的從零開始實現
- 6.5 循環神經網絡的簡單實現
- 6.6 通過時間反向傳播
- 6.7 門控循環單元(GRU)
- 6.8 長短期記憶(LSTM)
- 6.9 深度循環神經網絡
- 6.10 雙向循環神經網絡
- 7 優化算法
- 7.1 優化與深度學習
- 7.2 梯度下降和隨機梯度下降
- 7.3 小批量隨機梯度下降
- 7.4 動量法
- 7.5 AdaGrad算法
- 7.6 RMSProp算法
- 7.7 AdaDelta
- 7.8 Adam算法
- 8 計算性能
- 8.1 命令式和符號式混合編程
- 8.2 異步計算
- 8.3 自動并行計算
- 8.4 多GPU計算
- 9 計算機視覺
- 9.1 圖像增廣
- 9.2 微調
- 9.3 目標檢測和邊界框
- 9.4 錨框
- 10 自然語言處理
- 10.1 詞嵌入(word2vec)
- 10.2 近似訓練
- 10.3 word2vec實現
- 10.4 子詞嵌入(fastText)
- 10.5 全局向量的詞嵌入(Glove)
- 10.6 求近義詞和類比詞
- 10.7 文本情感分類:使用循環神經網絡
- 10.8 文本情感分類:使用卷積網絡
- 10.9 編碼器--解碼器(seq2seq)
- 10.10 束搜索
- 10.11 注意力機制
- 10.12 機器翻譯