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操作是其亮点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值