训练神经网络的方法分享-Andrej Karpathy
原作者:Andrej Karpathy
由深度学习与NLP翻译
几周前,我发布了一条关于“最常见的神经网络错误”的推文,列出了一些与训练神经网络相关的常见问题。这条推文获得了比我预期的多得多的参与度(包括网络研讨会)。显然,许多人都亲身经历了“卷积层是如何工作的”和“我们的convnet实现了最先进的结果”之间的巨大差距。
所以我想起了我满树灰尘的博客,把我的推文扩展成一篇长文。然而,与其列举更常见的错误或充实它们,我想更深入一点,谈谈如何避免这些错误(或者非常快速地修复它们)。这样做的诀窍是遵循某个过程,据我所知,这个过程并不经常被记录下来。让我们从激发它的两个重要观察开始。
1)神经网络训练是一个漏洞百出的抽象概念
据称,开始训练神经网络很容易。许多库和框架以展示30行代码解决数据问题的奇迹片段为荣,给人一种(错误的)印象,认为这些东西是即插即用的。常见的情况是:
>>> your_data = # plug your awesome dataset here
>>> model = SuperCrossValidator(SuperDuper.fit, your_data, ResNet50, SGDOptimizer)# conquer world here
这些库和例子激活了我们大脑中熟悉标准软件的部分—一个经常可以获得干净的API和抽象(abstraction)的地方。Request库证明:
>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.status_code200
太酷了。一个勇敢的开发人员承担了理解查询字符串、URL、GET/POST请求、HTTP连接等等的负担,并且在很大程度上隐藏了几行代码背后的复杂性。这是我们所熟悉和期待的。不幸的是,神经网络不是那样的。当你稍微偏离训练图像分类器的时候,它们并不是“现成的”技术。我试图在我的文章《Yes you should understand backprop》中指出这一点,选择反向传播并将其称为“leaky abstraction”,但不幸的是,情况要糟糕得多。Backrop+SGD不会神奇地让你的网络工作。Batch Norm不会神奇地使它收敛得更快。RNN不会神奇地让你“plug in”文本中。仅仅因为你可以把你的问题表述为RL并不意味着你应该这样做。如果你坚持使用这项技术,却不理解它是如何工作的,你很可能会失败。这让我想到…
2)神经网络训练无声无息地失败
当你中断或错误配置代码时,你通常会得到某种异常。你插入了一个整数,有东西需要一个字符串。该函数只需要3个参数。这将引入失败。这个关键并不存在。两个列表中的元素数量不相等。此外,通常可以为某个功能创建单元测试。
这只是训练神经网络的一个开始。从句法上来说,一切都可能是正确的,但整件事安排得并不恰当,这真的很难说。“可能的错误面”很大,合乎逻辑(与句法相反),并且单元测试非常棘手。例如,在数据扩充(data augmentation)过程中,当你左右旋转图像时,可能忘记旋转标签。你的网络仍然可以(令人震惊地)运行得很好,因为你的网络可以在内部学习检测旋转的图像,然后左右旋转它的预测。或者,由于一个接一个的错误,你的自回归模型意外地把它试图预测的东西作为输入。或者你试图clip你的梯度,但却clip了loss(loss),导致异常的例子在训练中被忽略。或者你从pretrained checkpoint中初始化你的权重,但没有使用原始平均值。或者你只是把正则化强度、学习率、衰减率、模型大小等设置搞砸了。因此,只有当你幸运的时候,你错误配置的神经网络才会抛出异常;大多数时候,它会训练,但只是无声地工作得更糟。
结果,(这很难过分强调)训练神经网络的“fast and furious”方法不起作用,只会导致痛苦。现在,痛苦是让神经网络正常工作的一个非常自然的部分,但是它可以通过彻底的、防御性的、偏执的和对基本上所有可能的事物的可视化的方式来缓解。在我的经验中,耐心和对细节的关注是与深度学习的成功最密切相关的品质。
建议(Recipe)
鉴于以上两个事实,我为自己开发了一个特定的过程,当我将神经网络应用于一个新问题时,我会尝试描述这个过程。你会发现它非常认真地遵循以上述两个原则。特别是,它从简单到复杂,在我们对将要发生的事情做出具体假设的每一步,然后要么用实验来验证它们,要么进行调查,直到我们发现一些问题。我们努力防止的是同时引入大量“未经验证”的复杂性,这必然会引入需要很长时间才能找到的错误/错误配置(如果有的话)。如果编写你的神经网络代码就像训练一个,你会希望使用非常小的学习率和猜测,然后在每次迭代后评估整个测试集。
1.与数据融为一体
训练神经网络的第一步是不要接触任何神经网络代码,而是从彻底检查数据开始。这一步至关重要。我喜欢花大量的时间(以小时为单位)浏览成千上万个例子,了解它们的分布并寻找模式。幸运的是,人的大脑很擅长这个。有一次,我发现数据包含重复的样本。另一次我发现损坏的图像/标签。寻找数据不平衡和biases。我通常也会关注我自己的数据分类过程,这暗示了我们最终将探索的架构类型。举个例子—非常局部的特征就足够了,还是我们需要全局背景?有多少变化,它采取什么形式?什么变化是虚假的,可以被预处理掉?空间位置重要吗,还是我们想要平均分配?细节有多重要,我们能承受对图像下采样多远?标签带有多少噪声?
此外,由于神经网络实际上是数据集的压缩/编译版本,你将能够查看你的网络(mis)预测结果,并了解它们可能来自哪里。如果你的网络给了你一些与你在数据中看到的不一致的预测,那就有问题了。
一旦你有了定性的感觉,写一些简单的代码来搜索/过滤/排序也是一个好主意,只要你能想到(例如标签的类型、注释的大小、注释的数量等等)。)并可视化它们沿任何轴的分布和异常值。异常值几乎总是会暴露数据质量或预处理中的一些缺陷。
2.建立端到端的学习/评估框架+获得dumb baseline
现在我们了解了我们的数据,我们选择奇幻的multi-scale QSPP FPN ResNet并开始训练我们的模型吗?当然不。那是通往苦难的道路。我们的下一步是建立一个完整的学习+评估框架,并通过一系列实验获得对其正确性的评价。在这个阶段,最好选择一些你不可能搞砸的简单模型—例如线性分类器,或者非常小的ConvNet。我们希望对其进行训练、可视化loss、任何其他指标(如准确性)、模型预测,并在此过程中进行一系列带有明确假设的消融实验。
这一阶段的提示和技巧:
1、固定随机种子。始终使用固定的随机种子来保证当你运行代码两次时,你将获得相同的结果。这消除了变异的因素,有助于保持正确的判断。
2、简化。确保禁用任何不必要的幻想。举个例子,在这个阶段一定要关闭任何数据扩充。数据扩充是一种正则化策略,我们可能会在以后加入,但目前这只是引入一些愚蠢错误的又一个机会。
3、在评估中添加有效数字。绘制测试loss曲线时,对整个(大)测试集进行评估。不要只绘制批次测试loss曲线,然后依靠Tensorboard对它们进行平滑处理。我们在追求正确性,并且非常愿意为了保持正确的判断而花费更多的时间。
4、验证loss @ init。验证你的loss从正确的loss值开始。例如,如果你正确初始化你的最后一层,你应该在初始化时测量softmax的-log(1/n_classes)。对于L2回归、Huberloss等,可以导出相同的默认值。
5、良好的初始化。正确初始化最终输出层的权重。例如,如果你正在回归一些平均值为50的值,则将最终偏差初始化为50。如果你有正负比例为1:10的不平衡数据集,在你的逻辑上设置偏差,这样你的网络在初始化时预测概率为0.1。正确设置这些将加快收敛速度,消除“曲棍球棒”类似的loss曲线,在最初的几次迭代中,你的网络基本上只是学习偏差。
6、人类baseline。监控除loss以外的人类可解释和可检查的指标(如准确性)。尽可能评估你自己(人类)的准确性,并与之进行比较。或者,对测试数据进行两次注释,对于每个示例,将一次注释视为预测,第二次注释视为真是情况。
7、input-indepent baseline。训练一个独立于输入的baseline(例如,最简单的方法是将所有输入设置为零)。这应该比实际插入数据而不清零时表现更差。是吗?即,你的模型是否学会从输入中提取任何信息?
8、过拟合one batch。过量拟合一批次仅有几个例子(如少至两个)。为此,我们增加模型的容量(例如添加层或filter),并验证我们可以达到最低的可实现损耗(例如零)。我也喜欢在同一个图中可视化标签和prediction,并确保一旦我们达到最小loss,它们最终会完美对齐。如果他们没有,某个地方有一个错误,我们不能继续下一个阶段。
9、验证减少training loss。在这个阶段,你可能会对你的数据集不太满意,因为你正在使用一个玩具模型。试着增加模型复杂度。你的训练loss下降了吗?
10、搭建网络前先进行可视化。可视化数据的正确位置就在y_hat = model(x)(或sess.run in tf)之前。也就是说,你想把进入你网络的东西形象化,把数据和标签的原始张量解码成形象化。这是唯一的“真理之源”。我数不清这救了我多少次,也暴露了数据预处理和扩充中的问题。
11、可视化预测动态(prediction dynamics)。在学习过程中,我喜欢在固定的测试批次上可视化模型prediction。这些预测变化的“dynamics”将会给你关于训练进展的难以置信的好直觉。很多时候,如果网络以某种方式摇摆太多,暴露出不稳定性,就有可能感觉到它在“挣扎”适应你的数据。非常低或非常高的学习率在抖动量方面也很容易被注意到。
12、使用backprop绘制依赖关系图。你的深度学习代码通常包含复杂、矢量化和广播操作。我遇到过的一个相对常见的错误是人们弄错了(例如,他们在某个地方使用view而不是transpose/permute),并且无意中在批处理维度上混合了信息。令人沮丧的事实是,你的网络通常仍然训练良好,因为它将学会忽略来自其他例子的数据。调试这个(以及其他相关问题)的一种方法是将一些示例I的loss设置为1.0,运行一直到输入的反向传递,并确保只在第I个示例上获得非零梯度。更一般地说,梯度给你关于什么信息取决于你的网络中什么的信息,这对调试是有用的。
13、归纳一个特例。这更像是一个通用的编码技巧,但是我经常看到人们仔细操作时也会产生错误,从头开始编写一个相对通用的功能。我喜欢为我现在正在做的事情写一个非常具体的函数,让它生效,然后在以后推广它,以确保我得到相同的结果。这通常适用于矢量化代码,在矢量化代码中,我几乎总是先写出完整的循环版本,然后一次一个循环地将其转换为矢量化代码。
3.过拟合
在这个阶段,我们应该对数据集有一个很好的理解,并且我们已经有了完整的学习+评估方式。对于任何给定的模型,我们都可以(可复制地)计算出我们trust的程度。我们还为独立于输入的baseline准备了我们的性能,一些dumb baseline的性能(我们最好击败它们),我们对人类的性能有一个粗略的感觉(我们希望达到这一点)。现在已经为迭代一个好的模型做好了准备。
寻找一个好模型的方法有两个阶段:首先获得一个足够大的模型,使它能够overfit(即过拟合训练loss),然后适当地调整它(放弃一些训练loss,以改善验证loss)。我喜欢这两个阶段的原因是,如果我们不能用任何模型达到低错误率,这可能再次表明一些问题、bugs或配置错误。
这一阶段的一些提示和技巧:
1、挑选模型。为了获得良好的学习效果,你需要为数据选择合适的架构。说到选择这个,我的第一个建议是:不要做英雄。我见过很多人渴望变得疯狂和有创造力,把神经网络工具箱的乐高积木堆成各种奇形怪状的模型。在项目的早期阶段强烈抵制这种诱惑。我总是建议人们只需找到最相关的论文,复制粘贴他们最简单的架构,就能获得良好的性能。例如,如果你正在分类图像,不要逞英雄,只需复制粘贴一个ResNet-50作为你的第一次运行的模型。你可以在以后做一些更自定义的事情,然后超越它。
2、Adam很安全。在设定baseline的早期阶段,我喜欢使用学习率为3e-4的adam。根据我的经验,adam对超参数更宽容,包括糟糕的学习率。对于ConvNet来说,一个调整良好的SGD几乎总是比adam略胜一筹,但是最佳学习率区域要窄得多,而且是针对具体问题的。(注意:如果你使用的是RNNs和相关的序列模型,那么更常见的是使用adam。同样,在项目的初始阶段,不要做英雄,跟着最相关的论文做。)
3、一次只复合一个。如果你有多个想法要在你的分类器上尝试,我建议你一个接一个地试,每次都要确保你得到预期的性能提升。一开始不要把所有的一且都混在一起。还有其他增加复杂性的方法—例如,你可以先尝试较小的图像,然后再放大,等等。
4、不要相信学习速率衰减默认值。如果你从其他领域重新编写代码,一定要非常小心学习速度的下降。你不仅希望针对不同的问题使用不同的衰减策略,而且更糟糕的是,在典型的实现中,衰减策略将基于当前的epoch数,而当前的epoch数可以根据数据集的大小有很大的不同。例如,在epoch30,ImageNet将衰减10。如果你不训练图像网,那么你几乎肯定不想这样。如果你不小心,你的代码可能会秘密地过早地将你的学习率降到零,不允许你的模型收敛。在我自己的工作中,我总是完全禁用学习速率衰减(我使用常数LR),并在最后一路调整它。
4.调整
理想情况下,我们现在在一个地方,我们有一个大模型,至少适合训练集。现在是时候调整它,通过放弃一些训练精度来获得一些验证精度。一些提示和技巧:
1、获取更多数据。首先,在任何实际环境中规范模型的最佳和首选方法是添加更多真实的训练数据。当你可以收集更多的数据时,花大量的工程周期试图从一个小数据集中挤出能量是一个非常常见的错误。据我所知,增加更多的数据几乎是唯一有保证的方法,可以几乎无限期地单调提高配置良好的神经网络的性能。另一种可能是ensemble(如果你能负担得起的话),但这在大约5个模型中最好的。
2、数据扩充。真实数据的第二大优点是半假数据—尝试更积极的数据扩充方式。
3、创造性的augmentation。如果半假数据不能做到这一点,假数据也可能会有所作为。人们正在寻找扩展数据集的创造性方法;例如,领域随机化(domain randomization)、使用模拟(simulation)、巧妙的混合,例如将(潜在模拟的)数据插入到场景(scenes),甚至使用GANs。
4、预训练。如果可以的话,即使你有足够的数据,使用一个预先处理的网络也不会有什么坏处。
5、坚持监督学习。不要对无人监督的预训练过于信任。与2008年的博客文章告诉你的不同,据我所知,没有一个版本在现代计算机视觉领域取得了显著的成果(尽管在NLP领域,最近BERT及类似的模型似乎取得成功,很可能是因为文本更为谨慎,信噪比更高)。
6、较小的输入维度。移除可能包含虚假信号的特征。如果数据集很小,任何增加的虚假输入都只是另一个overfit输入的机会。同样,如果底层细节不重要,尝试输入一个较小的图像。
7、较小的model size。在许多情况下,你可以在网络上使用领域知识约束来减小其规模。例如,在ImageNet中的输出层使用全连接层曾经很流行,但后来这些层被简单的平均池(average pooling)取代,减少了大量模型参数。
8、减小batch size。由于batch norm内部的规范化,较小的batch size在某种程度上对应于较强的规范化。这是因为batch经验平均值/标准是full平均值/标准的更近似版本,所以比例和偏移量会使你的batch“摆动”更多。
9、dropout。添加dropout。将dropout2d(空间drop)用于ConvNets。小心谨慎地使用它,因为dropout似乎不适合batch标准化。
10、weight decay。增加重量衰减惩罚。
11、early stopping。在你的模型即将溢出时,停止基于你测量的验证loss的训练。
12、试试更大的模型。我最后一次提到这一点,也是在提前stopping之后,但我在过去发现过几次,较大的模型最终当然会overfit使用,但它们的“提前停止”性能往往比较小型号好得多。
最后,为了获得你的网络是一个合理的分类器的额外信心,我喜欢可视化网络的第一层权重,并确保你得到有意义的结果。如果你的第一层filter看起来像噪音,那么可能有什么东西是关闭的。类似地,网络内部的激活有时会显示奇怪的工件并提示问题。
5.调整
现在,你应该“in the loop”,让你的数据集为获得low validation loss的体系结构探索一个广阔的模型空间。这一步的一些提示和技巧:
1、随机网格搜索。为了同时调整多个超参数,使用网格搜索来确保覆盖所有设置听起来很诱人,但是请记住,最好使用随机搜索来代替。直觉上,这是因为神经网络通常对某些参数比其他参数更敏感。在极限情况下,如果一个参数a很重要,但是改变b没有任何影响,那么你宁愿多次对一个参数进行更彻底的采样,而不是在几个固定点上。
2、超参数优化。周围有大量花哨的贝叶斯超参数优化工具箱,我的一些朋友说它们很好用,但我个人的经验是,探索模型和超参数的良好和广泛空间的最先进方法是使用实习生:)。开玩笑的。
6.挤出果汁
一旦找到最佳类型的架构和超参数,你仍然可以使用一些技巧来挤出系统中的最后一部分juice。
1、Ensemble。模型集合是获得任何事物2%精确度的一个非常有保证的方法。如果你负担不起测试时的计算,那就考虑用黑暗知识(dark knowledge)将你的系综提炼成一个网络。
2、让模型好好训练。我经常看到当验证loss趋于平稳时,人们试图停止模型学习。以我的经验来看,网络会在不直观的情况下持续训练很长时间。有一次,我在寒假期间不小心持续让一个模型学习,当我在一月份回来的时候,刚好获得SOTA(“state of the art”)。
结论
一旦你在这里成功了,你就拥有了成功的所有要素:你对技术、数据集和问题有了深刻的理解,你已经建立了整个学习/评估基础设施,并对其准确性有了很高的信心,你已经探索了越来越复杂的模型,通过预测每一步的方式获得了性能的提高。你现在已经准备好阅读大量的论文,尝试大量的实验,并得到你的SOTA结果。祝你好运!