神经网络训练手册

引言

最近看见一篇非常有实用价值的博客——由 Andrej Karpathy 编写的 A Recipe for Training Neural Networks,它总结了用神经网络开发项目的一般流程,常见的坑和相应解决方法。为了分享并加深自己的理解,在此转载并翻译博客内容,强烈推荐大家观看原文!


Intro

前几周我发布了一篇推特,列举了一些训练神经网络常见的坑,获得了超乎预想的关注量,很明显不少人都感受到了“我们的网络开始工作了!”和“我们的网络达到了SOTA!”之间的巨大差距。

所以我认为值得写一篇博客来扩展上面推特的内容,然而,相比列举常见错误和对应的解决方法,我更倾向于讨论如何避免这些错误(或者更快修复它们),避免错误的技巧是遵从规定的开发流程。下面先从我观察到的两个现象说起。

1) Neural net training is a leaky abstraction

据说训练神经网络很容易,因为大量深度学习框架总是自豪于展示他们使用极少代码就能训练网络,这给了我们网络训练是一种“即插即用”的虚假印象。例如下面的内容:

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

这让我们联想到可以获得清晰APIs和抽象类的软件开发,例如:

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

这样很酷!因为上述代码背后隐藏的复杂工作已经被勇敢的开发人员完成了,这也是我们熟悉并期望的。然而,神经网络却不是这样,一旦偏离训练 ImageNet 分类器,它就不是即插即用的现成技术。我在博客 Yes you should understand backprop 通过讨论 backprop 提到过这一点,但实际情况往往更糟,Backprop + SGD 不一定能让网络工作,使用 BatchNorm 不一定能收敛得更快,RNNs 不一定能顺利引入文本数据,如果在不理解技术的情况下坚持使用它,你很可能会遭遇失败。

2) Neural net training fails silently

当代码有问题时通常会遇见报错,例如,把整数赋给了需要字符串的函数,函数只需要3个参数而你给了4个,两个列表中的元素数量不同等等,为了避免这种错误,我们可以对每个函数进行单元测试(uint test)。

而对于训练网络来说,这才仅仅是开始。就算代码在语法上是正确的,整个网络也可能存在问题,并且非常难以察觉,例如,数据增强过程中你只镜像翻转了图像却忘记翻转了 label,尽管如此,网络也可能运行地很好,因为它学习到检测这些图像并将其翻转回来;或者你想要裁剪梯度却错误地裁剪了损失,导致异常样本在训练中被忽略;又或者你想从一个 checkpoint 继续训练,载入了网络参数,却忘记载入了 optimizer,scheduler 的参数以及其他参数等等。如果你足够幸运,编译器会给出报错,但大多数情况下只是网络的性能默默降低。

因此,以“速度与激情”的方式来训练网络是行不通的,按我的经验来说,成功训练网络最相关的是对细节的耐心和关注。

The recipe

认识到以上两个事实后,我为自己设计了一套使用神经网络解决问题的流程。最重要的是,要从简单到复杂一步步开发,每一步对接下来将发生什么做出具体假设,然后进行实验来验证或者调查(如果出现了问题)。需要极力避免的是一次性引入过多未验证的复杂操作,这一定会带来难以察觉的 bugs,如果像训练网络一样来写网络代码,我们将使用非常小的学习率,并且在每一次迭代后猜测并验证代码在整个测试集上的性能。

1. Become one with the data

训练网络的第一步是完整观察你的数据,不要碰任何代码!这一步很关键,我通常会花费大量的时间(以小时为单位)浏览数据,理解它们的分布和模式,寻找不平衡和异常的样本,有一次我发现数据包含重复样本,又有一次我发现数据存在损坏的图像 / 标签。我会关注自己分类数据的过程,这将帮助我决定网络结构,例如,局部特征就够了还是说需要全局特征?数据变化有多大,变化遵从什么格式?哪些影响是虚假的可以通过预处理消除?空间位置信息重要吗,是否可以平均池化掉?图像细节重要吗,能够将图像降采样多少?标签存在噪声吗?

除此之外,网络是数据的高效压缩,你能够通过观察网络的预测结果来推断它们是怎么得到的,如果网络的预测结果和数据差异很大,那么一定存在问题。

当你对数据有一个整体印象后,可以写一些简单的代码来搜索 / 过滤 / 排序任何你能想到的东西(例如标签类型,尺寸,数量等),可视化数据分布,打印数据某一维上的异常值。这些异常值通常意味着数据质量或预处理中的 bugs。

2. Set up the end-to-end training/evaluation skeleton + get dumb baselines

当理解数据以后,就可以使用那些非常花哨的 Multi-scale ASPP FPN ResNet 来训练网络了吗?当然不是,这样做只会让你受苦,下一步应该是建立一个完整的训练 + 验证框架,并通过一系列实验测试它的正确性。在这个阶段最好选择一些简单的模型,简单到不会出任何错误,例如线性分类器或非常小的卷积网络,我们将训练它,可视化损失,指标,预测结果,并带着假设进行一系列消融试验。

