杂记(十六) 训练神经网络的秘诀(翻译)


原文链接A Recipe for Training Neural Networks (karpathy.github.io)


几周前,我发布了一条关于“最常见的神经网络错误”的推文,列出了一些与训练神经网络相关的常见问题。这条推文的参与度比我预期的要高得多(包括网络研讨会😃)。显然,很多人都亲身经历过“这是卷积层的工作原理”和“我们的卷积网实现了最先进的结果”之间的巨大差距。

所以我认为刷掉我尘土飞扬的博客,将我的推文扩展到这个主题应得的长篇形式可能会很有趣。然而,我不想列举更常见的错误或充实它们,而是想更深入地挖掘并讨论如何完全避免犯这些错误(或非常快速地修复它们)。这样做的诀窍是遵循某个过程,据我所知,这个过程并不经常被记录下来。让我们从激发它的两个重要观察结果开始。

**最常见的神经网络错误:**1)你没有先尝试过度拟合单个批次。2) 您忘记切换网络的训练/评估模式。3)你忘了在.backward()之前.zero_grad()(在pytorch中)。4) 您将 softmax输出传递给需要原始对数的损失。;别人?😃

1)神经网络训练是一个漏洞百出的抽象

据称,开始训练神经网络很容易。许多库和框架以显示 30 行奇迹片段来解决您的数据问题而自豪,给人一种(错误的)印象,即这些东西是即插即用的。通常会看到以下情况:

>>> your_data = # plug your awesome dataset here
>>> model = SuperCrossValidator(SuperDuper.fit, your_data, ResNet50, SGDOptimizer)
# conquer world here

这些库和示例激活了我们大脑中熟悉标准软件的部分 - 一个通常可以实现干净的 API 和抽象的地方。请求库演示:

>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.status_code
200

很酷!一个勇敢的开发人员已经从你那里承担了理解查询字符串、URL、GET/POST 请求、HTTP 连接等的负担,并在很大程度上将复杂性隐藏在几行代码后面。这是我们所熟悉和期待的。不幸的是,神经网络不是那样的。它们不是“现成的”技术,只要你稍微偏离训练 ImageNet 分类器的那一刻。我试图在我的帖子“是的,你应该理解反向传播”中通过选择反向传播并称其为“泄漏的抽象”来表达这一点,但不幸的是,情况要糟糕得多。反向传播 + SGD 不会神奇地使您的网络正常工作。批处理范数并不能神奇地使它收敛得更快。RNN 不会神奇地让你“插入”文本。仅仅因为你可以把你的问题表述为RL并不意味着你应该这样做。如果你坚持使用这项技术而不了解它是如何工作的,你很可能会失败。这让我想到了…

2)神经网络训练静默失败

当您破坏或错误配置代码时,您通常会得到某种异常。你插入了一个整数,其中某些东西需要一个字符串。该函数仅需要 3 个参数。此导入失败。该密钥不存在。两个列表中的元素数不相等。此外,通常可以为特定功能创建单元测试。

在训练神经网络方面,这只是一个开始。在语法上,一切都可能是正确的,但整个事情没有正确安排,这真的很难说。“可能的错误面”很大,逻辑性很强(与语法相反),并且对单元测试非常棘手。例如,在数据增强期间,当您从左到右翻转图像时,您可能忘记翻转标签。你的网络仍然可以(令人震惊地)很好地工作,因为你的网络可以在内部学习检测翻转的图像,然后它左右翻转它的预测。或者,由于差一错误,您的自回归模型可能不小心将它试图预测的事物作为输入。或者您尝试裁剪梯度,但裁剪了损失,导致异常值示例在训练期间被忽略。或者您从预训练的检查点初始化了权重,但没有使用原始均值。或者你只是搞砸了正则化强度、学习率、衰减率、模型大小等的设置。因此,只有在幸运的情况下,配置错误的神经网络才会抛出异常;大多数时候它会训练,但默默地工作得更糟。

