ResNet论文网址:https://arxiv.org/abs/1512.03385

残差神经网络(ResNet)是由微软研究院的何恺明、张祥雨、任少卿、孙剑等人提出的。ResNet 在2015 年的ILSVRC(ImageNet Large Scale Visual Recognition Challenge)中取得了图像分类、检测、定位三个冠军。2016 年 CVPR 论文:《Deep Residual Learning for Image Recognition》就介绍了 ResNet,该论文截至当前(2020.1.3)已被引用超过 36500 次。

残差神经网络的主要贡献是发现了“退化现象(Degradation)”,并针对退化现象发明了 “快捷连接(Shortcut connection)”(或者跳过连接),极大的消除了深度过大的神经网络训练困难问题。神经网络的“深度”首次突破了100层、最大的神经网络甚至超过了1000层。

image.png

ILSVRC 2015 图像分类排名

从一个信念说起

在 2012 年的 ILSVRC 挑战赛中,AlexNet 取得了冠军,并且大幅度领先于第二名。由此引发了对 AlexNet 广泛研究,并让大家树立了一个信念——“越深网络准确率越高”。这个信念随着 VGGNet、Inception v1、Inception v2、Inception v3 不断验证、不断强化,得到越来越多的认可,但是,始终有一个问题无法回避,这个信念正确吗?
它是正确的,至少在理论上是正确的。

假设一个层数较少的神经网络已经达到了较高准确率,我们可以在这个神经网络之后,拼接一段恒等变换的网络层,这些恒等变换的网络层对输入数据不做任何转换,直接返回(y=x),就能得到一个深度较大的神经网络,并且,这个深度较大的神经网络的准确率等于拼接之前的神经网络准确率,准确率没有理由降低。

层数较多的神经网络,可由较浅的神经网络和恒等变换网络拼接而成,下图所示。

image.png
层数较多的神经网络


退化现象与对策

在讲退化这个概念之前先说一下梯度消失(Gradients Vanishing)和梯度爆炸 (Gradients Exploding) 这个概念。 也就是在训练神经网络的时候,导数或坡度有时会变得非常大,或者非常小,多个层以后梯度将以指数方式变大或者变小,这加大了训练的难度。

当网络很深时,很小的数乘起来将会变成 0(梯度消失),很大的数乘起来会变得非常大(梯度爆炸)

通过实验,ResNet随着网络层不断的加深,模型的准确率先是不断的提高,达到最大值(准确率饱和),然后随着网络深度的继续增加,模型准确率毫无征兆的出现大幅度的降低。以下曲线显示 20 层普通网络的训练误差和测试误差低于 56 层普通网络,这个现象与“越深的网络准确率越高”的信念显然是矛盾的、冲突的。ResNet团队把这一现象称为“退化(Degradation)”。

image.png

ResNet团队把退化现象归因为深层神经网络难以实现“恒等变换(y=x)”。乍一看,让人难以置信,原来能够模拟任何函数的深层神经网络,竟然无法实现恒等变换这么简单的映射了?

让我们来回想深度学习的起源,与传统的机器学习相比,深度学习的关键特征在于网络层数更深、非线性转换(激活)、自动的特征提取和特征转换,其中,非线性转换是关键目标,它将数据映射到高纬空间以便于更好的完成“数据分类”。随着网络深度的不断增大,所引入的激活函数也越来越多,数据被映射到更加离散的空间,此时已经难以让数据回到原点(恒等变换)。或者说,神经网络将这些数据映射回原点所需要的计算量,已经远远超过我们所能承受的。

退化现象让我们对非线性转换进行反思,非线性转换极大的提高了数据分类能力,但是,随着网络的深度不断的加大,我们在非线性转换方面已经走的太远,竟然无法实现线性转换。显然,在神经网络中增加线性转换分枝成为很好的选择,于是,ResNet 团队在 ResNet 模块中增加了快捷连接分枝,在线性转换和非线性转换之间寻求一个平衡。

残差网络

为了解决梯度消失 / 爆炸的问题,添加了一个跳过 / 快捷方式连接,将输入 x 添加到经过几个权重层之后的输出中,如下图所示:

image.png

残差网络构建块

输出为 H(x) = F(x) + x,权重层实际上是学习一种残差映射:F(x) = H(x) - x,即使权重层的梯度消失了,我们仍然始终具有标识 x 可以转移回较早的层。

ResNet网络架构

按照这个思路,ResNet团队分别构建了带有“快捷连接(Shortcut Connection)”的 ResNet 构建块、以及降采样的ResNet构建块,区别是降采样构建块的主杆分枝上增加了一个1×1的卷积操作,见下图。
image.png

下图展示了 34 层 ResNet 模型的架构图,仿照 AlexNet 的 8 层网络结构,我们也将 ResNet 划分成 8个构建层(Building Layer)。一个构建层可以包含一个或多个网络层、以及一个或多个构建块(如 ResNet构建块)。

image.png
34层ResNet模型架构图(此图来源于《TensorFlow深度学习实战大全》)

第一个构建层,由1个普通卷积层和最大池化层构建。
第二个构建层,由3个残差模块构成。
第三、第四、第五构建层,都是由降采样残差模块开始,紧接着3个、5个、2个残差模块。


ResNet 各个版本的网络架构如下所示:

image.png

实验结果


一个概念:10 -crops: 取图片(左上,左下,右上,右下,正中)以及它们的水平翻转。这 10 个 crops 在 CNN 下的预测输出取平均作为最终预测结果。

  • 图像分类
  1. ILSVRC

image.png
10-crop 下的检测结果

其中 plain-34 就是普通的卷积叠加起来的网络,把 ResNet 深度一直加深,错误率也一直降低

image.png
10-Crop + 多尺度全卷积


