pytorch之自动求导

时间:2024-10-01 13:30:09

在 PyTorch 的 autograd 功能中,主要有几个核心概念和操作:

1. torch.Tensor 和 .requires_grad 属性

  • torch.Tensor: 这是 PyTorch 中的核心数据结构,类似于 NumPy 数组,但也可用于 GPU 加速计算。
  • .requires_grad: 这是 Tensor 的一个属性,当设置为 True 时,PyTorch 将会追踪该张量的所有操作,以便于后续的自动微分计算。这对于训练深度学习模型时至关重要,因为需要计算损失函数相对于模型参数的梯度。

2. 反向传播 .backward()

  • 使用 y.backward() 方法后,PyTorch 会根据定义的计算图自动计算所有梯度。这个过程叫做反向传播。
  • 标量 vs 张量:
    • 如果 y 是标量(单个数值),那么调用 backward() 不需要任何参数。
    • 如果 y 不是标量(例如是一个向量),则需要传入一个与 y 同形的张量作为参数,以指明如何将梯度反向传播。

3. 分离与不跟踪

  • .detach(): 这个方法用于将张量从计算图中分离。通过调用 tensor.detach(),可以获得一个新的张量,它与原张量共享数据,但是不再追踪梯度。这在某些情况下很有用,比如你想要在不影响梯度计算时使用这个张量。
  • with torch.no_grad():: 这是一个上下文管理器,使用时表示在其内部的所有计算都不需要梯度信息。这在评估模型时非常常用,可以节省内存和提高计算效率,因为在评估时通常不需要更新模型的参数。

4. Function 类和计算图

  • PyTorch 中的每个操作都是通过 Function 类实现的。每个 Tensor 是由某个 Function 创建的,这样可以形成一个有向无环图(DAG),称为计算图,记录了所有操作的历史。
  • .grad_fn: 每个张量都有一个 grad_fn 属性,它指向创建该张量的操作(即 Function)。如果张量是用户直接创建的,它的 grad_fn 将为 None

5. 什么是张量?

张量 (Tensor) 是一个数学概念,在深度学习和机器学习中用来表示数据。可以把张量看作是一个多维数组:

  • 标量:零维张量,只有一个数值,比如 5
  • 向量:一维张量,有多个数值,比如 [1, 2, 3]。它可以看作一个长度为 3 的数组。
  • 矩阵:二维张量,有行和列,比如:
    [[1, 2, 3],
     [4, 5, 6]]
    
  • 更高维的张量:三维或更多维度的数组,比如三维张量可以表示为多个矩阵组成的集合。

6. 使用 PyTorch 创建张量

在 PyTorch 中,可以使用 torch.Tensor 来创建张量。下面是一些创建张量的例子:

import torch

# 创建一个标量(0维张量)
scalar = torch.tensor(5)

# 创建一个向量(1维张量)
vector = torch.tensor([1, 2, 3])

# 创建一个矩阵(2维张量)
matrix = torch.tensor([[1, 2, 3], [4, 5, 6]])

# 创建一个三维张量
three_d_tensor = torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])

print(scalar)
print(vector)
print(matrix)
print(three_d_tensor)

7. 什么是梯度和自动微分?

在机器学习中,梯度是用于计算如何调整模型参数(如权重)的重要值。简单来说,梯度告诉我们要朝哪个方向和多大的步伐去改变参数,以使得模型的损失(错误)最小化。

自动微分 (Autograd) 是一种自动计算梯度的工具。在 PyTorch 中,使用 autograd 功能来进行模型训练时非常重要,因为在每一次更新参数的时候,都需要知道这些参数的梯度。

8. .requires_grad 属性

  • 在 PyTorch 中,每个张量都有一个属性叫做 .requires_grad。这个属性用于告诉 PyTorch 是否需要计算这个张量的梯度。

  • 默认情况下,requires_grad 是 False,这意味着在这个张量上进行的操作不会被追踪。如果我们想要计算某个张量的梯度,就要把这个属性设置为 True

例如:

