一文解释 PyTorch求导相关 (backward, autograd.grad)

文摘   科技   2024-04-08 20:27   北京  

点击上方蓝字关注我们

反向传播(Backpropagation)是使训练深度模型在计算上易于处理的关键算法。对于现代神经网络来说,与简单的实现相比,它可以使梯度下降的训练速度快1000万倍。这就是一个模型需要一周的训练时间和20万年训练时间的区别。


除了在深度学习中的应用之外,反向传播在许多其他领域也是一种强大的计算工具,从天气预报到分析数值稳定性——它只是有不同的名字。事实上,该算法在不同领域已经被重新发明了至少几十次。近年来在运筹优化邻域也应用广泛,比如一直挺火的端到端的预测后优化(End-to-End Predict-Then-Optimize)


从根本上讲,这是一种快速计算导数的技术。这是一个必不可少的技巧,不仅在深度学习中,而且在各种各样的数值计算情况下都是如此。


本文详细解释了pytorch求导、pytorch中的dynamic graph,原文链接:一文解释 PyTorch求导相关 (backward, autograd.grad) - 知乎 (zhihu.com)(二阶求导那里手机打开可能排版有问题,可文末点击阅读原文)
01

PyTorch工作机制

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

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

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

数据可分为:叶子节点(leaf node)和非叶子节点;叶子节点是用户创建的节点,不依赖其它节点;它们表现出来的区别在于反向传播结束之后,非叶子节点的梯度会被释放掉,只保留叶子节点的梯度,这样就节省了内存。如果想要保留非叶子节点的梯度,可以使用retain_grad()方法。

torch.tensor 具有如下属性:

  • 查看 运算名称 grad_fn

  • 查看 是否可以求导 requires_grad

  • 查看 是否为叶子节点 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) # 第二种


一个简单的求导例子是:y=(x+1)*(x+2),计算∂y/∂x ,给定x=2,画出计算图

手算的话

02

使用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属性里。

03

使用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 这一梯度,完整的返回值其实是一个元组,保留第一个元素

04

二阶求导



再举一个复杂一点且高阶求导的例子:z=x^2*y,计算∂y/∂x,∂y/∂y,∂y^2/∂x^2,给定x=2,y=3

手算的话

求一阶导可以用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的导数呢?因为无论是backward还是autograd.grad在计算一次梯度后图就被释放了,如果想要保留,需要添加retain_graph=True

x = torch.tensor(2.).requires_grad_()y = torch.tensor(3.).requires_grad_()z = x * x * ygrad_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在保留原图的基础上再建立额外的求导计算图,也就是会把 ∂y/∂x=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(),相当于直接从 ∂y/∂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 = 12x.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>)




05

向量求导

有没有发现前面都是对标量求导,如果不是标量会怎么样呢?

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 = torch.tensor([1., 2.]).requires_grad_()y = x * x
y.sum().backward()print(x.grad)>>>tensor([2., 4.])

再具体一点来解释,让我们写出求导计算的雅可比矩阵, y=[y1,y2] 是一个向量

而我们希望最终求导的结果是[∂y1/∂x1,∂y2/∂x2],那怎么得到呢?注意 ∂y1/∂x2和 ∂y2/∂x1是0,那是不是

所以不用y.sum()的另一种方式是

x = torch.tensor([1., 2.]).requires_grad_()y = x * x
y.backward(torch.ones_like(y))print(x.grad)>>>tensor([2., 4.])

也可以使用autograd。上面和这里的torch.ones_like(y) 位置指的就是雅可比矩阵左乘的那个向量。

x = torch.tensor([1., 2.]).requires_grad_()y = x * x
grad_x = torch.autograd.grad(outputs=y, inputs=x, grad_outputs=torch.ones_like(y))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.])
06

需要注意的点

梯度清零

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 操作,因为反向传播时会访问原来的对象地址


反向传播可能比我们看到的那样还神奇,colah's blog里写的就非常好,后续再分享


END










 











 







 





                 

小马过河啊
要好好学习呀!
 最新文章