ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

深度学习调参方法论:学习率扫描、batch size与loss排查

深度学习调参方法论:学习率扫描、batch size与loss排查

深度学习调参经验分享这个话题,我憋了很久。带过几个项目、被loss曲线折磨过无数个通宵之后,我最常被问到的问题不是“这个网络结构怎么设计的”,而是“为什么我的loss不降”“学习率到底设多少”“batch size开多大合适”。这类问题没有标准答案,但它有方法论。调参不是玄学,它更像是在一个高维空间里做有策略的搜索:你得先知道哪些维度值得搜、搜索的步长该多大、什么时候该判定一条路走不通及时止损。这篇内容就是把我自己在不同项目里踩过的坑、沉淀下来的流程整理出来,包括超参数的优先级排序、学习率扫描的具体做法、显存和batch size的权衡、过拟合与欠拟合的判别表,以及一套可以直接抄走的实验记录模板。刚上手训练模型、正被loss曲线折磨的同学能看懂,已经能跑通但总觉得模型“差一口气”的人也能拿到东西。下面所有数值都基于常见实践,具体请结合你自己的数据和硬件条件微调,别当教条。

1. 先把调参这件事想清楚:整体设计与思路拆解

1.1 调参调的到底是什么:从一个高维搜索空间说起

很多人一上来就想着“调参”,但脑子里其实没有一张地图。我的习惯是先把这个搜索空间画出来:模型结构、优化器、学习率及调度、batch size、正则化强度、数据增强强度、损失函数形式、训练轮数,这七八个维度构成了绝大多数项目的调参空间。每一个维度都不是连续的实数轴,而是被工程条件切成了一段一段的离散选项。比如学习率,你不可能真的去试1e-3和1.0001e-3,实际有意义的是按对数刻度排布的一组值。

理解了这一点,调参的策略就清楚了:我们不是要找到全局最优,而是要在有限的试验次数内,用最少的算力找到一个“足够好且稳定”的配置。这句话听起来像废话,但它直接决定了后面所有的取舍。我见过太多人把时间耗在给一个已经收敛的模型抠0.2个点的精度上,却从没验证过自己的数据划分有没有问题。

还有一个容易被忽略的点:调参的收益是严重不均衡的。学习率选错一个数量级,模型可能完全不收敛;而把dropout从0.3调到0.25,大概率只是噪声范围内的波动。所以第一件事永远是排序,而不是开跑。

1.2 优先级排序:哪些参数先动,哪些可以先放着

我自己的优先级排序常年稳定,从高到低大致是这样:数据质量与划分 > 学习率与调度 > batch size与优化器 > 正则化强度 > 数据增强 > 网络结构细节。前面几项的影响是以数量级计的,后面的影响往往在百分位级别。

为什么把数据放第一位?因为我在实际项目里遇到的“调参调不动”的案例,超过一半根因在数据:标签噪声、训练集和验证集分布不一致、预处理时归一化参数在训练集上算了却在测试集上重算了一遍。这些问题的症状和超参数没调好几乎一模一样,都是loss不降或者涨不上去,但它们不是调参能救的。

学习率排第二,是因为它决定的是“能不能走到一个好的盆地”,而不是“在盆地里站得多稳”。batch size和优化器排第三,它们和学习率强耦合,通常要一起动。正则化和数据增强排在后面,因为它们的作用是控制泛化差距,只有在模型已经能拟合训练集的前提下才有讨论意义。结构细节排最后,除非你有明确的先验,否则改结构是最贵的试验,一次改动动辄几小时。

提示:如果你只有一天时间做调参,把80%的时间花在学习率扫描和数据检查上,剩下的20%再去动别的。这个分配在绝大多数项目里都是稳的。

1.3 实验设计:怎么让每一次试验都榨出信息量

调参最怕的不是试错,是试完之后不知道结论。我给自己定了几条硬规矩,执行下来效率提升非常明显。

第一条,一次只动一个维度。这条听起来保守,但它是唯一能让你把功劳归因清楚的做法。同时改学习率和数据增强,涨点了你不知道是谁的功劳,掉点了你也不知道该回滚谁。唯一的例外是强耦合的参数组,比如学习率和batch size、优化器和weight decay,这些必须成组动。

第二条,每个结论至少跑两个随机种子。深度学习训练的方差比大多数人想象的大,同一份配置换个种子差0.5到1个点精度是家常便饭。我曾经因为一次单种子的实验涨了0.8个点,兴冲冲地把配置推到全量训练,结果三个种子平均下来反而掉了0.3。从那以后我的规矩是:小改动看趋势跑单种子,准备定稿的配置必须跑三种子看均值。

