ResNet
Last updated
Last updated
残差网络(Residual Network, ResNet)是何恺明等人于 2015 年提出的一系列网络结构,并借此获得了 ILSVRC 2015 数个项目的冠军。 为了解决深层网络难以训练的问题,ResNet 引入了残差学习的概念,并在网络结构中引入了“超前”连接,让激活值可以经过较少的卷积层就可以传到输出。
实践证明这种方式是非常有效的。 使用残差结构以后,更深层的网络可以训练出更高的分类准确率。
在 ResNet 出现之前,改进神经网络的主流逻辑是堆叠更多的层,加深网络的结构。 理论上,堆叠更多的层可以增强网络的表达能力,深层网络应该可以取得不差于浅层网络的性能。 然而实验却给出了相反的结论:使用相同的梯度下降方法对网络进行训练,深层网络取得的分类精度反而低于浅层网络。 这说明,虽然理论上深层网络的表达能力增强了,或者说可表达的函数的空间变大了,但是在这个更大的函数空间中找到最优解却更为困难。
在论文中,作者将这种现象称为深层网络的退化(degradation)现象。
基本模块使用两个卷积层构成残差项,每个卷积层都使用 3x3 的卷积核且附带 BN,第一个卷积层后附带 ReLU,第二个卷积层后不使用 ReLU,而是对求和的结果使用 ReLU 。
瓶颈模块使用三个卷积层构成残差项,其中第一和第三个卷积层使用 1x1 的卷积层对通道数进行变换,中间的卷积层在低通道数的情况下使用 3x3 的卷积核扩展感受野。这种设计主要是为了降低卷积层的运算量,因为卷积层的运算量正比于输入和输出通道数的乘积。与基本模块类似,前两个卷积层后连接有 BN 和 ReLU 层,第三个卷积后连接 BN 层并且在求和后应用 ReLU。
残差模块中的求和是逐个元素进行的。如果我们的卷积层使用 stride=1 和 padding=1 的配置,那么残差模块输出的特征图的形状就和输入特征图的形状相同,自然可以求和。
随着网络层数的加深,我们通常会降低特征图的空间分辨率,并加特征的通道数。例如输入 56x56 大小,64 通道的特征,输出 28x28 大小,128 通道的特征图。
为了保证求和的两个特征图在形状上完全相同,我们需要同时对直通分支和残差分支进行相同的操作。针对残差分支,我们通常在第一个卷积层使用 stride=2 进行空间降采样,并扩展输出的通道数。针对直通分支,我们会额外增加一个 1x1 的卷积,使用 stride=2 降低空间分辨率,并增加输出的通道数。
一个完整的 ResNet 网络由若干残差模块连接构成,残差块的个数决定了网络的总层数,标准配置有 18、34、50、101、152 五种,在 ResNet-18 和 ResNet-34 中,残差模块使用基本模块,而在深层的 ResNet 结构中,残差模块使用瓶颈模块,以控制网络的总计算量。五种配置的详细结构如下图所示:
虽然层数有所不同,五种配置都可以分为五个阶段。第一个阶段为普通的卷积层,可以将 224x224 的图片转换为 112x112 分辨率、64 通道的特征图。
随后的四个阶段都是由残差模块级联而成的,在每个阶段的开始,我们需要对特征图进行空间降采样,并扩展通道数。其中第二阶段稍有特殊,在阶段开始使用池化进行降采样,其余阶段都是通过 stride=2 的卷积曾进行降采样。
经过五个阶段的降采样,224x224 的图象被缩小为 7x7 分辨率(1/32 比例),512 或 2048 通道的特征图。随后,一个 global average pooling 层和一个全连接层输出 1000 类的条件概率。
五种配置的不同之处仅在于残差模块的选取以及残差模块的个数。
torchvision 在 resnet.py 中实现了上述五种标准的 ResNet 结构。 用户可以通过类似 model = resnet18()
的代码生成模型的实例。 这份代码实现了 ResNet 中的两种残差模块——基本模块 BasicBlock
和瓶颈模块 Bottleneck
,并根据不同 ResNet 结构中残差模块的数量实现了五种结构。下面简单对实现进行讲解。
resnet
子模块对外有五个主要接口: resnet18
,resnet34
,resnet50
,resnet101
,resnet152
(还有一些 ResNeXt 相关的,这里暂时也不考虑)分别对应五种不同的 ResNet 结构。这五个接口是五个函数接口,我们以 ResNet18 举例,它的实现如下:
当用户调用 resnet18
时,程序会根据预先设置的残差模块类型 BasicBlock
以及四个阶段中残差模块的个数 [2, 2, 2, 2]
根据工厂函数 _resnet
构建一个 ResNet18 的实例。 其他函数的实现与 resnet18
类似,只不过使用的模块类型和残差模块的个数有所不同。
工厂函数 _resnet
中调用了工厂类 ResNet
实现 ResNet 的计算逻辑,并处理了加载预训练参数的逻辑。
ResNet
类型是 ResNet 模型的主体实现,它的前传函数给出了 ResNet 从输入到输出的计算逻辑,如下所示。 为了清楚,我们将每段代码的具体意义以注释的形式标注在对应的位置。
工厂类型 ResNet
的实现非常直接,可以很容易地和我们上面讲到的 ResNet 结构对应起来。
conv1 的部分比较简单,conv2 到 conv5 的部分则通过 _make_layer
生成,并在 _forward_impl
函数中函数中串联起来。 _make_layer
将多个残差模块串联在一起,构成一个 ResNet 的 conv 阶段,具体的实现及相应的注释如下:
下面我们简要介绍下 BasicBlock 的实现,BottleNeck 和 BasicBlock 的实现逻辑类似。 都是通过几个 Conv2D 卷积层串连实现残差部分。 如果需要降采样,则 conv1 的 stride 在 __init__
中配置为 2,否则为 1。 同时,直通通路会使用 _make_layer
中生成并传入的 downsample
模块减半空间分辨率,并倍增通道数。 需要注意的是,最后一个 ReLU 再直通和残差求和之后,而不是求和之前。
作为目前最实用的模型之一,ResNet 的一些细节改进也层出不穷。 本节介绍一些针对 ResNet 结构的改进,在通用或者特定的情况下可以提高图像分类准确率。
原始的 ResNet 会使用 1x1,stride=2 的卷积层对特征图进行空间上的降采样,产生长宽仅有 1/2 的下一层特征图。 这种降采样方式会在两种场合下使用:
如果使用这种卷积层,前一层特征图上 3/4 的位置的特征值并没有参与运算。 为了能更充分地利用前一层产生的特征,我们可以
针对直通分支,我们先使用 Average Pooling 进行降采样,再使用 1x1 的卷积层转换特征的通道数。
针对残差分支,我们应用 1x1 卷积降低通道数时使用 stride=1,即不进行空间降采样,在第二个卷积层,即 3x3 的卷积层再使用 stride=2 进行降采样。torchvision 工具包采取了这种方式。
这两种改进方式都以少量增加的计算量为代价,实现了对前层特征的充分利用。
针对残差分支改进后的结构通称为 ResNet v1b,针对二者都进行改进的结构称为 ResNet v1d。
我们称残差块之前的结构为 stem (茎)。 在标准的 RseNet 结构中,stem 由一个卷积层和一个池化层构成,其中卷积核的大小为 7x7。
对 ResNet 结构的一种改进使用 3 个 3x3 的卷积层替代这个 7x7 的卷积层,可以在相同感受野的情况下增加层数并减少参数个数。类似的技巧在 VGGNet 、Inception 中也有所使用。 在原始的 ResNet 结构中,第一个卷积层是 64 通道的,如果使用 3 个 3x3 的卷积层,则前两个卷积层会使用 32 通道,第三个卷积层使用 64 通道。
另外,如果将 ResNet 应用于 CIFAR10 等图像分辨率较小的数据集,也可以使用单个 3x3 卷积代替 7x7 的卷积。
基于 v1b 进行 stem 改进后的结构也通称为 ResNet v1c。
ResNet 的提前激活版本是何恺明等人与 2016 年 ECCV 上提出的一种对 ResNet 的改进。在原始的 ResNet 结构中,残差分支和直通分支求和后,会应用 ReLU 激活函数,之后再输入下一个残差模块。 如果我们剔除残差分支,单独考虑直通分支,出去降采样等操作之外,直通分支上有一系列的非线性函数。
论文认为,直通分支对网络的训练是非常重要的,因而将 ReLU 从求和后移动到了残差分支。这样直通分支上就没有任何非线性函数,输入信号也可以通过直通分支传递到网络的任意深度。
这种改进也通称为 ResNet v2。
ResNet 原始论文: Deep Residual Learning for Image Recognition
ResNet v2: Identity Mappings in Deep Residual Networks
我们可以从理论的角度更清楚地阐述退化问题。 考虑一个浅层的神经网络,它表达了一个含参数的函数 。 如果我们叠加一个新的层 ,则这个深了一层的网络表达了两个函数的复合函数,即 。
如果我们通过设置参数 使得 成为一个恒等函数,例如使用恒等映射对应的卷积核,那么这个深一层的网络就可以表达出所有浅层网络可以表达的函数。 如果我们对使 产生一些变化,那么复合函数 就可以表达出更多的函数,而如果这些函数中有可以提高分类精度的函数,那么深层网络就可以在数据集上取得更高的精度。 然而实验结果却意味着,直接学习出这样的 或许并不容易。
基于这个观察,论文作者提出了残差学习的概念。既然让优化器学习出一个近似恒等的映射 并不容易,那么我们将 显式表达成一个 的形式,即恒等项与一个复杂项的求和。当 为恒零函数时, 可以表达一个恒等映射,而作者猜想,通过学习 得到想要的映射的难度要低于直接学习 $$g$ 。这就是残差学习的概念。
残差形式可以很直接地被一个神经网络表示,我们只需要用若干层叠加的子网络表示 部分,再将这个子网络的输入和输出相加,就可以让这个网络表示 。
在 ResNet 结构中,残差块 的实现方式有两种,一种称为基本模块(basic block)一种称为瓶颈模块(bottleneck block)。