原文地址:AUTOMATIC DIFFERENTIATION WITH TORCH.AUTOGRAD
当训练神经网络的时候,最常用的算法就是反向传播。在该算法中,参数(模型的权重)是根据loss function对于给定的参数的梯度进行调整的。
为了计算这些梯度,Pytorch内置了微分引擎,被叫做torch.autograd。其支持对于任意计算图的自动计算。
考虑这样一个简单的一层神经网络,其输入是input,参数有w和b,并且拥有一些loss函数,可以用以下方式在Pytorch中定义它:
import torch
x = torch.ones(5)
y = torch.zeros(3)
print(x)
print(y)
w = torch.randn(5, 3, requires_grad=True)
print(w)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w) + b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)
Tensors, Functions 和计算图
这段代码定义了以下的计算图。
在这个网络中,w和b都是我们需要去优化的参数,之所以,我们必须能够去计算Loss函数对于这些变量的梯度梯度。为了实现此,我们对这些tensor设置了requires_grad属性。
注意:你可在创建tensor的时候设置参数requires_grad,或者之后使用x.requires_grad_(True)方法。
一个我们使用tensor去构建计算图的函数事实上是一个类Function的对象。这个对象知道如何在正方向计算函数,也知道如何在反向传播的过程中计算其的导数。对于反向传播函数的引用被存储在grad_fn tensor的属性中。你能够找到更多的关于Function的信息在文档中。
print(f"Gradient function for z = {z.grad_fn}")
print(f"Gradient function for loss = {loss.grad_fn}")
out:
Gradient function for z = <AddBackward0 object at 0x7f6175b58d60>
Gradient function for loss = <BinaryCrossEntropyWithLogitsBackward0 object at 0x7f6175b58a30>
计算梯度
为了在神经网络中去优化的参数的权重,我们必须计算我们Loss函数对于参数的导数,即我们必须得知loss对于w的偏导于loss对于b的偏导,分别在固定x和y的值的时候。为了计算这些导数,我们能够调用loss.backward()函数,然后检索来自w.grad和b.grad的值。
loss.backward()
print(w.grad)
print(b.grad)
tensor([[0.3313, 0.0626, 0.2530],
[0.3313, 0.0626, 0.2530],
[0.3313, 0.0626, 0.2530],
[0.3313, 0.0626, 0.2530],
[0.3313, 0.0626, 0.2530]])
tensor([0.3313, 0.0626, 0.2530])
注意:
-
我们仅能够从计算图的叶子节点获得grad属性,当该节点的requires_grad属性被设置为True时。对于其他在图中的节点,梯度将不再可用。
-
我么仅能够在给定的图中对参数因素使用backward一次来执行梯度计算。如果你需要对同一张计算图多次调用backward。我们需要给backward传递retain_graph=True。
关闭梯度追踪
通常情况下,所有的tensors都会设置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
另外可以达到相同效果的方法是对于tensor使用detach()方法
z = torch.matmul(x, w) + b
z_det = z.detach()
print(z_det.requires_grad)
False
这里有两个让你关闭梯度追踪的可能原因。
- 将你的神经网络中的一些参数标记为冻结参数。
- 当你仅仅只是做前向传播的时候加速你的计算。因为在tensors上不去追踪梯度的计算将会更加高效。
关于计算图的更多内容
概念上来说,自动梯度(autograd)在一个由Function对象构成的有向无环图(Directed Acyclic Graph, DAG)保存数据(tensors)的记录和所有被执行的操作(以及随之产生的新向量)在这个有向无环图中,叶节点是输入的tensors,根节点是输出的tensors。通过跟踪这个图的根和叶子节点,你能够使用链式法则来自动计算梯度。
在前向传播中,自动梯度计算同时做了以下两件事
- 执行必须的操作去计算tensor的结果。
- 在DAG中维护操作的梯度计算函数。
当.backward()在DAG根结点被调用时,反向传播通道开始。autograd做了如下工作:
- 从每一个.grad_fn中计算梯度。
- 在各自的tensor的grad属性中积累这些值。
- 使用链式法则,一直传播到叶tensors。
NOTE
在PyTorch中DAGs是动态的。值得注意的是,该图表是从头开始创建的;在每一次.backward()被调用之后,自动梯度开始重新构建一个新的图。这正是为何允许你在你的模型中使用控制流结构的原因;如果你有所需,你可以在每一个迭代器上改变形状,大小和操作。
可选阅读:张量梯度和雅可比乘积
在许多的例子中,我们拥有一个标量的loss function,然后我们必须去计算一些参数的梯度。但是,这里有一些例子当我们的输出函数是一个任意的tensor。在这个例子中,Pytorch允许你去计算所谓的雅可比乘积,而不是真正的梯度。
对于一个向量函数
y
⃗
=
f
(
x
⃗
)
\vec{y}=f(\vec{x})
y=f(x),当
x
⃗
=
⟨
x
1
,
…
,
x
n
⟩
\vec{x}=\langle x_1,\dots,x_n\rangle
x=⟨x1,…,xn⟩和
y
⃗
=
⟨
y
1
,
…
,
y
m
⟩
\vec{y}=\langle y_1,\dots,y_m\rangle
y=⟨y1,…,ym⟩的时候,
y
⃗
\vec{y}
y向量对
x
⃗
\vec{x}
x向量的梯度被雅可比行列式所表示。
J
=
(
∂
y
1
∂
x
1
⋯
∂
y
1
∂
x
n
⋮
⋱
⋮
∂
y
m
∂
x
1
⋯
∂
y
m
∂
x
n
)
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)
J=
∂x1∂y1⋮∂x1∂ym⋯⋱⋯∂xn∂y1⋮∂xn∂ym
PyTorch允许你去计算雅可比乘积
v
T
⋅
J
v^T\cdot J
vT⋅J从一个所给的输入向量
v
=
(
v
1
…
v
m
)
v=(v_1 \dots v_m)
v=(v1…vm)而不是计算雅可比行列式本身
这通过将v作为参数来调用.backward方法来实现。v的大小必须与原始我们想计算乘积的tensor一致,因为我们希望通过其
来计算乘积。
inp = torch.eye(4, 5, requires_grad=True)
# 先对所有元素+1,再对所有元素*2,最后对矩阵进行转置。
out = (inp+1).pow(2).t()
# 传入一个和y同行的权重向量进行加权得到一个标量。
out.backward(torch.ones_like(out), retain_graph=True)
print(f"First call\n{inp.grad}")
out.backward(torch.ones_like(out), retain_graph=True)
print(f"\nSecond call\n{inp.grad}")
inp.grad.zero_()
out.backward(torch.ones_like(out), 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.]])
Second call
tensor([[8., 4., 4., 4., 4.],
[4., 8., 4., 4., 4.],
[4., 4., 8., 4., 4.],
[4., 4., 4., 8., 4.]])
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.]])
需要注意到的是我们调用backward在第二次的时候,使用的是相同的参数,但是梯度的值却不同了。之所以会发生这种现象是因为当我们使用backward传播时,PyTorch累加了梯度,也就是说被计算出梯度的值对于所有的在计算图中的叶节点都被累加到了grad属性中。如果你想计算正确的梯度,你需要在之前将grad属性清零。在实际训练中,优化器帮助我们做到这一点。
NOTE
以前,我们调用backward()函数不添加任何参数。这一操作本质上等同于调用backward(torch.tensor(1.0)), 这是一种在标量函数中计算梯度的非常有用的方法,比如在神经网络的训练中计算loss时。