VGG 网络在特征表示上有极大的优势,但深度网络训练起来非常困难。为了解决这个问题,研究者提出了一系列的训练技巧,如 Dropout、归一化(批量正则化,Batch Normalization)。
2015年,何凯明为了降低网络训练难度,解决梯度消失的问题,提出了残差网络(Residual Network,ResNet)。
图1 梯度消失
ResNet 通过引入跳跃结构(skip connection),让 CNN 学习残差映射。残差结构(Bottleneck)如图 2 所示。
图2 残差结构
图2 的残差结构中,输入 x ,先是 1 x 1 卷积核,64 卷积层,最后是 1 x 1 卷积核,256 卷积层,维度先变小再变大。网络的输出为 H(x),如果没有引入跳跃结构分支, H(x) = F(x),根据链式法则对 x 求导,梯度变得越来越小。引入分支之后,H(x) = F(x) + x,对 x 求导,得到的局部梯度为 1,且当梯度进行反向传播时,梯度也不会消失。
图 3 是 ResNet 的结构,图中展示了 18 层、34 层、50 层、101 层、152 层框架细节,图中 “ x 2” 和 “ x 23 ” 表示该卷积层重复 2 次或 23 次。我们可以发现所有的网络都分成 5 部分,分别是 conv1、conv2_x、conv3_x、conv4_x、conv5_x。
图3 ResNet的结构
图 3 中 conv1 使用的是 7 x 7 的卷积核。当通道数一致时,卷积参数的计算量是 7 x 7 的卷积核 大于 3 x 3 的卷积核 ;当通道数不一致时,若通道数小,则可以采用大的卷积核。
对于第一个卷积层的通道数为 3 时,3 个 3 x 3 卷积核与 1 个 7 x 7 卷积核的感受野效果一样,但 1 个 7 x 7 却比 3 个 3 x 3 的参数多。在 VGG 19 层和 ResNet 34 层里,参数的计算量如图 4 所示,ResNet 34 层采用 1 个 7 x 7 的卷积核的计算量远小于 VGG 19 层采用 3 个 3 x 3 的卷积核。
图4 参数的计算量
图 3 中卷积层 conv2_x 和 conv3_x 的输出(output size)的大小分别为56 x 56 和28 x 28,如果卷积层 conv2_x 采用跳跃结构到 conv3_x,由于特征图的维度不一致,不能直接相加,此时的跳跃结构可采用卷积,以保证特征图的维度一致,特征图可以进行相加操作。
图 3 中最后一行的 FLOPs (floating-point operations) 指的是浮点运算次数,可以衡量框架的复杂度。框架的复杂度与权重和偏差(bias)有关。输入图像的高、宽、通道数分别用 H i n 、 W i n 、 D i n H_{in}、 W_{in}、D_{in} Hin、Win、Din表示;输出的特征图的高、宽、通道数分别用 H o u t 、 W o u t 、 D o u t H_{out}、 W_{out}、D_{out} Hout、Wout、Dout 表示;卷积核的宽和高分别用 F w 、 F h F_w、F_h Fw、Fh表示; N p N_p Np表示特征图一个点的计算量,其计算公式如下:
N p = F w × F h × D i n × D o u t + D o u t N_p = F_w \times F_h \times D_{in} \times D_{out} + D_{out} Np=Fw×Fh×Din×Dout+Dout
一次卷积的 FLOPs 的计算公式如下:
F L O P s : N p × H o u t × W o u t FLOPs: N_p \times H_{out} \times W_{out} FLOPs:Np×Hout×Wout
对于全连接层,输入的特征图会拉伸为1 x N i n N_in Nin 的向量,输出的向量维度为 1 x N o u t N_out Nout,则一次全连接层的 FLOPs 计算公式如下:
F L O P s : N i n × H o u t + W o u t FLOPs: N_{in} \times H_{out} + W_{out} FLOPs:Nin×Hout+Wout
可以使用工具包 Flops 在 PyTorch 中计算网络的复杂度。
图5 ResNet 34 与 VGG 16 网络的 FLOPs
ResNet 网络参考了 VGG 19 网络,在其基础上进行了修改,变化主要体现在 ResNet 直接使用 stride=2 的卷积做下采样,并且用 Global Average Pool 层替换了全连接层。
ResNet 使用两种残差结构,如下图 5 所示。左图对应的是浅层网络,当输入和输出维度一致时,可以直接将输入加到输出上。右图对应的是深层网络。对于维度不一致时(对应的是维度增加一倍),采用 1 x 1 的卷积,先降维再升维。
两种残差结构的代码实现如下,class BasicBlock(nn.Module) 指的是浅层网络 ResNet 18/34 的残差单元:
import torch
import torch.nn as nn
class BasicBlock(nn.Module):
# ResNet 18/34
expansion = 1
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
self.residual_function = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True),
nn.Conv2d(out_channels, out_channels * BasicBlock.expansion, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(out_channels * BasicBlock.expansion) # 相加之后再激活
)
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != BasicBlock.expansion * out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels * BasicBlock.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels * BasicBlock.expansion)
)
def forward(self, x):
return nn.ReLU(inplace=True)(self.residual_function(x) + self.shortcut(x)) # 此处是相加 和 ReLU 激活
class BottleNeck(nn.Module)指的是深层网络 ResNet 50/101/152 的残差单元:
class BottleNeck(nn.Module):
# ResNet 50/101/152
expansion = 4
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
self.residual_function = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True),
nn.Conv2d(out_channels, out_channels, stride=stride, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True),
nn.Conv2d(out_channels, out_channels * BottleNeck.expansion, kernel_size=1, bias=False),
nn.BatchNorm2d(out_channels * BottleNeck.expansion)
)
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels * BottleNeck.expansion:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels * BottleNeck.expansion, kernel_size=1, bias=False),
nn.BatchNorm2d(out_channels * BottleNeck.expansion)
)
def forward(self, x):
return nn.ReLU(inplace=True)(self.residual_function(x) + self.shortcut(x))
ResNet 的整体结构如下:
from torch.nn.modules import padding
from torch.nn.modules.batchnorm import BatchNorm2d
class ResNet(nn.Module):
def __init__(self, in_chans, block, num_block, num_classes=100) -> None:
super().__init__()
self.block = block
self.in_channels = 64 # 输入通道
self.conv1 = nn.Sequential(
# nn.Conv2d(in_chans, 64, kernel_size=3, stride=1, padding=1,bias=False)
# 大卷积核,效果不好,用小的,第一层
nn.Conv2d(in_chans, 64, kernel_size=3, stride=1, padding=1,bias=False),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True)
)
self.pool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.conv2_x = self._make_layers(block, 64, num_block[0], 1) # 最后一个是stride
self.conv3_x = self._make_layers(block, 128, num_block[1], 2)
self.conv4_x = self._make_layers(block, 256, num_block[2], 2)
self.conv5_x = self._make_layers(block, 512, num_block[3], 2)
self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
def _make_layers(self, block, out_channels, num_blocks, stride):
# 函数名前面带着下划线,是被保护的名字,不会通过【from module import *】导入该函数
strides = [stride] + [1] * (num_blocks - 1) # 第一个降采样
layers = []
for stride in strides:
layers.append(block(self.in_channels, out_channels, stride))
self.in_channels = out_channels * block.expansion
return nn.Sequential(*layers)
def forward(self, x):
f1 = self.conv1(x)
f2 = self.conv2_x(self.pool(f1))
f3 = self.conv3_x(f2)
f4 = self.conv4_x(f3)
f5 = self.conv5_x(f4)
output = self.avg_pool(f5)
output = output.view(output.size(0), -1)
output = self.fc(output)
return f1, f2, f3, f4, f5, output
在 ResNet 类中的 forward( )函数规定了网络数据的流向:
(1)数据进入网络后先经过卷积(conv1),再进行下采样pool(f1);
(2)然后进入中间卷积部分(conv2_x, conv3_x, conv4_x, conv5_x);
(3)最后数据经过一个平均池化(avgpool)和全连接层(fc)输出得到结果;
中间卷积部分主要是下图中的蓝框部分,红框部分中的 [2, 2, 2, 2] 和 [3, 4, 6, 3] 等则代表了 bolck 的重复次数。
ResNet18和其他Res系列网络的差异主要在于 conv2_x ~conv5_x,其他的部件都是相似的。
def resnet18(in_chans):
return ResNet(in_chans, BasicBlock,[2, 2, 2, 2])
def resnet34(in_chans):
return ResNet(in_chans, BasicBlock,[3, 4, 6, 3])
def resnet50(in_chans):
return ResNet(in_chans, BottleNeck,[3, 4, 6, 3])
def resnet101(in_chans):
return ResNet(in_chans, BottleNeck,[3, 4, 23, 3])
def resnet152(in_chans):
return ResNet(in_chans, BottleNeck,[3, 8, 36, 3])
参考资料:
https://zhuanlan.zhihu.com/p/54289848