结果,(这很难怎么强调)训练神经网络的“速度与激情”方法行不通,只会导致痛苦。现在,痛苦是让神经网络正常工作的一个完全自然的部分,但它可以通过彻底的、防御性的、偏执的和痴迷于基本上所有可能的事情的可视化来减轻。根据我的经验,与深度学习成功最相关的品质是耐心和对细节的关注。

方法

鉴于上述两个事实,我为自己开发了一个特定的过程,在将神经网络应用于新问题时,我将遵循该过程,我将尝试描述该过程。你会看到它非常认真地对待上述两个原则。特别是,它从简单到复杂,在每一步中,我们都会对将要发生的事情做出具体的假设,然后通过实验验证它们或进行调查,直到我们发现一些问题。我们试图防止的是同时引入大量“未经验证”的复杂性,这必然会引入错误/错误配置,这些错误/错误配置将需要很长时间才能找到(如果有的话)。如果编写神经网络代码就像训练神经网络代码一样,那么您需要使用非常小的学习率和猜测,然后在每次迭代后评估完整的测试集。

1. 与数据融为一体

训练神经网络的第一步是完全不接触任何神经网络代码,而是从彻底检查数据开始。这一步很关键。我喜欢花费大量时间(以小时为单位)浏览数千个示例,了解它们的分布并寻找模式。幸运的是,你的大脑非常擅长这一点。有一次,我发现数据包含重复的示例。还有一次,我发现损坏的图像/标签。我寻找数据不平衡和偏见。我通常还会关注我自己对数据进行分类的过程,这暗示了我们最终将探索的架构类型。举个例子 - 非常本地化的功能就足够了,还是我们需要全局上下文?有多少变化,它采取什么形式?哪些变化是虚假的,可以预先处理掉?空间位置重要还是我们想平均它?细节有多重要,我们能承受多远的图像下采样?标签有多嘈杂?

此外,由于神经网络实际上是数据集的压缩/编译版本,因此您将能够查看网络(错误)预测并了解它们可能来自哪里。如果你的网络给你的预测似乎与你在数据中看到的不一致,那么就有问题了。

一旦你有了定性的感觉,编写一些简单的代码来搜索/过滤/排序,按你能想到的任何内容(例如标签类型、注释的大小、注释的数量等)进行搜索/过滤/排序,并可视化它们的分布和沿任何轴的异常值。尤其是异常值,几乎总是能发现数据质量或预处理方面的一些错误。

2. 设置端到端训练/评估框架 + 获取哑基线

现在我们了解了我们的数据,我们是否可以使用我们超级花哨的多尺度 ASPP FPN ResNet 并开始训练令人敬畏的模型?当然没有。那是通往苦难的道路。我们的下一步是建立一个完整的训练+评估骨架,并通过一系列实验获得对其正确性的信任。在这个阶段,最好选择一些你不可能以某种方式搞砸的简单模型 - 例如线性分类器,或者一个非常小的ConvNet。我们需要训练它,可视化损失,任何其他指标(例如准确性),模型预测,并在此过程中执行一系列具有明确假设的消融实验。

