深度学习经典网络解析:ResNet

文章目录

1.背景介绍

网络出自论文《Deep Residual Learning for Image Recognition》我们都知道增加网络的宽度和深度可以很好的提高网络的性能,深的网络一般都比浅的的网络效果好,比如说一个深的网络A和一个浅的网络B,那A的性能至少都能跟B一样,为什么呢?因为就算我们把B的网络参数全部迁移到A的前面几层,而A后面的层只是做一个等价的映射,就达到了B网络的一样的效果。一个比较好的例子就是VGG,该网络就是在AlexNet的基础上通过增加网络深度大幅度提高了网络性能。对于原来的网络,如果简单地增加深度,会导致梯度弥散或梯度爆炸。对于该问题的解决方法是正则化初始化和中间的正则化层(Batch Normalization),这样的话可以训练几十层的网络。

虽然通过上述方法能够训练了,但是又会出现另一个问题,就是退化问题,网络层数增加,但是在训练集上的准确率却饱和甚至下降了。这个不能解释为过拟合,因为过拟合应该表现为在训练集上表现更好才对。退化问题说明了深度网络不能很简单地被很好地优化。作者通过实验:通过浅层网络等同映射构造深层模型,结果深层模型并没有比浅层网络有等同或更低的错误率,推断退化问题可能是因为深层的网络并不是那么好训练,也就是求解器很难去利用多层网络拟合同等函数。

网络过深导致的问题

(1)梯度消失和梯度爆炸

梯度消失:若每一层的误差梯度小于1,反向传播时,网络越深,梯度越趋近于0

梯度爆炸:若每一层的误差梯度大于1,反向传播时,网络越深,梯度越来越大

(2)退化问题

随着层数的增加,预测效果反而越来越差。如下图所示

为什么随着网络层级越深,模型效果却变差了呢?

假设一个神经网络如下图所示:

根据神经网络反向传播的原理,先通过正向传播计算出output,然后与样本比较得出误差值损失Etotal

根据误差损失结果,利用"链式法则"求偏导,使结果误差反向传播从而得出权重w调整的梯度。下图是输出结果到隐含层的反向传播过程(隐含层到输入层的反向传播过程也是类似):

通过不断迭代,对参数矩阵进行不断调整后,使得输出结果的误差值更小,使输出结果与事实更加接近。

从上面的过程可以看出,神经网络在反向传播过程中要不断地传播梯度,而当网络层数加深时,梯度在传播过程中会逐渐消失(假如采用Sigmoid函数,对于幅度为1的信号,每向后传递一层,梯度就衰减为原来的0.25,层数越多,衰减越厉害),导致无法对前面网络层的权重进行有效的调整。

2. ResNet网络

ResNet的核心是残差结构,我们知道网络越深的时候,提取到的不同层次的信息会越多,但随着网络层数的增加,网络很大程度可能出现梯度消失和梯度爆炸情况,loss不减反增,传统对应的解决方案则是数据的初始化,批标准化(batch normlization)和正则化,但是这样虽然解决了梯度传播的问题,深度加深了,可以训练十几或者几十层的网络,却带来了另外的问题,就是网络性能的退化问题,深度加深了,错误率却上升了,并且确定这不是过拟合导致的,因为过拟合训练集的准确率应该很高。因此为解决随着深度加深,性能不退化,残差网络就出现了,深度残差网络用来解决性能退化问题,其同时也解决了梯度问题,更使得网络的性能也提升了,用了残差结构的网络深度可以到达几百层。

网络中的亮点

1.超深的网络结构(超过1000层)。

2.提出residual(残差结构)模块。

3.使用Batch Normalization加速训练(丢弃dropout)。

解决方法

  • 为了解决梯度消失或梯度爆炸问题,ResNet论文提出通过数据的预处理以及在网络中使用 BN(Batch Normalization)层来解决。
  • 为了解决深层网络中的退化问题,可以人为地让神经网络某些层跳过下一层神经元的连接,隔层相连,弱化每层之间的强联系。这种神经网络被称为残差网络 (ResNets)。ResNet论文提出了residual结构(残差结构)来减轻退化问题,下图是使用residual结构的卷积网络,可以看到随着网络的不断加深,效果并没有变差,而是变的更好了。(虚线是train error,实线是test error)

残差学习

残差即观测值H(x)与估计值x之间的差。

深度网络的退化问题至少说明深度网络不容易训练。但是我们考虑这样一个事实:现在你有一个浅层网络,你想通过向上堆积新层来建立深层网络,一个极端情况是这些增加的层什么也不学习,仅仅复制浅层网络的特征,即这样新层是恒等映射(Identity mapping)。在这种情况下,深层网络应该至少和浅层网络性能一样,那么退化问题就得到了解决。

