autograd
包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。下面介绍如何使用 autograd
包来进行自动求梯度的有关操作,autograd
包的核心类是 Tensor
Tensor.requires_grad = True
,开始追踪此 Tensor
上的所有操作,这样就可以利用链式法则进行梯度传播了Tensor.detach()
将 Tensor
从追踪记录中分离出来,防止将来的计算被追踪,这样梯度就传不过去了;还可以用 with torch.no_grad()
将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为在评估模型时不需要计算可训练参数(.requires_grad = True
)的梯度Tensor.backward()
来完成所有梯度计算,梯度将累计到 Tensor.grad
属性中(重复求梯度的话,通常要先用 .grad.data.zero_()
把累积的梯度清零)y.backward()
时,如果 y
是标量,则不需要传入参数;否则,需要传入一个与 y
同形的 Tensor
(解释见 3.2 节)Function
类
Function
对象用于反向计算输入的梯度,Function
和 Tensor
结合在一起就能构建一个记录整个计算过程的有向无环图(DAG),即 “计算图”Tensor.grad_fn
属性创建 Tensor
对应的 Function
对象实例并指向它,如果此 Tensor
是通过某些运算得到的,则 grad_fn
返回一个与这些运算相关的对象;否则返回 None
Tensor.grad_fn
属性,将重复生成不同的 Function
对象实例PyTorch中,张量 torch.Tensor
是存储和变换数据的主要工具,请参考:PyTorch入门(1)—— Tensor基本数据操作
如果把函数计算过程看作一颗树
Tensor
.is_leaf = True
.grad_fn = None
.requires_grad = False
,除非在创建时使用 requires_grad=True
进行设置Tensor
计算出来的 Tensor
.is_leaf = False
.grad_fn
生成的 Function
实例反应了上一步计算过程;除非计算它的 Tensor
都是.requires_grad = False
,这时其 .requires_grad = False
,.grad_fn = None
Tensor.requires_grad_(True/False)
,可以用in-place的方式改变 .requires_grad
属性示例
import torch
# 直接创建的 Tensor,生成的 Function 对象为 None
x = torch.ones(2,2,requires_grad=True)
print(x)
print(x.grad_fn)
'''
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
None
'''
# 计算得到的Tensor,生成对应计算类型的Function对象'
print((x+2).grad_fn) #
print((x-2).grad_fn) #
print(x.sum().grad_fn) #
# 重复访问 .grad_fn 属性,将生成并指向不同的 Function 对象实例
y = x+2
print(y.grad_fn) #
print(y.grad_fn) #
a = torch.randn(2, 2) # 如果不显式设置,默认 .requires_grad = False
print(a.requires_grad) # False
b = (a * a).sum() # 使用 .requires_grad = False 的Tensor计算的Tensor,仍然.requires_grad = False,.grad_fn = None
print(b.requires_grad) # False
print(b.grad_fn) # None
a.requires_grad_(True) # in-place方式直接设置 requires_grad = True
print(a.requires_grad) # True
b = (a * a).sum()
print(b.requires_grad) # True
print(b.grad_fn) #
print(a.is_leaf,b.is_leaf) # True False
Tensor.backward()
来完成所有梯度计算,梯度将累计到 Tensor.grad
属性中y.backward()
时
y
是标量,则不需要传入参数y
是张量,需要传入一个与 y
同形的 Tensor
。若不传入,报错 grad can be implicitly created only for scalar outputs
设 x = { x 1 , x 2 , x 3 , x 4 } \pmb{x} = \{x_1,x_2,x_3,x_4\} xxx={x1,x2,x3,x4} 是一个 1 × 4 1\times 4 1×4 的向量,求下式梯度 f ( x ) = 1 4 ∑ i = 1 4 3 ( x i + 2 ) 2 f(\pmb{x})=\frac{1}{4}\sum_{i=1}^43(x_i+2)^2 f(xxx)=41i=1∑43(xi+2)2
这个梯度 ∂ f ∂ x \frac{\partial f}{\partial \mathbf{x}} ∂x∂f 也将是一个同尺寸( 1 × 4 1\times 4 1×4)的向量
x = torch.tensor([[1,1,2,2],],dtype = torch.float,requires_grad=True)
a = x+2
b = a*a
c = 3*b
d = c.mean()
print(c) # tensor([[27., 27., 48., 48.]], grad_fn=)
# c.backward() # 报错 grad can be implicitly created only for scalar outputs
d.backward() # 求梯度
print(x.grad) # tensor([[4.5000, 4.5000, 6.0000, 6.0000]])
可以把计算图如下画出,可见,有 ∂ f ∂ x i = ∂ d ∂ x i = ∂ d ∂ c i ∂ c i ∂ b i ∂ b i ∂ a i ∂ a i ∂ x i = 1 4 ⋅ 3 ⋅ ( 2 x i + 4 ) ⋅ 1 = 1.5 x i + 3 \frac{\partial f}{\partial x_i} =\frac{\partial d}{\partial x_i}= \frac{\partial d}{\partial c_i}\frac{\partial c_i}{\partial b_i}\frac{\partial b_i}{\partial a_i}\frac{\partial a_i}{\partial x_i}=\frac{1}{4}·3·(2x_i+4)·1=1.5x_i+3 ∂xi∂f=∂xi∂d=∂ci∂d∂bi∂ci∂ai∂bi∂xi∂ai=41⋅3⋅(2xi+4)⋅1=1.5xi+3
注意:grad在反向传播过程中是累加的,每一次运行 .backward()
,梯度都会累加之前的梯度,所以一般在反向传播前把梯度清零
x = torch.tensor([[1,1,2,2],],dtype = torch.float,requires_grad=True)
# 第一遍反向传播计算梯度
d = ((x + 2) * (x + 2) * 3).mean()
d.backward()
print(x.grad) # tensor([[4.5000, 4.5000, 6.0000, 6.0000]])
# 重复反向传播,梯度会累加
d = ((x + 2) * (x + 2) * 3).mean()
d.backward()
print(x.grad) # tensor([[ 9., 9., 12., 12.]])
# 在反向传播之前先把梯度清零
d = ((x + 2) * (x + 2) * 3).mean()
x.grad.data.zero_() # inplace 操作清零累积的梯度
d.backward()
print(x.grad) # tensor([[4.5000, 4.5000, 6.0000, 6.0000]])
设一个函数值和自变量都是向量的函数 y = f ( x ) \pmb{y}=f(\pmb{x}) yyy=f(xxx),那么 ∂ y ∂ x \frac{\partial \mathbf{y}}{\partial \mathbf{x}} ∂x∂y 是一个雅可比矩阵
J = [ ∂ y 1 ∂ x 1 … ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 … ∂ y m ∂ x n ] (3) J = \left[ \begin{matrix} \frac{\partial y_1}{\partial x_1} & \dots & \frac{\partial y_1}{\partial x_n}\\ \vdots & \ddots & \vdots \\ \frac{\partial y_m}{\partial x_1} & \dots & \frac{\partial y_m}{\partial x_n} \end{matrix} \right] \tag{3} J=⎣⎢⎡∂x1∂y1⋮∂x1∂ym…⋱…∂xn∂y1⋮∂xn∂ym⎦⎥⎤(3)
Pytorch 中的自动求导其实就是在计算一些雅可比矩阵的乘积。设有标量函数 l = g ( y ) l=g(\pmb{y}) l=g(yyy),标量 l l l 对向量 y \pmb{y} yyy 的梯度为 ∂ l ∂ y = [ ∂ l ∂ y 1 … ∂ l ∂ y m ] \frac{\partial l}{\partial \pmb{y}} = \left[\frac{\partial l}{\partial y_1} \dots \frac{\partial l}{\partial y_m} \right] ∂yyy∂l=[∂y1∂l…∂ym∂l] 根据链式法则,我们有标量 l l l 关于向量 x \pmb{x} xxx 的梯度为
∂ l ∂ x = ∂ l ∂ y J = [ ∂ l ∂ y 1 … ∂ l ∂ y m ] [ ∂ y 1 ∂ x 1 … ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 … ∂ y m ∂ x n ] = [ ∂ l ∂ x 1 … ∂ l ∂ x n ] \frac{\partial l}{\partial \pmb{x}} = \frac{\partial l}{\partial \pmb{y}} J = \left[\frac{\partial l}{\partial y_1} \dots \frac{\partial l}{\partial y_m} \right] \left[ \begin{matrix} \frac{\partial y_1}{\partial x_1} & \dots & \frac{\partial y_1}{\partial x_n}\\ \vdots & \ddots & \vdots \\ \frac{\partial y_m}{\partial x_1} & \dots & \frac{\partial y_m}{\partial x_n} \end{matrix}\right] = \left[\frac{\partial l}{\partial x_1} \dots \frac{\partial l}{\partial x_n} \right] ∂xxx∂l=∂yyy∂lJ=[∂y1∂l…∂ym∂l]⎣⎢⎡∂x1∂y1⋮∂x1∂ym…⋱…∂xn∂y1⋮∂xn∂ym⎦⎥⎤=[∂x1∂l…∂xn∂l]
前文说到,若对向量/张量执行 .backward()
操作,必须传入一个与其同形的 Tensor
。这其实是为了避免向量/张量对张量求导,这种计算太复杂了。
举个例子,假设形状为
m x n
的矩阵X
经过运算得到了p x q
的矩阵Y
,Y
又经过运算得到了s x t
的矩阵Z
。那么按照前面讲的规则,dZ/dY
应该是一个s x t x p x q
四维张量,dY/dX 是一个p x q x m x n
的四维张量。问题来了,怎样反向传播?怎样将两个四维张量相乘???这要怎么乘???就算能解决两个四维张量怎么乘的问题,四维和三维的张量又怎么乘?导数的导数又怎么求,这一连串的问题,感觉要疯掉……
Pytorch中只允许标量对张量求导,得到和自变量同形的张量。所以,必要的时候,我们要通过对元素加权求和的方式把张量转换为标量。假设张量 y \pmb{y} yyy 是张量 x \pmb{x} xxx 的函数, w \pmb{w} www 和 y \pmb{y} yyy 同形,则 y.backward(w)
代表:先计算标量 l=torch.sum(y*w)
,然后求 ∂ l ∂ x \frac{\partial l}{\partial \mathbf{x}} ∂x∂l。对于3.1节的示例,有
∂ c ∂ x ∣ w = [ w 1 ∂ c 1 ∂ x 1 … w n ∂ c 4 ∂ x 4 ] , 其 中 w i ∂ c i ∂ x i = 6 w i x i + 12 w i \frac{\partial \pmb{c}}{\partial \pmb{x}} {\bigg|}_{\mathbf{w}} =\left[w_1 \frac{\partial c_1}{\partial x_1} \dots w_n \frac{\partial c_4}{\partial x_4}\right], 其中 w_i \frac{\partial c_i}{\partial x_i} = 6w_ix_i+12w_i ∂xxx∂ccc∣∣∣∣w=[w1∂x1∂c1…wn∂x4∂c4],其中wi∂xi∂ci=6wixi+12wi
x = torch.tensor([[1,1,2,2],],dtype = torch.float,requires_grad=True)
a = x+2
b = a*a
c = 3*b
c.backward(torch.tensor([[1,0.5,1,0.5],],dtype = torch.float))
print(x.grad) # tensor([[18., 9., 24., 12.]])
requires_grad=False
,梯度就无法从 y 2 y_2 y2 往回传播,这时有x = torch.tensor(1.0, requires_grad=True)
y1 = x ** 2
with torch.no_grad():
y2 = x ** 3
y3 = y1 + y2
print(x.requires_grad) # True
print(y1, y1.requires_grad) # tensor(1., grad_fn=) True
print(y2, y2.requires_grad) # tensor(1.) False
print(y3, y3.requires_grad) # tensor(2., grad_fn=) True
y3.backward()
print(x.grad) # tensor(2.)
#y2.backward() # 报错: element 0 of tensors does not require grad and does not have a grad_fn
要想修改 Tensor tensor
的值,且不影响梯度计算,可以对 tensor.data
进行操作
tensor.data
也是一个 Tensor
,且 tensor.data.requires_grad = False
,也就是说 tensor.data
独立于计算图之外,修改它不会被 autograd
记录,也就不会影响反向传播算出的梯度
x = torch.ones(1,requires_grad=True)
y = 3*x
y.backward() # 求梯度
print(x.grad) # tensor([3.])
x = torch.ones(1,requires_grad=True)
y = 3*x
# 通过.data修改,不影响计算图,不影响梯度结果
y.data = 4*x
y.backward() # 求梯度
print(x.grad) # tensor([3.])
x = torch.ones(1,requires_grad=True)
y = 3*x
# 直接修改,影响计算图,影响梯度结果
y = 4*x
y.backward() # 求梯度
print(x.grad) # tensor([4.])