对标量张量(如损失值)调用 .backward() 会触发计算图中所有 requires_grad=True 的张量的梯度。需要了解的一个主要特性是,PyTorch 默认会累积梯度。默认梯度累积行为如果您在多次调用 .backward() 之间不清除梯度,PyTorch 会将新计算的梯度添加到叶张量(参数)的.grad属性中已有的值上。我们用一个简单例子来说明这一点:import torch # 创建一个需要梯度的张量 x = torch.tensor([2.0], requires_grad=True) # 执行一些操作 y = x * x z = y * 3 # z = 3 * x^2 # 第一次反向传播 # dz/dx = 6*x = 6*2 = 12 z.backward(retain_graph=True) # retain_graph=True 允许后续的反向传播调用 print(f"After first backward pass, x.grad: {x.grad}") # 执行另一个操作(可以相同也可以不同) # 为简单起见,我们再次使用相同的 z 进行演示 # 注意:在实际应用中,您可能会基于新的输入或模型的不同部分计算新的损失。 z.backward() # 第二次反向传播 # 我们预期新梯度 (12) 会被加到已有的梯度 (12) 上 print(f"After second backward pass, x.grad: {x.grad}") # 手动清零梯度 x.grad.zero_() print(f"After zeroing, x.grad: {x.grad}")运行此代码会产生类似于以下的输出:After first backward pass, x.grad: tensor([12.]) After second backward pass, x.grad: tensor([24.]) After zeroing, x.grad: tensor([0.])请注意,第二次调用 z.backward() 如何将新计算的梯度(12)加到之前存储的梯度(12)上,结果为 24。这种累积是刻意为之的,并且有重要的用途。为何累积梯度?模拟更大批量这种默认行为的主要原因是为了方便梯度累积。当训练大型模型需要大批量数据以实现稳定收敛,但现有 GPU 内存无法一次性容纳如此大的批量时,此方法就很有用。与其处理一个大批量,不如这样操作:将大批量分成若干个更小的迷你批量。每次处理一个迷你批量:执行前向传播并计算损失。对当前迷你批量的损失调用 .backward()。为此迷你批量计算的梯度将添加到模型参数的 .grad 属性中。对大批量内的所有迷你批量重复步骤 2-3。在处理完所有迷你批量并累积其梯度后,使用 optimizer.step() 执行一次优化器更新步。此步骤使用所有迷你批量梯度的总和来更新模型权重,从而有效地模拟了更大批量的一次更新步。非常重要的一点是,在开始处理下一个大批量(如果不是累积,则为下一个迷你批量)之前,使用 optimizer.zero_grad() 清除梯度。这使您可以使用模型所需的有效批量大小进行训练,即使它不能一次性完全载入内存,从而牺牲计算时间来提高内存效率。标准训练中 optimizer.zero_grad() 的必要性在标准的训练循环中,您在每次迭代中处理一个批量、计算损失、计算梯度并更新权重,您通常不希望来自前一个批量的梯度影响当前的更新步。每个批量的梯度计算应该是独立的。由于 PyTorch 默认累积梯度,如果在计算新批量的梯度之前未能清除它们,将导致不正确的更新。优化器将使用新旧梯度的混合,从而损害训练过程。这就是为什么在标准的 PyTorch 训练循环中,您几乎总能看到 optimizer.zero_grad() 被调用的原因。它将优化器管理的所有参数的 .grad 属性重置,以确保随后的 .backward() 调用完全基于当前批量的损失来计算梯度。典型的训练迭代结构如下所示:# 假设 model, dataloader, loss_fn, 和 optimizer 已定义 # 遍历 epoch... # 遍历批量... # 1. 获取数据批量 inputs, labels = data_batch inputs, labels = inputs.to(device), labels.to(device) # 将数据移动到适当的设备 # 2. 清零梯度 # 重要:在处理新批量之前清除之前的梯度 optimizer.zero_grad() # 3. 前向传播:计算模型预测 outputs = model(inputs) # 4. 计算损失 loss = loss_fn(outputs, labels) # 5. 反向传播:计算梯度 loss.backward() # 6. 优化器更新步:更新模型权重 optimizer.step() # ... (记录日志、评估等)optimizer.zero_grad() 的放置位置很重要。它应该在您计算当前迭代的损失并执行反向传播之前发生,确保当前批量的梯度计算有一个干净的开始。虽然它通常放在循环的开头,但技术上它只需要在 loss.backward() 之前发生。然而,将其放在开头是常见做法,并且能清楚地划分新批量处理的开始。总而言之,梯度累积是 PyTorch 的一个内置功能,对于模拟更大的批量数据很有用。然而,在标准训练循环中,您必须通过在每次迭代开始时调用 optimizer.zero_grad() 来明确阻止这种累积,以确保模型更新仅基于当前批量的数据是正确的。