Pytorch Tutorial 使用torch.autograd进行自动微分
本文翻译自 PyTorch 官网教程。
原文:https://pytorch.org/tutorials/beginner/basics/autogradqs_tutorial.html#optional-reading-tensor-gradients-and-jacobian-products
在训练神经网络时,最常使用到的算法就是反向传播(back propagation)。在该算法中,参数(模型权重)会根据损失函数相对于给定参数的**梯度(gradient)**进行更新。
为了计算这些梯度,PyTorch 中有一个内置的微分引擎,叫做 torch.autograd
。它可以自动地计算任意计算图的梯度。
考虑下面一个最简单的单层神经网络,其输入为 x
,权重参数为 w
和 b
,还有一个损失函数(此处采用二进制交叉熵)。在 PyTorch 中可以按照以下方式来定义上述神经网络:
import torchx = torch.ones(5) # input tensor
y = torch.zeros(3) # expected output
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w)+b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)
张量、函数和计算图
上述代码定义了这样一个计算图(computational graph):
在该网络中,w
和 b
是需要优化更新的权重参数。因此,我们需要能够计算损失函数关于这两个变量的梯度。为此,我们设置这两个变量的 requires_grad
属性为 True
。
我们也可以在创建张量之后,再使用
x.requires_grad_(True)
方法来设置requires_grad
的值。
我们将某个函数作用于张量来构建计算图,该函数实际上是 Function
类的一个对象。这个对象知道在前向传播时怎样计算该函数,也知道在反向传播时怎样计算它的微分。反向传播函数的是依据张量的 grad_fn
属性。更多信息请参考 Function
的[文档][https://pytorch.org/docs/stable/autograd.html#function]。
print('Gradient function for z =', z.grad_fn)
print('Gradient function for loss =', loss.grad_fn)
输出:
Gradient function for z = <AddBackward0 object at 0x7f06c1316a90>
Gradient function for loss = <BinaryCrossEntropyWithLogitsBackward0 object at 0x7f06c1316a90>
计算梯度
为了优化更新网络中的权重参数,我们需要计算损失函数关于权重参数的导数,即我们需要在给定 x
和 y
值的情况下的 ∂losss∂w\frac{\partial losss}{\partial w}∂w∂losss 和 ∂losss∂b\frac{\partial losss}{\partial b}∂b∂losss。 为了计算这些导数,我们调用 loss.backward()
函数,然后再来查看 w.grad
和 b.grad
的值。
loss.backward()
print(w.grad)
print(b.grad)
输出:
tensor([[0.2225, 0.2403, 0.3150],[0.2225, 0.2403, 0.3150],[0.2225, 0.2403, 0.3150],[0.2225, 0.2403, 0.3150],[0.2225, 0.2403, 0.3150]])
tensor([0.2225, 0.2403, 0.3150])
- 我们只能得到
requires_grad
设置为True
且为计算图中叶子结点的grad
属性,计算图中的所有其他结点的梯度是无法得到的。- 通过
backward()
,我们只能对某张计算图进行一次求梯度的运算,因为为了提升性能,通常计算图在进行一次求梯度的计算后就会被释放。如果我们需要多次对同一张计算图调用backward()
,我们需要在调用backward
的时候传入retain_grap=True
。
禁用梯度跟踪
默认情况下,所有的 requires_grad=True
的张量都会追踪它们的计算历史,并支持梯度计算。然而,有些情况下我们并不需要做这些。比如说,我们已经训练好了一个模型然后想将它应用于某些输入数据,即我们只想要对网络进行前向计算(就是推理时)。我们可以将我们的计算代码放到一个上下文管理器 torch.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
这里是我们可能会想要禁用梯度追踪的一些原因:
- 将我们网络中的某些参数冻结,这时我们要微调一个预训练过的网络时很常见的场景。
- 在我们只需要进行前向传播时加速运算,因为不进行梯度追踪的计算肯定会更高效。
更多关于计算图的知识
从概念上讲,autograd 在由 Function 对象组成的有向无环图 (DAG) 中记录数据(张量)和所有执行的操作(以及生成的新张量)。 在这个 DAG 中,叶子节点是输入张量,根节点是输出张量。 通过从根节点到叶子节点跟踪此图,可以使用链式法则自动计算梯度。
在前向传播中,autograd 同时完成这两件事情:
- 运行指定的操作来计算结果向量
- 将各操作的梯度函数保存在 DAG 中
当在 DAG 根上调用 .backward()
时,反向传递开始, 然后自动求导:
- 根据各个
.grad_fn
计算梯度 - 将它们累加到对应的张量的
.grad
属性中 - 使用链式求导法则,一直传播到叶子张量
在 PyTorch 中 DAG 是动态的
需要注意的重要一点是,在每次
.backward()
调用之后,DAG 是从头开始重新创建的。autograd 开始构建一张新图。这也是为什么在 PyTorch 中支持控制流语句; 我们可以根据需要在每次迭代时更改形状、大小和操作。
选读:张量梯度和雅克比乘积
在许多情况下,我们得到的损失都是一个标量,然后我们去计算它关于各个权重参数的梯度。然而,在某些情况下我们会输出一个任意维度的张量,PyTorch 允许我们计算所谓的雅克比乘积,而非真实的梯度。
对于一个向量 y⃗=f(x⃗)\vec{y}=f(\vec{x})y=f(x),其中 x⃗=<x1,…,xn>\vec{x}=<x_1,\dots,x_n>x=<x1,…,xn> ,y⃗=<y1,…,yn>\vec{y}=<y_1,\dots,y_n>y=<y1,…,yn>,则 y⃗\vec{y}y 关于 x⃗\vec{x}x 的梯度可以由雅克比矩阵给出:
PyTorch 允许我们计算给定输入向量 v=(v1,…,vn)v=(v_1,\dots,v_n)v=(v1,…,vn) 的雅克比乘积 vTv^TvT 而非雅克比矩阵本身。只需在调用 backward
的时候将 vvv 作为参数传入。vvv 的尺寸需要与我们想要计算雅克比乘积的原始张量相同:
inp = torch.eye(5, requires_grad=True)
out = (inp+1).pow(2)
out.backward(torch.ones_like(inp), retain_graph=True)
print("First call\n", inp.grad)
out.backward(torch.ones_like(inp), retain_graph=True)
print("\nSecond call\n", inp.grad)
inp.grad.zero_()
out.backward(torch.ones_like(inp), retain_graph=True)
print("\nCall after zeroing gradients\n", inp.grad)
输出:
First calltensor([[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 calltensor([[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 gradientstensor([[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
时,梯度的值会不同。这是因为在调用 backward
进行反向传播时,PyTorch 会累加梯度,即计算梯度得到的值会被加到计算图中所有叶子结点的 .grad
属性上。因此一般区情况下,为了计算的梯度正确,我们需要再每一步计算梯度前先将已有梯度清零(zero_grad()
)。在实际的训练中,优化器会帮助我们做这一步。
译者注:有时也可以通过这一累加特性变相增大 batch_size。
之前我们调用无参数的
backward()
函数。 这本质上等同于调用backward(torch.tensor(1.0))
,这在标量值函数的情况下计算梯度时很有用,例如神经网络训练期间的损失。
扩展阅读
- Autograd Mechanics