經典網絡結構(五):ResNet (殘差網絡)

本節參考:《深度學習之PyTorch物體檢測實戰》
《DIVE INTO DEEP LEARNING》、吳恩達深度學習視頻


讓我們先思考一個問題:對神經網絡模型添加新的層,充分訓練後的模型是否只可能更有效地降低訓練誤差?理論上,原模型解的空間只是新模型解的空間的子空間。也就是說,如果我們能將新添加的層訓練成恆等映射 f(x)=xf(x)=x ,新模型和原模型將同樣有效。由於新模型可能得出更優的解來擬合訓練數據集,因此添加層似乎更容易降低訓練誤差。然而在實踐中,添加過多的層後訓練誤差往往不降反升。即使利用批量歸一化帶來的數值穩定性使訓練深層模型更加容易,該問題仍然存在。

Residual block (殘差塊)

設輸入爲 xx 。假設我們希望學出的理想映射爲 f(x)f(x) ,從而作爲下圖中上方激活函數的輸入。左圖虛線框中的部分需要直接擬合出該映射 f(x)f(x) ,而右圖虛線框中的部分則需要擬合出有關恆等映射的殘差映射 f(x)xf(x)-x 。殘差映射在實際中往往更容易優化。
假如將恆等映射作爲我們希望學出的理想映射 f(x)f(x) 。我們只需將下圖中右圖虛線框內上方的加權運算(如仿射)的權重和偏差參數學成0,那麼 f(x)f(x) 即爲恆等映射。也就是說在引入殘差塊中的 skip connection之後,即使在原有網絡的基礎上多加了兩層網絡,這兩層網絡也很容易學習爲恆等映射可以保證加深層之後至少不會對原有網絡的性能產生影響。實際中,當理想映射 f(x)f(x) 極接近於恆等映射時,殘差映射也易於捕捉恆等映射的細微波動。下圖中右圖也是ResNet的基礎塊,即殘差塊(residual block)。在殘差塊中,輸入可通過跨層的數據線路更快地向前傳播。
同時,通過 skip connection,反向傳播時信號也可以無衰減地傳遞,可以緩解因加深層而導致的梯度消失或者梯度爆炸現象
在這裏插入圖片描述
ResNet沿用了VGG全 3×33×3 卷積層的設計。殘差塊裏首先有2個有相同輸出通道數的 3×33×3 卷積層。每個卷積層後接一個BN層和ReLU激活函數。然後我們將輸入跳過這2個卷積運算後直接加在最後的ReLU激活函數前。

注意:skip connection 是被加在激活函數之前的,同時如果xxf(x)f(x)形狀不同的話,則還要對快捷結構中的xx 添加權重ww或引入1×11×1 卷積層並加上適當的步幅來變換形狀。如果想改變通道數,就需要引入一個額外的 1×11×1 卷積層來將輸入變換成需要的形狀後再做相加運算。因此可以看到在ResNet中用到了很多3×33\times3, padding 爲1的“相同卷積”。

class Residual(nn.Module):
    def __init__(self, in_channel, out_channel, stride=1):
        super(Residual, self).__init__()
        self.bottleneck = nn.Sequential(
            nn.Conv2d(in_channel, out_channel, 3, stride, padding=1, bias=False),
            nn.BatchNorm2d(out_channel),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channel, out_channel, 3, padding=1, bias=False),
            nn.BatchNorm2d(out_channel),
            )
        self.relu = nn.ReLU(inplace=True)
        self.downsample = nn.Sequential(
                nn.Conv2d(in_channel, out_channel, 1, stride),
                nn.BatchNorm2d(out_channel),
            )

    def forward(self, x):
        out = self.bottleneck(x)
        identity = self.downsample(x)
        out += identity
        out = self.relu(out)
        return out

對於比較深的網絡, ResNet論文中介紹了一個“瓶頸”架構來降低模型複雜度:

