查看全课大纲(第 9 / 40 节)
  1. 1.从规则到学习:深度学习解决什么问题
  2. 2.表示学习与端到端训练
  3. 3.张量、计算图与自动微分
  4. 4.训练三要素:数据、模型、目标函数
  5. 5.从感知机到多层感知机
  6. 6.反向传播:手推一个两层网络
  7. 7.激活函数与初始化
  8. 8.优化器:SGD → Momentum → Adam
  9. 9.正则化:权重衰减、Dropout、BN/LN
  10. 10.卷积的直觉与参数共享
  11. 11.池化、感受野与下采样
  12. 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
  13. 13.残差连接与归一化为什么有效
  14. 14.迁移学习与特征可视化
  15. 15.RNN 与 BPTT
  16. 16.LSTM/GRU:门控如何缓解梯度问题
  17. 17.Seq2Seq 与编码器-解码器
  18. 18.注意力前史:Bahdanau 加性注意力
  19. 19.Self-Attention:QKV 与缩放点积
  20. 20.多头注意力:并行的多种关系
  21. 21.位置编码:正弦与可学习
  22. 22.残差、LayerNorm 与前馈层
  23. 23.从零实现一个最小 Transformer block
  24. 24.训练稳定性、复杂度与常见误区
  25. 25.自监督与语言建模目标
  26. 26.BERT:掩码语言模型与双向编码
  27. 27.GPT:自回归生成与缩放定律
  28. 28.指令微调与对齐:SFT → RLHF/DPO
  29. 29.参数高效微调:LoRA 与适配器
  30. 30.自编码器与 VAE
  31. 31.GAN:对抗训练与模式崩塌
  32. 32.扩散模型:前向加噪与反向去噪
  33. 33.扩散模型的应用与采样加速
  34. 34.对比学习与 CLIP
  35. 35.ViT:把图像当序列
  36. 36.多模态架构:对齐、融合与生成
  37. 37.能力边界、幻觉与评测
  38. 38.PyTorch 2.x 训练循环与混合精度
  39. 39.实验管理、评测与复现
  40. 40.推理部署与成本:量化/蒸馏概览

正则化:权重衰减、Dropout、BN/LN

小象实战讲义 · 深度学习基础:从神经网络到 Transformer

神经网络拥有强大的拟合能力,但这也是一把双刃剑。当模型过于复杂而数据不足时,它会“记住”训练数据的噪声和细节,导致在未见过的测试数据上表现糟糕,这就是过拟合。本节将深入探讨三种核心的正则化技术——权重衰减、Dropout 和批/层归一化——它们从不同角度出发,帮助模型学习更鲁棒、泛化能力更强的特征,从而有效对抗过拟合。学完本节,你将能回答:为什么训练时要给损失函数“加罚项”?Dropout 在训练和测试时为何表现不同?BatchNorm 和 LayerNorm 的根本区别是什么?

💡 核心导读

  1. 权重衰减:通过在损失函数中添加模型参数的 L2 范数作为惩罚项,迫使权重向零收缩,从而限制模型复杂度,是一种显式的正则化。
  2. Dropout:在训练时随机“丢弃”一部分神经元,迫使网络不依赖于任何单一特征,从而学习更鲁棒的特征表示,是一种隐式的正则化。
  3. 批归一化 (BatchNorm):对每个 mini-batch 的每个特征维度进行归一化,稳定中间层的分布,加速训练并带来一定的正则化效果。
  4. 层归一化 (LayerNorm):对单个样本的所有特征进行归一化,不依赖于 batch 统计量,常用于序列模型(如 Transformer)和 batch size 较小的场景。

🧭 前置知识

  • 神经网络的前向传播与反向传播(链式法则)。
  • 损失函数(如均方误差、交叉熵)的基本概念。
  • 张量的基本操作和形状 (batch, ...) 的概念。

权重衰减:给模型复杂度“上枷锁”

权重衰减是深度学习中最经典的正则化方法。其核心思想是:更小的权重通常对应着更简单的模型。复杂的模型(如高阶多项式)往往需要更大的权重值来拟合训练数据中的细微波动(噪声),而简单的模型则权重较小。

怎么做? 我们在原始的损失函数 L(θ)L(\theta) 上,增加一个关于所有权重参数 θ\theta 的惩罚项 R(θ)R(\theta),形成新的目标函数: J(θ)=L(θ)+λR(θ)J(\theta) = L(\theta) + \lambda R(\theta) 其中 λ>0\lambda > 0 是正则化强度超参数,控制惩罚力度。