image.png

10-Crop + 多尺度全卷积 + 6 个模型融合,错误率降到了 3.57%

  1. CIFAR-10 数据集

作者们干脆把网络深度加到了 1202 层,此时网络优化起来也没有那么困难,即仍可以收敛,但是,当层数从 110 增加到 1202 时,发现错误率从 6.43%增加到 7.93%,可能是因为 CIFAR10 样本少,层数增大到 1202 层时会因为 overfit 造成错误率提升。

image.png

  • 目标检测

PASCAL VOC 2007/2012 数据集 mAP (%) 测试结果如下:

image.png

MS COCO 数据集 mAP (%) 测试结果如下:

image.png

通过将 ResNet-101 应用于 Faster R-CNN,ResNet 可以获得比 VGG-16 更好的性能

代码复现

  1. ##########################
  2. ### MODEL
  3. ##########################
  4. def conv3x3(in_planes, out_planes, stride=1):
  5. """3x3 convolution with padding"""
  6. return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
  7. padding=1, bias=False)
  8. class Bottleneck(nn.Module):
  9. expansion = 4
  10. def __init__(self, inplanes, planes, stride=1, downsample=None):
  11. super(Bottleneck, self).__init__()
  12. self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
  13. self.bn1 = nn.BatchNorm2d(planes)
  14. self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,
  15. padding=1, bias=False)
  16. self.bn2 = nn.BatchNorm2d(planes)
  17. self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)
  18. self.bn3 = nn.BatchNorm2d(planes * 4)
  19. self.relu = nn.ReLU(inplace=True)
  20. self.downsample = downsample
  21. self.stride = stride
  22. def forward(self, x):
  23. residual = x
  24. out = self.conv1(x)
  25. out = self.bn1(out)
  26. out = self.relu(out)
  27. out = self.conv2(out)
  28. out = self.bn2(out)
  29. out = self.relu(out)
  30. out = self.conv3(out)
  31. out = self.bn3(out)
  32. # print(out.shape)
  33. if self.downsample is not None:
  34. residual = self.downsample(x)
  35. out += residual
  36. out = self.relu(out)
  37. # print(out.shape)
  38. return out
  39. class ResNet(nn.Module):
  40. def __init__(self, block, layers, num_classes, grayscale):
  41. self.inplanes = 64
  42. if grayscale:
  43. in_dim = 1
  44. else:
  45. in_dim = 3
  46. super(ResNet, self).__init__()
  47. self.conv1 = nn.Conv2d(in_dim, 64, kernel_size=7, stride=2, padding=3,
  48. bias=False)
  49. self.bn1 = nn.BatchNorm2d(64)
  50. self.relu = nn.ReLU(inplace=True)
  51. self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
  52. self.layer1 = self._make_layer(block, 64, layers[0])
  53. self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
  54. self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
  55. self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
  56. self.avgpool = nn.AvgPool2d(7, stride=1, padding=2)
  57. #self.fc = nn.Linear(2048 * block.expansion, num_classes)
  58. self.fc = nn.Linear(2048, num_classes)
  59. for m in self.modules():
  60. if isinstance(m, nn.Conv2d):
  61. n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
  62. m.weight.data.normal_(0, (2. / n)**.5)
  63. elif isinstance(m, nn.BatchNorm2d):
  64. m.weight.data.fill_(1)
  65. m.bias.data.zero_()
  66. def _make_layer(self, block, planes, blocks, stride=1):
  67. downsample = None
  68. if stride != 1 or self.inplanes != planes * block.expansion:
  69. downsample = nn.Sequential(
  70. nn.Conv2d(self.inplanes, planes * block.expansion,
  71. kernel_size=1, stride=stride, bias=False),
  72. nn.BatchNorm2d(planes * block.expansion),
  73. )
  74. layers = []
  75. layers.append(block(self.inplanes, planes, stride, downsample))
  76. self.inplanes = planes * block.expansion
  77. for i in range(1, blocks):
  78. layers.append(block(self.inplanes, planes))
  79. return nn.Sequential(*layers)
  80. def forward(self, x):
  81. x = self.conv1(x)
  82. x = self.bn1(x)
  83. x = self.relu(x)
  84. x = self.maxpool(x)
  85. x = self.layer1(x)
  86. x = self.layer2(x)
  87. x = self.layer3(x)
  88. x = self.layer4(x)
  89. #x = self.avgpool(x)
  90. x = x.view(x.size(0), -1)
  91. logits = self.fc(x)
  92. probas = F.softmax(logits, dim=1)
  93. return logits, probas
  94. def resnet101(num_classes, grayscale):
  95. """Constructs a ResNet-101 model."""
  96. model = ResNet(block=Bottleneck,
  97. layers=[3, 4, 23, 3],
  98. num_classes=NUM_CLASSES,
  99. grayscale=grayscale)
  100. return model
  101. def resnet50(num_classes, grayscale):
  102. """Constructs a ResNet-50 model."""
  103. model = ResNet(block=Bottleneck,
  104. layers=[3, 4, 6, 3],
  105. num_classes=NUM_CLASSES,
  106. grayscale=grayscale)
  107. return model
  108. def resnet34(num_classes):
  109. """Constructs a ResNet-34 model."""
  110. model = ResNet(block=Bottleneck,
  111. layers=[3, 4, 6, 3],
  112. num_classes=NUM_CLASSES,
  113. grayscale=GRAYSCALE)
  114. return model
  115. def resnet18(num_classes):
  116. """Constructs a ResNet-18 model."""
  117. model = ResNet(block=BasicBlock,
  118. layers=[2, 2, 2, 2],
  119. num_classes=NUM_CLASSES,
  120. grayscale=GRAYSCALE)
  121. return model

参考