Appearance
自动求导(自动微分)
深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。 实际中,根据设计好的模型,系统会构建一个计算图(computational graph), 来跟踪计算是哪些数据通过哪些操作组合起来产生输出。 自动微分使系统能够随后反向传播梯度。 这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。
一、为什么需要自动微分
在深度学习里,训练模型的核心是更新参数,而更新参数需要计算梯度(也就是参数对损失函数的导数)。手工计算梯度特别麻烦,尤其是模型很复杂的时候,不仅容易算错,还会浪费很多时间。自动微分就是深度学习框架给我们提供的功能,它能帮我们自动计算梯度,不用我们自己动手算,大大提高了效率。
简单来说,自动微分就是框架帮我们 “自动求导” 的工具,它会跟踪数据的计算过程,然后顺着计算过程往回算,把每个参数的梯度都算出来。
二、自动微分的基本原理
自动微分的原理很简单:
框架会先构建一个计算图,就像记录计算流程的地图一样,跟踪数据是通过哪些操作一步步计算出结果的。
然后通过反向传播,顺着计算图往回走,计算每个参数对结果的梯度(也就是导数)。
三、计算简单函数的梯度
我们先从一个简单的函数开始,看看自动微分怎么用。比如我们想计算函数 y=2x⊤x 关于 x 的梯度,x 是一个长度为 4 的向量。
1. 为张量分配梯度内存
首先我们需要给 x 分配一块内存,用来存储计算出来的梯度,就像准备一个空盒子来装梯度结果:
python
import torch
# 创建一个长度为4的向量x
x = torch.tensor([0.0, 1.0, 2.0, 3.0], requires_grad=True)这里的requires_grad=True就是告诉框架,我们需要计算这个张量的梯度。
2. 记录计算过程
接下来我们需要记录 y 的计算过程,这样框架才能知道怎么反向传播:
python
# 记录计算过程,计算y
y = 2 * torch.dot(x, x)这里的torch.dot(x, x)就是计算 x 和 x 的点积,也就是 x₁²+x₂²+x₃²+x₄²,然后乘以 2 得到 y。
3. 反向传播计算梯度
现在我们可以让框架反向传播,计算 y 关于 x 的梯度:
python
# 反向传播,计算梯度
y.backward()这时候,x 的梯度就被计算出来了,我们可以通过x.grad来查看:
python
print(x.grad)运行结果是tensor([ 0., 4., 8., 12.]),这个结果是对的,因为 y=2x⊤x 的梯度是 4x,x 是 [0,1,2,3],所以梯度就是 [0,4,8,12]。
4. 验证梯度是否正确
我们可以手动验证一下,看看框架算的梯度对不对:
python
print(x.grad == 4 * x)运行结果是tensor([True, True, True, True]),说明框架算的梯度是对的。
四、梯度的累积和清除
框架会自动累积梯度,也就是说,如果我们再计算一次梯度,新的梯度会加到原来的梯度上,这样就会出错。比如我们再计算一个新的函数 y=x.sum () 的梯度:
python
# 不清除原来的梯度,直接计算新的梯度
y = x.sum()
y.backward()
print(x.grad)运行结果是tensor([1., 5., 9., 13.]),这显然不对,因为 y=x.sum () 的梯度应该是 [1,1,1,1],但是框架把原来的梯度 [0,4,8,12] 和新的梯度 [1,1,1,1] 加起来了。
所以我们每次计算新的梯度之前,需要先清除原来的梯度:
python
# 清除原来的梯度
x.grad.zero_()
# 重新计算y=x.sum()的梯度
y = x.sum()
y.backward()
print(x.grad)现在运行结果是tensor([1., 1., 1., 1.]),就正确了。
五、非标量变量的反向传播
刚才我们计算的 y 是标量(一个数字),如果 y 是向量(多个数字),怎么计算梯度呢?比如 y=x*x,这时候 y 是一个和 x 一样长的向量:
python
# 清除原来的梯度
x.grad.zero_()
# 计算y=x*x,y是向量
y = x * x
# 反向传播,框架会默认对y求和,然后计算梯度
y.sum().backward()
print(x.grad)运行结果是tensor([0., 2., 4., 6.]),这其实就是 y=x*x 求和后的梯度,也就是 2x,是对的。
如果你不想对 y 求和,也可以指定一个梯度参数,比如:
python
# 清除原来的梯度
x.grad.zero_()
# 计算y=x*x
y = x * x
# 手动指定梯度参数,这里用全1的向量,相当于对y求和
y.backward(torch.ones_like(y))
print(x.grad)运行结果和之前一样,也是tensor([0., 2., 4., 6.])。
六、分离计算:把部分计算当作常数
有时候我们希望把部分计算从计算图里分离出来,当作常数来处理。比如我们有 y=xx,然后 z=ux,我们希望把 u 当作常数,计算 z 关于 x 的梯度,这时候可以用detach()函数:
python
# 清除原来的梯度
x.grad.zero_()
# 计算y=x*x
y = x * x
# 把y分离出来,得到u,u的值和y一样,但是框架不会跟踪u的计算过程
u = y.detach()
# 计算z=u*x
z = u * x
# 反向传播计算梯度
z.sum().backward()
# 查看梯度,这时候梯度应该等于u,也就是x*x
print(x.grad == u)运行结果是tensor([True, True, True, True]),说明框架把 u 当作常数来计算梯度了,梯度就是 u,也就是 x*x。
如果我们不分离 y,直接计算 z=xxx 的梯度,梯度应该是 3x²,和现在的结果不一样,这就是分离计算的作用。
七、控制流的梯度计算
自动微分还有一个很厉害的功能:即使函数里有条件、循环这些控制流,它也能正确计算梯度。比如我们定义一个函数 f (a),里面有 while 循环和 if 判断:
python
def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c这个函数的计算过程取决于 a 的值,但是我们还是可以用自动微分计算梯度:
python
# 创建一个随机的a
a = torch.randn(size=(), requires_grad=True)
# 计算f(a)
d = f(a)
# 反向传播计算梯度
d.backward()
# 验证梯度是否正确,因为f(a)其实是k*a,所以梯度就是d/a
print(a.grad == d / a)运行结果是tensor(True),说明自动微分正确计算了梯度,即使函数里有循环和条件判断。
八、小结
自动微分是框架帮我们自动计算梯度的工具,能大大提高训练模型的效率,不用我们手动求导。
使用自动微分的时候,需要先给张量设置
requires_grad=True,告诉框架我们需要计算这个张量的梯度。计算梯度的时候,先记录计算过程,然后调用
backward()函数反向传播,就可以得到梯度了。框架会累积梯度,所以每次计算新的梯度之前,需要用
zero_()函数清除原来的梯度。即使函数里有控制流(比如循环、条件判断),自动微分也能正确计算梯度。
(注:文档部分内容可能由 AI 生成) 源地址