- 参考:《动手学深度学习(PyTorch版)》—— 李沐
- 注意:由于本文是jupyter文档转换来的,代码不一定可以直接运行,有些注释是jupyter给出的交互结果,而非运行结果!!
- 深度学习中经常需要对函数求梯度(gradient)。PyTorch提供的
autograd
包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。下面介绍如何使用autograd
包来进行自动求梯度的有关操作,autograd
包的核心类是Tensor
1. 基础概念
- 开启和停止追踪
- 开启追踪:设置
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
对象实例
2. Tensor
-
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) # <AddBackward0 object at 0x00000223DC515F60> print((x-2).grad_fn) # <SubBackward0 object at 0x00000223DC515F98> print(x.sum().grad_fn) # <SumBackward0 object at 0x00000223E1F63240>
# 重复访问 .grad_fn 属性,将生成并指向不同的 Function 对象实例 y = x+2 print(y.grad_fn) # <AddBackward0 object at 0x00000223DC515048> print(y.grad_fn) # <AddBackward0 object at 0x00000223DC515F60>
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) # <SumBackward0 object at 0x000001CA54F0DC50> print(a.is_leaf,b.is_leaf) # True False
3. 梯度
- 使用
Tensor.backward()
来完成所有梯度计算,梯度将累计到Tensor.grad
属性中 - 在
y.backward()
时- 如果
y
是标量,则不需要传入参数 - 如果
y
是张量,需要传入一个与y
同形的Tensor
。若不传入,报错grad can be implicitly created only for scalar outputs
- 如果
3.1 示例
-
设 x = { x 1 , x 2 , x 3 , x 4 } \pmb{x} = \{x_1,x_2,x_3,x_4\} x={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(x)=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=<MulBackward0>) # 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]])
3.2 向量求导
-
设一个函数值和自变量都是向量的函数 y = f ( x ) \pmb{y}=f(\pmb{x}) y=f(x),那么 ∂ 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(y),标量 l l l 对向量 y \pmb{y} y 的梯度为 ∂ 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] ∂y∂l=[∂y1∂l…∂ym∂l] 根据链式法则,我们有标量 l l l 关于向量 x \pmb{x} x 的梯度为
∂ 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] ∂x∂l=∂y∂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} y 是张量 x \pmb{x} x 的函数, w \pmb{w} w 和 y \pmb{y} y 同形,则
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 ∂x∂c w=[w1∂x1∂c1…wn∂x4∂c4],其中wi∂xi∂ci=6wixi+12wix = 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.]])
3.3 中断梯度追踪
- 考虑这个标量函数:
y
3
=
y
1
+
y
2
=
x
2
+
x
3
y_3 = y_1+y_2=x^2+x^3
y3=y1+y2=x2+x3,这时有
∂ y 3 ∂ x = ∂ y 1 ∂ x + ∂ y 2 ∂ x = 2 x + 3 x = 5 x \frac{\partial y_3}{\partial x} = \frac{\partial y_1}{\partial x} + \frac{\partial y_2}{\partial x} = 2x+3x = 5x ∂x∂y3=∂x∂y1+∂x∂y2=2x+3x=5x 如果将其中的 y 2 y_2 y2 设置为禁止追踪requires_grad=False
,梯度就无法从 y 2 y_2 y2 往回传播,这时有
∂ y 3 ∂ x = ∂ y 1 ∂ x = 2 x \frac{\partial y_3}{\partial x} = \frac{\partial y_1}{\partial x}= 2x ∂x∂y3=∂x∂y1=2x 计算图如下
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=<PowBackward0>) True print(y2, y2.requires_grad) # tensor(1.) False print(y3, y3.requires_grad) # tensor(2., grad_fn=<AddBackward0>) 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
3.4 在不影响反向传播的情况下修改Tensor的值
-
要想修改
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.])