PyTorch能成为主流深度学习框架,autograd包功不可没。它让开发者只关注前向计算逻辑,反向传播的梯度求导完全交给框架自动完成。但很多人用了一段时间后仍会遇到各种疑惑:为什么某些张量的grad属性是None?为什么一个epoch里loss不降反升,最后发现是梯度累积导致的?这些问题根源都在于对autograd机制理解不够透彻。本文将系统梳理PyTorch自动微分包的核心知识点,并汇总常见的踩坑问题。

autograd的核心:动态计算图是如何构建的
PyTorch采用动态图机制,也叫define-by-run。意思是计算图在代码执行的过程中实时构建,每做一次张量运算,框架就会在背后生成一个Function节点,并记录这次运算的输入、输出以及求导方法。这一点与TensorFlow早期的静态图有本质区别——静态图需要先定义完整计算图再喂数据,而动态图每次前向传播都会新建一张图,天然适合处理变长序列、动态结构等场景。
举个简单的例子,当我们执行 y = x * 2 + 1 时,autograd会分别记录乘法操作和加法操作,形成两个节点。当调用 y.backward() 时,反向传播从y开始,沿着这些节点逆向遍历,利用链式法则逐层计算梯度。整个图在backward执行完毕后默认会被释放,这就是为什么同一个张量不能连续调用两次backward,除非在调用时指定 retain_graph=True。
import torch x = torch.tensor([1.0, 2.0], requires_grad=True) y = x * 2 + 1 z = y.mean() z.backward() # 反向传播,计算梯度 print(x.grad) # tensor([1., 1.]) # 再次调用会报错,因为计算图已被释放 # z.backward() # RuntimeError z.backward(retain_graph=True) # 这样才能重复求导
理解动态图还有个关键点:图中的节点分为叶子节点和非叶子节点。x这种由用户直接创建的张量是叶子节点,梯度会保存在它的grad属性里;而 y、z这类中间结果是非叶子节点,默认不保留梯度。如果想查看中间变量的梯度,可以调用 y.retain_grad(),调试复杂网络时这个技巧非常实用。
requires_grad、no_grad与detach:三个最容易混淆的用法
requires_grad是张量的一个布尔属性,决定它是否参与梯度追踪。直接用 torch.tensor 创建的张量默认是False,而神经网络模型的参数 nn.Parameter 默认是True。有一个隐式规则需要注意:只要运算的输入中有一个张量的requires_grad为True,输出的requires_grad也会是True,且输出会记录整个运算历史。
推理阶段我们不需要计算梯度,此时推荐用 torch.no_grad() 上下文管理器包裹代码块。它会临时关闭梯度追踪,既省内存又加速计算。还有一种方式是调用 tensor.detach(),返回一个与原张量共享数据但脱离计算图的新张量,常用于把张量送入监控指标计算或者可视化流程。两者区别在于:no_grad作用于一段代码区域,detach作用于单个张量。
model = torch.nn.Linear(10, 1)
input_data = torch.randn(4, 10)
# 推理时关闭梯度,节省显存
with torch.no_grad():
output = model(input_data)
print(output.requires_grad) # False
# detach分离计算图
loss_value = torch.randn(3, requires_grad=True).sum()
detached = loss_value.detach()
print(detached.requires_grad) # False一个高频踩坑场景:训练时想打印loss的数值,写了 total_loss += loss,结果total_loss带着计算图,显存越占越多。正确做法是 total_loss += loss.item(),item方法返回Python数值,彻底切断与图的关联。类似的,往列表里存张量时也应存 loss.detach() 或 loss.item(),否则整个训练历史的计算图都无法被垃圾回收。
梯度累积、梯度清零与常见异常排查
PyTorch的梯度默认是累积式的。调用backward后,梯度会累加到 x.grad 上而不是覆盖,这是为了支持梯度累积这种特殊训练策略(比如显存不够时用小batch模拟大batch)。但在常规训练中,如果忘记在每次backward前清零梯度,就会导致梯度越滚越大,训练彻底崩掉。标准写法是每轮迭代先调用 optimizer.zero_grad()。
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for data, target in dataloader:
optimizer.zero_grad() # 清空历史梯度,必须在backward之前
output = model(data)
loss = loss_fn(output, target)
loss.backward() # 反向传播,梯度累积到param.grad
optimizer.step() # 用梯度更新参数排查梯度问题时,有几个实用技巧。第一,如果发现某个参数的grad是None,先检查它是否真的参与到了最终的loss计算中——若某分支的输出没被用到,autograd根本不会为该分支计算梯度。第二,可以用 torch.autograd.grad 手动求任意节点对指定输入的梯度,不污染grad属性,适合验证中间逻辑。第三,怀疑梯度爆炸或消失时,遍历参数打印梯度范数是最直接的手段:
for name, param in model.named_parameters():
if param.grad is not None:
print(name, param.grad.norm().item())
else:
print(name, "梯度为None,可能未参与loss计算")另外几个常见报错也值得记住。报错信息中包含“does not require grad”通常是对requires_grad为False的张量调用了backward;RuntimeError提示图已被释放就是前面说的重复backward问题;如果张量是整数类型还强行backward,会提示只能对标量输出求导或类型不支持梯度。遇到“只能对标量求导”的报错,要么对输出先做mean或sum,要么在backward里传入与输出同形状的gradient参数作为权重。
总的来说,autograd的设计哲学是记录与回放:前向时记录操作轨迹,反向时按轨迹执行链式法则。把动态图、叶子节点、梯度累积、no_grad这几件事搞明白,日常开发中九成以上的梯度问题都能自行定位。剩下的一成,多半是多进程或分布式场景下的特殊行为,那属于进阶话题,建议在掌握基础机制之后再深入研究。
PyTorch autograd自动微分反向传播修改时间:2026-09-06 03:24:35