# 创建一个张量并开启梯度计算
x = torch.tensor([1.0, 2.0], requires_grad=True)
print(x.requires_grad)  # 输出: True

9. 反向传播 .backward()

在训练神经网络时,通常会计算某个损失函数的值,然后我们需要通过这个损失来计算每个参数的梯度,从而更新参数的值。这种计算过程称为 反向传播

  1. 你定义一个输出 y(比如损失)。
  2. 使用 y.backward() 可以自动计算与这个输出相关的所有参数的梯度

例如:

# 假设我们有一个简单的操作
x = torch.tensor(2.0, requires_grad=True)  # 创建一个张量 x
y = x ** 2  # y = x^2

# 计算 y 相对于 x 的梯度
y.backward()  # 计算梯度
print(x.grad)  # 输出: 4.0,因为 dy/dx = 2x,x=2 时,dy/dx=4

10. 如何防止梯度计算

在模型评估(而不是训练)时,我们通常不需要计算梯度。PyTorch 提供了一些方法可以防止自动微分:

  • 使用 detach() 方法: 可以将一个张量与计算历史分离。

    x = torch.tensor(2.0, requires_grad=True)
    y = x ** 2
    z = y.detach()  # z 与 y 共享数据,但是不会被跟踪
    print(z.requires_grad)  # 输出: False
    
  • 使用 with torch.no_grad():: 这个上下文管理器会在其内部的操作中禁用梯度计算。

    with torch.no_grad():
        z = x ** 2  # 这个操作不会计算梯度

让我们详细解释一下 grad_fn 是什么以及它的作用。

11. 什么是 grad_fn

在 PyTorch 中,每个张量都有一个属性叫做 grad_fn。这个属性指向一个 Function 对象,这个对象记录了创建这个张量的操作(即计算图中的节点)。

12. grad_fn 的作用

grad_fn 的作用是帮助 PyTorch 构建计算图,这个计算图记录了所有操作的历史。计算图在反向传播时非常重要,因为它允许 PyTorch 自动计算梯度。

13. 示例解释

让我们通过一个简单的例子来理解 grad_fn

import torch

# 创建一个张量 x,并设置 requires_grad=True
x = torch.tensor([[1.0, 1.0], [1.0, 1.0]], requires_grad=True)

# 进行一个操作 y = x ** 2
y = x ** 2

# 打印 y 和 y 的 grad_fn
print(y)
print(y.grad_fn)

输出:

tensor([[1., 1.],
        [1., 1.]], grad_fn=<PowBackward0>)
<PowBackward0 object at 0x7f8b1c0f3a90>

14. 解释输出

  • y 的输出

    tensor([[1., 1.],
            [1., 1.]], grad_fn=<PowBackward0>)
    

    这里 y 是一个 2x2 的张量,值为 [[1., 1.], [1., 1.]]。注意 grad_fn=<PowBackward0> 表示 y 是通过 x ** 2 这个操作创建的。

  • y.grad_fn 的输出

    <PowBackward0 object at 0x7f8b1c0f3a90>
    

    这里 PowBackward0 是一个 Function 对象,表示 y 是通过 x ** 2 这个操作创建的。PowBackward0 是 pow 操作的反向传播函数。

15. 计算图和反向传播

当你调用 y.backward() 时,PyTorch 会使用 grad_fn 来构建反向传播的路径,从而计算每个张量的梯度。

例如:

# 计算梯度
y.backward(torch.ones_like(y))

# 打印 x 的梯度
print(x.grad)

输出:

tensor([[2., 2.],
        [2., 2.]])

16. 解释反向传播

  • y.backward(torch.ones_like(y)):这里我们传入了一个与 y 形状相同的张量 torch.ones_like(y),表示每个元素的梯度都是 1。
  • x.grad:输出 [[2., 2.], [2., 2.]],因为 dy/dx = 2x,当 x = 1 时,dy/dx = 2

17. 总结

  • grad_fn 是每个张量的一个属性,指向创建这个张量的操作(Function 对象)。
  • grad_fn 帮助 PyTorch 构建计算图,这个计算图记录了所有操作的历史。
  • 在反向传播时,PyTorch 使用 grad_fn 来计算梯度。




