合工大周啸课题组

神经网络的一份训练配方

本文转载于 Karpathy 写的 A Recipe for Training Neural Networks,被誉为“炼丹师圣经”。

几周前,我在 Twitter 上发布了一条关于“最常见的神经网络错误”的推文,列出了一些与神经网络训练相关的常见陷阱。这条推文获得的互动比我预想的要多得多(甚至还包括一场网络研讨会 🙂)。

显然,很多人都亲身经历过这样一种巨大差距:“这里是卷积层如何工作的解释”“我们的卷积网络达到了当前最先进(state of the art)的结果” 之间的差距。

因此,我觉得也许可以把我那尘封已久的博客翻出来,把那条推文扩展成它应得的长文形式。然而,我并不打算继续枚举更多常见错误,或者只是把它们详细展开,而是想稍微深入一点,讨论如何从根本上避免犯这些错误(或者至少能非常快地修复它们)

做到这一点的关键,是遵循某种特定的过程。据我所知,这个过程并不常被系统性地记录下来。我们先从两个重要的观察开始,它们正是这个过程的动机来源。

1)神经网络训练是一种“有漏洞的抽象”

据说,开始训练神经网络是很容易的。大量库和框架都以展示 30 行的“奇迹代码片段”为荣,这些代码声称可以解决你的数据问题,从而给人一种(错误的)印象:这项技术是即插即用的。

你经常会看到类似这样的东西:

>>> your_data = # 把你超棒的数据集塞到这里
>>> model = SuperCrossValidator(SuperDuper.fit, your_data, ResNet50, SGDOptimizer)
# 在这里征服世界

这些库和示例会激活我们大脑中那一部分熟悉于标准软件工程的区域——在那个世界里,干净的 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》中试图阐明这一点,我当时以反向传播为例,称它是一种“有漏洞的抽象(leaky abstraction)”,但现实情况要糟糕得多。

反向传播 + SGD 并不会神奇地让你的网络正常工作。Batch Normalization 并不会神奇地让它更快收敛。RNN 并不会神奇地让你“随便把文本塞进去”。而且,仅仅因为你可以把问题表述成强化学习,并不意味着你就应该这么做。

如果你坚持在不了解其工作原理的情况下使用这些技术,你很可能会失败。

这就引出了第二个观察。

2)神经网络训练会“悄无声息地失败”

当你破坏或错误配置传统代码时,通常会得到某种异常提示:

此外,通常还可以为某些功能编写单元测试。而在神经网络训练中,这些还只是开始。

一切在语法上都可能是正确的,但整体的组织方式却是错误的,而且这一点非常难以察觉。

“可能出错的空间”非常巨大,这些错误往往是逻辑层面的(而非语法层面的),而且极难通过单元测试发现。

因此,你配置错误的神经网络只有在你运气好的时候才会抛出异常;大多数时候,它会正常训练,但悄无声息地表现得差一点。

正因为如此(而这一点真的怎么强调都不为过),“快而猛烈”的神经网络训练方式是行不通的,只会带来痛苦。

当然,痛苦是让神经网络工作良好过程中完全正常的一部分,但它是可以被缓解的——方法是:彻底、谨慎、偏执,并且痴迷于几乎所有可能的可视化。

在我的经验中,与深度学习成功最强相关的品质是:耐心对细节的关注

配方

基于上述两个事实,我为自己制定了一套特定的流程,在将神经网络应用于一个新问题时,我都会严格遵循它。下面我将尝试对这一流程进行描述。

你会看到,这个流程非常严肃地对待前面提到的两个原则。特别是,它从简单开始,逐步走向复杂,并且在每一步中,我们都会对将要发生的事情提出具体假设,然后通过实验来验证这些假设,或者持续调查,直到找到问题所在。

我们竭力避免一次性引入大量“未经验证”的复杂性,因为那几乎注定会引入一些 bug 或错误配置,而它们要么会花费你极长时间才能找到,要么你永远也找不到。