最常用的是 L2 正则化(即权重衰减),惩罚项是权重的平方和(L2 范数的平方): R(θ)=12∑iθi2R(\theta) = \frac{1}{2} \sum_i \theta_i^2 这里的 12\frac{1}{2} 是为了求导后形式简洁。

为什么有效? 在梯度下降更新时,参数更新公式变为: θ←θ−η∇L(θ)−ηλθ\theta \leftarrow \theta - \eta \nabla L(\theta) - \eta \lambda \theta 可以看到,每次更新时,参数 θ\theta 会额外减去一个 ηλθ\eta \lambda \theta,相当于被“拉向”零点,从而抑制其增长过大。这迫使模型在拟合数据和保持参数较小之间做出权衡。

代码实现:在 PyTorch 中,权重衰减通常直接在优化器(如 torch.optim.SGD 或 torch.optim.Adam)的 weight_decay 参数中设置。

import torch
import torch.nn as nn

torch.manual_seed(0)

# 一个简单的线性模型 + 小批量数据
model = nn.Linear(in_features=10, out_features=1, bias=True)
inputs = torch.randn(4, 10)   # batch=4, feature=10
targets = torch.randn(4, 1)
criterion = nn.MSELoss()

# 前向 + 反向,得到不含正则项的原始梯度
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()

w_raw = [round(v, 4) for v in model.weight.grad[0].tolist()]
b_raw = [round(v, 4) for v in model.bias.grad.tolist()]
print("原始梯度(只来自损失函数):")
print("  weight.grad[0][:5] =", w_raw[:5])
print("  weight.grad[0][5:] =", w_raw[5:])
print("  bias.grad          =", b_raw)

# weight_decay 在优化器内部做的事:有效梯度 = 原始梯度 + lambda * 参数
lam = 0.01
grad_w_eff = model.weight.grad + lam * model.weight
grad_b_eff = model.bias.grad + lam * model.bias

w_eff = [round(v, 4) for v in grad_w_eff[0].tolist()]
b_eff = [round(v, 4) for v in grad_b_eff.tolist()]
print(f"\nweight_decay={lam} 时,优化器实际使用的有效梯度:")
print("  weight.grad[0][:5] =", w_eff[:5])
print("  weight.grad[0][5:] =", w_eff[5:])
print("  bias.grad          =", b_eff)
原始梯度(只来自损失函数):
  weight.grad[0][:5] = [0.129, 0.2177, -1.8752, 1.2173, -0.6619]
  weight.grad[0][5:] = [0.5921, 1.7392, 0.1549, -0.7654, 0.2276]
  bias.grad          = [-0.3303]

weight_decay=0.01 时,优化器实际使用的有效梯度:
  weight.grad[0][:5] = [0.1289, 0.2194, -1.8778, 1.215, -0.6631]
  weight.grad[0][5:] = [0.593, 1.7391, 0.1574, -0.7656, 0.2284]
  bias.grad          = [-0.3313]

对比两组数:第 1 个权重 0.129 → 0.1289 变小了,因为它本身是负的(λ·θ < 0,把梯度往负方向推);第 2 个 0.2177 → 0.2194 变大了,因为它本身是正的(λ·θ > 0)。两种情况的净效果一致——更新后 θ - η(g + λθ) = (1-ηλ)θ - ηg,参数每步都先被乘上一个略小于 1 的系数,这就是「权重衰减」名字的由来。

这段代码为了和公式对照,把偏置也一起算了;实践中偏置通常不衰减(它只影响输出的整体平移,不代表模型复杂度),常见做法是把 weight decay 只作用于权重,或对 bias、归一化层的 γ/β 单独设 0。PyTorch 的 weight_decay 默认对优化器中所有参数生效,要区别对待就得自己分组。

Dropout:训练时随机“失活”神经元

Dropout 由 Srivastava 等人在 2014 年提出(论文 Dropout: A Simple Way to Prevent Neural Networks from Overfitting,JMLR 2014),是一种在训练阶段随机让部分神经元“失活”的技术。

机制是什么? 在每次前向传播时,每个神经元(或更一般地,每个元素)以概率 p 被随机设置为 0(“丢弃”),以概率 1-p 被保留。被丢弃的神经元在该次前向和反向传播中不参与计算。