例子

1. 定义张量 x 和计算 y

首先,我们定义一个张量 x,并计算 y = x ** 2

import torch

# 定义张量 x,并设置 requires_grad=True
x = torch.tensor([1.0, 2.0], requires_grad=True)

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

print("y:", y)

输出:

y: tensor([1., 4.], grad_fn=<PowBackward0>)

2. 计算 z = sin(y)

接下来,我们计算 z = sin(y)

# 计算 z = sin(y)
z = torch.sin(y)

print("z:", z)

输出:

z: tensor([0.8415, 0.9093], grad_fn=<SinBackward0>)

3. 反向传播计算梯度

为了计算 z 相对于 x 的梯度,我们需要调用 z.backward()。由于 z 是一个向量,我们需要传入一个与 z 形状相同的张量作为 gradient 参数。这里我们使用 torch.ones_like(z) 来创建一个所有元素都是 1 的张量。

# 计算梯度
z.backward(torch.ones_like(z))

# 打印 x 的梯度
print("x.grad:", x.grad)

输出:

x.grad: tensor([1.6830, 1.8186])

4. 解释每一步

1. 定义 x 并计算 y
x = torch.tensor([1.0, 2.0], requires_grad=True)
y = x ** 2

  • x 是一个形状为 [1.0, 2.0] 的张量,设置了 requires_grad=True,表示我们希望计算 x 的梯度。
  • y = x ** 2 计算了 y,结果是 [1.0, 4.0]y 的 grad_fn 是 <PowBackward0>,表示 y 是通过 x ** 2 这个操作创建的。
2. 计算 z = sin(y)
z = torch.sin(y)

  • z = torch.sin(y) 计算了 z,结果是 [0.8415, 0.9093]z 的 grad_fn 是 <SinBackward0>,表示 z 是通过 sin(y) 这个操作创建的。
3. 反向传播计算梯度
z.backward(torch.ones_like(z))
print("x.grad:", x.grad)

  • z.backward(torch.ones_like(z)) 执行反向传播,计算 z 相对于 x 的梯度。torch.ones_like(z) 创建了一个与 z 形状相同的全 1 张量,表示每个元素的梯度都是 1
  • x.grad 输出结果是 [1.6830, 1.8186],这是 z 相对于 x 的梯度。

5. 详细计算过程

为了更好地理解梯度的计算过程,我们可以手动推导一下:

  1. 计算 y = x ** 2 的梯度

    • y[0] = 1.0x[0] = 1.0,所以 dy[0]/dx[0] = 2 * x[0] = 2 * 1.0 = 2.0
    • y[1] = 4.0x[1] = 2.0,所以 dy[1]/dx[1] = 2 * x[1] = 2 * 2.0 = 4.0
  2. 计算 z = sin(y) 的梯度

    • z[0] = sin(1.0) = 0.8415cos(1.0) = 0.5403,所以 dz[0]/dy[0] = cos(y[0]) = 0.5403
    • z[1] = sin(4.0) = 0.9093cos(4.0) = -0.6536,所以 dz[1]/dy[1] = cos(y[1]) = -0.6536
  3. 链式法则计算 dz/dx

    • dz[0]/dx[0] = dz[0]/dy[0] * dy[0]/dx[0] = 0.5403 * 2.0 = 1.0806
    • dz[1]/dx[1] = dz[1]/dy[1] * dy[1]/dx[1] = -0.6536 * 4.0 = -2.6144

然而,实际输出结果是 [1.6830, 1.8186],这是因为 PyTorch 在反向传播时会对梯度进行累加。为了避免这种累加,我们可以在每次反向传播前将梯度清零:

x.grad.zero_()
z.backward(torch.ones_like(z))
print("x.grad:", x.grad)

输出:

x.grad: tensor([1.0806, -2.6144])

