特斯拉AI主管Andrej Karpathy的神经网络训练指导

Andrej Karpathy是目前全球顶尖的计算机视觉专家,博士毕业于斯坦福,现就任于特斯拉,担任AI部门主管。在他2019年4月发布的最新博文中,他跟大家分享了他关于训练神经网络的一些心得。本篇就对这篇英文撰写的博文进行翻译,方便各位学习。如有翻译不准确的地方还请指正,我会即刻修改。(本文约9000字)

原文地址:http://karpathy.github.io/2019/04/25/recipe/


几周前,我就“最为常见的神经网络错误”发布了一篇推特,在里面列举了一些常见的训练神经网络的小陷阱。结果这篇推特获得了不小的讨论,超出了我的预期(其中还有一场网络研讨)。显然,有很多的人都经历过“这是卷积层如何工作的”和“我们的卷积神经网络获得了最牛的结果”这之间的巨大落差。

所以我想,打理一下我的博客,从那篇推特延伸开来讨论一下这个值得深挖的话题,会比较有趣。不过,我想深入一点讨论我们如何能够避免那些问题(或是快速修复它们),而不仅仅是罗列一通那些常见的错误。这儿的技巧就在于——要遵循某一种过程,而这个过程,据我所知好像没什么人经常记录下来。我们就先从两个与之紧密相连的、可观察到的点开始说起吧。

1)神经网络训练是一种技术露底(Leaky abstraction)

(注:Leaky abstraction在更通用的软件开发背景下,讲述的是一种抽象,它应该做到尽可能的抽象化,但却泄露了内部的本不该被上层开发者看到一些细节。这儿有篇把这个概念讲得很生动的博文:https://zhuanlan.zhihu.com/p/26803553

开始训练神经网络目前是非常简单的。我们有各式各样的库和框架,各自标榜能够用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

这太酷了!有那么个勇敢的开发者,从你身上把诸如理解query字符串、url、GET/POST请求,HTTP连接等等的负担都拿走,然后用区区几行代码掩盖了背后庞大的复杂。这是我们熟悉并期望的。不幸的是,神经网络根本不是这样的。一旦你略微从训练ImageNet分类器深入一点点,它们就不是什么现成的技术了。我试着在我的博文Yes you should understand backprop中通过聚焦反向传播,称其为“技术露底”来阐明这种观点,但情况往往是更严峻的。反向传播+SGD并不会神奇地使你的网络工作;Batch normalization并不会神奇地加速网络收敛;RNN不会神奇地让你“插入”文字;而且只因为你能够把你的问题描述为强化学习问题并不意味着你就该那么做。如果你坚持用这些你并不熟悉的技术你有可能就会失败。这让我想到了第二点……

2)神经网络是无声地失败的

当你搞崩了或是误配置了一段代码,你常常会收到某些抛出的异常。你可能在某个期望收到字符串的地方放了个整数进去;函数只接受3个参数;这个导入失败了;这个键值不存在;两个列表里头的元素数量不一致……除此之外,为某些功能设计单元测试总是可行的。(注:我觉得Andrej这里想表达的意思是,通过这些异常和单元测试我们总能知道代码在那里崩了。但是这些在神经网络里行不通)

而对于训练神经网络来说这仅仅是开始。所有东西语法上都是对的,但是整体上没有妥当配置,而这又是很难看出来的。潜在的错误面(注:可能指的是所有错误的来源)极为庞大、逻辑化(对比于语法上而言),也很难做单元测试。比如说,当你左右翻转了图像做数据增广的时候你可能忘了要把对应的labels也翻转——你的神经网络仍旧能(令人震惊地)良好工作,因为它能够在内部学习检测翻转了的图像然后翻转它的预测值。或者你的自回归模型因为你的一个off-by-one的bug(注:那些在迭代中循环变量边界值没设对而造成数据分割错误)把该预测用的样本拿来训练了。或者你试着裁剪你的梯度,结果你裁剪了损失值,使得边缘样本在训练时被无视了。或者你从一个预训练的检查点初始化了网络权重,但你忘了用了最初的平均值。或者你就是把正则化的强度、学习速率、衰减速率、模型尺寸啥的给弄砸了。所以,如果幸运的话,你的这个误配置的神经网络会抛出异常;大部分时候它能训练但只是静悄悄地表现得糟糕一点。

