Autograd–automatic gradient,顾名思义,是能够帮我们自动求解梯度的包。所有Pytorch神经网络的核心都是autograd包,因此在正式开始训练我们的第一个网络之前,先让我们看看autograd具体能做些什么。
不论是深度学习还是更为基础的神经网络,它们“学习”的能力都是围绕Back Propagation(BP)(即反向传播)建立起来的,而BP的数学基础就是微分。想对BP做进一步了解的朋友可以移步这里,几个高赞回答都解答的很完善了。对于一个庞大的网络,为其中的每一个运算求微分是极其繁琐的,而autograd包将我们从数学的深渊中解放了出来。
对于所有的张量运算,autograd包都可以为我们提供其所对应的微分。auotgrad是一种由运行流程定义的(define-by-run)框架,也就意味着整个网络后向传播的过程将取决与代码的执行流程。在某些情况下或许用户需要在迭代中对输入的张量做不同的操作,那么autograd会自动根据操作的改变进行调整。
下面让我们结合一些简单例子来进一步了解autograd。
在上一篇文章中我们已经了解过torch.Tensor
这个包了。如果将一个tensor的属性.requires_grad
设为True,那么框架就会自动追踪所有基于该tensor的运算。在运算结束后执行.backward()
,所有运算相关的梯度都将会被自动求解出来,并且该张量对应的梯度会被累加到属性.grad
上。
如果想要将某个tensor移出追踪的列表,可以使用.detach()
函数完成解绑,解绑完成后在该张量上的后续计算都不会被autograd追踪。
如果只是想要暂时停止追踪(有助于节省内存消耗),可以通过with torch.no_grad():
将指定代码块包裹住。这个方法在评估模型时尤其有用,在模型评估的代码块中不涉及梯度计算以及参数更新,暂时停止梯度计算有助于提升程序的运行效率。
autograd的实现中还有一个非常重要的类:Function
。
Tensor
之间通过Function
进行相互连接并建立起一张无环图,这张无环图包含了完整的计算流程。每个tensor都有.grad_fn
属性来描述创造该张量所使用的Function
。需要注意这里有一个特例,用户创建的tensor的grad_fn
是None
。
在计算导数的时候可以直接调用Tensor
的方法:.backward()
。如果该tensor是一个标量(即张量内部只有一个元素),那么用户不需要为backward()
指定任何参数。反之则需为gradient参数传入一个大小匹配的张量,稍后我们会做讨论,现在先来看看标量的后向传播。
动手创建一个张量,并将requires_grad
设为True:
import torch
x = torch.ones(2, 2, requires_grad=True)
print(x)
# Out:
# tensor([[1., 1.],
# [1., 1.]], requires_grad=True)
执行一次张量运算:
y = x + 2
print(y)
print(y.grad_fn)
# Out:
# tensor([[3., 3.],
# [3., 3.]], grad_fn=)
# 生成该张量相关的运算
再试试其他的运算:
z = y * y * 3
out = z.mean()
print(z)
print(out)
# Out:
# tensor([[27., 27.],
# [27., 27.]], grad_fn=)
# tensor(27., grad_fn=)
.requires_grad_(...)
能够修改Tensor中requires_grad
属性的值,其默认为False
。
a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)
False
True
<SumBackward0 object at 0x7fac3a8d0c18>
现在我们可以在刚才创建的名为out
的张量上做后向传播(BP)了,由于out
中仅有一个元素,out.backward()
等价于out.backward(torch.tensor(1.))
。
out.backward()
print(x.grad)
# Out:
# tensor([[4.5000, 4.5000],
# [4.5000, 4.5000]])
程序最终输出了大小为2x2的矩阵,其中每个值均为4.5。
这个结果是如何得到的呢?方便起见,我们简写out
为 o o o,根据上述操作可以写出 o o o的实际表达式为: o = 1 4 ∑ i z i o=\frac{1}{4}\sum_{i}z_i o=41∑izi,且 z i = 3 ( x i + 2 ) 2 z_i=3(x_i+2)^2 zi=3(xi+2)2。很容易求得 ∂ o ∂ x i = 3 2 ( x i + 2 ) \frac{\partial o}{\partial x_i}=\frac{3}{2}(x_i+2) ∂xi∂o=23(xi+2),在 x i = 1 x_i=1 xi=1时有 ∂ o ∂ x i ∣ x i = 1 = 9 2 \frac{\partial o}{\partial x_i}|_{x_i=1}=\frac{9}{2} ∂xi∂o∣xi=1=29。具体到4.5这个值的意义,其实就是 o o o所对应的函数表达式在 x i = 1 x_i=1 xi=1处的梯度为4.5。
针对这个例子,如果我们要求函数的极小值点,则只需要不断延梯度的反方向行走。假设步长lr
为0.2,那么 x i _ n e w = x i − l r ∗ 4.5 , x i _ n e w = 0.1 x_{i\_new}=x_i-lr*4.5,x_{i\_new}=0.1 xi_new=xi−lr∗4.5,xi_new=0.1,通过不断迭代可以在 x i = − 2 x_i=-2 xi=−2处找到极小值点,此时再经由反向传播获得的x.grad
值为0.0,也符合数学上对于极小值点的定义。
上面提到了out
中元素数量可能大于一的情况,例如我们通过函数获得了一个向量 y ⃗ = f ( x ⃗ ) \vec{y}=f(\vec x) y=f(x),那么 y ⃗ \vec y y关于 x ⃗ \vec x x的梯度将可以表示成一个雅可比矩阵(Jacobian matrix):
J = ( ∂ y 1 ∂ x 1 ⋯ ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y n ∂ x 1 ⋯ ∂ y n ∂ x n ) J=\begin{pmatrix} \frac{\partial y_1}{\partial x_1} & \cdots & \frac{\partial y_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_n}{\partial x_1} & \cdots & \frac{\partial y_n}{\partial x_n} \end{pmatrix} J=⎝⎜⎛∂x1∂y1⋮∂x1∂yn⋯⋱⋯∂xn∂y1⋮∂xn∂yn⎠⎟⎞
而torch.autograd
是用于计算雅可比向量积的工具,即:当给定一个向量 v = ( v 1 v 2 ⋯ v n ) T v=(v_1 v_2 \cdots v_n)^T v=(v1v2⋯vn)T,计算 v T ⋅ J v^T \cdot J vT⋅J。如果给定的函数输出结果为标量 l = g ( y ⃗ ) l=g(\vec y) l=g(y),那么其梯度可以写作 v = ( ∂ l ∂ y 1 ⋯ ∂ l ∂ y m ) T v=(\frac{\partial l}{\partial y_1} \cdots \frac{\partial l}{\partial y_m})^T v=(∂y1∂l⋯∂ym∂l)T,进一步根据链式法则结合上面得到的雅可比矩阵,可以推导出雅可比向量积即为标量 l l l相对于输入 x ⃗ \vec x x的梯度:
J T ⋅ v = ( ∂ y 1 ∂ x 1 ⋯ ∂ y m ∂ x 1 ⋮ ⋱ ⋮ ∂ y 1 ∂ x n ⋯ ∂ y m ∂ x n ) ⋅ ( ∂ l ∂ y 1 ⋮ ∂ l ∂ y m ) = ( ∂ l ∂ x 1 ⋮ ∂ l ∂ x n ) J^T \cdot v = \begin{pmatrix} \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{pmatrix} \cdot \begin{pmatrix} \frac{\partial l}{\partial y_1} \\ \vdots \\ \frac{\partial l}{\partial y_m} \end{pmatrix} = \begin{pmatrix} \frac{\partial l}{\partial x_1} \\ \vdots \\ \frac{\partial l}{\partial x_n} \end{pmatrix} JT⋅v=⎝⎜⎛∂x1∂y1⋮∂xn∂y1⋯⋱⋯∂x1∂ym⋮∂xn∂ym⎠⎟⎞⋅⎝⎜⎛∂y1∂l⋮∂ym∂l⎠⎟⎞=⎝⎜⎛∂x1∂l⋮∂xn∂l⎠⎟⎞
(注: v T ⋅ J v^T \cdot J vT⋅J给出的结果与 J T ⋅ v J^T \cdot v JT⋅v在内容上是相同的,两者分别为行向量与列向量)
现在让我们来看一个雅可比向量积应用的实例:
x = torch.randn(3, requires_grad=True)
y = x * 2
while y.data.norm() < 1000:
y = y * 2
print(y)
# Out:
# tensor([ -2.6975, 472.1523, -1040.3346], grad_fn=)
很明显,这时的函数输出y
已经不再是一个标量了。torch.autograd
无法直接求解出雅可比矩阵,但可以通过向backward
传入一个向量来获取对应的雅可比向量积:
v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)
print(x.grad)
# Out:
# tensor([4.0960e+02, 4.0960e+03, 4.0960e-01])
当我们希望autograd停止对于张量梯度追踪的时候,可以使用with torch.no_grad():
将其包入其中:
print(x.requires_grad)
print((x ** 2).requires_grad)
with torch.no_grad():
print((x ** 2).requires_grad)
# Out:
# True
# True
# False
该方法在训练模型时的验证(validation)步骤中将起到很大作用,在推理时暂停了对于梯度的追踪,可以极大的节省验证时间与消耗的内存。
另一种方法是使用.detach()
方法创建一个新的张量,其内容与原始张量全部相同,唯一区别就是requires_grad为False:
print(x.requires_grad)
y = x.detach()
print(y.requires_grad)
print(x.eq(y).all())
# Out:
# True
# False
# tensor(True)