正则化:逼模型泛化的税 本节摘要:你的模型训练精度 99%、测试精度 60%——它在背书而非学习。正则化(Regularization)就是你强加在复杂度上的税,逼模型去泛化。参数足够多的神经网络能记住任何数据集——Zhang 等人 2017 年用标准网络在带随机标签的 ImageNet 上训练,达到接近零的训练损失,把一百万对毫无规律的输入输出背得一字不差,训练损失完美,测试精度为零。这就是过拟合,而且模型越大越严重:GPT-3 有 1750 亿参数、约 5000 亿 token 训练集,这容量足以逐字背下大量训练数据。没有正则化,它只会原样复述训练样本,学不到可泛化的模式。
本节摘要:你的模型训练精度 99%、测试精度 60%——它在背书而非学习。正则化(Regularization)就是你强加在复杂度上的税,逼模型去泛化。参数足够多的神经网络能记住任何数据集——Zhang 等人 2017 年用标准网络在带随机标签的 ImageNet 上训练,达到接近零的训练损失,把一百万对毫无规律的输入输出背得一字不差,训练损失完美,测试精度为零。这就是过拟合,而且模型越大越严重:GPT-3 有 1750 亿参数、约 5000 亿 token 训练集,这容量足以逐字背下大量训练数据。没有正则化,它只会原样复述训练样本,学不到可泛化的模式。训练与测试表现之间的鸿沟就是过拟合缝隙,本节的每一招都从不同角度攻击它:Dropout 强迫网络不依赖单个神经元,权重衰减防止单个权重过大,BatchNorm 平滑损失面让优化器找到更平、更泛化的最小,LayerNorm 在 BatchNorm 失效处(小 batch、变长序列)顶上,RMSNorm 省掉均值计算快 10%。每招都简单,合在一起就是「背书的模型」与「泛化的模型」之间的分野。本节从零实现 dropout、L2 权重衰减、BatchNorm、LayerNorm、RMSNorm,讲清 train/eval 模式切换的玄机,给出按过拟合严重度选正则策略的决策图。
阅读完本节,你应当能够:
参数够多的神经网络能记住任何数据集。这不是假设——Zhang 等人(2017)用标准网络在带随机标签的 ImageNet 上训练,网络在完全随机的标签分配上达到接近零的训练损失,把一百万对毫无规律的输入输出背得一字不差。训练损失完美,测试精度为零。
这就是过拟合,而且模型越大越糟。GPT-3 有 1750 亿参数、训练集约 5000 亿 token,这容量足以逐字背下大量训练数据,没有正则化它只会原样复述,学不到可泛化的模式。
训练表现与测试表现之间的缝隙就是过拟合缝隙。本节的每一招都从不同角度攻击它:Dropout 强迫网络不依赖单个神经元,权重衰减防止单个权重过大,BatchNorm 平滑损失面让优化器找到更平、更泛化的最小,LayerNorm 在 BatchNorm 失效处顶上,RMSNorm 省掉均值计算快 10%。每招都简单,合在一起就是「背书的模型」与「泛化的模型」之间的分野。
每个模型都处在从欠拟合(太简单抓不住模式)到过拟合(太复杂连噪声都抓)的谱上,甜蜜点在中间,正则化把模型从过拟合一侧推向中间。
最简单且解释最优雅的正则化。训练时,以概率 p 把每个神经元的输出置零。
output = activation(z) * mask 其中 mask[i] ~ Bernoulli(1 - p)
p = 0.5 时每次前向传播一半神经元被置零,网络必须学冗余表示,因为它无法预测哪些神经元可用。这防止「协同适应」——神经元学会依赖特定其它神经元在场。
集成解释:N 个神经元的网络配 dropout 会产生 2^N 个可能的子网络(每个神经元开关的组合),训练时近似同时训练所有 2^N 个子网络、各看不同小批量。测试时用全部神经元(不 dropout)并把输出乘 (1 − p) 以匹配训练期望值,这等价于对 2^N 个子网络的预测取平均——用一个模型得到一个巨型集成。
实践中缩放在训练时而非测试时做(反向 dropout):
训练时: output = activation(z) * mask / (1 - p) 测试时: output = activation(z) (无需改动)
更干净,因为测试代码完全不用知道 dropout。默认比率:Transformer p = 0.1,MLP p = 0.5,CNN p = 0.2~0.3。dropout 越强 = 正则越强 = 欠拟合风险越大。
把所有权重的平方和加进损失:
total_loss = task_loss + (lambda / 2) * sum(w_i^2)
正则项梯度是 lambda × w,这意味着每步每个权重都按其幅度比例向零收缩,大权重罚得更重,模型被推向没有单个权重主导的解。这有助泛化:过拟合模型往往有大权重放大训练噪声,权重衰减保持小权重,限制有效容量,迫使模型依赖鲁棒、可泛化的特征而非背下的怪癖。lambda 控制强度,典型值:AdamW 训 Transformer 用 0.01,SGD 训 CNN 用 1e-4,严重过拟合模型用 0.1。如第 06 节所述,权重衰减与 L2 在 SGD 中等价、在 Adam 中不等价,用 Adam 时务必用 AdamW(解耦权重衰减)。
在每层输出送入下一层前,按小批量做归一化。
mu = (1/B) * sum(x_i) (批均值) sigma^2 = (1/B) * sum((x_i - mu)^2) (批方差) x_hat = (x_i - mu) / sqrt(sigma^2 + eps) (归一化) y = gamma * x_hat + beta (缩放平移)
gamma、beta 是可学习参数,允许网络在最优时撤销归一化。没有它们,你就强迫每层输出零均值单位方差,未必是网络想要的。
训练 vs 推理的分裂:训练时 mu、sigma 来自当前小批量;推理时用训练期间累积的滑动平均(动量 0.1,即 90% 旧 + 10% 新)。BatchNorm 为何有效至今仍有争议:原论文说它减少「内部协变量偏移」(层输入分布随早期层更新而变),Santurkar 等人(2018)证明这个解释错了。真实原因是 BatchNorm 让损失面更光滑——梯度更可预测、Lipschitz 常数更小,优化器能安全地迈更大步,这就是 BatchNorm 让你能用更高学习率、更快收敛的缘由。
BatchNorm 有根本限制:它依赖批统计。batch=1 时均值方差无意义;batch 小(< 32)时统计噪声大、伤性能。这对目标检测(显存限制 batch)和语言建模(序列长度可变)很要命。
按特征而非按批归一化。对单个样本:
mu = (1/D) * sum(x_j) (特征均值) sigma^2 = (1/D) * sum((x_j - mu)^2) (特征方差) x_hat = (x_j - mu) / sqrt(sigma^2 + eps) y = gamma * x_hat + beta
D 是特征维。每个样本独立归一化,不依赖 batch 大小,这正是 Transformer 用 LayerNorm 而非 BatchNorm 的原因:序列长度可变、batch 经常很小(生成时甚至是 1)、训练与推理计算完全一致。Transformer 里 LayerNorm 加在每个自注意力块和每个前馈块之后(Post-LN),或加在它们之前(Pre-LN,训练更稳)。
去掉均值减法的 LayerNorm,由 Zhang 与 Sennrich(2019)提出。
rms = sqrt((1/D) * sum(x_j^2)) y = gamma * x / rms
仅此而已。没有均值计算、没有 beta 参数。观察发现:LayerNorm 里的再居中(减均值)对模型性能贡献很小,却要算力。去掉它,同等精度、约 10% 更少开销。LLaMA、LLaMA 2/3、Mistral 及多数现代 LLM 用 RMSNorm 取代 LayerNorm——在数十亿参数、数万亿 token 的规模下,这 10% 的节省相当可观。
它不改模型而改数据:保持标签的同时变换训练输入——图像(随机裁剪、翻转、旋转、颜色抖动、cutout)、文本(同义词替换、回译、随机删除)、音频(时间拉伸、音高平移、加噪)。效果与正则化相同:增大训练集有效规模,让模型更难背具体样本。只见过一次原图的模型能背下它,见过 50 种增强版的模型被迫学不变结构。
最简正则器:验证损失开始上升就停训,那时模型还没过拟合。实践中每轮记验证损失、存最佳模型、继续训一个「耐心」窗口(典型 5~20 轮),窗口内验证损失不改善就停,加载存的最好模型。
完整代码见原课程 phases/03-deep-learning-core/07-regularization/code/ 相应文件。
import random class Dropout: def __init__(self, p=0.5): self.p = p self.training = True self.mask = None def forward(self, x): if not self.training: return list(x) self.mask = [] output = [] for val in x: if random.random() < self.p: self.mask.append(0) output.append(0.0) else: self.mask.append(1) output.append(val / (1 - self.p)) # 反向 dropout 缩放 return output def backward(self, grad_output): return [g / (1 - self.p) if m else 0.0 for g, m in zip(grad_output, self.mask)]
反向 dropout 的精髓:训练时除以 (1 − p),测试时原样输出,测试代码无需感知 dropout。
def l2_regularization(weights, lambda_reg): penalty = sum(w * w for w in weights) return lambda_reg * 0.5 * penalty def l2_gradient(weights, lambda_reg): return [lambda_reg * w for w in weights]
维护批统计与推理用的滑动统计,带可学习 gamma/beta。训练时按当前批归一化并更新滑动均值/方差,推理时用滑动统计。
按样本特征维归一化,带可学习 gamma/beta,与 batch 大小无关,训练推理一致。
class RMSNorm: def __init__(self, num_features, eps=1e-6): self.gamma = [1.0] * num_features self.eps = eps self.num_features = num_features def forward(self, x): rms = math.sqrt(sum(xi * xi for xi in x) / len(x) + self.eps) return [self.gamma[j] * x[j] / rms for j in range(self.num_features)]
在圆形数据上分训练/测试集,比较无正则 vs dropout vs dropout+权重衰减时,训练-测试精度缝隙如何变化。正则越强,缝隙越小,但训练精度也会降——找到甜蜜点是工程的核心。
PyTorch 把全部归一化与正则化做成模块:
import torch import torch.nn as nn model = nn.Sequential( nn.Linear(784, 256), nn.BatchNorm1d(256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, 128), nn.BatchNorm1d(128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, 10), ) model.train() out_train = model(torch.randn(32, 784)) model.eval() out_test = model(torch.randn(1, 784))
model.train() / model.eval() 切换至关重要:它开关 dropout,并告诉 BatchNorm 用批统计还是滑动统计。推理前忘了 model.eval() 是深度学习里最常见的 bug 之一——测试精度会随机波动,因为 dropout 还开着、BatchNorm 还在用小批量统计。
Transformer 的模式则不同:用 LayerNorm 而非 BatchNorm,dropout p=0.1 而非 0.5,这是 Transformer 的默认值。
本节产出(位于原课程 outputs/):
prompt-regularization-advisor.md:一个提示,诊断过拟合并推荐正确的正则化策略。下一节,我们讲权重初始化——初始化错了训练压根起不来,初始化对了 50 层能像 3 层一样顺。