评价此页
torch.autograd">

torch.autograd 简介#

创建于:2017 年 3 月 24 日 | 上次更新:2025 年 1 月 10 日 | 上次验证:2024 年 11 月 05 日

torch.autograd 是 PyTorch 的自动微分引擎,它为神经网络的训练提供动力。在本节中,您将对 autograd 如何帮助神经网络进行训练有一个概念性的理解。

背景#

神经网络 (NN) 是一组嵌套函数的集合,这些函数在一些输入数据上执行。这些函数由参数(包括权重和偏置)定义,这些参数在 PyTorch 中存储在张量中。

NN 的训练分两步进行

前向传播:在前向传播中,NN 对正确的输出做出最佳猜测。它通过其每个函数运行输入数据来进行此猜测。

反向传播:在反向传播中,NN 根据其猜测中的误差成比例地调整其参数。它通过从输出向后遍历,收集误差相对于函数参数的导数(梯度),并使用梯度下降优化参数来实现这一点。有关反向传播的更详细演练,请查看此3Blue1Brown 的视频

在 PyTorch 中使用#

让我们看一下单个训练步骤。对于此示例,我们从 torchvision 加载预训练的 resnet18 模型。我们创建一个随机数据张量来表示具有 3 个通道的单个图像,高度和宽度为 64,并且其相应的 label 初始化为一些随机值。预训练模型中的标签具有形状 (1,1000)。

注意

本教程仅适用于 CPU,不适用于 GPU 设备(即使张量已移动到 CUDA)。

import torch
from torchvision.models import resnet18, ResNet18_Weights
model = resnet18(weights=ResNet18_Weights.DEFAULT)
data = torch.rand(1, 3, 64, 64)
labels = torch.rand(1, 1000)
Downloading: "https://download.pytorch.org/models/resnet18-f37072fd.pth" to /var/lib/ci-user/.cache/torch/hub/checkpoints/resnet18-f37072fd.pth

  0%|          | 0.00/44.7M [00:00<?, ?B/s]
 65%|██████▍   | 29.0M/44.7M [00:00<00:00, 303MB/s]
100%|██████████| 44.7M/44.7M [00:00<00:00, 334MB/s]

接下来,我们通过模型的每一层运行输入数据以进行预测。 这是前向传递

prediction = model(data) # forward pass

我们使用模型的预测和相应的标签来计算误差(loss)。下一步是通过网络反向传播此误差。当我们对误差张量调用 .backward() 时,反向传播启动。然后,Autograd 计算每个模型参数的梯度,并将其存储在参数的 .grad 属性中。

loss = (prediction - labels).sum()
loss.backward() # backward pass

接下来,我们加载一个优化器,在本例中为 SGD,学习率为 0.01,动量为 0.9。我们在优化器中注册模型的所有参数。

optim = torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)

最后,我们调用 .step() 以启动梯度下降。 优化器通过存储在 .grad 中的梯度调整每个参数。

optim.step() #gradient descent

此时,您拥有训练神经网络所需的一切。 以下部分详细介绍了 autograd 的工作原理 - 请随意跳过它们。


Autograd 中的微分#

让我们看看 autograd 如何收集梯度。 我们创建两个张量 ab,其中 requires_grad=True。 这向 autograd 发出信号,表明应对它们执行的每个操作进行跟踪。

import torch

a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)

我们从 ab 创建另一个张量 Q

\[Q = 3a^3 - b^2 \]
Q = 3*a**3 - b**2

让我们假设 ab 是 NN 的参数,Q 是误差。 在 NN 训练中,我们想要误差相对于参数的梯度,即

\[\frac{\partial Q}{\partial a} = 9a^2 \]
\[\frac{\partial Q}{\partial b} = -2b \]

当我们在 Q 上调用 .backward() 时,autograd 会计算这些梯度并将它们存储在各个张量的 .grad 属性中。

我们需要在 Q.backward() 中显式传递一个 gradient 参数,因为它是一个向量。gradient 是一个与 Q 具有相同形状的张量,它表示 Q 相对于自身的梯度,即

\[\frac{dQ}{dQ} = 1 \]

等效地,我们也可以将 Q 聚合为一个标量并隐式调用 backward,例如 Q.sum().backward()

梯度现在存储在 a.gradb.grad

# check if collected gradients are correct
print(9*a**2 == a.grad)
print(-2*b == b.grad)
tensor([True, True])
tensor([True, True])

可选阅读 - 使用 autograd 的向量微积分#

从数学上讲,如果您有一个向量值函数 \(\vec{y}=f(\vec{x})\),那么 \(\vec{y}\) 相对于 \(\vec{x}\) 的梯度是 Jacobian 矩阵 \(J\)

