据说开始训练神经网络很容易,许多库和框架都以可以用30行的奇迹代码片段解决你的数据问题而自豪,给人一种即插即用的(错误)印象。常见的做法是:
1 | # plug your awesome dataset here your_data = |
通常可以获得简洁的API和抽象。比如:
1 | 'https://api.github.com/user', auth=('user', 'pass')) r = requests.get( |
这很酷!开发者勇敢地承担了理解查询字符串、url、GET/POST请求、HTTP连接等重担,并在很大程度上隐藏了几行代码背后的复杂性。这是我们所熟悉和期望的。
不幸的是,神经网络不是这样的。如果你稍微偏离了训练ImageNet分类器,那么它们就不是“现成的”技术。
在我以前介绍反向传播的博文中,我试图通过使用反向传播并将其称为“抽象泄漏”来说明这一点,但不幸的是,情况要糟糕得多。Backprop + SGD并不会神奇地让你的网络工作。Batch norm并不能神奇地使其更快地收敛。RNN不会神奇地让你“插入”文本。你可以用RL表示你的问题,但仅仅是这样并不意味着你应该这样做。如果你坚持使用这种技术而不了解它的工作原理,那么你很可能会失败。这使我想到……
当你修改或错误配置代码时,通常会遇到某种异常。比如你插入了一个包含预期字符串的整数。这个函数只需要3个参数。但导入失败了。这个键根本不存在。这两个列表中的元素数量不相等。此外,通常可以为某个特定功能创建单元测试。
这只是训练神经网络的开端。从语法上来说,一切都是正确的,但整件事的安排并不妥善,这真的很难判断。“可能的错误范围”很大,符合逻辑(与语法相反),并且很难进行单元测试。例如,在数据增强过程中,当你从左到右翻转图像时,可能忘记了翻转标签。你的网络仍然可以(令人震惊地)很好地工作,因为你的网络可以在内部学习检测翻转的图像,然后左右翻转它的预测。或者你的自回归模型会因为一个off-by-one的bug而不小心将它试图预测的东西作为输入。或者你试着修剪梯度,但结果却减少了损失,导致在训练期间忽略了异常值的例子。或者你从一个预训练的检查点初始化你的权重,但没有使用原始平均值。或者你只是搞砸了正则化强度、学习率、衰减率、模型大小等的设置。因此,只有在运气好的情况下,错误配置的神经网络才会抛出异常;大多数情况下,它会继续训练,但默默地使运行变糟。
因此,用“快速而激烈”的方法来训练神经网络是行不通的,只会导致痛苦。痛苦虽然是让神经网络正常工作的一个非常自然的部分,但它是可以通过彻底的、防御性的、偏执的,以及对几乎所有可能的事情进行可视化来减轻的。根据我的经验,与深度学习成功最相关的品质是耐心和关注细节。
如何训练一个神经网络
基于以上两个事实,我为自己开发了一个特定的过程,当我将神经网络应用到一个新的问题时,我都遵循这个过程。本文中我将尝试描述这个过程。
你会看到,它是非常重视上述两个原则的。特别是,它是从简单到复杂构建的,每一步我们都对将要发生的事情做出具体的假设,然后通过实验验证它们,或者进行调查,直到发现问题。我们极力避免的是同时引入大量“未经验证”的复杂性,这必然会引入错误/错误配置,而这些错误/错误配置将永远无法找到。如果像训练一样编写神经网络代码,你会想要使用非常小的学习率并猜测,然后在每次迭代之后评估完整的测试集。
1. 梳理数据
训练神经网络的第一步是完全不接触任何神经网络代码,而是从彻底检查数据开始。这一步至关重要。我喜欢花大量时间(以小时为单位)浏览数千个示例,了解它们的分布并寻找模式。幸运的是,我们的大脑非常擅长做这个。有一次,我发现数据中包含重复的例子。另一次我发现了损坏的图像/标签。我会寻找数据中的不平衡和偏差。我通常也会关注我自己对数据进行分类的过程,这个过程暗示了我们最终要探索的各种架构类型。
举个例子,只有局部的特性是否足够,还是需要全局上下文?有多少变量,它们以什么形式出现?哪些变量是假的,可以预先处理的?空间位置是否重要,或者我们是否想要将其平均池化?细节有多重要,我们能在多大程度上对图像进行降采样?标签有多少噪音?
此外,由于神经网络实际上是数据集的压缩/编译版本,因此你能够查看你的网络预测并了解它们可能来自何处。如果你的网络给你的预测与你在数据中看到的不一致,那就是有什么地方错了。
一旦获得了一种定性的感觉,那么写一些简单的代码来搜索/过滤/排序也是一个好主意,不管你能想到什么(例如标签的类型,注释的大小,注释的数量,等等),并将它们的分布和沿着任何轴的异常值可视化。异常值几乎总是能揭示数据质量或预处理中的一些bug。
2. 建立端到端的训练/评估框架+获取简单的基线
现在我们了解了我们的数据,那么就可以实现炫酷的多尺度ASPP FPN ResNet,并开始训练超棒的模型了吗?肯定不是。这是一条痛苦的道路。下一步是建立一个完整的训练+评估框架,通过一系列的实验获得对其正确性的信任。在这个阶段,最好选择一些你不可能搞砸的简单模型——例如一个线性分类器,或者一个非常小的卷积网络。我们希望训练这个网络,可视化损失,任何其他指标(例如准确性),模型预测,并在此过程中执行一系列带有明确假设的消融实验。
这个阶段的提示和技巧:
-
随机种子(random seed)。总是使用固定的随机seed来保证当你运行代码两次时,将得到相同的结果。这消除了变异因素,将有助于保持你的理智。
-
简化。确保不要有任何不必要的幻想。例如,在这个阶段一定要关闭所有数据增强。数据增强是一种正则化策略,我们稍后可能会将其合并进来,但目前它只是会引入一些愚蠢的bug。
-
在评估中添加有效数字。当绘制测试损失时,在整个(大型)测试集上运行评估。不要只在batches上绘制测试损失,然后依赖于在Tensorboard中平滑它们。我们要追求正确,很愿意放弃时间。
-
在初始化时验证损失。验证你的损失是否以正确的损失值开始。例如,如果你正确地初始化了最后一层,你应该在初始化时测量softmax的-log(1/n_classes)。同样的缺省值可以用于L2回归、Huber损失等。
-
正确地初始化。正确初地始化最后一层权重。例如,如果你要回归一些平均值为50的值,那么要将最终偏差初始化为50。如果你有一个不平衡的数据集,其正负比为1:10,那么在你的日志上设置偏差,使你的网络预测概率在初始化时为0.1。正确设置这些参数将加快收敛速度,并消除“hockey stick”损失曲线,在最初几次迭代中,你的网络基本上只是学习偏差。
-
human baseline。监控人类可解释和可检查的损失以外的指标(例如准确性)。尽可能地评估你自己(人类)的准确性,并与之进行比较。或者,对测试数据进行两次注释,对于每个示例,将一个注释作为预测,将第二个注释作为ground truth。
-
input-indepent baseline。训练一个独立于输入的基线(例如,最简单的方法就是将所有输入设置为零)。这应该比实际插入数据而不将其归零的情况更糟。也就是说,可以知道你的模型是否学会从输入中提取任何信息?
-
在单个批数据上过拟合。只过拟合包含少量例子(例如只有两个)的一个batch。为此,我们需要增加模型的容量(例如添加层或过滤器),并验证我们可以达到的最低损失值(例如0)。我也喜欢在同一个图中可视化标签和预测,并确保一旦达到最小损失,它们最终会完美地对齐。如果没有完美对齐,那么在某个地方就有一个bug,我们无法继续到下一个阶段。
-
验证训练损失的下降。在这个阶段,你可能希望在数据集上实现欠拟合,因为该阶段的模型是一个玩具模型。试着增加一点它的容量,在看看训练损失是否下降了。
-
在输入网络前进行可视化。在运行y_hat = model(x)(或tf中运行sess.run)之前,是可视化数据的正确时间。也就是说,我们需要准确地可视化输入给你的网络的东西,把原始的数据张量和标签解码并可视化。这是唯一的“真理之源”。这个过程为我节省了大量时间,并揭示了数据预处理和增强方面的问题。
-
可视化预测动态。在训练过程中,我喜欢将固定测试batch的模型预测可视化。这些预测如何移动的“动态”将为你提供关于训练进展的良好直觉。很多时候,如果网络以某种方式太过波动,显示出不稳定性,你可能会觉得网络“难以”拟合你的数据。非常低或非常高的学习率在波动的数量中也很容易被注意到。
-
使用反向传播来绘制依赖关系图。深度学习代码通常包含复杂、向量化和broadcast操作。我遇到过的一个比较常见的bug是,人们使用view而不是在某个地方进行transpose/permute,无意中混淆了批处理的维度信息。然而,你的网络通常仍然训练良好,因为它将学会忽略来自其他示例的数据。一种debug的方法是将某个示例i的损失设置为1.0,然后运行反向传播一直到输入,并确保只在i-thexample上得到一个非零梯度。也就是说,梯度提供了网络中的依赖关系信息,这对于debug非常有用。
-
一般化特殊案例。这是一种更为通用的编码技巧,但是我经常看到人们在使用这个技巧时产生bug,尤其是从头编写一个相对通用的函数时。我喜欢为我正在做的事情写非常具体的函数,让这个函数能work,然后再一般化,确保得到相同的结果。这通常适用于向量化代码,我会先写出完整的循环版本,然后再将它转换为向量化代码。
3.过拟合
在这个阶段,我们应该对数据集有一个很好的理解,我们有完整的训练和评估pipeline。对于任何给定的模型,我们可以(可重复地)计算出一个可信的量度。我们还可以利用独立于输入的baseline的性能(并击败了这些基线性能),我们大致了解人类的表现。这一阶段的任务是对一个性能不错的模型进行迭代。
找到一个好模型可以分为两个极端:首先得到一个足够大的模型,它可以是过拟合的,然后在适当调整(放弃一些训练损失,以改善验证损失)。我认为采取这两个阶段的好处是,如果我们根本无法使用任何模型达到足够低的错误率,这种方式可能再次暴露一些问题、bug或错误配置。
关于这个阶段的一些提示和技巧:
- 挑选模型。为了获得良好的训练损失,需要为数据选择合适的架构。我的建议是:不要逞英雄。我见过很多人疯狂的人,将神经网络工具箱的各种工具像堆乐高积木一样堆叠在各种奇异的架构中。应该在项目早期阶段尽力避免这样做。
我总是建议先找到最相关的一些论文,先把其中最简单的架构原样照搬过来,以获得良好的性能。比如你做得是图像分类,不要想着当英雄,先把ResNet-50拿过来用。后面可以做一些更自定义设置和改进,并实现比它更好的性能。
-
选Adam总没错。在设定baseline方法的早期阶段,我喜欢使用学习率为3e-4的Adam架构。根据我的经验,Adam对超参数更加宽容,包括不良的学习率。对于ConvNets来说,一个经过适当调整的SGD几乎总会略优于Adam,但前者最佳学习率区域要窄得多,而且受到具体问题所限。(如果使用的是RNN和相关的序列模型,那么在项目早期阶段更常用Adam。再说一遍,不要逞英雄,多参考相关论文。)
-
一次只针对一个对象进行复杂化。如果有多个信号进入分类器,建议逐个引入,确保每次信号的引入都获得预期的性能提升。一开始不要一股脑地把全部信号都喂给模型。增加复杂度还有别的方法,比如可以尝试先插入较小的图像,然后再放大,诸如此类。
-
不要过于相信默认的学习率衰减。如果是重用来自其他领域的代码,那么在处理学习速度衰减时一定要非常小心。您不仅希望针对不同的问题使用不同的衰减计划,而且 - 更糟糕的是 - 在典型的实施中,计划将基于当前的epoch数,该值会根据数据集的大小而广泛变化。
比如,ImageNet将在30个epoch上衰减10倍。如果你没有训练ImageNet那么这肯定不是想要的结果。如果你不小心,代码可能会过早地将学习率趋零,导致模型无法收敛。在我自己的研究中总是完全禁用学习率衰减(学习速率为常数)并在最后进行调整。
4.正则化
理想情况下,我们要处理的是大型模型,至少能够拟合训练集。现在是时候通过放弃一些训练准确性,进行一些正则化处理,以获得一些验证准确性。以下是这方面一些提示和技巧:
-
获取更多的数据。首先,在任何实际环境中,最佳和首选方法是添加更多真实的训练数据。当可以收集更多数据时,就不要再花费大量时间尝试从小型数据集中挤性能了。据我所知,添加更多数据几乎是唯一无限期改善配置良好的神经网络性能的方式。还有一种方式是合奏(如果你能负担得起的话),但是在5个模型之后最高。
-
数据增强。仅次于真实数据的半真实的数据,需要尝试更积极的数据增强。
-
创造性数据增强。如果半假数据不能做到这一点,假数据也许可以。人们正在寻找扩展数据集的创新方法;例如,域随机化,模拟的使用,巧妙的混合,比如将(可能模拟的)数据引入场景,甚至是GAN中。
-
预训练。即使有足够的数据,如果可以,使用预训练网络也几乎没坏处。
-
坚持监督式学习。不要对无监督的预训练抱有过分的信心。据我所知,没有任何一种无监督学习在现代计算机视觉任务上有很强的表现(虽然现在NLP领域诞生了BERT等优秀模型,但这很可能是因为文本数据更成熟的形式,以及更高的信噪比)。
-
更小的输入维度。去除可能包含虚假信号的特征。如果数据集很小,任何添加的虚假输入都可能造成过拟合。同样,如果低级细节无关紧要,请尝试输入更小的图像。
-
较小的模型。很多时候可以在网络上使用域知识约束来减小模型大小。例如,过去在ImageNet骨干网顶部使用全连接层很流行,但现在已改用简单的平均池化,从而去掉了过程中的大量参数。
-
减小批大小。由于归一化是基于批大小的,更小的batch size会具有更明显的正则化效果。这是因为批量经验均值/标准是完整均值/标准的更近似版本,因此标度和偏移量“摆动”您的批次更多。
-
Drop。添加dropout。对ConvNets使用dropout2d(空间丢失)。建议谨慎使用dropout,因为似乎不太不适合批归一化。
-
权重衰减。增加权重衰减惩罚。
-
early stopping。early stopping停止训练。根据验证损失停止训练,在出现过拟合之前获得模型。
-
尝试更大的模型。我最后提到这一点,并且只是在提前停止之后,但我在过去曾经发现过几次大模型最终过拟合,但是它们的“早停”性能往往比小模型的性能要好得多。
最后,为了进一步确保网络是一个合理的分类器,我喜欢对网络第一层权重进行可视化,并确保获得有意义的边缘。如果第一层滤波器看起来像噪音,那么可能需要去掉一些东西。类似地,网络中的激活函数有时也会有异常出现,暴露出一些问题。
5.精细调整
现在应该使用数据集探索宽泛的模型空间,以获得低验证损失的体系结构。下面是一些提示和技巧:
-
随机网格搜索。为了同时调整多个超参数,使用网格搜索确保覆盖所有设置,这听起来很诱人,但请记住,最好使用随机搜索。直观地说,这是因为神经网络通常对某些参数比其他参数更敏感。在极限情况下,如果一个参数很重要,但是改变另一个参数没有效果,那还不如对第一个参数进行彻底采样。
-
超参数优化。目前有很多花哨的贝叶斯超参数优化工具箱,也有一些成功应用的实例,但我个人的经验是,探索优质大宽度模型和超参数空间的最好方式是找个实习生。哈哈,开句玩笑。
6.性能压榨
在确定了最佳类型的体系结构和超参数后,仍然可以利用一些技巧最后“压榨”一下系统的性能:
-
集成。模型是一种非常有保证的方法,可以在任何事情上获得2%的准确率。如果您在测试时无法负担计算,请考虑使用黑暗知识将您的整体提升到网络中。
-
保持训练。我经常看到人们在验证损失趋于平稳时就想停止模型训练。根据我的经验,网络会长时间不间断地进行训练。有一次我在冬假期间不小心没有停止训练,当我第二1月份回来时,发现模型性能达到了SOTA水平。
最后
走到这一步,相信你已经获得了成功的所有要素:对技术,数据集和问题有了深刻的理解,建立了整个模型的训练/评估基础设施,并对其准确性有了很高的信心,并探索了越来越复杂的模型,以预测每一步的方式获得性能改进。现在是时候开始阅读大量论文,尝试大量实验,准备好获得SOTA结果吧。祝你好运!