为什么需要缩放? 为了保证训练和测试时信号的“期望强度”一致,在训练时,保留的神经元的输出会被放大 11−p\frac{1}{1-p} 倍。这样,在测试时(不使用 Dropout),所有神经元都参与计算,其输出的期望值与训练时相同。

PyTorch 实现:

import torch
import torch.nn as nn

torch.manual_seed(0)

# 定义一个 Dropout 层,丢弃概率 p=0.5
dropout = nn.Dropout(p=0.5)

# 模拟一个 batch 的数据,形状 (batch=2, feature=6)
x = torch.arange(12, dtype=torch.float32).view(2, 6)
print("原始输入 x:")
print(x)

# 训练模式下的前向传播(默认是训练模式)
x_train = dropout(x)
print("\n训练模式下(p=0.5)Dropout 后的输出:")
print(x_train)

# 计算缩放因子:1/(1-p) = 2
scale_factor = 1.0 / (1.0 - 0.5)
print(f"\n缩放因子 1/(1-p) = {scale_factor}")
print("验证:非零元素是否被放大了2倍?")
# 找出原始输入中对应位置被保留的元素
mask = (x_train != 0)
print("被保留的元素(原始值):", x[mask].tolist())
print("Dropout 后的值:", x_train[mask].tolist())
print("是否等于原始值 * 2?", torch.allclose(x_train[mask], x[mask] * scale_factor))

# 切换到评估模式
dropout.eval()
x_eval = dropout(x)
print("\n评估模式下 Dropout 后的输出(应等于原始输入):")
print(x_eval)
print("是否等于原始输入?", torch.allclose(x_eval, x))
原始输入 x:
tensor([[ 0.,  1.,  2.,  3.,  4.,  5.],
        [ 6.,  7.,  8.,  9., 10., 11.]])

训练模式下(p=0.5)Dropout 后的输出:
tensor([[ 0.,  0.,  4.,  0.,  0.,  0.],
        [12., 14.,  0., 18., 20., 22.]])

缩放因子 1/(1-p) = 2.0
验证:非零元素是否被放大了2倍?
被保留的元素(原始值): [2.0, 6.0, 7.0, 9.0, 10.0, 11.0]
Dropout 后的值: [4.0, 12.0, 14.0, 18.0, 20.0, 22.0]
是否等于原始值 * 2? True

评估模式下 Dropout 后的输出(应等于原始输入):
tensor([[ 0.,  1.,  2.,  3.,  4.,  5.],
        [ 6.,  7.,  8.,  9., 10., 11.]])
是否等于原始输入? True

从输出可以看到,大约一半的元素被置零,而保留的元素值被放大为原来的 2 倍。在评估模式下,Dropout 层只是一个恒等映射。

为什么 Dropout 能防止过拟合?

  1. 破坏神经元间的共适应关系:防止网络过于依赖少数几个关键神经元,迫使所有神经元都必须具备一定的预测能力。
  2. 模型平均的近似:每次前向传播相当于采样了一个不同的“子网络”。训练过程可以看作是在训练大量共享参数的子网络,测试时则近似于这些子网络的平均,这类似于集成学习,能提升泛化性能。

归一化层:稳定训练分布的利器

深度神经网络训练中的一个难题是内部协变量偏移:随着网络参数的更新,每一层输入的分布会发生变化,导致后续层需要不断适应新的分布,这会减慢训练速度。归一化层通过对层的输入进行规范化,稳定其分布,从而加速训练。

需要说明的是,「消除内部协变量偏移」是 BN 原论文给出的解释,但后来的研究(Santurkar et al., 2018)用实验指出:即使人为注入协变量偏移,BN 依然有效——它的收益更多来自让损失曲面更平滑、梯度更可预测(改善优化条件数)。所以把 BN 理解为「稳定数值尺度、改善优化」比「消除分布漂移」更贴近实际。

批归一化 (Batch Normalization)

批归一化 (BN) 由 Ioffe 和 Szegedy 于 2015 年提出(论文 Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift,arXiv:1502.03167)。

