自动微分#

参考autogradqs

在训练神经网络时,最常用的算法是 反向传播 (back propagation)。在该算法中,参数(模型权值)根据损失函数相对于给定参数的 梯度 (gradient)进行调整。

为了计算这些梯度,PyTorch 内置了名为 torch.autograd 的微分引擎。它对任何计算图,支持自动计算梯度。

考虑最简单的单层神经网络,输入 x,参数 wb,以及一些损失函数。它可以在 PyTorch 中以如下方式定义:

import torch
from torch import nn
from torch.nn import functional as F

x = torch.ones(5)  # 输入 tensor
y = torch.zeros(3)  # 期望的 output
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w)+b
loss = F.binary_cross_entropy_with_logits(z, y)

张量、函数和计算图#

在这个网络中,wb 是需要优化的参数。因此,需要能够计算相对于这些变量的损失函数的梯度。为了做到这一点,设置这些张量的 requires_grad 属性。

备注

可以在创建张量时设置 requires_grad 的值,或者稍后使用 x.requires_grad_(True) 方法。

应用在张量上构造计算图的函数实际上是 Function 类的对象。该对象知道如何在前向过程计算损失,也知道如何在反向传播步骤中计算其导数。对向后传播函数的引用存储在张量的 grad_fn 属性中。

小技巧

Function 是抽象基类,其子类需要实现静态方法 forward()backward()。然后,要在前向传递中使用自定义运算,请调用类方法 apply。不要直接调用 forward()

为了确保正确性和最佳性能,请确保在 ctx 上调用正确的方法,并使用 torch.autograd.gradcheck() 验证向后函数。

例如:

class Exp(Function):
    @staticmethod
    def forward(ctx, i):
        result = i.exp()
        ctx.save_for_backward(result)
        return result
    @staticmethod
    def backward(ctx, grad_output):
        result, = ctx.saved_tensors
        return grad_output * result
# Use it by calling the apply method:
output = Exp.apply(input)
print(f"z 的梯度函数: {z.grad_fn}")
print(f"loss 的梯度函数: {loss.grad_fn}")
z 的梯度函数: <AddBackward0 object at 0x7f6cf5e1bd30>
loss 的梯度函数: <BinaryCrossEntropyWithLogitsBackward0 object at 0x7f6cf5e1b7c0>

计算梯度#

为了优化神经网络中参数的权值,需要计算损失函数对参数的导数,即在 xy 的某些固定值下,需要计算 \(\frac{\partial loss}{\partial b}\)\(\frac{\partial loss}{\partial w}\)。为了计算这些导数,调用 loss.backward(),然后从 w.gradb.grad 检索值:

loss.backward()
print(w.grad)
print(b.grad)
tensor([[0.2498, 0.0348, 0.2548],
        [0.2498, 0.0348, 0.2548],
        [0.2498, 0.0348, 0.2548],
        [0.2498, 0.0348, 0.2548],
        [0.2498, 0.0348, 0.2548]])
tensor([0.2498, 0.0348, 0.2548])

备注

  • 只能获得计算图的叶节点的 grad 属性,它们的 requires_grad 属性设置为 True。对于图中的所有其他节点,梯度将不可用。

  • 由于性能原因,只能在给定的图上使用一次 backward 梯度计算。如果需要对同一个图进行多次 backward 调用,则需要将 retain_graph=True 传递给 backward 调用。

禁用梯度追踪#

默认情况下,所有 requires_grad=True 的张量都会跟踪它们的计算历史,并支持梯度计算。但是,在某些情况下,不需要这样做,例如,训练模型后,只是想把它应用到一些输入数据上,即只想通过网络进行正向计算。可以通过使用 no_grad() 块包围计算代码来停止跟踪计算:

z = torch.matmul(x, w)+b
print(z.requires_grad)

with torch.no_grad():
    z = torch.matmul(x, w)+b
print(z.requires_grad)
True
False

