重读经典:完全解析特征学习大杀器ResNet

共 9973字,需浏览 20分钟

 ·

2020-12-02 12:14

↑ 点击蓝字 关注极市平台

作者丨刘昕宸@知乎
来源丨https://zhuanlan.zhihu.com/p/268308900
编辑丨极市平台

极市导读

 

通过堆叠神经网络层数(增加深度)可以非常有效地增强表征,提升特征学习效果,但是会出现深层网络的性能退化问题,ResNet的出现能够解决这个问题。本文用论文解读的方式展现了ResNet的实现方式、分类、目标检测等任务上相比SOTA更好的效果。 >>加入极市CV技术交流群,走在计算机视觉的最前沿

论文标题:Deep Residual Learning for Image Recognition



1 motivation

通过总结前人的经验,我们常会得出这样的结论:通过堆叠神经网络层数(增加深度)可以非常有效地增强表征,提升特征学习效果。
为什么深度的网络表征效果会好?
深度学习很不好解释,大概的解释可以是:网络的不同层可以提取不同抽象层次的特征,越深的层提取的特征越抽象。因此深度网络可以整合low-medium-high各种层次的特征,增强网络表征能力。
那好,我们就直接增加网络深度吧!
但是事情好像并没有那么简单!
梯度优化问题:
我们不禁发问:Is learning better networks as easy as stacking more layers?
首先,深度网络优化是比较困难的,比如会出现梯度爆炸/梯度消失等问题。不过,这个问题已经被normalized initialization和batch normalization等措施解决得差不多了。

退化问题:
好,那就直接上deeper network吧!
但是新问题又来了:deeper network收敛是收敛了,却出现了效果上的degradation
deeper network准确率饱和后,很快就退化了
为什么会这样呢?网络更深了,参数更多了,应该拟合能力更强了才对啊!噢,一定是过拟合了。
但似乎也不是过拟合的问题:
因为56-layer网络(红线)的training error(左图)也比20-layer网络(黄线)要高,这就应该不是过拟合了啊!
那么究竟是什么原因导致了deeper network degradation问题呢?

现在,我们换一种思路来构建deeper network:
the added layers are identity mapping, and the other layers are copied from the learned shallower model.(在原始的浅层网络基础上增加的层视为是identity mapping)
也就是假设浅层网络已经可以得到一个不错的结果了,那我接下来新增加的层啥也不干,只是拟合一个identity mapping,输出就拟合输入,这样总可以吧。
这样的话,我们觉得:这样构建的深层网络至少不应该比它对应的浅层training error要高。对吧。
但是实验又无情地表明:这样却又不能得到(与浅层网络)一样好的结果,甚至还会比它差!
看来,深度网络的优化并不容易的!
总结一下:直觉上深度网络应该会有更好的表征能力,但是事实却是深度网络结果会变差,由此我们认为深度网络的优化部分出了问题,深度网络的参数空间变得更复杂提升了优化的难度。

那么,ResNet来了。
我们就想啊,与其直接拟合一个desired underlying mapping  ,不如让网络尝试拟合一个residual mapping  。
也就是:
原先的映射  ,被转换为了 
我们在这里假设优化残差映射(residual mapping)  是比优化原映射  要容易的。
比如如果现在恒等映射(identity mapping)是最优的,那么似乎通过堆叠一些非线性层的网络将残差映射为0,从而拟合这个恒等映射,最种做法是更容易的。
 可以通过如上图所示的短路连接(shortcut connection)结构来实现。
shortcut就是设计的可以skip几层的结构,在ResNet中就是起到了相当于一个最最简单的identity mapping,其输出被加到了stacked layers的输出上。这样做既没有增加新的参数,也没有增加计算复杂性。
ResNet的具体结构,后面会详细介绍。

接下来,本文在ImageNet和CIFAR-10等数据集上做实验,主要是想验证2个问题:
  1. deep residual nets比它对应版本的plain nets更好优化,training error更低。

  2. deep residual nets能够从更深的网络中获得更好的表征,从而提升分类效果。

2 solution

ResNet想做什么?
learning residual functions with reference to the layer inputs, instead of learning unreferenced functions.
理解不了没关系,接着往下看。

2.1 Residual Learning

前提:如果假设多个非线性层能够渐近一个复杂的函数,那么多个非线性层也一定可以渐近这个残差函数。
令  表示目标拟合函数。
所以与其考虑拟合  ,不如考虑拟合其对应的残差函数  .这两种拟合难度可能是不同的。

