ResNet 原理与代码复现

关注【CV算法恩仇录】

ResNet 模型原理

VGG 网络在特征表示上有极大的优势,但深度网络训练起来非常困难。为了解决这个问题,研究者提出了一系列的训练技巧,如 Dropout、归一化(批量正则化,Batch Normalization)。

2015年,何凯明为了降低网络训练难度,解决梯度消失的问题,提出了残差网络(Residual Network,ResNet)。
ResNet 原理与代码复现_第1张图片

图1 梯度消失

ResNet 通过引入跳跃结构(skip connection),让 CNN 学习残差映射。残差结构(Bottleneck)如图 2 所示。
ResNet 原理与代码复现_第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。
ResNet 原理与代码复现_第3张图片
图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 的卷积核。

ResNet 原理与代码复现_第4张图片
图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} HinWinDin表示;输出的特征图的高、宽、通道数分别用 H o u t 、 W o u t 、 D o u t H_{out}、 W_{out}、D_{out} HoutWoutDout 表示;卷积核的宽和高分别用 F w 、 F h F_w、F_h FwFh表示; 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 中计算网络的复杂度。
ResNet 原理与代码复现_第5张图片
图5 ResNet 34 与 VGG 16 网络的 FLOPs

ResNet 代码复现

ResNet 网络参考了 VGG 19 网络,在其基础上进行了修改,变化主要体现在 ResNet 直接使用 stride=2 的卷积做下采样,并且用 Global Average Pool 层替换了全连接层。

ResNet 使用两种残差结构,如下图 5 所示。左图对应的是浅层网络,当输入和输出维度一致时,可以直接将输入加到输出上。右图对应的是深层网络。对于维度不一致时(对应的是维度增加一倍),采用 1 x 1 的卷积,先降维再升维。

ResNet 原理与代码复现_第6张图片图5 残差结构

两种残差结构的代码实现如下,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 的重复次数。
ResNet 原理与代码复现_第7张图片

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])

ResNet 原理与代码复现_第8张图片

参考资料:
https://zhuanlan.zhihu.com/p/54289848

你可能感兴趣的:(CV,深度学习,神经网络,cnn)