第三条,先在小规模上筛,再在全量上确认。用10%的数据、20%的epoch做粗筛,速度快一个数量级,能筛掉大部分明显不行的方向。但要注意,小规模上的最优配置不一定在大规模上最优——训练步数变多以后,正则化强度和学习率衰减策略的相对重要性会变化。所以小规模只用来做“淘汰”,不用来做“选定”。

2. 核心超参数的细节解析与实操要点

2.1 学习率:最值钱的那一个数字,以及怎么科学地找它

学习率的本质是参数空间里的步长。步长太大,你在损失曲面上反复横跳,loss会剧烈震荡甚至直接发散;步长太小,你能走到的位置精度够了,但训练轮数翻几倍,时间成本扛不住。更麻烦的是,损失曲面在不同方向上曲率差异巨大,同一个标量学习率对所有参数一视同仁,本身就是个粗糙的近似。

找学习率的实用做法是学习率扫描,思路很简单:让学习率从极小值开始,每个batch按指数增长,同时记录loss。具体实现上我用的是从1e-7开始、每步乘以1.1的指数增长,跑100个step,最终学习率大约到1.4e-3;如果想把上限拉到0.05左右,把乘数改成1.3、跑50步就够,因为1.3的50次方约等于5×10^5,乘以1e-7正好落在0.05附近。这个区间的选择依据是:绝大多数常用模型的可用学习率都落在1e-6到1e-1之间,超出这个范围要么不收敛要么直接爆炸。

拿到曲线之后怎么读,是关键。我自己看的不是loss最低点,而是loss下降最陡的那一段的终点,然后取它的十分之一作为初始学习率。为什么是十分之一而不是直接用那个点?因为扫描过程中模型在极短步数内被推着走,处于一种“试探”状态,直接用那个临界值大概率会在后续训练中发散;留一个数量级的余量,既能保证前期下降速度,又留了容错空间。

不同优化器的大致起点可以参考下表,但这只是起点,一定要扫描:

优化器常见起始学习率备注
SGD + momentum 0.90.1对batch size极其敏感,需要配合warmup
Adam1e-3 到 3e-4最常用的默认值,收敛快但泛化有时略差
AdamW1e-3 到 1e-4权重衰减解耦后更适合大模型
Adagrad / RMSprop1e-2 到 1e-3现在较少作为首选

配一个warmup几乎是必选项,尤其是大batch或者深网络。前1%到5%的步数把学习率从0线性拉上来,这一步的意义在于避免训练初期参数被巨大的梯度推到一个糟糕的区域。我试过在某个深层网络上关掉warmup,前200步loss直接从2.3跳到nan,加上warmup之后稳稳收敛。

2.2 batch size:显存、速度与泛化之间的三方拉扯

batch size是调参里最容易被“硬件绑架”的参数。你的显存决定了上限,但上限不等于最优。从优化角度看,batch size决定了梯度估计的噪声水平:小batch梯度噪声大,某种程度上起到了正则化作用,往往泛化更好,但训练慢且不稳定;大batch梯度准,训练快,但容易收敛到尖锐的极小值,泛化可能变差。

这里有个经验规则:线性缩放。当batch size放大k倍时,学习率也大致放大k倍。比如baseline是batch 256配学习率0.1,那么batch 1024就配0.4。但这条规则只在batch size变化不超过8倍、并且配合warmup的情况下成立;再往上就会出现收益递减,需要改用LARS之类的分层自适应方法,或者干脆放弃线性缩放。我在一个项目里把batch从128直接拉到1024,按规则把学习率从0.05提到0.4,不加warmup的结果是前50步直接发散,加了5%的warmup之后才恢复正常。

如果显存不够又想要大batch,用梯度累积:累积n个step的梯度再更新一次,等效batch size就是原来的n倍。但要注意两个坑。第一个是BatchNorm,梯度累积时每个micro-batch单独算BN统计量,等价batch的统计量是有偏的,这种情况要么换成GroupNorm/LayerNorm,要么用同步BN。第二个是学习率调度器,它是按优化器step计数的,梯度累积后step数变少,调度节奏会乱,需要手动按等效step重新算。

至于batch size到底设多少合适,我的实际操作是:在显存允许范围内选一个能让GPU利用率跑到80%以上的值,然后在这个值附近做一次{一半, 原值, 两倍}的三点比较,配合学习率一起扫,选验证集最好的。不要在这上面花超过两轮实验的时间。

2.3 优化器与正则化:什么时候值得换,什么时候别折腾

