少女祈祷中...

关于自动求导机制以及优化器的工作原理. 主要是GPT的说明, 夹杂了一些自己的总结.

PyTorch 具有自动求导的机制, 使得我们在定义好神经网络和损失函数后, 只需要调用

1
2
3
optimizer.zero_grad()  
loss.backward()
optimizer.step()

就可以完成神经网络参数的更新, 非常地方便.

那么, 具体的工作原理是怎么样的呢? 为什么调用一个step就可以完成一步更新?

大致流程

  1. 对于tensor中的运算, PyTorch会自动的构建一个有向图(计算图), 记录他们的运算过程. 例如, 我们可以通过计算图对损失函数loss进行溯源, 一直追溯到 X, W之类的tensor量.
  2. 在调用 loss.backward()的时候, tensor就会自动完成这个溯源过程, 不断通过链式求导法则计算梯度, 并将相关的梯度存放在对应的tensor中, 例如, 将 LX\frac{\partial L}{\partial X} 存放在 X 中, 将 LW\frac{\partial L}{\partial W} 存放在 W 中等.
  3. 当然, 并不是所有的梯度都会被存放. 只有那些被创建时, 设置参数 requires_grad=True的张量才会存放梯度. 因此, 对于样本 X, 它们的梯度实际上是无法访问的; 而神经网络的各种参数, 如W, b, 它们属于torch.nn.Parameter对象, 因此天然会存储相应的梯度.
  4. 新计算出来的梯度会被加到之前已经有的梯度上(而不是将旧的结果清除). 因此, 在每次更新之前, 我们都需要运行 optimizer.zero_grad(), 将参数上旧的梯度清除. (当然, 有的时候梯度也不用清除, 而是让它们累加起来, 例如我们想运行10个minibatch, 然后将他们的梯度一起更新的情况. )
  5. 对于"一次运算过程", 它的梯度只能被求解一次. 例如下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
import torch  

# 创建需要求导的张量
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)

# 定义一个简单的损失函数
L = x^2 + y^2L = x**2 + y**2

# 反向传播计算梯度
L.backward()
L.backward()

就会报错. 同时, 为了说明梯度会进行累加的性质, 我们可以运行下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch  

# 创建需要求导的张量
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)
# 定义一个简单的损失函数 L = x^2 + y^2L = x**2 + y**2
# 反向传播计算梯度
L.backward()
# 输出 x 和 y 的梯度
print(x.grad) # 输出: tensor(4.)
print(y.grad) # 输出: tensor(6.)
L = x**2 + y**2
# 反向传播计算梯度
L.backward()
# 输出 x 和 y 的梯度
print(x.grad) # 输出: tensor(8.)
print(y.grad) # 输出: tensor(12.)

可以发现, 两次运算的梯度会累加起来.

因此, 我们就能够理解下面这三行代码的含义了

1
2
3
optimizer.zero_grad()  # 将paraments的梯度清零. 这些paraments是创建优化器时传递给它的
loss.backward() # pytorch根据计算图进行反向传播. 它会计算涉及到的所有量的梯度, 并存储那些应该存储的量(例如对于参数W和b的梯度)
optimizer.step() # 优化器根据paraments的梯度做梯度下降, 有可能是简单的SGD算法, 也可能是更复杂的Adam

当我们想要实现一些更复杂的功能, 例如, 在某些时候只对神经网络的一部分进行更新. 这时我们只需要为这些局部创建单独的局部优化器即可, 并单独地调用它们即可. 对于其他东西都不需要改动.

下面是GPT关于这部分内容的具体说明.


关于自动求导机制

PyTorch 的自动求导机制依赖于 计算图,它能够自动计算梯度。计算图是一个有向图,其中每个节点代表一个操作(如加法、乘法等),而边则代表数据流(张量)。每次执行一个操作时,PyTorch 会动态地构建这个计算图,记录每个操作的输入、输出以及操作本身。当你调用 .backward() 时,PyTorch 会根据计算图自动反向传播,计算梯度。