所以结果是(这一点也很难夸大),以“速度与激情”一般的方式训练神经网络是行不通的并只会导致承受苦果。目前来看,承担苦果是让一个神经网络好好工作的很自然的一部分,但是它能够通过我们仔细、步步为营、乃至神经质,以及对所有可能的东西都疯狂地做可视化来被稍微减轻一点。我的经验里和成功完成深度学习项目最直接、最相关的品质当属耐心和对细节的注意了。

训练指导

鉴于以上两个事实,当在一个新问题里使用神经网络的时候,我已经自我发展出了一套遵循的流程,我会在下头试着描述它。你将会看见它很认真地对待上述两个原则。具体来说,这套流程从简单到复杂,在每一步,我们会设想关于什么会发生的切实假设,然后要么用实验来验证假设,要么仔细检查直到发现错误。我们尽全力要避免的,是那种一次性爆发出来很多的、未验证的复杂问题,它们绝对会带来bug或是错配置,会花上我们很久很久才能找到。如果把编写你的神经网络代码比作训练神经网络,那么你必须要用很小的学习速率,要猜,要在每次迭代后在整个测试集上头做评估。

1. 与数据合而为一

训练神经网络的第一步是不要去碰任何神经网络相关的代码,反之,要全面地检查你的数据。这一步非常关键。我喜欢动用不少的时间(以小时计量)来浏览几千个数据样本,理解它们的分布,寻找一些模式规律。幸运的是,你的大脑做这个挺在行。有一次我发现了数据里存在重复的样本。还有一次我发现了损坏了的图片/标签。我还会寻找数据不平衡和偏差。我一般还会注意我给数据分类的过程,这个过程会暗示我们最终探索的模型架构是什么样子。举个例子:局部特征对我们来说够了,还是说我们需要更多的全局特征/语境?数据的变化有多大,是以什么样的形式展现出来的?有哪些变化看上去有点假,可以通过预处理丢掉?空间位置信息是否有用,还是说我们可以通过平均池化丢弃掉?细节的成分有多重要,我们可以把图片降采样到什么程度?标签数据有多少噪声?

除此之外,因为神经网络是你的数据集的一个有效压缩/编译过后的版本,所以你可以查看你的网络的(误)预测,理解这些预测是从哪儿产生的。如果说你的网络给你了一个预测值,这个值和你自己在数据里看到的信息不相吻合,那么有些地方肯定是出错了。

一旦你有了一种对数据定性的了解,你可以写一些简单的代码来查找、过滤、排序那些你能想到的东西(比如:标签类型,注释尺寸、注释数量等)并在任何一个维度上可视化它们的分布以及一些边缘点(outliers)。尤其是边缘点,经常能发掘出数据质量或是预处理过程中的一些bug。

2. 搭好端到端的训练/评估代码框架 + 搞一个简单的基准

现在我们理解了我们的数据,我们可以接触那些超级牛掰闪闪发光的Multi-scale ASPP FPN ResNet开始训练酷炫的模型了吗?当然不是。这么做走上的是痛苦的不归路。我们的下一步是要搭好全套的训练+评估用的代码框架,通过一系列实验对你的框架的准确性建立信心。在这一步,最好是用一个非常简单的、你几乎不可能玩坏的模型——比如说一个线性分类器,或一个很小很小的卷积网络。我们会去训练它,可视化损失函数或其他任何指标(比如准确度),模型预测值,以及根据一些明确的假设做一系列的模型简化测试(ablation study)。

