前四章把一件事讲通了:怎样搭出神经网络,怎样做前向传播,又怎样沿计算图把梯度传回来。可真正开始训练后,你会发现“公式能运行”和“实验可信”之间还隔着一段路。
验证集变好,可能只是数据泄漏;训练损失不降,可能不是模型太小,而是输入尺度、初始化或梯度出了问题;加上正则化后指标变差,也不能直接断言正则化无效,因为训练轮数、学习率和权重衰减的实现可能同时变了。
这一章不提供一张包治百病的超参数表。我们要建立的是一套排查顺序:先保证评估没有作弊,再从训练误差与验证误差定位问题,随后选择正则化或稳定训练的工具,最后用梯度检查和可复现实验确认结论。以后换数据、换网络,这套顺序仍然能用。

数据划分不是把一个表格随机切成三份就结束了。三份数据承担不同职责:

测试集之所以要单独保留,是因为我们会反复查看验证结果。试过的模型越多,越可能无意中挑到一个“刚好适合这份验证集”的方案。测试集像一只封条完好的信封:模型选择结束后再打开,它评估的是整套开发流程,而不只是最后一次参数更新。
不存在对所有任务都正确的 60/20/20、80/10/10 或 98/1/1。合理大小取决于至少四件事:总样本量、类别稀有程度、指标波动、以及你希望区分多小的性能差异。
比如验证集上有 100 个少数类样本,模型只多识别对 2 个,指标变化就可能很明显;这时验证集即使占总数据的比例不低,仍可能不足。反过来,百万级独立同分布样本中,较小比例的验证集也可能给出很窄的置信区间。实践里应先问“这份集合能否稳定地区分候选方案”,再决定比例。
随机划分也不是默认答案。如果同一个人贡献了多张图片、同一台设备连续产生许多记录,样本之间就有关联。把同一主体的数据拆到训练和测试两边,会让测试变成“见过同一个人的另一张图”。时间序列则常按时间切分,用过去预测未来。分组、时间和业务来源通常比一个漂亮的百分比更重要。
验证集和测试集应代表模型部署时真正会遇到的目标分布,并尽量保持一致。训练集可以额外吸收公开数据、合成数据或较旧数据,但你要知道这些来源与目标分布的差异。否则,验证集优化出的选择未必能迁移到测试或线上环境。
“来自同一分布”不是要求三份文件逐像素相似,而是要求验证与测试遵守同一套抽样口径。例如都来自未来一个月的真实用户请求、具有相同标签定义和纳入规则。若线上分布本身分成多个地区或设备,评估也应保留这些切片。
泄漏指训练流程使用了预测时本不该知道的信息。它不只发生在把测试样本直接用于训练时,还可能藏在预处理和标签构造里:
可靠的顺序是先确定划分,再只在训练部分执行所有带有 fit 含义的操作。验证集和测试集只能调用已经拟合好的 transform。如果使用交叉验证,预处理也要放进每个折的训练管线中,而不是在折外对全量数据先拟合。
只要你根据测试集结果改变了任何选择——包括模型、阈值、特征或数据清洗规则——这份集合就已经参与开发,不再是独立测试集。可以把它降级为新的验证集,但需要另找一份未参与决策的数据做最终评估。
训练结束后只看一个验证准确率,几乎无法决定下一步。我们至少要同时看训练集表现、验证集表现、任务本身可达到的参考水平,以及具体错了哪些样本。
在这套实践语言里,高偏差表示模型连训练数据中的可学习规律都没有充分拟合;高方差表示训练表现与验证表现之间有明显落差。可以先看两段差距:
参考误差可以来自可靠的人类表现、成熟系统、噪声分析或领域上限,但它不是凭空设成 0。若标签本身有歧义,人类也经常意见不一,那么训练误差为 8% 未必表示严重欠拟合。
假设某任务的参考误差约为 5%,下面几组结果可以这样读:

这张表给的是诊断线索,不是自动处方。训练误差高时,继续训练、调学习率、改初始化和增加容量都可能有用;但如果根因是标签错了,再加十层网络也不会解决。验证差距大时,更多数据、数据增强、权重衰减或简化模型可能有帮助;但如果训练与验证来自不同口径,应先修数据。
单个终点会掩盖训练过程。把训练损失和验证损失画在同一横轴上,常见形态有:
nan:优先查数值范围、学习率和梯度爆炸;若模型错了 500 个样本,随机抽取一批逐条标注错误类型,往往比再试五个正则化系数更有信息。你可以记录:标签是否可信、输入是否模糊、是否属于某个业务切片、是否需要上下文、是否是系统性混淆。
例如 100 个抽样错误里,45 个来自严重模糊,30 个是标签规则不一致,只有 10 个像典型过拟合。此时收集更清晰的数据、修标签规范,可能比单纯增加 Dropout 更直接。错误类别不是互斥的,同一个样本可以同时被标为“低光照”和“标签可疑”。
准确率适合类别相对均衡、误判代价接近的任务。类别极不均衡时,还要看精确率、召回率、PR 曲线或按类别指标;阈值必须在验证集上选择。先让指标对应真实目标,偏差—方差诊断才有意义。
当训练集拟合得很好、验证集明显落后时,限制模型对训练样本细节的依赖是一条常见路线。L2 正则化在经验损失上增加参数平方和。若一个批次有 个样本,可写成:

