目录
残差网络(ResNet)是什么?
残差网络(Residual Network,简称 ResNet)是 2015 年由何凯明等人提出的一种深度卷积神经网络架构,其核心创新是残差块(Residual Block) 和跳跃连接(Skip Connection),专门用于解决深度神经网络在训练过程中出现的 “性能退化问题”(即当网络深度增加到一定程度后,模型的训练误差和测试误差反而会上升)。
核心结构:残差块(Residual Block)
ResNet 的基本组成单元是残差块,其结构如下图(简化):
- 输入为 x;
- 经过若干卷积层、批量规范化(BN)、激活函数(如 ReLU)后,得到 “残差函数” F(x)(这是网络需要学习的部分);
- 通过跳跃连接将输入 x 直接与 F(x)相加,得到残差块的输出 H(x)。
残差块的详解
1. 残差块的结构
一个典型的残差块由以下部分组成:
- 卷积层:通常包含 2-3 个卷积操作(如 3×3 卷积),用于提取特征;
- 批归一化(Batch Normalization):加速训练,缓解梯度消失;
- 激活函数:如 ReLU,引入非线性;
- 跳跃连接(Skip Connection):将输入 x 直接加到卷积层的输出上(需保证维度匹配)。
2. 残差块的核心公式
残差块的输出 H(x) 定义为:
- x:残差块的输入;
- F(x):残差函数(由残差块内的卷积层、BN 层、激活函数等学习的映射,即 “残差”);
- H(x):残差块的输出(残差 F(x) 与输入 x 的和)。
3. 输入与输出维度不匹配时的调整
当残差块的输入 x 与残差函数 F(x)的维度不同(如通道数或空间尺寸不同)时,需要通过投影映射(Projection Mapping) 调整 x 的维度,公式变为:
:1×1 卷积层(或全连接层)的参数矩阵,用于将 x 投影到与 F(x)相同的维度(仅在维度不匹配时使用)。
4. 两种常见残差块结构
- 基本残差块(Basic Block):适用于较浅网络(如 ResNet-18、ResNet-34),包含 2 个 3×3 卷积层;
- 瓶颈残差块(Bottleneck Block):适用于深层网络(如 ResNet-50、ResNet-101),通过 1×1 卷积压缩维度,减少计算量,结构为 “1×1 卷积→3×3 卷积→1×1 卷积”。
跳跃连接的详解
跳跃连接(Skip Connection,也译作 “残差连接”)是深度学习中一种重要的网络设计技巧,主要用于解决深层神经网络训练时的梯度消失 / 爆炸问题,以及缓解 “深度增加但性能不升反降” 的退化现象。
1.跳跃连接的核心思想
在传统的深层神经网络中,每一层的输出仅作为下一层的输入(即 “链式传递”)。当网络过深时,梯度在反向传播过程中会逐渐衰减(或放大),导致浅层参数难以更新;同时,模型可能因过度拟合或优化困难而出现 “退化”(深度增加,训练误差反而上升)。
跳跃连接的解决思路是:将网络中某一层的输入 “跳过” 若干中间层,直接与后续层的输出进行融合(通常是简单的相加或拼接)。这样做的本质是让网络学习 “残差”(输入与输出的差异),而非直接学习复杂的映射关系。
2. 跳跃连接的常见形式
根据融合方式和应用场景,跳跃连接主要有以下两种形式:
残差连接(Residual Connection) 常见于 ResNet 及其变体,公式为:
其中:
- x 是某一层的输入;
- F(x, W) 是中间若干层(称为 “残差块”)学习的残差映射;
- y 是融合后的输出(输入与残差相加)。
若输入x与残差F(x, W)的维度不同,会通过一个 1×1 卷积层调整x的维度(称为 “投影捷径”),再进行相加。
跳跃拼接(Skip Concatenation) 常见于 U-Net、Transformer 等模型,核心是将不同层的特征 “拼接” 而非 “相加”。例如:
- 在 U-Net 中,编码器的高分辨率特征会与解码器的低分辨率特征拼接,以保留细节信息;
- 在 Transformer 的多头注意力机制中,输入会与注意力输出拼接后再通过前馈网络,增强特征的多样性。
3. 跳跃连接的作用
缓解梯度消失 / 爆炸: 反向传播时,梯度可以通过跳跃连接直接传递到浅层,避免因多层叠加导致的梯度衰减。例如,在残差连接中,梯度\(\frac{\partial y}{\partial x} = \frac{\partial F}{\partial x} + 1\),确保梯度至少为 1,保证浅层参数可更新。
解决网络退化问题: 当网络深度增加时,跳跃连接允许模型 “选择” 是否使用中间层的输出(若中间层学习的残差为 0,则\(y = x\),相当于直接传递输入,避免过度拟合)。
融合多尺度特征: 在拼接式跳跃连接中,不同层的特征(如低层级的细节特征和高层级的语义特征)被融合,提升模型对复杂数据的表达能力。
简化学习目标: 网络只需学习输入与输出的 “残差”(通常是较小的变化),而非直接学习从输入到输出的复杂映射,降低了优化难度。
残差网络的数学公式
对于由 n 个残差块组成的 ResNet,其整体输出可表示为:
:第 l-1个残差块的输出(即第 l 个残差块的输入);
:第 l 个残差块的参数;
:第 l 个残差块的输出。
ResNet 有多个变体的主要区别是什么
ResNet 有多个变体,主要区别在于残差块数量和总层数:
- ResNet-18:18 层(8 个残差块,每个残差块含 2 层卷积);
- ResNet-50/101/152:使用 “瓶颈残差块”(Bottleneck Block,含 1×1+3×3+1×1 卷积),减少参数和计算量,适合更深的网络。
ResNet的作用
解决 “深度网络性能退化问题” 传统深度网络(如 VGG)在层数超过一定值后,会出现 “退化”:增加层数反而导致精度下降(并非过拟合,因为训练误差也会上升)。ResNet 通过残差块让网络学习 “残差” 而非直接学习输出,使优化更简单 —— 当网络需要拟合恒等映射(即输入等于输出)时,只需让 \(F(x) = 0\) 即可,避免了复杂的参数调整。
缓解梯度消失 / 爆炸问题 跳跃连接允许梯度直接从深层传递到浅层,避免了梯度在反向传播过程中因多层乘法而逐渐消失或爆炸,使深层网络的训练成为可能。
支持训练超深网络 借助残差块,ResNet 可以轻松训练到数十层、数百层甚至上千层(如 ResNet-152),而传统网络在几十层时就会退化。
成为计算机视觉的基础架构 ResNet 的设计思想被广泛应用于图像分类、目标检测、语义分割等任务,是后续许多先进网络(如 FPN、Mask R-CNN)的基础。
为什么需要同时调整通道数和尺寸?
当输入通道数(3)与输出通道数(6)不同时,直接做残差连接(Y += X)会因维度不匹配而失败(Y 是 6 通道,X 是 3 通道,无法相加)。此时需要通过1×1 卷积解决两个问题:
- 通道数调整:1×1 卷积的输出通道数设为 6,将输入 X 的 3 通道升维为 6 通道,与 Y 的通道数一致。
- 尺寸匹配:1×1 卷积的步长设为 2,同步将 X 的尺寸从 6×6 降为 3×3,与 Y 的尺寸(3×3)一致。
最终,经过 1×1 卷积调整后的 X,与卷积路径输出的 Y 实现通道数(6=6) 和尺寸(3×3=3×3) 完全匹配,才能执行
Y += X
的残差连接。
1×1 卷积的 “小身材大作用”(*)
作用 核心逻辑 典型场景 通道数调整 直接改变输出通道数,参数少 残差块维度匹配、特征图升维 / 降维 跨通道特征融合 同一空间位置的多通道特征线性组合 多通道特征整合 减少计算量 前置降维,降低大卷积核的计算成本 Inception 模块、残差块 替代全连接层 保留空间结构的同时做特征抽象 语义分割、NIN 网络 简单说,1×1 卷积就像一个 “灵活的特征处理器”:既能高效调整维度,又能融合信息,还能减少计算量,是现代 CNN 架构的 “瑞士军刀”。
残差块结构图
残差块整体逻辑+实现代码
1.整体逻辑
2.实现代码
"""
文件名: 7.6 残差网络(ResNet)的残差块实现
作者: 墨尘
日期: 2025/7/14
项目名: dl_env
备注: 实现ResNet的基础组件——残差块(Residual Block),包含跳跃连接和残差学习逻辑
"""
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l # 用于后续扩展(如训练流程)
class Residual(nn.Module): #@save
"""
残差块(Residual Block):ResNet的核心组件,通过跳跃连接实现残差学习
参数:
input_channels: 输入特征图的通道数
num_channels: 输出特征图的通道数
use_1x1conv: 是否使用1x1卷积(用于输入输出通道数/尺寸不匹配时的维度调整)
strides: 第一个卷积层的步长(用于控制特征图尺寸是否缩小)
"""
def __init__(self, input_channels, num_channels,
use_1x1conv=False, strides=1):
super().__init__()
# 第一个卷积层:3x3卷积,步长由strides指定,padding=1保证卷积后尺寸不变(当strides=1时)
self.conv1 = nn.Conv2d(
input_channels, num_channels,
kernel_size=3, padding=1, stride=strides
)
# 第二个卷积层:3x3卷积,步长固定为1(保持尺寸),padding=1
self.conv2 = nn.Conv2d(
num_channels, num_channels,
kernel_size=3, padding=1
)
# 1x1卷积层(可选):用于调整输入X的通道数和尺寸,使与输出Y维度匹配
if use_1x1conv:
self.conv3 = nn.Conv2d(
input_channels, num_channels,
kernel_size=1, stride=strides # 步长与conv1一致,确保尺寸同步调整
)
else:
self.conv3 = None # 不使用时为None
# 批量规范化层(BN层):每个卷积层后都接BN层,稳定训练
self.bn1 = nn.BatchNorm2d(num_channels) # 对应conv1的输出
self.bn2 = nn.BatchNorm2d(num_channels) # 对应conv2的输出
def forward(self, X):
"""
残差块的前向传播:实现残差学习(Y = F(X) + X)
参数:
X: 输入特征图,形状为(批量大小, input_channels, 高, 宽)
返回:
残差块的输出特征图,形状为(批量大小, num_channels, 高', 宽')
"""
# 第一步:conv1 + BN + ReLU(残差函数F(X)的第一部分)
Y = F.relu(self.bn1(self.conv1(X))) # Y形状:(批量大小, num_channels, 高1, 宽1)
# 第二步:conv2 + BN(残差函数F(X)的第二部分,暂不激活)
Y = self.bn2(self.conv2(Y)) # Y形状:(批量大小, num_channels, 高2, 宽2)
# 若使用1x1卷积,调整输入X的维度(通道数和尺寸)以匹配Y
if self.conv3:
X = self.conv3(X) # X形状变为:(批量大小, num_channels, 高2, 宽2)
# 核心:残差连接(跳跃连接)——将输入X与残差函数F(X)相加
Y += X # 此时Y和X维度必须完全一致(通道数、高、宽均相同)
# 最后通过ReLU激活函数输出
return F.relu(Y)
if __name__ == '__main__':
# 测试1:输入输出通道数相同(3→3),不使用1x1卷积,步长为1(尺寸不变)
blk = Residual(input_channels=3, num_channels=3) # 输入3通道,输出3通道
X = torch.rand(4, 3, 6, 6) # 随机输入:4个样本,3通道,6x6尺寸
Y = blk(X)
print("测试1输出形状:", Y.shape) # 输出:(4, 3, 6, 6)(通道和尺寸均不变)
# 测试2:输入输出通道数不同(3→6),使用1x1卷积调整维度,步长为2(尺寸减半)
blk = Residual(input_channels=3, num_channels=6, use_1x1conv=True, strides=2)
print("测试2输出形状:", blk(X).shape) # 输出:(4, 6, 3, 3)(通道加倍,尺寸减半)
残差块实验结果
残差网络(例:ResNet-18)的整体逻辑+实现代码
1.整体逻辑
2.实现代码
"""
文件名: 7.6 残差网络(ResNet)的残差块实现
作者: 墨尘
日期: 2025/7/14
项目名: dl_env
备注: 实现完整ResNet-18网络,包含残差块、残差层和端到端训练流程
"""
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l # 用于数据加载和训练
# 手动显示图像相关库
import matplotlib.pyplot as plt # 绘图库
import matplotlib.text as text # 用于修改文本绘制(解决符号显示问题)
# -------------------------- 核心解决方案:解决文本显示问题 --------------------------
# 定义替换函数:将Unicode减号(U+2212,可能导致显示异常)替换为普通减号(-)
def replace_minus(s):
"""
解决Matplotlib中Unicode减号显示异常的问题
参数:
s: 待处理的字符串或其他类型对象
返回:
处理后的字符串或原始对象(非字符串类型)
"""
if isinstance(s, str): # 判断输入是否为字符串
return s.replace('\u2212', '-') # 替换特殊减号为普通减号
return s # 非字符串直接返回
# 重写matplotlib的Text类的set_text方法,解决减号显示异常
original_set_text = text.Text.set_text # 保存原始的set_text方法
def new_set_text(self, s):
"""
重写后的文本设置方法,在设置文本前先处理减号显示问题
"""
s = replace_minus(s) # 调用替换函数处理文本中的减号
return original_set_text(self, s) # 调用原始方法设置文本
text.Text.set_text = new_set_text # 应用重写后的方法
# -------------------------- 字体配置(确保中文和数学符号正常显示)--------------------------
plt.rcParams["font.family"] = ["SimHei"] # 设置中文字体(支持中文显示)
plt.rcParams["text.usetex"] = True # 使用LaTeX渲染文本(提升数学符号显示效果)
plt.rcParams["axes.unicode_minus"] = True # 确保负号正确显示(避免显示为方块)
plt.rcParams["mathtext.fontset"] = "cm" # 设置数学符号字体为Computer Modern(更美观)
d2l.plt.rcParams.update(plt.rcParams) # 让d2l库的绘图工具继承上述字体配置
class Residual(nn.Module): #@save
"""
残差块(Residual Block):ResNet的核心组件,通过跳跃连接实现残差学习
参数:
input_channels: 输入特征图的通道数
num_channels: 输出特征图的通道数
use_1x1conv: 是否使用1x1卷积(用于输入输出通道数/尺寸不匹配时的维度调整)
strides: 第一个卷积层的步长(用于控制特征图尺寸是否缩小)
"""
def __init__(self, input_channels, num_channels,
use_1x1conv=False, strides=1):
super().__init__()
# 第一个卷积层:3x3卷积,步长由strides指定,padding=1保证卷积后尺寸不变(当strides=1时)
self.conv1 = nn.Conv2d(
input_channels, num_channels,
kernel_size=3, padding=1, stride=strides
)
# 第二个卷积层:3x3卷积,步长固定为1(保持尺寸),padding=1
self.conv2 = nn.Conv2d(
num_channels, num_channels,
kernel_size=3, padding=1
)
# 1x1卷积层(可选):用于调整输入X的通道数和尺寸,使与输出Y维度匹配
if use_1x1conv:
self.conv3 = nn.Conv2d(
input_channels, num_channels,
kernel_size=1, stride=strides # 步长与conv1一致,确保尺寸同步调整
)
else:
self.conv3 = None # 不使用时为None
# 批量规范化层(BN层):每个卷积层后都接BN层,稳定训练
self.bn1 = nn.BatchNorm2d(num_channels) # 对应conv1的输出
self.bn2 = nn.BatchNorm2d(num_channels) # 对应conv2的输出
def forward(self, X):
"""
残差块的前向传播:实现残差学习(Y = F(X) + X)
参数:
X: 输入特征图,形状为(批量大小, input_channels, 高, 宽)
返回:
残差块的输出特征图,形状为(批量大小, num_channels, 高', 宽')
"""
# 第一步:conv1 + BN + ReLU(残差函数F(X)的第一部分)
Y = F.relu(self.bn1(self.conv1(X))) # Y形状:(批量大小, num_channels, 高1, 宽1)
# 第二步:conv2 + BN(残差函数F(X)的第二部分,暂不激活)
Y = self.bn2(self.conv2(Y)) # Y形状:(批量大小, num_channels, 高2, 宽2)
# 若使用1x1卷积,调整输入X的维度(通道数和尺寸)以匹配Y
if self.conv3:
X = self.conv3(X) # X形状变为:(批量大小, num_channels, 高2, 宽2)
# 核心:残差连接(跳跃连接)——将输入X与残差函数F(X)相加
Y += X # 此时Y和X维度必须完全一致(通道数、高、宽均相同)
# 最后通过ReLU激活函数输出
return F.relu(Y)
def resnet_block(input_channels, num_channels, num_residuals,
first_block=False):
"""
构建ResNet中的残差层(由多个残差块组成)
参数:
input_channels: 输入通道数
num_channels: 输出通道数
num_residuals: 残差块数量
first_block: 是否为第一个残差层(决定是否调整尺寸)
"""
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
if __name__ == '__main__':
# 测试残差块功能
# 测试1:输入输出通道数相同(3→3),不使用1x1卷积,步长为1(尺寸不变)
blk = Residual(input_channels=3, num_channels=3) # 输入3通道,输出3通道
X = torch.rand(4, 3, 6, 6) # 随机输入:4个样本,3通道,6x6尺寸
Y = blk(X)
print("测试1输出形状:", Y.shape) # 输出:(4, 3, 6, 6)(通道和尺寸均不变)
# 测试2:输入输出通道数不同(3→6),使用1x1卷积调整维度,步长为2(尺寸减半)
blk = Residual(input_channels=3, num_channels=6, use_1x1conv=True, strides=2)
print("测试2输出形状:", blk(X).shape) # 输出:(4, 6, 3, 3)(通道加倍,尺寸减半)
# -------------------------- 构建完整ResNet-18网络 --------------------------
# 第一个模块:初始卷积+池化(不改变通道数,尺寸减半两次)
b1 = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3), # 输入1通道(灰度图),输出64通道
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 尺寸减半
)
# 后续4个残差模块:每个模块包含多个残差块
# b2:不改变通道数和尺寸(因first_block=True,且strides=1)
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
# b3~b5:每个模块的第一个残差块将通道数翻倍,尺寸减半
b3 = nn.Sequential(*resnet_block(64, 128, 2)) # 64→128,尺寸减半
b4 = nn.Sequential(*resnet_block(128, 256, 2)) # 128→256,尺寸减半
b5 = nn.Sequential(*resnet_block(256, 512, 2)) # 256→512,尺寸减半
# 全连接分类器:全局平均池化+线性层
net = nn.Sequential(
b1, b2, b3, b4, b5,
nn.AdaptiveAvgPool2d((1, 1)), # 全局平均池化,将特征图压缩为1×1
nn.Flatten(), # 展平为一维向量
nn.Linear(512, 10) # 线性层,输出10类(对应Fashion-MNIST的10个类别)
)
# -------------------------- 测试网络架构 --------------------------
# 输入形状测试:模拟一个样本,观察每一层的输出形状
X = torch.rand(size=(1, 1, 224, 224)) # 输入:1个样本,1通道,224×224尺寸
print("\n网络逐层输出形状:")
for layer in net:
X = layer(X)
print(f"{layer.__class__.__name__:15} 输出形状: \t{X.shape}")
# -------------------------- 训练模型 --------------------------
# 训练参数设置
lr, num_epochs, batch_size = 0.05, 10, 256
# 加载Fashion-MNIST数据集(调整图像大小为96×96以适应网络输入)
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
# 训练模型:使用d2l库的训练函数,自动支持GPU
print("\n开始训练ResNet-18模型...")
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
# 显示训练过程中的损失和精度曲线(保持窗口打开)
plt.show(block=True)