Appearance
残差网络(ResNet)
一、ResNet 的背景:解决深层网络的 “退化” 问题
之前我们学的深层卷积神经网络,比如 VGG,随着层数增加,理论上应该能学到更复杂的特征,准确率应该更高。但实际训练的时候会出现一个奇怪的问题:当网络层数超过一定数量后,模型的准确率不仅不提升,反而会下降,这就是网络退化问题。比如一个 50 层的网络,训练误差和测试误差都比 20 层的网络高,这不是过拟合,因为过拟合是训练误差低、测试误差高,而退化问题是训练误差也升高了。
2015 年,何恺明等人提出了残差网络(ResNet),完美解决了这个问题,ResNet 在当年的 ImageNet 图像识别挑战赛中夺冠,之后成为了很多深层神经网络的基础。ResNet 的核心思路很简单:让网络更容易学到 “恒等映射”,也就是让新添加的层可以很容易地变成 “啥也不做” 的层,这样即使网络很深,也不会出现退化问题。
二、残差块:ResNet 的核心
ResNet 的核心是残差块(residual block),它的思路是把原来要学习的映射从f(x)改成学习f(x)-x(也就是 “残差”)。
一个正常块(左图)和一个残差块(右图)
1. 残差块的原理
原来的网络层要直接拟合输出f(x),而残差块拟合的是输入和输出的差值f(x)-x。这样如果我们希望这个层是 “恒等映射”(也就是输出等于输入,f(x)=x),只需要让残差为 0 就行,也就是让网络学习到f(x)-x=0,这比直接让网络学习f(x)=x容易多了 —— 只需要让卷积层的权重和偏置都设为 0,就能实现这个效果。
实际中,当我们需要学习的映射接近恒等映射时,残差块可以很容易地捕捉到这种细微的变化,避免了深层网络的退化问题。
2. 残差块的代码实现(PyTorch 版)
我们用 PyTorch 来实现残差块:
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_1x1conv=False, strides=1):
super().__init__()
# 第一个3×3卷积层,步幅是strides,填充1,保证尺寸不变(当strides=1时)
self.conv1 = nn.Conv2d(input_channels, num_channels,
kernel_size=3, padding=1, stride=strides)
# 第二个3×3卷积层,步幅1,填充1,尺寸不变
self.conv2 = nn.Conv2d(num_channels, num_channels,
kernel_size=3, padding=1)
# 如果需要改变通道数或者尺寸,就用1×1卷积层调整输入
if use_1x1conv:
self.conv3 = nn.Conv2d(input_channels, num_channels,
kernel_size=1, stride=strides)
else:
self.conv3 = None
# 批量规范化层(BN层),用来稳定训练
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
def forward(self, X):
# 先经过第一个卷积、BN层、ReLU激活
Y = F.relu(self.bn1(self.conv1(X)))
# 再经过第二个卷积、BN层
Y = self.bn2(self.conv2(Y))
# 如果有1×1卷积,就调整输入的形状
if self.conv3:
X = self.conv3(X)
# 把输入和输出相加,再经过ReLU激活
Y += X
return F.relu(Y)代码解释:
input_channels:输入的通道数num_channels:输出的通道数use_1x1conv:是否需要用 1×1 卷积来调整输入的通道数和尺寸,比如当我们要改变通道数或者缩小图片尺寸时,就需要用这个参数strides:第一个卷积层的步幅,用来调整输出的尺寸
这个残差块的流程是:输入先经过第一个卷积层提取特征,然后用 BN 层稳定训练,再经过 ReLU 激活;然后经过第二个卷积层和 BN 层;如果输入和输出的形状不一样,就用 1×1 卷积调整输入的形状;最后把输入和输出相加,再经过 ReLU 激活,这样输入就可以直接通过 “shortcut 连接”(跨层连接)传到输出,避免了深层网络的梯度消失问题。
三、ResNet 模型的结构(以 ResNet-18 为例)
ResNet 就像用残差块搭积木一样,我们以 ResNet-18 为例,看看它的结构:
ResNet-18 架构
1. 定义残差块模块
首先我们定义一个函数,用来生成由多个残差块组成的模块:
python
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 blk2. 构建 ResNet-18
然后我们用这个函数构建 ResNet-18:
python
# 第一层:7×7卷积层 + 最大池化层
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))
# 四个残差块模块
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))ResNet-18 的结构详解:
第一层(b1):
7×7 的卷积层,输入通道是 1(因为 Fashion-MNIST 是灰度图),输出通道 64,步幅 2,填充 3,这样输入 28×28 的图片(resize 到 96×96 后)经过这个卷积层后,尺寸变成 48×48
然后是 BN 层和 ReLU 激活
然后是 3×3 的最大池化层,步幅 2,填充 1,尺寸变成 24×24
四个残差块模块:
b2:第一个残差块模块,有 2 个残差块,通道数保持 64 不变,尺寸也不变(因为第一个残差块的步幅是 1)
b3:第二个残差块模块,有 2 个残差块,通道数变成 128,步幅 2,尺寸变成 12×12
b4:第三个残差块模块,有 2 个残差块,通道数变成 256,步幅 2,尺寸变成 6×6
b5:第四个残差块模块,有 2 个残差块,通道数变成 512,步幅 2,尺寸变成 3×3
最后一层:
全局平均池化层,把每个通道的 3×3 特征图变成 1×1 的平均值,这样输出的形状是 (批量大小,512, 1, 1)
然后展平成 (批量大小,512)
最后是全连接层,输出 10 类,对应 Fashion-MNIST 的 10 个类别
整个 ResNet-18 一共有 18 层:1 个 7×7 卷积层 + 4 个模块 ×2 个残差块 ×2 个卷积层 + 1 个全连接层,所以叫 ResNet-18。我们也可以通过调整每个模块的残差块数量,得到更深的网络,比如 ResNet-34(每个模块的残差块数量是 3、4、6、3)、ResNet-50(用瓶颈残差块,每个模块的残差块数量是 3、4、6、3)等。
四、训练 ResNet
我们用 Fashion-MNIST 数据集来训练 ResNet-18,把图片 resize 到 96×96,这样训练更快:
python
lr, num_epochs, batch_size = 0.05, 10, 256
# 加载数据集,把图片resize到96×96
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
# 训练模型,用GPU训练
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())训练结果大概是:训练准确率在 0.92 左右,测试准确率在 0.91 左右,比 VGG 的准确率高,而且训练更稳定,不会出现深层网络的退化问题。
五、小结
ResNet 解决了深层网络的退化问题,通过残差块让网络更容易学习恒等映射,即使网络很深,也能稳定训练。
残差块的核心是拟合残差映射
f(x)-x,而不是直接拟合f(x),这样更容易训练,尤其是当我们需要学习恒等映射时,只需要让残差为 0 就行。ResNet 的结构简单,由残差块组成,容易修改和扩展,我们可以通过调整残差块的数量和类型,得到不同深度的 ResNet。
ResNet 对后来的深层网络设计影响很大,比如 DenseNet、ResNeXt、EfficientNet 等都是基于 ResNet 的思路。
(注:文档部分内容可能由 AI 生成) 源地址