深度学习中的残差网络(ResNet):原理与实现
深度学习中的残差网络(ResNet):原理与实现
残差网络(ResNet)是深度学习领域的重要创新,通过引入残差学习解决了深度网络训练中的退化问题。本文将详细介绍ResNet的核心概念、残差块的结构以及常见的操作,包括identity-add、projected-add和scale操作,并提供PyTorch实现代码。
残差网络(ResNet)
残差网络(Residual Network),简称ResNet,是一种深度卷积神经网络(CNN)架构,由微软研究院提出,它在2015年的ImageNet竞赛中获得了冠军。ResNet的核心思想是引入了“残差学习”来解决深度网络训练中的退化问题,即随着网络层数的增加,网络的性能反而下降。
1、残差学习
在传统的深度网络中,如果我们添加更多的层,理论上网络的学习能力应该更强。然而,实际上,当网络非常深时,直接学习未加工的特征表示会变得越来越困难,导致梯度消失或爆炸,使得网络难以训练。ResNet通过引入残差学习解决了这个问题。
残差学习的基本思想是,如果一个较浅层的网络已经能够学习到某些特征,那么增加额外的层时,这些层可以被训练成恒等映射(identity mapping),即直接传递输入到输出,而不改变已经学习到的特征。这样,即使网络很深,增加的层也不会损害网络的性能。
2、函数类
假设有一类特定的神经网络架构$F$,包括学习速率和其他超参数设置。 对于所有$f \in F$,存在一些参数集(例如权重和偏置),这些参数可以通过在合适的数据集上进行训练而获得。假设$f^$是真正想要得到的函数,如果$f^ \in F$,那可以比较快的训练出来,但是正常情况下不会刚好就能训练出来的。所以,尝试找到一个函数$f_F^*$,这是在$F$中的最佳选择。
例如,给定一个具有$X$特性(特征)和$y$标签的数据集,可以尝试通过解决以下优化问题来找到$f_F^*$:
为了更近似真正$f^$的函数,需要设计一个更强大的架构$F'$。 也就是说预计$f_{F'}^$比$f_F^$“更近似于$f^$”。但是如果$F \nsubseteq F'$,则无法保证新的函数“更近似”于需要找的函数。事实上,$f_{F'}^$可能比预期的效果更差:对于非嵌套函数(non-nested function)类,虽然$F_3$比$F_1$更接近$f^$,但$F_6$却离的更远了。而对于右侧的嵌套函数(nested function)类$F_1 \subseteq \ldots \subseteq F_6$,就可以避免上述问题。
对于非嵌套函数类,较复杂(由较大区域表示)的函数类不能保证更接近“真”函数( $f^*$ )。只有当较复杂的函数类包含较小的函数类时,才能确保提高它们的性能。 对于深度神经网络,如果我们能将新添加的层训练成恒等映射(identity function)$f(x) = x$,新模型和原模型将同样有效。 同时,由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。
换个比较通俗的理解就是:在神经网络的上下文中,如果较浅的网络能够学习到某些特征,那么当我们向这个网络添加额外的层时,这些新层可以被训练成恒等映射。这意味着新模型(更深的网络)在理论上至少和原模型(较浅的网络)一样有效,因为至少它们可以复制原模型的行为。
残差网络的核心思想是:每个附加层都应该更容易地包含原始函数作为其元素之一。
3、残差块
ResNet的基本构建单元是残差块(residual block),每个残差块包含两个或多个卷积层,以及一个跳过这些卷积层的恒等连接(skip connection)。恒等连接允许输入直接添加到这些卷积层的输出上,实现了残差连接。
假设如下图所示,原始输入为$x$,而希望学出的理想映射为$f(x)$(作为下图上方激活函数的输入)。左图中虚线框中的部分需要直接拟合出该映射$f(x)$,而右图虚线框中的部分则需要拟合出残差映射$f(x) - x$。
残差映射在现实中往往更容易优化。从上述提到的恒等映射作为希望学出的理想映射$f(x)$,只需要将右侧虚线框内上方的加权运算(如仿射)的权重和偏置参数设置为0,那么$f(x)$就是恒等映射。
事实上,当理想映射$f(x)$极接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。如下方右图是ResNet的基础架构——残差块(residual block)。 在残差块中,输入可通过跨层数据线路更快地向前传播。
左图为正常块,右图为残差块
ResNet沿用了VGG完整的3×3卷积层设计。 残差块里首先有2个有相同输出通道数的3×3卷积层。 每个卷积层后接一个批量规范化层和ReLU激活函数。 然后通过跨层数据通路,跳过这2个卷积运算,将输入直接加在最后的ReLU激活函数前。 这样的设计要求2个卷积层的输出与输入形状一样,从而使它们可以相加。 如果想改变通道数,就需要引入一个额外的1×1卷积层来将输入变换成需要的形状后再做相加运算。 残差块的实现如下:
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
class Residual(nn.Module):
def __init__(self, input_channels, num_channels,
use_1x1conv=False, strides=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:
self.conv3 = nn.Conv2d(input_channels, num_channels,
kernel_size=1, stride=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X
return F.relu(Y)
代码段说明:使用PyTorch框架实现的一个残差块(Residual Block)的类定义。
导入必要的模块:代码开始处导入了PyTorch库、神经网络模块(
nn
)、函数式接口(F
)以及d2l
模块(这通常是一个深度学习辅助库,用于提供一些额外的函数和类)。定义
Residual
类:Residual
类继承自nn.Module
,是PyTorch中定义新层或块的基类。构造函数
__init__
:
- 在构造函数中,初始化了两个卷积层
conv1
和conv2
: conv1
:第一个卷积层,使用3x3的卷积核,填充(padding)为1,步长为strides
。conv2
:第二个卷积层,同样使用3x3的卷积核,填充为1。- 如果
use_1x1conv
为真,会初始化第三个卷积层conv3
: conv3
:一个1x1的卷积层,用于在增加网络深度时匹配通道数和尺寸。如果输入和输出通道数相同,且不希望使用1x1卷积,conv3
可以设为None
。- 初始化两个批量归一化层
bn1
和bn2
。
- 前向传播函数
forward
:
- 首先,输入数据
X
通过第一个卷积层conv1
,随后通过批量归一化层bn1
和ReLU激活函数。 - 然后,结果通过第二个卷积层
conv2
和批量归一化层bn2
。 - 如果定义了
conv3
(即use_1x1conv=True
),则将原始输入X
通过conv3
和步长为strides
的卷积操作来匹配通道数和尺寸。 - 最后,将通过
conv2
的输出Y
与可能经过conv3
的X
相加(实现残差连接),再通过ReLU激活函数输出最终结果。
此代码生成两种类型的网络: 一种是当use_1x1conv=False
时,应用ReLU非线性函数之前,将输入添加到输出。 另一种是当use_1x1conv=True
时,添加通过1×1卷积调整通道和分辨率。
包含以及不包含 1×1 卷积层的残差块
一、identity-add 操作
"Identity-add"是一个在深度学习,特别是在神经网络的残差网络(Residual Networks, ResNets)中常见的操作。这个术语通常指的是一个特定的操作,它结合了两个操作:一个是恒等操作(Identity Operation),另一个是加法操作(Addition Operation)。
恒等操作(Identity Operation)
恒等操作指的是一个函数,它将输入直接映射到输出,而不进行任何改变。在数学中,这通常表示为 $f(x) = x$。在神经网络中,恒等操作可以用于通过一个层而不改变数据的值。
加法操作(Addition Operation)
加法操作是将两个数值相加得到一个结果的操作。在神经网络的上下文中,这可能涉及到将两个张量(tensor)相加,其中张量是多维的数据数组。
Identity-Add 在残差网络中的应用
在残差网络中,"identity-add"通常用于实现残差连接(Residual Connections)。残差连接允许网络中的信号绕过一些层直接传播,这有助于解决深度网络中的梯度消失问题。在这种情况下,"identity-add"操作通常如下所示:
- 网络的一部分执行一个或多个非线性变换(例如,卷积、激活函数等)。
- 与此同时,网络的另一部分直接通过一个恒等层,即不做任何变换,直接传递原始输入。
- 最后,这两部分的结果通过加法操作相加。
数学上,如果 $x$ 是输入,$H(x)$ 是经过一系列非线性变换后的输出,则残差连接可以表示为 $F(x) = x + H(x)$,其中 $F(x)$ 是最终的输出。
为什么使用 Identity-Add
使用"identity-add"的原因包括:
- 缓解梯度消失:在深度网络中,直接相加可以有助于梯度在反向传播过程中更有效地传播。
- 参数共享:在残差连接中,恒等操作不需要额外的参数,因此不会增加网络的参数数量。
- 灵活性:这种结构允许网络学习残差映射(即输入和输出之间的残差),这在某些情况下比直接学习未加工的输出更容易。
二、projected-add操作
"Projected-add"操作是深度学习中的一种技术,特别是在构建残差网络(Residual Networks)或者密集连接网络(Densely Connected Convolutional Networks,DenseNets)时使用。它结合了"投影"(Projected)和"加法"(Addition)两个概念,通常用于处理不同维度或不同特征空间的张量相加时的维度匹配问题。
投影(Projection)
在神经网络中,投影层通常是一个全连接层或一个卷积层,其目的是将输入数据的维度或特征空间映射到一个不同的维度或特征空间。这可以通过减少输入张量的宽度(即特征通道数)或深度(即单元或神经元的数量)来实现。
加法(Addition)
加法操作在神经网络中指的是将两个张量的对应元素相加,这是实现残差连接或密集连接的关键步骤。
Projected-Add 操作
当两个张量需要相加,但它们的维度不匹配时,直接相加可能会导致错误,因为它们的对应元素无法一一对应。为了解决这个问题,可以使用"Projected-add"操作:
- 投影操作:首先,对于维度较大的张量,通过一个全连接层或卷积层进行投影,减少其维度,使其与另一个张量的维度相匹配。
- 加法操作:然后,将经过投影的张量与另一个张量进行逐元素相加。
Projected-Add 在残差网络中的应用
在残差网络中,尤其是在深层网络中,输入和输出的维度可能不一致。例如,一个残差分支可能通过几个卷积层,这些层可能会减少特征图的空间维度或通道数。为了使残差分支的输出能够与输入相加,需要使用投影层来确保两者的维度一致。
为什么使用 Projected-Add
使用"Projected-add"的原因包括:
- 维度匹配:确保不同维度的张量可以进行加法操作。
- 参数效率:通过投影层,可以减少网络的参数数量,因为投影层通常比原始层具有更少的参数。
- 避免计算瓶颈:在某些情况下,直接相加可能会导致计算瓶颈,而投影可以平衡计算负载。
示例
假设我们有一个残差分支,输入张量 $I$ 的尺寸是 $[batchsize, channels, height, width]$,而残差分支的输出 $O$ 的尺寸由于卷积层的步长或池化层的操作变为 $[batchsize, channels/2, height/2, width/2]$。为了将 $O$ 和 $I$ 相加,我们需要一个投影层 $P$ 来将 $I$ 的尺寸从 $[batchsize, channels, height, width]$ 减少到 $[batchsize, channels/2, height/2, width/2]$,然后再执行加法操作 $I' = P(I) + O$。
这样,"Projected-add"操作允许不同尺寸的张量在残差连接中相加,同时保持网络的深度和性能。
三、scale 操作
scale 操作通常指的是对张量(tensor)的每个元素进行缩放(或乘以一个标量)。这个操作可以应用于单个神经元的输出、一层的输出、或者整个网络的输出。Scale操作可以用来调整数据的尺度,使其更适合后续的计算或优化,例如梯度下降。
以下是scale操作的一些关键点:
- 标量乘法:Scale操作涉及将一个张量的每个元素乘以一个常数标量(即一个数值)。这个标量可以是任何实数值。
- 维度不变:这个操作不会改变张量的维度。也就是说,如果输入是一个 $n$ 维张量,输出也将是一个 $n$ 维张量。
- 数据归一化:Scale操作常用于数据预处理阶段,以归一化数据,使其具有零均值和单位方差,或者缩放到一个特定的范围,如 $[0,1]$ 或 $[-1,1]$。
- 权重初始化:在神经网络的权重初始化中,scale操作可以用来调整权重的初始尺度,这有时可以帮助网络训练的稳定性和收敛速度。
- 层操作:在某些深度学习架构中,如Batch Normalization(批量归一化)之后的残差连接,可能会使用scale操作来调整归一化后的特征的尺度。
- 学习参数:在某些情况下,scale操作可能会使用一个可学习的参数,这意味着网络在训练过程中会优化这个缩放因子,以更好地适应训练数据。
- 数值稳定性:Scale操作可以帮助提高数值稳定性,尤其是在深度网络中,通过避免过大或过小的数值来减少梯度消失或爆炸的风险。
- 特征放缩:在特征提取过程中,scale操作可以用来放大或缩小特征的影响,这在某些情况下可以提高模型的表达能力。
- 损失函数的缩放:在计算损失函数时,有时也会使用scale操作来调整损失值的尺度,以便于梯度下降的优化。
在实际应用中,scale操作的具体实现可能会根据上下文和需求有所不同。例如,在某些深度学习框架中,scale操作可能通过简单的逐元素乘法实现;而在其他情况下,可能会通过专门的层或操作符来实现,这些层或操作符可能会包含额外的功能,如梯度裁剪或正则化。
暂时用到的就是这几个,后续有其他的再更新。
本文原文来自CSDN