本阶段的技巧:

  • fix random seed,使用固定的随机数种子保证能复现代码结果,这消除了变化因素并让你保持理智。
  • simplify,不要添加任何不必要的操作,例如,不使用任何数据增强,数据增强是一种正则化策略并且可以在之后添加,现阶段只会增加出 bug 的可能性。
  • add significant digits to your eval,当验证模型在测试集上的准确性时,不要仅仅只是画出损失曲线并平滑它,将重要的数值打印出来!我们在追求准确性并且愿意牺牲一些时间来保持理智。
  • verify loss @ init,验证你的损失是否从正确的值开始。(译者:这一条我也不是很理解,大家可以在评论区指点我一下,谢谢)
  • init well,正确初始化网络最后一层的参数,例如,如果你在回归均值为 50 的数据,那么初始化最后一层的 bias 为 50;如果你的数据不平衡,正负样本比例大概是 1:10,那么初始化最后一层的 bias 使网络初始输出概率大约为 0.1。这将加速网络的收敛,避免网络在初始迭代阶段只学习 bias。
  • human baseline,监测除了损失以外便于人类理解的指标(例如准确率),尽可能测试你自己在这个任务上的准确率,能一定程度体现网络在这个任务上的性能上界。
  • input-indepent baseline,训练一个与输入无关的 baseline,(最简单的方法就是将输入设置为0),它的性能应该比有数据输入时差,能一定程度上体现网络是否提取到了数据中的信息。
  • overfit one batch,验证网络是否能在一个包含少量样本(例如2个样本)的 batch 上过拟合,可以通过增加网络能力(例如增加网络层数)验证损失是否足够接近 0 来实现。我也喜欢同时显示预测结果和标签,查看当损失足够小时它们是否能完美匹配上,如果不能,说明哪里一定存在着 bug。
  • verify decreasing training loss,该阶段因为网络十分简单,对数据应该欠拟合,尝试增加一点点网络的能力,验证训练损失是否按照预想有所下降?
  • visualize just before the net,可视化数据的正确位置应该正好在 y_hat = model(x) (或者 sess.run 在 tf 中)前一行,这能 准确 显示进入网络的到底是什么东西。我数不清它帮助了我多少次,因为它揭示了数据预处理 / 增强中可能存在的问题。
  • visualize prediction dynamics,训练时,我喜欢在测试集一个固定的 batch 上可视化网络的预测结果,这些预测结果的动态变化将使我对训练进展有非常好的直观感受,很多次当预测结果以某种方式抖动时,我都感觉网络在努力地拟合数据,抖动过强意味着不稳定,从中也能看出学习率是设置的过大还是过小。
  • use backprop to chart dependencies,深度学习代码通常会包含一些复杂的矢量化操作,一种常见的 bug 是人们不经意间弄混了 batch 维度上的信息(例如在某处使用了 view 而不是 transpose/permute),遗憾的是网络仍然能正常训练因为它会学习忽略来自其他样本的数据。一种 debug 方法是将损失设置为关于样本 i 的简单形式(例如样本 i 所有输出的和),执行梯度反向传播,确保只有关于样本 i 的梯度不为零,相同的策略也可以应用到自回归模型来保证只使用了 1…t-1 的数据。普遍来说,梯度信息能告诉你网络依赖什么数据,这对 debug 很有用。
  • generalize a special case,这听起来更像是一种通用的编程建议,但我发现人们经常会因为从一开始就写相对通用的函数而引入 bug,对我来说,我会先写一个非常具体的函数,让它正常运行起来,之后再扩展为通用形式并保证能获得相同结果。当我矢量化代码时,我经常会先写出完整的循环版本,然后再一层层转化为对应的矢量操作。

3. Overfit

到本阶段为止,我们已经对数据有了充分的理解,拥有了完整的训练 + 验证流程,对任意给定模型可以复现某个指标的计算;我们也有了基础 baseline,与输入无关的 baseline 并大致理解了人类在该任务上能达到的效果(我们希望模型也能达到)。本阶段的目标是迭代出一个好模型。

我寻找好模型的方法分为两步:第一步是让模型变得足够强大以至于能过拟合(体现在训练损失上),第二步是适当泛用化模型(牺牲一些训练损失来优化验证损失)。采用这两步的原因是如果使用任何模型都不能过拟合,那么一定存在着某些问题。

