编译 | skura
有一次,我在 twitter 上看到 Jeremy Howard 引用 Yann LeCun 关于 batch 大小的话:
Twitter 上关于 batch 的讨论
自从我在 Fastai 找到了一个非常好的学习率查找工具后,我就一直在想这个问题,我一直在想是否有一个有用的 batch 大小查找工具,人们可以用来快速地得到一个合适的 batch 大小来训练他们的模型。
提醒一下,Fastai 中使用的学习速率查找器通过测试不同的学习速率来确定能最大程度地减少损失的数值,从而帮助找到正确的学习速率。更详细的解释可以在这里找到:https://sgugger.github.io/how-do-you-find-A-good-learning-rate.html
在我的脑海中,做一个 bacth 大小查找器的想法已经有很长一段时间了,在得到 Jeremy 里米的激励后,我决定开始这一旅程,实现一个 batch 大小查找器来训练神经网络。
今天我想和大家分享完成一篇论文的历程,在我看来,这些文章都很有趣,也许也会激励你去尝试更多的东西!
1. 一个关于大小的故事
有关 batch 大小的 OC meme
一个常见的看法是不应该使用大 batch,因为这只会导致模型过大,并且可能会耗尽内存。显然这个观点是正确的,但前者比后者更复杂,为了回答这个问题,我们将深入研究 OpenAI 论文「An Empirical Model of Large-Batch Training」。
我非常推荐这篇文章,它解释了许多易于理解、记忆的想法。
首先,我们的目标是通过随机梯度下降法将损失最小化,并且有一个真正的潜在环境,我们将在这个环境下最小化损失。然而,我们不能访问整个数据集上的真实梯度,因此我们必须用有限的 batch 大小来近似梯度。
因为我们在一个 batch 上取平均值,如果我们的 batch 很小,就会有很多噪音存在,我们可能只在噪音上训练我们的模型。尽管如此,应用几个连续的更新是正确的策略,但我们也可以直接使用更大的批处理大小,它在计算效率上更高,并直接将噪声平均化。然而,在一个特定的大小之后,如果梯度已经是精确的,就没有必要使批处理更大,因为这只是在计算上的浪费,精度几乎没有提高。
此外,通过使用更大的 batch 尺寸(达到 GPU 允许的合理数量),我们加快了训练速度,这相当于采用了几个大步骤,而不是许多小步骤。因此,对于更大的 batch 尺寸,在相同的时间段,我们有时可以在计算时间上获得 2 倍的增益!
其次,有一个称为「简单噪音等级」的统计数据,它帮助我们确定什么是好的 batch 大小,定义为:
简单噪声标度方程
G 是损失 L 在 n 个参数上的实际梯度。
如果我们使用比简单噪声尺度小的 batch,可以通过增加 batch 来加快训练速度,反之,如果我们使用比简单噪声尺度大的 batch,我们只会浪费算力。
为了进一步了解这一统计数字的含义,让我们研究每一个术语:
- 分子是梯度中每个变量的方差之和,它是对梯度中噪声的一种测量。
- 分母是梯度的平方范数,我们称之为标度,给出了梯度接近于零的局部极小值的度量。
因此,我们的梯度越嘈杂,我们想要的 batch 越大,这是自然的,因为我们想要在正确的方向上采取梯度步骤。相反,如果梯度没有噪声,我们将从较少的步骤中获益更多,因为我们不需要平均出大量的观测值并分别使用它们。
另一方面,我们离最小值越近,batch 处理尺寸就越大,因为我们希望采取更谨慎的步骤,离局部最小值越近,因为我们不想超过它,错过正确的方向。
最后,简单的噪音量表为我们提供了一个工具来回答「较大的 batch 将使我们过度拟合,而较小的 batch 有助于规范化」的问题:
不一定!如果你的任务已经很复杂,并且近似的梯度很吵,你可能会感兴趣的是有一个更大的 batch 大小来确保你的模型没有训练太多的噪音。这并不是说一个较大的 batch 会使你过拟合,而是一个较小的 batch 会通过噪声注入增加更多的正则化。但是如果你不能正确拟合,你会添加正则化吗?
2. 论文实践
现在我们已经了解了为什么选择正确的 batch 大小很重要,以及如何通过简单的噪声规模统计找到一个好的 batch 大小,现在是时候实现它了!
记住,简单的噪声比例方程是:
简单噪声标度方程
问题是,我们不仅需要知道实际的梯度,而且还需要知道这个梯度的方差,这就增加了难度。为了解决这个问题,作者提出了两种不同的统计方法来近似简单噪声尺度的分子和分母。
尺度估计量
噪声估计
在这里,我们使用两种不同的 batch 大小——B big 和 B small,使用以下公式计算实际梯度的两种不同估计:
给定批大小的近似梯度
一旦我们有了这两个近似值,我们就可以用公式计算简单的噪声标度:
简单噪声尺度的近似
为了确保该估计量具有很小的方差,作者在整个训练过程中计算了几个简单的噪声尺度估计量,并对其进行了平均。
如文中所述,一种自然的方法是利用多个 GPU 计算每个 GPU 的局部梯度,即小梯度,然后将其与不同 GPU 的平均梯度(即大梯度)进行比较。尽管如此,这个方法假设我们有多个 GPU,这不是我们大多数人所面临的情况。
因此,必须找到一种有效的方法来实现这种单一的 GPU。现在,我将和你们分享我如何解决这个问题的具体推理过程!
本文其余部分使用的代码可以在这里找到:
https://colab.research.google.com/drive/15lTG_r03yqSwShZ0JO4XaoWix LMXMEmv
在第一行代码中,我建立了一个 Fastai 环境,在 MNIST 上运行一个模型,因为这个数据集已经在论文中进行了测试,他们得到了平均 900 的简单噪声等级。
我不会太详细地解释这些代码,因为我需要一整篇文章来解释 Fastai 是如何将所有东西与其 API 结合在一起的,但是这些代码应该是一个好的开始。如果您想进一步帮助理解代码,请在注释中告诉我,我可以解释它,甚至可以写一篇关于编码部分的文章。
A. 使用指数移动平均的第一种方法
考虑到我在论文中提出的统计数字并不是很有用,我没有多 GPU ,所以我想我可以跳过它,直接计算方差,通过做近似计算:
首先,我用给定的 batch 估计梯度近似实际梯度。
然后,当协方差矩阵的计算可以看作两个平均值时,我试图用指数移动平均来近似它,因为我不想在训练中储存许多梯度。
计算出的批次上噪声和简单噪声刻度的运行平均值
正如你在这里看到的,结果很奇怪,简单的噪音等级太颠簸了,噪音比噪音大得多,这给了一个非常糟糕的简单噪音等级,没有意义。
B. 存储梯度
我们看到使用指数移动平均不是一个好的近似协方差矩阵的方法。
解决这个问题的另一种方法是简单地预先设置 n 个梯度保持,然后我们将简单地计算 n 个不同的梯度,并使用那些 n 个梯度来近似协方差矩阵。
它开始显示结果,但是它的计算非常棘手:X 轴是以这种方式存储的简单的噪声等级计算的 batch 数。虽然它似乎提供了某种结果,但在实践中是不可用的,因为我已经存储了数百个梯度!
C. 进行两次训练
在又一次失败后,我决定按照论文的思路,计算他们的两个统计数字。尽管如此,当我只有一个 GPU 的时候,我需要有一个方法在训练中得到两批不同尺寸的产品。
然后我想,为什么要做一个单一的训练,我实际上可以运行两个不同 batch 大小的训练,然后计算它?
所以我采用了这个想法,使用 B big=2 * B small,这将允许我计算它们各自的梯度,并使用它们以指数移动平均的方式计算 G 和 S,如本文所述。
和第一种方法一样,它产生了奇怪的结果!此外,当我思考这个问题时,我得到的 batch 可能在两次运行之间不一样,因为没有任何东西强迫小 batch 包含在大 batch 批中。另外,我需要运行两个训练阶段来计算这个,所以这个方法不是很好。
D. 连续批处理
最后,我意识到最好的方法似乎是第二种方法,但有些东西必须修改,因为我不想保留大量的梯度来计算统计数据。
然后,一个非常简单但有效的想法浮现在脑海中:如果我不是像论文中那样以并行方式平均几个 batch,而是以顺序方式平均连续的 batch 呢?
这就意味着我只需要设置一个参数,我调用 n_batch,这是在计算大小梯度之前我必须存储的 batch 数,然后我就可以按顺序计算论文的统计数据了!
这样实施之后,我得到了以下结果:
结果很不错!在这篇论文中,他们认为增长趋势是可以预期的,因为噪声可能保持不变,而梯度的尺度将随着我们接近最小值而减小,这将导致一个越来越简单的噪声尺度。
因为我们很可能设置不同,我也无法访问他们的代码,所以我们的结果略有不同,但是在论文中,作者提到了一个简单的噪声等级,从 50 开始,达到 900,这才是重要的。考虑到在理论上和实践中起作用的许多近似值,结果可以变化,但是如文中所解释的,不应该有超过一个数量级的变化。
因此,经过这段漫长的旅程,似乎有一个正在起作用的实现。尽管论文对此几乎没有帮助,但最好的部分是,要在实践中使用它,你只需要一行代码!
这里的参数对应关系是:
- learn:一个 Fastai 学习器
- lr:可以使用 lr_find()找到执行训练循环的学习速率
- num_it:你要处理的批数,可以设置为 None,它将在一个时间段内自动训练
- n_batch:在计算简单噪声标度之前要存储的批数。20 似乎在不同的任务中都能很好地工作。
- beta:指数移动平均值的 beta 参数,用来计算方差和梯度的比例。如果绘图不太规则,请尝试增加到 0.999 或更多(如果需要),或增加 n_batch 参数
3. 在不同任务上测试 batch 大小查找器
是时候迈出大步了!
现在我们已经有了一个有用的实现,看看它在实践中如何辅助找到一个好的 batch 大小可能会很有趣。
首先,我们将研究 Rossmann 数据集。这个数据集已经在 Fastai courses v3 中进行了探索,你可以在这里找到它:https://github.com/Fastai/course-v3/blob/master/nbs/dl1/lesson6-rossmann.ipynb
在这里,我将简单地运行我的 batch 大小查找器,并做与原来完全相同的训练,但 batch 大小要考虑到简单噪声比例。
现在如何解释?这意味着,对于给定的学习速率,训练似乎收敛到一个大约 500 的简单噪声标度,即噪声和标度在训练后期趋于稳定。因此,计算时间和效率之间的最佳折衷似乎是拥有 512 的 batch 大小。
在对 512 和 64 的 batch 进行相同的训练之后,我们可以观察到一些情况:
batch 512的第一个单周期训练
批处理大小为 64 的第一个单周期训练
batch 大小为 512,训练速度比 batch 大小为 64 的快了近 4 倍!此外,尽管 batch 大小 512 采取的步骤较少,但最终它具有更好的训练损失和稍差的验证损失。
然后,如果我们看看每个 batch 的第二个训练周期损失:
batch 大小为 512 的第二个单周期训练损失
批量为 64 的第二个单周期训练损失
在这里我们可以看到,与 batch 大小 512 相比,64 的训练要坎坷得多,后者的结果并不过分,因为验证损失继续减少。
最后,我们可以观察到最后一个训练周期的以下结果:
大小为 512 的 batch 最后一个单周期训练损失
batch 大小为 64 的最后一个单周期训练损失
最后,如果我们把 Rossmann 的结果加起来,使用 512 而不是 64 的 batch 大小:
- 训练时间减 4
- 提供更好的训练和验证损失,以及感兴趣的指标
我研究过文本和图像数据,但是考虑到它们要大得多,特别是预训练模型非常大,当我尝试用 batch 训练时,我使用了 CUDA,由于内存不足,所以我不会在这里显示结果,但你可以在 Colab Notebook 上查看。
结论
我们在这篇文章中看到了很多东西!我希望你喜欢这趟旅程,如果你有什么需要记住的话,那就是下面这些:
- 没有神奇的 batch 大小数字,比如 32,这取决于数据的复杂性和 GPU 约束。我们看到,小 batch 可以通过噪声注入帮助调整,但如果你想学习的任务很难完成,这可能是不行的。此外,运行许多小步骤还需要更多的时间。相反,大 batch 可以真正加快你的训练速度,甚至有更好的泛化性能。
- 使用「An Empirical Model of Large- Batch Training」中引入的简单噪声尺度度量,可以很好地知道哪个 batch 尺寸是好的。我在这里提供了第一个快速实现:https://github.com/DanyWind/fastai_bs_finder 。你可以在自己的数据集上进行尝试,特别是在推荐系统或表格模型上,这样你就不太可能遇到 CUDA 内存不足的情况。
- 不要害怕尝试,一点小小的推动有时会促使你去做出很好的结果!我大概在 6 个月前看到了这篇论文,直到我真正尝试(并且多次失败)去实现它,我才真正关注它。但现在,我不仅可以与一个大型社区分享这些结果,它还帮助我更好地了解 batch 大小是如何工作的,以及它的常见概念和可能的错误。所以现在就不要犹豫,去实现酷的东西,即使它不能直接工作也无所谓,旅程比目的地更有价值!
via:https://medium.com/@danielhuynh_48554/implementing-a-batch-size-finder-in-fastai-how-to-get-a-4x-speedup-with-better-generalization-813d686f6bdf