1. 自动微分:让计算变得简单
在深度学习中,求导 是几乎所有优化算法的核心步骤。虽然在简单模型中,求导往往只需要一些基本的微积分技巧,但随着模型复杂度的增加,手动求导变得异常繁琐且容易出错。为了解决这个问题,自动微分(Automatic Differentiation,简称 AD)应运而生。自动微分能够高效地帮助我们计算复杂模型中的梯度,极大地简化了深度学习中的优化过程。
2. 自动微分是如何工作的?
当我们设计一个模型时,深度学习框架会构建一个计算图(Computational Graph)。这个图记录了从输入数据到输出结果的所有操作步骤。随后,通过 反向传播(Backpropagation),框架能够自动计算出目标函数关于模型参数的导数。这个过程就是 自动微分。
简单来说,反向传播会遍历整个计算图,并且通过链式法则依次计算每个参数的偏导数,从而更新模型参数。
3. 一个简单的例子
为了更好地理解自动微分,下面是一个简单的例子。假设我们有一个向量 ,想要计算一个关于 的函数,并求出其梯度。
- 首先,我们定义向量 :
import torch
x = torch.arange(4.0, requires_grad=True)
print(x) # tensor([0., 1., 2., 3.], requires_grad=True)
- 然后,我们定义函数 ,也就是 为 的自乘积再乘以 2:
y = 2 * torch.dot(x, x)
print(y) # tensor(28., grad_fn=<MulBackward0>)
- 我们现在对 进行 反向传播,并查看 关于 的梯度:
y.backward()
print(x.grad) # tensor([ 0., 4., 8., 12.])
手动验证一下这个梯度:对于函数 ,其梯度应该为 ,我们可以用代码验证这一点:
print(x.grad == 4 * x) # tensor([True, True, True, True])
4. 非标量输出的梯度
当函数的输出 不是标量时,反向传播计算的结果会是一个矩阵。这时,我们需要对输出进行进一步处理。
例如,假设我们有:
x = torch.arange(4.0, requires_grad=True)
print(x) # tensor([0., 1., 2., 3.], requires_grad=True)
y = x * x
print(y) # tensor([0., 1., 4., 9.], grad_fn=<MulBackward0>)
在这种情况下,输出 不是标量,而是一个向量。我们可以对其进行求和操作,然后计算梯度:
y.sum().backward()
print(x.grad) # tensor([0., 2., 4., 6.])
这表明每个元素的梯度与输入 的导数是一一对应的。
5. 分离计算图
在深度学习中,当我们进行反向传播时,系统会计算我们指定变量的梯度。这个过程依赖于一个计算图,它会记录从输入到输出的所有运算。这让我们可以在反向传播时追踪变量之间的依赖关系,自动计算每个参数的导数。
然而,有些情况下我们并不希望所有计算都记录在计算图中。比如,在某个计算步骤中,我们不希望“追踪”变量的变化——我们想把它当作一个常量来使用,而不是用来计算梯度的变量。这时,就可以用“分离计算图”的方法,把计算图中的一部分“切断”,让它不参与反向传播。
为什么要分离?
- 计算效率:减少不必要的计算,节省资源。
- 灵活性:在复杂模型中,可能需要对某些变量的梯度进行手动控制或不计算梯度,以便实现特定需求。
假设我们有一个向量 ,我们想计算两个变量 和 ,并在某些情况下只对 的一部分操作求导。
- 初始化向量
import torch
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) # 向量 x
- 计算 和
- 计算
- 分离 :我们通过
.detach()把 从计算图中“分离”出来,生成一个新的变量 。
y = x * x
print(y) # tensor([1., 4., 9.], grad_fn=<MulBackward0>)
u = y.detach() # 将 y 的值分离到 u 中,u 不会记录计算图信息
- 计算
# 现在我们用分离后的 𝑢 来计算 𝑧,得到:
z = u * x
print(z) # tensor([ 1., 8., 27.], grad_fn=<MulBackward0>)
- 反向传播
当我们调用 z.sum().backward() 时,计算图只会对 的导数进行计算,并且在计算
的时候,不会追踪 是如何通过 计算得来的。
z.sum().backward()
print(x.grad) # tensor([1., 4., 9.])
print(x.grad == u) # tensor([True, True, True])
结果:反向传播过程中,梯度只会在 处停止,不会计算 对 的影响。
- 不分离时的情况
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) # 向量 x
y = x * x
print(y) # tensor([1., 4., 9.], grad_fn=<MulBackward0>)
u = y.clone() # 这里并没有分离出 u
# 现在我们用未分离的 𝑢 来计算 𝑧,得到:
z = y * x
print(z) # tensor([ 1., 8., 27.], grad_fn=<MulBackward0>)
z.sum().backward()
print(x.grad) # tensor([ 3., 12., 27.])
6. 控制流中的自动微分
自动微分的强大之处在于,即使我们的函数包含复杂的控制流结构(如条件判断或循环),我们依然能够计算出梯度。来看一个使用 while 循环的例子:
import torch
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
torch.manual_seed(49)
a = torch.randn(size=(), requires_grad=True)
print(a) # tensor(-2.0157, requires_grad=True)
d = f(a)
print(d) # tensor(-103203.8359, grad_fn=<MulBackward0>)
d.backward()
print(a.grad) # tensor(51200.)
我们现在可以分析上面定义的 函数。 请注意,它在其输入 中是分段线性的。 换言之,对于任何 ,存在某个常量标量 ,使得 ,其中 的值取决于输入 ,因此可以用 验证梯度是否正确。
print(a.grad == d / a) # tensor(True)
小结
通过自动微分,我们能够极大地简化深度学习中的梯度计算。无论是复杂的计算图,还是带有控制流的函数,自动微分都能轻松应对。这使得我们可以专注于模型设计,而不必担心繁琐的数学推导。