传统的CNN网络如左图所示,这是一个普通的、两层的卷积+激活。经过两层卷积+一个激活,我们假定它输出为H(x)。与传统的网络结构相比,ResNet增加了短路连接(shortcut connection)或称为跳跃连接(skip connection) ,如右图所示:


为什么残差链接有良好的效果?可以使更深的网络训练?

首先我们要知道我们为什么会提出残差链接,因为在深度网络的反向传播中,通过导数的多次迭代,可能最后返回的梯度更新会很小(趋近于0),这样网络的权重就没有办法进行更新,从而导致权重无法训练。

有了残差链接之后假设卷积层学习的变换为𝐹(𝑋) ,残差结构的输出是𝐻(𝑋) ,则有:

𝐻(𝑋) = 𝐹(𝑋) + 𝑋那计算梯度(求导)的时候:𝐻'(𝑋) = 𝐹'(𝑋) + 1

无论𝐹'(𝑋)为多大,总的梯度至少有个1,从而保证了梯度的回传,网络得到更好的训练。

也就是说

  • 残差结构能够避免普通的卷积层堆叠存在信息丢失问题,保证前向信息流的顺畅。
  • 残差结构能够应对梯度反传过程中的梯度消失问题,保证反向梯度流的通顺。

ResNet的网络结构

ResNet中两种不同的ResNet block

ResNet block有两种,一种左侧两层的BasicBlock结构,一种是右侧三层的bottleneck结构,即将两个3x 3的卷积层替换为1x1+3x3+1x1,它通过1x1 conv来巧妙地缩减或扩张feature map维度,从而使得我们的3x3 conv的filters数目不受上一层输入的影响,它的输出也不会影响到下一层。中间3x3的卷积层首先在一个降维1x1卷积层下减少了计算,然后在另一个1x1的卷积层下做了还原。既保持了模型精度又减少了网络参数和计算量,节省了计算时间。

注意:搭建深层次网络时,采用三层的残差结构(bottleneck)。

先降后升为了主分支上输出的特征矩阵和捷径分支上输出的特征矩阵形状相同,以便进行加法操作。

注:CNN参数个数 = 卷积核尺寸×卷积核深度 × 卷积核组数 = 卷积核尺寸 × 输入特征矩阵深度 × 输出特征矩阵深度

注意:

对于短路连接,如果残差映射F(x)的维度与跳跃连接x的维度不同,那咱们是没有办法对它们两个进行相加操作的,必须对x进行升维操作,让他俩的维度相同时才能计算:

复制代码
zero-padding全0填充增加维度:
    此时一般要先做一个downsamp,可以采用stride=2的pooling,这样不会增加参数
采用新的映射(projection shortcut):
    一般采用1x1的卷积,这样会增加参数,也会增加计算量。

在resnet结构中,主分支与shortcut的输出特征矩阵shape必须相同,因此,如下图右侧所示虚线残差结构,在捷径分支上通过1x1的卷积核进行降维处理,并通过设置步长为2来改变分辨率,最终实现维度的匹配:

网络结构

ResNet网络是参考了VGG19网络,在其基础上进行了修改,并通过短路机制加入了残差单元,如下图所示。变化主要体现在ResNet直接使用stride=2的卷积做下采样,并且用global average pool层替换了全连接层。ResNet的一个重要设计原则是:当feature map大小降低一半时,feature map的数量增加一倍,这保持了网络层的复杂度。从下图中可以看到,ResNet相比普通网络每两层间增加了短路机制,这就形成了残差学习,其中虚线表示feature map数量发生了改变,即使用了虚线残差结构,通过1*1卷积来改变维度。下图展示的34-layer的ResNet,还可以构建更深的网络如表1所示。从表中可以看到,对于18-layer和34-layer的ResNet,其进行的两层间的残差学习,当网络更深时,其进行的是三层间的残差学习,三层卷积核分别是1x1,3x3和1x1。

从上图图可以看出,怎么有一些跳跃链接是实线,有一些是虚线,有什么区别呢?

因为经过跳跃链接后,H(x)=F(x)+x,如果F(x)和x的通道相同,则可直接相加,那么通道不同怎么相加呢。上图中的实线、虚线就是为了区分这两种情况的:

实线的Connection部分,表示通道相同,如上图的第一个粉色矩形和第三个粉色矩形,都是3x3x64的特征图,由于通道相同,所以采用计算方式为𝐻(𝑋) = 𝐹(𝑋) + 𝑋

