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. 从而缓解了梯度消失问题
相关推荐
tzc_fly13 分钟前
scPair:隐式特征选择提高single-cell paired多模态分析
深度学习
李歘歘18 分钟前
Stable Diffusion经典应用场景
人工智能·深度学习·计算机视觉
饭碗、碗碗香21 分钟前
OpenCV笔记:图像去噪对比
人工智能·笔记·opencv·计算机视觉
段传涛26 分钟前
AI Prompt Engineering
人工智能·深度学习·prompt
西电研梦34 分钟前
考研倒计时30天丨和西电一起向前!再向前!
人工智能·考研·1024程序员节·西电·西安电子科技大学
南门听露34 分钟前
适用于资源受限IoT系统的非对称语义图像压缩技术
深度学习·神经网络·物联网
催催1240 分钟前
手机领夹麦克风哪个牌子好,哪种领夹麦性价比高,热门麦克风推荐
网络·人工智能·经验分享·其他·智能手机
孤华暗香1 小时前
吴恩达《提示词工程》(Prompt Engineering for Developers)课程详细笔记
人工智能·笔记·prompt
rommel rain1 小时前
SpecInfer论文阅读
人工智能·语言模型·transformer
腾讯云开发者1 小时前
AI 驱动的创新与变革 | 第十届中国行业互联网大会暨腾讯云 TVP 行业大使三周年庆典零售专场精彩回顾
人工智能