🌈 个人主页:十二月的猫-CSDN博客
🔥 系列专栏: 🏀PyTorch从入门到实践_十二月的猫的博客-CSDN博客💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光
目录
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的代码来实现这个过程:
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,是整个计算图的根基。之所以用叶子节点的概念,是为了减少内存,在反向传播结束之后,非叶子节点的梯度会被释放掉 , 我们依然用上面的例子解释:
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()
来保留非任意节点的梯度值:
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是通过加法得到的,使用加法的求导法则。
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
,指定方式主要有两种:
x = torch.tensor(1.).requires_grad_() # 第一种
x = torch.tensor(1., requires_grad=True) # 第二种
3. 求解梯度方式
3.1 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.)
结果:
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()
求解
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(),代码如下:
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.)
autograd.grad():
1、在这里只会保留inputs里面的值的梯度,其他都会在计算结束后释放
2、整个计算图会被释放,同时不允许再次被生成(如果后面还用到计算图中的量则需要retain-graph去手动保存)
如下面代码:
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
的运算方式,不能进一步高阶求导
# 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 开始回传
# 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.)
总结:
1、两种求导方法在二次求导中可以组合使用,但是注意backward的特点
2、pyTorch使用
backward()
时默认会累加梯度
错误实例:
# 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()
时默认会累加梯度,需要手动把前一次的梯度清零
修改后代码如下:
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. 向量求导
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.])
核心思想:将向量转变为标量
另一种方式:
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.])
6. 梯度清零
Pytorch 的自动求导梯度不会自动清零,会累积,所以一次反向传播后需要手动清零。
x.grad.zero_()
而在神经网络中,我们只需要执行
optimizer.zero_grad()
如果想要学习更多pyTorch的知识,大家可以点个关注并订阅,持续学习、天天进步
你的点赞就是我更新的动力,如果觉得对你有帮助,辛苦友友点个赞,收个藏呀~~~