回到上面的讨论,如果被增加层能够被构建成identity mapping,那么深层网络的性能至少不应该比其对应的浅层版本要差。
这个表明:网络是在使用多层非线性网络趋近identity mapping做优化这里出了问题。
残差学习的这种方式,使得“如果identity mapping是最优的,网络的优化器直接将残差学习为0”就可以了,这样做是比较简单的。
但其实在真实情况下,identity mapping不一定是最优的映射啊!原文说这种设计仍有意义,这种设计help to precondition the problem.
也就是如果optimal function更接近identity mapping的话,优化器应该能够比较容易找到残差,而不是重新学习一个新的。
后面实验也表明了:残差网络各层输出的标准差是比较低的(如上图,后面会解释),这也验证了在identity mapping的基础上学习残差,确实是会更容易(identity mappings provide reasonable preconditioning.).
这里解释得还是比较含糊,但总结来说就是作者想解释,训练学习残差会有效降低学习的难度,可能据此来解决深层网络的性能退化问题。

2.2 Identity Mapping by Shortcuts

再回顾一下这个著名的残差块图片:
identity mapping实现得非常之简单,直接就用了个shortcut
形式化就是:
 表示residual mapping,比如上图,实际上就是2层网络,也就是 
然后直接将  与  element-wise相加
最后,给  套一个激活函数  .
这么设计(shortcut)有个巨大的好处,就是没有引入任何新的参数,也没有增加计算复杂度。

下面还有2个小问题:
问题1:关于 
因为  是element-wise相加,那么如果  和  维度不一样怎么办?
方案一:直接对  补0.
方案二:增加一个网络层(参数为  ),改变  的维度。即:
事实上,每个shortcut我们都可以加一个映射层  (实现起来可以就是个感知机)。不需要做维度转化时,  就是个方阵。但是后面实验表明,直接shortcut就已经足够好了,不需要再加那么多参数浪费计算资源。
问题2:关于 
 的结构应该是什么样的呢?
 可以是2层或者3层,也可以是更多;但是不要是1层,效果会不好。

最后,shortcut设计不仅针对全连接网络,卷积网络当然也是没问题的!

2.3 网络架构


受VGGNet(左边)启发,设计了34层的plain network(中间),以及其对应的residual network版本(右边)。
注意:中间plain network和右边residual network层数一致,网络参数也可以设计得完全一样(element-wise相加维度不match时直接补0就不会增加任何learnable parameters)。
34-layer plain network设计原则(遵循VGGNet):
  1. for the same output feature map size, the layers have the same number of filters

  2. if the feature map size is halved, the number of filters is doubled so as to preserve the time complexity per layer


3 dataset and experiments

3.1 ImageNet on Classification

3.1.1 与plain network的对比实验
这个实验是核心,为了说明residual network能够非常完美地解决“深度增加带来的degradation”问题!!!
左边是plain network,右边是ResNet;细线是train error,加粗线是val error
Plain network会出现网络的层数增加,train error和val error都会升高
什么原因呢???
首先排除过拟合,因为train error也会升高
其次排除梯度消失,网络中使用了batch normalization,并且作者也做实验验证了梯度的存在
事实上,34-layers plain network也是可以实现比较好的准确率的,这说明网络在一定程度上也是work了的。
作者猜测:We conjecture that the deep plain nets may have exponentially low convergence rates. 层数的提升会在一定程度上指数级别影响收敛速度。
下面是Residual Network与plain network的量化对比:
观察上面两张图,我们可以得出结论:
  1. 而ResNet却真正实现了网络层数增加,train error和val error都降低了,证明了网络深度确实可以帮助提升网络的性能。degradation problem在一定程度上得到了解决。

  2. 相对于plain 34-layers,ResNet 34-layers的top-1 error rate也降低了3.5%。resnet实现了在没有增加任何参数的情况下,获得了更低error rate,网络更加高效。

  3. 从plain/residual 18-layers的比较来看,两者的error rate差不多,但是ResNet却能够收敛得更快。

总结来说就是,ResNet在不增加任何参数的情况下,仅使用shortcuts and zero-padding for matching dimensions结构,就实现了:
解决了degradation problem,更高的准确率,更快的收敛速度
简直太强了!!!

3.1.2 Identity v.s. Projection shortcuts

所谓projection shortcuts,就是:shortcuts包括了一个可学习参数(可以用来对齐维度,使得element-wise相加可以实现):
设计了A,B,C三种实验:
A:单纯使用identity shortcuts:
维度不能对齐时使用zero padding来提升维度
此方案没有增加任何参数
B:仅仅在维度需要对齐时使用projection shortcuts,其余均使用parameter-free的identity shortcuts
C:全部使用projection shortcuts
下面是三种方案的实验结果:
ABC三种方案均明显好于plain版本
C虽然结果稍微优于B、C,但是却引入了大量的参数,增加了时空计算复杂度。
作者认为:projection shortcuts are not essential for addressing the degradation problem.
因此后面的实验仍然采用A或者B结构。

3.1.3 Deeper Bottleneck Architectures.

为了探索更深层的网络,保证训练时间在可控范围内,作者重又设计了bottleneck版本的building block
左边是原版本,右边是bottleneck版本
bottleneck版本是将卷积核换成了1*1,3*3,1*1的size,虽然层数增加到了3层,但是降低了参数量。
作者在这里是想探索深度的真正瓶颈,而不是追求很低的error rate,因此在这里使用了更加精简的bottleneck building block
50-layers:将34-layers的每个2-layer block换成了3-layers bottleneck block
101-layers/152-layers:增加更多的3-layers bottleneck block
网络具体参数可参考如下图:
实验结果如下所示:
网络越深,确实取得了更好的结果。Plain network的degradation problem似乎消失了。

