@wanghuijiao
2021-07-07T17:17:23.000000Z
字数 7296
阅读 717
模型训练
几周前,我(以下均指Andrej Karpathy)发布了一条关于“最常见的神经网络错误”的推文,列出了一些与训练神经网络相关的常见错误。这条推文获得了比我预期更多的关注(包括一个网络研讨会)。显然,很多人都曾亲身经历过“卷积神经网络就是这么工作的”和“我们的convnet实现了最先进的结果”之间的巨大差距。
所以我想,把我tweet的这个话题扩展到应该有的篇幅,可能会很好玩。然而,我不想详细列举更多常见的错误或充实它们,我想更深入地探讨一下如何能够完全避免这些错误(或快速修复它们)。这样做的诀窍是遵循一个特定的过程,据我所知,这个过程并不经常被记录下来。让我们从两个重要的观察开始。
据说开始训练神经网络很容易。许多库和框架都觉得使用30行代码来解决数据问题很了不起,这给人一种即插即用的(错误的)印象。常见的做法是:
>>> your_data = # plug your awesome dataset here
>>> model = SuperCrossValidator(SuperDuper.fit, your_data, ResNet50, SGDOptimizer)
# conquer world here
在我们的脑子里,标准的软件就应该是这样的,通常可以获得干净的api和抽象。使用Requests库演示一下:
>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.status_code
200
那样是很酷!开发人员承担了理解查询字符串、url、GET/POST请求、HTTP连接等重担,并在很大程度上隐藏了几行代码背后的复杂性。这是我们所熟悉和期望的。不幸的是,神经网络不是这样的。如果你稍微和训练ImageNet分类器不太一样的话,那么它们就不是“现成的”技术了。我试图在我的帖子" Yes you should understand backprop "中指出这一点,通过选择反向传播并将其称为“泄漏的抽象”,但真实情况要糟糕得多。Backprop + SGD不会神奇地让你的网络工作。Batch norm并不能神奇地使其更快地收敛。RNNs也不会神奇地让你的文本即插即用。仅仅因为你可以用增强学习表示你的问题并不意味着你应该这样做。如果你坚持使用这种技术而不了解它的工作原理,你很可能会失败。这使我想到……
当你破坏或配置代码错误时,通常会得到某些异常。你输入了一个整数,但是期望输入是一个字符串。这个函数只需要3个参数。导入失败。这个key不存在。这两个列表中的元素数量不相等。此外,通常可以为某个功能创建单元测试。
这只是训练神经网络的一个开始。从语法上来说,一切都是正确的,但整件事的安排并不正确,而且这真的很难判断。“可能的错误面”很大,符合逻辑(与语法相反),并且很难进行单元测试。例如,在数据增强过程中,当你从左到右翻转图像时,可能忘记了翻转标签。你的网络仍然可以(令人震惊地)很好地工作,因为你的网络可以在内部学会检测翻转的图像,然后左右翻转它的预测。或者你的自回归模型不小心把它试图预测的东西作为输入,因为有一个减一的错误。或者你试着修剪你的梯度,但结果却减少了损失,导致异常值的样本在训练中被忽略。或者你从一个预训练的检查点初始化你的权重,但没有使用原始平均值。或者你只是搞砸了正则化强度、学习率、衰减率、模型大小等的设置。因此,只有在幸运的情况下,错误配置的神经网络才会抛出异常,大多数情况下,它会训练,但是,这样默默地工作更糟。
因此,用“简单粗暴”的方法来训练神经网络是行不通的,只会导致痛苦。现在,痛苦是让神经网络正常工作的一个非常自然的部分,但是它可以通过彻底的,防御性的,偏执的,以及对几乎所有可能的事情的可视化着迷的方式来减轻。根据我的经验,耐心和对细节的关注是深度学习成功最重要的因素。
基于以上两个事实,我为自己开发了一个特定的过程,当我将神经网络应用到一个新的问题时,我将遵循这个过程,我将尝试描述它。你将看到,它非常认真地对待上述两个原则。特别是,它从简单到复杂,每一步我们都对将要发生的事情做出具体的假设,然后通过实验验证它们,或者进行调查,直到我们发现一些问题。我们极力避免的是同时引入大量“未经验证”的复杂性,这必然会引入错误/错误配置,而这些错误/错误配置将永远无法找到(如果有的话)。如果编写你的神经网络代码就像训练代码一样,你会想要使用非常小的学习率,然后去猜测,再在每次迭代之后评估完整的测试集。
训练神经网络的第一步是完全不接触任何神经网络代码,而是从彻底检查数据开始。这一步至关重要。我喜欢花大量的时间(以小时为单位)浏览数千个样本,理解它们的分布并寻找模式。幸运的是,你的大脑非常擅长这个。有一次,我发现数据中包含重复的样本。另一次我发现了损坏的图像/标签。我寻找数据的不平衡和偏差。我通常还会注意我自己对数据进行分类的过程,这暗示了我们最终将探索的架构类型。举个例子,非常局部的特征是否足够,或者我们是否需要全局上下文?有多少多样性,它以什么形式出现?哪些多样性是假的,是不是可以可以通过预处理挑出来?空间位置重要吗,我们用不用平均池化?细节有多重要,我们能在对图像降采样多少倍?标签的噪声程度怎么样?
此外,由于神经网络实际上是对数据集的压缩/编译版本,你可以查看你的网络的预测,并了解它们可能来自何处。如果你的网络给你的预测与你在数据中看到的不一致,那就错了。
一旦你有了一个定性的感觉,写一些简单的代码来搜索/过滤/排序也是一个好主意,不管你能想到什么(例如标签的类型,标注的尺寸,标签的数量,等等),然后可视化它们的分布,看看在每一个维度上的异常值。异常值几乎总是会发现数据质量或预处理中的一些bug。
现在我们了解了我们的数据了,那我们是不是就可以使用我们的超炫的多尺度ASPP FPN ResNet,并开始训练非常牛逼的模型了呢?肯定是不行的,那样做是自寻死路。我们的下一步是建立一个完整的训练+评估框架,通过一系列的实验获得对其正确性的信任。在这个阶段,最好选择一些你不可能搞砸的简单模型——例如一个线性分类器,或者一个非常小的卷积网络。我们想要训练它,可视化损失,所有的其他指标(例如准确性),模型预测,并在此过程中执行一系列带有明确假设的消融实验。
这个阶段的小贴士和技巧:
固定随机种子。总是使用固定的随机种子来保证当你运行代码两次时,将得到相同的结果。这消除了产生变化的一个因素,将有助于你保持理智。
简化。确保取消任何不必要的想法。例如,在这个阶段一定要关闭所有的数据增强。数据增强是一种正则化策略,我们稍后可能会将其合并进来,但目前它只是引入一些愚蠢bug的另一个机会。
将有效数字添加到您的eval中。当绘制测试损失时,在整个(大型)测试集上运行评估。不要只在每个batch上绘制测试,然后依赖于在Tensorboard中平滑它们。我们追求正确性,很愿意放弃时间保持理智。
验证损失@init。验证损失从正确的损失值开始。例如,如果你正确地初始化你的最后一层,你应该在softmax的初始化上度量 -log(1/n_classes)。同样的缺省值可以用于L2回归、Huber损失等。
正确初始化。正确初始化最后一层权重。例如,如果你要回归一些平均值为50的值,那么需要把偏差初始化为50。如果你有一个不平衡的数据集,其正负之比为1:10,那么需要在你的logits上设置偏差,使你的网络在初始化时预测概率为0.1。正确设置这些参数将加快收敛速度,并消除“曲棍球棒”损失曲线,在最初几次迭代中,你的网络基本上只是学习偏差。
人类基线。监控人类可解释和可检查的损失以外的指标(例如准确性)。只要有可能,评估你自己(人类)的准确性,并与之进行比较。或者,对测试数据进行两次标注,对于每个样本,将一个标注作为预测,将第二个标注作为基本事实。
独立于输入的基线。训练一个独立于输入的基线(例如,最简单的方法就是将所有输入设置为零)。这应该比使用实际数据的情况更糟糕。也就是说,你的模型是否可以学会从输入中提取有用信息?
在一个batch上过拟合。在只有一个batch上的少数几个样本(例如,只有两个)上做到过拟合。为此,我们增加模型的容量(例如添加层或滤波器),并验证我们可以达到的最低的可实现损失(例如零)。我也喜欢在相同的图中可视化标签和预测,并确保一旦我们达到最小损失,它们最终会完美地对齐。如果不是这样的,那么在某个地方就有一个bug,我们就无法继续到下一个阶段。
验证减少训练损失。在这个阶段,你期望你的数据集是欠拟合的,因为你正在使用一个玩具模型。试着增加一点它的容量。你的训练损失是不是减少了?
在进网络之前进行可视化。在 y_hat=model(x)(或tf中的 sess.run)之前是可视化数据的正确位置。也就是说,你想把进入网络的东西可视化,把原始的张量数据和标签解码成可视化的东西。这是唯一的“真理之源”。我已经数不清这个救了我多少次,暴露出了多少数据预处理和增强方面的问题。
可视化预测动态。在训练过程中,我喜欢将固定测试批次的模型预测可视化。这些预测如何移动的“动态”将为你提供关于训练进展的非常好的直觉。很多时候,如果网络在某种程度上太过摇摆,暴露出不稳定性,你可能会觉得网络“难以”拟合你的数据。非常低或非常高的学习率在抖动的数量中也很容易被注意到。
使用反向传播来绘制依赖关系图。你的深度学习代码通常包含复杂、向量化和广播等操作。我遇到过的一个比较常见的错误是,人们犯了这个错误(例如,他们使用 view而不是 transpose/permute),无意中混合了批处理维度上的信息。倒霉的是,你的网络通常仍然训练良好,因为它将学会忽略来自其他样本的数据。调试这个(和其他相关问题)的一种方法是,将某个样本i的损失设置为1.0,一直反向传播到输入,并确保只在第i个样本上得到非零梯度。更一般地说,梯度提供了关于网络中相互依赖的信息,这对于调试非常有用。
泛化到特殊的情况。这是一个通用的编码技巧,但是我经常看到人们在做超出他们能力范围的事情时会产生bug,从零开始编写一个相对通用的功能。我喜欢为我现在正在做的事情写一个非常具体的函数,让它工作,然后泛化它,确保得到相同的结果。这通常适用于向量化代码,我几乎总是先写出完整的循环版本,然后每次只将其转换为向量化代码。
在这个阶段,我们应该对数据集有一个很好的理解,我们有完整的训练+评估流程。对于任何给定的模型,我们都可以(可重复地)计算我们可信任的度量。我们还准备好了一个独立于输入的基线,一些简单基线的性能(我们最好战胜这些基线),并且我们对人类的性能有一个粗略的感觉(我们希望达到这个目标)。现在已经为迭代一个好的模型做好了准备。
我喜欢采用的找到一个好的模型的方法有两个阶段:首先获得一个足够大的模型,它可以过拟合(即关注训练损失),然后适当地对其进行正则化化(放弃一些训练损失,以改进验证损失)。我喜欢这两个阶段的原因是,如果我们不能达到任何模型的低错误率,这可能再次表明一些问题、bug或错误配置。
这一阶段的一些建议和技巧:
选择模型。要获得良好的训练损失,需要为数据选择合适的体系结构。当涉及到选择这一点时,我的第一条建议是:不要逞能。我见过很多人,他们渴望疯狂而富有创造性地将神经网络工具箱的乐高积木堆在各种对他们来说有意义的奇特架构中。在项目的早期阶段需要强烈抵制这种诱惑。我总是建议人们简单地找到最相关的文件,并复制粘贴他们最简单的架构,以获得良好的性能。例如,如果你要对图片进行分类,不要逞能,在第一次运行时复制粘贴一个ResNet-50即可。稍后你可以做一些更自定义的事情来解决这个问题。
Adam是安全的。在设置基线的早期阶段,我喜欢使用Adam,学习率为3e-4。根据我的经验,adam对超参数要宽容得多,包括糟糕的学习率。对于ConvNets来说,一个良好调优的SGD几乎总是略优于Adam,但是最优学习率区域要窄得多,而且是针对特定问题的。(注意:如果你在使用RNNs和相关的序列模型,则使用Adam更为常见。同样,在项目的初始阶段,不要逞能,跟随大多数相关论文所做的事情。)
一次只复杂化一个。如果你有多个信号要插入分类器,我建议你将它们一个接一个地插入,每次都要确保你获得预期的性能提升。不要一开始就把所有的东西都扔给你的模型。还有其他增加复杂性的方法——例如,你可以先插入较小的图像,然后将其放大,等等。
不要相信默认的学习率衰减值。如果你在重新使用来自其他领域的代码,那么一定要非常小心学习率衰减。你不仅想为不同的问题使用不同的衰减策略,而且更糟的是,在一个典型的实现中,策略将基于当前epoch数,它可以根据数据集的大小变化很大。ImageNet在epoch 30时衰减10。如果你没有训练ImageNet,那么你肯定不想要这个。如果你不小心的话,你的代码可能会秘密地将你的学习速度过早地降低到零,从而不允许你的模型收敛。在我自己的工作中,我总是完全禁用学习率衰减(我使用一个常量LR),并在最后再调优它。
理想情况下,我们现在拥有一个至少能在训练集上拟合的很好的大型模型。现在是时候对其进行正则化,并通过放弃一些训练精度来获得一些验证精度。以下是一些小贴士和技巧:
获得更多数据。首先,在任何实际设置中对模型进行正则化的最佳方法和首选方法是添加更多真实的训练数据。当你可以收集更多的数据时,花费大量的工程周期试图从一个小的数据集中压榨出性能,是一个非常常见的错误。据我所知,添加更多的数据几乎是唯一保证单调地提高一个配置良好的神经网络性能的方法,几乎是无限期的。另一种是模型集成(如果你负担的起的话),但最多也就5个模型。
数据增强。仅次于真实数据的是半假数据——尝试更积极的数据增强。
创造性增强。如果半假数据做不到这一点,假数据可能也会有所作为。人们正在寻找扩展数据集的创造性方法。例如,领域随机化、使用模拟、聪明的混合,比如将(潜在的模拟)数据插入场景,甚至是GANs。
预训练。即使你有足够的数据,如果可以的话,使用一个预先训练好的网络也不会有什么坏处。
坚持监督学习。不要对无监督训练过度兴奋。据我所知,没有哪个无监督学习的版本在视觉应用上取得了好的结果。
小的输入维数。删除可能包含假信号的特征。如果你的数据集很小,任何添加的伪输入都只是另一个过拟合的机会。同样,如果底层细节不太重要,可以尝试输入更小的图像。
小模型尺寸。在许多情况下,可以使用网络上的领域知识约束来减小其大小。例如,过去流行在ImageNet的主干顶部使用全连接层,但现在这些层已被简单的平均池化所取代,从而消除了大量参数。
减少批大小。由于batch norm,较小的batch大小对应较强的正则化。这是因为batch的经验平均值/std是完整的平均值/std的更近似的版本,所以比例和偏移量会使你的batch波动得更多。
添加dropout。对ConvNets使用dropout2d(空间dropout)。谨慎地使用这个函数,因为使用batch norm时,dropout似乎不太好用。
权值衰减。增加权值衰减的惩罚。
提前停止。基于已测量的验证损失停止训练,以便在模型即将过拟合时获取它。
试试大一点的模型。我只是在提前停止后才提到这一点,但我在过去发现过几次,当然更大的模型最终会过拟合得更多,但它们的“提前停止”性能往往比较小的模型好得多。
最后,为了进一步确信你的网络是一个合理的分类器,我喜欢可视化网络的第一层权重,并确保得到有意义的好边缘。如果你的第一层过滤器看起来像噪音,那么有些东西可能会出问题。类似地,网络内的激活有时会显示奇怪的纹理,这也提示有问题。
你现在应该“循环”使用你的数据集,探索广阔的模型空间,为的是实现低验证损失。这一步的一些小贴士和技巧:
随机网格搜索。同时调优多个超参数,使用网格搜索来确保覆盖所有设置听起来很诱人,但是请记住最好使用随机搜索。直观地说,这是因为神经网络对某些参数比其他参数更敏感。在极限情况下,如果一个参数a很重要,但是更改b没有效果,那么你宁愿更完整地采样a,而不是在几个定点上多次采样。
超参数优化。有大量的贝叶斯超参数优化工具箱,我的几个朋友也有过成功使用的报告,但我个人的经验是,探索一个非常大的模型空间和超参数的最先进的方法是使用实习生:)。只是开玩笑。
一旦你找到了最好的架构类型和超参数,你仍然可以使用更多的技巧来再压榨一下:
集成。模型集成是一种非常有保证的方法,可以获得2%的准确率提升。如果你在测试时无法负担计算开销,可以考虑使用dark knowledge将你的集成提取到一个网络中。
继续训练。我经常看到人们在验证损失趋于平稳时停止模型训练。在我的经验中,网络持续训练的时间长得出乎意料。有一次,我在寒假期间模型训练的时候不小心离开了,当我一月份回来的时候,那是SOTA(“最先进的技术”)。
一旦你看到这儿了,你会得到所有成功的配方:你有一个深入了解的技术,数据集和问题,你建立了整个训练/评估的基础设施并且对其准确性有很高的信心,你探索了越来越复杂的模型,你的每一步都得到了预期的性能改进。你也已经准备好阅读大量的论文,尝试大量的实验,并获得SOTA结果。好运!