ResNet学习总结


前言

本篇博文记载了自己学习ResNet的过程。


提示:以下是本篇文章正文内容,下面案例可供参考

一、ResNet系列网络图

在这里插入图片描述
首先网络结构图可以这样理解
① 层:conv1_x,conv2_x,…,conv_5x,其中conv2_x,…,conv_5x每层都是由若干个Residual块组成,经过不同层,特征矩阵的长宽size和深度channel发生改变
在这里插入图片描述
②Residual块:Residual块分为两种,实线Block虚线Block
在这里插入图片描述
区别在于:为了保证残差块主分支特征矩阵 F(x) 和 shortcut分支的特征矩阵 X 能进行相加,其shape要相同,故要根据主分支输出F(x)的shape决定是否需要将X的shape进行变化

二、代码

1.Model大致结构

在这里插入图片描述

2.Model.py

import torch.nn as nn
import torch

# 定义一个类 残差结构 Basic Block
class BasicBlock(nn.module): # 继承来自于nn.Modele
    expansion = 1            # 残差结构主分支中 卷积核的个数比 比如ResNet_18和ResNet_34中每个ConV层中几个残差结构的Channel都一样
    # 定义初始函数 残差结构需要的一系列层结构
    # 输入channel; 输出channel; stride 步距; downsample 是否下采样,对应于虚线残差结构中[1X1,128,s=2]
    def __init__(self,in_channel, out_channel, stride=1, downsample=None):
        super(BasicBlock, self).__init__()
        # 第一个卷积层ConV1 输入,输出深度channel,核大小=3,
        # 当stride=1即对应实线Residual,当stride=2即对应虚线Residual,改变输入特征矩阵的宽高为原来一半
        # 使用BN操作,无需添加偏置Bias
        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel, kernel_size=3,
                               stride=stride, padding=1, bias=False)
        # 其输入就是ConV1的输出Channel
        self.bn1 = nn.BatchNorm2d(out_channel)
        # 第二个卷积层Conv2 参数和ConV1差不多,只需注意输入输出Channles
        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel, kernel_size=3,
                               stride=1, padding=1, bias=False)
        # BN操作
        self.bn2 = nn.BatchNorm2d(out_channel)
        # 定义下采样方法
        self.downsample = downsample
    # 定义正向传播过程 输入特征矩阵x
    def forward(self, x):
        identity = x
        # 判断Residual模块是否需要下采样
        if self.downsample is not None:
            identity = self.downsample

            out = self.conv1(x)
            out = self.bn1(out)
            out = self.relu(out)

            out = self.conv2(out)
            out = self.bn2(out)

            out += identity
            out = self.relu(out)

            return out
            # 至此 ResNet-18和34完成

# 同样的方法 ResNet-50/101/152层


class Bottleneck(nn.Module):

    """
    注意:原论文中,在虚线残差结构的主分支上,第一个1x1卷积层的步距是2,第二个3x3卷积层步距是1。
    但在pytorch官方实现过程中是第一个1x1卷积层的步距是1,第二个3x3卷积层步距是2,
    这么做的好处是能够在top1上提升大概0.5%的准确率。
    可参考Resnet v1.5 https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch
    """
    # 因为ResNet-50/101/152中每个Conv_X块中第三个卷积层的Channel都是前两层的4倍
    expansion = 4
    # 定义初始化函数

    def __init__(self, in_channel, out_channel, stride = 1, downsample=None):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel, kernel_size=1, stride=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channel)
        # -------------------------------------
        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel, kernel_size=3, stride=stride, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channel)
        # -------------------------------------
        self.conv3 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel*self.expansion, kernel_size=1, stride=1, bias=False)
        self.bn3 = nn.BatchNorm2d(out_channel*self.expansion)
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample

    def forward(self, x):
        identify = x
        if self.downsample is not None:
            identify = self.downsample(x)

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        out += identify
        out = self.relu(out)

        return out

