pytorch之自动求导
在 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()
在训练神经网络时,通常会计算某个损失函数的值,然后我们需要通过这个损失来计算每个参数的梯度,从而更新参数的值。这种计算过程称为 反向传播。
- 你定义一个输出
y
(比如损失)。 - 使用
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. 详细计算过程
为了更好地理解梯度的计算过程,我们可以手动推导一下:
-
计算
y = x ** 2
的梯度:y[0] = 1.0
,x[0] = 1.0
,所以dy[0]/dx[0] = 2 * x[0] = 2 * 1.0 = 2.0
。y[1] = 4.0
,x[1] = 2.0
,所以dy[1]/dx[1] = 2 * x[1] = 2 * 2.0 = 4.0
。
-
计算
z = sin(y)
的梯度:z[0] = sin(1.0) = 0.8415
,cos(1.0) = 0.5403
,所以dz[0]/dy[0] = cos(y[0]) = 0.5403
。z[1] = sin(4.0) = 0.9093
,cos(4.0) = -0.6536
,所以dz[1]/dy[1] = cos(y[1]) = -0.6536
。
-
链式法则计算
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
。