【动手学深度学习】ResNet残差网络
【动手学深度学习】ResNet残差网络
- 1,背景
- 2,ResNet的提出
- 3,批量归一化
- 4,残差块
- 5,残差块的实现
- 5.1,导入库
- 5.2,定义残差块
- 5.3,查看输入输出情况
- 6,ResNet的实现
1,背景
在ResNet提出之前,深度学习领域已经认识到增加网络深度可以提升模型的表现,因为更深层次的网络能够捕捉到数据中更为抽象和复杂的特征。然而这一做法遇到了几个主要障碍:
- 梯度消失
- 梯度爆炸
- 退化问题
梯度消失和梯度爆炸问题:
当网络变得非常深时,传统的反向传播算法在更新权重时可能会遇到梯度消失或梯度爆炸的问题,这是因为反向传播过程中梯度是通过链式法则逐层传递的,如果每层的梯度小于1,则经过多层后梯度会趋近于零;反之,若每层的梯度大于1,则可能导致梯度爆炸。
退化问题:
除了梯度问题,研究者们还发现了所谓的退化问题——即随着网络深度的增加,训练误差和测试误差反而开始增大,而不是像预期那样继续减小。
这种现象表明,简单地堆叠更多层并不会自动带来更好的性能,反而可能导致模型表现变差。
2,ResNet的提出
针对这些问题,何凯明等人提出了残差网络(ResNet)
,其主要目的是解决深度神经网络训练过程中遇到的问题,尤其是梯度消失和梯度爆炸问题,以及随着网络层数增加而出现的退化现象。 ResNet在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。
3,批量归一化
训练深层神经网络是十分困难的,特别是在较短的时间内使他们收敛更加棘手。而批量规范化(batch normalization) 是一种流行且有效的技术,可持续加速深层网络的收敛速度。
简单来说,批量归一化就是对每一层的输入进行一种标准化处理,让这些输入数据的分布更加稳定。比如:假设有这么一批输入数据,它们的值范围差异很大,有的大有的小。如果直接把这些数据输入到下一层的神经网络中,可能会导致后面的节点激活值也变得不稳定,一会儿很大,一会儿很小。这样会使得网络的训练过程变得非常困难,可能需要花更长的时间才能收敛,甚至可能根本就无法收敛。而批量归一化会
对每一批输入数据进行处理
,计算这一批数据的均值和方差
。然后,用这些计算出来的均值和方差来对数据进行标准化,把每个数据点都调整到一个相对稳定、标准的范围内。 这样一来,输入到下一层的数据就不再是乱七八糟的了,它们都处于一个比较合理的区间内,这样后续的网络层就更容易处理这些数据,训练过程也会更加稳定和高效。 而且,批量归一化还有一个好处,它能增加模型的泛化能力。因为在训练过程中,每次都是对一批数据进行归一化,这样可以让模型接触到不同分布的数据,就好像给模型进行了一种“正则化”一样,避免模型过度依赖某些特定的输入特征,从而提高模型对各种输入的适应能力。
假设我们有一个小批量样本X={ x ( 1 ) , x ( 2 ) , x ( 3 ) . . . x ( m ) x^{(1)},x^{(2)},x^{(3)}...x^{(m)} x(1),x(2),x(3)...x(m)},其中:
- m是小批量的大小;
- x ( i ) x^{(i)} x(i) 表示第i个样本;
①对当前批次的数据,计算每个特征维度的平均值:
②计算每个特征维度的方差:
③使用上述均值和方差对输入数据进行标准化处理:
其中𝜖是一个很小的常数(如 1 0 − 5 10^{-5} 10−5),用于防止分母为零。
在上式基础上,引入两个可学习参数 𝛾和 𝛽,对标准化后的数据进行线性变换,恢复网络的表达能力:
注意:
- 在推理或预测阶段,由于无法获取一批次的数据来估计均值和方差,我们会使用训练过程中计算的移动平均值来估计整个训练集的均值和方差,并用这些估计值来进行归一化;
- 批量归一化方法只在包含样本较多时有效;
- 对RNN或序列数据性能交叉;
4,残差块
ResNet引入了残差块的概念,这些块允许信号直接从某一层传递到后面的层,而不需要经过额外的非线性变换。这种设计即使某些层没有学到有用的特征,它们也不会对整个网络造成负面影响,因为输入可以直接被传递下去。
如上图,假设我们有一个函数 f(𝑥),它代表的是输入 x 经过若干层变换后的输出。在传统的深层网络中,我们会试图让网络直接学习这个函数 f(x)。但在ResNet中,我们改为学习一个残差函数 𝐹(𝑥)=f(𝑥)−𝑥
,这样原始的映射就可以表示为 f(𝑥)=𝐹(𝑥)+x
。这里的 x 是输入,𝐹(𝑥) 是残差映射,而 f(𝑥) 则是最终的输出。通过这种方式,ResNet缓解了梯度消失的问题,并且使得构建更深的网络成为可能。
5,残差块的实现
残差块是ResNet的重要组件,接下来我们基于pytorch实现残差块。
5.1,导入库
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
5.2,定义残差块
class Residual(nn.Module): # init的参数分别是输入通道数、输出通道数、是否使用1×1的卷积层、卷积层的步幅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)# 若use_1x1conv=True,则创建一个 1x1卷积层匹配输入和输出的维度差异if use_1x1conv:self.conv3 = nn.Conv2d(input_channels, num_channels,kernel_size=1, stride=strides)else: # 否则为 Noneself.conv3 = None# 批量归一化:分别对应两个卷积层之后的批归一化操作self.bn1 = nn.BatchNorm2d(num_channels)self.bn2 = nn.BatchNorm2d(num_channels)def forward(self, X):# ①先通过第一个卷积层和批量归一化层,并应用 ReLU 激活函数得到中间特征图YY = F.relu(self.bn1(self.conv1(X)))# ②通过第二个卷积层和批量归一化层得到更新后的特征图YY = self.bn2(self.conv2(Y))# ③如果存在第三个卷积层,说明需要调整维度。此时对输入X做相应变换if self.conv3:X = self.conv3(X)# 将转换后的输入 X 加到特征图 Y 上(这就是所谓的残差连接)。Y += X# 最后应用 ReLU 激活函数并返回结果。return F.relu(Y)
以上代码根据有无第三个卷积层生成如下图所示的两种类型的网络:
5.3,查看输入输出情况
# 创建 Residual 对象,输入和输出通道书均为3。
# 由于输入和输出通道数相同,这意味着该残差块不会改变输入张量的通道数。
blk = Residual(3,3)
# 初始化输入张量:批量大小4,输出通道数为3,特征图空间尺寸为6×6
X = torch.rand(4, 3, 6, 6)Y = blk(X)
Y.shape
由于输入和输出通道数相同,该残差块不会改变输入张量的通道数。
运行结果如下:
接下来,我们在增加输出通道数的同时将步长设置为 2(会导致输出的高和宽减半),然后查看形状
blk = Residual(3,6, use_1x1conv=True, strides=2)
blk(X).shape
运行结果如下:
6,ResNet的实现
接下来使用pytorch实现ResNet。
ResNet的前两层:先是一个输出通道数为64、步幅为2的7×7卷积层;然后接步幅为2的3×3的最大汇聚层;并且ResNet每个卷积层后增加了批量规范化层;
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),nn.BatchNorm2d(64), nn.ReLU(),nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
-
ResNet第二部分使用4个由残差块组成的模块;
-
first_block用于标识某个残差块组是否是第一个残差块组;
-
对其中的第一个模块做了特别处理。如果是第一个序列,则第一个残差块保持输入特征图的尺寸和通道数不变;
-
之后的3个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半;
def resnet_block(input_channels, num_channels, num_residuals,first_block=False):blk = []for i in range(num_residuals):if i == 0 and not first_block: # 不是第一个模块blk.append(Residual(input_channels, num_channels,use_1x1conv=True, strides=2))else: # 是第一个模块blk.append(Residual(num_channels, num_channels))return blk
# 设置first_block=True判断是否为第一个残差块序列,第一个残差块序列可以使通道数同输入通道数一致
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))# 之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))
加入全局平均汇聚层和全连接层输出
net = nn.Sequential(b1, b2, b3, b4, b5,nn.AdaptiveAvgPool2d((1,1)),nn.Flatten(), nn.Linear(512, 10))
打印每一层的形状
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:X = layer(X)print(layer.__class__.__name__,'output shape:\t', X.shape)
运行结果如下:
模型训练:
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())
运行结果如下: