当前位置: 首页 > article >正文

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

目录

前言

0. 乘法变加法的思想

1. 函数类

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

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

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

补充:更多版本的ResNet

4. 训练模型

5. 小结

6. ResNet的两大卖点

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

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


前言

  • 课程全部代码(pytorch版)已上传到附件
  • 本章节为原书第7章(现代卷积),共分为7节,本篇是第6节:残差网络(ResNet)
  • 本节的代码位置为:chapter_convolutional-modern/resnet.ipynb
  • 本节的视频链接:29 残差网络 ResNet【动手学深度学习v2】_哔哩哔哩_bilibili


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]:

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]:

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]:

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]:

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]:

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]:

# 每个模块都是两个残差块的组合(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]:

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]:

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]:

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. 从而缓解了梯度消失问题


http://www.kler.cn/a/387638.html

相关文章:

  • 从0开始学PHP面向对象内容之(常用魔术方法续一)
  • 【go从零单排】Mutexes互斥锁
  • 第七部分:2. STM32之ADC实验--AD多通道(AD采集三路传感器模块实验:光敏传感器、热敏传感器、反射式传感器附赠温湿度传感器教程)
  • iOS 18.2 重磅更新:6个大动作
  • Python 实现阿里滑块全攻略
  • YOLO即插即用---PConv
  • FPGA时序分析和约束学习笔记(4、IO传输模型)
  • Linux命令学习,git命令
  • Node-Red二次开发:各目录结构说明及开发流程
  • Mac intel 安装IDEA激活时遇到问题 jetbrains.vmoptions.plist: Permission denied
  • 量化交易系统开发-实时行情自动化交易-Okex行情交易数据
  • Spark的Standalone集群环境安装
  • arcgis pro 学习笔记
  • 代码随想录算法训练营Day58 | 卡玛网 110.字符串接龙、卡玛网 105.有向图的完全可达性、卡玛网 106.岛屿的周长
  • MyBatisPlus 用法详解
  • SQL语句-MySQL
  • HuggingFace中from_pretrained函数的加载文件
  • Unity Shader分段式血条
  • 基于SSM社区便民服务管理系统JAVA|VUE|Springboot计算机毕业设计源代码+数据库+LW文档+开题报告+答辩稿+部署教+代码讲解
  • UE5 使用Niagara粒子制作下雨效果
  • Redis5:Redis实战篇内容介绍、短信登录
  • 青少年编程与数学 02-003 Go语言网络编程 19课题、Go语言Restful编程
  • C++笔记---lambda表达式
  • 【我的世界】宠物不认我了?怎么更换主人?(Java版)
  • 贪心算法day05(k次取反后最大数组和 田径赛马)
  • 贪心算法day3(最长递增序列问题)