这里有一些在这一步的小技巧:

  • 固定住随机数种子:永远都要用一个固定的随机数种子,来保证当你跑代码两遍的时候你能获得一样的输出。这么做移除了一个变动的因子,从而让你保持理性。
  • 简化:确保关掉所有不必要的花里胡哨。比如说,在这一步一定要关掉任何数据增广步骤。数据增广是我们在之后要采用的一种正则化手段,但目前它只是另一个增加愚蠢的bug的机会。
  • 增强你的评估的显著程度:当画损失曲线时,记得在整一个(大的)测试集上去做评估。千万别只是在几个batch上干这事儿,然后依赖Tensorboard来做平滑。我们追求的是正确程度,因此我们愿意花时间来保持理性。
  • 在初始化时验证损失函数值:要验证你的损失函数起始于一个正常的值。比如说,初始化时,如果你正确地初始化了最后一层,你应该在softmax上头测量一下-log(1/n_classes)的值。同样的方法也可用于L2回归,Huber值等。
  • 做好初始化:要正确地初始化最后一层的权值。比如说,如果你在回归一批均值为50的值,那就把最终层的bias设成50。如果你有一个正负比为1:10的不均衡的数据集,把logits的bias设置一下,使得你的网络初始化时就能预测大概为0.1的概率值。设好这些bias能帮助加快收敛,避开类似于曲棍球棒型的损失曲线——那种在开始几个迭代中你的网络只是在学习bias的那种过程。(注:不是特别确定这一句把握住了原意)
  • 人工基准:查看除了损失值以外那些人为可解读、检查的指标(比如准确度)。每当可行的时候,衡量一下你的(人工的)准确度,和机器的准确度做点比较。可以把测试数据标注两次,第一次当做人工预测值,第二次当做真实值。
  • 独立于输入的基准:训练一个独立于网络输入的基准(比如最简单的就是把输入都设为0)。这应该比你用实际数据时表现得差。真的是这样吗?你的模型是否学会了从输入中提取什么信息呢?
  • 过拟合一个batch:过拟合一个只有几个样本的batch(比如说只有两个样本)。为了这么做我们得增加模型的容量(举例而言,增加层数或filter数量)并验证我们可以达到能达到的最低的损失值(比如说0)。当我们达到最低的损失值时,我也比较喜欢在同一张图上画出标签和预测值的变化,确保它们是一致的。如果不一致,肯定哪里出了bug,我们可不能继续下一步。
  • 验证训练时损失在下降:到这一步你希望是在你的数据集上欠拟合中,因为你用的是一个很小的模型。稍微增加一点儿模型容量。这时你的训练损失是不是如预期那样下降了呢?
  • 在数据进入网络前可视化一下:可视化数据最毋庸置疑的地方就是在你打出y_hat = model(x)之前的地方(对TensorFlow是sess.run)。这就是说——你要精确地可视化进入网络的东西,将最最直接的数据张量和标签解码、绘制出来。这可是唯一的“真理之源”。我自己已经记不清有多少次这一步揭露了数据预处理、增广过程中的错误,并把我给救了。
  • 可视化预测值动态变化:模型训练时,我喜欢在一个固定的测试用batch上面画出预测值。这些值的变化过程能给你一种非常直接的直觉,告诉你训练在如何进行。很多时候,当预测值波动时,人都能感受到模型在“挣扎”着拟合数据,表现出了不稳定性。从波动的大小也能看出来学习速率太高或太低。
  • 使用反向传播描绘计算依赖:你的深度学习代码经常包含复杂的(complicated)、向量化的(vectorized)、维度上广播了的(broadcasted)运算。我遇到过的一个相对常见的bug是人们把这些运算搞错了(比如view代替了transpose/permute)然后不小心在batch维度上搞混了信息。你的网络一般仍能继续训练,因为它能学会无视别的样本的数据,但这是令人沮丧的。(注:因为你不想让它以这种方式学习)。 一种debug这个问题(以及其他相关问题)的方式是把一个样本i对应的损失值设为1.0 (注:别的设为0),一路做反向传播直到输入,然后确认你只在这个样本对应的位置获得非0的梯度 (注:如果在batch维度上信息混了,那在其他样本对应的位置也可能有非0的梯度)。广义而言,梯度能给你关于网络里谁依赖于谁的信息,对debug会很有帮助。
  • 泛化一个特殊情形:这一点是一个更通俗意义上的编程贴士,但是我也经常看见人们想着一步登天,从零开始写一个非常通用的函数。我喜欢先写一个非常特化的函数,只针对我目前在做的一些事情,让这个函数能工作,然后再改得稍微泛化一点,确保我能始终得到一个正确的结果。通常这涉及到矢量化你的代码,意思是我会先写带有很多(for/while)循环的代码,然后针对每一个循环慢慢改成以矢量形式表达的代码。