优化器的选择上,我的默认是AdamW,理由是它对学习率的敏感度低、几乎不需要调momentum、权重衰减解耦后对大模型更友好。只有在两种情况下我会考虑换回SGD:一是最终精度要求极高且训练预算充足,SGD配合精心调的调度往往能比Adam系高出零点几个点;二是要做知识蒸馏或者需要模型输出分布更平滑的场景。

权重衰减这个参数经常被忽略,但它的作用比dropout更稳定。我的经验区间是:小模型1e-4到1e-2,大模型1e-2到0.1。判断方法很直接——如果训练集loss和验证集loss的差距在持续扩大,先加weight decay,再考虑dropout。原因是weight decay直接约束参数范数,作用更平滑;dropout是随机置零,会引入额外的训练噪声,小数据上容易让收敛变慢。

label smoothing是另一个性价比很高的技巧,把硬标签0/1换成0.05/0.95这种软标签,通常能带来零点几个点的稳定提升,代价几乎为零。但要注意它和某些损失函数(比如带类别权重的交叉熵)一起用时要重新校准,否则会互相干扰。

注意:正则化的所有手段都是“在模型已经能拟合训练集”之后才有效。如果你的训练集准确率还停在60%,先别碰正则化,去查数据管线和模型容量。

2.4 数据增强与结构细节:性价比排序的最后一段

数据增强的强度本身也是个超参数,但很多人把它当成固定配方。我的做法是把增强强度定义成一个0到1的系数,用它去缩放旋转角度、裁剪比例、颜色抖动的幅度,然后在这个系数上做{0.5, 1.0, 1.5}三点比较。小数据集的结论通常是1.5更好,大数据集往往0.5就够,增强过强反而拖慢收敛。

结构细节上我有两条原则。第一,不要在没有诊断结论的情况下改结构。如果你不确定瓶颈在哪,改结构就是纯赌博。第二,改结构时优先动“宽度和深度”这类连续可调的维度,少动“加什么模块”这类离散决策。把层数从18加到34是个可预期的变化,插一个注意力模块进去则可能引入一堆新的超参数,把你的调参预算吃光。

3. 从零到跑通:一套可复现的调参流程

3.1 基线搭建与日志规范:别在流沙上盖楼

基线不是“随便跑一个能跑通的配置”,而是“一个你完全理解、并且能稳定复现的配置”。我搭基线的步骤固定:先用一个公认的简单配置跑通端到端,确认数据加载、前向、反向、验证、保存这条链路没有任何问题;然后固定随机种子,连跑两次,确认两次的loss曲线几乎重合——如果不重合,说明代码里有非确定性操作,比如数据加载的多进程顺序、某些算子、随机种子的作用域没覆盖全,这时候调参毫无意义。

日志规范比很多人想的重要。我要求每次实验必须记录:完整配置(用yaml存一份,不要只记在脑子里)、git提交号或代码快照、每条epoch的训练loss和验证指标、学习率的实际曲线、以及一句话的实验意图。最后这条最容易被省略,但它是最有价值的——三个月后你回头看,能一眼知道“哦这次是试把warmup从1%提到5%”。

# experiment_config.yaml 示例 seed: 42 data: train_ratio: 0.9 input_size: 224 augment_strength: 1.0 model: depth: 34 width: 64 train: optimizer: adamw lr: 3.0e-4 weight_decay: 0.05 batch_size: 128 grad_accum: 2 # 等效 batch 256 warmup_ratio: 0.05 scheduler: cosine epochs: 100 amp: true note: "baseline: adamw + cosine + 5% warmup"

3.2 学习率扫描的实操过程与参数手算

我把学习率扫描这一步单独拎出来讲,因为它值得单独花半天时间。操作上我用一个独立的脚本,把模型重新初始化,只跑100到200个step,每个step记录当前学习率和loss,最后画出一张对数横轴的曲线。

关键细节有三个。第一,扫描时要用真实的batch size和真实的数据管线,用假数据扫出来的结果没有参考价值。第二,扫描前要确认模型初始化正常,如果初始loss就是个离谱的值(比如分类任务类别数是10,初始loss应该是2.3左右,结果你看到20),先查初始化和归一化,别急着扫。第三,指数增长的基数决定分辨率,1.1的分辨率细但范围窄,1.3的范围宽但可能跨过最优点,我的习惯是先1.3粗扫定位区间,再用1.1细扫。

