如果按照李沐老師書上來,學完 VGG 后還有 NiN 和 GoogLeNet 要學,但是這兩個我之前聽都沒聽過,而且我看到我導師有發過 ResNet 相關的論文,就想跳過它們直接看后面的內容。
現在看來這不算是不踏實,因為李沐老師說如果卷積神經網絡只學一個架構的話,那就學這個 ResNet(Residual Network)。
隨著我們設計越來越深的網絡,深刻理解“新添加的層如何提升神經網絡的性能”變得至關重要。
加了更多層一定更有用嗎?如果不是的話,怎么樣加入新層可以有效提高精度呢?
我們通過下圖來進行理解。以前我們在網絡加入新的卷積層或者全連接層有點像左圖中的非嵌套函數類。
盡管隨著層數變多(函數 f 1 ? f 6 f_1-f_6 f1??f6?),能覆蓋的最優值范圍變大,但不一定能很有效的接近全局最優(藍色五角星)。
例如左圖中實際上加到 f 3 f_3 f3? 時的最優值距離五角星更近。
針對這一問題,何愷明等人(2016)提出了殘差網絡,其核心思想是:每個附加層都應該更容易地包含原始函數作為其元素之一。
就像右圖中的嵌套函數類,每次新加入函數都能保證不會離五角星更遠,進而一步一步逼近全局最優。
下面我們看看如何實現“嵌套”。
一、殘差塊
塊的思想我們在 VGG 中就了解過,可以幫助我們設計深層網絡。
以前我們是通過串聯起各層來擴大函數類(下左圖),而殘差塊(下右圖)通過加入一側的快速通道,來得到 f ( x ) = x + g ( x ) f(x)=x+g(x) f(x)=x+g(x) 的結構。
如此的話,就算虛線框中的 g ( x ) g(x) g(x) 沒有起到效果,我們也不會退步。
如果虛線框中的各層使得通道數改變,我們就需要加入 1$\times$1 卷積層來進行調整,以保證能加法順利進行。
對于上圖這類特殊的架構,我們需要采用自定義層的方式來實現。
class Residual(nn.Module): # 定義殘差塊def __init__(self, input_channels, num_channels,use_1x1conv=False, strides=1):super().__init__()self.conv1 = nn.Conv2d(input_channels, num_channels,kernel_size=3, padding=1, stride=strides)self.conv2 = nn.Conv2d(num_channels, num_channels,kernel_size=3, padding=1)if use_1x1conv:self.conv3 = nn.Conv2d(input_channels, num_channels,kernel_size=1, stride=strides)else:self.conv3 = Noneself.bn1 = nn.BatchNorm2d(num_channels)self.bn2 = nn.BatchNorm2d(num_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)Y += Xreturn F.relu(Y)
ResNet 沿用了 VGG 完整的 3$\times$3 卷積層設計。殘差塊里首先有兩個相同輸出通道數的卷積層,每個卷積層后面接一個批量規范化層和激活函數。
上述代碼通過調整參數use_1x1conv
參數的取值,來決定是否添加 1$\times$1 卷積層。
一般我們在增加通道數時,我們會通過調整strides
來使得高寬減半。
實際上,我們還可以改變塊中組件的位置,可得到各種殘差塊的變體。
二、ResNet 模型
ResNet 的第一層為輸出通道數 64、步幅 2 的 7$\times 7 卷積層,隨后接 B N 層和步幅為 2 的 3 7 卷積層,隨后接 BN 層和步幅為 2 的 3 7卷積層,隨后接BN層和步幅為2的3\times$3 的最大匯聚層。
b1 = 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))
之后使用 4 個由殘差塊組成的模塊,每個模塊由若干個同樣輸出通道數的殘差塊組成。
第一個模塊的通道數同輸入通道數一致。由于之前已經使用了步幅為 2 的最大匯聚層,因此無需減小高和寬。
之后的每個模塊在第一個殘差塊里將上一個模塊的通道數翻倍,并將高和寬減半。
def resnet_block(input_channels, num_channels, num_residuals,first_block=False): # 生成由殘差塊組成的模塊blk = []for i in range(num_residuals):# 除了第一個模塊,其他模塊的第一個殘差塊需要寬高減半if i == 0 and not first_block:blk.append(Residual(input_channels, num_channels,use_1x1conv=True, strides=2))else:blk.append(Residual(num_channels, num_channels))return blk
我們這里每個模塊使用 2 個殘差塊,其中其第一個模塊使用first_block
參數來避免寬高減半。
# b2 不需要通道數翻倍,寬高減半b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))b3 = nn.Sequential(*resnet_block(64, 128, 2))b4 = nn.Sequential(*resnet_block(128, 256, 2))b5 = nn.Sequential(*resnet_block(256, 512, 2))
最后,加入自適應平均匯聚層、展平層和全連接輸出層。AdaptiveAvgPool2d
的使用可以保證最后的輸出為 (1, 1),不用去管池化窗口的大小。
net = nn.Sequential(b1, b2, b3, b4, b5,nn.AdaptiveAvgPool2d((1, 1)),nn.Flatten(), nn.Linear(512, 10))
4 個模塊,每個模塊兩個殘差塊,一個殘差塊 2 個卷積層,加上最初的 7$\times$7 卷積層和最后的全連接層,共 18 層,故上述模型通常稱為 ResNet-18。
在訓練模型之前,我們來觀察一下各個模塊的輸入形狀是如何變化的。
# 查看各模塊輸出形狀X = torch.rand(size=(1, 1, 224, 224))for layer in net:X = layer(X)print(layer.__class__.__name__, 'output shape:\t', X.shape)
---------------------------------
Sequential output shape: torch.Size([1, 64, 56, 56])
Sequential output shape: torch.Size([1, 64, 56, 56])
Sequential output shape: torch.Size([1, 128, 28, 28])
Sequential output shape: torch.Size([1, 256, 14, 14])
Sequential output shape: torch.Size([1, 512, 7, 7])
AdaptiveAvgPool2d output shape: torch.Size([1, 512, 1, 1])
Flatten output shape: torch.Size([1, 512])
Linear output shape: torch.Size([1, 10])
第一個模塊出來后是 56$\times$56,我開始算不到,因為光算卷積就已經是小數了,沒往下算。后面上網查了下,發現是向下取整的,才明白。
這里放上尺寸的計算公式吧,參考這個:https://www.jianshu.com/p/612edc845ad5
卷積后,池化后尺寸計算公式:
(圖像尺寸-卷積核尺寸 + 2*填充值)/步長 +1
(圖像尺寸-池化窗尺寸 + 2*填充值)/步長 +1
后面 3 個模塊都是通道數加倍,寬高減半,減為 7 × \times × 7 后,最后通過匯聚層變為 1 × \times × 1,聚集所有特征。
三、訓練模型
同之前一樣,我們在 Fashion-MNIST 數據集上訓練 ResNet。
因為之前定義好了很多訓練相關函數,所以訓練代碼可以非常輕松的寫下來。
我都有點想寫一個自己的工具包了,這樣就不用每次都復制前面的代碼,而是像李沐老師的 d2l 一樣。
lr, num_epochs, batch_size = 0.05, 10, 128 # ResNet使用的參數train_iter, test_iter = load_data_fashion_mnist(batch_size, resize=224)train(net, train_iter, test_iter, num_epochs, lr, try_gpu())
原本書上的batch_size
是 256 的,但是我的 GPU 內存不夠,報錯,調成了 128。
這次訓練是最久的,早知道resize
成更小的尺寸了。我看到沐神說他改成 96 只是為了更快運行,就沒用 96,想同樣用 224,好和前面的模型對比。
訓練結果如下:
第 10 輪的訓練損失為 0.011
第 10 輪的訓練精度為 0.998
第 10 輪的測試集精度為 0.926
運行在 cuda:0 上,處理速度為 228.1 樣本/秒
這次的處理速度只是 VGG 的一半,但是效果是很不錯的,訓練損失僅有 0.011,訓練精度都接近 100%了都,而且測試集精度也不低。
可以看出 ResNet 確實是非常有效的網絡,它對后面的深層網絡也產生了非常深遠的影響。