class Resnet(nn.module):
    """
    block为定义的残差块,如果是18或34层就对应BasicBlock,如果是54,101,154层就对应BottleNeck
    block_nums残差结构数目 为列表结构 存储的ConV_X块有几个残差块 例如ResNet-18中每个ConV_X块中分别需要残差块个数为[2,2,2,2]
    num_classes训练集分类个数
    """
    def __init__(self,
                 block,
                 block_nums,
                 num_classes=1000,
                 include_top= True):
        super(Resnet, self).__init__()
        self.include_top = include_top
        self.in_channel = 64           # 输入的Channel为64
        # Conv1 为[7X7,c64,s2]
        # 输入channel为RGN三通道图像
        # 输出channel为64 即7X7的卷积核为64个 其他参数不详细解释
        self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(self.in_channel)
        self.relu = nn.ReLU(self.in_channel)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)  # 设置padding=1 保证输入特征矩阵缩减为原来一半
        # -------------生成ConV2---------------
        self.layer2 = self._make_layer(block, 64, block_nums[0])
        # -------------ConV3---------------
        self.layer3 = self._make_layer(block, 128, block_nums[1], stride=2)
        # -------------ConV4---------------
        self.layer4 = self._make_layer(block, 256, block_nums[2], stride=2)
        # -------------ConV5---------------
        self.layer5 = self._make_layer(block, 512, block_nums[3], stride=2)
        if self.include_top:  # 其默认为True
            self.avgpool = nn.AdaptiveAvgpool2d((1, 1))  # 自适应的平均池化下采样操作 无论输入特征矩阵大小为多少,最后out_size=[1,1]
            # 输入节点个数即为最终[1X1]特征矩阵的深度  输出节点个数即为分类别个数
            self.fc = nn.Linear(512*block.expansion, num_classes)
        # 对卷积层初始化操作
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
        """
        block 实线residual块或者虚线residual块
        channel 残差结构中 第一个卷积层所用的卷积核的个数 
        block_num 每个(层)卷积块中有多少个残差块
        stride 初始为1  
        -----------
        这里需要注意的是,ResNet18/34 从Conv1==>max pool==>Conv2时,矩阵的宽高不发生变化  深度channel也不发生变化为64     实线Residual
                   ResNet50/101/152 从Conv1==>max pool==>Conv2时,矩阵的宽高不发生变化  深度channel发生变化为64-->128   虚线Residual
        -----------
        """
        def _make_layer(self, block, channel, block_nums, stride =1):
            downsample = None
            """
             如果是ResNet18/30    则不进入循环   不进行下采样  Conv2第一个实线Residual, 特征矩阵大小不变,深度也不变
             如果是ResNet50/101/152 则进入循环    进行下采样   Conv2第一个虚线Residual, 特征矩阵大小不变,深度变为4X64
             事实上,ResNet50/101/152 进入下一个ConvX层时,第一个残差块都是虚线的,后面的残差块都是实的
            """
            if stride !=1 or self.in_channel != channel*block.expansion:
                # 虚线residual 输入channel为in_channel,输出应为in_channel*4, kernel_size=1
                downsample = nn.Sequential(nn.Conv2d(self.in_channel,
                                                     channel*block.expansion,
                                                     kernel_size=1,
                                                     stride=stride,
                                                     bias=False), nn.BatchNorm2d(channel*block.expansion)
                )
            # 定义一个空列表 用来存储每一个ConvX中的残差块
            layers = []
            layers.append(block(self.in_channel,            # 定义第一个【虚线残差块】,并压入layers
                                channel,
                                downsample=downsample,
                                stride=stride))
            self.in_channel = channel*block.expansion          # 如果是ResNet-18/30 expansion=1;而ResNet-50/101/152 expansion=4
            for _ in range(1, block_nums):                     # 将Conv2层,其他 【实线残差】 结构依次压进去
                layers.append(block(self.in_channel, channel)) # channel为残差块第一个卷积层卷积核的个数
            # 以【非关键字参数形式】 将layers传入 nn.sequential函数中
            return nn.Sequential(*layers)

    def forward(self, x):
        """
        x输入图像 -->conv1-->bn1-->Relu1-->maxpool--> 输出特征矩阵x
        """
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        # 接下来就是x-->conv2一系列残差结构-->conv3--conv4-->conv5
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.layer5(x)

        if self.include_top:
            x = self.avgpool(x)      # 平均池化下采样
            x = torch.flatten(x, 1)   # 展平操作
            x = self.fc(x)           # 全连接操作

        return x

