机器学习数学基础专题【左扬精讲】—— 大模型的训练循环:Epoch、Batch、Iteration 的三层骨架
机器学习数学基础专题【左扬精讲】—— 大模型的训练循环:Epoch、Batch、Iteration 的三层骨架
在深度学习里,训练循环(Training Loop)不是一个黑盒按钮,而是一套被严格拆解的流水线:Epoch 控制遍历节奏,Batch Loop 控制梯度更新频率,Iteration 才是真正发生前向传播、计算损失、反向传播与参数更新的最小单元。
本文将按照 What-How-Why 框架,把大模型训练循环从概念定义到代码落地完整拆开,并结合 PyTorch 惯用写法讲清楚每一层的职责与边界。
本文不引用具体文件行号(深度学习训练循环在 PyTorch 中是 API 调用组合,不是单一源码文件),所有代码片段均为通用 PyTorch 2.x 写法(torch.optim.Optimizer、torch.utils.data.DataLoader、torch.nn.Module.train() 等),可在 PyTorch 2.0+ 环境直接运行验证 。
本文涉及的核心 PyTorch 模块(PyTorch 2.x):
torch.nn.Module ← 模型基类:train() / eval() / forward() 约定
torch.utils.data.DataLoader ← Batch 装配与并行加载
torch.optim.Optimizer ← zero_grad() / step() 梯度更新契约
torch.optim.lr_scheduler ← 学习率调度
torch.cuda.amp.GradScaler ← 混合精度训练
torch.nn.utils.clip_grad_norm_ ← 梯度裁剪
训练循环 Epoch Batch Loop Iteration 前向传播 反向传播 优化器 损失函数 学习率调度 早停 断点续训
学习重点提示
★ 必须掌握
- 必须掌握:训练循环的三层结构 — Epoch → Batch Loop → Batch Iteration
- 必须掌握:一次 iteration 的固定顺序 — 数据加载 → 设备转移 → 前向传播 → 损失计算 → 反向传播 → 参数更新
- 必须掌握:optimizer.zero_grad() 必须放在循环最前面;loss.backward() 之后、optimizer.step() 之前不能插入跨设备同步
★ 建议掌握
- 建议掌握:model.train() 与 model.eval() 在 Dropout / BatchNorm 上的行为差异
- 建议掌握:DataLoader 中 num_workers / pin_memory / persistent_workers 的作用
- 建议掌握:梯度裁剪、混合精度、断点续训在长程训练中的必要性
本文目录
- 一、模型训练是什么 — What & Why
- 二、训练循环的三层骨架 — Epoch / Batch / Iteration
- 三、单次 Batch Iteration 的完整时序
- 四、训练器的核心组成:数据 / 模型 / 优化器 / 日志 / Checkpoint / 调度 / 早停
- 五、PyTorch 完整训练脚本全景
- 六、常见误区与调试思路
- 七、常见问题 FAQ(20 组)
- 八、Roadmap 后续预告
一、模型训练是什么 — What & Why
What — 模型训练在做什么?
模型训练的最终目标,是找到一组参数 θ,使得损失函数 L(y, f(x; θ)) 在训练集上尽可能小,同时在未见数据上仍有良好泛化能力。整个过程不是一次求闭解,而是在数据、模型、优化器三者配合下,通过反复迭代逐步逼近最优解。
更具体地说,训练就是让模型在"见过大量样本"之后,把输入 x 到输出 y 的映射关系编码进可学习的权重与偏置里。
Why — 为什么模型不能一次性学会,而必须循环训练?
问题一:数据规模远大于单次可处理范围
现代数据集动辄百万、千万甚至上亿条样本,受限于显存与内存,模型一次只能看到一小批数据。
问题二:优化器只能做局部更新
梯度下降类算法本质上是"看到当前 batch 的梯度后,对参数做一步小更新"。单步更新信息有限,必须通过多次迭代累计方向,才能稳定收敛。
没有循环训练会发生什么?
- 只跑一个 batch:模型只能记住这几十条样本,无法学到全局分布。
- 只跑一个 epoch:部分样本从未参与梯度计算,参数更新有偏。
- 不累积多步更新:梯度噪声会把参数带偏,训练震荡剧烈。
核心结论
训练循环的本质,是把大规模数据集拆成可管理的小批次,用多次迭代逐步逼近最优参数的过程。1 个目标 + 3 层循环 + 6 步迭代,是后续所有讨论的骨架。
二、训练循环的三层骨架 — Epoch / Batch / Iteration
What — 三层循环分别是什么?
训练循环被拆成三层,由外到内依次是:
- Epoch Loop:最外层循环,表示整个训练集被完整"过"了多少遍。
- Batch Loop:中间层循环,表示一个 epoch 内,把数据集切成多个 batch 后,逐个 batch 送进模型。
- Batch Iteration:最内层单元,一次只处理一个 batch,完成前向传播、损失计算、反向传播和参数更新。
这三层并不是简单的"次数统计",它们各自承担不同的职责:epoch 控制学习进度,batch loop 控制吞吐与梯度噪声,iteration 控制梯度计算与参数更新的原子性。
Why — 为什么要拆成三层,而不是只用一个循环?
问题一:单层循环无法区分"看过多少数据"和"更新了多少次"
drop last、shuffle、分布式训练等机制都依赖 batch 的粒度。如果把所有样本一次性堆进显存,小模型显存够用,但大模型立刻 OOM。
问题二:不同任务需要不同的 epoch 策略
小数据集往往需要几十上百个 epoch,而超大规模语料库可能只训练 1~3 个 epoch 就够了。没有 epoch 概念,调度器、日志和 checkpoint 都失去参照。
没有三层结构会发生什么?
- 无法灵活控制数据遍历遍数,学习进度难以评估。
- 无法插入 epoch 级操作,如验证、学习率重置、shuffle。
- batch 间无法做并行流水线,吞吐量上不去。
从时序上看,一个 epoch 启动后,先由 Sampler 决定样本顺序,再由 DataLoader 把样本组织成 batch。每个 batch 进入模型做一次前向与反向,最后 optimizer 做一次参数更新。一个 epoch 结束后,通常会进入验证循环,再决定是继续训练还是早停。
for epoch in range(num_epochs): # 1) Epoch Loop
model.train()
for batch in dataloader: # 2) Batch Loop
loss = train_step(model, batch, ...) # 3) Batch Iteration
validate(model, val_dataloader) # epoch 级验证
这里的外层 epoch 控制学习进度,内层 batch 控制参数更新步数;model.train() 必须在进入 batch loop 前调用,告知模型启用 Dropout 与 BatchNorm 的训练行为 。
速记口诀
- 1 个 epoch = 把训练集完整看一遍
- 1 个 batch loop = 把 epoch 切成多批喂给模型
- 1 次 iteration = 模型真正学习一次(前向 + 反向 + step)
三、单次 Batch Iteration 的完整时序
What — 一个 batch 从进来到更新完,经历了哪几步?
单次 batch iteration 的标准顺序是:
- 数据加载:从磁盘/内存/网络读取样本。
- 设备转移:把 batch 搬到 GPU/TPU。
- 前向传播:模型计算预测值。
- 计算损失:损失函数比较预测值与标签。
- 反向传播:自动微分计算梯度。
- 参数更新:优化器根据梯度修改参数。
这六步是训练循环的原子操作,无论模型多大、数据多复杂,最终都会被折叠进这六步。
Why — 每一步为什么不能省,也不能乱序?
数据加载不能省
没有数据加载,GPU 只能空转等待;如果加载太慢,训练会退化成 I/O 密集型任务。
设备转移不能在前向之后
模型和数据必须在同一设备上。如果数据还在 CPU 而模型在 GPU,前向传播会直接报 RuntimeError: Expected all tensors to be on the same device。
零梯度必须在反向传播之前
梯度是累加的。如果不清零,当前 batch 的梯度会和上一个 batch 的梯度混在一起,相当于 batch size 动态变化,导致优化方向错误。
反向传播不能在参数更新之前
优化器需要当前的梯度值才能更新参数;没有梯度,参数就永远不会变。
没有反向传播会发生什么?
- 模型无法从错误中学习,训练 loss 不会下降。
- 参数永远保持初始化状态,模型等价于随机猜测。
没有参数更新会发生什么?
- 梯度被正确计算,但模型权重不变,等价于"只看不做"。
- 训练曲线平坦,验证集指标毫无提升。
看下面这段 PyTorch 风格的标准训练步,六步顺序一目了然:
def train_step(model, batch, optimizer, loss_fn, device):
# Step 0: 清零梯度(必须在最前)
optimizer.zero_grad()
# Step 1 & 2: 数据加载 & 设备转移
inputs = batch["input"].to(device)
targets = batch["target"].to(device)
# Step 3: 前向传播
logits = model(inputs)
# Step 4: 计算损失
loss = loss_fn(logits, targets)
# Step 5: 反向传播
loss.backward()
# Step 6: 参数更新
optimizer.step()
return loss.item()
关键细节:zero_grad() 必须放在第一步,确保梯度清零;loss.backward() 之后,optimizer.step() 之前,不要插入任何会打断 autograd 图的操作(如 .item() 触发的同步、或显式 .cpu() 调用)。
总结
单次 iteration 是训练的最小原子。六步顺序固定:清零梯度 → 加载并搬运数据 → 前向 → 算损失 → 反向 → 更新参数。调换顺序或省略任何一步,都会直接破坏训练流程。
四、训练器的核心组成
4.1 数据准备:从 Dataset 到 DataLoader
What — 数据准备包含什么?
数据准备是从原始样本到可训练 batch 的全过程,包括读取、清洗、变换、padding、collate 和加载器组装。
在 PyTorch 生态里,torch.utils.data.Dataset 负责"单样本到张量",torch.utils.data.Sampler 负责"决定取哪条样本",torch.utils.data.DataLoader 负责"拼 batch 并并行加载"。
Why — 为什么数据准备如此关键?
问题一:I/O 瓶颈
深度学习训练极度依赖数据吞吐。如果 DataLoader 的 num_workers 过少,GPU 会大量时间等待 CPU 准备下一个 batch。
问题二:顺序敏感
shuffle=True 决定 batch 内的样本分布,直接影响梯度估计质量。固定顺序训练容易导致模型学到虚假相关。
问题三:内存峰值
如果 collate_fn 拼接方式不当,或者 prefetch_factor 过多,CPU 内存可能被打满,导致训练崩溃。
没有合理的数据准备会发生什么?
- GPU 利用率忽高忽低,训练时间被拉长数倍。
- batch 内样本长度不一致,模型无法正常前向传播。
- 重复数据或缺失数据悄悄进入训练,结果不可复现。
一个工业级 DataLoader 通常显式设置 num_workers、pin_memory、persistent_workers 和 prefetch_factor:
dataloader = DataLoader(
dataset,
batch_size=32,
shuffle=True,
num_workers=8,
pin_memory=True,
persistent_workers=True,
prefetch_factor=2,
drop_last=True,
)
pin_memory=True 配合 CUDA 流可以隐藏 Host-to-Device 拷贝延迟;persistent_workers=True 避免每个 epoch 重建 worker 进程的开销 。
4.2 模型初始化
What — 模型初始化在做什么?
模型初始化是把网络权重设置为合理的起始分布,让前向传播的激活值、反向传播的梯度保持在一个可学习的数值范围。
常见初始化策略包括:Xavier / Glorot、He / Kaiming、Orthogonal,以及针对大语言模型常用的 Normal(0, 0.02) 初始化。
Why — 为什么初始化不好,模型可能根本训不起来?
问题一:梯度爆炸或消失
如果初始化方差过大,前向激活值迅速饱和;如果方差过小,反向梯度迅速归零。深层网络尤其敏感。
问题二:对称性崩溃
如果同一层所有神经元被初始化为相同值,它们会永远学到相同特征,网络退化成"宽度为 1"的模型。
没有初始化会发生什么?
- PyTorch 默认使用 kaiming_uniform_ 给 Conv 层、uniform_ 给 Linear 层。如果完全跳过初始化,不同框架行为可能不同,结果不可复现。
- 大模型如果初始化不当,第一步 loss 就是 NaN,训练直接终止。
可以通过 torch.nn.init 显式覆盖默认初始化:
def init_weights(m):
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
if m.bias is not None:
nn.init.zeros_(m.bias)
model.apply(init_weights)
大模型通常还会对 nn.LayerNorm 的 weight 和 bias 做特殊处理,避免预训练阶段激活偏移 。
4.3 优化器与损失函数
What — 优化器和损失函数分别是什么?
torch.optim.Optimizer负责根据梯度修改模型参数;torch.nn 中的损失函数负责把模型输出和真实标签映射成一个可优化的标量。
PyTorch 内置了 SGD、Adam、AdamW、Adafactor 等优化器,以及 CrossEntropyLoss、MSELoss、BCEWithLogitsLoss 等常见损失。
Why — 为什么 AdamW 逐渐成为大模型默认选择?
问题一:SGD 需要精细调学习率
纯 SGD 没有自适应梯度缩放,学习率稍大就发散,稍小就收敛极慢。
问题二:Adam 的权重衰减实现有缺陷
原始 Adam 把 L2 正则直接加到梯度里,等价于对参数做无约束衰减;AdamW 把权重衰减与梯度更新解耦,实现真正的解耦型权重衰减(decoupled weight decay) 。
问题三:大模型训练对数值稳定性要求更高
AdamW 配合梯度裁剪、混合精度,是当前大模型训练的事实标准组合。
没有合适的优化器与损失会发生什么?
- 损失和优化器不匹配:用 MSE 做分类任务,梯度信号弱,模型学不动。
- 优化器超参错误:学习率过大,loss 直接 NaN;学习率过小,训练时间无限拉长。
大模型训练几乎都会把 torch.nn.utils.clip_grad_norm_ 和 AdamW 绑定使用:
optimizer = torch.optim.AdamW(
model.parameters(),
lr=3e-5,
weight_decay=0.01,
betas=(0.9, 0.95),
)
loss_fn = nn.CrossEntropyLoss(label_smoothing=0.1)
for batch in dataloader:
loss = train_step(model, batch, optimizer, loss_fn, device)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
注意:梯度裁剪放在 loss.backward() 之后、optimizer.step() 之前,防止梯度爆炸把参数冲偏。
4.4 日志记录与监控
What — 日志记录在训练中做什么?
日志记录负责把训练过程中的关键指标持久化,包括 loss、learning rate、gradient norm、GPU memory、throughput 等。
主流方案包括:TensorBoard、Weights & Biases、MLflow,以及轻量的 JSON / CSV 日志。
Why — 为什么"不记录"是训练失败最常见的原因之一?
问题一:无法诊断训练状态
如果不记录 loss 曲线,你根本不知道模型是收敛了、震荡了,还是从一开始就 NaN 了。
问题二:无法复现实验
深度学习包含大量随机性:dropout、shuffle、数据增强、初始化。没有日志,同一份代码在不同时间跑出完全不同的结果,也无法解释原因。
没有日志记录会发生什么?
- 训练 loss 异常时,无法判断是数据问题、模型问题还是超参问题。
- 无法对比不同实验,调参变成"玄学"。
- 线上出问题后,无法回溯训练时的超参与数据版本。
不需要复杂系统,也能用 torch.utils.tensorboard.SummaryWriter 记录核心指标:
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter(log_dir="runs/exp-001")
for step, batch in enumerate(dataloader, 1):
loss = train_step(model, batch, optimizer, loss_fn, device)
writer.add_scalar("train/loss", loss, step)
writer.add_scalar("train/lr", optimizer.param_groups[0]["lr"], step)
writer.close()
建议至少记录:train loss、val loss、learning rate、epoch、step。
4.5 模型保存与断点续训
What — 保存与断点续训是什么?
模型保存是指把训练好的参数、优化器状态、学习率调度器状态乃至随机数种子一起写入磁盘。断点续训是指从某个已保存的 checkpoint 恢复训练,而不是从头开始。
一个完整 checkpoint 至少应包含:epoch、model.state_dict()、optimizer.state_dict()、scheduler.state_dict()、rng_state。
Why — 为什么断点续训不是"加载权重"这么简单?
问题一:只保存模型权重会丢失优化器动量
Adam 等优化器维护了动量和二阶矩估计。如果只加载模型参数,优化器会从零开始积累动量,相当于前几万步白训了。
问题二:不保存 epoch/step 会导致学习率调度错位
学习率调度器依赖当前步数做退火。如果 step 重置为 0,学习率会突然跳回初始大值,破坏收敛节奏。
没有断点续训会发生什么?
- 训练中断后必须从头开始,数天的训练付诸东流。
- 分布式训练某节点掉线后,无法从中间状态恢复,资源浪费严重。
- 无法在最佳验证点保存模型,容易过拟合或欠拟合。
下面是最小可用但完整的断点续训实现:
def save_checkpoint(path, model, optimizer, scheduler, epoch, step):
torch.save({
"epoch": epoch,
"step": step,
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"scheduler_state_dict": scheduler.state_dict(),
}, path)
def load_checkpoint(path, model, optimizer, scheduler):
ckpt = torch.load(path, map_location="cpu")
model.load_state_dict(ckpt["model_state_dict"])
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
return ckpt["epoch"], ckpt["step"]
额外建议:把 torch.random.get_rng_state() 和 np.random.get_state() 也一并保存,实现完全可复现的续训 。
4.6 学习率调度
What — 学习率调度器在做什么?
torch.optim.lr_scheduler 根据训练步数或验证指标,动态调整优化器的学习率。常见策略包括:StepLR、CosineAnnealingLR、LinearLR、PolynomialLR,以及大模型常用的 CosineAnnealingWarmRestarts。
Why — 为什么固定学习率通常不是最优选择?
问题一:训练初期需要大步长快速下降
初期参数远离最优解,大学习率可以快速跨越平坦区域;但后期需要小步长精细微调。
问题二:大模型对学习率极其敏感
Transformer 类大模型通常配合 warmup + cosine decay。没有 warmup,前几步梯度噪声可能导致模型崩溃。
没有学习率调度会发生什么?
- 固定大学习率:后期在最优解附近震荡,无法收敛到最低点。
- 固定小学习率:前期收敛极慢,训练时间翻倍。
- 没有 warmup:大模型前几步 loss 直接爆炸。
这是大模型训练最常用的组合之一:
from torch.optim.lr_scheduler import LinearLR, CosineAnnealingLR, SequentialLR
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-5)
scheduler = SequentialLR(
optimizer,
schedulers=[
LinearLR(optimizer, start_factor=0.1, end_factor=1.0, total_iters=1000),
CosineAnnealingLR(optimizer, T_max=total_steps - 1000),
],
milestones=[1000],
)
注意:scheduler.step() 通常放在每个 batch 之后,而不是 epoch 之后,这样才能和 step 数严格对齐 。
4.7 异常处理与早停
What — 异常处理与早停是什么?
异常处理是指在训练循环中捕获 OOM、NaN、checkpoint 损坏 等异常,避免整个训练任务直接崩溃。早停(Early Stopping)是指当验证集指标连续若干个 epoch 没有提升时,提前终止训练,防止过拟合。
Why — 为什么异常处理和早停都是生产训练必备?
问题一:大模型训练任务极长
大模型训练通常需要数天甚至数周,一次 OOM 或 NaN 如果没有自动处理,意味着数天算力白费。
问题二:验证集是唯一实时反馈
训练 loss 持续下降不代表模型还在学有用特征;如果验证 loss 已经开始上升,说明模型在记住训练数据,继续训练只会更差。
没有异常处理与早停会发生什么?
- 一次异常中断导致整个分布式任务失败,资源释放不及时,影响后续调度。
- 没有早停,模型过拟合后性能持续下降,最佳检查点被覆盖。
- 无法自动恢复,必须人工介入,训练效率低下。
早停需要维护"最佳验证指标"和"耐心计数器":
best_val_loss = float("inf")
patience = 10
counter = 0
for epoch in range(num_epochs):
train_loss = train_epoch(model, dataloader, optimizer, loss_fn, device)
val_loss = evaluate(model, val_dataloader, loss_fn, device)
if val_loss < best_val_loss:
best_val_loss = val_loss
counter = 0
save_checkpoint("best.pt", model, optimizer, scheduler, epoch)
else:
counter += 1
if counter >= patience:
print("Early stopping triggered")
break
异常处理通常放在最外层 try/except,对 torch.cuda.OutOfMemoryError 和 RuntimeError 做分级响应:梯度裁剪可以恢复的就跳过 batch,否则降 batch size 重试。
五、PyTorch 完整训练脚本全景
What — 一个完整的大模型训练脚本长什么样?
一个完整脚本通常包含:参数解析、模型构建、数据管道、训练循环、验证循环、日志记录、checkpoint 保存、早停判断和异常捕获。
Why — 为什么需要把所有这些逻辑放进一个脚本里?
真实训练不是 Jupyter Notebook 里的几行演示代码,而是要跑数天、在分布式环境、面对显存波动、日志漂移和异常中断。必须把每个环节都显式管理,才能保证结果可复现、故障可恢复。
没有统一脚本会发生什么?
- 多个人维护不同版本脚本,实验结果无法对比。
- 关键参数散落在各处,review 成本极高。
- 异常发生时,没有标准恢复流程,只能人工重头训练。
下面把前面所有组件整合进一个可直接运行的结构化脚本:
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
def train_epoch(model, dataloader, optimizer, loss_fn, scheduler, device, step_offset):
model.train()
total_loss = 0.0
for step, batch in enumerate(dataloader, 1):
global_step = step + step_offset
optimizer.zero_grad()
inputs = {k: v.to(device) for k, v in batch.items() if k != "labels"}
labels = batch["labels"].to(device)
outputs = model(**inputs)
loss = loss_fn(outputs.logits, labels)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
scheduler.step()
total_loss += loss.item()
if global_step % 100 == 0:
writer.add_scalar("train/loss", loss.item(), global_step)
writer.add_scalar("train/lr", scheduler.get_last_lr()[0], global_step)
return total_loss / len(dataloader)
def evaluate(model, dataloader, loss_fn, device):
model.eval()
total_loss = 0.0
with torch.no_grad():
for batch in dataloader:
inputs = {k: v.to(device) for k, v in batch.items() if k != "labels"}
labels = batch["labels"].to(device)
outputs = model(**inputs)
loss = loss_fn(outputs.logits, labels)
total_loss += loss.item()
return total_loss / len(dataloader)
best_val_loss = float("inf")
patience = 10
counter = 0
global_step = 0
for epoch in range(num_epochs):
train_loss = train_epoch(model, train_loader, optimizer, loss_fn, scheduler, device, global_step)
val_loss = evaluate(model, val_loader, loss_fn, device)
global_step += len(train_loader)
if val_loss < best_val_loss:
best_val_loss = val_loss
counter = 0
save_checkpoint("best.pt", model, optimizer, scheduler, epoch, global_step)
else:
counter += 1
if counter >= patience:
print("Early stopping at epoch", epoch)
break
这个脚本虽然简化,但已经包含:gradient clipping、scheduler、TensorBoard、checkpoint、early stopping 五大核心机制 。
六、常见误区与调试思路
What — 训练新手最容易踩的坑有哪些?
最常见的误区包括:忘记调用 zero_grad()、在 no_grad() 上下文里调用 backward()、把验证集的样本也送进训练、学习率调度器 step() 调用位置错误、以及 batch size 设置后忘记同步调整学习率。
Why — 为什么这些细节如此致命?
深度学习训练是一个高度耦合的流水线。任何一环出错,结果都不会是"稍微差点",而是直接不收敛、NaN 或过拟合。
常见症状与根因:
- loss 为 NaN:学习率过大、梯度未裁剪、输入未归一化、标签错误。
- loss 不降:学习率过小、zero_grad 漏写、模型初始化不当、标签与任务不匹配。
- 训练快但验证差:过拟合、数据泄露、验证集采样错误。
- GPU 利用率低:DataLoader num_workers 过少、CPU 预处理太慢、batch 太碎。
调试思路:先固定随机种子,用一个极小数据集验证训练步能正常 loss 下降;再把 batch size、学习率、模型结构逐一放大;最后才上完整数据和大模型。
在任何大规模训练前,先用 4 条样本、2 个 epoch 验证整个 pipeline:
# 极小数据集 smoke test
tiny_dataset = torch.utils.data.TensorDataset(
torch.randn(4, 8),
torch.randint(0, 2, (4,)),
)
tiny_loader = DataLoader(tiny_dataset, batch_size=2)
tiny_model = nn.Linear(8, 2)
opt = torch.optim.SGD(tiny_model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
for epoch in range(2):
for x, y in tiny_loader:
opt.zero_grad()
logits = tiny_model(x)
loss = loss_fn(logits, y)
loss.backward()
opt.step()
print(f"epoch {epoch} loss {loss.item():.4f}")
如果这段代码都不能正常 loss 下降,那么大规模训练一定存在问题。
七、常见问题 FAQ(20 组)
FAQ 常见问题解答
Q1. 什么是训练循环?
训练循环是模型从数据中学习参数的重复执行流程。它把数据集拆成 epoch、batch 和 iteration,在每个最小单元里依次完成前向传播、损失计算、反向传播和参数更新,如此反复直到收敛。
Q2. 一个 epoch 和一个 iteration 有什么区别?
epoch 是遍历完整数据集的次数,iteration 是处理一个 batch 的次数。一个 epoch 包含多个 iteration,iteration 数量等于数据集样本数除以 batch size。
Q3. 为什么 batch size 不能无限大?
受限于显存大小,而且过大的 batch size 会降低梯度噪声,反而影响泛化。小 batch 提供的噪声有助于跳出 sharp minimum,大 batch 容易收敛到更差的局部最优 。
Q4. 为什么 optimizer.zero_grad() 必须在循环最开始?
因为梯度是累加的,必须在计算新梯度前清空历史。如果放到 backward() 之后,上一轮梯度会混入当前轮次,导致参数更新方向错误。
Q5. loss.backward() 和 optimizer.step() 谁先谁后?
一定是先 backward,再 step。backward 负责把 loss 变成梯度,step 负责用梯度更新参数;顺序颠倒会导致参数无法被正确修改。
Q6. 前向传播时如果不传标签,模型能训练吗?
不能。没有标签就无法计算损失,没有损失就无法反向传播,模型也就无法学习。自监督训练中的"伪标签"仍然是标签的一种形式。
Q7. 设备转移放在前向传播之后会怎样?
会报设备不匹配错误。模型在 GPU 上,输入还在 CPU 上,前向传播会报 RuntimeError。数据必须在进入模型前完成转移。
Q8. 学习率调度器应该放在 optimizer.step() 之前还是之后?
通常放在 step() 之后。因为当前步的更新应该基于本步的学习率;放在之前会导致第一步学习率就已经被调度器改变。
Q9. 为什么大模型训练常用 AdamW 而不是 Adam?
AdamW 实现了真正的解耦型权重衰减。原始 Adam 把 L2 正则混进梯度更新,导致权重衰减与梯度自适应耦合;AdamW 将其解耦,训练稳定性更好 。
Q10. 梯度裁剪有什么用?
防止梯度爆炸把参数更新到异常大的值。特别是大模型或 RNN 类结构,梯度范数容易失控;clip_grad_norm_ 可以把梯度限制在安全范围内。
Q11. 为什么 DataLoader 的 num_workers 不是越大越好?
过多 worker 会导致进程切换和内存复制开销上升。最佳值通常与 CPU 核心数、数据集大小、磁盘速度有关,需要通过 profiling 找到平衡点。
Q12. 早停的 patience 一般设多少?
视数据集大小而定,常见 5~20。小数据集 patience 可设小些,大模型训练 patience 通常设大些,避免因为正常震荡而过早停止 【推测】。
Q13. 断点续训为什么必须保存 optimizer 状态?
因为 Adam 等优化器维护了动量和二阶矩。如果只恢复模型权重,优化器会从零开始积累动量,前几步的更新方向会错误。
Q14. 为什么训练 loss 下降但验证 loss 上升?
这是典型的过拟合信号。模型开始记住训练数据的噪声和特例,而不是学习通用规律。此时应该早停、加正则、或扩充数据。
Q15. 混合精度训练会改变训练循环结构吗?
不会改变逻辑顺序,只是在前向和反向之间插入自动转换。使用 torch.cuda.amp.autocast 和 GradScaler 可以透明地减少显存占用并加速计算。
Q16. 为什么 batch size 改变后要调整学习率?
因为梯度噪声与 batch size 相关。batch size 增大时,梯度估计更准,噪声更小,通常需要按平方根比例或线性比例增大学习率,以保持噪声水平一致 【推测】。
Q17. DataLoader 的 drop_last=True 有什么作用?
丢弃不足一个 batch 的剩余样本。在 BatchNorm 等依赖 batch 统计量的层里,过小的 batch 会导致统计不稳定;分布式训练里,drop_last 还能保证每个设备上的 batch size 完全一致。
Q18. 训练时如何判断 GPU 是不是真的在全力工作?
看 nvidia-smi 中的 GPU-Util。如果长期低于 80%,说明瓶颈在数据加载或 CPU 预处理;如果接近 100%,说明训练已进入计算受限状态。
Q19. 为什么 model.train() 和 model.eval() 不能混用?
因为 Dropout 和 BatchNorm 在两种模式下行为不同。训练模式下 Dropout 会随机屏蔽神经元,BatchNorm 会更新 running mean/var;验证时必须关闭这些随机性,确保输出稳定。
Q20. 日志里应该至少记录哪些指标?
至少记录 train loss、val loss、learning rate、epoch、step。如果资源允许,再加上 gradient norm、GPU memory、throughput、best checkpoint 路径,能极大提升调参效率。
全篇总纲
- 1 个目标:找到使损失最小的参数 θ
- 3 层循环:Epoch → Batch Loop → Batch Iteration
- 6 步迭代:清零 → 加载 → 前向 → 算损失 → 反向 → 更新
- 7 大组成:数据 / 模型 / 优化器 / 日志 / Checkpoint / 调度 / 早停
- 5 个避坑:不调 zero_grad、不混 train/eval、不省 warmup、不忘梯度裁剪、不丢失 optimizer 状态
八、Roadmap 后续预告
后续预告
- 第 7 篇:大模型的优化器详解 — 从 SGD 到 AdamW,从动量到自适应学习率
- 第 8 篇:学习率调度器全解 — Warmup、Cosine、Linear、Polynomial、ReduceLROnPlateau
- 第 9 篇:混合精度与分布式训练 — 从单机单卡到 DDP、FSDP、DeepSpeed
- 第 10 篇:大模型推理与部署 — 从 PyTorch 到 ONNX / TensorRT / vLLM

浙公网安备 33010602011771号