3. 过拟合

到这一步我们既有了对数据集的良好认知,又有了一个完整的训练/评估代码流程。对任何模型我们都能(重复地)计算我们相信的一些测量值。我们同时也有了独立于输入的基准,一些简单基准的性能(当然,我们希望能超过这些性能指标),我们也有了人工在这个任务上的一些表现水平(这是我们希望达到的)。目前我们可以迭代出一个好的模型了。

我所喜欢的获得一个良好模型的方法有两个步骤:先搞一个很大的、过拟合的模型(这意味着仅关心训练损失值)然后恰当地正则化它(放弃一点训练损失,换来验证集上损失值的进步)。我喜欢这两步的原因是,如果我们用任何模型都不能获得一个较低的错误率,哪里就可能有一些问题、bug或是错误的配置了。

一些小技巧:

  • 选择模型: 想要达到不错的训练损失值,你得针对数据选择合适的模型架构。在这一点上我的首要建议是:别想着当英雄。我见过太多太多的人热衷于疯狂地、异想天开地把他们觉得有点意义的、各种新奇网络架构里的组件搭积木一样一层层往上堆。在你的项目的前期,务必要扛住这种诱惑。我始终都建议人们直接找一篇最相关的论文,然后复制粘贴论文里最简单能达到不错效果的模型。比如说:如果你要分类数据,第一轮里就别想着当英雄,直接复制粘贴一个ResNet-50吧。之后你当然可以做些别的来超越这个模型。
  • Adam很安全: 在早期设置基准线的时候我喜欢使用学习速率为3e-4的Adam(优化算法)。以我的经验,Adam对超参数更加通融,这当然包括了一个糟糕的学习速率。对于卷积神经网络,一个良好调参过的SGD优化算法几乎总是能略微胜过Adam一点点,但是它的最优学习速率值域更窄,更因问题而异。(如果你使用RNN或其他序列模型,Adam就更为常见。在你项目的初期,不要想着当个英雄去跟着那些相关论文做。)
  • 每次只做一点点复杂化: 如果你有多个信号 (注:大概指的是多方面的特征),我会建议你每次只增加一个来确保你能看见你所期待的模型水平的提升。不要一下子一股脑儿地把它们全甩给你的模型。此外还有一些别的增加模型复杂度的方式——你可以先放一张小图片进网络,之后再搞一张大图片,比方说。
  • 别相信缺省的学习速率衰减: 如果你正在为了其他领域而重新调节你的代码,一定要对学习速率衰减倍加小心。你可能不仅要为了不同的问题使用不同的衰减模式,还需要——或许更糟糕——典型的实现里,衰减模式会依赖当前的epoch数量,而epoch根据你的数据集尺寸不同又变化很大。比方说——ImageNet在第30个epoch的时候衰减为10倍。如果你用的不是ImageNet你当然不想用这种衰减模式。一不小心的话,你的代码就会把你的学习速率过早地降低到接近为0,导致模型不收敛。我自己干活的时候,总是整体地关掉学习速率递减(我用的是常数学习速率),然后在最后再做调整。

4. 正则化