操作步骤(对于全连接层): 对于一个 mini-batch 的输入 x∈RB×H\mathbf{x} \in \mathbb{R}^{B \times H}(B 是 batch size,H 是特征维度):

  1. 计算该 batch 内每个特征维度上的均值和方差: μj=1B∑i=1Bxij,σj2=1B∑i=1B(xij−μj)2\mu_j = \frac{1}{B} \sum_{i=1}^{B} x_{ij}, \quad \sigma_j^2 = \frac{1}{B} \sum_{i=1}^{B} (x_{ij} - \mu_j)^2
  2. 使用计算出的均值和方差对每个特征进行归一化: x^ij=xij−μjσj2+ϵ\hat{x}_{ij} = \frac{x_{ij} - \mu_j}{\sqrt{\sigma_j^2 + \epsilon}} ϵ\epsilon 是一个很小的数(如 1e-5),防止除零。
  3. 引入可学习的缩放和平移参数 γj\gamma_j 和 βj\beta_j,进行仿射变换: yij=γjx^ij+βjy_{ij} = \gamma_j \hat{x}_{ij} + \beta_j 这一步至关重要,它让网络有能力恢复原始的表征能力(如果 γ=σ2+ϵ,β=μ\gamma = \sqrt{\sigma^2 + \epsilon}, \beta = \mu,则恒等变换)。

训练与测试的区别:

  • 训练时:使用当前 mini-batch 的统计量(μ,σ\mu, \sigma)。
  • 测试时:使用在训练过程中通过移动平均估算出的全局均值和方差,而不是单个 batch 的统计量。

代码观察:

import torch
import torch.nn as nn

torch.manual_seed(0)

# 定义一个 BN 层,特征维度为 5
bn = nn.BatchNorm1d(num_features=5, affine=True, track_running_stats=True)
# affine=True 表示学习 gamma 和 beta
# track_running_stats=True 表示跟踪全局均值和方差(用于测试)

print("初始化的可学习参数:")
print("gamma (weight):", bn.weight.tolist())
print("beta (bias):", bn.bias.tolist())
print("运行均值 (running_mean):", bn.running_mean.tolist())
print("运行方差 (running_var):", bn.running_var.tolist())

# 模拟输入数据,形状 (batch=4, feature=5)
x = torch.randn(4, 5) * 2 + 1  # 均值为1,标准差为2的分布

def show(mat):
    """逐行打印,避免一行过长在手机上横向溢出。"""
    for row in mat.tolist():
        print("  ", [round(v, 4) for v in row])

print(f"\n输入数据 x (shape={tuple(x.shape)}):")
show(x)

# 训练模式下的前向传播
y_train = bn(x)
print("\n训练模式下 BN 输出 y_train:")
show(y_train)

# 查看训练后更新的运行统计量
print("\n一次前向传播后更新的运行统计量:")
print("  running_mean:", [round(v, 4) for v in bn.running_mean.tolist()])
print("  running_var: ", [round(v, 4) for v in bn.running_var.tolist()])

# 切换到评估模式
bn.eval()
y_eval = bn(x)
print("\n评估模式下 BN 输出 y_eval (使用 running_mean/running_var):")
show(y_eval)
初始化的可学习参数:
gamma (weight): [1.0, 1.0, 1.0, 1.0, 1.0]
beta (bias): [0.0, 0.0, 0.0, 0.0, 0.0]
运行均值 (running_mean): [0.0, 0.0, 0.0, 0.0, 0.0]
运行方差 (running_var): [1.0, 1.0, 1.0, 1.0, 1.0]

输入数据 x (shape=(4, 5)):
   [-1.2517, -1.3047, 0.4988, 0.1322, 2.1977]
   [-2.1102, 0.3173, 4.706, 1.9362, 0.6846]
   [3.8873, 1.5321, 3.7787, 4.1727, 2.8926]
   [-0.6874, 2.8637, 3.518, 5.01, 1.1075]

训练模式下 BN 输出 y_train:
   [-0.5213, -1.4035, -1.663, -1.4016, 0.5463]
   [-0.8907, -0.348, 1.0008, -0.4583, -1.1863]
   [1.6904, 0.4425, 0.4137, 0.7111, 1.3421]
   [-0.2784, 1.309, 0.2486, 1.1489, -0.7021]

一次前向传播后更新的运行统计量:
  running_mean: [-0.004, 0.0852, 0.3125, 0.2813, 0.1721]
  running_var:  [1.6199, 1.2149, 1.2326, 1.3877, 1.0017]