另一种实现相同结果的方法是对张量使用 detach() 方法:

z = torch.matmul(x, w)+b
z_det = z.detach()
print(z_det.requires_grad)
False

以下是禁用梯度跟踪的原因:

  • 将神经网络中的一些参数标记为 冻结参数 (frozen parameters)。这是对预训练的网络进行微调的非常常见的场景。

  • 在只进行正向传播的情况下 加快计算速度,因为在不跟踪梯度的张量上的计算将更加有效。

更多关于计算图的内容#

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

在forward 传播时,autograd 会同时做两件事:

  • 运行请求的运算来计算结果张量。

  • 在 DAG 中维护运算的 梯度函数

当在 DAG 根上调用 .backward() 时,后向传播开始。然后,autograd:从每个 .grad_fn 计算梯度,使用链式规则将它们累加到各自张量的 .grad 属性中,并一路传播到叶张量。

备注

在 PyTorch 中,DAG 是动态的。在每次 .backward() 调用之后,autograd 开始填充新的图。这正是允许你在模型中使用控制流语句的原因;如果需要,您可以在每次迭代中更改形状、大小和运算。

选读:张量梯度和雅可比积#

在很多情况下,损失函数是标量的,需要计算关于一些参数的梯度。然而,也有输出函数是任意张量的情况。在这种情况下,PyTorch 允许你计算所谓的雅可比乘积(Jacobian product),而不是实际的梯度。

对于向量函数 \(\vec{y}=f(\vec{x})\),其中 \(\vec{x}=\langle x_1,\dots,x_n\rangle\)\(\vec{y}=\langle y_1,\dots,y_m\rangle\)\(\vec{y}\)\(\vec{x}\) 的梯度雅可比矩阵:

\[\begin{split} \begin{align}J=\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) \end{align} \end{split}\]

与计算雅可比矩阵本身不同,PyTorch 允许你为给定的输入向量 \(v=(v_1 \dots v_m)\) 计算雅可比积 \(v^T\cdot J\)。这是通过 backward 调用参数 \(v\) 实现的。\(v\) 的大小应该和原始张量的大小一样,要根据它来计算乘积:

inp = torch.eye(5, requires_grad=True)
out = (inp+1).pow(2)
out.backward(torch.ones_like(inp), retain_graph=True)
print(f"First call\n{inp.grad}")
out.backward(torch.ones_like(inp), retain_graph=True)
print(f"\nSecond call\n{inp.grad}")
inp.grad.zero_()
out.backward(torch.ones_like(inp), retain_graph=True)
print(f"\nCall after zeroing gradients\n{inp.grad}")
First call
tensor([[4., 2., 2., 2., 2.],
        [2., 4., 2., 2., 2.],
        [2., 2., 4., 2., 2.],
        [2., 2., 2., 4., 2.],
        [2., 2., 2., 2., 4.]])

Second call
tensor([[8., 4., 4., 4., 4.],
        [4., 8., 4., 4., 4.],
        [4., 4., 8., 4., 4.],
        [4., 4., 4., 8., 4.],
        [4., 4., 4., 4., 8.]])

Call after zeroing gradients
tensor([[4., 2., 2., 2., 2.],
        [2., 4., 2., 2., 2.],
        [2., 2., 4., 2., 2.],
        [2., 2., 2., 4., 2.],
        [2., 2., 2., 2., 4.]])

小心

当使用相同的参数第二次调用 backward 时,梯度的值是不同的。这是因为在做反向传播时,PyTorch 会对梯度进行累加,即计算出的梯度的值被添加到计算图的所有叶子节点的 grad 属性中。如果你想计算正确的梯度,你需要在此之前将 grad 属性归零。在现实训练中,优化器可以帮助做到这一点。

备注

以前调用的是不带参数的 backward() 函数。这本质上相当于调用 backward(torch.tensor(1.0)),对于标量值函数(如神经网络训练期间的 loss),这是一种计算梯度的有用方法。