理想情况下,我们会有了一个大模型,它至少在我们的训练集上头能拟合。现在,是时候做正则化,以一些训练准确度来换取验证集上的准确度了。一些贴士和技巧:

  • 获得更多的数据: 首先,在任何实际情况下,当前最好,也是最推崇的模型正则化方法就是增加更多的训练数据了。一个常见的错误就是工程上不断地花时间从一个小数据集上榨取价值,而同样的时间你可以拿来去手机更多的数据。据我所知,增加更多的数据是唯一的确保一个配置良好的神经网络单调且几乎无限制地提升性能的方法。其他的方法就是模型聚合(如果你能承担得起的话)了,但是这大概要5个模型才能实现更好的效果。
  • 数据增广: 顺着更多的真数据,第二好的就是半假的数据——可以试一些比较激进点的数据增广手段。
  • 创意增广: 如果半假的数据不顶事儿,全假的数据可能帮点忙。人们正在探索扩展数据集的创意手段。比如说:域随机化模拟,巧妙的复合方法——比如在场景里插入(可能是模拟出来)的数据,甚至是声称对抗网络(GAN)。
  • 预训练: 即便你有了足够的数据,使用预训练好的网络权值总不会害你的。
  • 坚持监督式学习: 别对非监督式预训练太激动。不同于08年的那篇博文告诉你的,就我所知,没有哪个版本目前报出在现今计算机视觉领域有很强的结果的。(不过在自然语言处理领域,应用于BERT模型还不错,很可能是因为文本的直白天性,以及更高的信噪比)
  • 更小的输入维度:除掉那些可能带有虚假信号的特征。如果你的数据集比较小,任何额外的虚假信号都会带来额外的过拟合的可能性。类似地,如果低阶细节影响并不那么大,试着输入小一点的图片。
  • 减小batch尺寸: 因为每个batch里面的归一化操作,小一点的batch尺寸对应着更强的正则化。这是因为单个batch的均值和方差算是真个数据集的均值和方差的近似,所以(用小的batch)你获得的缩放和偏置值能更多地“摇摆”一下你的batch。
  • 随机失活: 用上随机失活。对神经网络,用上2D随机失活(空间域上的随机失活)。由于随机失活与批归一化好像不怎么兼容,所以要用的时候还是要小心,别用太多。
  • 权重衰减: 增加权重衰减惩罚。
  • 早停止:根据你的可测量验证集损失值,提早停止训练,赶上你的模型恰好要开始过拟合的那个点。
  • 试用一个更大的模型: 我把这条放在最后,只放在早停止后面,因为在过去我有几次发现更大的模型理所当然在结束时更多地过拟合,但它们的早停止的版本的性能常常比小模型要好。

到最后,为了得到更多的信心相信你的网络是一个不错的分类器,我喜欢可视化网络的第一层的权值,来保证你获得的是很不错的、合情理的边缘提取。如果你第一层的卷积核看上去像噪声,那可能有些东西错了。类似地,网络里的激活值有时会显示出一些奇怪的差错,也意味着潜在的问题。

5. 微调

你现在应和你的数据集在一个“循环”里,在广阔的模型空间里寻找能获得较低的验证集损失值的模型结构。到这一步我的一些贴士和建议有:

  • 网格化中的随机搜索: 同时调节多个模型超参数的时候,使用网格化(地毯式)搜索来涵盖所有的参数配置听上去是个不错的想法,但是要记住,要用的其实是随机搜索。直觉上,这是因为神经网络总是对某些参数比对其他参数更敏感。在一种极端情况下,假如参数a对模型有很大影响而参数b几乎没啥影响,那你就该更彻底地采样a,而不是采样少数的几个点。
  • 超参数优化:网络上有好多精妙的贝叶斯超参数优化工具,我的一些朋友也告诉过我用它们获得了成功,但我的个人建议是,探索一个好的、广的模型空间的最佳方法是雇一个实习生 (* ̄︶ ̄) 开个玩笑~

6. 提取更多的价值

一旦你找到了最好的模型结构和超参数,你还能用一些技巧来提取模型里更多的价值:

  • 模型聚合: 聚合模型大概是一个在任何任务上都能提升2%精度的方法。如果你在测试模型时经不住这等计算,你可以研究一下通过黑科技蒸馏一下你的聚合模型。
  • 让它继续训练: 我常常看见人们在验证集损失值看上去不再降的时候想停下训练来。在我的经验里,神经网络可以在很长的时间里保持训练。有一次我不小心在放寒假开始的时候把一个模型留着训练了,结果我一月份回来的时候,模型的性能是超顶尖水平的!

结语

到此,你已经有了成功的所有要素:你有了技术层面上的深入理解,你有了数据集和问题定义,你有了一整套的训练、验证用的基础代码架构,获得了很高的模型准度,你还探索了逐渐复杂的其他模型,每一步都按照你所预测的那样获得了性能提升。你现在可以读好多论文,尝试好多实验,获得最最棒的结果了。祝你好运!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值