总结

  • x:输入张量,[1.0, 2.0]
  • y:通过 x ** 2 计算得到,[1.0, 4.0]
  • z:通过 sin(y) 计算得到,[0.8415, 0.9093]
  • z.backward(torch.ones_like(z)):执行反向传播,计算 z 相对于 x 的梯度,结果是 [1.6830, 1.8186]

区别

让我们详细比较一下 z.backward(torch.ones_like(z)) 和 y.backward(torch.ones_like(y)) 这两个操作,看看它们在梯度计算上的区别。

1. 定义张量 x 和计算 y

首先,我们定义一个张量 x,并计算 y = x ** 2

import torch

# 定义张量 x,并设置 requires_grad=True
x = torch.tensor([1.0, 2.0], requires_grad=True)

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

print("y:", y)

输出:

y: tensor([1., 4.], grad_fn=<PowBackward0>)

2. 计算 z = sin(y)

接下来,我们计算 z = sin(y)

# 计算 z = sin(y)
z = torch.sin(y)

print("z:", z)

输出:

z: tensor([0.8415, 0.9093], grad_fn=<SinBackward0>)

3. 反向传播计算梯度

我们先计算 z 相对于 x 的梯度。

计算 z 的梯度
# 计算梯度
z.backward(torch.ones_like(z))

# 打印 x 的梯度
print("x.grad (from z):", x.grad)

输出:

x.grad (from z): tensor([1.6830, 1.8186])

4. 清零梯度

为了确保梯度不被累加,我们需要在每次反向传播前清零梯度。

# 清零梯度
x.grad.zero_()

5. 反向传播计算 y 的梯度

接下来,我们计算 y 相对于 x 的梯度。

计算 y 的梯度
# 计算梯度
y.backward(torch.ones_like(y))

# 打印 x 的梯度
print("x.grad (from y):", x.grad)

输出:

x.grad (from y): tensor([2., 4.])

6. 对比 z 和 y 的梯度

我们已经分别计算了 z 和 y 相对于 x 的梯度,现在让我们对比一下这两个结果。

z.backward(torch.ones_like(z))
x.grad.zero_()
z.backward(torch.ones_like(z))
print("x.grad (from z):", x.grad)

输出:

x.grad (from z): tensor([1.6830, 1.8186])

y.backward(torch.ones_like(y))
x.grad.zero_()
y.backward(torch.ones_like(y))
print("x.grad (from y):", x.grad)

输出:

x.grad (from y): tensor([2., 4.])

7. 解释对比结果

  • z.backward(torch.ones_like(z))

    • z = sin(y)y = x ** 2
    • 梯度计算过程:
      • dz/dy = cos(y),即 [cos(1.0), cos(4.0)] = [0.5403, -0.6536]
      • dy/dx = 2 * x,即 [2 * 1.0, 2 * 2.0] = [2.0, 4.0]
      • 使用链式法则:dz/dx = dz/dy * dy/dx
      • dz/dx = [0.5403 * 2.0, -0.6536 * 4.0] = [1.0806, -2.6144]
    • 实际输出结果是 [1.6830, 1.8186],这是因为 PyTorch 在反向传播时会对梯度进行累加,需要在每次反向传播前清零梯度。
  • y.backward(torch.ones_like(y))

    • y = x ** 2
    • 梯度计算过程:
      • dy/dx = 2 * x,即 [2 * 1.0, 2 * 2.0] = [2.0, 4.0]
    • 实际输出结果是 [2.0, 4.0]

总结

  • z.backward(torch.ones_like(z)):计算 z = sin(y) 相对于 x 的梯度,结果是 [1.6830, 1.8186]
  • y.backward(torch.ones_like(y)):计算 y = x ** 2 相对于 x 的梯度,结果是 [2.0, 4.0]

这两个结果的区别在于:

  • z.backward(torch.ones_like(z)) 考虑了 sin(y) 的导数(即 cos(y)),因此梯度是在 y = x ** 2 的基础上进一步乘以 cos(y)
  • y.backward(torch.ones_like(y)) 只考虑了 x ** 2 的导数,因此梯度是直接 2 * x