VGGNet与Inception出现后, 学者们将卷积网络不断加深以寻求更优越的性能, 然而随着网络的加深, 网络却越发难以训练, 一方面会产生梯度消失现象; 另一方面越深的网络返回的梯度相关性会越来越差,接近于白噪声, 导致梯度更新也接近于随机扰动。
ResNet(Residual Network, 残差网络) 较好地解决了这个问题, 并获得了2015年ImageNet分类任务的第一名。 此后的分类、 检测、 分割等任务也大规模使用ResNet作为网络骨架。
ResNet的思想在于引入了一个深度残差框架来解决梯度消失问题,即让卷积网络去学习残差映射, 而不是期望每一个堆叠层的网络都完整地拟合潜在的映射(拟合函数) 。 如图3.17所示, 对于神经网络, 如果我们期望的网络最终映射为H(x), 左侧的网络需要直接拟合输出H(x),而右侧由ResNet提出的子模块, 通过引入一个shortcut(捷径) 分支, 将需要拟合的映射变为残差F(x): H(x)-x。 ResNet给出的假设是: 相较于直接优化潜在映射H(x), 优化残差映射F(x)是更为容易的。
在ResNet中, 上述的一个残差模块称为Bottleneck。 ResNet有不同网络层数的版本, 如18层、 34层、 50层、 101层和152层, 这里以常用的50层来讲解。 ResNet-50的网络架构如图3.18所示, 最主要的部分在于中间经历了4个大的卷积组, 而这4个卷积组分别包含了3、 4、 6这3个Bottleneck模块。 最后经过一个全局平均池化使得特征图大小变为1×1,然后进行1000维的全连接, 最后经过Softmax输出分类得分。
由于F(x)+x是逐通道进行相加, 因此根据两者是否通道数相同, 存在两种Bottleneck结构。 对于通道数不同的情况, 比如每个卷积组的第一个Bottleneck, 需要利用1×1卷积对x进行Downsample操作, 将通道数变为相同, 再进行加操作。 对于相同的情况下, 两者可以直接进行相加。
利用PyTorch实现一个带有Downsample操作的Bottleneck结构, 新建一个resnet_bottleneck.py文件, 代码如下:
1 import torch.nn as nn 2 3 class Bottleneck(nn.Module): 4 5 def __init__(self, in_dim, out_dim, stride=1): 6 7 super(Bottleneck, self).__init__() 8 # 网路堆叠层是由1×1、 3×3、 1×1这3个卷积组成的, 中间包含BN层 9 self.bottlenceck = nn.Sequential( 10 nn.Conv2d(in_dim, in_dim, 1, bias=False), 11 nn.BatchNorm2d(in_dim), 12 nn.ReLU(inplace=True), 13 nn.Conv2d(in_dim, in_dim, 3, 1, 1, bias=False), 14 nn.BatchNorm2d(in_dim), 15 nn.ReLU(inplace=True), 16 nn.Conv2d(in_dim, out_dim, 1, bias=False), 17 nn.BatchNorm2d(out_dim) 18 ) 19 20 self.relu = nn.ReLU(inplace=True) 21 # Downsample部分是由一个包含BN层的1×1卷积组成 22 self.downsample = nn.Sequential( 23 nn.Conv2d(in_dim, out_dim, 1, 1), 24 nn.BatchNorm2d(out_dim) 25 ) 26 27 def forward(self, x): 28 identity = x 29 out = self.bottlenceck(x) 30 identity = self.downsample(x) 31 32 # 将identity(恒等映射) 与网络堆叠层输出进行相加, 并经过ReLU后输出 33 out += identity 34 out = self.relu(out) 35 return out
1 import torch 2 from resnet_bottleneck import Bottleneck 3 4 # 实例化Bottleneck, 输入通道数为64, 输出为256, 对应第一个卷积组的第一个Bottleneck 5 bottleneck_1_1 = Bottleneck(64, 256).cuda() 6 print(bottleneck_1_1) 7 # Bottleneck作为卷积堆叠层, 包含了1×1、 3×3、 1×1这3个卷积层 8 >> Bottleneck( 9 (bottlenceck): Sequential( 10 (0): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False) 11 (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 12 (2): ReLU(inplace=True) 13 (3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) 14 (4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 15 (5): ReLU(inplace=True) 16 (6): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False) 17 (7): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 18 ) 19 (relu): ReLU(inplace=True) 20 # 利用Downsample结构将恒等映射的通道数变为与卷积堆叠层相同, 保证可以相加 21 (downsample): Sequential( 22 (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1)) 23 (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 24 ) 25 ) 26 27 input = torch.randn(1, 64, 56, 56).cuda() 28 output = bottleneck_1_1(input) # 将输入送到Bottleneck结构中 29 print(input.shape) 30 >> torch.Size([1, 64, 56, 56]) 31 32 print(output.shape) 33 # 相比输入, 输出的特征图分辨率没变, 而通道数变为4倍 34 >> torch.Size([1, 256, 56, 56])