ResNet 残差网络 (乘法→加法的思想 - 残差连接是所有前沿模型的标配) + 代码实现 ——笔记2.16《动手学深度学习》

目录

前言

[0. 乘法变加法的思想](#0. 乘法变加法的思想)

[1. 函数类](#1. 函数类)

[2. 残差块 (讲解+代码)](#2. 残差块 (讲解+代码))

[QA: 残差这个概念的体现?](#QA: 残差这个概念的体现?)

[3. ResNet模型 (代码+讲解)](#3. ResNet模型 (代码+讲解))

补充:更多版本的ResNet

[4. 训练模型](#4. 训练模型)

[5. 小结](#5. 小结)

[6. ResNet的两大卖点](#6. ResNet的两大卖点)

[6.1 加深模型可以退化为浅层模型](#6.1 加深模型可以退化为浅层模型)

[6.2 用加法解决梯度消失问题](#6.2 用加法解决梯度消失问题)


前言

0. 乘法变加法的思想

  • 现在所有新的网络,不管是Bert还是Transformer,**Residual connection(残差连接)**算是标配了,得到了广泛应用
  • Residual connection 在一定程度上体现了"乘法变加法 "的思想
    • 比如Transformer在多头注意力机制之后,会将输入与注意力层的输出相加
  • "让乘法变加法"
  • 使用 "让乘法变加法" 来训练的模型,包括ResNet, LSTM, CNN
  • 原先是用乘法进行线性变换:在深度神经网络中,每一层的输出是该层里的权重参数输入的元素逐个相乘,然后求和
  • 乘法容易导致梯度消失/爆炸(指数效应)
  • ResNet的核心:层数很多的时候,使用加法而不是乘法 (来传递信号)
  • LSTM:时序就是句子长度,例如把句子按照单词 (一个单词一个时序) 划分成一个一个的时序 (输入)
    • 原始的时序神经网络是对每一个时序做乘法,句子太长就会梯度消失/爆炸
    • LSTM将乘法变成加法
  • 加法出问题的概率远低于乘法(关于为什么,可参考本文6. ResNet的两大卖点

随着我们设计越来越深的网络,深刻理解"新添加的层如何提升神经网络的性能"变得至关重要。更重要的是设计网络的能力,在这种网络中,添加层会使网络更具表现力, 为了取得质的突破,我们需要一些数学基础知识。

1. 函数类

Non-nested function classes (非嵌套 函数类)?; Nested function classes (嵌套函数类)√

:label:fig_functionclasses

因此,只有当较复杂的函数类包含较小的函数类时,我们才能确保提高它们的性能。 对于深度神经网络,如果我们能将新添加的层训练成恒等映射(identity function)𝑓(𝐱)=𝐱,新模型和原模型将同样有效。 同时,由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。

针对这一问题,何恺明 等人提出了残差网络(ResNet) :cite:He.Zhang.Ren.ea.2016。 它在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。 残差网络的核心思想是:每个附加层都应该更容易地包含原始函数作为其元素之一。 于是,**残差块(residual blocks)**便诞生了,这个设计对如何建立深层神经网络产生了深远的影响。 凭借它,ResNet赢得了2015年ImageNet大规模视觉识别挑战赛。

2. 残差块 (讲解+代码)

让我们聚焦于神经网络局部:如图 :numref:fig_residual_block所示,假设我们的原始输入为𝑥,而希望学出的理想映射为𝑓(𝐱)(作为 :numref:fig_residual_block上方激活函数的输入)。 :numref:fig_residual_block左图虚线框中的部分需要直接拟合出该映射𝑓(𝐱),而右图虚线框中的部分则需要拟合出残差映射𝑓(𝐱)−𝐱。 残差映射在现实中往往更容易优化。

  • 拟合: 就好比一个学生通过大量的练习题(数据)来摸索和总结出解题的方法和规律(拟合出的函数或模型),使用规律用输入得出正确的输出
  • **映射𝑓(𝐱):**在这里可理解为模型本身,用输入x得出正确(预测)的输出𝑓(𝐱),就是一个映射

QA: 残差这个概念的体现?

  • 为什么是叫残差网络:
    • **粗浅理解:**因为训练损失的时候是块的输出+块的输入x = f(x)这个映射(模型),因此块的输出 = f(x) - 块的输入x,f(x) - x就是残差啦

    • 深入理解:

      **问题18:**残差这个概念体现在什么地方? 就是因为 f(x) = x + g(x), 所以g(x)可以视为f(x)的残差吗?

      **李沐:**因为x来自于上个块的输入,在底层(靠近数据),会先训练x(靠近底层),如图所示:

      把模型类比为 f(x) = x (layer1) + g(x) (layer2),两个要训练的层的叠加

      蓝色线: 是一开始的模型 x (layer1),假设有152层,先训练前面18层,先学出个简单的模型

      红色线: 训练的后期再不断加入残差让模型更复杂,即在 x (layer1) 基础上叠加 g(x) (layer2) ,比如训练完152层,红色线蓝色线 的距离就是残差

以本节开头提到的恒等映射作为我们希望学出的理想映射𝑓(𝐱),我们只需将 :numref:fig_residual_block中右图虚线框内上方的加权运算(如仿射)的权重和偏置参数设成0,那么𝑓(𝐱)即为恒等映射。 实际中,当理想映射𝑓(𝐱)极接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。 :numref:fig_residual_block右图是ResNet的基础架构--残差块 (residual block)。 在残差块中,输入可通过跨层数据线路更快地向前传播

:label:fig_residual_block

ResNet沿用了VGG完整的3×3卷积层设计。 残差块里首先有2个有相同输出通道数的3×3卷积层。 每个卷积层后接一个批量规范化层和ReLU激活函数。 然后我们通过跨层数据通路,跳过这2个卷积运算,将输入直接加在最后的ReLU激活函数前。 这样的设计要求2个卷积层的输出与输入形状一样,从而使它们可以相加。 如果想改变通道数,就需要引入一个额外的1×1卷积层来将输入变换成需要的形状后再做相加运算。 残差块的实现如下:

In [1]:

python 复制代码
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
​
# 定义的是小残差块
class Residual(nn.Module):  #@save  # ※ 残差块的核心就在这儿 ※
    def __init__(self, input_channels, num_channels,  # use_1x1convzhi指要不要用1×1的卷积层
                 use_1x1conv=False, strides=1):  # 如果想改变通道数,就用1×1的卷积
        super().__init__()
        self.conv1 = nn.Conv2d(input_channels, num_channels,
                               kernel_size=3, padding=1, stride=strides)  # 窗口大小不变
        self.conv2 = nn.Conv2d(num_channels, num_channels,
                               kernel_size=3, padding=1)
        if use_1x1conv:  # 如果用上了1×1卷积
            self.conv3 = nn.Conv2d(input_channels, num_channels,
                                   kernel_size=1, stride=strides)
        else:  # 不改变通道数的情况
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(num_channels)  # 残差块里的两个BN
        self.bn2 = nn.BatchNorm2d(num_channels)  # 都是2D卷积,输入输出的形状是(批量, 通道数, 高, 宽)
​
    def forward(self, X):
        Y = F.relu(self.bn1(self.conv1(X)))  # 对着下方架构图看就很好理解
        Y = self.bn2(self.conv2(Y))
        if self.conv3:  # 判断有没有1×1卷积改变通道数
            X = self.conv3(X)  # 如果有, 用1×1卷积改变一下输入X的通道数

        # ※ 这里是残差连接(Residual Connection)的核心体现 ※
        Y += X  # 不管通道有没有改变, 都得践行一下残差连接的思想: 输出的残差 + X = f(X) ※
        return F.relu(Y)  # 再做一下relu
复制代码

如 :numref:fig_resnet_block所示,此代码生成两种类型的网络: 一种是当use_1x1conv=False时,应用ReLU非线性函数之前,将输入添加到输出。 另一种是当use_1x1conv=True时,添加通过1×1卷积调整通道和分辨率。

:label:fig_resnet_block

下面我们来查看[输入和输出形状一致]的情况。

In [2]:

python 复制代码
blk = Residual(3,3)  # 输入和输出形状一致=3的残差块
X = torch.rand(4, 3, 6, 6)  # (批量, 通道数, 高, 宽)
Y = blk(X)  # 输出的f(x)
Y.shape
复制代码
Out[2]:
复制代码
torch.Size([4, 3, 6, 6])

我们也可以在[增加输出通道数的同时,减半输出的高和宽]。

In [3]:

python 复制代码
blk = Residual(3,6, use_1x1conv=True, strides=2)  # strides=2 会让高宽减半
blk(X).shape
复制代码
Out[3]:
复制代码
torch.Size([4, 6, 3, 3])

3. ResNet模型 (代码+讲解)

ResNet的前两层跟之前介绍的GoogLeNet中的一样: 在输出通道数为64、步幅为2的7×7卷积层后,接步幅为2的3×3的最大汇聚层。 不同之处在于ResNet每个卷积层后增加了批量规范化层。

In [4]:

python 复制代码
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),  # 输入通道维1是因为咱使用的
                   nn.BatchNorm2d(64), nn.ReLU(), # stride=2 高宽减半      # Fashion-MNIST数据集是单通道的
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))  # 高宽再减半
复制代码

GoogLeNet在后面接了4个由Inception块组成的模块。 ResNet则使用4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。 第一个模块的通道数同输入通道数一致。 由于之前已经使用了步幅为2的最大汇聚层,所以无须减小高和宽。 之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。

下面我们来实现这个模块。注意,我们对第一个模块做了特别处理。

In [5]:

python 复制代码
def resnet_block(input_channels, num_channels, num_residuals,
                 first_block=False):
    blk = []  # 存放块
    for i in range(num_residuals):  # num_residuals:块的数量
        if i == 0 and not first_block:
            blk.append(Residual(input_channels, num_channels,
                                use_1x1conv=True, strides=2))  # 看架构图,第1个块一般是高宽减半, 通道翻倍
        else:  # 看架构图,第1个块之后的块一般不改变高宽和通道数
            blk.append(Residual(num_channels, num_channels))
    return blk  # 返回这些块用于nn.Sequential定义网络
复制代码

接着在ResNet加入所有残差块,这里每个模块(stage/残差块组)使用2个残差块。

In [6]:

python 复制代码
# 每个模块都是两个残差块的组合(stage/残差块组)
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))  # num_residuals=2时
b3 = nn.Sequential(*resnet_block(64, 128, 2))  # 一次循环时:i = 0;第二次循环时:i = 1,就循环两次
b4 = nn.Sequential(*resnet_block(128, 256, 2))  # 通道数不断加倍
b5 = nn.Sequential(*resnet_block(256, 512, 2))
# "*"指的是把block从list形式展开,展成nn.Sequential的可用传参
复制代码

最后,与GoogLeNet一样,在ResNet中加入全局平均汇聚层,以及全连接层输出。

In [7]:

python 复制代码
net = nn.Sequential(b1, b2, b3, b4, b5,
                    nn.AdaptiveAvgPool2d((1,1)),
                    nn.Flatten(), nn.Linear(512, 10))  # nn.Flatten()展平
复制代码

每个模块有4个卷积层(不包括恒等映射的1×1卷积层)。 加上第一个7×7卷积层和最后一个全连接层,共有18层。 因此,这种模型通常被称为ResNet-18。 通过配置不同的通道数和模块里的残差块数可以得到不同的ResNet模型,例如更深的含152层的ResNet-152。 虽然ResNet的主体架构跟GoogLeNet类似,但ResNet架构更简单,修改也更方便。这些因素都导致了ResNet迅速被广泛使用。 :numref:fig_resnet18描述了完整的ResNet-18。

:label:fig_resnet18

补充:更多版本的ResNet

  • 横坐标是计算速度,纵坐标是准确率
  • 圆点的面积大小,指的是内存占用的相对大小(圆点越大,越占内存)
  • ResNet经过多年改进,有了超多版本,一般使用预训练的resnet50就够啦
  • 可以看到resnet的效果非常好,而且有很多版本,比如resnet18,常用的是resnet50

在训练ResNet之前,让我们[观察一下ResNet中不同模块的输入形状是如何变化的]。 在之前所有架构中,分辨率降低,通道数量增加,直到全局平均汇聚层聚集所有特征。

In [8]:

python 复制代码
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)
复制代码
Sequential output shape:	 torch.Size([1, 64, 56, 56])
Sequential output shape:	 torch.Size([1, 64, 56, 56])
Sequential output shape:	 torch.Size([1, 128, 28, 28])
Sequential output shape:	 torch.Size([1, 256, 14, 14])
Sequential output shape:	 torch.Size([1, 512, 7, 7])
AdaptiveAvgPool2d output shape:	 torch.Size([1, 512, 1, 1])
Flatten output shape:	 torch.Size([1, 512])
Linear output shape:	 torch.Size([1, 10])

4. 训练模型

同之前一样,我们在Fashion-MNIST数据集上训练ResNet。

In [9]:

python 复制代码
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
复制代码

5. 小结

  • 残差块使得很深的网络更加容易训练;
    • 甚至可以训练一千层的网络;
  • 残差网络的 Residual connection(残差连接) 对随后的深层神经网络设计产生了深远影响,无论是卷积类网络还是全连接类网络;
  • 学习嵌套函数(nested function)是训练神经网络的理想情况。在深层神经网络中,学习另一层作为恒等映射(identity function)较容易(尽管这是一个极端情况)。
  • 残差网络(ResNet)对随后的深层神经网络设计产生了深远影响。

6. ResNet的两大卖点

6.1 加深模型可以退化为浅层模型

  1. 使得深层网络(x (layer1) + g(x) (layer2))在性能不好的时候能够退化为浅层网络(x (layer1))
  2. 模型加深,性能至少不会下降

6.2 用加法解决梯度消失问题

梯度大小的角度来解释,residual connection 使得靠近数据的层的权重 w 也能够获得比较大的梯度;因此,不管网络有多深,下面的层都是可以拿到足够大的梯度,使得网络能够比较高效地更新

  1. 乘法导致梯度消失:训练很深的神经网络时,由于(反向传播)计算梯度时的链式法则 中的乘法 ,导致容易在靠近输入的层出现梯度消失的问题:
    1. 由于这个计算过程中包含了多个乘法运算,如果在传播过程中每一项的梯度值都 小于 1,那么随着层数的增加,这些小于 1 的数不断相乘,就会导致梯度越来越小,靠近输入层的梯度可能会趋近于零,这就是梯度消失问题。
  2. 使用加法的残差连接:(不会导致梯度消失
    1. 残差连接的结构
    2. 加法不会导致梯度消失的原因
      1. 从而缓解了梯度消失问题
相关推荐
Leweslyh2 小时前
物理信息神经网络(PINN)八课时教案
人工智能·深度学习·神经网络·物理信息神经网络
love you joyfully2 小时前
目标检测与R-CNN——pytorch与paddle实现目标检测与R-CNN
人工智能·pytorch·目标检测·cnn·paddle
该醒醒了~2 小时前
PaddlePaddle推理模型利用Paddle2ONNX转换成onnx模型
人工智能·paddlepaddle
小树苗1932 小时前
DePIN潜力项目Spheron解读:激活闲置硬件,赋能Web3与AI
人工智能·web3
凡人的AI工具箱2 小时前
每天40分玩转Django:Django测试
数据库·人工智能·后端·python·django·sqlite
大多_C2 小时前
BERT outputs
人工智能·深度学习·bert
Debroon3 小时前
乳腺癌多模态诊断解释框架:CNN + 可解释 AI 可视化
人工智能·神经网络·cnn
反方向的钟儿3 小时前
非结构化数据分析与应用(Unstructured data analysis and applications)(pt3)图像数据分析1
人工智能·计算机视觉·数据分析
Heartsuit3 小时前
LLM大语言模型私有化部署-使用Dify的工作流编排打造专属AI搜索引擎
人工智能·dify·ollama·qwen2.5·ai搜索引擎·tavily search·工作流编排
剑盾云安全专家3 小时前
AI加持,如何让PPT像开挂一键生成?
人工智能·aigc·powerpoint·软件