工作原理

  1. 张量(Tensor)与计算图:在 PyTorch 中,张量是支持自动求导的对象。一个张量如果被标记为 requires_grad=True,那么 PyTorch 会自动记录它的操作历史(即计算图)。
  2. 前向传播:每次做张量操作时,PyTorch 会记录这个操作并将其作为计算图的一部分。例如,如果你对两个张量做加法,PyTorch 会创建一个加法节点,记录输入和输出。
  3. 反向传播:当你调用 .backward() 时,PyTorch 会从输出节点开始,沿着计算图的反向方向传播误差(梯度)。在这个过程中,PyTorch 会自动为每个张量计算梯度,并存储在张量的 .grad 属性中。

举个简单的例子

假设我们有一个简单的标量损失函数 L = x^2 + y^2,其中 xy 是张量,我们想计算这个损失函数相对于 xy 的梯度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch

# 创建需要求导的张量
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)

# 定义一个简单的损失函数 L = x^2 + y^2
L = x**2 + y**2

# 反向传播计算梯度
L.backward()

# 输出 x 和 y 的梯度
print(x.grad) # 输出: tensor(4.)
print(y.grad) # 输出: tensor(6.)

解释:

  1. 创建张量xy 被定义为 requires_grad=True,这表示我们希望计算它们的梯度。
  2. 前向传播:我们计算了 L = x^2 + y^2。PyTorch 在这个过程中记录了计算图,知道 L 是由 xy 计算出来的。
  3. 反向传播:调用 L.backward() 后,PyTorch 会自动计算 Lxy 的梯度。具体来说:
    • dL/dx = 2 * x = 4.0
    • dL/dy = 2 * y = 6.0
  4. 查看梯度:可以通过 x.grady.grad 查看对应的梯度。

更复杂的例子

如果涉及更复杂的计算图,PyTorch 会自动处理梯度传播:

1
2
3
4
5
6
# 更复杂的计算
z = x * y
z.backward() # 计算 z 对 x 和 y 的梯度

print(x.grad) # 这里输出的将是 6,因为 dz/dx = y
print(y.grad) # 这里输出的将是 4,因为 dz/dy = x

自动求导的优势

  • 动态图:PyTorch 使用动态图机制(Eager Execution),每次前向传播都会动态构建计算图,因此更灵活,适合调试和修改。
  • 梯度链式法则:通过计算图,PyTorch 能够自动应用链式法则来计算复杂函数的梯度。
  • 无需手动计算梯度:在传统的机器学习方法中,需要手动计算梯度并实现反向传播,而在 PyTorch 中,这些都由自动求导机制完成。

通过这种方式,PyTorch 能够让你非常方便地进行梯度计算,简化深度学习中的优化过程。


神经网络中, 参数与优化器和自动求导的关系

在神经网络中,参数通常是指模型中的可学习的权重和偏置,这些参数是网络在训练过程中需要优化的目标。优化器则用于根据损失函数的梯度来更新这些参数,以最小化损失。PyTorch 的自动求导和优化器协同工作,使得神经网络能够高效地进行训练。

1. 神经网络中的参数

在 PyTorch 中,神经网络的参数通常是 nn.Module 类的成员变量,它们是 torch.nn.Parameter 对象。 torch.nn.Parameter 是一个特殊的张量,它被标记为 requires_grad=True,表示该参数需要计算梯度。

举个例子,假设你定义了一个简单的全连接层(Linear):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch
import torch.nn as nn

# 定义一个简单的神经网络
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
self.fc = nn.Linear(10, 5) # 输入维度10,输出维度5

def forward(self, x):
return self.fc(x)

# 创建网络实例
model = SimpleNN()

在这里,self.fc 是一个 全连接层(Linear Layer),它的参数是权重矩阵(weight)和偏置向量(bias)。PyTorch 会自动将这些参数封装为 torch.nn.Parameter 对象,这样它们就能够参与梯度计算和优化。

2. 自动求导的工作原理

