PyTorch是动态图,即计算图的搭建和运算是同时的,随时可以输出结果;而TensorFlow是静态图。
在pytorch的计算图里只有两种元素:数据(tensor)和 运算(operation)
运算包括了:加减乘除、开方、幂指对、三角函数等可求导运算
数据可分为:叶子节点(leaf node)和非叶子节点;叶子节点是用户创建的节点,不依赖其它节点;它们表现出来的区别在于反向传播结束之后,非叶子节点的梯度会被释放掉,只保留叶子节点的梯度,这样就节省了内存。如果想要保留非叶子节点的梯度,可以使用retain_grad()方法。
torch.tensor 具有如下属性:
- 查看 是否可以求导 requires_grad
- 查看 运算名称 grad_fn
- 查看 是否为叶子节点 is_leaf
- 查看 导数值 grad
针对requires_grad属性,自己定义的叶子节点默认为False,而非叶子节点默认为True,神经网络中的权重默认为True。判断哪些节点是True/False的一个原则就是从你需要求导的叶子节点到loss节点之间是一条可求导的通路。
当我们想要对某个Tensor变量求梯度时,需要先指定requires_grad属性为True,指定方式主要有两种:
x = torch.tensor(1.).requires_grad_() # 第一种
x = torch.tensor(1., requires_grad=True) # 第二种
PyTorch提供两种求梯度的方法:backward() and torch.autograd.grad() ,他们的区别在于前者是给叶子节点填充.grad字段,而后者是直接返回梯度给你,我会在后面举例说明。还需要知道y.backward()其实等同于torch.autograd.backward(y)
一个简单的求导例子是:
y
=
(
x
+
1
)
∗
(
x
+
2
)
y=(x+1)*(x+2)
y=(x+1)∗(x+2),计算
∂
y
/
∂
x
\partial y /\partial x
∂y/∂x ,假设给定
x
=
2
x=2
x=2, 先画出计算图:
手算的话,
∂
y
∂
x
=
∂
y
∂
a
∂
a
∂
x
+
∂
y
∂
b
∂
b
∂
x
=
x
+
2
+
x
+
1
=
7
\frac{\partial y}{\partial x}=\frac{\partial y}{\partial a} \frac{\partial a}{\partial x} + \frac{\partial y}{\partial b}\frac{\partial b}{\partial x} = x+2+x+1=7
∂x∂y=∂a∂y∂x∂a+∂b∂y∂x∂b=x+2+x+1=7
使用backward()
x = torch.tensor(2., requires_grad=True)
a = torch.add(x, 1)
b = torch.add(x, 2)
y = torch.mul(a, b)
y.backward()
print(x.grad)
>>>tensor(7.)
看一下这几个tensor的属性:
print("requires_grad: ", x.requires_grad, a.requires_grad, b.requires_grad, y.requires_grad)
print("is_leaf: ", x.is_leaf, a.is_leaf, b.is_leaf, y.is_leaf)
print("grad: ", x.grad, a.grad, b.grad, y.grad)
>>>requires_grad: True True True True
>>>is_leaf: True False False False
>>>grad: tensor(7.) None None None
使用backward()函数反向传播计算tensor的梯度时,并不计算所有tensor的梯度,而是只计算满足这几个条件的tensor的梯度:
- 类型为叶子节点、
- requires_grad=True
- 依赖该tensor的所有tensor的requires_grad=True。
所有满足条件的变量梯度会自动保存到对应的grad属性里。
使用autograd.grad()
x = torch.tensor(2., requires_grad=True)
a = torch.add(x, 1)
b = torch.add(x, 2)
y = torch.mul(a, b)
grad = torch.autograd.grad(outputs=y, inputs=x)
print(grad[0])
>>>tensor(7.)
因为指定了输出y,输入x,所以返回值就是
∂
y
/
∂
x
\partial y/\partial x
∂y/∂x这一梯度,完整的返回值其实是一个元组,保留第一个元素就行,后面元素是?
再举一个复杂一点且高阶求导的例子: z = x 2 y z=x^2y z=x2y,计算 ∂ z / ∂ x , ∂ z / ∂ y , ∂ 2 z / ∂ x 2 \partial z/\partial x,\partial z/\partial y,\partial^2z/\partial x^2 ∂z/∂x,∂z/∂y,∂2z/∂x2 ,假设给定 x = 2 , y = 3 x=2, y=3 x=2,y=3
手算的话:
∂
z
∂
x
=
2
x
y
→
12
,
∂
z
∂
y
=
x
2
→
4
,
∂
2
z
∂
x
2
=
2
y
→
6
\frac{\partial z}{\partial x}=2xy \to12,\frac{\partial z}{\partial y}=x^2 \to 4,\frac{\partial^2z}{\partial x^2}=2y \to 6
∂x∂z=2xy→12,∂y∂z=x2→4,∂x2∂2z=2y→6
求一阶导可以用backward().
x = torch.tensor(2., requires_grad=True)
y = torch.tensor(3., requires_grad=True)
z = x * x * y
z.backward()
print(x.grad, y.grad)
>>>tensor(12.) tensor(4.)
也可以用autograd.grad()
x = torch.tensor(2.).requires_grad_()
y = torch.tensor(3.).requires_grad_()
z = x * x * y
grad_x = torch.autograd.grad(outputs=z, inputs=x)
print(grad_x[0])
# grad_y = torch.autograd.grad(outputs=z, inputs=y) 无法对y进行求导了
>>>tensor(12.)
为什么不在这里面同时也求对y的导数呢?因为无论是backward
还是autograd.grad
在计算一次梯度后图就被释放了,如果想要保留,需要添加retain_graph=True
x = torch.tensor(2.).requires_grad_()
y = torch.tensor(3.).requires_grad_()
z = x * x * y
grad_x = torch.autograd.grad(outputs=z, inputs=x, retain_graph=True)
grad_y = torch.autograd.grad(outputs=z, inputs=y)
print(grad_x[0], grad_y[0])
>>>tensor(12.) tensor(4.)
再来看如何求高阶导,理论上其实是上面的grad_x
再对x
求梯度,试一下看:
x = torch.tensor(2.).requires_grad_()
y = torch.tensor(3.).requires_grad_()
z = x * x * y
grad_x = torch.autograd.grad(outputs=z, inputs=x, retain_graph=True)
grad_xx = torch.autograd.grad(outputs=grad_x, inputs=x)
print(grad_xx[0])
>>>RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
报错了,虽然retain_graph=True
保留了计算图和中间变量梯度, 但没有保存grad_x
的运算方式,需要使用creat_graph=True
在保留原图的基础上再建立额外的求导计算图,也就是会把
∂
z
/
∂
x
=
2
x
y
\partial z/\partial x=2xy
∂z/∂x=2xy这样的运算存下来。
grad_xx这里也可以直接用backward(),相当于直接从 ∂ z / ∂ x = 2 x y \partial z/\partial x=2xy ∂z/∂x=2xy开始回传:
# autograd.grad() + backward()
x = torch.tensor(2.).requires_grad_()
y = torch.tensor(3.).requires_grad_()
z = x * x * y
grad = torch.autograd.grad(outputs=z, inputs=x, create_graph=True)
grad[0].backward()
print(x.grad)
>>>tensor(6.)
也可以先用backward()
然后对x.grad
这个一阶导继续求导:
# backward() + autograd.grad()
x = torch.tensor(2.).requires_grad_()
y = torch.tensor(3.).requires_grad_()
z = x * x * y
z.backward(create_graph=True)
grad_xx = torch.autograd.grad(outputs=x.grad, inputs=x)
print(grad_xx[0])
>>>tensor(6.)
那是不是也可以直接用两次backward()
呢?第二次直接x.grad
从开始回传,我们试一下:
# backward() + backward()
x = torch.tensor(2.).requires_grad_()
y = torch.tensor(3.).requires_grad_()
z = x * x * y
z.backward(create_graph=True) # x.grad = 12
x.grad.backward()
print(x.grad)
>>>tensor(18., grad_fn=<CopyBackwards>)
发现了问题,结果不是6,而是18,发现第一次回传时输出x
梯度是12。这是因为PyTorch使用backward()
时默认会累加梯度,也就是12+6=18
,需要手动把前一次的梯度清零:
x = torch.tensor(2.).requires_grad_()
y = torch.tensor(3.).requires_grad_()
z = x * x * y
z.backward(create_graph=True)
x.grad.data.zero_()
x.grad.backward()
print(x.grad)
>>>tensor(6., grad_fn=<CopyBackwards>)
有没有发现前面都是对标量求导,如果不是标量会怎么样呢?
x = torch.tensor([1.,2.]).requires_grad_()
y=x+1
y.backward()
print(x.grad)
>>>RuntimeError: grad can be implicitly created only for scalar outputs
报错了,因为只能标量对标量,标量对向量求梯度, x x x可以是标量或者向量,但 y y y只能是标量;所以只需要先将$$y转变为标量,对分别求导没影响的就是求和。
此时,
x
=
[
x
1
,
x
2
]
,
y
=
[
x
1
2
,
x
2
2
]
,
y
′
=
y
.
s
u
m
(
)
=
x
1
2
+
x
2
2
,
∂
y
′
∂
x
1
=
2
x
1
→
2
,
∂
y
′
∂
x
2
=
2
x
2
→
4
x=[x_1,x_2],y=[x_1^2, x_2^2],y\prime=y.sum()=x_1^2+x_2^2, \\ \frac{\partial y\prime}{\partial x_1}=2x_1 \to 2,\frac{\partial y\prime}{\partial x_2}=2x_2 \to 4
x=[x1,x2],y=[x12,x22],y′=y.sum()=x12+x22,∂x1∂y′=2x1→2,∂x2∂y′=2x2→4
x = torch.tensor([1., 2.]).requires_grad_()
y = x * x
y.sum().backward()
print(x.grad)
>>>tensor([2., 4.])
再具体一点来解释,让我们写出求导计算的雅可比矩阵,
y
=
[
y
1
,
y
2
]
\boldsymbol y=[y_1,y_2]
y=[y1,y2]是一个向量,
J
=
[
∂
y
∂
x
1
,
∂
y
∂
x
2
]
=
[
∂
y
1
∂
x
1
∂
y
1
∂
x
2
∂
y
2
∂
x
1
∂
y
2
∂
x
2
]
\boldsymbol J=[\frac{\partial \boldsymbol y}{\partial x_1},\frac{\partial \boldsymbol y}{\partial x_2}]=\begin{bmatrix} \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{bmatrix}
J=[∂x1∂y,∂x2∂y]=[∂x1∂y1∂x1∂y2∂x2∂y1∂x2∂y2]
而我们希望最终的求导结果是
[
∂
y
1
∂
x
1
,
∂
y
2
∂
x
2
]
[\frac{\partial y_1}{\partial x_1}, \frac{\partial y_2}{\partial x_2}]
[∂x1∂y1,∂x2∂y2],那怎么得到呢?注意
∂
y
1
∂
x
2
\frac{\partial y_1}{\partial x_2}
∂x2∂y1和
∂
y
2
∂
x
1
\frac{\partial y_2}{\partial x_1}
∂x1∂y2都是0,那是不是
[
∂
y
1
∂
x
1
,
∂
y
2
∂
x
2
]
T
=
[
∂
y
1
∂
x
1
∂
y
1
∂
x
2
∂
y
2
∂
x
1
∂
y
2
∂
x
2
]
[
1
1
]
[\frac{\partial y_1}{\partial x_1}, \frac{\partial y_2}{\partial x_2}]^\mathsf{T}=\begin{bmatrix} \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{bmatrix}\begin{bmatrix} 1 \\ 1 \end{bmatrix}
[∂x1∂y1,∂x2∂y2]T=[∂x1∂y1∂x1∂y2∂x2∂y1∂x2∂y2][11]
所以不用y.sum()
的另一种方式是:
x = torch.tensor([1., 2.]).requires_grad_()
y = x * x
y.backward(torch.ones_like(x))
print(x.grad)
>>>tensor([2., 4.])
也可以使用autograd
。上面和这里的torch.ones_like(x)
位置指的就是雅可比矩阵右乘的那个向量。
x = torch.tensor([1., 2.]).requires_grad_()
y = x * x
grad_x = torch.autograd.grad(outputs=y, inputs=x, grad_outputs=torch.ones_like(x))
print(grad_x[0])
>>>tensor([2., 4.])
或者
x = torch.tensor([1., 2.]).requires_grad_()
y = x * x
grad_x = torch.autograd.grad(outputs=y.sum(), inputs=x)
print(grad_x[0])
>>>tensor([2., 4.])
下面是着重强调以及引申的几点
- 梯度清零
Pytorch 的自动求导梯度不会自动清零,会累积,所以一次反向传播后需要手动清零。
x.grad.zero_()
而在神经网络中,我们只需要执行optimizer.zero_grad()
- 使用detach()切断,不会再往后计算梯度
假设有模型A和模型B,我们需要将A的输出作为B的输入,但训练时我们只训练模型B,那么可以这样做input_B = output_A.detach()
如果还是以前面的为例子,将a切断,将只有b一条通路,且a变为叶子节点。x = torch.tensor([2.], requires_grad=True) a = torch.add(x, 1).detach() b = torch.add(x, 2) y = torch.mul(a, b) y.backward() print("requires_grad: ", x.requires_grad, a.requires_grad, b.requires_grad, y.requires_grad) print("is_leaf: ", x.is_leaf, a.is_leaf, b.is_leaf, y.is_leaf) print("grad: ", x.grad, a.grad, b.grad, y.grad) >>>requires_grad: True False True True >>>is_leaf: True True False False >>>grad: tensor([3.]) None None None