【鼠鼠学AI代码合集#6】自动微分

时间:2024-10-07 07:13:58

简单的例子

在深度学习中,自动微分是一个关键技术,它通过构建计算图来简化求导过程。这个过程允许我们高效地计算模型参数的梯度,从而在优化中更新这些参数。

以简单例子为基础,考虑函数 ( y = 2x^\top x ) 的梯度计算。我们首先创建一个向量 ( x ) 并启用梯度计算:

import torch

x = torch.arange(4.0, requires_grad=True)

在计算 ( y ) 的值之前,我们需要确保有一个地方来存储梯度。计算 ( y ) 的值如下:

y = 2 * torch.dot(x, x)

接下来,通过调用 y.backward(),我们可以自动计算 ( y ) 关于 ( x ) 的梯度:

y.backward()
print(x.grad)  # 输出: tensor([ 0.,  4.,  8., 12.])

这个结果符合理论,因为 ( \frac{\partial y}{\partial x} = 4x )。

在计算其他函数时,记得清除之前的梯度值,以免累积:

x.grad.zero_()
y = x.sum()
y.backward()
print(x.grad)  # 输出: tensor([1., 1., 1., 1.])

代码总和

import torch

# 创建一个包含4个元素的向量x,并启用梯度计算
x = torch.arange(4.0, requires_grad=True)

# 计算函数y = 2 * (x的点积x)
y = 2 * torch.dot(x, x)

# 自动计算y关于x的梯度
y.backward()

# 打印x的梯度,应该是[0, 4, 8, 12]
print(x.grad)  # 输出: tensor([ 0.,  4.,  8., 12.])

# 验证梯度是否正确,应该等于4 * x
print(x.grad == 4 * x)  # 输出: tensor([True, True, True, True])

# 清除之前的梯度值,以防累积
x.grad.zero_()

# 计算新的函数y = x的和
y = x.sum()

# 自动计算新的y关于x的梯度
y.backward()

# 打印新的x的梯度,应该是[1, 1, 1, 1]
print(x.grad)  # 输出: tensor([1., 1., 1., 1.])

非标量变量的反向传播

在处理非标量变量的反向传播时,向量 ( y ) 关于向量 ( x ) 的导数通常被表示为一个矩阵。虽然在高级机器学习中可能会遇到高阶张量,但在实际应用中,我们通常计算批量中每个样本的损失函数的偏导数之和。

以下是一个示例代码,演示如何在非标量情况下进行反向传播:

import torch

# 创建一个向量x,并启用梯度计算
x = torch.arange(4.0, requires_grad=True)

# 检查x.grad是否为None
if x.grad is not None:
    x.grad.zero_()  # 清除之前的梯度

# 定义非标量函数y
y = x * x

# 通过调用y的总和进行反向传播
y.sum().backward()

# 打印x的梯度,输出每个元素的偏导数之和
print(x.grad)  # 输出: tensor([0., 2., 4., 6.])

在这里,我们使用 y.sum().backward() 来计算每个样本的偏导数和。这种方法简单有效,适用于处理批量样本的情况。

分离计算(Detaching Computation)

在深度学习中,有时我们希望在反向传播时,将某些计算部分视为常数,而不通过这些部分计算梯度。这种操作可以通过分离计算来实现,具体方法是使用 PyTorch 的 detach() 函数。这一操作非常有用,尤其是在一些需要定制梯度流动的场景中。

1. 背景:反向传播的计算图

在一般情况下,深度学习框架会记录所有的计算操作,构建一个计算图(Computational Graph),然后通过反向传播算法计算损失函数对模型参数的梯度。这意味着在反向传播过程中,梯度会沿着计算图从输出层向输入层传播。

2. 问题场景

假设我们有如下的计算关系:

  • ( y = x * x ) (这是 yx 计算得到的)
  • ( z = y * x ) (zyx 的函数)

如果我们按照默认的反向传播机制来计算 ( z ) 对 ( x ) 的梯度,梯度会通过 y 回溯到 x,考虑 ( y ) 的计算过程。然而,某些情况下,我们可能希望y 视为常数,而不追踪 y 的计算过程。在这种情况下,我们可以使用 detach() 来从计算图中分离 y

3. 分离计算的实现

步骤:

  1. 计算 y = x * x
  2. 使用 detach() 函数分离 y,生成新变量 u。此时,uy 有相同的值,但它不再依赖 x
  3. 计算 z = u * x,并进行反向传播。

代码示例:

import torch

# 创建一个向量 x 并启用梯度计算
x = torch.arange(4.0, requires_grad=True)

# 如果 x.grad 还没有被分配,先检查是否为 None
if x.grad is not None:
    x.grad.zero_()  # 清除之前的梯度

# 计算 y = x * x
y = x * x

# 将 y 从计算图中分离,生成新变量 u
u = y.detach()

# 计算 z = u * x,此时 u 不再与 x 的计算有关
z = u * x

# 进行反向传播
z.sum().backward()

# 打印计算得到的梯度
print(x.grad)

解释:

  • 通过 y.detach(),我们生成了一个与 y 数值相同的 u,但 ux 没有梯度关系。在计算 z = u * x 并进行反向传播时,u 被视为常数,因此梯度不会向前回溯到 y 的计算。
4. 后续梯度计算

虽然我们在上一步中将 y 分离了,导致梯度不会传递到 x,但是我们仍然可以在 y 上调用反向传播,重新计算它的梯度。因为 y 仍然与 x 有关联,计算图并没有完全销毁。

# 再次检查 x.grad 是否需要被清除
if x.grad is not None:
    x.grad.zero_()

# 在 y 上调用反向传播,得到 y = x * x 对 x 的导数
y.sum().backward()

# 检查梯度是否与 2 * x 相同
print(x.grad == 2 * x)  # 输出: tensor([True, True, True, True])

解释:

  • 这一步重新在 y 上调用 backward(),此时计算的是 ( y = x * x ) 对 ( x ) 的导数,即 ( \frac{dy}{dx} = 2x )。
  • 清除之前的梯度后,新的梯度值与预期的结果一致。
5. 总结
  • 分离计算(detach):通过 detach() 函数,我们可以将某些张量从计算图中分离,使其在反向传播时被视为常量,梯度不会回溯到这些分离的变量。
  • 灵活性:即便一个变量被 detach() 分离出来,我们仍然可以在后续的计算中对其调用 backward() 来计算它的梯度。
  • 应用场景:分离计算常用于需要控制梯度流动、实现自定义梯度更新策略或避免计算过于复杂的反向传播时。

通过这种方式,我们可以在复杂的深度学习模型中灵活控制计算图的构建和梯度的传播方向。