这个阶段的提示和技巧:

  • 修复随机种子。始终使用固定的随机种子,以保证在运行代码两次时将获得相同的结果。这消除了变化因素,并将帮助您保持理智。
  • 化繁为简。确保禁用任何不必要的花哨。例如,在此阶段一定要关闭任何数据增强。数据增强是一种正则化策略,我们稍后可能会将其纳入其中,但就目前而言,这只是引入一些愚蠢错误的另一个机会。
  • 将有效数字添加到评估中。绘制测试损失图时,对整个(大型)测试集运行评估。不要只是批量绘制测试损失,然后依靠在 Tensorboard 中对其进行平滑处理。我们追求正确性,非常愿意放弃时间保持理智。
  • 验证 loss @ init。验证您的损失是否从正确的损失值开始。例如,如果正确初始化了最后一层,则应在初始化时在softmax上进行测量。对于 L2 回归、Huber 损失等,可以导出相同的默认值。-log(1/n_classes)
  • 初始化好。正确初始化最终层权重。例如,如果您要回归一些平均值为 50 的值,则将最终偏差初始化为 50。如果您的数据集的正负比例为 1:10,则在对数上设置偏差,以便您的网络在初始化时预测概率为 0.1。正确设置这些将加速收敛并消除“曲棍球棒”损失曲线,在最初的几次迭代中,您的网络基本上只是在学习偏差。
  • 人类基线。监控除损失之外的人类可解释和可检查的指标(例如准确性)。只要有可能,评估你自己(人类)的准确性,并与之进行比较。或者,对测试数据进行两次注释,对于每个示例,将一个注释视为预测,将第二个注释视为真实值。
  • 输入独立基线。训练一个与输入无关的基线(例如,最简单的方法是将所有输入设置为零)。这应该比实际插入数据而不将其清零时的性能更差。是吗?也就是说,你的模型是否学会了从输入中提取任何信息?
  • 过拟合一批。过拟合仅包含几个示例的单个批次(例如,只有两个)。为此,我们增加了模型的容量(例如添加层或过滤器),并验证我们是否可以达到最低的可实现损失(例如零)。我还喜欢在同一张图中可视化标签和预测,并确保一旦我们达到最小损失,它们最终会完美对齐。如果他们不这样做,则某处存在错误,我们无法继续下一阶段。
  • 验证减少训练损失。在这个阶段,你的数据集有望欠拟合,因为你正在使用玩具模型。尝试稍微增加其容量。你的训练损失是否得到了应有的下降?
  • 在网前可视化。可视化数据的明确正确位置位于 your (或 tf) 之前。也就是说,您希望准确可视化进入网络的内容,将原始张量的数据和标签解码为可视化效果。这是唯一的“真相来源”。我数不清这拯救了我多少次,并揭示了数据预处理和增强方面的问题。y_hat = model(x)``sess.run
  • 可视化预测动态。我喜欢在训练过程中可视化固定测试批次上的模型预测。这些预测如何移动的“动态”将使您对训练的进展情况有很好的直觉。很多时候,如果网络以某种方式摆动太多,可能会感觉到网络“难以”适应您的数据,从而揭示了不稳定性。非常低或非常高的学习率也很容易在抖动量中引起注意。
  • 使用 backprop 绘制依赖关系图。深度学习代码通常包含复杂的矢量化和广播操作。我遇到过几次的一个相对常见的错误是人们弄错了(例如,他们使用而不是在某个地方),并且无意中在批处理维度上混合了信息。一个令人沮丧的事实是,您的网络通常仍然可以正常训练,因为它将学会忽略来自其他示例的数据。调试此问题(以及其他相关问题)的一种方法是将损失设置为微不足道的东西,例如示例 i 的所有输出的总和,一直运行反向传递到输入,并确保仅在第 i 个输入上获得非零梯度。例如,可以使用相同的策略来确保时间 t 的自回归模型仅依赖于 1…t-1。更一般地说,梯度为您提供了有关哪些内容取决于网络中的内容的信息,这对于调试非常有用。view``transpose/permute
  • 概括一个特例。这更像是一个通用的编码技巧,但我经常看到人们在咬得比他们能咀嚼的更多时会创建错误,从头开始编写一个相对通用的功能。我喜欢为我现在正在做的事情编写一个非常具体的函数,让它起作用,然后稍后推广它,确保我得到相同的结果。这通常适用于矢量化代码,我几乎总是先写出完全循环的版本,然后才将其转换为矢量化代码,一次循环一个。
3.过拟合