对第 层权重求导,正则项贡献为:
若使用普通梯度下降,更新可以整理为:
这就是“衰减”一词的直观来源:每次更新都先把原权重缩小一点,再加上数据梯度带来的变化。权重整体受限后,模型通常不容易依赖少数极端参数,但这不是“大权重一定代表过拟合”的充分证明,也不保证每个任务都能受益。
不同教材和框架可能把 、 吸收到 中。两个实现即使都把参数命名为 weight_decay=1e-4,实际作用也可能因损失是求和还是求平均、每个 epoch 的更新次数、学习率调度而不同。比较实验时要看更新公式,不能只比超参数名字。
实践中常只衰减权重矩阵,不衰减偏置和归一化层的缩放、平移参数。原因不是数学上绝对禁止,而是这些一维参数的角色与大权重矩阵不同,许多架构采用分组设置后更稳定。是否排除仍应由验证实验确认。
对普通 SGD,把 L2 梯度加到数据梯度中,与按相应比例直接缩小参数可以互相换算。Adam 这类自适应方法会按坐标缩放梯度;如果先把 L2 项加进梯度,它也会被动量和二阶矩估计处理,因此不再等价于直接衰减参数。
AdamW 把权重衰减从损失梯度更新中拆开。PyTorch 的 AdamW 文档也明确说明,衰减项不会累积到动量与方差中。下一章学习 Adam 时,我们会再看它为什么要保存一阶矩和二阶矩;此处先记住:使用自适应优化器时,不要把“在损失里加 L2”和“AdamW 的解耦权重衰减”当成同一实现。
import torch
decay, no_decay = [], []
for name, parameter in model.named_parameters():
if not parameter.requires_grad:
continue
if parameter.ndim == 1 or name.endswith(".bias"):
no_decay.append(parameter)
else:
decay.append(parameter)
optimizer = torch.optim.AdamW(
[
{"params": decay, "weight_decay": 1e-4},
{
正则化强度与学习率、训练时长和模型规模会相互影响。一次只比较一个改动,并同时记录训练与验证曲线。若训练误差也大幅恶化,常见原因是正则化过强,或者优化设置还没有配合调整。
Dropout 在训练时对激活随机采样一个伯努利掩码。设保留概率为 ,某层原激活为 ,掩码为 ,inverted dropout 的训练计算是:
因为 ,所以:
除以 的动作发生在训练期。推理时不再随机丢弃单元,也不需要再乘或除保留概率,整个 Dropout 层相当于恒等映射。这正是 PyTorch Dropout 的行为。
import torch.nn as nn
model = nn.Sequential(
nn.Linear(128, 256),
nn.ReLU(),
nn.Dropout(p=0.2),
nn.Linear(256, 10),
)
model.train() # Dropout 随机置零,并按 1 / (1 - p) 缩放
train_one_epoch(model, optimizer)
model.eval() # Dropout 关闭;归一化层也切换到评估行为
validation_metrics = evaluate(model, validation_loader)每次前向传播使用不同掩码,某个单元不能假设另一个单元必然存在。网络因而需要学习在多种局部缺失组合下仍能工作的表示。原始 Dropout 论文把测试时的完整网络解释为对许多稀疏子网络预测的一种高效近似。
这个解释不等于“Dropout 总会提升性能”。丢弃率过高会降低有效容量,让训练更慢;在已经使用强数据增强、归一化或其他正则化的网络中,额外 Dropout 可能收益很小。卷积层、注意力层和全连接层也没有一条通用的最佳丢弃率,应该根据验证曲线与架构习惯选择。
Dropout 会使同一批数据的损失在两次前向传播中不同,也会破坏数值梯度检查的前提。排查基础实现时,可以先设 p=0 或切到确定性模式,确认没有 Dropout 时损失能下降、一个小批次能被拟合,再逐步加入正则化。
忘记调用 model.eval() 会让验证和线上推理继续随机丢弃激活,结果随运行波动。反过来,训练时误用 model.eval(),又会让 Dropout 失效,并改变 BatchNorm 等层的行为。训练/评估模式应成为显式检查项。
正则化不只发生在参数上。数据增强改变训练样本,早停改变训练所走的路径。两者都很实用,但前提是规则与任务匹配。
对输入做变换并保持标签不变,实际上是在告诉模型:“这种变化不应改变答案。”水平翻转一张普通物体照片,类别通常不变;但翻转含文字的路牌、左右眼医学影像或具有方向含义的手势,标签语义可能被破坏。
所以增强规则不能从“图像任务常用列表”里机械复制。可以按下面顺序审查:
先写清部署环境中哪些变化真实存在,例如拍摄角度、亮度、裁剪位置或背景噪声。
再逐项确认变换后标签是否仍成立。数字 6 旋转后可能接近 9,文本镜像后也不再保持原语义。
对检测、分割、关键点等任务,同步变换标注。图片翻转而掩码不翻转,会直接制造错误监督。
最后只在训练集启用随机增强,验证和测试使用确定性的必要预处理,以便指标可比较。
增强强度也需要验证。裁掉目标主体、把颜色改到传感器不可能出现的范围,虽然制造了“新样本”,却可能把任务改成学习人工伪影。最直观的检查是把增强后的样本和标签画出来,让领域人员确认它们仍然合理。
过参数化网络可能先学到稳定模式,继续训练后再逐渐贴合噪声。早停持续监控验证指标,在一段时间没有改善后停止,并恢复历史上最好的检查点。
一个稳妥实现通常包含:
monitor:与任务目标一致的验证指标;mode:指标应增大还是减小;patience:允许多少次无改善,避免被单次噪声触发;min_delta:多大的变化才算真正改善;早停的优点是节省计算,并且常产生正则化效果。缺点也很具体:它占用验证集做训练轮数选择;嘈杂的验证指标可能让停止点不稳定;过早停止会把“优化还没到位”误判成“继续训练会过拟合”。如果后续还有学习率下降阶段,耐心值也要给模型足够时间响应新的学习率。
早停和权重衰减解决问题的方式不同,不必把它们强行设成二选一。实践里可以同时使用:权重衰减改变每次更新,早停限制训练轨迹长度。是否组合以及训练多久,都由验证实验决定。
不同特征的数值尺度相差很大时,优化器可能在陡峭方向来回震荡,而在平缓方向前进很慢。对每个特征做标准化,是常见的输入预处理。设训练集第 个特征的均值与标准差为:
对训练、验证、测试和线上输入都使用同一组训练统计量:
用于避免常数特征导致除零。验证集和测试集即使有自己的均值与标准差,也不能各自重新拟合,否则同一个原始数值会在不同集合中获得不同含义,而且评估数据的信息会进入预处理。
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
pipeline = make_pipeline(
StandardScaler(),
LogisticRegression(max_iter=1000),
)
pipeline.fit(X_train, y_train) # scaler 只在训练集拟合
validation_score = pipeline.score(X_val, y_val)
test_score = pipeline.score(X_test, y_test)StandardScaler 做的是减去均值、除以标准差,并不保证变换后的分布呈高斯形状。若特征有极端离群点,均值和标准差会很敏感,可以考虑基于分位数的稳健缩放,但它同样只能在训练集上拟合。
图像像素固定除以 255 属于预先规定的尺度变换,不需要从数据估计统计量;若还要做逐通道均值与标准差标准化,这些统计量应来自训练数据或明确的预训练模型规范。
输入标准化发生在数据管线中,使用固定的训练集统计量。BatchNorm 是网络中的一层:训练时使用小批量统计并更新运行统计,评估时使用保存的运行统计。两者都涉及中心化和缩放,但位置、参数和训练/推理行为不同。
这也解释了为什么 model.eval() 同时关系到 Dropout 与 BatchNorm。评估时若 BatchNorm 仍使用当前小批量统计,单个样本的预测可能依赖同批次里还有谁,指标也会随批次组成变化。
“先标准化再划分”是非常隐蔽的泄漏。正确顺序永远是先划分,再在训练集上拟合 scaler;交叉验证时则要在每个训练折内重新拟合。使用 Pipeline 可以把这条边界写进代码结构。
反向传播把许多层的局部导数连乘。简化地写,从第 层回到第 层会经过一串雅可比矩阵:
若这些局部变换在主要方向上的尺度长期小于 1,梯度会逐层缩小;长期大于 1,梯度可能迅速放大。真实网络中的方向与非线性更复杂,但“多次相乘会放大尺度偏差”是理解问题的核心。
梯度消失常表现为靠近输入的层几乎不更新,训练损失下降很慢,Sigmoid 或 tanh 激活大量落在饱和区。梯度爆炸常表现为梯度范数突然变得极大、损失剧烈跳动,甚至出现 inf 或 nan。
可以每隔若干步记录逐层激活均值、标准差、零值比例和梯度范数。只看最终损失,很难分辨是输入尺度、初始化、激活、学习率还是某一层实现导致的问题。
Glorot/Xavier 初始化试图同时照顾前向与反向的方差,常见均匀或正态版本围绕下面的尺度设计:
对 ReLU 一类整流激活,He/Kaiming 初始化考虑到约有一部分激活被截为 0,常用:
因此正态分布的标准差是 ,不是方差本身。初始化不是训练中的修补动作,它从第一个前向传播开始决定激活和梯度是否落在合适范围。选择时还要与激活函数以及框架的 fan_in、fan_out 约定对应。
归一化层和残差连接也能改善深层网络的信号传播,但它们不会替代正确初始化。学习率过大时,即使初始尺度合理,参数也可能在一次更新后冲到不稳定区域。
按全局范数裁剪时,先把所有参数梯度视为一个向量 。给定上限 :
当范数不超过 时梯度不变;超过时整体按同一比例缩小,方向保持不变。裁剪对偶发爆炸特别有用,在循环网络和长序列训练中很常见。
但裁剪处理的是症状,不会让已经消失的梯度重新出现,也不能修正错误的反向公式。如果每一步都被大幅裁剪,应继续检查学习率、损失尺度、初始化和数据异常,而不是只把阈值越调越小。
optimizer.zero_grad()
loss.backward()
total_norm = torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=1.0,
)
optimizer.step()记录裁剪前的 total_norm 和触发频率。偶尔裁剪与每步裁剪代表的训练状态不同。阈值没有跨任务通用的绝对值,它与损失缩放、批量大小和参数化方式有关。
手写反向传播最麻烦的错误,不一定让程序崩溃。少乘一个系数、把转置写错,损失仍可能缓慢下降。梯度检查用数值近似与解析梯度逐项比较,是一种小规模的实现测试。
对参数向量 的第 个分量,中心差分为:
其中 只有第 位为 1。中心差分的截断误差通常比单边差分更小,但 也不能无限减小:太大会偏离局部线性近似,太小则会被浮点舍入误差淹没。
把所有解析梯度展平成 ,可以计算对称的相对误差:
相对误差越小越好,但不存在适用于所有函数、精度与参数尺度的单一通过线。ReLU 在 0 处不可导,某些参数恰好落在拐点附近时,数值差分可能跨过两侧;float32 也比 float64 更容易受舍入影响。应结合逐参数差异定位,而不是只盯一个神奇阈值。
数值差分假设 与 除参数扰动外完全相同。因此检查时要:
def central_difference(loss_fn, theta, index, epsilon=1e-6):
theta_plus = theta.copy()
theta_minus = theta.copy()
theta_plus[index] += epsilon
theta_minus[index] -= epsilon
return (
loss_fn(theta_plus) - loss_fn(theta_minus)
) / (2.0 * epsilon)若整体误差偏大,可以按层打印最大的若干差异。某层的 dW 全部异常,通常说明那一层的缓存、转置或激活导数有问题;只有偏置异常,则优先检查求和轴。找到问题后,再用不同的 和另一小批数据复查。
梯度检查不能放进正常训练循环。一个含 个参数的完整中心差分大约需要额外做 次前向计算,只适合调试小模型、单个自定义算子或抽样参数。
实践技巧很多,真正有用的是顺序。下面这份清单适合一个新任务的最小闭环。
固定 Python、NumPy 和深度学习框架的随机种子,能控制常见随机来源;数据加载 worker、随机增强和采样器也要显式传种子。PyTorch 还可以请求确定性算法,但某些确定性实现更慢,而且跨框架版本、平台或 CPU/GPU 仍不保证逐位一致。
所以复现实验至少要保存:完整配置、数据划分索引、最佳检查点、优化器状态、随机数状态、软件版本和指标计算代码。我们追求的是让另一位开发者知道数据如何进入模型、每个选择为何发生,并能在相同环境下重现结论范围。
一条很实用的完成标准是:你能用同一份配置重新跑出方向一致的结果,能解释训练与验证差距来自哪里,能指出测试集何时被打开,也能从检查点恢复训练。做到这些,实验才从“这次跑得不错”变成可审查的工程记录。
本章的正则化与诊断回答“模型为什么不泛化、训练为什么不稳定、评估是否可信”。下一章的 Momentum、RMSprop 和 Adam 主要回答“怎样利用历史梯度,让更新方向更稳定、收敛更快”。
两类问题要分开看。优化器能把训练损失降得更快,却不自动保证更好的测试表现;正则化能改善泛化,也可能让训练损失更高。进入下一章时,我们会保留本章的固定划分、曲线、梯度范数和检查点,用它们判断优化算法到底改变了什么。