pytorch autograd.grad

pytorch是动态图,即计算图的搭建和运算是同时的,随时可以输出结果;而tensorflow是静态图。

在pytorch的计算图里只有两种元素:数据(tensor)和运算(operation)

运算包括:加减乘除、开方、幂指对、三角函数等可求导运算。

数据可分为:叶子节点和非叶子节点;叶子节点是用户创建的节点,不依赖其他节点;叶子节点和非叶子节点的区别在于反向传播结束后,非叶子节点的梯度会被释放掉,只保留叶子节点的梯度,这样就节省了内存。如果想要保留非叶子节点的梯度,可以使用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()torch.autograd.grad(),这两种方法的区别在于前者是给叶子节点填充.grad字段,而后者是直接返回梯度。需要知道的一点是y.backward()其实等同于torch.autograd.backward(y)

一个简单的求导例子是 y = ( x + 1 ) × ( x + 2 ) y=(x+1)\times(x+2) y=(x+1)×(x+2),计算 ∂ y ∂ x \frac{\partial y}{\partial x} xy,假定 x = 2 x=2 x=2,先画出计算图:

在这里插入图片描述
整个链式求导过程为:

∂ y ∂ x = ∂ y ∂ a ∂ a ∂ x + ∂ y ∂ b ∂ b ∂ x = ( x + 2 ) × 1 + ( x + 1 ) × 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)\times1+(x+1)\times1\to7 xy=ayxa+byxb=(x+2)×1+(x+1)×17

使用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的梯度:1.类型为叶子节点、2.requires_grad=True、3.依赖该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)

# (tensor(7.),)

因为指定了输出y,输入x,所以返回值就是 ∂ y ∂ x \frac{\partial y}{\partial x} xy这一梯度,注意整个返回值是一个元组,但我们仅需要元组的第一个元素。

再举另外一个复杂一点且高阶求导的例子: z = x 2 y z=x^2y z=x2y,计算 ∂ z ∂ x , ∂ z ∂ y , ∂ 2 z ∂ 2 x \frac{\partial z}{\partial x},\frac{\partial z}{\partial y},\frac{\partial^2 z}{\partial^2 x} xz,yz,2x2z,假设给定 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 ∂ 2 x = 2 y → 6 \frac{\partial z}{\partial x}=2xy\to12,\frac{\partial z}{\partial y}=x^2\to4,\frac{\partial^2 z}{\partial^2 x}=2y\to6 xz=2xy12,yz=x24,2x2z=2y6

求一阶导可以用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])
>>>tensor(12.)

在这里如果这样写代码,同时输出y的导数:

grad_x=torch.autograd.grad(outputs=z,inputs=x)
grad_y=torch.autograd.grad(outputs=z,inputs=y)

此时会这样进行报错:

RuntimeError: Trying to backward through the graph a second time (or directly access saved tensors after they 
have already been freed). Saved intermediate values of the graph are freed when you call .backward() or 
autograd.grad(). Specify retain_graph=True if you need to backward through the graph a second time 
or if you need to access saved tensors after calling backward.

这是因为无论是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的运算方式,需要使用create_graph=True在保留原图的基础上再建立额外的求导计算图,也就是会把 ∂ z ∂ x = 2 x y \frac{\partial z}{\partial x}=2xy xz=2xy这样的运算存下来:

# autograd.grad() + 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, create_graph=True)
grad_xx = torch.autograd.grad(outputs=grad_x, inputs=x)

print(grad_xx[0])
>>>tensor(6.)

grad_xx这里也可以直接用backward(),相当于直接从 ∂ z ∂ x = 2 x y \frac{\partial z}{\partial x}=2xy xz=2xy开始回传

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这个一阶导继续求导

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从开始回传,我们试一下

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()时默认会累加梯度,需要手动把前一次的梯度清零

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可以是标量或者向量,但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'=y.sum()=x_1^2+x_2^2,\frac{\partial y'}{\partial x_1}=2x_1\to2,\frac{\partial y'}{\partial x_2}=2x_2\to4 x=[x1,x2],y=[x12,x22],y=y.sum()=x12+x22,x1y=2x12,x2y=2x24

x = torch.tensor([1., 2.]).requires_grad_()
y = x * x

y.sum().backward()
print(x.grad)
>>>tensor([2., 4.])

具体一点来解释,我们写出求导计算的雅可比矩阵, y = [ y 1 , y 2 ] 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 ] J=[\frac{\partial y}{\partial x_1},\frac{\partial 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=[x1y,x2y]=[x1y1x1y2x2y1x2y2],但是我们希望最终求导的结果是 [ ∂ y 1 ∂ x 1 , ∂ y 2 ∂ x 2 ] [\frac{\partial y_1}{\partial x_1},\frac{\partial y_2}{\partial x_2}] [x1y1,x2y2],我们怎么得到这个值呢?注意 ∂ y 1 ∂ x 2 \frac{\partial y_1}{\partial x_2} x2y1和\frac{\partial y_2}{\partial x_1}是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}]^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} [x1y1,x2y2]T=[x1y1x1y2x2y1x2y2][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.])

注意这里的torch.ones_like(x)指的就是雅可比矩阵右乘的那个向量,我们也可以使用autograd

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

原位操作 in-place

这个操作改变值,但是不改变对象地址

注意:叶子节点不可执行in-place操作,因为反向传播时会访问原来的对象地址。

  • 13
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值