评估模式下 BN 输出 y_eval (使用 running_mean/running_var):
   [-0.9803, -1.261, 0.1678, -0.1265, 2.0239]
   [-1.6548, 0.2105, 3.9573, 1.4048, 0.5121]
   [3.0574, 1.3127, 3.122, 3.3034, 2.7182]
   [-0.5369, 2.5208, 2.8872, 4.0141, 0.9346]

看两组输出:训练模式下 BN 用当前 batch 自己的均值/方差做归一化,每一列基本被拉成零均值、单位方差;切到 eval() 后,用的是 running_mean / running_var——它们从 0 和 1 出发、按 momentum=0.1 只平滑更新了一步,离真实统计量还很远,所以 y_eval 与 x 并不相等,也没有像训练输出那样被严格标准化。

这正是 BN 最容易踩的坑:running_mean / running_var 是训练过程中累积出来的,步数少时很不准。真实训练要跑成百上千步,它才逼近数据的全局统计量;训练步数太少就推理,或在 eval() 下用没训练过的模型,输出会明显偏移。

层归一化 (Layer Normalization)

层归一化 (LN) 由 Ba 等人在 2016 年提出(论文 Layer Normalization,arXiv:1607.06450),旨在解决 BN 对 batch size 依赖过强的问题(小 batch 时统计量噪声大)。

核心区别:

  • BN:在 batch 维度 上计算统计量,对每个特征通道进行归一化。适用于 (B, C, H, W) 或 (B, D) 的数据。
  • LN:在 特征维度 上计算统计量,对每个样本单独进行归一化。适用于 (B, ...),归一化发生在最后几个指定的维度。

公式(对于形状为 (B, D) 的输入): 对于单个样本 xi∈RD\mathbf{x}_i \in \mathbb{R}^{D}: μi=1D∑j=1Dxij,σi2=1D∑j=1D(xij−μi)2\mu_i = \frac{1}{D} \sum_{j=1}^{D} x_{ij}, \quad \sigma_i^2 = \frac{1}{D} \sum_{j=1}^{D} (x_{ij} - \mu_i)^2 x^ij=xij−μiσi2+ϵ\hat{x}_{ij} = \frac{x_{ij} - \mu_i}{\sqrt{\sigma_i^2 + \epsilon}} yij=γjx^ij+βjy_{ij} = \gamma_j \hat{x}_{ij} + \beta_j 注意,γ\gamma 和 β\beta 的维度是 (D,),在特征维度上共享。

代码对比 BN 与 LN:

import torch
import torch.nn as nn

torch.manual_seed(0)

# 模拟一个序列数据,形状 (batch=2, seq_len=3, feature=4)
x = torch.randn(2, 3, 4)
print("输入数据 x (shape=(2, 3, 4)):")
print(x)

# 1. LayerNorm:normalized_shape=[4] 表示只对最后一个维度(feature)归一化,
#    每个 token 独立算均值和方差;若写 [3, 4],则把 seq_len 和 feature 一起归一化。
ln = nn.LayerNorm(normalized_shape=[4], elementwise_affine=True)
y_ln = ln(x)
print("\nLayerNorm 输出 (normalized_shape=[4]), 形状:", tuple(y_ln.shape))
print("样本0, token0 的4个特征:", [round(v, 4) for v in y_ln[0, 0].tolist()])
# 验证:计算样本0,token0的均值和方差
sample_0_token_0 = x[0, 0]
mean = sample_0_token_0.mean()
var = sample_0_token_0.var(unbiased=False)  # LayerNorm 使用有偏估计
normalized = (sample_0_token_0 - mean) / torch.sqrt(var + 1e-5)
print("手动计算样本0,token0的归一化结果:", [round(v, 4) for v in normalized.tolist()])