虚线的的Connection部分,表示通道不同,如上图的第一个绿色矩形和第三个绿色矩形,分别是3x3x64和3x3x128的特征图,通道不同,采用的计算方式为𝐻(𝑋) = 𝐹(𝑋) + w𝑋,其中w是卷积操作,用来调整x维度的。

ResNet的网络结构图如图所示:

这是ResNet不同层数的网络结构图。

可以看到,结构大差不差。不论是18层、34层、50层、还是101层、152层。

上来都是一个7x7的卷积层,然后是一个3x3的最大池化下采样。

然后就是按照图中的conv2_x、conv3_x、conv4_x、conv5_x中的残差结构。

最后再跟一个平均池化下采样,和全连接层,sofmax输出。


首先,ResNet使用ImagesNet数据集,采用的默认输入尺寸是(224, 224, 3),RGB图像,三通道

按照表中,我们可以看到,图片输入之后,首先是一个7x7,64,stride 2

也就是一个卷积层,卷积核大小为7x7,输出通道为64(也就是卷积核个数),stride=2。

没说padding,我们需要自己算一下,表里写了这一层的输出是112x112

但是我们做池化的时候,也可以采用向上取整

那就是说 112 = (224 - 7 + 2P)/ 2 + 1

化简后就是 111 = (217 + 2P)/2 = 108.5+P

所以P=3 所以Padding是3

所以我们输入图片进来,第一层

python 复制代码
in_channel=3,out_channel=64,kernel_size=7,stride=2,padding=3

没有偏置bias。经过这一层我们会得到大小为112x112的尺寸,通道数为64

然后经过一个3x3的最大池化下采样,stride=2

W_{out}=(W_{in}-F+2P)/S+1

池化层也采用向下取整。所以 56=(112 - 3 + 2P)/2 +1 计算出来P=1

所以第二层池化层是

python 复制代码
in_channel=3,out_channel=64,kernel_size=7,stride=2,padding=3

经过池化层,我们会得到一个56x56,64通道的输出,紧接着就是conv2_xconv2_x、conv3_x、conv4_x、conv5_x中对应的一系列残差结构,Resnet-18网络中具体的卷积和数和输入输出特征图大小如下图所示:

代码

python 复制代码
"""
# 搭建resnet-layer模型
#
"""
import torch
import torch.nn as nn
 
 
class BasicBlock(nn.Module):
    """搭建BasicBlock模块"""
    expansion = 1
 
    def __init__(self, in_channel, out_channel, stride=1, downsample=None):
        super(BasicBlock, self).__init__()
 
        # 使用BN层是不需要使用bias的,bias最后会抵消掉
        self.conv1 = nn.Conv2d(in_channel, out_channel, kernel_size=3, padding=1, stride=stride, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channel)    # BN层, BN层放在conv层和relu层中间使用
        self.conv2 = nn.Conv2d(out_channel, out_channel, kernel_size=3, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channel)
 
        self.downsample = downsample
        self.relu = nn.ReLU(inplace=True)
 
    # 前向传播
    def forward(self, X):
        identity = X
        Y = self.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
 
        if self.downsample is not None:    # 保证原始输入X的size与主分支卷积后的输出size叠加时维度相同
            identity = self.downsample(X)
 
        return self.relu(Y + identity)
 
 