3.2 CIFAR-10实验与分析

实线是test error,虚线是train error
左边是plain network,中间是ResNet,右边是ResNet with 110 layers and 1202 layers.
结论基本与之前一致,但在1202层时,ResNet还是出现了degradation现象(结果比110层差),作者认为是过拟合。

另外:Analysis of Layer Responses
关于response:The responses are the outputs of each  layer, after BN and before other nonlinearity (ReLU/addition).
从上图我们可以直接看出:ResNet较于plain network,一般来说response std更小。
并且:deeper ResNet has smaller magni- tudes of responses
这就说明了:
  1. residual functions(即  ) might be generally closer to zero than the non-residual functions.

  2. When there are more layers, an individual layer of ResNets tends to modify the signal less.(也就是后面逐渐就接近identity mapping,要拟合的残差越来越小,离目标越来越近)


4 code review

ResNet实现非常简单,网上各种实现多如牛毛,这里仅随意找了个实现版本作为对照:
代码基于CIFAR-10的:
2层的BasicBlock:
class BasicBlock(nn.Module):
expansion = 1

def __init__(self, in_planes, planes, stride=1, option='A'):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)

self.shortcut = nn.Sequential()
if stride != 1 or in_planes != planes:
if option == 'A':
"""
For CIFAR10 ResNet paper uses option A.
"""

self.shortcut = LambdaLayer(lambda x:
F.pad(x[:, :, ::2, ::2], (0, 0, 0, 0, planes//4, planes//4), "constant", 0))
elif option == 'B':
self.shortcut = nn.Sequential(
nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(self.expansion * planes)
)

def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(x)
out = F.relu(out)
return out

ResNet骨架:
解释一下:
forward函数中定义resnet骨架:
  1. 首:1层conv

  2. 身:由BasicBlock构成layer1、layer2、layer3,个数分别为  ,因为每个BasicBlock有2层,所以总层数是 

  3. 尾:1层fc

所以总共有  层!
layer1, layer2, layer3输出维度分别是16,32,64
class ResNet(nn.Module):
def __init__(self, block, num_blocks, num_classes=10):
super(ResNet, self).__init__()
self.in_planes = 16

self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(16)
self.layer1 = self._make_layer(block, 16, num_blocks[0], stride=1)
self.layer2 = self._make_layer(block, 32, num_blocks[1], stride=2)
self.layer3 = self._make_layer(block, 64, num_blocks[2], stride=2)
self.linear = nn.Linear(64, num_classes)

self.apply(_weights_init)

def _make_layer(self, block, planes, num_blocks, stride):
strides = [stride] + [1]*(num_blocks-1)
layers = []
for stride in strides:
layers.append(block(self.in_planes, planes, stride))
self.in_planes = planes * block.expansion

return nn.Sequential(*layers)

def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = F.avg_pool2d(out, out.size()[3])
out = out.view(out.size(0), -1)
out = self.linear(out)
return out

最后,像堆积木一样,通过设置layer1、layer2、layer3的BasicBlock个数来堆出不同层的ResNet:
def resnet20():
return ResNet(BasicBlock, [3, 3, 3])


def resnet32():
return ResNet(BasicBlock, [5, 5, 5])


def resnet44():
return ResNet(BasicBlock, [7, 7, 7])


def resnet56():
return ResNet(BasicBlock, [9, 9, 9])


def resnet110():
return ResNet(BasicBlock, [18, 18, 18])


def resnet1202():
return ResNet(BasicBlock, [200, 200, 200])

5 conclusion

ResNet核心就是residual learning和shortcut identity mapping,实现方式极其简单,却取得了极其好的效果,在分类、目标检测等任务上均是大比分领先SOTA,这种非常general的创新是非常不容易的,这也是ResNet备受推崇的原因吧!
另外给我的启示就是:不仅仅是"talk is cheap, show me the code"了,而是"code is also relatively cheap, show me ur sense and thinking"!

推荐阅读



添加极市小助手微信(ID : cvmart2),备注:姓名-学校/公司-研究方向-城市(如:小极-北大-目标检测-深圳),即可申请加入极市目标检测/图像分割/工业检测/人脸/医学影像/3D/SLAM/自动驾驶/超分辨率/姿态估计/ReID/GAN/图像增强/OCR/视频理解等技术交流群:每月大咖直播分享、真实项目需求对接、求职内推、算法竞赛、干货资讯汇总、与 10000+来自港科大、北大、清华、中科院、CMU、腾讯、百度等名校名企视觉开发者互动交流~

△长按添加极市小助手

△长按关注极市平台,获取最新CV干货

觉得有用麻烦给个在看啦~  
浏览 41
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报