文献漫步#1:Andrej Karpathy:一个关于神经网络训练的经验总结

观前提示
Tips
本文于2019年4月25日发布于其个人博客。虽然发布有了一段时间了,但时至今日其中的许多思想值得借鉴。笔者在阅读文献时对该博客进行了翻译和总结,整理成该文章供后续学习(不是机翻)。朋友们可以从“阅读原文”获取博客原文链接。
作者介绍:Andrej Karpathy,斯坦福大学计算机博士,师从李飞飞,研究方向为计算机视觉和自然语言处理的交叉领域,CS231n首席讲师,目前在特斯拉担任人工智能高级主管。
引子
Intro
前几天我发了一个帖子,讨论了神经网络训练中的一些常见错误,尽管写得很琐碎,没想到这个帖子得到了超乎想象的关注。显然,很多人在“卷积神经网络如何工作”和“自身的卷积网络表现优秀(SOTA)”之间存在着巨大的认识空白。
所以我想展开这个话题说一说,同时让大家学会快速避坑。在开始之前,先说造成这种现象的两个关键诱因吧。
01
网络训练缺乏具体解释
毫无疑问,现在开始一场深度学习的训练已经越来越容易了。很多问题可以在30行代码内完成,给人训练网络非常容易的错觉。这些代码往往长成这样:
>>> your_data = # plug your awesome dataset here>>> model = SuperCrossValidator(SuperDuper.fit, your_data, ResNet50, SGDOptimizer)#conquerworldhere
看上去很酷对不?这些简洁的代码背后其实是依赖项将接口的高度整合。正如对网页的请求指令,你只需要调用requests依赖,在get函数中输入网页参数就行,这一切都是非常自然的。但神经网络不是这样“所见即所得”的,很多参数不会像你想象中那样执行:反向传播不一定有用,批处理未必收敛得很快……这些都是由于你不够了解网络的运行机制造成的。
02
你为什么逐渐放弃了深度学习
很多初学者在调试的时候都会面临这样或那样的报错:在该输入字符的地方输入了一个数字;这个函数只允许输入3个参数;依赖项导入失败;那个关键字不存在;列表元素不一致等等,单元调试已经是家常便饭。
其实这只是网络训练的开始,哪怕所有的结构都语法合理,整个网络也未必会正确地运行,这真的很玄学,也给调参带来了巨大困难。例如,虽然你在扩充数据集时只随机化了图像同时忽略了随机化标签,你的网络照样可以运行,就是结果有那么一些奇怪罢了。又或者,自动回归网络在输入的边界条件上存在bug(译者注:这个我还真有体会,还记得某目标检测算法不能处理一些目标框设在边上的图像,会报错)。又或者,你本来只是想修改一下梯度,结果却修改了损失函数,导致一些图像无法参与训练。又或者,你只是想修改正则化、学习率、衰减率、模型大小等等,幸运的话,你会收到一个报错;可是大部分的时候,模型只会默默地训练,然后很久后你发现新的模型并不work。
好了,吐槽了这么多我只想说一件事,想速成神经网络几乎是不可能的。虽然在做成一个work的神经网络前经历一系列痛苦却是必需的,但这些焦虑可以通过可视化结构来缓解。于我而言,在深度学习领域取得成功最重要的是耐心和对细节的重视。
我的经验
My Experience
基于上述事实,我在使用神经网络解决新问题时摸索出了一套独特的流程。这套流程由两个原则指导。一是在讲述的时候我会由简单到复杂,同时每一步都会给出具体例子证明它的有效性。二是我们尽可能避免一次性出现大量未经验证的复杂参数,这会导致今后出现无法解决的bug。如果你在写神经网络代码时感觉就像训练一样东西,那你必然会先使用非常小的学习率然后猜测应该的学习率最后才是评估整个测试集。(译者注:这一句不太通顺但作者就是这么写的qwq)
01
彻底了解你的数据
训练神经网络的第一步不是去GitHub上找什么代码,而是彻底研究你的数据。这一步很关键,我会花数小时时间浏览我的数据,研究它们的分布,寻找数据集特征。相比计算机,这一点对人来说很容易。有一次我发现数据集里包含了重复的数据;另一次我还发现了崩坏的样本,我这么做是为了寻找数据的不平衡性和偏差。我也会特别留意数据分类的过程,因为这与最终网络的结构直接挂钩。这个过程我会时常问自己:是样本数据自身的特点就已经满足需要还是需要洞察全局数据?异常样本的数量和形式怎么决定?哪些是需要被预处理出去?样本的空间位置是否影响最终的判断?(样本)细节会影响最终判断多少以及需要下采样多少次?标签噪声有多大?等等。
来自ImageNet的一些样本
除此之外,既然神经网络可以看做你的数据集的一个压缩版本,你也应该明白你的网络预测从何而来。如果你的网络出现了与事实不符的预测,就意味着网络结构不太work。
一旦你有了这样的想法,用简单的代码将数据的不同类别可视化出来将是一个不错的主意。特别是对异常样本的处理,能够极大地降低代码出问题的概率。
02
建立端到端训练/评估框架、训练基线
现在我们已经理解数据了,这就意味着我们可以接触ResNet这样的训练网络了吗?当然不是。下一步要做的是建立一条完整的训练框架,并通过一系列实验证明正确性。这个阶段,你最好使用一些不容易“翻车”的简单模型,例如线性分类器,或者很小的卷积网络。我们要训练它,观察它的损失,评估准确度,预测结果。同时做一些验证性实验验证假设。
《论基线的自我修养》[1]
在这个阶段可以用到的技巧:
随机种子法:使用随机种子避免得到完全相同的输出,可以保持模型的鲁棒性。
最简化:确保任何多余的设置不被执行,比如数据增强。数据增强是一个在后续实验中进行正则化处理的操作,但是现阶段只会带来一些奇怪的bug。
在验证集里增加必要的数据:验证集评估时损失函数需要对整个验证集进行评估。不要尝试用批处理+可视化程序的平滑方案。因为我们在追求准确率,这些操作只会让模型不思进取。
确保初始损失的正确性:确保你的损失在正确的损失值上开始。例如,当你初始化第一层网络,你需要在初始化时将损失softmax化。同样的操作也要在L2正则化等其他损失上进行。
正确地初始化:确保第一层的权重初始化正确。例如,如果需要对一些均值为50的数据进行回归,初始化的值就设成50。如果你的数据集正负样本极度不均衡(比如1:10),你需要对偏置进行修改,让网络的初始预测为0.1。正确地设置这些初始值可以加快收敛速度,同时避免只学习偏置导致你的损失曲线呈现“曲棍球状”。
人工判断基线:监视可以被度量的指标,而不只是损失(例如准确度)。只要可以,就用人工准确度与你的算法相比较。也可以对测试数据进行两次注释,对于每个示例,将一个注释作为预测,将第二个注释作为答案。
建立输入相互独立的基线:例如当输入为零时,预测效果应该比你实际输入的值更差,如果不是这样,你的模型是否学了一些输入外的特征?
过拟合某一批次:过拟合一个样本少的单一批次。这么做可以提升模型容量(如增加层数和过滤器等)来证明损失可以降低到0。我也经常可视化自己的预测结果,以确保预测收敛到正确的位置。如果这一步出现了问题,我们不能继续进行下去。
确保训练损失的下降:到了这一步你一定希望你的数据集是欠拟合的,你要完善你的网络,看看损失是否同预期一样下降。
在网络之前完成样本可视化:进行可视化的位置一定是在调用网络之前。因为你需要保证进入网络的内容可控,所以将原始数据和标签进行可视化才是正确的做法。值得一提的是,这一点曾多次帮我解决了数据预处理和数据增强中的困难。
动态预测可视化:我倾向于在模型训练过程中可视化固定批次的模型预测结果。将预测结果动态化可以帮助你深入洞察模型处理的过程。很多时候,震荡的损失函数会让你觉得数据很难被网络调教,不合理的学习率也容易造成这样的问题。
使用反向传播绘制依赖关系:你的深度学习代码总会包含一些复杂的、量化的、用于传播的操作。我几次遇到的一个相对常见的错误是,你会弄错它们(例如,使用视图(view)而不是在某处进行转置(transpose)/置换(permute)),并且无意间在批次维度中混合了信息。糟糕的是你的网络照样训练地OK,因为它会学着去忽略其他样本的信息。这种问题的应对办法可以是,把损失设置地很细,比如对某个样本所有输出的求和,反向传播到输入,并确保只在这个样本上得到一个非零的梯度值。同样的策略可以用在时序收敛问题上。更普遍的,梯度告诉你网络上起作用的部分,这对后续调试很有用。
概括一个具体例子:这个是一个更普适的编程思想,我常常看到有人面对一堆无法理解的代码,无法调试其中的问题。试着针对函数列一个提纲吧。我常常会对特定功能编写函数,确保能够工作,然后将函数融合到一个更大的函数中。这通常适用于向量化代码,我几乎总是先写出完全循环的版本,然后一次循环将其转换为向量化代码。
03
过拟合
到了这个阶段我们已经对数据集和整个训练/评估过程有了充分的认识。对于任何已知的模型我们都可以给出对应的度量标准了。我们也知道了为什么要有基线,怎么结合人工判断。现在是时候迭代出一个好的模型了。
来自ResNet家族的漂亮的损失曲线[2]
找到一个好模型通常需要两步。首先模型通常足够大来过拟合(训练损失能足够低),同时能够正确地正则化(牺牲训练提高验证)。事实上,如果模型不能达到足够低的损失,它还是会出现各种各样的错误。
在这个阶段可以用到的技巧:
挑选模型:为了达到一个较好的损失,你需要选择正确的网络结构。这里我有个小建议:不要逞能。我见过很多人,他们像搭积木一样,将自己的模型搭成一个“四不像”。你需要遏制这种冲动,尤其是开始挑选的时候。我常常提醒他们要从最接近的论文里选择最简单的模型进行试验,往往这些模型也能取得好的结果。例如,如果你尝试给图像进行分类,不要上来就用ResNet50。你可以稍后再做一些更自定义的操作,并优化模型。
Adam损失是合理的:在早期的基线设计中我会选用Adam损失,学习率为0.0003。在我的理解中Adam对超参数的容忍度更高,特别是学习率。对于卷积网络,一个好的SGD设计效果会略比Adam更优,但是优化学习率的区间更小和更加需要具体问题具体分析。(注:如果你使用RNN等序列模型,用Adam会更加常见,再次强调,在项目初期不要逞能,只选最接近的论文用到的模型)
一次只复杂一点点:如果你要输入很多信号,那我建议你一次只处理一个信号,并确保有好的效果。比如,你可以先试着输入小图片,再处理一些大一点的。
不要迷信默认的学习率衰减(decay):如果你是搬运的别处的代码,一定要小心学习率衰减的设置。这不只是对于不同的问题使用不同的衰减这么简单;而是对于特定的模型结构,使用特定的衰减是必需的,甚至会受到数据集规模的影响。例如,ImageNet每30轮迭代衰减十倍,如果你不使用ImageNet你很可能不需要这些策略。如果你不够小心,你的代码可能会让学习率过快地降到0,让你的模型不再收敛。我在实际项目中经常关闭衰减(事实上我的学习率是恒定的),直到项目最后才会调整这个参数。
04
正则化
走到这一步,我们应该有了一个可以很好地处理训练集的模型了。现在要做的工作是完成模型的正则化,使得模型具有更好的泛化性能。
数据增强[3]
在这个阶段可以用到的技巧:
获取更多的数据:这是最好的也是最推荐的正则化模型的思路。一个常见的误区是在本来就很小的数据集中试图压榨出更多的信息。但根据我的理解,只有切实地加入更多的数据才能保证神经网络的性能直线上升。当然我们也不能忽视模型融合的作用,但是参与融合的模型不应超过五个。
数据增强:半假数据(half-fake)是仅次于真实数据的数据增强形式。
更多数据增强方式:如果半假数据并不奏效,假数据也许可以考虑。这方面人们有很多研究:GAN、域随机化(domain randomization)、模拟数据引入等。
预训练:如果可以的话,使用预先训练的网络几乎不会有问题,哪怕你已经有了足够的数据。
坚持监督学习:不要对无监督学习抱有太多期待,据我所知,无监督学习还没有哪个模型能够胜任如今的计算机视觉任务。(注:虽然在自然语言处理领域BERT和它的相关模型这些无监督项目做得还不错,但这得益于文本的独特性和更高的信噪比)(译者注:半监督学习是当前很流行的领域,感兴趣的朋友可以看看)
缩小输入的维度:移除那些可能带来干扰的特征。任何多余的输入只会让你的模型过拟合的概率大大提升。同样地,如果低品质的图像(译者注:这里应该指被多次下采样的图)不能带来优势,那就换一张小一点的图吧。
让模型规模变小:很多情况下你可以使用域的知识让网络体积变小。例如,在ImageNet的backbone的最后面增加一个全连接层曾经是一个很时髦的操作,不过后来被能够缩小大量参数的平均池化层给代替了。
减小批处理的规模:这是由于基于更少批处理数量的归一化能得到更好的正则化效果。具体来说就是,这种批次的数字特征(期望/方差)能更真实地反映整个数据集的情况。
Drop:增加Dropout层,对于卷积层可以使用二维Dropout层。不过这一层的增加要慎重,因为对于批归一化的表现似乎并不理想。
权重衰减:增加对权重衰减的惩罚。
提前终止:根据验证集的损失判断是否要结束训练避免过拟合。
尝试更大的模型:我特意强调这点一定要放在最后,特别是要放在“提前终止”之后。不过我也发现过几次,更大的模型会过拟合地更加彻底,但是它们“提前终止”时的表现效果也是远优于小模型的。
终于,现在可以自信地说你的网络是一个合格的分类器了。我一般会可视化网络的第一层权重以确保(过滤器)有意义。如果你的第一层过滤器看上去像噪声,一定是哪里出问题了。对于层中的激活函数也可以用类似的思路检测。
05
调优
你现在就将处于如何从数据集中挖掘出适合模型改进的更多空间以降低损失这个“无限循环”中了。调优的过程无止境,这个阶段可以用到的技巧:
随机网格搜索:这对于同步地处理多个超参数有很大帮助,不过要注意的是随机性(详见延伸阅读1)。这是由于在神经网络中有些参数比其他参数敏感。在极端情况下,如果参数a能够改善效果但是参数b对效果毫无影响,那你就要多对参数a进行调整。
超参数优化:现在的编程环境有一堆的基于超参数优化的工具箱,我有好一些朋友都宣布了使用成功。不过我的经验是,最好找一个实习生帮你做:)
06
榨取性能
当你挑选出了最好的架构和超参数,接下来要做的就是增加亿点细节了。
模型融合:模型融合可以提高大约2%的准确度,经验之谈。如果你没有足够的算力,可以试试知识蒸馏(详见延伸阅读2)。
让它学下去:我经常看见有人在验证集停止收敛时试着停止训练。事实上网络会训练地远比我们想象地远。有一次我无意中把网络训练了一个圣诞假期,然后我一月份回来时它就最佳了。
结论
Conclusion
当你看到这里时,恭喜你,你已经具备了所有成功的要素:对技术、对数据集和对问题的深度认知,你已经建立了整个训练/评估框架,并在实验时取得了很好的成绩。同时你具备了去研究更大模型的能力,如何在每一步中向着更好的方向改进。论文在向你招手,去做更多的实验吧,取得更好的结果吧。祝你好运!
延伸阅读:
1.Random Search for Hyper-Parameter Optimization
2. Distilling the Knowledge in a Neural Network
注:这两篇论文可以在公众号后台回复【论文】获取
我想说的
Epilog
作为深度学习领域深耕多年的学者,Karpathy先生的观点浅显却不失独到,能很好地解答不少初学者在项目实施过程中的困惑。值得一提的是,他将整个项目从无到有的过程能毫无保留地分享给大家,对于初学者可以尽快进入状态,对于科研工作者的优化方向同样具有指导意义。
作为“文献漫步”系列的第一篇文章,我也经过了大量时间的阅读和挑选,才选择以这篇博文作为这个系列的开始。并且我会将其和“读研随笔”正片做一些区分,那边更侧重自己的感悟,这边更侧重具体文献的叙述。这些文献的内容很可能不是第一时间发表的,朋友们可以从一些更加专业的公众号看见最新的文章,但是我可以保证我这边的文章主要是自己整理出来的,我也会增加一些自己阅读过程中的感悟,欢迎各位批评指正。
假期来了。是时候多更一些了。
图片来源:
[1]https://laptrinhx.com/implementing-efficientnet-a-powerful-convolutional-neural-network-2754178756/
[2]https://modelzoo.co/model/resnet-mxnet
[3]https://bair.berkeley.edu/blog/2019/06/07/data_aug/
点击下方“阅读原文”可以获取博客原文
你“在看”我吗?

版权声明