class Residual(nn.Module):
    def __init__(self, in_channel, out_channel, stride=1):
        super(Residual, self).__init__()
        self.bottleneck = nn.Sequential(
            nn.Conv2d(in_channel, in_channel, 1, bias=False),
            nn.BatchNorm2d(in_channel),
            nn.ReLU(inplace=True),
            nn.Conv2d(in_channel, in_channel, 3, stride, padding=1, bias=False),
            nn.BatchNorm2d(in_channel),
            nn.ReLU(inplace=True),
            nn.Conv2d(in_channel, out_channel, 1, bias=False),
            nn.BatchNorm2d(out_channel),
            )
        self.relu = nn.ReLU(inplace=True)
        self.downsample = nn.Sequential(
                nn.Conv2d(in_channel, out_channel, 1, stride),
                nn.BatchNorm2d(out_channel),
            )

    def forward(self, x):
        out = self.bottleneck(x)
        identity = self.downsample(x)
        out += identity
        out = self.relu(out)
        return out
# 通道數翻倍,寬高減半
net = Residual(3, 6, 2)
x = torch.randn(4, 3, 6, 6)
net(x).shape
torch.Size([4, 6, 3, 3])
# 保持形狀不變
net = Residual(3, 3)
x = torch.randn(4, 3, 6, 6)
net(x).shape
torch.Size([4, 3, 6, 6])

ResNet網絡結構

在這裏插入圖片描述
在這裏插入圖片描述

  • ResNet的前兩層跟之前介紹的GoogLeNet中的一樣:在輸出通道數爲64、步幅爲2的 7×77×7 卷積層後接步幅爲2的 3×33×3 的最大池化層。不同之處在於ResNet每個卷積層後增加的批量歸一化層。
  • GoogLeNet在後面接了4個由Inception塊組成的模塊。ResNet則使用4個由殘差塊組成的模塊,每個模塊使用若干個同樣輸出通道數的殘差塊。第一個模塊的通道數同輸入通道數一致。由於之前已經使用了步幅爲2的最大池化層,所以無須減小高和寬。之後的每個模塊在第一個殘差塊裏將上一個模塊的通道數翻倍,並將高和寬減半
  • 最後,與GoogLeNet一樣,加入全局平均池化層後接上全連接層輸出
def resnet_block(in_channels, out_channels, num_residuals, first_block=False):
    blk = nn.Sequential()
    for i in range(num_residuals):
        blk.add_module(str(i), Residual(in_channels, out_channels, stride=2 if i == 0 and not first_block else 1))
        in_channels = out_channels
    return blk

class ResNet(nn.Module):
    def __init__(self, in_channel, class_num):
        super(ResNet, self).__init__()
        self.stem = nn.Sequential(
            nn.Conv2d(in_channel, 64, 7, stride=2, padding=3),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(3, stride=2, padding=1),
            )
        resnet_blocks = []
        in_channels = [64, 64, 128, 256, 512]
        for i in range(4):
            resnet_blocks += [resnet_block(in_channels[i], in_channels[i + 1], 2, first_block=True if i == 0 else False)]
        self.resnet_blocks = nn.Sequential(*resnet_blocks)
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Linear(512, class_num)

    def forward(self, x):
        out = self.stem(x)
        out = self.resnet_blocks(out)
        out = self.avg_pool(out).view(-1, 512)
        out = self.fc(out)
        
        return out

這裏每個模塊裏有4個卷積層(不計算 1×1 卷積層),加上最開始的卷積層和最後的全連接層,共計18層。這個模型通常也被稱爲ResNet-18。通過配置不同的通道數和模塊裏的殘差塊數可以得到不同的ResNet模型,例如更深的含152層的ResNet-152

改進殘差塊內的結構

作者在 Identity Mappings in Deep Residual Networks 中提出了下面的新結構:
在這裏插入圖片描述
將殘差塊內的“卷積-BN-ReLU”結構改成了“BN-ReLU-卷積”結構,實驗表明這個結構更容易泛化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章