\[J = \left(\begin{array}{cc} \frac{\partial \bf{y}}{\partial x_{1}} & ... & \frac{\partial \bf{y}}{\partial x_{n}} \end{array}\right) = \left(\begin{array}{ccc} \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}}\\ \vdots & \ddots & \vdots\\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}} \end{array}\right)\]

一般来说,torch.autograd 是一个用于计算向量-雅可比乘积的引擎。 也就是说,给定任何向量 \(\vec{v}\),计算乘积 \(J^{T}\cdot \vec{v}\)

如果 \(\vec{v}\) 恰好是标量函数 \(l=g\left(\vec{y}\right)\) 的梯度

\[\vec{v} = \left(\begin{array}{ccc}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right)^{T}\]

那么根据链式法则,向量-雅可比乘积将是 \(l\) 相对于 \(\vec{x}\) 的梯度

\[J^{T}\cdot \vec{v} = \left(\begin{array}{ccc} \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{1}}\\ \vdots & \ddots & \vdots\\ \frac{\partial y_{1}}{\partial x_{n}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}} \end{array}\right)\left(\begin{array}{c} \frac{\partial l}{\partial y_{1}}\\ \vdots\\ \frac{\partial l}{\partial y_{m}} \end{array}\right) = \left(\begin{array}{c} \frac{\partial l}{\partial x_{1}}\\ \vdots\\ \frac{\partial l}{\partial x_{n}} \end{array}\right)\]

向量-雅可比乘积的这种特性是我们在上面的示例中使用的; external_grad 表示 \(\vec{v}\)

计算图#

从概念上讲,autograd 在有向无环图 (DAG) 中保留数据(张量)和所有已执行操作(以及生成的新张量)的记录,该图由Function 对象组成。 在此 DAG 中,叶子是输入张量,根是输出张量。 通过跟踪从根到叶的此图,您可以使用链式法则自动计算梯度。

在前向传递中,autograd 同时执行两件事

  • 运行请求的操作以计算结果张量,以及

  • 在 DAG 中维护操作的梯度函数

当在 DAG 根上调用 .backward() 时,反向传递启动。 然后 autograd

  • 从每个 .grad_fn 计算梯度,

  • 将它们累积在各个张量的 .grad 属性中,以及

  • 使用链式法则,一直传播到叶张量。

以下是我们示例中 DAG 的可视化表示。 在图中,箭头方向是前向传递的方向。 节点表示前向传递中每个操作的反向函数。 蓝色叶节点表示我们的叶张量 ab

../../_images/dag_autograd.png

注意

DAG 在 PyTorch 中是动态的 值得注意的重要一点是,该图是从头开始重新创建的; 每次调用 .backward() 后,autograd 都会开始填充一个新图。 这正是允许您在模型中使用控制流语句的原因; 您可以在每次迭代时根据需要更改形状、大小和操作。

从 DAG 中排除#

torch.autograd 跟踪所有张量上的操作,这些张量的 requires_grad 标志设置为 True。 对于不需要梯度的张量,将此属性设置为 False 会将其从梯度计算 DAG 中排除。

即使只有一个输入张量具有 requires_grad=True,操作的输出张量也将需要梯度。

x = torch.rand(5, 5)
y = torch.rand(5, 5)
z = torch.rand((5, 5), requires_grad=True)

a = x + y
print(f"Does `a` require gradients?: {a.requires_grad}")
b = x + z
print(f"Does `b` require gradients?: {b.requires_grad}")
Does `a` require gradients?: False
Does `b` require gradients?: True

在 NN 中,不计算梯度的参数通常称为冻结参数。 如果您事先知道不需要这些参数的梯度,则“冻结”模型的一部分会很有用(这通过减少 autograd 计算提供了一些性能优势)。

在微调中,我们冻结模型的大部分内容,通常只修改分类器层以对新标签进行预测。 让我们通过一个小例子来演示这一点。 和以前一样,我们加载一个预训练的 resnet18 模型,并冻结所有参数。

from torch import nn, optim

model = resnet18(weights=ResNet18_Weights.DEFAULT)

# Freeze all the parameters in the network
for param in model.parameters():
    param.requires_grad = False

假设我们想在新数据集中微调模型,该数据集具有 10 个标签。 在 resnet 中,分类器是最后一个线性层 model.fc。 我们可以简单地将其替换为新的线性层(默认情况下未冻结),该层充当我们的分类器。

model.fc = nn.Linear(512, 10)

现在,模型中的所有参数(model.fc 的参数除外)都被冻结。 唯一计算梯度的参数是 model.fc 的权重和偏差。

# Optimize only the classifier
optimizer = optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)

请注意,尽管我们在优化器中注册了所有参数,但唯一计算梯度(因此在梯度下降中更新)的参数是分类器的权重和偏差。

相同的排除功能可用作 torch.no_grad() 中的上下文管理器


进一步阅读:#

脚本的总运行时间:(0 分 0.679 秒)