ResNet详解

569 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

ResNet

深度网络退化问题

在前面的学习网络的学习中,我们可以发现VGG到GoogLeNet,这些网络深度都不高,可能最多20层左右。

img

当增加网络层数后,网络可以进行更加复杂的特征模式的提取,所以当模型更深时理论上可以取得更好的结果,但是我们从上图发现,结果恰恰相反,深度网络出现了退化问题,56 层的网络比20层的网络效果要差,这会不会是过拟合问题?首先排除,因为在测试集上56层网络误差,有收敛,依旧比较高。网络中存在梯度消失或者爆炸的问题,模型越深,越难训练。

残差

怎么解决梯度消失或者爆炸的问题呢?从一种角度考虑:如果我们在基础模型上加新的层,最坏的情况:如果新的层不学习,是不是新的模型和基础模型一样了,至少不会更差。“什么都不学习”,学术上被称为恒等映射(Identity Mapping),用数学表达式表示为:H(x)=xH(x) = x

残差块结构图如下

img

假设输入为 xx,学习到特征结果记为 H(x)H(x)

我们可以根据结构发现:H(x)=F(x)+xH(x)=F(x)+xF(x)F(x)是原始的学习特征,极端情况下原始学习特征F(x)=0F(x)=0,那么最后结果为 H(x)=xH(x)=x,此时模型就什么都不学习,做了恒等映射。实际上F(x)F(x)不会为0,这也会使得堆积层在输入特征基础上学习到新的特征,从而拥有更好的性能,尽可能去拟合我们期望的值。

具体实现上,残差块有多种构建方式,如下图所示。

image.png

左侧模型:当经过3×33\times3 卷积层后如果没有改变输入 xx 的维度,那么他们之间可以相加。

右侧模型:当经过3×33\times3 卷积层后如果改变输入 xx 的维度,采用1×11\times1 卷积层增加维度,可以相加。

class Residual(nn.Module):  #@save
    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 = None
        self.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 += X
        return F.relu(Y)

网络结构

ResNet 网络是参考了VGG19网络,在其基础上进行了修改,加入残差单元。与 VGG19 不同的是,ResNet 用global average pool层替换了全连接层。在图中也会看到输入形状发生变化(虚线表示形状发生变化),残差就会使用第二种方式1×11\times1卷积层增加维度保证可以相加。ResNet的一个重要设计原则是:当输入形状大小降低一半时,输入通道的数量增加一倍,这保持了网络层的复杂度,这样就可以构建更深的网络。

img

不同深度ResNet结构图如下:

img

从表中可以看到,对于 18-layer 和 34-layer 的 ResNet,其进行的两层间(3×33\times3)的残差学习,当网络更深时,其进行的是三层间的残差学习,三层卷积核分别是1×11\times13×33\times31×11\times1

为什么会这样子?

选择64-layer 模型与152-layer模型 其中一个残差块比较参数量:

64-layer:3×3×64×64+3×3×64×64=737283\times3\times64\times64+3\times3\times64\times64=73728

152-layer:1×1×64×256+3×3×64×64+1×1×64×256=696321\times1\times64\times256+3\times3\times64\times64+1\times1\times64\times256=69632

两者比较,深度更深的网络,参数量减少了,在算力有限的情况下,深层网络中的残差基础块应该减少算力消耗。

img

作者对比18-layer和34-layer的网络效果,如上图所示。可以看到普通的网络出现退化现象,但是 ResNet 很好的解决了退化问题。

其实还有更优的残差结构,就是 pre-activation 字面意思就是将激活函数提到前面来,改进的方向就是:对BatchNorm、ReLU和卷积层的位置进行调整。

图7 BatchNorm、ReLU和卷积层不同位置的比较

根据作者们的证明和分析,图中的e能获得很大的收益,在CIFAR-10上的表现最好。

代码

第一个模块:输出通道数为64、步幅为2的7×77 \times 7卷积层后,接步幅为2的3×33 \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))

构建ResNet模块部分,封装成函数形式:

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

其实在每次输入形状发生变化时候,其实都是需要使用第二种残差单元,从ResNet-34可以看出第一个残差块是不需要使用第二种。

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))
net = nn.Sequential(b1, b2, b3, b4, b5,
                    nn.AdaptiveAvgPool2d((1,1)),
                    nn.Flatten(), nn.Linear(512, 10))

参考: