1. 第七期课程走到中后段,最大的瓶颈变成了训练流程
第七期的前半程,大家主要精力都花在理解图像分类、卷积网络结构、损失函数和优化器上。那时候的作业题多半是填空或补全代码,只要理论框架搭起来,跑通一个前向传播和反向更新,任务就算完成。可到了后半程,画风突然变了:同一个网络结构、同一份训练数据,有人能把验证精度训到88%,有人却卡在71%上不去。最初我以为差距来自模型设计,翻来覆去改网络层数、换激活函数,结果毫无起色。真正把精度抬上去的,反而是回头检查训练调试流程——这成了我做第二十六篇笔记的契机。
1.1 训练调试在整套课程中的位置
如果按课程的知识线梳理,图像分类基础、卷积网络原理、经典网络结构这些属于“搭积木”阶段;迁移学习、训练技巧、超参调优则属于“把积木变成作品”的阶段。第二十六篇笔记正好落在这两个阶段的交界处。基础理论全部讲完,final project要求大家自己从零训练一个模型并做报告,这时候训练调试能力就成了刚需。
所谓训练调试,不是简单地把学习率从0.1改成0.01就完事。它是一整套系统化排查流程:数据预处理是否合理、标注有没有错、初始化尺度对不对、损失函数和标签是否匹配、优化器参数有没有互相打架、正则化力度是否恰当、验证集评估方式是否公平。任何一个环节出了问题,最终都会反映成一种现象——损失不降、精度停滞、过拟合或欠拟合。而现象背后的原因往往不止一个,只盯着单一参数调整,很容易掉进“调来调去也没好转”的死循环。
1.2 为什么这一篇选择完整复盘而不是继续讲新模型
第七期进行到这个节点,继续堆新网络架构对学习帮助已经不大。识别一个模型只需要几分钟,但把任意一个模型稳定训到可用状态,需要大量踩坑经验。我在本地复现实验时发现,很多报错和异常指标根本查不到标准答案,只能靠推理定位。这类经验恰恰是课程笔记里最值得沉淀的部分。所以这篇没有引入新结构,而是把一套调参流程完整走了一遍,顺便记录下每个环节背后的判断依据。对刚进入调参阶段的同学来说,这比再背几个网络结构词条更实用。
2. 复现一个调参场景:小分类任务从零到可用的全过程
为了方便说明,我自己搭了一个小规模图像分类实验作为载体。任务很简单:十类日常物品,训练集两万张,验证集两千张,输入分辨率224×224。网络用的是ResNet-18。选择ResNet不是因为它性能一定最好,而是它结构稳定、先验知识丰富,一旦出现异常指标,更容易把问题定位到训练流程而不是模型本身。这个选择逻辑很重要:调参实验里,变量越少,结论就越可靠。
2.1 第一次跑起来时我用了哪些初始配置
初始配置直接沿用一套比较经典的组合:batch size 64,SGD加momentum 0.9,权重衰减5e-4,初始学习率0.1,总共训练80个epoch。损失函数是交叉熵,数据增强只做了随机水平翻转和随机裁剪。这套配置在很多标准数据集上都能跑出不错的结果,所以我一开始也不想搞特殊化,先看默认环境下会出现什么问题。
第一个epoch结束时,训练损失在4.1左右,这个数值接近十类任务的随机猜测水平(ln10约等于2.3,但加上Softmax放大效应和未收敛状态,4.1也算正常)。前五个epoch,损失从4.1降到2.3,速度看起来还行。但到第十个epoch,训练损失还在1.9附近徘徊,验证集top-1精度只有55%左右。这个阶段我做的第一件事不是调学习率,而是检查数据和梯度——这两类问题不排除,后面所有超参调整都是在给错误流程打补丁。
2.2 损失不降时的标准化排查链路
我整理了一套排查顺序,每次实验遇到奇怪现象都按这个链路走,效率比随手改参数高得多。
- 先确认数据管道:直接从加载器里取一个batch,逐个检查图像与标签是否对齐、是否有全黑或全白图、增强后的图像是否超出正常像素范围。这次实验中我发现大约0.3%的图片在裁剪后只剩背景区域,标签信息几乎丢失,虽然比例不大,但会拖低验证精度上限。
- 再检查前向输出:打印网络最后一层的输出shape和数值范围。如果logits接近全零或数值特别大,说明初始化或归一化存在问题。
- 做梯度检查:取三张图,用数值梯度与反向传播梯度做对比,确认梯度计算没有bug。
- 单独跑一个batch,观察loss是否正常下降,排除训练循环里隐藏的状态污染问题。
- 最后才是优化器与学习率的调整。
这套链路在第2.1节的实验里确实发现了问题。数据清洗后,同样的配置在第十轮训练损失降到了1.4,验证精度升到61%。这轮排查让我意识到,很多“难调”的模型,其实是连基本数据问题都没解决。
2.3 一个重要的判断:先让模型在训练集上work
在进入超参数调整之前,还有一道关键检查——先在小样本上做过拟合测试。我从训练集里随机取16张图,关掉数据增强,让模型在这16张图上跑几十轮。如果loss不能降到接近0,说明网络结构或数据管道有硬伤。这个测试的价值在于:它能快速区分“模型没有能力学习”和“优化器参数不合适”。
我那次测试里,16张图的loss降到了0.02,说明网络本身没问题,问题出在泛化与收敛速度上。做完这一步,我才放心地开始调学习率和优化器。这种“先证明能记住,再谈泛化”的顺序,几乎成了我后续所有训练任务的标准动作。
3. 学习率与优化器:最容易被低估的两个旋钮
学习率可能是整个训练流程里最重要的一个超参数,但它远不是“设个0.1”那么简单。同样的网络结构,学习率策略不同,最终精度可以差出十个百分点。更重要的是,学习率与优化器参数不是独立工作的,它们之间会互相影响。这一节我把学习率策略和优化器参数拆开讲,最后再合并它们做一轮验证。
3.1 学习率热身与余弦退火的实际收益
在第2节的实验里,初始学习率0.1直接用于训练,前几个epoch损失下降明显,但到中期就开始波动。我在另外一组对比实验里,将前5个epoch设为热身阶段,让学习率从0线性升到0.1,之后再用余弦退火,在75个epoch内把学习率降到接近0。结果训练损失曲线稳定很多,验证精度从61%提到了66%。
学习率热身的作用其实是在规避大学习率下的模型参数剧烈震荡。训练刚开始,网络权重还是随机状态,梯度信号的信噪比很低,直接使用大学习率可能让权重在前期偏离最优区域。用低学习率先走几步,让BN统计量、中间层的数值分布稳定下来,再加大学习率,参数更新方向会更可信。余弦退火则保证了训练后期的小步长精细调整,让模型有机会落到更优的局部极小值附近。
实现方法也不复杂,用循环控制或者现成的调度器都能做。但要注意,热身轮数和总epoch数需要匹配,对于80个epoch的训练,5到10轮热身是比较合理的区间。如果总共只有20个epoch,热身占掉一半就有点浪费了。
3.2 Adam、SGD动量与权重衰减的组合陷阱
很多同学从SGD切换到Adam后,发现训练初期loss降得飞快,但后期精度反而不如SGD,其中一大原因就是权重衰减的处理方式。SGD配合L2权重衰减是直接把衰减项加进梯度里,Adam则带着一阶矩和二阶矩的归一化,权重衰减项的受力方式完全不同。如果沿用SGD的习惯给Adam加L2正则,等于把衰减项也做了自适应缩放,效果往往不如预期。
更稳妥的做法是用解耦权重衰减,也就是把权重衰减从梯度中分离出来,直接在参数更新时乘以一个衰减系数。这在PyTorch里对应的是AdamW优化器。我在实验中对比了L2正则的Adam和AdamW,在相同epoch数下,AdamW的验证精度高出约1.5个百分点,而且训练损失曲线更平滑。如果你的项目一直用的是自带L2的Adam,可以试试切换成AdamW,不改变其他配置,很多时候就能白捡一点精度。
另外,SGD的momentum默认0.9、Adam的betas默认(0.9, 0.999),这些默认值在大多数任务里都够用,没有必要一开始就改它们。真正值得花时间的,还是确定初始学习率和整体衰减策略。
3.3 用分段验证快速缩小超参搜索范围
如果超参空间太大,直接在完整训练上跑网格搜索,成本高得离谱。我常用的办法是缩短训练轮数,只做趋势检测。比如固定batch size 64,扫描学习率0.3、0.1、0.03、0.01、0.003,每档只训练10个epoch,然后对比训练损失下降速度与验证精度变化。
下面这组数据来自我本地复现的一次扫描,仅用于观察趋势,不代表绝对基准:
| 学习率 | 10轮后训练损失 | 10轮后验证top-1 | 结论 |
|---|---|---|---|
| 0.3 | 3.5 | 34% | 发散风险高 |
| 0.1 | 1.3 | 58% | 可继续深调 |
| 0.03 | 0.9 | 62% | 当前阶段最优 |
| 0.01 | 1.0 | 55% | 下降偏慢 |
| 0.003 | 1.5 | 47% | 明显过慢 |
只看这张表,0.03是最佳区间。但我不会直接拿0.03跑完整训练,而是把范围缩小到0.03到0.1之间再扫一轮,同时加上warmup和余弦退火,看长期表现谁更稳定。这个“先粗扫定范围,再细扫定数值”的策略,能够把完整训练从十几次压缩到三五次,省下来的时间非常可观。
4. 数值稳定性:梯度检查、初始化与批归一化背后的逻辑
调参数的优先级往往被过度关注,数值稳定性这类隐形问题反而容易被忽略。实际上,梯度计算出错、初始化尺度不合适、BN统计不稳定,都会在没有明显报错的情况下悄悄拖低模型性能。这一节把三者放在一起讲,因为它们最终影响的都是同一件事:神经网络中数值信号的合理流动。
4.1 梯度检查的正确打开方式与常见误判
梯度检查是验证反向传播正确性的最直接手段。做法很简单:对某个参数theta,用中心差分近似数值梯度,即(f(theta+eps) - f(theta-eps)) / (2*eps),然后与反向传播得到的梯度做相对误差对比。问题在于,很多人检查梯度时喜欢用整个batch的所有参数,结果相对误差永远很大,根本看不出问题所在。
正确的做法是只选取两三个样本、几个代表性的参数层做检查,并且把网络设为确定性模式,关闭Dropout和随机增强。相对误差在1e-7左右说明实现基本正确,1e-5以内可以接受,超过1e-3就需要怀疑实现是否有bug。由于浮点计算本身存在舍入误差,数值梯度使用的eps不宜太小,我一般取1e-5或1e-6。如果误差只出现在特别深或特别浅的层,还需要分别查看,避免某个层的问题被平均值掩盖。
我做第2节的实验时,用这个方法发现了residual分支与主分支在相加时少了恒等映射的梯度尺度调整的问题,修正后同配置的精度提升了大约4个百分点。这种低级Bug在代码review阶段很难发现,但梯度检查一跑就现原形。
4.2 初始化尺度为什么对深层网络影响这么大
神经网络训练的起点是权重初始化,一个容易被忽略但影响巨大的细节。如果用方差过大的分布初始化,深层网络每一层输出的数值分布会逐渐发散,梯度传到浅层时要么爆炸要么消失。反之,如果初始化方差太小,网络早期基本学不动。
不同激活函数的初始化策略也不同。ReLU族网络一般用Kaiming初始化,它根据每层输入神经元的数量fan_in来缩放方差,使得ReLU的输入信号在前向传播中保持稳定。tanh或sigmoid网络则多用Xavier初始化,它同时考虑fan_in和fan_out,把输出方差控制在合理范围。实际实验中,我把ResNet-18的初始化从默认Kaiming换成方差偏大的版本,训练损失出现了明显震荡,验证精度始终无法突破50%,换回Kaiming后一切正常。
如果你的网络用了预训练权重,这部分可以不用操心。但自己从零搭建模型时,一定要确认每一层初始化方法是否与激活函数匹配,尤其是自定义层,别只盯着卷积层,全连接层和BatchNorm的初始化同样重要。
4.3 批归一化对数值范围的缓冲作用
BatchNorm的全称含义是让层输入分布尽量稳定,它统计当前batch的均值与方差,对特征做标准化,再用可学习参数做缩放和偏移。这让训练过程对初始化尺度的依赖大大降低,同时允许使用更大的学习率。
但BatchNorm在batch size较小时会引入噪声。如果只有一个batch里统计的均值和方差偏离整体分布,模型的推理阶段就会累积误差。我在实验中将batch size从64降到8时,验证精度下降明显,这不是过拟合问题,而是BN估计不稳定导致的。如果因为显存限制必须使用小batch,可以考虑用小batch计算BN但累积全局统计量,或者在评估阶段使用ema维护的running mean和running var。对于分类任务,通常每卡batch size维持在16以上比较稳妥。
5. 过拟合信号与正则化的正确用法:先学会判断再谈调参
过拟合是训练调试里绕不开的话题,但很多人一看到验证精度低就急于增加正则化,反而把模型压得太死。正确的思路是先判断当前状态到底属于欠拟合、正常拟合还是过拟合,然后针对性地调整。这个判断主要靠训练损失、验证损失和两者的差距来完成。
5.1 先让模型过拟合一个样本,排除学习能力问题
第2.3节提到的“小样本过拟合测试”在这里作用更加突出。拿到一个训练任务后,我会先把训练集缩小到8到16个样本,关闭所有数据增强,让模型在这些样本上训练足够长的轮数。如果loss能降到接近0,说明模型具备学习能力,问题出在泛化策略或数据量上。如果loss降不下去,那就是网络结构、初始化、学习率或数据管道出了问题。
这个测试还有另一个用途:观察训练损失下降速度,判断学习率是否在一个合理量级。如果16张图都学得很慢,说明学习率可能过小;如果loss剧烈震荡,说明学习率可能偏大。很多调参问题在这个阶段就能直接看出来,不需要花几十个epoch去验证。
5.2 数据增强与Dropout的配合边界
数据增强是最有效的正则化手段之一,但它的力度需要匹配任务。图像分类里,随机水平翻转和随机裁剪几乎是无脑可加的。而颜色抖动、旋转、缩放这类的强度则要谨慎。过强的颜色抖动会让物体本来的颜色特征失真,一旦数据集本身颜色信息对分类很重要,增强反而会把标签与图像特征之间的联系打断。
BatchNorm与Dropout同时使用时也需要注意顺序。如果网络结构是“卷积块+全连接+Dropout”,通常把Dropout放在全连接层之后、输出层之前。如果Dropout用在BN之后的激活之前,BN的分布统计会被打乱,等于在标准化后的信号里再随机丢弃,效果不稳定。我通常的做法是:在全连接层前设置Dropout(概率0.2到0.5),优先保住卷积提取的特征完整性,再在全连接层做随机丢弃。这个安排对ResNet这类结构比较友好。
5.3 早停与模型快照:保存最优状态而不是最后一轮
很多人训练完直接保存最后一个epoch的权重,这在学习率退火做得不好时,会白白丢掉验证精度最高的状态。我习惯在每个epoch结束(或每隔5个epoch)评估验证损失,并记录top-1精度。如果验证指标比之前更好,就把当前权重保存为best_model;如果连续10个epoch没有提升,就触发早停,恢复最优权重并结束训练。
这个策略和余弦退火配合起来效果尤其好:训练后期学习率已经很小,loss曲线平缓,早停能避免模型在验证集上因微小过拟合而波动。实际项目中,早停常常可以提前结束10%到20%的训练轮数,同时精度不低于多跑完所有epoch的模型。对于刚入门的朋友,我的建议是把“早停+最优快照”作为默认配置写进训练脚本,无论调什么参数都能少交点学费。
6. 把调参变成可复现的流程:记录、随机种子与我的三条经验
调参本身不复杂,复杂的是让每次实验都可比较、可复现。这个环节往往被当成“工程小事”,实际上它对学习效率的影响巨大。第二十六篇笔记最后这一部分,我想聊聊如何把整套调参过程组织成一套固定流程,顺便分享三条我认为最值得记住的经验。
6.1 每次训练必须记录的指标
训练时只盯着验证精度远远不够,需要记录的指标至少包括:训练损失、验证损失、学习率当前值、top-1与top-5精度、训练耗时、显存占用、数据增强开关、优化器参数、随机种子。这些信息最好自动输出到日志文件或可视化面板,不要依赖眼睛记录。
我见过很多朋友调了半天参数,最后问他们“上一组实验学习率是多少”完全答不上来,其实不是记性差,是把记录这件事想得太简单了。一套结构化的记录习惯,可以让同一份超参数在两周后仍然能准确复现。我的做法是把所有配置写成一个配置文件,训练脚本每次根据配置生成实验目录,目录里保存完整日志和最优权重。这样每次回看实验时,任何指标都能定位到当时的训练条件。
6.2 随机种子与可复现性的细节
随机种子对可复现性的影响比很多人想象的大。除了模型权重初始化,数据加载时的shuffle顺序、数据增强的随机操作、Dropout的随机行为,都会影响训练结果。我的习惯是给模型初始化、数据加载器、数据增强三个环节分别设置不同的随机种子,并在训练开始前统一固定全局种子。这样即使换一台机器或重跑多次,结果也不会出现数量级的跳跃。
载入模型时,如果使用多线程加载数据,需要确认每个epoch的shuffle是否都使用相同的随机流程。PyTorch这类框架里,给DataLoader设置固定的生成器对象是常用做法。细节处理好了,别人运行你的代码时,往往能直接复现相同指标;细节没处理,哪怕训练十次,每次结果都可能差出两三个百分点,根本没法判断是调参生效还是运气起作用。
6.3 第二十六篇笔记里我认为最实用的三条经验
第一条,先证明模型能过拟合小样本,再谈泛化。这条规则让我避免了很多无效调参。模型如果有学习能力,小样本上必须能快速降到低损失,不然问题在更底层。
第二条,学习率策略永远优先于其他超参数。warmup加余弦退火的组合,在大多数视觉任务里都能带来稳定的精度提升,而且实现成本极低。任何调参项目,我都会先把这条落进去。
第三条,每一次实验都要记录完整配置与指标。数据增强、学习率、优化器、随机种子、损失曲线,这些信息本身比某个具体精度数字更有价值,因为它们能帮你建立“什么参数组合在什么任务上更有效”的经验库。
调参的瓶颈从来不在于记住某几个参数值,而在于形成一套可复现、可推理的流程。把这篇笔记当作一份操作参考,在下一个任务里按链路走一遍,你会发现那些看似玄学的问题,大部分都能被这类系统方法拆解掉。