PyTorch自动求导


PyTorch的自动求导在torch.autograd包中实现。torch.Tensor和torch.Function为autograd包中的两个核心类,他们互相连接并生成一个有向非循环图。

1.1 自动求导要点

autograd包在自动求导时,需要考虑以下事项:

  • 创建叶子结点的Tensor,使用requires_grad参数指定是否记录对其的操作,以便之后利用backward函数求解。requires_grad参数默认为False,当其为True时,与这个节点相依赖的其他节点也会变成True(这条路径都被污染了)
  • 通过requires_grad_()方法修改Tensor汇总的requires_grad属性,可以调用.detach()或者with torch.no_grad()不再计算张量的梯度,便于进行评估、测试模型。
  • 通过运算创建的非叶子结点,会被自动赋予grad_fn属性,表示梯度函数,叶子结点的grad_fnNone
  • 对最后得到的Tensor执行backward函数(反向传播中作为根节点),会自动计算各变量的梯度,并将累加结果放在grad属性中,当计算完成后,非叶子结点的grad属性会被自动释放
  • backward函数接收参数,该参数应该与调用该函数的Tensor维度相同,或者是可以广播的维度。如果求导的Tensor是一个标量,backward中的参数可以省略掉
  • 反向传播的中间缓存会被清空,如果需要进行多次传播,需要指定函数中的参数retain_graphTrue。在多次反向传播过程中,梯度是累加的。
  • 非叶子结点的梯度在被backward()函数调用后就会被清空。
  • 可以通过torch.no_grad()包裹代码块来阻止autograd去跟踪哪些标记为.requesgrad=True的张量的历史记录。

1.2 计算图

在整个过程中,PyTorch采用计算图的形式进行组织,该计算图为动态图,它的计算图在每次正向传播过程中都将重新构建。而其他的架构(TF是后面才引进的),一般都是静态图。

计算图是一种有向无环图(DAG),用来表示算子与变量之间的关系,直观且高效。

一般来说,圆形表示变量,矩形表示算子。例如表达式z=wx+b,变量为x, w, b,这些变量是用户所创建的,不依赖于其他变量,因此是叶子结点。该表达式的计算图如下:

1
2
3
4
5
6
7
graph BT;
id1((dz))-->id2[addBackward]
id2-->id3((db))
id2-->id4((dy))
id4-->id5[mulBackward]
id5-->id6((dx))
id5-->id7((dw))
1
2
3
4
5
6
7
graph TB
i1((x))-->o1[mul]
i2((w))-->o1
o1-->i3((y))
i4((b))-->o2[add]
i3-->o2
o2-->i5((z))

image-20230319231749030

在这个过程中,叶子节点为x,w,b,非叶节点为z, y

当嗲用backward()函数后,autograd会从根节点z进行反向溯源,并根据链式法则计算每个叶子结点的梯度,并累加grad属性汇总。对于非叶子结点或算子的操作记录在grad_fn中,叶子结点的grad_fnNone


1.3 反向传播

标量反向

1
2
3
4
5
6
torch.autograd.backward(tensor,grad_tensors=None,retain_graph=None,create_graph=False,grad_variables=None)

# tensor 是用于计算的张良
# grad_tensors 是用来计算非标量的梯度,形状需要与tensor保持一致
# retain_graph 重复利用计算图
# create_graph 计算更高阶段梯度

对标量的backward无需指定参数。

1
2
3
4
5
6
7
8
9
10
11
import torch
x=torch.Tensor([2])
w=torch.randn(1,requires_grad=True)
b=torch.randn(1,requires_grad=True)

y=torch.matmul(w,x)
z=torch.add(y,b)

z.backward()
print(w.grad)
print(b.grad)

非标量反向

PyTorch不允许张量对张量求导,需要采用标量对张量进行求导,因此,如果目标张量对一个非标量调用backward,需要传入一个gradient参数,该参数也是张量,形状要跟调用backward的张量相同。

这个参数负责乘以需要求导参数的雅可比矩阵。

举个例子,我们有:

x=(x1=2,x2=3),y=(y1=x12+3x2,y2=x22+2x1)x=(x_1=2,x_2=3),y=(y_1=x^2_1+3x_2,y_2=x_2^2+2x_1)

那么对张量yy求雅可比矩阵(一阶偏导),得到的结果为:

ȷ=y1x1y1x2y2x1y2x2=2x1322x2\jmath=\begin{vmatrix} \frac{\partial y_1}{\partial x_1} & \frac{\partial y_1}{\partial x_2}\\ \frac{\partial y_2}{\partial x_1}&\frac{\partial y_2}{\partial x_2} \end{vmatrix}=\begin{vmatrix} 2x_1&3\\2&2x_2 \end{vmatrix}

x=[2,3]时,有:

ȷ=4326ȷT=4236\jmath=\begin{vmatrix} 4&3\\2&6 \end{vmatrix} \\ \\ \jmath^T=\begin{vmatrix} 4&2\\3&6 \end{vmatrix}

由于不支持Tensor对Tensor的求导,所以我们借助额外的向量,将其转化为标量对向量的求导。例如向量[0,1]T[0,1]^T,我们可以得到ȷ[1]=[2,6]T\jmath[1]=[2 ,6]^T,这表示y2y_2xx求导的梯度。同样,采用向量[1,0]T[1,0]^T表示y1y_1xx的梯度。

用个例子看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch

x=torch.tensor([[2,3]],dtype=torch.float,requires_grad=True)
j=torch.zeros(2,2)
y=torch.zeros(1,2)
y[0,0]=x[0][0]**2+3*x[0][1]
y[0,1]=x[0][1]**2+2*x[0][0]

# 计算y_1梯度
y.backward(torch.Tensor([[1,0]]),retain_graph=True) # 不释放图,咱等会还要用
j[0]=x.grad
x.grad=torch.zeros_like(x.grad)

y.backward(torch.Tensor([[0,1]]))
j[1]=x.grad

print(j)


'''
tensor([[4., 3.],
[2., 6.]])
'''

1.4 生命周期

image-20230323200359329

1.5 切断分支的反向传播

训练过程中,有时候我们想保持一部分的网络参数不变,而支队其中一部分的参数进行调整,只训练部分分支,那么这时候就可以通过detach()函数来切断一些分支的反向传播。

1
2
3
detach_()

# 将节点设置为叶子,requires_grad=False grad_fn=None

这个怎么说呢,比如y=x2y=x^2z=yxz=yx,此时我们想把yy视为常数cc,也就是zx=c\frac{\partial z}{\partial x}=c,可以用以下的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch

x=torch.ones(2,requires_grad=True)
y=x**2+3
c=y.detach_()
z=y*x
z.sum().backward()
print(x.grad==c)
print(x.grad)

tensor([True, True])
tensor([4., 4.])

如果我们不用detach,得到的结果就应该是:3*2+3=6了。

1
2
3
4
5
6
7
8
9
10
import torch

x=torch.ones(2,requires_grad=True)
y=x**2+3
# c=y.detach_()
z=y*x
z.sum().backward()
print(x.grad)

tensor([6., 6.])