在 PyTorch 训练循环中,zero_grad() 必须在 loss.backward() 之前调用(且绝不能位于 backward() 和 optimizer.step() 之间),其具体位置(循环开头或 backward() 前瞬时)不影响梯度计算逻辑,但影响代码可读性与多优化器场景下的健壮性。
在 pytorch 训练循环中,`zero_grad()` 必须在 `loss.backward()` 之前调用(且绝不能位于 `backward()` 和 `optimizer.step()` 之间),其具体位置(循环开头或 `backward()` 前瞬时)不影响梯度计算逻辑,但影响代码可读性与多优化器场景下的健壮性。
zero_grad() 的核心作用是清空 optimizer 所管理参数的 .grad 属性(即上一轮反向传播累积的梯度)。PyTorch 默认采用梯度累加(gradient accumulation) 机制——每次 backward() 都会将新梯度累加到现有 .grad 上,而非覆盖。因此,若不显式调用 zero_grad(),模型参数的梯度将包含历史批次的残留值,导致错误的参数更新。
✅ 正确前提:zero_grad() 必须在 backward() 之前、且不能在 backward() 和 step() 之间。
❌ 错误示例:
loss.backward() optimizer.zero_grad() # ❌ 危险!梯度已累加,但未清零就 step → 错误更新 optimizer.step()
? 实际位置选择取决于工程实践,而非数学差异:
-
单优化器标准训练循环:推荐置于 epoch/iteration 开头,结构清晰、不易遗漏:
for epoch in range(num_epochs): for batch in dataloader: optimizer.zero_grad() # ✅ 清零放在最前 outputs = model(batch.x) loss = criterion(outputs, batch.y) loss.backward() # ✅ backward 依赖 clean grad optimizer.step() # ✅ step 使用当前梯度 -
多优化器场景(如 GAN、Encoder-Decoder):建议按模块就近调用,避免混淆:
# Encoder 更新 enc_opt.zero_grad() enc_loss = compute_encoder_loss(...) enc_loss.backward() enc_opt.step() # Decoder 更新 dec_opt.zero_grad() # ✅ 独立清零,防止 encoder 梯度污染 decoder 参数 dec_loss = compute_decoder_loss(...) dec_loss.backward() dec_opt.step()
⚠️ 注意事项:
- 实验结果波动 ≠ zero_grad() 位置导致:你观察到的 accuracy 变化更可能源于随机性(数据加载顺序、Dropout、权重初始化等)。务必通过固定随机种子(torch.manual_seed(), numpy.random.seed(), random.seed())确保可复现性。
- 不要依赖“隐式清零”:即使某次训练未调用 zero_grad() 也看似收敛,实则是梯度意外累加形成的伪优化路径,泛化性极差。
- model.zero_grad() vs optimizer.zero_grad():前者仅清零 model.parameters() 的梯度;后者清零 optimizer.param_groups 中所有参数的梯度。当 optimizer 管理子集参数(如只微调部分层)时,应优先使用 optimizer.zero_grad()。
总结:zero_grad() 是训练稳定性的“守门员”,位置灵活性服务于代码可维护性,而非算法逻辑。坚持“backward() 前必清零、多优化器就近清、单优化器循环首行清”,即可兼顾正确性与工程健壮性。


