class BottleNeck(nn.Module):
    """搭建BottleNeck模块"""
    # BottleNeck模块最终输出out_channel是Residual模块输入in_channel的size的4倍(Residual模块输入为64),shortcut分支in_channel
    # 为Residual的输入64,因此需要在shortcut分支上将Residual模块的in_channel扩张4倍,使之与原始输入图片X的size一致
    expansion = 4
 
    def __init__(self, in_channel, out_channel, stride=1, downsample=None):
        super(BottleNeck, self).__init__()
        # 默认原始输入为224,经过7x7层和3x3层之后BottleNeck的输入降至64
        self.conv1 = nn.Conv2d(in_channel, out_channel, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channel)    # BN层, BN层放在conv层和relu层中间使用
        self.conv2 = nn.Conv2d(out_channel, out_channel, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channel)
        self.conv3 = nn.Conv2d(out_channel, out_channel * self.expansion, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(out_channel * self.expansion)  # Residual中第三层out_channel扩张到in_channel的4倍
 
        self.downsample = downsample
        self.relu = nn.ReLU(inplace=True)
 
    # 前向传播
    def forward(self, X):
        identity = X
 
        Y = self.relu(self.bn1(self.conv1(X)))
        Y = self.relu(self.bn2(self.conv2(Y)))
        Y = self.bn3(self.conv3(Y))
 
        if self.downsample is not None:    # 保证原始输入X的size与主分支卷积后的输出size叠加时维度相同
            identity = self.downsample(X)
 
        return self.relu(Y + identity)
 
 
class ResNet(nn.Module):
    """搭建ResNet-layer通用框架"""
    # num_classes是训练集的分类个数,include_top是在ResNet的基础上搭建更加复杂的网络时用到,此处用不到
    def __init__(self, residual, num_residuals, num_classes=1000, include_top=True):
        super(ResNet, self).__init__()
 
        self.out_channel = 64    # 输出通道数(即卷积核个数),会生成与设定的输出通道数相同的卷积核个数
        self.include_top = include_top
 
        self.conv1 = nn.Conv2d(3, self.out_channel, kernel_size=7, stride=2, padding=3,
                               bias=False)    # 3表示输入特征图像的RGB通道数为3,即图片数据的输入通道为3
        self.bn1 = nn.BatchNorm2d(self.out_channel)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.conv2 = self.residual_block(residual, 64, num_residuals[0])
        self.conv3 = self.residual_block(residual, 128, num_residuals[1], stride=2)
        self.conv4 = self.residual_block(residual, 256, num_residuals[2], stride=2)
        self.conv5 = self.residual_block(residual, 512, num_residuals[3], stride=2)
        if self.include_top:
            self.avgpool = nn.AdaptiveAvgPool2d((1, 1))    # output_size = (1, 1)
            self.fc = nn.Linear(512 * residual.expansion, num_classes)
 
        # 对conv层进行初始化操作
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)
 
    def residual_block(self, residual, channel, num_residuals, stride=1):
        downsample = None
 
        # 用在每个conv_x组块的第一层的shortcut分支上,此时上个conv_x输出out_channel与本conv_x所要求的输入in_channel通道数不同,
        # 所以用downsample调整进行升维,使输出out_channel调整到本conv_x后续处理所要求的维度。
        # 同时stride=2进行下采样减小尺寸size,(注:conv2时没有进行下采样,conv3-5进行下采样,size=56、28、14、7)。
        if stride != 1 or self.out_channel != channel * residual.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.out_channel, channel * residual.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(channel * residual.expansion))
 
        block = []    # block列表保存某个conv_x组块里for循环生成的所有层
        # 添加每一个conv_x组块里的第一层,第一层决定此组块是否需要下采样(后续层不需要)
        block.append(residual(self.out_channel, channel, downsample=downsample, stride=stride))
        self.out_channel = channel * residual.expansion    # 输出通道out_channel扩张
 
        for _ in range(1, num_residuals):
            block.append(residual(self.out_channel, channel))
 
        # 非关键字参数的特征是一个星号*加上参数名,比如*number,定义后,number可以接收任意数量的参数,并将它们储存在一个tuple中
        return nn.Sequential(*block)
 
    # 前向传播
    def forward(self, X):
        Y = self.relu(self.bn1(self.conv1(X)))
        Y = self.maxpool(Y)
        Y = self.conv5(self.conv4(self.conv3(self.conv2(Y))))
 
        if self.include_top:
            Y = self.avgpool(Y)
            Y = torch.flatten(Y, 1)
            Y = self.fc(Y)
 
        return Y
 
 
# 构建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)
 
 
# 构建ResNet-50模型
def resnet50(num_classes=1000, include_top=True):
    return ResNet(BottleNeck, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
 
 
# 模型网络结构可视化
net = resnet34()
 
"""
# 1. 使用torchsummary中的summary查看模型的输入输出形状、顺序结构,网络参数量,网络模型大小等信息
from torchsummary import summary
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = net.to(device)
summary(model, (3, 224, 224))    # 3是RGB通道数,即表示输入224 * 224的3通道的数据
"""
 
 
"""
# 2. 使用torchviz中的make_dot生成模型的网络结构,pdf图包括计算路径、网络各层的权重、偏移量
from torchviz import make_dot
X = torch.rand(size=(1, 3, 224, 224))    # 3是RGB通道数,即表示输入224 * 224的3通道的数据
Y = net(X)
vise = make_dot(Y, params=dict(net.named_parameters()))
vise.view()
"""
 
 
"""
# Pytorch官方ResNet模型
from torchvision.models import resnet34
"""
 
相关推荐
NAGNIP9 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab10 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab10 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP13 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年13 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼14 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS14 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区15 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈15 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang16 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx