查看全课大纲(第 13 / 40 节)
- 1.从规则到学习:深度学习解决什么问题
- 2.表示学习与端到端训练
- 3.张量、计算图与自动微分
- 4.训练三要素:数据、模型、目标函数
- 5.从感知机到多层感知机
- 6.反向传播:手推一个两层网络
- 7.激活函数与初始化
- 8.优化器:SGD → Momentum → Adam
- 9.正则化:权重衰减、Dropout、BN/LN
- 10.卷积的直觉与参数共享
- 11.池化、感受野与下采样
- 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
- 13.残差连接与归一化为什么有效
- 14.迁移学习与特征可视化
- 15.RNN 与 BPTT
- 16.LSTM/GRU:门控如何缓解梯度问题
- 17.Seq2Seq 与编码器-解码器
- 18.注意力前史:Bahdanau 加性注意力
- 19.Self-Attention:QKV 与缩放点积
- 20.多头注意力:并行的多种关系
- 21.位置编码:正弦与可学习
- 22.残差、LayerNorm 与前馈层
- 23.从零实现一个最小 Transformer block
- 24.训练稳定性、复杂度与常见误区
- 25.自监督与语言建模目标
- 26.BERT:掩码语言模型与双向编码
- 27.GPT:自回归生成与缩放定律
- 28.指令微调与对齐:SFT → RLHF/DPO
- 29.参数高效微调:LoRA 与适配器
- 30.自编码器与 VAE
- 31.GAN:对抗训练与模式崩塌
- 32.扩散模型:前向加噪与反向去噪
- 33.扩散模型的应用与采样加速
- 34.对比学习与 CLIP
- 35.ViT:把图像当序列
- 36.多模态架构:对齐、融合与生成
- 37.能力边界、幻觉与评测
- 38.PyTorch 2.x 训练循环与混合精度
- 39.实验管理、评测与复现
- 40.推理部署与成本:量化/蒸馏概览
残差连接与归一化为什么有效
残差连接与归一化为什么有效?
小象实战讲义 · 深度学习基础:从神经网络到 Transformer
在上一节,我们学习了卷积神经网络的基本结构,并看到了VGG等网络通过堆叠卷积层来增加深度。然而,简单地增加层数会遇到“退化”问题:更深的网络训练误差反而更大。本节将深入探讨两个解决深度网络训练难题的核心机制:残差连接与归一化。学完本节,你将能回答:为什么ResNet能训练上千层的网络?BatchNorm和LayerNorm分别解决了什么问题,它们有何不同?这些机制在代码中如何实现?
💡 核心导读
- 残差连接:将学习目标从“完整的映射”改为“残差映射”,通过恒等快捷连接实现,解决了深度网络的梯度消失和退化问题。
- 批归一化:在训练时对每个特征通道在批次维度上进行归一化,稳定层输入的分布,加速训练并允许使用更高的学习率。
- 层归一化:在训练和测试时对单个样本的所有特征进行归一化,不依赖批次大小,尤其适用于循环神经网络和小批量场景。
- 机制对比:理解BatchNorm和LayerNorm在归一化维度上的根本区别,以及它们各自适用的场景。
🧭 前置知识
- 卷积层和全连接层的前向传播过程。
- 梯度下降和反向传播的基本概念(链式法则)。
- 张量的基本操作和形状概念,例如
(batch, channel, height, width)。 - 激活函数(如ReLU)及其饱和区概念。
深度网络的训练难题:退化与梯度
在《Deep Residual Learning for Image Recognition》(He et al., 2015,arXiv:1512.03385)中,作者指出,随着网络深度增加,模型性能会先饱和,然后迅速下降。关键的是,这种退化并非由过拟合导致,因为更深的网络在训练集上的误差也更高。
论文中给出了一个直观的例子:一个56层的“普通”网络比20层的网络训练误差更高。这引出了一个核心问题:理论上,一个更深的模型至少可以通过将新增的层设置为恒等映射,并复制浅层网络的权重,来达到与浅层网络相同的性能。既然这种解是存在的,为什么我们的优化器找不到它?
答案是:对当前的求解器而言,用堆叠的非线性层去逼近恒等映射出乎意料地困难。ResNet 论文特别澄清:这些「普通」网络已经用了 BatchNorm,前向信号与反向梯度的尺度都是健康的,所以退化并不是梯度消失或爆炸导致的;它是网络已经能开始收敛之后才暴露出来的优化困难——存在「构造上必然成立」的恒等解,但优化器找不到它。
残差学习:让网络学习“差值”
为了解决上述问题,残差网络提出了一个巧妙的思路:不直接让堆叠的层去拟合目标映射 ,而是让它们去拟合残差映射 。
原始的映射因此被重构为 。这里的核心假设是:优化残差映射 比优化原始的、无参考的映射 更容易。
为什么?
- 极端情况:如果对于某些层,恒等映射 是最优的,那么将残差 推向零,比用一堆非线性层去拟合恒等映射要容易得多。
- 一般情况:如果最优函数更接近恒等映射而非零映射,那么求解器相对于恒等映射寻找扰动(即残差),也比从头学习一个新函数要容易。
残差块的实现:恒等快捷连接
这个 的公式可以通过带有“快捷连接”的前馈神经网络来实现。在ResNet中,快捷连接执行简单的恒等映射,并将其输出与堆叠层的输出相加。
一个基础的残差块可以形式化定义为: 其中 和 是块的输入和输出向量, 代表要学习的残差函数(例如,两个3x3卷积层)。
当输入和输出的维度一致时,快捷连接就是恒等映射。当维度增加时(例如下采样),论文中采用了两种策略:1) 快捷连接仍然执行恒等映射,额外用零填充缺失的维度;2) 使用1x1卷积进行投影来匹配维度。
下面我们用代码实现一个最简单的残差块,并观察其前向传播的形状变化。
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
class BasicBlock(nn.Module):
"""一个基础的残差块,包含两个3x3卷积层。假设输入输出通道数相同。"""
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
# 快捷连接
# 如果步长不为1或输入输出通道数不同,则需要投影快捷连接
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
identity = x
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(identity) # 关键:残差连接
out = F.relu(out)
return out
# 构造一个模拟输入(batch=2, channel=64, height=32, width=32)
x = torch.randn(2, 64, 32, 32)
block = BasicBlock(64, 64, stride=1) # 通道不变,不进行下采样
block.eval() # 切换到评估模式,避免BatchNorm的随机性
with torch.no_grad():
output = block(x)
print("输入 x.shape =", x.shape)
print("输出 output.shape =", output.shape)
print("快捷连接 shortcut 是一个:", block.shortcut)
# 验证快捷连接是恒等映射(在此例中)
if isinstance(block.shortcut, nn.Sequential) and len(block.shortcut) == 0:
print("快捷连接执行恒等映射(直接相加)。")
else:
print("快捷连接包含1x1卷积以匹配维度。")输入 x.shape = torch.Size([2, 64, 32, 32])
输出 output.shape = torch.Size([2, 64, 32, 32])
快捷连接 shortcut 是一个: Sequential()
快捷连接执行恒等映射(直接相加)。归一化:稳定层输入的分布
深度网络训练的另一个核心挑战是内部协变量偏移。在《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》(Ioffe & Szegedy, 2015,arXiv:1502.03167)中,作者将其定义为:由于网络参数在训练期间不断更新,导致每一层输入的分布也随之发生变化。
这种变化迫使后续层需要不断适应新的输入分布,降低了训练效率。它还会导致梯度问题:例如,对于Sigmoid或Tanh激活函数,如果输入绝对值很大,梯度会趋近于零(饱和区),训练将非常缓慢。
批归一化的核心思想是:在每一层的激活函数之前,对输入进行归一化,使其均值为0,方差为1。但关键点在于,这个均值和方差不是在整个数据集上计算(不现实),而是在当前训练的小批量数据上计算。
批归一化(BatchNorm)的算法
对于一个具有 维输入的小批量数据 ,BatchNorm 对每一维(通常是特征通道)独立地进行如下操作:
- 计算小批量均值和方差:
- 归一化: ( 是一个很小的常数,用于数值稳定性)
- 缩放与偏移: 其中 和 是可学习的参数(分别称为“增益”和“偏置”),使得网络可以恢复原始的表示能力(如果最优的话)。
在训练时,均值和方差来自当前小批量。在测试/推理时,通常使用训练阶段估算的移动平均均值和方差。
import torch
import torch.nn as nn
torch.manual_seed(0)
# 模拟一个卷积层的输出特征图 (batch=4, channels=3, height=5, width=5)
# BatchNorm2d 在通道维度(dim=1)上进行归一化。
batch_size, channels, h, w = 4, 3, 5, 5
feature_map = torch.randn(batch_size, channels, h, w)
# 使用 PyTorch 的 BatchNorm2d
bn_layer = nn.BatchNorm2d(num_features=channels, eps=1e-5, momentum=0.1)
# 默认处于 train 模式:用当前批次的均值/方差归一化,并同步更新 running 统计量
print("原始特征图(第一个样本的第一个通道,前几个值):")
for row in feature_map[0, 0, :3]:
print(" ", [round(v.item(), 4) for v in row[:3]])
with torch.no_grad():
output = bn_layer(feature_map)
print("\nBatchNorm 后(第一个样本的第一个通道,前几个值):")
for row in output[0, 0, :3]:
print(" ", [round(v.item(), 4) for v in row[:3]])
print("\nBatchNorm 层的状态:")
print(" gain (weight):", [round(w.item(), 4) for w in bn_layer.weight.data])
print(" bias (bias):", [round(b.item(), 4) for b in bn_layer.bias.data])
print(" running_mean:", [round(m.item(), 4) for m in bn_layer.running_mean])
print(" running_var:", [round(v.item(), 4) for v in bn_layer.running_var])原始特征图(第一个样本的第一个通道,前几个值):
[-1.1258, -1.1524, -0.2506]
[0.692, -0.316, -2.1152]
[0.35, 0.3081, 0.1198]
BatchNorm 后(第一个样本的第一个通道,前几个值):
[-1.2134, -1.2428, -0.2459]
[0.796, -0.3183, -2.3071]
[0.4179, 0.3717, 0.1635]
BatchNorm 层的状态:
gain (weight): [1.0, 1.0, 1.0]
bias (bias): [0.0, 0.0, 0.0]
running_mean: [-0.0028, 0.0017, 0.0032]
running_var: [0.9827, 0.9868, 0.9924]看两组数:归一化前这一通道的取值有正有负、整体偏负;归一化后每个值都被拉向「以 0 为中心、方差约 1」的尺度(gamma=1, beta=0 时不做额外缩放)。注意 running_mean / running_var 也变了——train 模式下它们会按 momentum=0.1 往当前批次的统计量靠拢,eval 推理时用的就是这两个累积值,而不是当前 batch 的统计量。
层归一化(LayerNorm)的引入
BatchNorm 依赖于足够大的小批量来获得稳定的统计量,这不适用于小批量、在线学习或循环神经网络。因为RNN在不同时间步的统计量不同,且序列长度可变。
《Layer Normalization》(Ba et al., 2016,arXiv:1607.06450)提出了另一种方案:对单个样本的所有特征(或指定维度)计算均值和方差进行归一化。
对于一个包含 个隐藏单元的层,其输入向量 ,层归一化计算: 然后进行类似的缩放和偏移:。
关键区别:LayerNorm 的统计量计算不跨批次,只依赖于当前样本。因此,它在训练和测试时的行为完全一致,且对批次大小不敏感。
import torch
import torch.nn as nn
torch.manual_seed(0)
# 模拟一个全连接层或RNN时间步的输出 (batch=2, features=6)
# LayerNorm 默认在最后一个维度进行归一化。
batch_size, features = 2, 6
x = torch.randn(batch_size, features)
print("输入 x:")
for i in range(batch_size):
print(" ", [round(v.item(), 4) for v in x[i]])
# 使用 PyTorch 的 LayerNorm
# normalized_shape 可以是一个整数(最后一维大小)或元组(指定要归一化的维度)
layer_norm = nn.LayerNorm(normalized_shape=features, elementwise_affine=True)
# 初始时,gamma=1, beta=0
with torch.no_grad():
output = layer_norm(x)
print("\nLayerNorm 后输出:")
for i in range(batch_size):
print(" ", [round(v.item(), 4) for v in output[i]])
# 手动验证第一个样本的计算
sample = x[0]
mean = sample.mean()
var = sample.var(unbiased=False) # LayerNorm 使用有偏方差(除以N)
std = torch.sqrt(var + layer_norm.eps)
manual_norm = (sample - mean) / std
# 应用可学习参数(初始为1和0)
manual_output = manual_norm * layer_norm.weight + layer_norm.bias
print(f"\n手动验证第一个样本:")
print(f" 均值 μ = {mean.item():.4f}")
print(f" 方差 σ² = {var.item():.4f}")
print(f" 手动归一化后(前3个值): {[round(v.item(), 4) for v in manual_output[:3]]}")
print(f" LayerNorm 输出(前3个值): {[round(v.item(), 4) for v in output[0, :3]]}")
print(f" 两者是否接近? {torch.allclose(manual_output, output[0], atol=1e-5)}")输入 x:
[1.541, -0.2934, -2.1788, 0.5684, -1.0845, -1.3986]
[0.4033, 0.838, -0.7193, -0.4033, -0.5966, 0.182]
LayerNorm 后输出:
[1.6176, 0.1452, -1.3681, 0.837, -0.4898, -0.7419]
[0.8003, 1.5688, -1.1845, -0.6259, -0.9677, 0.409]
手动验证第一个样本:
均值 μ = -0.4743
方差 σ² = 1.5522
手动归一化后(前3个值): [1.6176, 0.1452, -1.3681]
LayerNorm 输出(前3个值): [1.6176, 0.1452, -1.3681]
两者是否接近? True残差与归一化的协同效应
在实践中,残差连接和归一化层(尤其是BatchNorm)常常一起使用,它们共同解决了深度网络训练的稳定性问题。
- 残差连接:提供了清晰的梯度通路(快捷连接),使得梯度可以直接回传,缓解了梯度消失。
- 归一化:稳定了每一层输入的分布,使得优化过程更加平滑,允许使用更大的学习率,并对模型有一定的正则化效果。
在ResNet的原始设计中,每个残差块内部就采用了“卷积 -> BatchNorm -> ReLU”的顺序。这种组合成为了构建极深网络的基石。
📝 动手练一练
问题:假设我们有一个两层的全连接网络(输入 10 维 → 隐藏 20 维 → 输出 5 维)。请为它加上残差连接(残差块内部是「线性 → LayerNorm → ReLU」的重复),写出修改后的前向传播伪代码。再思考:如果隐藏层维度改成 30,残差连接需要怎么调整?
👉 点击查看参考答案
思路:残差连接通常加在具有两个或更多线性变换(如全连接层、卷积层)的块上。当输入输出维度一致时,使用恒等快捷连接;不一致时,需要使用一个线性投影(如一个无偏置的全连接层)来匹配维度。层归一化通常应用于线性变换的输出之后、非线性激活之前。
伪代码(修改后):
定义网络:
线性层1: input_dim -> hidden_dim
层归一化1: 对 hidden_dim 维度
线性层2: hidden_dim -> hidden_dim
# 说明:为接残差,这一层输出维度需与(投影后的)输入一致
层归一化2: 对 hidden_dim 维度
线性层3: hidden_dim -> output_dim
前向传播(x):
identity = x
# 第一个残差块
out = 线性层1(x)
out = 层归一化1(out)
out = ReLU(out)
out = 线性层2(out)
out = 层归一化2(out)
# 处理快捷连接
if identity.shape != out.shape:
# 需要投影匹配维度,可用无偏置线性层
# 例如 nn.Linear(input_dim, hidden_dim, bias=False)
identity = 线性投影层(identity)
out = out + identity # 残差连接
out = ReLU(out)
# 输出层
out = 线性层3(out)
return out关于投影层:在这个结构里 identity 就是网络输入(10 维),而残差块的输出是 hidden_dim 维——所以 hidden_dim=20 时本来就需要一层 nn.Linear(10, 20, bias=False) 把 identity 投影到 20 维,不是「维度变了才要加」。把 hidden_dim 改成 30 后,整体结构不变,只需把投影层相应换成 nn.Linear(10, 30, bias=False),残差相加依然成立。
常见错误
| 错误理解或写法 | 现象或报错 | 原因 | 正确做法 |
|---|---|---|---|
在测试阶段忘记将 BatchNorm 切换到 eval() 模式。 | 模型性能不稳定,每次推理结果有微小差异。 | train() 模式下的 BatchNorm 使用当前批次的统计量,而不是训练好的移动平均。 | 在模型推理前调用 model.eval();或在 torch.no_grad() 上下文中确保 BN 处于评估模式。 |
在使用 LayerNorm 时,normalized_shape 参数设置错误。 | 运行时形状错误,如 RuntimeError: Expected normalized_shape… to be of size…。 | normalized_shape 需要与输入张量最后若干维的大小完全匹配。 | 仔细核对输入张量希望被归一化的那些维度的大小。例如,对于形状 (B, T, D) 的序列输入,若想对每个时间步的特征独立归一化,应使用 LayerNorm(D)。 |
| 认为残差连接可以完全解决梯度消失。 | 在极深的网络中(如1000层),训练仍然可能失败。 | 残差连接改善了梯度流,但并非万能。仍需要合理的初始化、归一化以及合适的优化器配合。 | 将残差连接、归一化、合适的初始化(如He初始化)结合使用。 |
| 在维度不匹配时,残差连接直接相加。 | RuntimeError: The size of tensor a must match the size of tensor b。 | 快捷连接的输出与残差分支的输出形状(通道数、高、宽)不一致。 | 使用1x1卷积(对于CNN)或线性投影(对于全连接)来调整快捷连接的维度,或者使用池化调整空间尺寸。 |
本章小结
- 残差学习通过让网络学习输入与目标输出之间的“残差”,并通过恒等快捷连接实现,有效缓解了深度网络的退化问题和梯度消失难题。
- 批归一化通过对每个特征通道在批次维度上进行归一化,减少了内部协变量偏移,大幅加速训练,并起到正则化作用,但依赖于足够大的批次大小。
- 层归一化对单个样本的所有指定特征进行归一化,其行为在训练和测试时一致,不依赖批次大小,特别适用于RNN和小批量训练场景。
- 残差连接和归一化技术是构建现代深度神经网络(如ResNet, Transformer)不可或缺的组件,它们共同提升了模型的优化稳定性和可扩展性。
📋 行动清单
- 在代码中实现一个包含 BatchNorm 的残差块和一个包含 LayerNorm 的残差块,比较它们在相同输入下的输出差异。
- 将上一节实现的简单CNN中的几个卷积层替换为残差块,观察训练曲线是否更稳定。
- 尝试将 BatchNorm2d 的
momentum参数从默认的0.1改为0.9,观察训练时running_mean和running_var的更新速度变化。
延伸阅读
Deep Residual Learning for Image Recognition(He et al., 2015, arXiv:1512.03385)—— 残差连接原始论文 - https://arxiv.org/abs/1512.03385Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift(Ioffe & Szegedy, 2015, arXiv:1502.03167)—— BatchNorm 原始论文 - https://arxiv.org/abs/1502.03167Layer Normalization(Ba, Kiros & Hinton, 2016, arXiv:1607.06450)—— LayerNorm 原始论文 - https://arxiv.org/abs/1607.06450How Does Batch Normalization Help Optimization?(Santurkar et al., 2018, arXiv:1805.11604)—— 对「消除内部协变量偏移」这一解释的实验再检验 - https://arxiv.org/abs/1805.11604
—— 小象教研组