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. 从而缓解了梯度消失问题
相关推荐
风象南11 小时前
Claude Code这个隐藏技能,让我告别PPT焦虑
人工智能·后端
Mintopia12 小时前
OpenClaw 对软件行业产生的影响
人工智能
陈广亮13 小时前
构建具有长期记忆的 AI Agent:从设计模式到生产实践
人工智能
会写代码的柯基犬13 小时前
DeepSeek vs Kimi vs Qwen —— AI 生成俄罗斯方块代码效果横评
人工智能·llm
Mintopia13 小时前
OpenClaw 是什么?为什么节后热度如此之高?
人工智能
爱可生开源社区13 小时前
DBA 的未来?八位行业先锋的年度圆桌讨论
人工智能·dba
叁两16 小时前
用opencode打造全自动公众号写作流水线,AI 代笔太香了!
前端·人工智能·agent
前端付豪16 小时前
LangChain记忆:通过Memory记住上次的对话细节
人工智能·python·langchain
strayCat2325516 小时前
Clawdbot 源码解读 7: 扩展机制
人工智能·开源