# 2. 使用 BatchNorm1d,需要将数据视为 (batch*seq_len, feature)
# 重塑数据
x_reshaped = x.view(-1, 4)  # shape (6, 4)
bn = nn.BatchNorm1d(num_features=4, affine=False)  # 暂时关闭仿射变换以便对比
y_bn_flat = bn(x_reshaped)
y_bn = y_bn_flat.view(2, 3, 4)
print("\nBatchNorm1d 输出 (重塑后计算,无仿射变换), 形状:", tuple(y_bn.shape))
print("样本0, token0 的4个特征:", [round(v, 4) for v in y_bn[0, 0].tolist()])
# 验证:计算整个重塑后 batch 在特征维度上的均值和方差
global_mean = x_reshaped.mean(dim=0)
global_var = x_reshaped.var(dim=0, unbiased=False)
normalized_bn = (x_reshaped - global_mean) / torch.sqrt(global_var + 1e-5)
print("手动计算 BN (基于6个token) 的归一化结果:", [round(v, 4) for v in normalized_bn[0].tolist()])
输入数据 x (shape=(2, 3, 4)):
tensor([[[-1.1258, -1.1524, -0.2506, -0.4339],
         [ 0.8487,  0.6920, -0.3160, -2.1152],
         [ 0.4681, -0.1577,  1.4437,  0.2660]],

        [[ 0.1665,  0.8744, -0.1435, -0.1116],
         [ 0.9318,  1.2590,  2.0050,  0.0537],
         [ 0.6181, -0.4128, -0.8411, -2.3160]]])

LayerNorm 输出 (normalized_shape=[4]), 形状: (2, 3, 4)
样本0, token0 的4个特征: [-0.9539, -1.0196, 1.2137, 0.7598]
手动计算样本0,token0的归一化结果: [-0.9539, -1.0196, 1.2137, 0.7598]

BatchNorm1d 输出 (重塑后计算,无仿射变换), 形状: (2, 3, 4)
样本0, token0 的4个特征: [-2.0843, -1.6067, -0.5491, 0.3289]
手动计算 BN (基于6个token) 的归一化结果: [-2.0843, -1.6067, -0.5491, 0.3289]

关键区别显现:

  • LayerNorm:样本0,token0 的归一化只依赖于它自己的4个特征值。
  • BatchNorm:样本0,token0 的归一化依赖于所有6个 token(2个样本x3个token)在对应特征维度上的值。

因此,LN 对 batch size 不敏感,非常适合 RNN、Transformer 等序列模型,以及 batch size 变化或很小的场景。而 BN 在图像等固定 batch size 的任务中表现优异。

📝 动手练一练

问题:假设你正在训练一个语言模型,输入序列的 shape 是 (batch_size, seq_len, hidden_dim)。你决定在 Transformer 块的开头添加一个归一化层。你应该选择 BatchNorm1d 还是 LayerNorm?请写一小段代码,对一个形状为 (2, 5, 8) 的随机张量应用你选择的归一化方法,并打印出第一个样本、第一个 token 的归一化结果。

👉 点击查看参考答案

在语言模型或 Transformer 中,通常使用 LayerNorm。因为:

  1. 序列长度 seq_len 可能变化,BatchNorm 在序列维度上计算统计量不稳定。
  2. 不同样本的序列之间没有对齐关系,跨样本归一化没有意义。
  3. LayerNorm 对每个样本独立操作,与 batch size 无关,更稳定。
import torch
import torch.nn as nn

torch.manual_seed(0)

# 模拟输入:batch=2, seq_len=5, hidden_dim=8
x = torch.randn(2, 5, 8)
print("输入形状:", tuple(x.shape))

# 使用 LayerNorm,对最后一个维度 hidden_dim 进行归一化
ln = nn.LayerNorm(normalized_shape=8, elementwise_affine=False)  # 关闭仿射以便观察纯归一化
y = ln(x)

raw = [round(v, 4) for v in x[0, 0].tolist()]
print("\n第一个样本、第一个 token 的原始值:")
print("  [0:4] =", raw[:4])
print("  [4:8] =", raw[4:])

norm = [round(v, 4) for v in y[0, 0].tolist()]
print("归一化后的值:")
print("  [0:4] =", norm[:4])
print("  [4:8] =", norm[4:])

# 手动验证
token = x[0, 0]
mean = token.mean()
var = token.var(unbiased=False)
manual_norm = (token - mean) / torch.sqrt(var + 1e-5)
manual = [round(v, 4) for v in manual_norm.tolist()]
print("手动计算归一化:")
print("  [0:4] =", manual[:4])
print("  [4:8] =", manual[4:])
print("结果是否一致?", torch.allclose(y[0,0], manual_norm, atol=1e-5))
输入形状: (2, 5, 8)

第一个样本、第一个 token 的原始值:
  [0:4] = [-1.1258, -1.1524, -0.2506, -0.4339]
  [4:8] = [0.8487, 0.692, -0.316, -2.1152]
归一化后的值:
  [0:4] = [-0.7006, -0.7294, 0.2513, 0.0519]
  [4:8] = [1.4468, 1.2764, 0.1801, -1.7766]
