Python 理解PyTorch中的累积梯度

Python 理解PyTorch中的累积梯度,python,deep-learning,pytorch,gradient-descent,Python,Deep Learning,Pytorch,Gradient Descent,我试图理解PyTorch中梯度累积的内部工作原理。我的问题与这两个有点相关: 对第二个问题的公认答案的评论表明,如果一个小批次太大,无法在单个正向过程中执行梯度更新,因此必须将其拆分为多个子批次,则可以使用累积梯度 考虑以下玩具示例: import numpy as np import torch class ExampleLinear(torch.nn.Module): def __init__(self): super().__init__()

我试图理解
PyTorch
中梯度累积的内部工作原理。我的问题与这两个有点相关:

对第二个问题的公认答案的评论表明,如果一个小批次太大,无法在单个正向过程中执行梯度更新,因此必须将其拆分为多个子批次,则可以使用累积梯度

考虑以下玩具示例:

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]
这正是人们所期望的结果。现在假设我们希望利用梯度累积逐样本处理数据集样本:

    # 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]
同样,正如预期的那样,在调用
.backward()
方法时计算梯度

最后,我的问题是:“引擎盖下”到底发生了什么

我的理解是,对于
损失
变量,计算图会从
动态更新为
操作,除了
损失
张量外,任何地方都不会保留关于每个向前传球所用数据的信息,该张量可在向后传球之前更新


以上段落中的推理是否有任何警告?最后,在使用渐变累积时,是否有任何最佳实践可遵循(即,我在示例2中使用的方法是否会产生反效果)?

您实际上并没有累积渐变。如果您有一个
.backward()
调用,只需退出
optimizer.zero_grad()
就没有任何效果,因为梯度从一开始就已经为零(技术上讲
,但它们将为零) 自动初始化为零)

两个版本之间的唯一区别是如何计算最终损失。第二个示例中的for循环与第一个示例中的PyTorch进行相同的计算,但您单独进行计算,PyTorch无法优化(并行化和向量化)for循环,这在GPU上产生了特别惊人的差异,假设张量不是很小

在讨论渐变累积之前,让我们先从您的问题开始:

最后,我的问题是:“引擎盖下”到底发生了什么

当且仅当其中一个操作数已经是计算图的一部分时,在计算图中跟踪张量上的每个操作。当您设置张量的
requires_grad=True
时,它将创建一个具有单个顶点的计算图,即张量本身,它将保持图中的一片叶子。使用该张量的任何操作都将创建一个新的顶点,这是该操作的结果,因此操作数到该顶点之间有一条边,跟踪所执行的操作

a=torch.tensor(2.0,需要_grad=True)
b=火炬张量(4.0)
c=a+b#=>张量(6,梯度fn=

每个中间张量自动需要梯度,并有一个
梯度fn
,该函数用于计算与其输入相关的偏导数。由于链式规则,我们可以以相反的顺序遍历整个图形,以计算与每个单叶相关的导数,这是我们使用的参数蚂蚁优化。这就是反向传播的思想,也被称为反向模式分化。更多细节,我建议阅读

PyTorch使用了这个确切的想法,当您调用
loss.backward()
时,它以相反的顺序遍历图形,从
loss
开始,并计算每个顶点的导数。每当到达一片叶子时,计算出的张量导数存储在其
.grad
属性中

在您的第一个示例中,这将导致:

MeanBackward -> PowBackward -> SubBackward -> MulBackward`
第二个示例几乎相同,只是您手动计算平均值,而不是为损失计算单个路径,而是为损失计算的每个元素计算多个路径。为了澄清,单个路径也计算每个元素的导数,但在内部,这再次为某些选项打开了可能性误导

#示例1
损失=(y-y*2
#=>张量([16,4.],梯度fn=)
#例2
损失=[]
对于范围内的k(len(y)):
y_hat=model2(x[k])
损失追加((y[k]-y_-hat)**2)
损失
#=>[张量([16.],梯度fn=),张量([4.],梯度fn=)]
在任何一种情况下,都会创建一个只反向传播一次的图,这就是它不被视为梯度累积的原因

梯度积累 梯度累积是指在更新参数之前执行多次向后传递的情况。目标是对多个输入(批次)具有相同的模型参数,然后根据所有这些批次更新模型参数,而不是在每个批次之后执行更新

让我们重温一下您的示例。
x
的大小为[2],这是我们整个数据集的大小。出于某种原因,我们需要基于整个数据集计算梯度。当使用2的批大小时,这是很自然的情况,因为我们将一次拥有整个数据集。但是如果我们只能拥有大小为1的批,会发生什么?我们可以单独运行它们,并在每个批之后更新模型和往常一样,但是我们不计算整个数据集的梯度

我们需要做的是,使用相同的模型参数单独运行每个样本,并在不更新模型的情况下计算梯度。现在您可能会想,这不是您在第二个版本中所做的吗?几乎,但不完全,并且您的版本中存在一个关键问题,即您使用的内存量与第一个版本,因为您有相同的计算,因此计算图中的值数量相同

我们如何释放内存?我们需要去掉前一批的张量和计算图,因为这会使用大量内存来跟踪反向传播所需的所有内容
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)