《动手学深度学习 Pytorch版》 2.5 自动微分
2.5.1 一个简单的例子
import torch
假设我们函数 关于列向量 求导。
x = torch.arange(4.0)
x.requires_grad_(True) # 自动求导机制的必要参数 此处两句等效于 x=torch.arange(4.0,requires_grad=True)
x, x.grad
(tensor([0., 1., 2., 3.], requires_grad=True), None)
y = 2 * torch.dot(x, x) # 计算y
y
tensor(28., grad_fn=<MulBackward0>)
y.backward() # 调用反向传播函数自动计算关于 x 的每个分量的梯度
x.grad
tensor([ 0., 4., 8., 12.])
x.grad == 4 * x # 快速验证一下上面计算的梯度是否正确
tensor([True, True, True, True])
我对此的理解是 关于列向量 求导,实际上就是求 的导函数 在 、、、处的值,梯度为
不同于手工计算,先利用求导规则算出导函数 ,然后再分别将上述 值代入。
自动求导相当于把一个自变量 的不同值 、、、 作为不同的自变量 、、、 分别代入函数中再相加形成一个多元函数,也就是 ,再借助求偏导时可以把其他自变量当常数这一点去分别求偏导,又因为是加法,因此对当前变量求偏导时其他自变量求导为0。对于这个由加法生成的多元函数的梯度则为 ,刚好是一元函数的梯度组成的向量。
因此 y 最后一般都采取加法,例如本例和2.5.2中的例子实际上时一样的(就差个系数),因为2.5.2中的例子最后也要调用 sum 函数或者与全一同行矩阵做哈达玛积再求和,使非标量向量转换为标量向量。
'''再试一下另一个函数'''
x.grad.zero_() # 默认情况下会累积梯度,在求新梯度之前需要进行清除
y = x.sum() # 显然它的梯度应该是一串1
y.backward()
x.grad
tensor([1., 1., 1., 1.])
2.5.2 非标量变量的反向传播
PyTorch不让张量对张量求导,只允许标量对张量求导。因此,目标量对一个非标量调用 backward() 时需要传入一个 gradient 参数(该参数指定微分函数关于self的梯度,实际上就是 y 和 gradient 做一个点积,这里gradient 参数用的是全一向量,因此用sum是一样的),以使张量对张量的求导转换为标量对张量的求导。
x.grad.zero_()
y = x * x
y.sum().backward() # 等价于 y.backward(torch.ones(len(x)))
y, y.sum(), x.grad
(tensor([0., 1., 4., 9.], grad_fn=<MulBackward0>),
tensor(14., grad_fn=<SumBackward0>),
tensor([0., 2., 4., 6.]))
2.5.1已经论述了为何使用加法,在此参照此文写个例子实验一下。
此例分别计算 和 在点 、、 处的偏导数。
手工计算易得二者的梯度表达式分别是: 和 。
代入数据得:
在三个点的梯度分别为:、、.
在三个点的梯度分别为:、、
以下是两种计算方式。
m = torch.tensor([[2., 3.], [6., 7.], [4., 1.]], requires_grad=True) # 两个自变量的值列表
n = torch.zeros(m.shape[0], m.shape[1]) # 初始化目标张量
for i in range(m.shape[0]):
n[i][0] = m[i][0] ** 2 + 3 * m[i][1] # 定义映射关系1
n[i][1] = 2 * m[i][0] + m[i][1] ** 2 # 定义映射关系2
# retain_graph=True是为了方便多次反向传播
n.backward(torch.Tensor([[1, 0], [1, 0], [1, 0]]), retain_graph=True) # 此处相当于(n * torch.Tensor([[1, 0], [1, 0], [1, 0]])).sum().backward()
grad1 = m.grad.clone() # 暂存函数1的求导结果
m.grad.zero_()
n.backward(torch.Tensor([[0, 1], [0, 1], [0, 1]]), retain_graph=True) # 此处相当于(n * torch.Tensor([[0, 1], [0, 1], [0, 1]])).sum().backward()
torch.stack((grad1, m.grad)) # 扩维拼接
tensor([[[ 4., 3.],
[12., 3.],
[ 8., 3.]],
[[ 2., 6.],
[ 2., 14.],
[ 2., 2.]]])
m = torch.tensor([[2., 6., 4.], [3., 7., 1.]], requires_grad=True) # 两个自变量的值列表
n = torch.zeros(m.shape[0], m.shape[1]) # 初始化目标张量
n[0] = m[0] ** 2 + 3 * m[1] # 定义映射关系1
n[1] = 2 * m[0] + m[1] ** 2 # 定义映射关系2
# retain_graph=True是为了方便多次反向传播
n.backward(torch.Tensor([[1, 1, 1], [0, 0, 0]]), retain_graph=True) # 此处相当于(n * torch.Tensor([[1, 1, 1], [0, 0, 0]])).sum().backward()
grad1 = m.grad.clone() # 暂存函数1的求导结果
m.grad.zero_()
n.backward(torch.Tensor([[0, 0, 0], [1, 1, 1]]), retain_graph=True) # 此处相当于(n * torch.Tensor([[0, 0, 0], [1, 1, 1]])).sum().backward()
torch.stack((grad1, m.grad), dim=0) # 扩维拼接
tensor([[[ 4., 12., 8.],
[ 3., 3., 3.]],
[[ 2., 2., 2.],
[ 6., 14., 2.]]])
2.5.3 分离计算
假设 是关于 的函数,而 是关于 和 的函数,如果希望在计算 是关于 的梯度时将 视为一个常数且只考虑 在 计算后发挥作用,则可以分离 来返回一个新变量 ,该变量与 具有相同的值,但是丢弃了计算图中如何计算 的任何信息,梯度不会向后流经 到 .
如下例,计算 关于 的偏导数时将 作为常数处理,所以偏导数 ,而不是计算 关于 x 的导数
x.grad.zero_()
y = x * x
u = y.detach()
z= u * x
z.sum().backward()
x.grad == u
tensor([True, True, True, True])
# 随后再算y也不影响
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
tensor([True, True, True, True])
2.5.4 Python控制流的梯度计算
自动微分可以兼容一些需要使用Python控制流的复杂函数。
def f(a):
b = a * 2
while b.norm() < 1000:
b *= 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()
a, d, a.grad
(tensor(1.4756, requires_grad=True),
tensor(1510.9863, grad_fn=<MulBackward0>),
tensor(1024.))
虽然上述函数很复杂,但是我们至少知道它是线性的,也就是说 ,所以可以使用 验证梯度是否正确。
a.grad == d / a
tensor(True)
练习
(1)为什么计算二阶导数比一阶导数的开销要更大?
二阶那不得多导一遍嘛,肯定开销大。
(2)在运行反向传播函数之后,立即再次运行它,看看会发生什么。
# d.backward() # 会报 Trying to backward through the graph a second time 错,需要加 retain_graph=True
(3)在控制流的例子中,我们计算 关于 的导数,如果将变量 更改为随机向量或矩阵,会发生什么?
a = torch.randn(size=(1, 4), requires_grad=True) # 换向量的话给 d 调用一下 sum() 即可
d = f(a)
d.sum().backward()
a, d, a.grad, a.grad == d / a # 每个梯度都一样诶
(tensor([[ 0.4611, -1.3128, 0.9323, 0.0999]], requires_grad=True),
tensor([[ 472.1350, -1344.2858, 954.7211, 102.3088]],
grad_fn=<MulBackward0>),
tensor([[1024., 1024., 1024., 1024.]]),
tensor([[True, True, True, True]]))
a = torch.randn(size=(3, 4), requires_grad=True) # 换矩阵也是给 d 调用一下 sum() 即可
d = f(a)
d.sum().backward()
a, d, a.grad, a.grad == d / a # 每个梯度都一样诶
(tensor([[-0.6832, -0.9762, -0.9894, -1.1655],
[ 0.6858, 0.7037, -0.7619, -0.5135],
[ 0.0172, 1.4394, 1.6180, 0.8451]], requires_grad=True),
tensor([[-349.7996, -499.8079, -506.5559, -596.7241],
[ 351.1539, 360.2910, -390.0962, -262.9074],
[ 8.7919, 736.9765, 828.3993, 432.6812]], grad_fn=<MulBackward0>),
tensor([[512., 512., 512., 512.],
[512., 512., 512., 512.],
[512., 512., 512., 512.]]),
tensor([[True, True, True, True],
[True, True, True, True],
[True, True, True, True]]))
(4)重新设计一个求控制流梯度的例子,运行并分析结果。
试一下导数不存在点求导会怎样。
def g(x):
if x < 1: # 可以换 <= 试试
return x+1
else:
return 2*x
x = torch.tensor(1., requires_grad=True)
g1 = g(x)
g1.backward()
x.grad # 看来是不在意什么分段不分段的
tensor(2.)
def h(x): # y=|x|
return abs(x)
x1 = torch.tensor(0., requires_grad=True)
x2 = torch.tensor(1., requires_grad=True)
h1 = h(x1)
h2 = h(x2)
h1.backward()
h2.backward()
x1.grad, x2.grad # 在不可导点能求出来一个无意义值
(tensor(0.), tensor(1.))
(5)使 ,绘制 和 的图像,其中后者不使用 。
from d2l import torch as d2l # 方便调用前一节那个 plot 函数
x = torch.arange(-6.5, 6.5, 0.1, requires_grad=True)
y = torch.sin(x)
y.sum().backward()
# 注意,需要先用 tensor.detach().numpy() 把 tensor 转 array 才能使用
d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.detach().numpy()], 'x', 'f(x)', legend=['f(x)', "f'(x)"])
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了