当你执行前向传播(通过 model(x))时,PyTorch 会自动构建计算图,记录每个操作。例如,对于一个全连接层,前向传播是矩阵乘法与加法操作。PyTorch 会根据这些操作计算梯度并存储在计算图中。

当你调用 .backward() 时,PyTorch 会通过 反向传播 根据计算图自动计算所有参数的梯度。这些梯度被存储在各个张量的 .grad 属性中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 假设输入数据 x 和目标 y
x = torch.randn(1, 10) # 输入张量 (batch_size, input_dim)
y = torch.randn(1, 5) # 目标输出 (batch_size, output_dim)

# 前向传播
output = model(x)

# 定义损失函数并计算损失
loss_fn = nn.MSELoss() # 均方误差损失
loss = loss_fn(output, y)

# 反向传播
loss.backward()

# 查看参数的梯度
print(model.fc.weight.grad) # 权重的梯度
print(model.fc.bias.grad) # 偏置的梯度

3. 优化器的工作原理

优化器(Optimizer) 是用来根据计算出的梯度来更新神经网络的参数。优化器根据指定的算法(如 SGD、Adam 等)来调整每个参数,以最小化损失函数。

常见的优化器包括:

  • SGD(Stochastic Gradient Descent):基本的随机梯度下降算法。
  • Adam(Adaptive Moment Estimation):一种更复杂的优化算法,结合了动量和自适应学习率。

使用优化器的步骤:

  1. 创建优化器:将模型的参数传给优化器。
  2. 更新参数:在每次训练循环中,通过 .zero_grad() 清除旧的梯度,进行一次前向传播计算损失,调用 .backward() 计算梯度,然后使用优化器的 .step() 更新参数。

代码示例

1
2
3
4
5
6
7
8
9
10
# 使用 Adam 优化器
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

# 训练循环
for epoch in range(100): # 假设训练 100 个 epoch
optimizer.zero_grad() # 清除梯度
output = model(x) # 前向传播
loss = loss_fn(output, y) # 计算损失
loss.backward() # 反向传播,计算梯度
optimizer.step() # 更新参数

解释

  1. optimizer.zero_grad():每次进行反向传播前,我们需要清除上一次迭代的梯度信息,因为 PyTorch 会默认将梯度累积(以便支持 RNN 等类型的网络)。调用 zero_grad() 可以确保在每次迭代时梯度从零开始。
  2. loss.backward():执行反向传播,计算各个参数的梯度(存储在 .grad 属性中)。
  3. optimizer.step():使用计算出的梯度来更新网络中的参数。具体更新方法依赖于优化算法(比如 SGD、Adam 等)。

梯度更新过程

假设我们使用标准的随机梯度下降(SGD),参数的更新规则为:

[

θnew=θoldηLθ\theta_{new} = \theta_{old} - \eta \cdot \frac{\partial L}{\partial \theta}

]

其中:

  • (θold\theta_{old}) 是当前的参数,
  • (θnew\theta_{new}) 是更新后的参数,
  • (η\eta) 是学习率,
  • (Lθ\frac{\partial L}{\partial \theta}) 是损失函数相对于参数的梯度。

例如,使用 SGD 优化器时,PyTorch 会自动计算每个参数的梯度,并根据梯度和学习率来更新参数。

1
2
3
4
5
6
7
8
9
10
# 使用 SGD 优化器
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

# 训练过程
for epoch in range(100):
optimizer.zero_grad() # 清除梯度
output = model(x) # 前向传播
loss = loss_fn(output, y) # 计算损失
loss.backward() # 反向传播,计算梯度
optimizer.step() # 使用梯度更新参数

4. 总结

  • 参数是神经网络中的可学习对象,通常是权重和偏置,它们会随着训练不断更新。
  • 自动求导是通过计算图来自动计算损失函数对每个参数的梯度。
  • 优化器负责根据计算得到的梯度来更新参数,使得模型逐渐收敛,最小化损失函数。

PyTorch 的自动求导和优化器使得训练神经网络变得非常高效且简洁,极大地简化了模型训练过程中的梯度计算和参数更新。