Skip to content

梯度累积(gradient accumulation)

Gradient Accumulation

ds p39 1

在内存受限的情况下,始终建议以较小的批大小训练神经网络。

尽管如此,有一种叫做梯度累积(gradient accumulation)的技术,可以让我们在不必显式增大批大小的情况下(逻辑上)增大批大小。感到困惑?让我们在本章中加以理解。但在此之前,我们必须先了解……

神经网络中的主要内存开销

ds p39 2

来自于反向传播。这是因为在反向传播期间,我们必须将各层激活值存储在内存中。毕竟,它们被用来计算梯度。

网络越大,需要存储在内存中的激活值就越多。而且,在内存受限的情况下,使用较大的批大小会导致:

  • 存储大量激活值
  • 使用这些大量激活值来计算梯度

这可能导致资源消耗超过可用量——从而导致训练失败。但通过减小批大小,我们可以限制内存使用并完成网络训练。

假设我们正在用小批量训练一个神经网络。

ds p40 1

我们按如下方式训练网络:

  • 在每个小批量上:
    • 运行前向传播并存储激活值。
    • 在反向传播期间:■ 计算损失 ■ 计算梯度 ■ 更新权重

梯度累积修改了反向传播的最后一步,即权重更新。更具体地说,我们不必在每个小批量上都更新权重,而是可以这样做:

ds p40 2

  1. 在一个小批量上运行前向传播。2. 计算网络中权重的梯度值。3. 暂不更新权重。4. 在下一个小批量上运行前向传播。5. 计算权重的梯度值,并将其与第 2 步得到的梯度相加。6. 对更多小批量重复步骤 3-5。7. 仅在处理了几个小批量之后才更新权重。

这一技术之所以有效,是因为跨多个小批量累积

ds p41 1

梯度所得到的梯度之和,与我们把它们放在一起处理时的梯度之和相同。因此,从逻辑上讲,使用梯度累积,我们可以在不必显式增大批大小的情况下模拟更大的批大小。

例如,假设我们想使用 64 的批大小。然而,当前内存只能支持 16 的批大小。没问题!

  • 我们可以使用大小为 16 的批大小。
  • 我们可以累积每个小批量的梯度。
  • 我们可以每 8 个小批量才更新一次权重。

因此,实际上我们使用的是 16*8(=128)的批大小,而不是我们最初打算的 64。

让我们来看看如何实现这一点。在 PyTorch 中,典型的训练循环实现如下:

ds p42 1

  • 我们清零梯度
  • 运行前向传播
  • 计算损失
  • 计算梯度
  • 更新权重

然而,如前所述,如有需要,我们可以仅在几次迭代之后才更新权重。因此,我们必须继续累积梯度,而这正是 loss.backward() 所做的事情。

此外,由于 optimizer.zero_grad() 会清零梯度,我们必须仅在更新权重之后才执行它。这一思路的实现如下:

ds p43 1

  • 首先,我们定义 acc_steps——即希望在其之后更新权重的小批量数量。
  • 接下来,我们运行前向传播。
  • 然后,我们计算损失和梯度。
  • 如前所述,我们暂时不更新权重,而是让梯度再累积几个小批量。
  • 我们仅在 if 条件为真时才更新权重。
  • 更新之后,我们清除已累积的梯度。

这样,我们就能在内存受限的环境下优化神经网络训练。

在结束之前,必须指出:梯度累积并不是在内存受限情况下改善运行时间的灵丹妙药。事实上,我们也可以从我的实验中验证这一点:

ds p44 1

相反,它的目标是降低整体内存使用。当然,确实我们只在几次迭代之后才更新权重,所以它会比每次迭代都更新稍快一些。然而,我们仍然在处理和计算小批量上的梯度,这才是这里的核心操作。尽管如此,好处在于即使你不受内存限制,也仍然可以使用梯度累积。

  • 指定你通常使用的批大小。
  • 运行前向传播。
  • 计算损失和梯度。
  • 仅在几次迭代之后才更新。

你可以在这里下载 notebook:https://bit.ly/3xNCfFt。

本站内容仅供学习交流,版权归原作者所有。