
搞过卷积神经网络CNN训练的人肯定都跟 epoch、迭代次数iteration和 batchsize 这三个词打过交道。网上教程一抓一大把要么丢个公式要么直接甩一段代码。真正让人挠头的是这三个数到底谁决定了谁我该先调哪个为什么训练日志里写的是epoch1-step8687.ckpt而不是我想象的epoch1这些坑我基本都踩过一遍今天就把它们掰碎了讲清楚。先扫个盲。不管你的网络里有卷积层、汇聚层还是全连接层训练过程本质上都在干同一件事拿一批样本计算损失把梯度反向传回去更新每个卷积核里的权重。而 epoch、迭代次数、batchsize 这三个概念恰好就是描述“拿多少样本、走多少步、循环几轮”的三个数字。你只要吃透这一套坐标系就看懂了所有训练日志也能自己算清楚训练到底要跑多久、什么时候该停。1. 先从一次“参数更新”说起CNN 训练的最小单位1.1 训练的本质在损失曲面上一步一步往下走CNN 训练不是在“背答案”。给定一张猫的图片网络先通过卷积层提取边缘、纹理再经过汇聚层压缩信息最后由全连接层输出一个类别预测。这个预测和真实标签之间会有差距我们把这个差距定义成损失函数。训练的目标就是不断调整每一层里的权重参数让这个损失函数的值越来越小。假设损失函数是一个坑坑洼洼的山地地形你的网络权重就是站在山上的人。每一次参数更新就是根据当前点的梯度方向朝“下山”的方向迈一小步。迈步这个动作才是整个深度学习里最核心、最消耗 GPU 的时刻。后面要讲的 batchsize、迭代次数全都在描述“这一步怎么迈”“迈了多少次”。1.2 batchsize一次更新到底吃多少样本既然每次更新都要计算梯度那问题来了用多少样本来估计梯度才算靠谱一次只用 1 个样本梯度噪声极大走两步退一步路径歪歪扭扭但计算量小、更新频率高。一次用全部训练集梯度估计最准可代价是必须把所有样本都算完才能更新一次显存装不下计算也慢得离谱。于是就有了 mini-batch 的折中方案每次从训练集里随机抽一小撮样本用这一小撮的平均梯度来更新参数。这一小撮样本的数量就是 batchsize。举例说batchsize32意味着你每次挑 32 张图同时送进网络算出平均损失和平均梯度更新一次参数。这里有个值得记下的点batchsize 通常取 16、32、64、128 这类 2 的幂主要是因为显卡对这类对齐尺寸更友好、矩阵运算效率更高数学上并没有强制规定必须是 2 的幂。真取 24、48 也能跑只是性能可能略差。1.3 迭代次数参数一共更新了多少步深度学习语境下的迭代次数iteration指的就是“完成一次前向计算、一次反向传播、一次参数更新”的完整过程。一次迭代对应一个 batch。所以迭代次数本质上是“参数更新了多少次”而不是“看了多少轮数据”。代码里经常能看到global_step 1这个 global step 就是全局迭代次数也就是你说的“步数”。很多初学者会把 iteration 理解成一个 epoch 内的循环次数这在局部是对的一个 epoch 内会跑完若干个 batch每个 batch 就是一次 iteration。但全局来看iteration 是一个不断累计的数字。论文里写的“训练 100k 个 iterations”意思就是参数被更新了 10 万次跟你把数据集看完几轮没有直接关系。1.4 epoch完整把所有数据过一遍一个 epoch 指什么把整个训练集完整地看一遍。假设训练集有 30000 张图batchsize128那么一个 epoch 里会有 30000 除以 128 约等于 235 个 batch也就有 235 次迭代。跑 10 个 epoch就是把这 30000 张图切 10 遍每遍切的时候通常还要重新打乱顺序。为什么要引入 epoch 这个单位因为单看迭代次数你不知道模型到底把数据看了几遍。在 MNIST6 万张图上迭代 2000 次相当于 2 个多 epoch在 ImageNet128 万张图上迭代 2000 次连 0.2 个 epoch 都不到。两个模型嘴上说“都训练了 2000 步”实际见过的数据量完全不同。把每个 epoch 理解成“一轮复习”迭代次数理解成“做了多少道题”这个类比特别好用。2. 三者之间的换算关系以及代码里的真实组织方式2.1 核心公式一个例子算得明明白白三个值的关系可以用两个公式锁死每个 epoch 的迭代次数 ⌈训练样本数 N / batchsize B⌉总迭代次数 epoch 数 × 每个 epoch 的迭代次数注意那个向上取整符号。最后一批样本不够一个 batch 时照样会前向反向一次所以不能简单四舍五入。举一个可复现的例子假设训练集有 30000 个样本batchsize128那么每个 epoch 的迭代次数 ⌈30000 / 128⌉ 235 次。你定 50 个 epoch总迭代次数就是 235 × 50 11750 步。如果训练日志显示epoch1-step8687.ckpt恰好每个 epoch 就是 8687 步说明模型刚好完整看完第一轮数据如果每个 epoch 有 10000 步那 8687 步意味着第一轮还没看完。在实际训练里真正影响训练时长的往往是总迭代次数。因为 GPU 每一次前向反向的耗时基本固定总步数越多久越久。想估算训练时间就数总步数别数 epoch。总步数 样本数 ÷ batchsize × epoch 数这个公式在你规划预算的时候极其管用。2.2 训练代码里到底怎么嵌套PyTorch 实例有人说概念都懂一看代码还是晕。这里给一段最常见的 CNN 训练循环伪代码你跟着捋一遍就通了from torch.utils.data import DataLoader # dataloader 的 batch_size 就是 batchsize # shuffleTrue 表示每个 epoch 都重新打乱数据顺序 loader DataLoader(train_dataset, batch_size128, shuffleTrue) num_epochs 50 global_step 0 for epoch in range(num_epochs): for batch_idx, (inputs, labels) in enumerate(loader): # 前向计算 outputs model(inputs) loss loss_fn(outputs, labels) # 反向传播 更新参数 optimizer.zero_grad() loss.backward() optimizer.step() global_step 1 # 打印日志epoch、batch_idx、global_step、loss if global_step % 500 0: print(fEpoch {epoch1}, Batch {batch_idx1}, Step {global_step}, Loss {loss.item():.4f})外层for epoch负责轮数内层for batch负责遍历当前 epoch 的所有 batch。enumerate(loader)返回的batch_idx是当前 epoch 内的迭代次数从 0 数到len(loader)-1而global_step是整个训练过程从第 1 步开始累计的。len(loader)就是这个 epoch 的迭代次数等于样本数除以 batchsize 后向上取整。2.3 为什么模型文件叫epoch1-step8687.ckpt认清 global step很多训练框架保存 checkpoint 时喜欢用“epoch step”共同命名比如你经常会看到的epoch1-step8687.ckpt。这里的 step 就是 global step也就是从训练开始到保存那一刻的总迭代次数。这样命名的最大好处是恢复训练时优化器状态、学习率调度器都能精确回到同一个位置。学习率调度特别依赖步数。如果你用余弦退火或 warmup学习率是按照 global step 算出来的不是按 epoch 算的。训练断掉之后如果不恢复 step而是从头重新数学习率就会错位后续 loss 可能突然跳高甚至直接崩掉。所以看到这种 checkpoint 命名别只当它是文件名里面藏着恢复训练的关键信息。3. batchsize 到底怎么选显存、梯度质量、学习率一个都不能少3.1 小 batch 和大 batch梯度噪声的博弈选 batchsize 最核心的权衡是梯度估计的准确度和计算效率之间的博弈。小 batch 的梯度样本量少方差大每次更新的方向不太稳但这种噪声有时候反而是好事能帮模型跳出尖锐的局部极小值找到更平坦的泛化区域。大 batch 的梯度更平滑、方向更稳定收敛曲线看起来更漂亮但一味加大 batchsize在相同 epoch 数下泛化能力可能变差这是很多研究里观察到的“大 batch 泛化差距”。工程上还有两个硬约束显存和数据并行。batchsize 增大前向反向时中间特征图的显存占用几乎线性上涨显存不够就 OOM。多卡训练时真正生效的总 batchsize 单卡 batchsize × GPU 数量。我最初用 4 张卡单卡设 64以为自己在跑 64 的 batch其实总 batch 是 256学习率却没跟着改后果就是训练又慢又不稳定。3.2 学习率必须跟着 batchsize 联动很多人只改 batchsize不改学习率训练结果反而变差原因就在这里。一个经验法则是线性缩放规则当总 batchsize 翻倍时学习率大概也翻倍。比如原来 batchsize128、学习率 0.1改成 batchsize256学习率可以试着提到 0.2。直觉是batch 大了梯度噪声更小你有底气迈更大的步子。但是训练初期参数还没稳定直接放大步很容易跑飞所以大规模训练通常会配个 warmup 阶段前几个 epoch 让学习率从 0 或者一个很小的值线性升到目标值。当 batch 增大到特别夸张的程度比如 8192 甚至更大时线性缩放不再完全适用warmup 也要相应拉长。我自己的基准做法是先定 batchsize再根据训练曲线调整学习率最后才动 epoch。3.3 不同场景 batchsize 的经验参考与其背教条不如记几个常用区间然后在自己的验证集上微调任务类型常见 batchsize备注图像分类CIFAR、ImageNet 等32 ~ 256网络越深越大通常取小一点目标检测大图训练2 ~ 16单卡图片尺寸大显存压力大NLP Transformer 预训练32 ~ 512依赖梯度累积和大 batch极小数据集几百张图16 ~ 全量可考虑全 batch 直接上注意这些是参考不是硬规则。小 batch 需要调低学习率大 batch 需要调高学习率并且 epoch 数可能也要跟着变。判断标准始终是验证集的表现而不是训练日志里的 loss 有多低。4. epoch 定多少合适训练这趟车什么时候该下4.1 别被训练 loss 骗了一切以验证集为准epoch 数没有标准答案因为不同的数据集、不同的任务、不同的 batchsize收敛曲线差得远。最可靠的做法是盯住验证集上的指标。训练 loss 一直在下降但验证 loss 开始回升这是过拟合的典型信号。反过来训练 loss 和验证 loss 都在高位下不去则是欠拟合需要加大模型、调高学习率或加训练时长。我习惯每几个 epoch 保存一次 checkpoint同时把训练 loss 和验证 loss 画在一张图里。看到验证 loss 连续 10 个 epoch 没有创新低就用早停策略直接加载之前验证指标最好的权重。很多跑完 100 个 epoch 之后回头看最佳模型出现在第 60 个 epoch后面的 40 个 epoch 纯属浪费电。如果项目在一个小数据集上可以很便宜地多试几次你会更相信“动态决定 epoch”比“一开始定死 200”靠谱得多。4.2 常见的 epoch 设置和学习率衰减策略除了看曲线业界也沉淀了一些标准做法。小数据集比如 MNIST10 ~ 30 个 epoch 就足够CIFAR-10 常见 50 ~ 200ImageNet 级别的预训练通常 100 ~ 300。但现代训练更依赖学习率调度器而不是单纯堆 epoch。比较主流的是余弦退火学习率从初始值按照余弦曲线缓慢降到接近 0配合 warmup 使用训练曲线很平稳。也有更传统的阶梯式下降比如每 30 个 epoch 把学习率乘以 0.1。另一个容易被忽视的是 EMA指数移动平均。很多项目会在训练接近尾声时对参数做指数滑动平均用平滑后的参数做推理往往比最后一刻的参数泛化更好。如果训练日志里出现了 EMA 相关的指标别忘了这是和 epoch 相关的策略你至少得保证在最后一段 epoch 里EMA 参数有机会充分更新。4.3 超大数据集怎么绕开 epoch 的限制当你遇到上亿甚至几十亿样本的场景一个 epoch 就等于几百万步训练好几天都看不到一个 epoch 结束。这时候再拿“epoch”当循环单位就太笨了。很多大规模训练脚本直接设最大步数比如max_steps1000000训练数据用一个无限循环的 DataLoader模型只看 global step不关心数据看过几遍。这种模式下你仍然可以用公式换算max_steps 期望的 epoch 数 × 样本总数 ÷ batchsize。比如你有 1000 万样本batchsize1024想让模型“看 20 遍数据”那max_steps 20 × 10,000,000 / 1024 ≈ 195312。之后调整 batchsize也要同步调 max_steps确保见过的样本总量不变。这也是为什么在大型训练里step 往往比 epoch 更像“时间单位”。5. 我在真实训练里踩过的坑和问题排查记录5.1 验证集 loss 震荡不下降先检查 shuffle 和权重衰减我一开始拿 CNN 做图像分类训练 loss 掉得漂亮验证 loss 却上蹿下跳经常连续几个 epoch 不稳定。后来排查发现罪魁祸首有两个。一个是 DataLoader 忘记开shuffleTrue。当你的训练集按类别顺序排列每个 batch 里全是同一类图片梯度被类别特征带偏模型学不到普适规律。另一个是优化器里weight_decay设得太大把权重压得太狠特征提取层被“捆住”了。调参顺序建议是先保证 shuffle 开着再调 weight_decay最后才去动 batchsize 和学习率。5.2 显存不够但想要大 batch梯度累积怎么玩我刚跑一个稍大一点的模型时目标 batchsize 是 64但单卡 batchsize 一设成 64 就显存溢出。后来验证了梯度累积这个办法显存只允许 16那就分 4 次小批量走每次照常前向反向但不立即更新参数累积 4 次梯度后再统一更新。作用等价于用一个 64 的 batch。实现上有一个细节非常容易出错要把每次小批量的 loss 除以累积次数或者手动对梯度做平均否则实际梯度相当于被放大了 4 倍学习率也要对应缩小。accum_steps 4 # 目标 batchsize 32 / 单卡 batchsize 8 4 optimizer.zero_grad() for micro_step in range(accum_steps): loss model(inputs_chunk) / accum_steps loss.backward() optimizer.step()还有一个坑BatchNorm 层在小批量上计算的统计量和大批量不完全一致。如果你的网络里用了 BatchNorm梯度累积并不会 100% 等于真正的大 batch必要时需要换成同步 BN或者干脆减少累积倍数。5.3 训练中断后怎么精准恢复从 checkpoint 里的 step 续跑有一回分布式训练跑到一半进程挂了我的启动脚本忘了加载 scheduler 状态只恢复模型权重和 optimizer学习率从新的初始值开始。结果恢复后 loss 不仅没掉下去反而明显反弹。这就是 checkpoint 里 step 的价值。正确的恢复流程是从 checkpoint 里同时读出 epoch、global_step、optimizer 和 scheduler 的状态然后从那个 step 继续。PyTorch 里可以这样组织保存和加载checkpoint { epoch: epoch, global_step: global_step, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), scheduler_state_dict: scheduler.state_dict() } torch.save(checkpoint, fepoch{epoch}-step{global_step}.ckpt)如果你的训练脚本没有记录 step中断恢复就非常痛苦尤其用 cosine schedule 时一个数字的偏差都会导致学习率曲线错位。所以我建议任何脚本都养成保存并读取 global_step 的习惯。5.4 新手最容易搞混的问题速查表常见疑问正解epoch 是不是越多越好不是验证集指标会告诉你什么时候该停batchsize 必须设成 2 的幂吗不是只是硬件效率考虑迭代次数和 batchsize 成反比吗每个 epoch 内的迭代次数与 batchsize 成反比总迭代次数与 batchsize 成反比epoch1-step8687.ckpt里的 step 是什么是 global step即参数已更新的总次数改了 batchsize其他不用改吗一般要同步调学习率必要时调 max_steps训练日志里为什么不只看 epoch因为 global step 精确描述学习率位置和恢复点大数据集还用 epoch 循环吗通常改成 max_steps 循环更可控最后讲一点个人用法。我后来看训练进度第一眼永远看global_step / total_steps这个比例第二眼才看验证集曲线。batchsize、epoch、迭代次数说到底是一组坐标你只要把坐标轴定义清楚任何训练框架的日志都能一眼读懂。调试深度学习模型时最贵的不是 GPU而是被错误参数浪费掉的试错时间希望这篇能帮大家把那部分时间省下来。