本阶段的技巧:

  • picking the model,为了获得较好的训练损失,你需要选择合适的网络结构,对此我的建议是:不要妄想做英雄,我见过太多人痴迷于将各种自己认为有意义的网络模块堆叠在一起,但在项目早期一定要抵制这种诱惑,我经常建议人们选择最相关的论文并复制粘贴他们最简单的结构来取得较好的性能,例如,如果你在分类图像,第一次运行时可以仅仅复制粘贴 ResNet-50,之后再做自定义修改并获得更好的性能。
  • adam is safe,在设置 baseline 的早期阶段我喜欢使用学习率为 3e-4 的 Adam,通过实验,我发现 Adam 对于超参数不是特别敏感(包括很差的学习率),对于卷积网络,调整好的 SGD 通常会比 Adam 好一点,但最佳的学习率范围通常非常狭隘并且和具体问题有关(如果你在使用 RNNs 和相关的序列模型,使用 Adam 更加常见,在项目初期,不要妄想做英雄,跟随相关论文来做)。
  • complexify only one at a time,如果分类器需要对不同复杂程度的数据进行拟合,我的建议是每次只增加一点复杂度进行训练,并保证每次能获得期望的提升,不要一开始就把所有数据丢给模型。也有其他增加复杂度的方式——例如一开始输入小图像然后逐渐增加尺寸。
  • do not trust learning rate decay defaults,如果你使用别人的代码,请谨慎对待学习率衰减(learning rate decay),学习率衰减的典型实现方式都是基于当前 epoch 数的,它会随着训练数据多少产生很大的变化,例如 ImageNet 每 30 代会缩减 10 倍学习率,如果不是训练 ImageNet 的话你当然不想这样衰减。如果你对学习率衰减不上心的话,它可能很快就让你的学习率衰减到零,使网络难以收敛,我在工作中经常完全不使用学习率衰减,直到最后才去微调它。

4. Regularize

理想来说,我们已经得到了一个性能不错的模型(至少它能拟合训练数据),现在到了泛化它,通过牺牲部分训练准确率来提升验证准确率的时候了,本阶段的一些技巧:

  • get more data,到目前为止泛化模型最好和最推荐的方法是增加更多真实训练数据,当你有能力获得更多数据时,榨干小训练集来提升泛化性是不理智的,据我所知,增加数据是单调提升配置好的网络的性能的唯一方法,另一个方法是模型融合(如果你负担得起的话),但大约在 5 个模型后就会达到峰值。
  • data augment,比真实数据次优的是半仿真数据,尝试更积极的数据增强吧!
  • creative augmentation,如果半仿真数据的性能提升还不够,可以使用仿真数据。人们发现了一些创造性扩展数据的方法,例如,domain randomization,使用 simulationhybrids 方法例如把目标插入到不同景象中,甚至使用 GAN 生成数据。
  • pretrain,使用预训练网络几乎不会产生害处,即使你拥有充足的数据。
  • stick with supervised learning,不要对无监督预训练过分激动,就我目前所知,在计算机视觉领域它还没有产生足够强大的效果(虽然近期在 NLP 领域似乎表现得很好)。
  • smaller input dimensionality,删除可能包含冗余信息的特征,当你的数据集很小时,冗余信息的输入会增加过拟合的可能性,如果图像的细节不重要,尝试输入更小的图像。
  • smaller model size,很多情况下可以通过先验知识来减小网络尺寸,例如,过去 ImageNet 倾向于在 backbone 后放一个全连接层,但现在已经被平均池化层替代了,这省去了大量的参数。
  • decrease the batch size,更小的 batch size 某种程度上意味着更强的泛化性,因为这样统计出来的均值和方差更加多样,增加了数据的多样性。
  • drop,增加 dropout 层,对卷积网络使用 dropout2d,使用的时候要小心因为 dropout 似乎和 batchNorm 的兼容不好。
  • weight decay,增加 weight decay 损失。
  • early stopping,根据验证损失,在模型即将过拟合的时候停止训练。
  • try a larger model,尽管更大的模型最后通常会过拟合,但我多次发现大模型 early stopped 性能可能比小模型要好。

最后,为了进一步验证网络,我喜欢可视化网络第一层的参数以确保它们看起来像合理的边缘,如果你第一层的滤波器看起来像噪声,那么可能出现了问题。相似的,网络内部的激活函数有时会出现奇怪的伪影,这也暗示着问题。

5. Tune

广泛探索模型和超参数空间以实现更低的验证损失!针对该阶段的技巧:

  • random over grid search,为了同时调整多个超参数,使用网格搜索来保证所有设置的收敛听起来似乎很迷人,但请记住最好使用随机搜索,神经网络对一些超参数比其他的更敏感,如果参数 a 很重要而参数 b 没什么效果,你应该对参数 a 的空间充分采样,而不是在一些固定的点多次采样。
  • hyper-parameter optimization,存在着大量的贝叶斯超参数优化工具并且我的一些朋友使用它们取得了成功。

6. Squeeze out the juice

当你找到最优的网络结构和超参数后,仍然有一些技巧来榨干这个系统的最后一点性能:

  • ensembles,模型融合几乎可以保证在任何任务上获得 2% 的准确度提升,如果你不能承担这个计算量,可以尝试使用 dark knowledge 把融合蒸馏到网络中。
  • leave it training,当验证损失似乎趋于平稳时,很多人就这样停止了训练,根据我的经验,网络的训练会持续很长时间。有一次寒假我忘记了停止模型的训练,当我一月份回来时,它就达到了 SOTA。

Conclusion

到这里为止你已经拥有了成功的所有要素:你对技术,数据和问题有了深刻的理解,你建立了完整的训练 + 验证流程并对它的准确性充满信心,你逐步探索越来越复杂的模型,按照你期望的方式获得了性能提升,现在你已经可以去阅读大量文献,尝试大量实验,然后得到你的 SOTA !祝你好运!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值