Vik*_*Cat 14 python gradient-descent deep-learning pytorch
我试图理解PyTorch
. 我的问题与这两个有些相关:
为什么我们需要在 PyTorch 中调用 zero_grad()?
对第二个问题的已接受答案的评论表明,如果小批量太大而无法在单个前向传递中执行梯度更新,因此必须将其拆分为多个子批次,则可以使用累积梯度。
考虑以下玩具示例:
import numpy as np
import torch
class ExampleLinear(torch.nn.Module):
def __init__(self):
super().__init__()
# Initialize the weight at 1
self.weight = torch.nn.Parameter(torch.Tensor([1]).float(),
requires_grad=True)
def forward(self, x):
return self.weight * x
if __name__ == "__main__":
# Example 1
model = ExampleLinear()
# Generate some data
x = torch.from_numpy(np.array([4, 2])).float()
y = 2 * x
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
y_hat = model(x) # forward pass
loss = (y - y_hat) ** 2
loss = loss.mean() # MSE loss
loss.backward() # backward pass
optimizer.step() # weight update
print(model.weight.grad) # tensor([-20.])
print(model.weight) # tensor([1.2000]
Run Code Online (Sandbox Code Playgroud)
这正是人们所期望的结果。现在假设我们要利用梯度累积逐个样本处理数据集:
# Example 2: MSE sample-by-sample
model2 = ExampleLinear()
optimizer = torch.optim.SGD(model2.parameters(), lr=0.01)
# Compute loss sample-by-sample, then average it over all samples
loss = []
for k in range(len(y)):
y_hat = model2(x[k])
loss.append((y[k] - y_hat) ** 2)
loss = sum(loss) / len(y)
loss.backward() # backward pass
optimizer.step() # weight update
print(model2.weight.grad) # tensor([-20.])
print(model2.weight) # tensor([1.2000]
Run Code Online (Sandbox Code Playgroud)
再次如预期的那样,在.backward()
调用该方法时计算梯度。
最后我的问题是:“幕后”到底发生了什么?
我的理解是,计算图形被动态地更新从去<PowBackward>
到<AddBackward>
<DivBackward>
的操作loss
变量,并且没有关于用于每个直传数据信息的任何地方保留除了loss
可以直到向后通更新张量。
对上段中的推理有什么警告吗?最后,在使用梯度累积时是否有任何最佳实践可遵循(即我在示例 2 中使用的方法是否会适得其反)?
Mic*_*ngo 26
你实际上并不是在积累梯度。optimizer.zero_grad()
如果您只有一次.backward()
调用,则离开没有任何影响,因为梯度开始时已经为零(从技术上讲None
,它们会自动初始化为零)。
两个版本之间的唯一区别在于您如何计算最终损失。第二个示例的 for 循环与 PyTorch 在第一个示例中执行的计算相同,但是您单独执行它们,并且 PyTorch 无法优化(并行和矢量化)您的 for 循环,这在 GPU 上产生了特别惊人的差异,当然张量并不小。
在开始梯度积累之前,让我们从你的问题开始:
最后我的问题是:“幕后”到底发生了什么?
当且仅当操作数之一已经是计算图的一部分时,张量上的每个操作都在计算图中被跟踪。当您设置requires_grad=True
张量时,它会创建一个具有单个顶点的计算图,即张量本身,它将在图中保留为叶子。任何使用该张量的操作都会创建一个新的顶点,这是操作的结果,因此从操作数到它有一条边,跟踪执行的操作。
a = torch.tensor(2.0, requires_grad=True)
b = torch.tensor(4.0)
c = a + b # => tensor(6., grad_fn=<AddBackward0>)
a.requires_grad # => True
a.is_leaf # => True
b.requires_grad # => False
b.is_leaf # => True
c.requires_grad # => True
c.is_leaf # => False
Run Code Online (Sandbox Code Playgroud)
每个中间张量都自动需要梯度并且有一个grad_fn
,它是计算相对于其输入的偏导数的函数。由于链式法则,我们可以以相反的顺序遍历整个图来计算关于每个叶子的导数,这是我们想要优化的参数。这就是反向传播的思想,也称为反向模式微分。有关更多详细信息,我建议阅读计算图上的微积分:反向传播。
PyTorch 使用了这个确切的想法,当你调用loss.backward()
它时,它以相反的顺序遍历图形,从 开始loss
,并计算每个顶点的导数。每当到达叶子时,该张量的计算导数存储在其.grad
属性中。
在您的第一个示例中,这将导致:
MeanBackward -> PowBackward -> SubBackward -> MulBackward`
Run Code Online (Sandbox Code Playgroud)
第二个示例几乎相同,不同之处在于您手动计算平均值,并且损失计算的每个元素都有多个路径,而不是单个路径。澄清一下,单一路径也计算每个元素的导数,但在内部,这又为一些优化开辟了可能性。
MeanBackward -> PowBackward -> SubBackward -> MulBackward`
Run Code Online (Sandbox Code Playgroud)
在任何一种情况下,都会创建一个仅反向传播一次的图,这就是它不被视为梯度累积的原因。
梯度累积是指在更新参数之前执行多次反向传递的情况。目标是为多个输入(批次)使用相同的模型参数,然后根据所有这些批次更新模型参数,而不是在每个批次之后执行更新。
让我们重新审视你的例子。x
有大小[2],这是我们整个数据集的大小。出于某种原因,我们需要基于整个数据集计算梯度。当使用批量大小为 2 时,情况自然是这样,因为我们将一次拥有整个数据集。但是如果我们只能有大小为 1 的批次会发生什么?我们可以像往常一样单独运行它们并在每批之后更新模型,但是我们不会计算整个数据集的梯度。
我们需要做的是,使用相同的模型参数单独运行每个样本,并在不更新模型的情况下计算梯度。现在您可能会想,这不是您在第二个版本中所做的吗?几乎,但不完全是,并且您的版本中存在一个关键问题,即您使用的内存量与第一个版本相同,因为您具有相同的计算,因此计算图中的值数量相同。
我们如何释放内存?我们需要摆脱前一批的张量以及计算图,因为它使用大量内存来跟踪反向传播所需的一切。计算图在.backward()
调用时自动销毁(除非retain_graph=True
指定)。
# Example 1
loss = (y - y_hat) ** 2
# => tensor([16., 4.], grad_fn=<PowBackward0>)
# Example 2
loss = []
for k in range(len(y)):
y_hat = model2(x[k])
loss.append((y[k] - y_hat) ** 2)
loss
# => [tensor([16.], grad_fn=<PowBackward0>), tensor([4.], grad_fn=<PowBackward0>)]
Run Code Online (Sandbox Code Playgroud)
输出(为了便于阅读,我删除了包含消息的参数):
Batch size 1 (batch 0) - grad: tensor([-16.])
Batch size 1 (batch 0) - weight: tensor([1.], requires_grad=True)
Batch size 1 (batch 1) - grad: tensor([-20.])
Batch size 1 (batch 1) - weight: tensor([1.], requires_grad=True)
Batch size 1 (final) - grad: tensor([-20.])
Batch size 1 (final) - weight: tensor([1.2000], requires_grad=True)
Run Code Online (Sandbox Code Playgroud)
如您所见,模型对所有批次保持相同的参数,同时累积梯度,最后进行一次更新。请注意,损失需要按批次进行缩放,以便在整个数据集上具有与使用单个批次相同的重要性。
虽然在此示例中,在执行更新之前使用整个数据集,但您可以轻松更改它以在一定数量的批次后更新参数,但您必须记住在执行优化器步骤后将梯度归零。一般配方是:
def calculate_loss(x: torch.Tensor) -> torch.Tensor:
y = 2 * x
y_hat = model(x)
loss = (y - y_hat) ** 2
return loss.mean()
# With mulitple batches of size 1
batches = [torch.tensor([4.0]), torch.tensor([2.0])]
optimizer.zero_grad()
for i, batch in enumerate(batches):
# The loss needs to be scaled, because the mean should be taken across the whole
# dataset, which requires the loss to be divided by the number of batches.
loss = calculate_loss(batch) / len(batches)
loss.backward()
print(f"Batch size 1 (batch {i}) - grad: {model.weight.grad}")
print(f"Batch size 1 (batch {i}) - weight: {model.weight}")
# Updating the model only after all batches
optimizer.step()
print(f"Batch size 1 (final) - grad: {model.weight.grad}")
print(f"Batch size 1 (final) - weight: {model.weight}")
Run Code Online (Sandbox Code Playgroud)
您可以在HuggingFace - 大批量训练神经网络:1-GPU、多 GPU 和分布式设置的实用技巧中找到处理大批量大小的方法和更多技术。
归档时间: |
|
查看次数: |
8129 次 |
最近记录: |