【PyTorch入门·求导相关】一文解释 PyTorch的求导 (backward、autograd.grad)

🌈个人主页: 十二月的猫-CSDN博客

🔥 系列专栏: 🏀PyTorch入门宝典_十二月的猫的博客-CSDN博客

💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光

目录

[1. 动态图与静态图](#1. 动态图与静态图)

[静态图(如 TensorFlow 1.x)](#静态图(如 TensorFlow 1.x))

[动态图(如 PyTorch)](#动态图(如 PyTorch))

[2. PyTorch中的计算图(动态图)](#2. PyTorch中的计算图(动态图))

[2.1 动态图的初步推导](#2.1 动态图的初步推导)

[2.2 动态图的叶子节点](#2.2 动态图的叶子节点)

[2.2.1 retain_grad()](#2.2.1 retain_grad())

[2.2.2 grad_fn](#2.2.2 grad_fn)

[2.3 张量求导属性总结](#2.3 张量求导属性总结)

[3. 求解梯度方式](#3. 求解梯度方式)

[3.1 backward求解](#3.1 backward求解)

[3.2 autograd.grad()求解](#3.2 autograd.grad()求解)

[4 二阶求导](#4 二阶求导)

[4.1 计算图、中间变量梯度和高阶求导](#4.1 计算图、中间变量梯度和高阶求导)

[4.1.1 计算图、中间变量梯度](#4.1.1 计算图、中间变量梯度)

retain_graph=True

[4.1.2 高阶求导](#4.1.2 高阶求导)

create_graph=True

[4.2 二阶求导](#4.2 二阶求导)

[5. 向量求导](#5. 向量求导)

[6. 梯度清零](#6. 梯度清零)


1. 动态图与静态图

PyTorch是动态图,即计算图的搭建和运算是同时的,随时可以输出结果;而TensorFlow是静态图,即先搭建计算图,后将需要求解梯度的数值代入得到结果

静态图(如 TensorFlow 1.x)

优点

  • 性能优化:静态图在构建时就可以进行优化,例如内存优化和计算图融合,提高了运行效率。
  • 部署稳定性:由于计算图在运行前已完全定义,部署时的行为更可预测,适用于生产环境。

缺点

  • 开发复杂性:调试和开发更为复杂,因为需要先定义整个计算图,然后执行。这可能会使得错误难以定位和修复。
  • 灵活性差:一旦定义了计算图,动态改变图结构较困难,不适合需要动态变化的计算任务。

动态图(如 PyTorch)

优点

  • 灵活性高:图在运行时动态创建,使得模型结构可以根据输入动态调整,适合需要动态计算的任务。
  • 调试友好:因为图是在运行时动态生成的,调试时可以逐步执行和检查中间结果,使得错误定位更为直观。

缺点

  • 性能优化难度:动态图在运行时生成,因此可能难以进行一些图级优化,影响性能。
  • 部署挑战:相对于静态图,动态图在生产环境中的稳定性和优化程度可能不如静态图。

2. PyTorch中的计算图(动态图)

2.1 动态图的初步推导

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

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

数据可分为:叶子节点 (leaf node)和非叶子节点

  • 结点表示数据 ,如向量、矩阵、张量;
  • 边表示运算 ,如加减乘除卷积等;

上图是用计算图表示:

y=(x+w)∗(w+1)y=(x+w)∗(w+1)

其中呢,a=x+w ,b=w+1 , y=a∗b. (a和b是类似于中间变量的那种感觉。)

Pytorch在计算的时候,就会把计算过程用上面那样的动态图存储起来。现在我们计算一下y关于w的梯度:

(上面的计算中,w=1,x=2)

现在我们用Pytorch的代码来实现这个过程:

python 复制代码
import torch
w = torch.tensor([1.],requires_grad = True)
x = torch.tensor([2.],requires_grad = True)

a = w+x
b = w+1
y = a*b

y.backward()
print(w.grad)

结果为:

2.2 动态图的叶子节点

2.2.1 retain_grad()

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

这个图中的叶子节点,是w和x,是整个计算图的根基。之所以用叶子节点的概念,是为了减少内存,在反向传播结束之后,非叶子节点的梯度会被释放掉 , 我们依然用上面的例子解释:

python 复制代码
import torch
w = torch.tensor([1.],requires_grad = True)
x = torch.tensor([2.],requires_grad = True)

a = w+x
b = w+1
y = a*b

y.backward()
print(w.is_leaf,x.is_leaf,a.is_leaf,b.is_leaf,y.is_leaf)
print(w.grad,x.grad,a.grad,b.grad,y.grad)

运行结果是:

可以看到只有x和w是叶子节点,然后反向传播计算完梯度后(.backward()之后),只有叶子节点的梯度保存下来了。

当然也可以通过.retain_grad()来保留非任意节点的梯度值:

python 复制代码
import torch
w = torch.tensor([1.],requires_grad = True)
x = torch.tensor([2.],requires_grad = True)

a = w+x
a.retain_grad()
b = w+1
y = a*b

y.backward()
print(w.is_leaf,x.is_leaf,a.is_leaf,b.is_leaf,y.is_leaf)
print(w.grad,x.grad,a.grad,b.grad,y.grad)

运行结果:

2.2.2 grad_fn

torch.tensor有一个属性grad_fn,grad_fn的作用是记录创建该张量时所用的函数,这个属性反向传播的时候会用到。例如在上面的例子中,y.grad_fn=MulBackward0,表示y是通过乘法得到的。所以求导的时候就是用乘法的求导法则。同样的,a.grad=AddBackward0表示a是通过加法得到的,使用加法的求导法则。

python 复制代码
import torch
w = torch.tensor([1.],requires_grad = True)
x = torch.tensor([2.],requires_grad = True)

a = w+x
a.retain_grad()
b = w+1
y = a*b

y.backward()
print(y.grad_fn)
print(a.grad_fn)
print(w.grad_fn)

运行结果是:

2.3 张量求导属性总结

torch.tensor 具有如下属性:

  • 查看 是否可以求导requires_grad
  • 查看 运算名称 grad_fn
  • 查看 是否为叶子节点 is_leaf
  • 查看 导数值 grad

requires_grad 是 PyTorch 中一个重要的属性,用于指定一个张量是否需要计算梯度。

设置 requires_grad=True 使得该张量在执行操作时会记录操作历史,以便在调用 backward() 方法时计算梯度。

通常,输入数据的张量设置为 requires_grad=True,以便在训练过程中自动进行反向传播和梯度更新。

当我们想要对某个Tensor变量求梯度时,需要先指定requires_grad属性为True,指定方式主要有两种:

python 复制代码
x = torch.tensor(1.).requires_grad_() # 第一种

x = torch.tensor(1., requires_grad=True) # 第二种

3. 求解梯度方式

3.1 backward求解

python 复制代码
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.)

结果:

python 复制代码
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属性里

3.2 autograd.grad()求解

python 复制代码
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 这一梯度,完整的返回值其实是一个元组,保留第一个元素就行,后面元素是?

4 二阶求导

4.1 计算图、中间变量梯度和高阶求导

4.1.1 计算图、中间变量梯度

retain_graph=True

  • 作用 :在执行反向传播时,PyTorch 默认会释放计算图以节省内存。如果你需要对同一计算图进行多次反向传播(例如,计算多个梯度或进行一些需要重复反向传播的操作),你需要设置 retain_graph=True 来保留计算图。
  • 限制 :即使你设置了 retain_graph=True,它只保留了原始计算图及其计算结果,但不包括梯度的进一步计算(即梯度本身的计算图)。

上面仅仅需要y关于z的梯度,因此用retain_graph=True保留计算图和中间变量梯度即可

4.1.2 高阶求导

create_graph=True

  • 作用create_graph=True 使得在反向传播时创建一个新的计算图。这个新的计算图会包含原始计算图中的梯度的计算过程。这是为了支持更高阶的梯度计算,例如二阶导数。
  • 使用场景 :当你需要对梯度进行进一步的反向传播时,比如计算梯度的梯度(即二阶导数)时,就需要使用 create_graph=True

4.2 二阶求导

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

一阶导可以用backward(),代码如下:

python 复制代码
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()实现:

python 复制代码
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.)

autograd.grad():

1、在这里只会保留inputs里面的值的梯度,其他都会在计算结束后释放

2、整个计算图会被释放,同时不允许再次被生成(如果后面还用到计算图中的量则需要retain-graph去手动保存)

如下面代码:

python 复制代码
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求梯度,试一下看

python 复制代码
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的运算方式,不能进一步高阶求导

python 复制代码
# 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=2xy 开始回传

python 复制代码
# 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这个一阶导继续求导

python 复制代码
# 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.)

总结:

1、两种求导方法在二次求导中可以组合使用,但是注意backward的特点

2、pyTorch使用**backward()时默认会累加梯度**

错误实例:

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

修改后代码如下:

python 复制代码
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>)

5. 向量求导

python 复制代码
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 转变为标量,对分别求导没影响的就是求和。

python 复制代码
x = torch.tensor([1., 2.]).requires_grad_()
y = x * x

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

**核心思想:**将向量转变为标量

另一种方式:

python 复制代码
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) 位置指的就是雅可比矩阵左乘的那个向量。

python 复制代码
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.])

或者

python 复制代码
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.])

6. 梯度清零

Pytorch 的自动求导梯度不会自动清零,会累积,所以一次反向传播后需要手动清零。

x.grad.zero_()

而在神经网络中,我们只需要执行

optimizer.zero_grad()

如果想要学习更多pyTorch的知识,大家可以点个关注并订阅,持续学习、天天进步

你的点赞就是我更新的动力,如果觉得对你有帮助,辛苦友友点个赞,收个藏呀~~~

相关推荐
CV学术叫叫兽6 分钟前
快速图像识别:落叶植物叶片分类
人工智能·分类·数据挖掘
湫ccc15 分钟前
《Python基础》之pip换国内镜像源
开发语言·python·pip
hakesashou16 分钟前
Python中常用的函数介绍
java·网络·python
菜鸟的人工智能之路26 分钟前
极坐标气泡图:医学数据分析的可视化新视角
python·数据分析·健康医疗
菜鸟学Python27 分钟前
Python 数据分析核心库大全!
开发语言·python·数据挖掘·数据分析
小白不太白95028 分钟前
设计模式之 责任链模式
python·设计模式·责任链模式
WeeJot嵌入式32 分钟前
卷积神经网络:深度学习中的图像识别利器
人工智能
喜欢猪猪34 分钟前
Django:从入门到精通
后端·python·django
糖豆豆今天也要努力鸭40 分钟前
torch.__version__的torch版本和conda list的torch版本不一致
linux·pytorch·python·深度学习·conda·torch
脆皮泡泡41 分钟前
Ultiverse 和web3新玩法?AI和GameFi的结合是怎样
人工智能·web3