关于自动求导机制以及优化器的工作原理. 主要是GPT的说明, 夹杂了一些自己的总结.
PyTorch 具有自动求导的机制, 使得我们在定义好神经网络和损失函数后, 只需要调用
1 | optimizer.zero_grad() |
就可以完成神经网络参数的更新, 非常地方便.
那么, 具体的工作原理是怎么样的呢? 为什么调用一个step就可以完成一步更新?
大致流程
- 对于tensor中的运算, PyTorch会自动的构建一个有向图(计算图), 记录他们的运算过程. 例如, 我们可以通过计算图对损失函数loss进行溯源, 一直追溯到
X, W
之类的tensor量. - 在调用
loss.backward()
的时候, tensor就会自动完成这个溯源过程, 不断通过链式求导法则计算梯度, 并将相关的梯度存放在对应的tensor中, 例如, 将 存放在X
中, 将 存放在W
中等. - 当然, 并不是所有的梯度都会被存放. 只有那些被创建时, 设置参数
requires_grad=True
的张量才会存放梯度. 因此, 对于样本X
, 它们的梯度实际上是无法访问的; 而神经网络的各种参数, 如W, b
, 它们属于torch.nn.Parameter
对象, 因此天然会存储相应的梯度. - 新计算出来的梯度会被加到之前已经有的梯度上(而不是将旧的结果清除). 因此, 在每次更新之前, 我们都需要运行
optimizer.zero_grad()
, 将参数上旧的梯度清除. (当然, 有的时候梯度也不用清除, 而是让它们累加起来, 例如我们想运行10个minibatch, 然后将他们的梯度一起更新的情况. ) - 对于"一次运算过程", 它的梯度只能被求解一次. 例如下面的代码
1 | import torch |
就会报错. 同时, 为了说明梯度会进行累加的性质, 我们可以运行下面的代码:
1 | import torch |
可以发现, 两次运算的梯度会累加起来.
因此, 我们就能够理解下面这三行代码的含义了
1 | optimizer.zero_grad() # 将paraments的梯度清零. 这些paraments是创建优化器时传递给它的 |
当我们想要实现一些更复杂的功能, 例如, 在某些时候只对神经网络的一部分进行更新. 这时我们只需要为这些局部创建单独的局部优化器即可, 并单独地调用它们即可. 对于其他东西都不需要改动.
下面是GPT关于这部分内容的具体说明.
关于自动求导机制
PyTorch 的自动求导机制依赖于 计算图,它能够自动计算梯度。计算图是一个有向图,其中每个节点代表一个操作(如加法、乘法等),而边则代表数据流(张量)。每次执行一个操作时,PyTorch 会动态地构建这个计算图,记录每个操作的输入、输出以及操作本身。当你调用 .backward()
时,PyTorch 会根据计算图自动反向传播,计算梯度。
工作原理
- 张量(Tensor)与计算图:在 PyTorch 中,张量是支持自动求导的对象。一个张量如果被标记为
requires_grad=True
,那么 PyTorch 会自动记录它的操作历史(即计算图)。 - 前向传播:每次做张量操作时,PyTorch 会记录这个操作并将其作为计算图的一部分。例如,如果你对两个张量做加法,PyTorch 会创建一个加法节点,记录输入和输出。
- 反向传播:当你调用
.backward()
时,PyTorch 会从输出节点开始,沿着计算图的反向方向传播误差(梯度)。在这个过程中,PyTorch 会自动为每个张量计算梯度,并存储在张量的.grad
属性中。
举个简单的例子
假设我们有一个简单的标量损失函数 L = x^2 + y^2
,其中 x
和 y
是张量,我们想计算这个损失函数相对于 x
和 y
的梯度。
1 | import torch |
解释:
- 创建张量:
x
和y
被定义为requires_grad=True
,这表示我们希望计算它们的梯度。 - 前向传播:我们计算了
L = x^2 + y^2
。PyTorch 在这个过程中记录了计算图,知道L
是由x
和y
计算出来的。 - 反向传播:调用
L.backward()
后,PyTorch 会自动计算L
对x
和y
的梯度。具体来说:dL/dx = 2 * x = 4.0
dL/dy = 2 * y = 6.0
- 查看梯度:可以通过
x.grad
和y.grad
查看对应的梯度。
更复杂的例子
如果涉及更复杂的计算图,PyTorch 会自动处理梯度传播:
1 | # 更复杂的计算 |
自动求导的优势
- 动态图:PyTorch 使用动态图机制(Eager Execution),每次前向传播都会动态构建计算图,因此更灵活,适合调试和修改。
- 梯度链式法则:通过计算图,PyTorch 能够自动应用链式法则来计算复杂函数的梯度。
- 无需手动计算梯度:在传统的机器学习方法中,需要手动计算梯度并实现反向传播,而在 PyTorch 中,这些都由自动求导机制完成。
通过这种方式,PyTorch 能够让你非常方便地进行梯度计算,简化深度学习中的优化过程。
神经网络中, 参数与优化器和自动求导的关系
在神经网络中,参数通常是指模型中的可学习的权重和偏置,这些参数是网络在训练过程中需要优化的目标。优化器则用于根据损失函数的梯度来更新这些参数,以最小化损失。PyTorch 的自动求导和优化器协同工作,使得神经网络能够高效地进行训练。
1. 神经网络中的参数
在 PyTorch 中,神经网络的参数通常是 nn.Module
类的成员变量,它们是 torch.nn.Parameter
对象。 torch.nn.Parameter
是一个特殊的张量,它被标记为 requires_grad=True
,表示该参数需要计算梯度。
举个例子,假设你定义了一个简单的全连接层(Linear
):
1 | import torch |
在这里,self.fc
是一个 全连接层(Linear Layer),它的参数是权重矩阵(weight
)和偏置向量(bias
)。PyTorch 会自动将这些参数封装为 torch.nn.Parameter
对象,这样它们就能够参与梯度计算和优化。
2. 自动求导的工作原理
当你执行前向传播(通过 model(x)
)时,PyTorch 会自动构建计算图,记录每个操作。例如,对于一个全连接层,前向传播是矩阵乘法与加法操作。PyTorch 会根据这些操作计算梯度并存储在计算图中。
当你调用 .backward()
时,PyTorch 会通过 反向传播 根据计算图自动计算所有参数的梯度。这些梯度被存储在各个张量的 .grad
属性中。
1 | # 假设输入数据 x 和目标 y |
3. 优化器的工作原理
优化器(Optimizer) 是用来根据计算出的梯度来更新神经网络的参数。优化器根据指定的算法(如 SGD、Adam 等)来调整每个参数,以最小化损失函数。
常见的优化器包括:
- SGD(Stochastic Gradient Descent):基本的随机梯度下降算法。
- Adam(Adaptive Moment Estimation):一种更复杂的优化算法,结合了动量和自适应学习率。
使用优化器的步骤:
- 创建优化器:将模型的参数传给优化器。
- 更新参数:在每次训练循环中,通过
.zero_grad()
清除旧的梯度,进行一次前向传播计算损失,调用.backward()
计算梯度,然后使用优化器的.step()
更新参数。
代码示例
1 | # 使用 Adam 优化器 |
解释
optimizer.zero_grad()
:每次进行反向传播前,我们需要清除上一次迭代的梯度信息,因为 PyTorch 会默认将梯度累积(以便支持 RNN 等类型的网络)。调用zero_grad()
可以确保在每次迭代时梯度从零开始。loss.backward()
:执行反向传播,计算各个参数的梯度(存储在.grad
属性中)。optimizer.step()
:使用计算出的梯度来更新网络中的参数。具体更新方法依赖于优化算法(比如 SGD、Adam 等)。
梯度更新过程
假设我们使用标准的随机梯度下降(SGD),参数的更新规则为:
[
]
其中:
- () 是当前的参数,
- () 是更新后的参数,
- () 是学习率,
- () 是损失函数相对于参数的梯度。
例如,使用 SGD 优化器时,PyTorch 会自动计算每个参数的梯度,并根据梯度和学习率来更新参数。
1 | # 使用 SGD 优化器 |
4. 总结
- 参数是神经网络中的可学习对象,通常是权重和偏置,它们会随着训练不断更新。
- 自动求导是通过计算图来自动计算损失函数对每个参数的梯度。
- 优化器负责根据计算得到的梯度来更新参数,使得模型逐渐收敛,最小化损失函数。
PyTorch 的自动求导和优化器使得训练神经网络变得非常高效且简洁,极大地简化了模型训练过程中的梯度计算和参数更新。