原文地址:
传统思路
在resnet还没有被提出来的时候,通常的思路是通过堆叠更多的层就能实现。但是随着网络深度的增加,就会发现不仅网络模型会变得非常大,而且会暴露出一个问题,即退化问题:随着网络深度的增加,准确率达到饱和,然后迅速下降。作者提出这种下降不是由于过拟合引起的,而且在适当的深度模型上添加更多的层会导致更高的训练误差。
一般认为的是在浅层网络之后堆叠更多的层构成一个深层网络,这个深层网络最差的效果也不能比浅层的网络差,但是得到的结果却恰恰相反。于是作者得出一个结论:不是所有系统都很容易优化,也就是说深层网络不容易被优化。
残差结构的提出:
为了解决作者提出的退化问题,使得网络优化更加容易。
不希望堆叠几个层直接适合所需的底层映射,而是显式地让这些层适合残差映射。我们如果假设多个非线性层可以渐近逼近复杂函数,那么就相当于假设多个非线性层可以渐近逼近残差函数H(x)−x。形式上,我们将期望的底层映射表示为H(x),让堆叠的非线性层适合F(x)的另一个映射,即H(x)−x。功能快捷键F(x) +x的表达式可以通过具有“快捷连接”的前馈神经网络来实现。快捷连接是指跳过一层或多层的连接。
ResNet 网络
上式中即可以表示残差模块的计算过程。F函数表示带有BN和非线性激活函数(ReLU)堆叠的两层或三层网络。WsX表示恒等的快捷连接。Ws 主要为了匹配输入向量和输出向量的维度到同一纬度底下。注意:x和F的维度必须相同。
上图是两层或三层的残差网络架构。
网络架构
从Vgg出发,我们构建一个简单的卷积神经网络。
(i)对于相同的输出特征图尺寸,层具有相同数量的滤波器;
(ii)如果特征图尺寸减半,则滤波器数量加倍,以便保持每层的时间复杂度。我们通过步长为2的卷积层直接执行下采样。
下面的后两个网络以全局平均池化层和具有softmax的1000维全连接层结束。ImageNet的网络架构例子。左:作为参考的VGG-19模型40。中:具有34个参数层的简单网络(36亿FLOPs)。右:具有34个参数层的残差网络(36亿FLOPs)。带点的快捷连接增加了维度。
基于上述的简单网络,我们插入快捷连接(右),将网络转换为其对应的残差版本。当输入和输出具有相同的维度时(图中的实线快捷连接)时,可以直接使用恒等快捷连接。当维度增加(图中的虚线快捷连接)时,我们考虑两个选项:
A)快捷连接仍然执行恒等映射,额外填充零输入以增加维度。此选项不会引入额外的参数;
B)方程(2)中的投影快捷连接用于匹配维度(由1×1卷积完成)。对于这两个选项,当快捷连接跨越两种尺寸的特征图时,它们执行时步长为2(Fm网格要变小)。
在一般的实现过程中,对于维度的不匹配问题,我们一般都采用第二个解决方案,即通过1×1卷积完成维度匹配问题。
ResNet18和ResNet34 使用的是如下residual模块
# 两层的Residual模块,经过该模块维度不变
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
接下来对BasicBlock进行介绍。
layer1的结构比较简单,没有downsample。图中方框内便是BasicBlock的主要结构——两个3×3卷积层。每个layer都由若干Block组成,又因为layer1的两个block结构完全相同。
layer2和layer1就有所不同了,首先64×56×56 的输入进入第1个block的conv1,这个conv1的stride变为2,和layer1不同(图中红圈标注),这是为了降低输入尺寸,减少数据量,输出尺寸为128×28×28。
到第1个block的末尾处,需要在output加上residual,但是输入的尺寸为64×56×56 ,所以在输入和输出之间加一个 1×1 卷积层,stride=2(图中侧边红圈标注),作用是使输入和输出尺寸统一。
由于已经降低了尺寸,第2个block的conv1的stride就设置为1。由于该block没有降低尺寸,residual和输出尺寸相同,所以也没有downsample部分。
layer3和layer4结构和layer2相同,无非就是通道数变多,输出尺寸变小,就不再赘述。
ResNet50和ResNet152 使用的是如下residual模块
# 经过此Residual模块,维度会变化,需要通过1*1卷积进行维度匹配
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 * 4, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * 4)
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
接下来对Basicblock进行介绍。
和Basicblock不同的一点是,每一个Bottleneck都会在输入和输出之间加上一个卷积层,只不过在layer1中还没有downsample,这点和Basicblock是相同的。至于一定要加上卷积层的原因,就在于Bottleneck的conv3会将输入的通道数扩展成原来的4倍,导致输入一定和输出尺寸不同。
尺寸为256×56×56 的输入进入layer2的第1个block后,首先要通过conv1将通道数降下来,之后conv2负责将尺寸降低(stride=2,图中从左向右数第2个红圈标注)到输出处,由于尺寸发生变化,需要将输入downsample,同样是通过stride=2的1×1卷积层实现。
之后的3个block(layer2有4个block)就不需要进行downsample了(无论是residual还是输入),从左向右数第3、4个红圈标注,stride均为1。
layer3和layer4结构和layer2相同,无非就是通道数变多,输出尺寸变小,就不再赘述。
# 整体的ResNet网络结构
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
self.inplanes = 64
super(ResNet, self).__init__()
# 先经过一个普通的卷积模块
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 下面每一个layer代表一个整体的残差网络模块
self.layer1 = self._make_layer(block, 64, layers[0])
# 经过卷积,stride=2 提取道德特征图的维度会减半
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
self.avgpool = nn.AvgPool2d(7, stride=1)
self.fc = nn.Linear(512 * block.expansion, num_classes)
# 初始化参数
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
# 快捷连接
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
# 添加每一个Residual模块
layers = []
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
# 剩余的Residual模块
for i in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
现在,ResNet网络已经成为处理数据、提取特征的基础性模块