• ResNet 原理与代码复现


    关注【CV算法恩仇录】

    ResNet 模型原理

    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} 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 中计算网络的复杂度。
    在这里插入图片描述
    图5 ResNet 34 与 VGG 16 网络的 FLOPs

    ResNet 代码复现

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

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

    在这里插入图片描述图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 激活
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    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))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    在 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])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

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

  • 相关阅读:
    衣康酸/马来酸酐/腰果酚接枝聚苯乙烯多元共聚阳离子树脂微球/聚苯乙烯负载阳离子聚电解质微球合成方法
    VAP动画效果参数使用记录
    【FPGA】正确处理设计优先级--或许能帮你节省50%的资源
    VUE day_08(7.26)学子商城项目3
    app自动化(五)POM模式框架搭建
    天天基金股票数据爬取
    《药事管理学》整理重点
    Springboot整合Shiro+JWT实现认证授权
    Overview of Computer Graphics
    一个 .net 8 + Azure 登录 + Ant Design Blazor 的基本后台框架
  • 原文地址:https://blog.csdn.net/u010751974/article/details/126175106