# 定义ResNet-34


def resnet34(num_classes=1000, include_top=True):
    return Resnet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)


def resnet101(num_classes=1000, include_top=True):
    return Resnet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes, include_top=include_top)

3.网络亮点和细节

3.1 Residual块

搞明白ResNet网络的问题以后,我们需要思考一个问题,引入的残差块到底解决了什么问题?在He的原文中有提到是解决深层网络的一种退化问题,但并明确说明是什么问题!
这里我参考了知乎“薰风初入弦”作者的观点,原帖链接如下,欢迎大家阅读原文,支持原作者

知乎:Resnet到底在解决一个什么问题呢?–作者:薰风初入弦(上海交通大学 计算机科学与技术博士在读)

借用上作者帖子中的一句话来简单回答这个问题:

“当我们堆叠一个模型时,理所当然的会认为效果会越堆越好。因为,假设一个比较浅的网络已经可以达到不错的效果,那么即使之后堆上去的网络什么也不做,模型的效果也不会变差。

然而事实上,这却是问题所在。“什么都不做”恰好是当前神经网络最难做到的东西之一。MobileNet V2的论文[2]也提到过类似的现象,由于非线性激活函数Relu的存在,每次输入到输出的过程都几乎是不可逆的(信息损失)。我们很难从输出反推回完整的输入。

也许赋予神经网络无限可能性的“非线性”让神经网络模型走得太远,却也让它忘记了为什么出发(想想还挺哲学)。这也使得特征随着层层前向传播得到完整保留(什么也不做)的可能性都微乎其微。

用学术点的话说,这种神经网络丢失的“不忘初心”/“什么都不做”的品质叫做恒等映射(identity mapping)。

因此,可以认为Residual Learning的初衷,其实是让模型的内部结构至少有恒等映射的能力。以保证在堆叠网络的过程中,网络至少不会因为继续堆叠而产生退化!


3.2 BN操作

BN操作目的是使得一批Batch的Feature Map满足均值为0,方差为1的分布规律。例如下图1所示,对于Conv1来说输入的就是满足某一分布的特征矩阵,但对于Conv2而言输入的feature map就不一定满足某一分布规律了,标准化处理也就是是使得feature map满足均值为0,方差为1的分布规律。
在这里插入图片描述它是如何能够做到加速网络收敛?缓解梯度消失?
通过查找资料后发现这里面有几个关键点:
(1)中心极限定理:
根据中心极限定理,在样本较多的情况下,独立分布的叠加收敛于正态分布。以RGB图像中R通道为例,一个batch就相当于一个独立分布,多个batch叠加,正好符合中心极限定理的条件,也就用正态分布来标准化了。
(2)Internal Covariate Shift:
网络中间层在训练过程中,数据分布的改变称之为Internal Covariate Shift,即内部协变量偏移,而BN就是要解决在训练过程中,中间层数据分布发生改变的情况。如图2所示,
在这里插入图片描述
但是单纯的了减均值除方差归一化操作会带来一个问题,数据大量存在于Sigmod函数的非饱和区域:
在这里插入图片描述
在这一区域内绝大多数激活函数都会有一个问题:函数梯度变化程线性增长,不能保证对数据的非线性变换,从而影响数据表征能力,降低神经网络的作用。
因此作者加入了一个反变化,确保至少可以把数据还原为原数据,这里的两个参数都是需要后续去学习的:
在这里插入图片描述
最后,BN的本质就是利用优化变一下方差大小和均值位置,使得新的分布更切合数据的真实分布,保证模型的非线性表达能力。

最后解决梯度消失问题,加速网络收敛就顺理成章了,就是改变数据的分布,合适的数据愤分布,就会避免梯度消失,加速模型收敛。

总结