拿到曲线之后,找loss下降最陡的区段。举个具体的例子:某次扫描里,学习率在1e-5到3e-4区间loss从2.28平稳下降到1.61,过了3e-4之后开始震荡,到1e-3时loss飙到3以上。这种情况下最陡区段的终点大约在2e-4,取十分之一就是2e-5,但考虑到余弦退火前有warmup托底,我实际会取5e-5到1e-4这个区间再跑两次完整训练做确认。这个“取十分之一”的规则不是铁律,只是提供了一个安全的起点,如果模型有warmup保护,可以往上探半个数量级。

还有一点常被忽略:扫描得到的最优学习率和最终训练用的学习率不是一回事。扫描是恒定学习率,而实际训练通常配余弦或阶梯衰减,衰减后等效学习率更低,所以实际起点可以比扫描推荐值略高一些。

3.3 分阶段调参时间表:把预算花在刀刃上

我给自己定的调参节奏大致分三段,每段的目标不同。

阶段数据规模目标典型试验数
粗筛10%数据, 20% epoch淘汰明显不行的方向15-25次
精调50%数据, 50% epoch在学习率/batch/正则上找局部最优8-12次
确认全量数据, 全量epoch用2-3个种子确认最终配置3次

粗筛阶段我会一次性把想试的方向都列出来,然后按“影响大且便宜”的顺序排队。学习率三个点、batch两个点、weight decay三个点,加上几种增强强度的组合,差不多二十次,用10%的数据每次都很快,一天之内能出结论。精调阶段就收敛到两三个维度上做网格,其他全部冻结。确认阶段最贵但也最不能省,因为前面的结论都是在小规模上得出的,全量训练时正则化和调度策略的相对重要性会变。

这里有个反直觉的观察:大部分项目的调参收益在精调阶段就基本榨干了,继续往下抠的边际收益非常低。所以我给自己设了一条止损线——如果连续三轮实验验证集指标的提升都小于0.3个点,就停止调参,转去做数据侧的工作,或者接受当前结果。

3.4 用脚本把重复劳动自动化

调参到一定次数之后,你会发现大量动作是机械的:改配置、起任务、记录结果、画对比图。这部分必须自动化,否则时间全耗在搬砖上。

我的做法是一个简单的配置驱动脚本:所有超参数写进yaml,脚本读配置、建目录、起训练、训练结束自动把指标追加到一个csv里。这样跑二十组实验只是写二十个yaml的事。再往上一层,可以用一个循环做参数网格,把笛卡尔积展开成一批任务顺序执行,每个任务结束就写一行汇总结果。

import itertools, yaml, subprocess, os grid = { "lr": [1e-4, 3e-4, 1e-3], "weight_decay": [0.01, 0.05], "batch_size": [128, 256], } base = yaml.safe_load(open("experiment_config.yaml")) for i, combo in enumerate(itertools.product(*grid.values())): cfg = dict(base) cfg["train"] = {**base["train"], **dict(zip(grid.keys(), combo))} exp_dir = f"runs/exp_{i:03d}" os.makedirs(exp_dir, exist_ok=True) yaml.safe_dump(cfg, open(f"{exp_dir}/config.yaml", "w")) subprocess.run(["python", "train.py", "--config", f"{exp_dir}/config.yaml", "--out", exp_dir], check=True)

这段代码本身没什么技术含量,但它带来的纪律性很重要:每一组实验都有独立的目录、独立的配置快照、独立的输出,事后对比不会串。我吃过一次亏,两次实验复用了同一个输出目录,日志互相覆盖,最后完全分不清哪个结果对应哪组配置,只能重跑。

4. 常见问题与排查技巧实录

4.1 loss不降或者降了不涨点:分层排查法

这是最高频的问题,我的排查顺序是从下往上,先排除最简单的可能。

第一层,看数据。取一个batch出来,人工过一遍:标签对不对、图像和标签有没有错位、归一化的均值方差是不是按训练集统计的、数据增强有没有把有效信息破坏掉。我遇到过一次数据增强把关键区域裁掉了20%以上的样本,模型怎么调都上不去,关掉增强立刻涨了4个点。

第二层,看模型能不能过拟合一个小样本。拿20个样本反复训200步,正常模型应该能把训练loss压到接近0。如果压不下去,说明模型容量、损失函数或者梯度传导有问题,此时调任何超参数都没用。

第三层,看学习率和梯度。打印每个参数组的梯度范数,如果大部分层的梯度常年接近0,说明学习率太小或者初始化有问题;如果梯度范数剧烈波动甚至出现inf,说明学习率太大或者有数值不稳定。

第四层,看训练集和验证集的差距。两者都低说明欠拟合,去加容量或减正则;训练低验证高说明过拟合,去加正则或数据;两者都高且训练loss震荡,回到学习率。

