ResNet详解
在ResNet网络中有如下几个亮点:
- 提出residual结构(残差结构),并搭建超深的网络结构(突破1000层)
- 使用Batch Normalization加速训练(丢弃dropout)
在ResNet网络提出之前,传统的卷积神经网络都是通过将一系列卷积层与下采样层进行堆叠得到的。但是当堆叠到一定网络深度时,就会出现两个问题。
- 梯度消失或梯度爆炸。
- 退化问题(degradation problem)。
在ResNet论文中说通过数据的预处理以及在网络中使用BN(Batch Normalization)层能够解决梯度消失或者梯度爆炸问题。
但是对于退化问题(随着网络层数的加深,效果还会变差,如下图所示)并没有很好的解决办法。
所以ResNet论文提出了residual结构(残差结构)来减轻退化问题。下图是使用residual结构的卷积网络,可以看到随着网络的不断加深,效果并没有变差,反而变的更好了。
残差结构(residual)
残差指的是什么? 其中ResNet提出了两种mapping:一种是identity mapping,指的就是下图中”弯弯的曲线”,另一种residual mapping,指的就是除了”弯弯的曲线“那部分,所以最后的输出是 y=F(x)+x
identity mapping
顾名思义,就是指本身,也就是公式中的x,而residual mapping指的是“差”,也就是y−x,所以残差指的就是F(x)部分。
下图是论文中给出的两种残差结构。左边的残差结构是针对层数较少网络,例如ResNet18层和ResNet34层网络。右边是针对网络层数较多的网络,例如ResNet101,ResNet152等。为什么深层网络要使用右侧的残差结构呢。因为,右侧的残差结构能够减少网络参数与运算量。同样输入一个channel为256的特征矩阵,如果使用左侧的残差结构需要大约1170648个参数,但如果使用右侧的残差结构只需要69632个参数。明显搭建深层网络时,使用右侧的残差结构更合适。
多年的努力就为了今天写代码(多数代码中你看困惑的地方)
如下图所示,该残差结构的主分支是由两层3x3的卷积层组成,而残差结构右侧的连接线是shortcut分支也称捷径分支(注意为了让主分支上的输出矩阵能够与我们捷径分支上的输出矩阵进行相加,必须保证这两个输出特征矩阵有相同的shape)。如果刚刚仔细观察了ResNet34网络结构图的同学,应该能够发现图中会有一些虚线的残差结构。在原论文中作者只是简单说了这些虚线残差结构有降维的作用,并在捷径分支上通过1x1的卷积核进行降维处理。而下图右侧给出了详细的虚线残差结构,注意下每个卷积层的步距stride,以及捷径分支上的卷积核的个数(与主分支上的卷积核个数相同)。
BasiceBlock:第一个卷积改变图像大小,所以它决定了identity mapping是否下采样(代码中的if)
Bolleneck:第二个卷积改变图像大小,所以它决定了identity mapping是否下采样(代码中的if)
对于我们ResNet18/34/50/101/152,表中conv3_x, conv4_x, conv5_x所对应的一系列残差结构的第一层残差结构都是虚线残差结构。因为这一系列残差结构的第一层都有调整输入特征矩阵shape的使命(将特征矩阵的高和宽缩减为原来的一半,将深度channel调整成下一层残差结构所需要的channel)。为了方便理解,下面给出了ResNet34的网络结构图,图中简单标注了一些信息。
resnet的层次结构图:
很重要,因为resnet都是一个鸟样,主要就是在搭建conv1-5,其中conv1单独拿出来说,很简单
Conv2d(3,64,kernel_size=7,stride=2,padding=3,bias=False)
(pytorch)就是他,你问我为啥,鬼知道为啥,就测试这样结果更好,感受野大。
1)BasicBlock() 残差块 解释
resnet18() 函数调用ResNet() 类,通过输入初始化参数:BasicBlock ,[2,2,2,2],实例化一个resnet18 model: 先看下ResNet()大类,输入哪些初始化话参数就可以实例化为 resnet18 模型:BasicBlock
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = nn.BatchNorm2d(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x # 其实这里不应该叫residual,应该写为:identity mapping = x,用identity mapping代替residual
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
BasicBlock要解决的一个重要问题就是,identity mapping这个直连的维度 和 F(x) 输出的维度不一样无法直接相加的问题:采用一个kernel=1的conv2d卷积核融合并降低通道信息,如果H/W尺度也不一样就设计stride。下面是在ResNet()定义中 定义的一个下采样模块,在BasicBlock实例化的时候作为了输入参数。
stride!=1:要下采样,要改变size inplanes!=planes*block.expansion:维度不同,改变维度(从上面图知道从上到下所有的channel是不变的,所以要壹planes为基准)
2)制作stage的函数 __make_layer() 解释
_make_layer()要解决:根据不同的基本block,完成一个stage 网络结构的构建。
3) __make_layer() 中用到的重要参数 类属性expansion 和 成员变量self.inplanes
BasicBlock()(或Bottleneck())类中的类属性expandsion,用来指定下一个BasicBlock的输入通道是多少。因为就算在stage中,第一个block结束之后,下一个block的输入通道数目已经变化了,已经不是 同一个stage 的 第一个block 的输入通道数目。self.inplanes 的重要作用:self.inplanes一共有在block中两次使用:
每个stage中(一个_make_layer()就是一个stage),第一次使用时,self.inplanes 等于上一个stage的输出通道数,后面self.inplanes都等于同一个数目,就是每个block的输出通道数目。
因为分为BasicBlock()和Bottleneck() 两个基本的block类,对应不同深度的resnet,这两种block最后的输出通道是不一样的,为了标记这两个类输出通道数目的不同,设置了一个类属性expansion。根据类属性expansion和我们指定的输出通道参数planes,可以确定对于这两种block 结束之后的输出通道数目。