在这个阶段,我们应该对数据集有很好的理解,并且我们有完整的训练 + 评估管道在工作。对于任何给定的模型,我们都可以(可重复地)计算出我们信任的指标。我们还拥有与输入无关的基线的性能,一些愚蠢的基线的性能(我们最好击败这些基线),并且我们对人类的性能有一个粗略的了解(我们希望达到这一点)。现在,已经为迭代一个好的模型做好了准备。

我喜欢寻找一个好的模型的方法有两个阶段:首先让一个模型足够大,以至于它可以过度拟合(即关注训练损失),然后适当地对其进行正则化(放弃一些训练损失以改善验证损失)。我喜欢这两个阶段的原因是,如果我们根本无法使用任何模型达到低错误率,这可能再次表明存在一些问题、错误或配置错误。

这个阶段的一些提示和技巧:

  • 选择模型。为了达到良好的训练损失,您需要为数据选择合适的架构。当谈到选择这个时,我的#1建议是:不要成为英雄。我见过很多人渴望变得疯狂和有创造力,将神经网络工具箱的乐高积木堆叠在各种对他们有意义的奇特架构中。在项目的早期阶段强烈抵制这种诱惑。我总是建议人们简单地找到最相关的论文,然后复制粘贴他们最简单的架构,以实现良好的性能。例如,如果您要对图像进行分类,请不要成为英雄,只需在第一次运行时复制粘贴 ResNet-50。你可以稍后做一些更自定义的事情,并击败它。
  • 亚当是安全的。在设定基线的早期阶段,我喜欢使用学习率为 3e-4 的 Adam。根据我的经验,Adam 对超参数(包括糟糕的学习率)更为宽容。对于 ConvNets,经过良好调整的 SGD 几乎总是会略胜 Adam ,但最佳学习率区域要窄得多,并且特定于问题。(注意:如果您使用的是 RNN 和相关序列模型,则更常用的是 Adam。同样,在项目的初始阶段,不要成为英雄,跟随最相关的论文所做的任何事情。
  • 一次只复杂一个。如果您有多个信号要插入分类器,我建议您将它们一个接一个地插入,并且每次都确保获得预期的性能提升。不要一开始就把厨房水槽扔到你的模型上。还有其他方法可以增加复杂性 - 例如,您可以尝试先插入较小的图像,然后再放大它们,等等。
  • 不要相信学习率衰减默认值。如果您要重新利用来自其他领域的代码,请始终非常小心学习率衰减。您不仅希望对不同的问题使用不同的衰减计划,而且更糟糕的是,在典型的实现中,计划将基于当前纪元,而当前纪元可能会因数据集的大小而有很大差异。例如,ImageNet 将在第 10 纪元上衰减 30。如果你没有训练ImageNet,那么你几乎肯定不希望这样做。如果你不小心,你的代码可能会偷偷地过早地将你的学习率推到零,不允许你的模型收敛。在我自己的工作中,我总是完全禁用学习率衰减(我使用恒定的 LR),并在最后一直调整它。
4.正则化

理想情况下,我们现在有一个至少适合训练集的大型模型。现在是时候对其进行正则化,并通过放弃一些训练精度来获得一些验证精度了。一些提示和技巧:

  • 获取更多数据。首先,在任何实际环境中,对模型进行正则化的最佳和首选方法是添加更多真实的训练数据。花费大量的工程周期试图从一个小数据集中榨取汁液,这是一个非常常见的错误,而你可以收集更多的数据。据我所知,添加更多数据几乎是几乎无限期地单调提高配置良好的神经网络性能的唯一保证方法。另一个是合奏(如果你买得起的话),但在 ~5 个模型之后达到顶峰。
  • 数据增强。真实数据的下一个最好的东西是半假数据 - 尝试更积极的数据增强。
  • 创意增强。如果半假数据不这样做,假数据也可能做一些事情。人们正在寻找扩展数据集的创造性方法;例如,域随机化、模拟的使用、巧妙的混合,例如将(可能模拟的)数据插入场景甚至是 GAN。
  • 预训练。如果可以的话,使用预训练网络很少会有什么坏处,即使你有足够的数据。
  • 坚持监督学习。不要对无监督的预训练过度兴奋。与2008年的那篇博文不同,据我所知,它没有一个版本在现代计算机视觉中报告了强大的结果(尽管NLP现在似乎在BERT和朋友那里做得很好,很可能是由于文本的更深思熟虑的性质,以及更高的信噪比)。
  • 较小的输入维度。移除可能包含杂散信号的特征。如果您的数据集很小,任何添加的虚假输入都只是另一个过度拟合的机会。同样,如果低级细节无关紧要,请尝试输入较小的图像。
  • 较小的模型尺寸。在许多情况下,您可以使用网络上的域知识约束来减小其大小。例如,在ImageNet的主干网顶部使用全连接层曾经是很流行的,但后来这些层已被简单的平均池化所取代,从而消除了过程中的大量参数。
  • 减小批大小。由于批处理范数内部的归一化,较小的批次大小在某种程度上对应于更强的正则化。这是因为批次经验平均值/标准是完整平均值/标准值的更近似版本,因此比例和偏移量会更多地“摆动”您的批次。
  • 下降。添加 dropout。将 dropout2d(空间丢弃)用于 ConvNet。谨慎/小心地使用它,因为 dropout 似乎不适合批量规范化。
  • 重量衰减。增加重量衰减惩罚。
  • 提早停止。根据测量的验证损失停止训练,以便在模型即将过拟合时捕获模型。
  • 尝试更大的模型。我最后提到这一点,也是在提前停止之后才提到的,但我过去曾多次发现,较大的模型最终当然会过度拟合,但它们的“提前停止”性能通常比较小的模型要好得多。

最后,为了更确信您的网络是一个合理的分类器,我喜欢可视化网络的第一层权重,并确保您获得有意义的漂亮边。如果第一层滤波器看起来像噪点,则可能是有问题。同样,网络内部的激活有时会显示奇怪的伪影并暗示问题。

5.调整

您现在应该与数据集“循环”,探索实现低验证损失的架构的宽模型空间。此步骤的一些提示和技巧:

  • 随机网格搜索。为了同时调整多个超参数,使用网格搜索来确保覆盖所有设置可能听起来很诱人,但请记住,最好改用随机搜索。直观地说,这是因为神经网络通常对某些参数比其他参数更敏感。在极限中,如果参数 a 很重要,但更改 b 不起作用,那么您宁愿更彻底地对 a 进行采样,而不是多次在几个固定点进行采样。
  • 超参数优化。市面上有大量花哨的贝叶斯超参数优化工具箱,我的一些朋友也报告说它们取得了成功,但我个人的经验是,探索一个漂亮而广泛的模型和超参数空间的最先进方法是使用实习生:)。开玩笑。
6.挤出果汁

一旦你找到了最好的架构类型和超参数,你仍然可以使用更多的技巧来从系统中榨取最后的汁液:

  • 合奏。模型集成几乎可以保证在任何事情上获得 2% 的准确率。如果你在测试时负担不起计算费用,可以考虑使用暗知识将你的集成提炼成一个网络。
  • 让它接受培训。我经常看到有人试图在验证损失似乎趋于平稳时停止模型训练。根据我的经验,网络会持续很长时间的训练。有一次,我在寒假期间不小心离开了模特训练,当我在一月份回来时,它是SOTA(“最先进的”)。
结论

一旦你成功了,你将拥有成功的所有要素:你对技术、数据集和问题有深刻的理解,你已经建立了整个训练/评估基础设施,并对其准确性有了很高的信心,你已经探索了越来越复杂的模型,以你预测每一步的方式获得性能改进。您现在已经准备好阅读大量论文,尝试大量实验,并获得 SOTA 结果。祝你好运!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值