手动计算归一化:
  [0:4] = [-0.7006, -0.7294, 0.2513, 0.0519]
  [4:8] = [1.4468, 1.2764, 0.1801, -1.7766]
结果是否一致? True

常见错误

错误理解或写法现象或报错原因正确做法
在评估模式下忘记调用 model.eval()Dropout/BatchNorm 仍在工作,导致测试结果随机或不稳定。nn.Dropout 和 nn.BatchNorm 在训练和评估模式下的行为不同。在模型测试/推理前,务必调用 model.eval();训练时调用 model.train()。
将权重衰减应用于所有参数(包括偏置)模型可能欠拟合,收敛变慢。偏置项的大小不直接代表模型复杂度,对其衰减通常无益。在优化器中设置 weight_decay,或自定义参数组,对权重和偏置设置不同的衰减率。
在序列任务中误用 BatchNorm小 batch 或变长序列下性能波动大、不稳定。BN 依赖 batch 维度统计量,小 batch 噪声大,变长序列填充值会污染统计量。序列模型优先使用 LayerNorm。如果必须用 BN,需小心处理填充位置(如使用 mask)。
手动实现 Dropout 时忘记缩放训练和测试时网络输出的期望值不同,测试性能下降。训练时丢弃神经元导致激活值期望减小,需要放大以补偿。使用 nn.Dropout 或确保训练时对保留的激活值乘以 1/(1-p)。
混淆 nn.BatchNorm1d/2d 的输入维度RuntimeError: expected 2D or 3D input (got 4D input) 等。BatchNorm1d 期望 (B, C) 或 (B, C, L);BatchNorm2d 期望 (B, C, H, W)。根据数据实际维度(忽略 batch 维)选择对应的 BN 层。对于 (B, C, H, W) 用 BatchNorm2d。

本章小结

  1. 权重衰减通过向损失函数添加参数范数惩罚项,显式地限制模型复杂度,防止过拟合。
  2. Dropout在训练时随机屏蔽神经元,是一种高效的隐式正则化,能防止神经元共适应,近似于模型集成。
  3. BatchNorm 对每个特征通道跨 batch 进行归一化,稳定分布、加速训练,但其效果依赖于足够大的 batch size。
  4. LayerNorm 对每个样本的所有指定特征进行归一化,不依赖于 batch,特别适合序列模型和小 batch 场景。
  5. 归一化层通常包含可学习的缩放和平移参数,以保留网络的表达能力。
  6. 务必注意模型在训练模式和评估模式下的切换,这对 Dropout 和 BatchNorm 的行为至关重要。

📋 行动清单

  • 在现有的一个简单全连接网络代码中,为优化器添加 weight_decay=1e-3,观察训练损失和验证损失曲线的变化。
  • 修改上面的代码,在网络的隐藏层后添加 nn.Dropout(0.3),并确保在训练和评估时正确切换模式,比较添加 Dropout 前后模型的验证准确率。
  • 分别用 nn.BatchNorm1d 和 nn.LayerNorm 处理同一个 (4, 10) 的张量,打印出它们计算出的均值和方差,直观理解二者统计量的计算范围差异。

延伸阅读

本段 PyTorch 官方文档/教程条目的框架出处:PyTorch: An Imperative Style, High-Performance Deep Learning Library(Paszke et al., 2019,arXiv:1912.01703);页面内容以 PyTorch 2.14.1 为准。

  1. Dropout: A Simple Way to Prevent Neural Networks from Overfitting(Srivastava et al., 2014, JMLR 15) - https://jmlr.org/papers/v15/srivastava14a.html
  2. Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift(Ioffe & Szegedy, 2015, arXiv:1502.03167) - https://arxiv.org/abs/1502.03167
  3. Layer Normalization(Ba, Kiros & Hinton, 2016, arXiv:1607.06450) - https://arxiv.org/abs/1607.06450
  4. How Does Batch Normalization Help Optimization?(Santurkar et al., 2018, arXiv:1805.11604)—— 对「消除内部协变量偏移」这一解释的再检验 - https://arxiv.org/abs/1805.11604
  5. PyTorch 官方文档 torch.nn.Dropout / BatchNorm1d / LayerNorm(2.14.1) - https://pytorch.org/docs/stable/nn.html

—— 小象教研组