1.学习深度网络要牢牢把握特征矩阵经过不同层后的Shape的情况,这样就可以把握网络的结构
2.注意每个网络的提出的创新点,解决了什么问题
ResNet提出的残差块和BN操作是其亮点

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ResNet(残差网络)是一种深度卷积神经网络,最初由微软亚洲研究院的Kaiming He等人提出。ResNet的主要贡献是提出了残差学习的概念,解决了深度神经网络中的梯度消失和梯度爆炸问题,允许构建更深的网络。 ResNet通过在网络中引入残差块(Residual Block)来实现残差学习。残差块由两个卷积层和跨越连接组成,其中跨越连接将输入直接添加到输出中。这个跨越连接使得可以学习到残差,即网络可以学习到输入和输出之间的差异。这种方式可以使得网络学习到更加复杂的特征,并且可以让网络更加容易优化。 ResNet还提出了一种深度增加的策略,即采用“跳跃连接”(skip connection)的方式来连接卷积层,使得网络可以更加深。这种方式可以使得网络的深度达到上百层,而且相比于其他深度网络,ResNet的训练速度更快,精度更高。 总之,ResNet深度学习领域中具有重要的意义,引入了残差学习的概念,解决了深度神经网络中的梯度消失和梯度爆炸问题,允许构建更深的网络。 ### 回答2: ResNet是一种深度学习网络模型,是残差网络(Residual Network)的缩写。它于2015年由Kaiming He等人提出,是一种用于解决深度神经网络训练中梯度消失和模型退化问题的重要创新。 ResNet的核心思想是引入了残差模块,通过利用跨层的直接连接来构建更深的网络模型。在传统的深度网络中,随着网络层数的增加,网络的训练误差通常会增加,这是由于梯度在反向传播过程中逐层传递而导致的。ResNet通过跳跃连接(shortcut connections)来解决了这个问题,在每个残差模块中引入了一个恒等映射,使得梯度能够直接跨过几个层进行传递,有效地缓解了梯度消失的问题。 具体来说,ResNet通过在卷积层之后添加“残差块”来构建深度网络。每个残差块包含了两个或更多的卷积层,其中包括了主要的卷积层和恒等映射。通过逐层地堆叠残差块,可以构建出深度更大的网络,从而提高网络的表达能力。 此外,ResNet还引入了全局平均池化层和批归一化层来进一步提升网络性能。全局平均池化层用于取代传统网络中的全连接层,能够减少参数量并有效避免过拟合;批归一化层则用于规范化每个深度层的输入,加速网络的训练过程并提高模型的泛化能力。 总结而言,ResNet是一种通过引入残差模块来构建深度网络的方法。它通过跳跃连接和恒等映射解决了梯度消失和模型退化的问题,可以用于解决大规模图像识别、目标检测和语义分割等复杂任务。在深度学习领域中,ResNet已经成为了许多重要应用领域的基础模型。 ### 回答3: ResNet(残差网络)是一种深度学习网络模型,用于解决神经网络在训练过程中的梯度消失和梯度爆炸问题。它的核心思想是通过引入跨层连接和残差学习,使得网络可以更深,更容易训练。 在传统的神经网络中,每一层的输入都与前一层的输出直接相连。当网络变得非常深时,这些连接会导致梯度在反向传播过程中逐渐衰减,使得训练过程变得困难。为了解决这个问题,ResNet引入了跨层连接,即将当前层的输出直接加到后续层的输入上,这样可以保留前面层的信息,并且使得梯度能够更好地传播。 具体来说,ResNet使用了残差学习的概念。残差是指当前层输出与后续层输入之间的差,而残差学习的目标是将这个差尽可能地减小。为了实现这个目标,ResNet在网络的每个主要构建块中都使用了残差块。残差块由两个连续的3x3卷积层组成,每个卷积层之间添加批量归一化和激活函数,最后将输入与输出相加。通过这种方式,残差块可以学习出残差信息,并将其传递给后续层,有效地解决了梯度消失和梯度爆炸问题。 此外,为了进一步加深网络,ResNet还引入了残差块的堆叠。堆叠多个残差块可以生成更深的网络,提高网络的表达能力。在实际应用中,ResNet经常被用于图像分类任务,通过不断堆叠残差块和下采样操作,可以构建非常深的网络,达到很好的分类效果。 总的来说,ResNet是一种通过引入跨层连接和残差学习来解决梯度消失和梯度爆炸问题的深度学习网络模型。它的设计思想简单而有效,在许多深度学习任务中取得了很好的表现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值