第一部分.文献阅读
首先,作者抛出了一个问题:
学习更好的网络就像堆叠更多的层那么简单吗?
回答这个问题的一个很大的障碍就是众所周知的梯度消失/梯度爆炸,它从一开始就阻止了收敛。现在这个问题主要是通过初始归一化和中间层归一化来解决,可以使数十层的网络收敛。
但是当更深的网络可以收敛的时候,存在一个退化的问题:
当网络深度逐渐增加时,精度达到饱和,然后开始退化(目前还不是很懂这个问题的本质原因)
这种退化不是过拟合的原因。
针对这种退化的原因,作者想出了一个构造更深模型的方法:
在浅层网络的基础上构造恒等映射层
这种构造方式不会使深层网络比浅层网络有更大的训练误差。
让网络学习H(x)-x而不是学习H(x)
这种方式的优势在于:
一个极端的情况,假设当前精度已饱和,进行恒等映射是最优的,那么将残差学习为0比堆叠一堆层去学习恒等映射要容易
恒等映射既不增加额外的参数,也不增加运算复杂度。
作者发现:
1.深度残差网络很容易去优化,但是与之对应的“朴素”网络(仅堆叠层)在深度增加的时候有更高的训练误差
2.深度残差网络可以简单的从大幅增加的网络深度中获取更高的精度,比之前的网络都好
残差块定义如下:
但是上式只能用于F和x可以直接相加的情况(维度相同),还有一个更普适的式子:
Ws可以用来对x进行线性变换从而匹配维度。
恒等映射(式1)足以解决问题,并且是最经济的,Ws仅在匹配维度时会使用。
F的形式是灵活的,本实验中采用的是2层和3层的,更多层也可以;但如果是1层的,就类似于线性变换层。
在输入和输出具有相同维度的时候,shortcut可以直接使用(图中实线部分);
当维度增加时,有两种情况(图中虚线部分):
1.依旧为恒等映射,额外的维度用0补充,此方法不引入额外的参数;
2.通过1*1卷积来匹配维度;
当shortcut跨越两种不同尺寸的feature map时,步长设为2。
具体实验细节如下:
训练:
输入为224*224的图片,从图片和他的镜像中随机采样,并且每个像素减去均值;
使用标准颜色增强;
每次卷积后都做一次BN;
batch size设为256;
lr从0.1开始,当错误率停滞时,lr/10,最终训练了600000轮;
weight decay=10,momentum=0.9,不使用dropout;
测试:
使用10-crop测试(详见AlexNet)
第二部分.pytorch实践
这里主要看了pytorch官方实现残差快的代码,简单易懂。
第一种残差块如上图所示,基础版,由两个3 * 3的卷积组成。
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
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
expansion是残差结构中输出维度是输入维度的多少倍,BasicBlock没有升维,所以expansion = 1
残差结构是在求和之后才经过ReLU层
与基础版的不同之处只在于这里是三个卷积,分别是1x1,3x3,1x1,分别用来压缩维度,卷积处理,恢复维度,接着就是网络主体了。
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,
padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * self.expansion, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out