为了解决两个问题:
1)退化问题(degradation problem)
2)梯度消失/爆炸
什么是resnet?
将网络学习目标改为学习残差函数,也就是目标值与预测值的差,通过一个跳跃连接可以解决梯度消失问题,这样就可以搭建更深的网络结构,得到更好的训练结果
卷积输入和输出尺寸关系:
模型退化
神经网络层数越深,网络就能进行更加复杂的特征提取,理论上可以取得更好的结果,但是实验发现网络深度增加时,训练误差和测试误差增大,这种现象称为模型退化问题。
梯度消失和爆炸
神经网络的经典问题,解决方法:对输入数据和中间层数据进行归一化操作,这种方法可以保证网络在反向传播中采用随机梯度下降(SGD),从而让网络达到收敛。但是这种方法只对几十层的网络有用,当网络深度增加时,效果就不理想了。
残差学习
假设建立深层网络,当我们不断堆积新的层,但是新增加的层什么也不学习,只是复制浅层网络的特征,即恒等映射(Identity mapping)。在这种情况下,深层网络应该至少和浅层网络性能一样,也不应该出现退化现象。
对于一个堆叠的网络结构,当输入为x时其学到的特征为H(x),现在希望其可以学到残差F(x)=H(x)-x,这样原始的学习特征为F(x)+x。之所以这样是因为残差学习相比原始特征直接学习更容易。当残差为0时, 此时堆积层仅仅做了恒等映射,至少网络性能不会下降,实际残差不会为0,这也会使得堆积层在输入特征基础上学习到新的特征,从而拥有更好的性能。
ResNet网络结构
ResNet参考了VGG19网络,在其基础上进行了修改,并通过短路机制加入了残差单元。变化主要体现在ResNet直接使用stride=2的卷积做下采样,并且用global average pool层替换了全连接层。
ResNet的一个重要设计原则是:当feature map大小降低一半时,feature map的数量增加一倍,这样保持了网络的复杂度。
注意,这里网络结构中padding部分没给,但是可以根据维度减半推出padding的大小。
残差单元
ResNet使用了两种残差单元,左侧对应的是浅层网络,右侧对应的是深层网络,称为Bottleneck Architectures。
三层残差单元结构的卷积核分别是1x1,3x3和1x1,通过1x1卷积来巧妙的缩减或扩张feature map维度,从而使得我们的3x3卷积的filter数目不受上一层输入的影响,输出也不会影响到下一层。中间3x3的卷积层首先在一个降维1x1卷积层下减少了计算,然后在另一个1x1的卷积层下做了还原。既保持了模型精度又减少了网络参数和计算量,节省了计算时间。
对于短路连接(shortcut),当输入和输出的维度一致时,可以直接将输入加到输出上。当输入和输出维度不一致时,就不能直接相加(对应的虚线连接),有两种策略:
(1)使用zero-padding增加维度,一般是先做一个downsampling,可以采用stride=2的pooling,这样不会增加参数。
(2)采用新的映射(projection shortcut),一般是采用1x1的卷积,这样会增加参数,也会增加计算量。当然恒等映射也可以使用projection shortcut,只是参数是方阵。
实验结果
ResNet使用了更深的网络,在ImageNet上采用了152层,是VGG的8倍深度,但仍然拥有较低的复杂度,由于网络层数更深,准确率更好,取得了2015年ImageNet分类任务和目标检测任务的冠军。
同样,在COCO 数据集中目标检测和图像分割任务上都取得了第一的成绩。
作者还尝试将网络增加到1000层,但是出现了退化问题,作者分析认为是产生了过拟合,数据集太小。
总结
ResNet是何恺明何博士的辉煌战绩之一,是CNN图像处理史上的一件里程碑事件,主要有如下两点:
(1)提出了残差网络结构,将网络的拟合对象转变为拟合残差。在一定程度上解决了模型退化问题以及网络的梯度消失/爆炸问题,突破了1000层的网络深度,使得大规模深度网络成为可能。
(2)使用Batch Normalization加速训练(舍弃dropout)
另外,论文还是有很多trick的,比如1x1的卷积增加/缩放维度,使用stride=2替代pooling等等,非常精彩的著作。
代码实现
本地代码存放路径为:~/PycharmProjects/DL_tutorials/CNNs/ResNet
"""
ResNet模型搭建
"""
import torch.nn as nn
import torch
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channel)
self.relu = nn.ReLU()
self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channel)
self.downsample = downsample
def forward(self, x):
identity = x
if self.downsample is not None:
identity = self.downsample(x)
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out += identity
out = self.relu(out)
return out
class Bottleneck(nn.Module):
"""
注意:原论文中,在虚线残差结构的主分支上,第一个1x1卷积层的步距是2,第二个3x3卷积层步距是1。
但在pytorch官方实现过程中是第一个1x1卷积层的步距是1,第二个3x3卷积层步距是2,
这么做的好处是能够在top1上提升大概0.5%的准确率。
可参考Resnet v1.5 https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch
""" expansion = 4
def __init__(self, in_channel, out_channel, stride=1, downsample=None,
groups=1, width_per_group=64):
super(Bottleneck, self).__init__()
width = int(out_channel * (width_per_group / 64.)) * groups
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=width,
kernel_size=1, stride=1, bias=False) # squeeze channels
self.bn1 = nn.BatchNorm2d(width)
# -----------------------------------------
self.conv2 = nn.Conv2d(in_channels=width, out_channels=width, groups=groups,
kernel_size=3, stride=stride, bias=False, padding=1)
self.bn2 = nn.BatchNorm2d(width)
# -----------------------------------------
self.conv3 = nn.Conv2d(in_channels=width, out_channels=out_channel*self.expansion,
kernel_size=1, stride=1, bias=False) # unsqueeze channels
self.bn3 = nn.BatchNorm2d(out_channel*self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
def forward(self, x):
identity = x
if self.downsample is not None:
identity = self.downsample(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)
out += identity
out = self.relu(out)
return out
class ResNet(nn.Module):
def __init__(self,
block,
blocks_num,
num_classes=1000,
include_top=True,
groups=1,
width_per_group=64):
super(ResNet, self).__init__()
self.include_top = include_top
self.in_channel = 64
self.groups = groups
self.width_per_group = width_per_group
self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(self.in_channel)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, blocks_num[0])
self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
if self.include_top:
self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) # output size = (1, 1)
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
def _make_layer(self, block, channel, block_num, stride=1):
downsample = None
if stride != 1 or self.in_channel != channel * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(channel * block.expansion))
layers = []
layers.append(block(self.in_channel,
channel,
downsample=downsample,
stride=stride,
groups=self.groups,
width_per_group=self.width_per_group))
self.in_channel = channel * block.expansion
for _ in range(1, block_num):
layers.append(block(self.in_channel,
channel,
groups=self.groups,
width_per_group=self.width_per_group))
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)
if self.include_top:
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
def resnet34(num_classes=1000, include_top=True):
# https://download.pytorch.org/models/resnet34-333f7ec4.pth
return ResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
def resnet50(num_classes=1000, include_top=True):
# https://download.pytorch.org/models/resnet50-19c8e357.pth
return ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
def resnet101(num_classes=1000, include_top=True):
# https://download.pytorch.org/models/resnet101-5d3b4d8f.pth
return ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes, include_top=include_top)
def resnext50_32x4d(num_classes=1000, include_top=True):
# https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth
groups = 32
width_per_group = 4
return ResNet(Bottleneck, [3, 4, 6, 3],
num_classes=num_classes,
include_top=include_top,
groups=groups,
width_per_group=width_per_group)
def resnext101_32x8d(num_classes=1000, include_top=True):
# https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth
groups = 32
width_per_group = 8
return ResNet(Bottleneck, [3, 4, 23, 3],
num_classes=num_classes,
include_top=include_top,
groups=groups,
width_per_group=width_per_group)
Batch Normalization
Batch Normalization是google团队在2015年论文《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》提出的。通过该方法能够加速网络的收敛并提升准确率。之前的办法是减少学习率(不稳定的收敛),通过这种方法可以使用较大的学习率。
我们在进行图像预处理过程中通常进行标准化处理,进而作为网络的输入。而Batch Normalization的目的就是使我们的feature map也满足均值为0,方差为1的分布规律。
对于一个d维的输入,我们将对其每一个维度进行标准化处理。
举个例子,batch_size=2的RGB三通道的图像BN过程如下:
原文公式中的和
分别用来调整数值分布的方差大小和数值均值的位置,这两个参数是在反向传播过程中学习得到的,
的默认值是1,
的默认值是0
使用BN的注意事项:
(1)训练时要将traning参数设置为True,在验证时将trainning参数设置为False。在pytorch中可通过创建模型的model.train()和model.eval()方法控制。
(2)batch size尽可能设置大点,设置小后表现可能很糟糕,设置的越大求的均值和方差越接近整个训练集的均值和方差。
(3)建议将bn层放在卷积层(Conv)和激活层(例如Relu)之间,且卷积层不要使用偏置bias,因为没有用,参考下图推理,即使使用了偏置bias求出的结果也是一样的