症状最可能的原因优先处理
loss一直是nan学习率过大 / 数据有inf或异常值降学习率10倍,检查数据范围
loss下降极慢学习率过小 / 归一化不当做学习率扫描,检查输入分布
训练loss正常,验证指标不涨过拟合 / 验证集分布不同加weight decay和增强,核查数据划分
前几十步正常,之后突然发散缺少warmup / 梯度爆炸加warmup和梯度裁剪
指标在某个值附近反复横跳学习率过大 / batch过小噪声大降学习率,或增大batch配合缩放

4.2 过拟合与欠拟合的判别:别凭感觉,看数字

判断标准我一般用两个数字:训练集指标和验证集指标的差值,以及验证集指标的绝对水平。差值大于5个点、且验证指标已经连续几轮不涨,基本可以认定过拟合。这时候的处理顺序是:先加weight decay,再看数据增强强度,最后才动dropout。理由是weight decay不引入随机性,效果最可预测。

欠拟合的判别更简单:训练集指标本身就上不去。这时候先别急着加容量,先确认优化是否充分——把epoch翻倍看看还能不能涨。很多所谓的欠拟合其实是训练轮数不够或者学习率调度衰减太快。如果翻倍后确实到顶了,再考虑加宽加深。

有个坑要提醒:早停的触发条件不要只看验证loss。有些任务验证loss在涨但主指标还在涨,这是正常的,因为loss对置信度敏感而主指标不一定。我一般会同时监控两个,主指标优先。

4.3 几个让人怀疑人生的坑,都是我真踩过的

第一个坑,随机种子只设了torch的,没设numpy和python的。结果数据打乱顺序每次都不一样,两次实验的差异完全不可比。

第二个坑,验证集的预处理跟着训练集一起随机增强。这个错误极其隐蔽,症状是验证指标虚高且波动大,你以为是模型不稳定,其实是验证集每次都在变。

第三个坑,混合精度训练时损失缩放没处理好,小梯度被下溢成0,表现为某些层根本不更新。如果开了amp(混合精度)之后指标反而掉了,先查梯度缩放,或者干脆先关掉amp验证基线。混合精度大概能省40%左右的显存,但它是“免费午餐”的前提是你把数值稳定性处理干净。

第四个坑,对比实验时忘了同步epoch数。学习率从1e-4降到3e-5,收敛需要的epoch也变多了,你按原来的轮数跑,当然看起来更差。这种误判在新手期特别常见,做对比实验时一定要把“训练预算”当成一个受控变量固定住。

第五个坑,在验证集上反复调参调到过拟合。这个坑最深,因为你的所有实验指标都很好,上线就崩。我的做法是留一份绝对不碰的测试集,只在最后确认阶段用一次,而且只用一次。

5. 经验沉淀:把调参结果变成可复用的资产

5.1 实验记录模板与命名规范

一套好的记录能让你三个月后回溯成本降到接近零。我用的模板字段固定:实验编号、日期、一句话意图、改动的参数、完整配置文件路径、三个种子的结果、结论(涨/跌/无变化)、下一步。这八个字段写下来不超过两分钟,但省掉的重复劳动是数量级的。

命名上我坚持“日期_意图_版本”的格式,比如0312_lr-sweep_v2,不用exp1、test_new、final_final这类名字。后者的结局一定是三个月后你对着几十个目录猜哪个是哪个。

还有一条,结论必须写下来,不能只留在脑子里。我见过太多团队里同一个坑被不同的人踩两遍,就是因为结论没落成文字。哪怕结论是“这条路走不通”,也值得记一行。

5.2 什么时候该停止调参

这个问题比“怎么调”更难,因为它反人性。我的判断标准有三条,中两条就停:一是连续三轮实验的提升小于0.3个点;二是验证指标的标准差已经接近甚至超过提升幅度;三是当前瓶颈明显不在超参数上(比如数据量本身就不足)。

停下来之后做什么?我的经验是,同样的时间投到数据侧(清洗、补充、重新标注)的回报,通常远高于继续抠超参数。这一点我在几个项目里反复验证过:把一个有噪声的标签集清洗一遍带来的提升,往往相当于把学习率、batch size、weight decay 全部精调一遍的总和。

最后分享一个我个人一直在用的小技巧:每次调参之前,先写一句话回答“我认为这次改动能带来什么,为什么”。写不出来就别跑。这个动作成本极低,但它能把你的实验从“随便试试”变成“有假设的验证”,长期下来积累的不仅是模型指标,还有你对这个任务的判断力。我在实际使用中发现,真正让调参变快的不是算力,而是这种“每次都带着假设去跑”的习惯。

返回列表