自动微分#
参考:autogradqs
在训练神经网络时,最常用的算法是 反向传播 (back propagation)。在该算法中,参数(模型权值)根据损失函数相对于给定参数的 梯度 (gradient)进行调整。
为了计算这些梯度,PyTorch 内置了名为 torch.autograd
的微分引擎。它对任何计算图,支持自动计算梯度。
考虑最简单的单层神经网络,输入 x
,参数 w
和 b
,以及一些损失函数。它可以在 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)
张量、函数和计算图#
在这个网络中,w
和 b
是需要优化的参数。因此,需要能够计算相对于这些变量的损失函数的梯度。为了做到这一点,设置这些张量的 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>
计算梯度#
为了优化神经网络中参数的权值,需要计算损失函数对参数的导数,即在 x
和 y
的某些固定值下,需要计算 \(\frac{\partial loss}{\partial b}\) 和 \(\frac{\partial loss}{\partial w}\)。为了计算这些导数,调用 loss.backward()
,然后从 w.grad
和 b.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}\) 的梯度雅可比矩阵:
与计算雅可比矩阵本身不同,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),这是一种计算梯度的有用方法。