论文地址:Deep Residual Learning for Image Recognition
参考:撑起计算机视觉半边天的ResNet【论文精读】、ResNet论文逐段精读【论文精读】、【李沐论文精读系列】
一、导论
深度神经网络的优点:可以加很多层把网络变得特别深,然后不同程度的层会得到不同等级的feature,比如低级的视觉特征或者是高级的语义特征。但是学一个好的网络,就是简简单单的把所有网络堆在一起就行了吗?如果这样,网络做深就行了。
提出问题:随着网络越来越深,梯度就会出现爆炸或者消失
- 解决办法就是:1、在初始化的时候要做好一点,就是权重在随机初始化的时候,权重不要特别大也不要特别小。2、在中间加入一些normalization,包括BN(batch normalization)可以使得校验每个层之间的那些输出和他的梯度的均值和方差相对来说比较深的网络是可以训练的,避免有一些层特别大,有一些层特别小。使用了这些技术之后是能够训练(能够收敛),虽然现在能够收敛了,但是当网络变深的时候,性能其实是变差的(精度会变差)
- 文章提出出现精度变差的问题不是因为层数变多了,模型变复杂了导致的过拟合,而是因为训练误差也变高了(overfitting是说训练误差变得很低,但是测试误差变得很高),训练误差和测试误差都变高了,所以他不是overfitting。虽然网络是收敛的,但是好像没有训练出一个好的结果
深入讲述了深度增加了之后精度也会变差
- 考虑一个比较浅一点的网络和他对应的比较深的版本(在浅的网络中再多加一些层进去),如果钱的网络效果还不错的话,神的网络是不应该变差的:深的网络新加的那些层,总是可以把这些层学习的变成一个identity mapping(输入是x,输出也是x,等价于可以把一些权重学成比如说简单的n分之一,是的输入和输出是一一对应的),但是实际情况是,虽然理论上权重是可以学习成这样,但是实际上做不到:假设让SGD去优化,深层学到一个跟那些浅层网络精度比较好的一样的结果,上面的层变成identity(相对于浅层神经网络,深层神经网络中多加的那些层全部变成identity),这样的话精度不应该会变差,应该是跟浅层神经网络是一样的,但是实际上SGD找不到这种最优解。
- 这篇文章提出显式地构造出一个identity mapping,使得深层的神经网络不会变的比相对较浅的神经网络更差,它将其称为deep residual learning framework。
- 要学的东西叫做,假设现在已经有了一个浅的神经网络,他的输出是,然后要在这个浅的神经网络上面再新加一些层,让它变得更深。新加的那些层不要直接去学,而是应该去学,是原始的浅层神经网络已经学到的一些东西,新加的层不要重新去学习,而是去学习学到的东西和真实的东西之间的残差,最后整个神经网络的输出等价于浅层神经网络的输出x和新加的神经网络学习残差的输出之和,将优化目标从转变成为了。
- 上图中最下面的红色方框表示所要学习的
- 蓝色方框表示原始的浅层神经网络
- 红色阴影方框表示新加的层
- o表示最终整个神经网络的输出
- 这样的好处是:只是加了一个东西进来,没有任何可以学的参数,不会增加任何的模型复杂度,也不会使计算变得更加复杂,而且这个网络跟之前一样,也是可以训练的,没有任何改变。
下面这张图就对应了上一张图的简笔画。
二、related work
残差连接如何处理输入和输出的形状是不同的情况
- 第一个方案是在输入和输出上分别添加一些额外的0,使得这两个形状能够对应起来然后可以相加
- 第二个方案是之前提到过的全连接怎么做投影,做到卷积上,是通过一个叫做1*1的卷积层,这个卷积层的特点是在空间维度上不做任何东西,主要是在通道维度上做改变。所以只要选取一个1*1的卷积使得输出通道是输入通道的两倍,这样就能将残差连接的输入和输出进行对比了。在ResNet中,如果把输出通道数翻了两倍,那么输入的高和宽通常都会被减半,所以在做1*1的卷积的时候,同样也会使步幅为2,这样的话使得高宽和通道上都能够匹配上。
implementation中讲了实验的一些细节
- 把短边随机的采样到256和480(AlexNet是直接将短边变成256,而这里是随机的)。随机放的比较大的好处是做随机切割,切割成224*224的时候,随机性会更多一点
- 将每一个pixel的均值都减掉了
- 使用了颜色的增强(AlexNet上用的是PCA,现在我们所使用的是比较简单的RGB上面的,调节各个地方的亮度、饱和度等)
- 使用了BN(batch normalization)
- 所有的权重全部是跟另外一个paper中的一样(作者自己的另外一篇文章)。注意写论文的时候,尽量能够让别人不要去查找别的文献就能够知道你所做的事情
- 批量大小是56,学习率是0.1,然后每一次当错误率比较平的时候除以10
- 模型训练了60*10^4个批量。建议最好不要写这种iteration,因为他跟批量大小是相关的,如果变了一个批量大小,他就会发生改变,所以现在一般会说迭代了多少遍数据,相对来说稳定一点
- 这里没有使用dropout,因为没有全连接层,所以dropout没有太大作用
- 在测试的时候使用了标准的10个crop testing(给定一张测试图片,会在里面随机的或者是按照一定规则的去采样10个图片出来,然后再每个子图上面做预测,最后将结果做平均)。这样的好处是因为训练的时候每次是随机把图片拿出来,测试的时候也大概进行模拟这个过程,另外做10次预测能够降低方差。
- 采样的时候是在不同的分辨率上去做采样,这样在测试的时候做的工作量比较多,但是在实际过程中使用比较少。
三、实验
3.1 不同配置的ResNet结构
- 上表是整个ResNet不同架构之间的构成信息(5个版本)
- 第一个7*7的卷积是一样的
- 接下来的pooling层也是一样的
- 最后的全连接层也是一样的(最后是一个全局的pooling然后再加一个1000的全连接层做输出)
- 不同的架构之间,主要是中间部分不一样,也就是那些复制的卷积层是不同的
- conv2.x:x表示里面有很多不同的层(块)
- 【3*3,64】:46是通道数
- 模型的结构为什么取成表中的结构,论文中并没有细讲,这些超参数是作者自己调出来的,实际上这些参数可以通过一些网络架构的自动选取
- flops:整个网络要计算多少个浮点数运算。卷积层的浮点运算等价于输入的高乘以宽乘以通道数乘以输出通道数再乘以核的窗口的高和宽
3.2 残差结构效果对比
- 上图中比较了18层和34层在有残差连接和没有残差连接的结果
- 左图中,红色曲线表示34的验证精度(或者说是测试精度)
- 左图中,粉色曲线表示的是34的训练精度
- 一开始训练精度是要比测试精度高的,因为在一开始的时候使用了大量的数据增强,使得寻来你误差相对来说是比较大的,而在测试的时候没有做数据增强,噪音比较低,所以一开始的测试误差是比较低的
- 图中曲线的数值部分是由于学习率的下降,每一次乘以0.1,对整个曲线来说下降就比较明显。为什么现在不使用乘0.1这种方法:在什么时候乘时机不好掌控,如果乘的太早,会后期收敛无力,晚一点乘的话,一开始找的方向更准一点,对后期来说是比较好的
- 上图主要是想说明在有残差连接的时候,34比28要好;另外对于34来说,有残差连接会好很多;其次,有了残差连接以后,收敛速度会快很多,核心思想是说,在所有的超参数都一定的情况下,有残差的连接收敛会快,而且后期会好。
3.3 残差结构中,输入输出维度不一致如何处理
A. pad补0,使维度一致;
B. 维度不一致的时候,使其映射到统一维度,比如使用全连接或者是CNN中的1×1卷积(输出通道是输入的两倍)。
C. 不管输入输出维度是否一致,都进行投影映射。(就算输入输出的形状是一样的,一样可以在连接的时候做个1*1的卷积,但是输入和输出通道数是一样的,做一次投影)
从上述结果可以看到,B和C效果差不多,都比A好。但是做映射会增加很多复杂度,考虑到ResNet中大部分情况输入输出维度是一样的(也就是4个模块衔接时通道数会变),作者最后采用了方案B。
3.4 深层ResNet引入瓶颈结构Bottleneck
在ResNet-50及以上的结构中,模型更深了,可以学习更多的参数,所以通道数也要变大。比如前面模型配置表中,ResNet-50/101/152的第一个残差模块输出都是256维,增加了4倍。
如果残差结构还是和之前一样,计算量就增加的太多了(增加16倍),划不来。所以重新设计了Bottleneck结构,将输入从256维降为64维,然后经过一个3×3卷积,再升维回256维。这样操作之后,复杂度和左侧图是差不多的。这也是为啥ResNet-50对比ResNet-34理论计算量变化不大的原因。(实际上1×1卷积计算效率不高,所以ResNet-50计算还是要贵一些)
3.5 代码实现
resnet中残差块有两种:(use_1x1conv=True/False)
- 步幅为2 ,高宽减半,通道数增加。所以shortcut连接部分会加一个1×1卷积层改变通道数
- 步幅为1,高宽不变
残差块代码实现
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
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)#每个bn都有自己的参数要学习,所以需要定义两个
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训练起来比较快?
- 一方面是因为梯度上保持的比较好,新加一些层的话,加的层越多,梯度的乘法就越多,因为梯度比较小,一般是在0附近的高斯分布,所以就会导致在很深的时候就会比较小(梯度消失)。虽然batch normalization或者其他东西能够对这种状况进行改善,但是实际上相对来说还是比较小,但是如果加了一个ResNet的话,它的好处就是在原有的基础上加上了浅层网络的梯度,深层的网络梯度很小没有关系,浅层网络可以进行训练,变成了加法,一个小的数加上一个大的数,相对来说梯度还是会比较大的。也就是说,不管后面新加的层数有多少,前面浅层网络的梯度始终是有用的,这就是从误差反向传播的角度来解释为什么训练的比较快。
- 在CIFAR上面加到了1000层以上,没有做任何特别的regularization,然后效果很好,overfitting有一点点但是不大。SGD收敛是没有意义的,SGD的收敛就是训练不动了,收敛是最好收敛在比较好的地方。做深的时候,用简单的机器训练根本就跑不动,根本就不会得到比较好的结果,所以只看收敛的话意义不大,但是在加了残差连接的情况下,因为梯度比较大,所以就没那么容易收敛,所以导致一直能够往前(SGD的精髓就是能够一直能跑的动,如果哪一天跑不动了,梯度没了就完了,就会卡在一个地方出不去了,所以它的精髓就在于需要梯度够大,要一直能够跑,因为有噪音的存在,所以慢慢的他总是会收敛的,所以只要保证梯度一直够大,其实到最后的结果就会比较好)
为什么ResNet在CIFAR-10那么小的数据集上他的过拟合不那么明显?
虽然模型很深,参数很多,但是因为模型是这么构造的,所以使得他内在的模型复杂度其实不是很高,也就是说,很有可能加了残差链接之后,使得模型的复杂度降低了,一旦模型的复杂度降低了,其实过拟合就没那么严重了
- 所谓的模型复杂度降低了不是说不能够表示别的东西了,而是能够找到一个不那么复杂的模型去拟合数据,就如作者所说,不加残差连接的时候,理论上也能够学出一个有一个identity的东西(不要后面的东西),但是实际上做不到,因为没有引导整个网络这么走的话,其实理论上的结果它根本过不去,所以一定是得手动的把这个结果加进去,使得它更容易训练出一个简单的模型来拟合数据的情况下,等价于把模型的复杂度降低了。