如果编写神经网络代码像训练神经网络一样,你一定会使用一个非常小的学习率,并在每一次迭代之后对整个测试集进行评估。

1. 与数据融为一体

训练神经网络的第一步是:完全不要触碰任何神经网络代码,而是开始彻底检查你的数据。

这一步至关重要。

我喜欢花费大量时间(以小时计)浏览成千上万个样本,理解它们的分布,并寻找其中的模式。幸运的是,人脑在这方面非常擅长。

有一次,我发现数据中包含重复样本。另一次,我发现了损坏的图像或标签。我会寻找数据不平衡和偏差。

我通常也会关注自己在对数据进行分类时的思考过程,因为这会暗示我们最终应当探索何种类型的网络结构。例如:

此外,由于神经网络本质上是你的数据集的一种压缩或编译形式,你将能够通过查看网络的(错误)预测结果,理解这些预测可能来源于数据中的哪些部分。

如果你的网络给出了与你在数据中观察到的现象明显不一致的预测,那说明某些地方一定出了问题。

在获得定性认知之后,再写一些简单代码来搜索、筛选或排序你能想到的任何维度(例如:标签类型、标注大小、标注数量等),并对它们的分布以及各个维度上的离群点进行可视化,通常也是一个好主意。

尤其是离群点,几乎总是会揭示数据质量或预处理中的 bug。

2. 搭建端到端训练/评估骨架 + 做一些“愚蠢基线”

现在我们已经理解了数据,我们是不是就可以拿起我们超级华丽的 Multi-scale ASPP FPN ResNet,然后开始训练很强的模型了?当然不是。那就是通往痛苦的道路。

我们的下一步是搭建一个完整的训练 + 评估骨架,并通过一系列实验来建立对其正确性的信任。在这个阶段,最好选择某个你不可能在实现上搞错的简单模型——例如,一个线性分类器,或者一个非常小的卷积网络。我们会想要训练它、可视化 loss、可视化其他指标(例如 accuracy)、可视化模型预测,并沿途进行一系列带有明确假设的消融实验。

这个阶段的一些 tips & tricks:

3. 过拟合(Overfit)

到这里,我们应该已经对数据集有了较好的理解,并且训练 + 评估管线已经跑通。对于任意给定的模型,我们可以(可复现地)计算一个我们信任的指标。我们也已经掌握了输入无关基线的性能、一些“愚蠢基线”的性能(我们最好要超过它们),并且对人类大概能做到什么水平也有一个粗略感觉(我们希望能达到这一水平)。现在,迭代一个好模型的舞台已经搭好了。

我喜欢用两阶段的方法来寻找好模型:首先找到一个足够大的模型,使它能够过拟合(也就是说,专注于训练 loss);
然后对它进行适当正则化(牺牲一些训练 loss,提升验证 loss)。我之所以喜欢这两步,是因为如果我们无论如何都无法用任何模型达到低错误率,那可能再次表明存在问题、bug 或错误配置。

这个阶段的一些 tips & tricks:

4. 正则化(Regularize)

理想情况下,我们现在应该处于这样一个阶段:我们有一个足够大的模型,它至少能拟合训练集。现在该对它进行正则化,通过牺牲一些训练准确率来获得更好的验证准确率了。

一些 tips & tricks:

最后,为了进一步增强对网络是一个合理分类器的信心,我喜欢可视化网络第一层的权重,并确保你能看到一些合理的边缘结构。如果你的第一层滤波器看起来像噪声,那么可能有问题。类似地,网络内部的激活有时也会显示奇怪的伪影,并提示问题。

5. 调参(Tune)

你现在应该已经处于这样一个状态:围绕你的数据集形成了一个闭环,在一个宽广的模型空间里探索能够达到低验证 loss 的架构。

这一步的一些 tips and tricks:

6. 榨干最后一点性能(Squeeze out the juice)

一旦你找到了最好的架构类型和超参数,你还可以用一些技巧把系统里最后的性能“挤出来”:

结论(Conclusion)

当你走到这里时,你就拥有了成功所需的全部要素:

祝你好运!

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »