注意
跳转至页面底部 下载完整示例代码。
理解 requires_grad, retain_grad, Leaf 和 Non-leaf 张量#
作者: Justin Silver
本教程通过一个简单的示例解释了 requires_grad、retain_grad、叶子张量 (leaf tensors) 和非叶子张量 (non-leaf tensors) 的细微差别。
开始之前,请确保您了解 张量及其操作方法。对 自动求导 (autograd) 工作原理 的基础知识也会有所帮助。
设置#
首先,请确保 已安装 PyTorch,然后导入必要的库。
import torch
import torch.nn.functional as F
接下来,我们实例化一个简单的网络以专注于梯度计算。该网络包含一个仿射层 (affine layer),后接一个 ReLU 激活函数,最后是预测张量与标签张量之间的均方误差 (MSE) 损失。
请注意,参数(W 和 b)必须设置 requires_grad=True,这样 PyTorch 才能跟踪涉及这些张量的操作。我们将在未来的 章节 中详细讨论这一点。
# tensor setup
x = torch.ones(1, 3) # input with shape: (1, 3)
W = torch.ones(3, 2, requires_grad=True) # weights with shape: (3, 2)
b = torch.ones(1, 2, requires_grad=True) # bias with shape: (1, 2)
y = torch.ones(1, 2) # output with shape: (1, 2)
# forward pass
z = (x @ W) + b # pre-activation with shape: (1, 2)
y_pred = F.relu(z) # activation with shape: (1, 2)
loss = F.mse_loss(y_pred, y) # scalar loss
叶子张量与非叶子张量#
运行前向传播后,PyTorch autograd 构建了一个 动态计算图,如下图所示。这是一个 有向无环图 (DAG),它记录了输入张量(叶子节点)、对这些张量的所有后续操作以及中间/输出张量(非叶子节点)。计算图通过使用微积分中的 链式法则,从图的根节点(输出)到叶子节点(输入)计算每个张量的梯度。
graph TD
x["x<br/>is_leaf=True<br/>requires_grad=False<br/>retains_grad=False<br/>grad=None"]
W["W<br/>is_leaf=True<br/>requires_grad=True<br/>retains_grad=False<br/>grad=None"]
b["b<br/>is_leaf=True<br/>requires_grad=True<br/>retains_grad=False<br/>grad=None"]
matmul["x @ W"]
z["z = x @ W + b<br/>is_leaf=False<br/>requires_grad=True<br/>retains_grad=False<br/>grad=None"]
relu["y_pred = relu(z)<br/>is_leaf=False<br/>requires_grad=True<br/>retains_grad=False<br/>grad=None"]
y["y<br/>is_leaf=True<br/>requires_grad=False<br/>retains_grad=False<br/>grad=None"]
loss["loss = mse(y_pred, y)<br/>is_leaf=False<br/>requires_grad=True<br/>retains_grad=False<br/>grad=None"]
x --> matmul
W --> matmul
matmul --> z
b --> z
z --> relu
relu --> loss
y --> loss
如果一个节点不是通过至少一个设置了 requires_grad=True 的输入进行张量运算的结果(例如 x, W, b 和 y),PyTorch 就会将其视为叶子 (leaf);其他所有节点都被视为非叶子 (non-leaf)(例如 z, y_pred 和 loss)。您可以通过探测张量的 is_leaf 属性来以编程方式验证这一点。
x.is_leaf=True
z.is_leaf=False
叶子节点和非叶子节点的区别决定了在反向传播后,该张量的梯度是否会被存储在 grad 属性中,从而能否用于 梯度下降。我们将在 下一节 中对此进行更多介绍。
现在让我们研究 PyTorch 如何计算并存储计算图中张量的梯度。
requires_grad#
为了构建可用于梯度计算的计算图,我们需要在张量构造函数中传入 requires_grad=True 参数。默认情况下,该值为 False,因此 PyTorch 不会跟踪任何已创建张量的梯度。要验证这一点,尝试不设置 requires_grad,重新运行前向传播,然后执行反向传播。您将看到
>>> loss.backward()
RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
此错误意味着 autograd 无法反向传播到任何叶子张量,因为 loss 没有在跟踪梯度。如果您需要更改该属性,可以在张量上调用 requires_grad_()(注意末尾的下划线)。
我们可以像上面使用 is_leaf 属性那样,对哪些节点需要进行梯度计算进行完整性检查。
x.requires_grad=False
W.requires_grad=True
z.requires_grad=True
记住一点很有用:非叶子张量默认情况下 requires_grad=True,否则反向传播将会失败。如果张量是叶子张量,那么只有在用户明确设置的情况下,它才会拥有 requires_grad=True。另一种表述方式是:如果张量的至少一个输入需要梯度,那么该张量也会需要梯度。
该规则有两个例外:
总之,requires_grad 告诉 autograd 哪些张量在反向传播工作时需要计算其梯度。这与哪些张量的 grad 字段被填充是两回事,后者是下一节的主题。
retain_grad#
为了实际进行优化(例如 SGD、Adam 等),我们需要运行反向传播以便提取梯度。
调用 backward() 会填充所有设置了 requires_grad=True 的叶子张量的 grad 字段。grad 是损失相对于我们要探测的张量的梯度。在运行 backward() 之前,此属性被设置为 None。
W.grad=tensor([[3., 3.],
[3., 3.],
[3., 3.]])
b.grad=tensor([[3., 3.]])
您可能对我们网络中的其他张量感到好奇。让我们检查剩余的叶子节点。
x.grad=None
y.grad=None
这些张量的梯度没有被填充,因为我们没有明确告知 PyTorch 计算它们的梯度(requires_grad=False)。
现在让我们看看一个中间的非叶子节点。
print(f"{z.grad=}")
/var/lib/workspace/beginner_source/understanding_leaf_vs_nonleaf_tutorial.py:230: UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its .grad attribute won't be populated during autograd.backward(). If you indeed want the .grad field to be populated for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. If you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor instead. See github.com/pytorch/pytorch/pull/30531 for more information. (Triggered internally at /__w/pytorch/pytorch/build/aten/src/ATen/core/TensorBody.h:493.)
print(f"{z.grad=}")
z.grad=None
PyTorch 为该梯度返回 None,并警告我们正在访问非叶子节点的 grad 属性。尽管 autograd 为了反向传播必须计算中间梯度,但它假定您事后不需要访问这些值。要改变这种行为,我们可以在张量上使用 retain_grad() 函数。这会告知 autograd 引擎在调用 backward() 后填充该张量的 grad。
# we have to re-run the forward pass
z = (x @ W) + b
y_pred = F.relu(z)
loss = F.mse_loss(y_pred, y)
# tell PyTorch to store the gradients after backward()
z.retain_grad()
y_pred.retain_grad()
loss.retain_grad()
# have to zero out gradients otherwise they would accumulate
W.grad = None
b.grad = None
# backpropagation
loss.backward()
# print gradients for all tensors that have requires_grad=True
print(f"{W.grad=}")
print(f"{b.grad=}")
print(f"{z.grad=}")
print(f"{y_pred.grad=}")
print(f"{loss.grad=}")
W.grad=tensor([[3., 3.],
[3., 3.],
[3., 3.]])
b.grad=tensor([[3., 3.]])
z.grad=tensor([[3., 3.]])
y_pred.grad=tensor([[3., 3.]])
loss.grad=tensor(1.)
我们得到与之前相同的 W.grad 结果。还要注意,因为 loss 是标量,损失相对于自身的梯度简单地为 1.0。
如果我们现在查看计算图的状态,会发现中间张量的 retains_grad 属性已经改变。按照惯例,对于任何叶子节点,此属性都会打印 False,即使它需要其梯度。
graph TD
x["x<br/>is_leaf=True<br/>requires_grad=False<br/>retains_grad=False<br/>grad=None"]
W["W<br/>is_leaf=True<br/>requires_grad=True<br/>retains_grad=False<br/>grad=torch.Tensor"]
b["b<br/>is_leaf=True<br/>requires_grad=True<br/>retains_grad=False<br/>grad=torch.Tensor"]
matmul["x @ W"]
z["z = x @ W + b<br/>is_leaf=False<br/>requires_grad=True<br/>retains_grad=True<br/>grad=torch.Tensor"]
relu["y_pred = relu(z)<br/>is_leaf=False<br/>requires_grad=True<br/>retains_grad=True<br/>grad=torch.Tensor"]
y["y<br/>is_leaf=True<br/>requires_grad=True<br/>retains_grad=False<br/>grad=None"]
loss["loss = mse(y_pred, y)<br/>is_leaf=False<br/>requires_grad=True<br/>retains_grad=True<br/>grad=torch.Tensor"]
x --> matmul
W --> matmul
matmul --> z
b --> z
z --> relu
relu --> loss
y --> loss
如果您在叶子张量上调用 retain_grad(),它不会执行任何操作,因为叶子张量默认情况下已经保留了它们的梯度(当 requires_grad=True 时)。如果我们对设置了 requires_grad=False 的张量调用 retain_grad(),PyTorch 实际上会抛出错误,因为它无法存储从未计算过的梯度。
>>> x.retain_grad()
RuntimeError: can't retain_grad on Tensor that has requires_grad=False
总结表#
使用 retain_grad() 和 retains_grad 仅对非叶子节点有意义,因为对于设置了 requires_grad=True 的叶子张量,其 grad 属性本身就已经会被填充。默认情况下,这些非叶子节点在反向传播后不会保留(存储)它们的梯度。我们可以通过重新运行前向传播、告知 PyTorch 存储梯度,然后执行反向传播来改变这一点。
下表可作为上述讨论的参考。以下场景是仅有的对 PyTorch 张量有效的情况。
|
|
|
|
|
|---|---|---|---|---|
|
|
|
将 |
抛出错误 |
|
|
|
将 |
无操作(已保留) |
|
|
|
无操作 |
将 |
|
|
|
无操作 |
无操作(已保留) |
结论#
在本教程中,我们介绍了 PyTorch 何时以及如何计算叶子和非叶子张量的梯度。通过使用 retain_grad,我们可以访问 autograd 计算图中中间张量的梯度。
如果您想了解更多关于 PyTorch autograd 系统的工作原理,请访问下方的 参考文献。如果您对本教程有任何反馈(改进、更正错别字等),请使用 PyTorch 论坛 和/或 问题追踪器 与我们联系。
参考文献#
脚本总运行时间: (0 分 0.366 秒)