A Recipe for Training Neural Networks [中文翻译, part 1]
最近拜读大神Karpathy的经验之谈 A Recipe for Training Neural Networks https://karpathy.github.io/2019/04/25/recipe/,这个秘籍对很多深度学习算法训练过程中遇到的各自问题进行了总结,并提出了很多很好的建议,翻译于此,希望能够帮助更多人学到这些内容。
译文如下:
几周前,我发布了一条关于“最常见的神经网络错误”的推文,列出了一些与训练神经网络相关的常见问题。这条推文得到了比我预期的要多得多的认同(包括网络研讨会:))。显然,很多人个人遇到了“卷积层是这样工作的”和“我们的卷积网络达到最先进结果”之间的巨大差距。
所以我认为如果清空我尘土飞扬的博客并将这个推文扩展到这个主题应该得到的长篇形式应该是有趣的事情。然而,与其列举常见的错误或深入分析它们,我更想深入挖掘并讨论如何避免出现这些错误(或者非常快速地修复它们)。这样做的关键是遵循某个过程,据我所知,这个过程并没有文档记录下来。让我们从促使我做这个的两个重要发现开始吧。
1. 神经网络训练的困难是一种假象
据称很容易开始训练神经网络。许多图书和框架都以展示了如果采用30行代码解决您的数据问题,并以此而自豪。这给大家一个非常错误的印象,即这些东西是即插即用。常见的示例代码如下:
>>> your_data = # 导入数据 >>> model = SuperCrossValidator(SuperDuper.fit, your_data, ResNet50, SGDOptimizer) # 由此征服世界这些
这些库和示例激活了我们大脑里面熟悉标准软件的部分 - 通常它们认为干净和抽象的API是可以轻易获得的。 比如,后面的功能可以采用如下调用:
>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass')) >>> r.status_code 200
这很酷! 一个勇敢的开发人员已经承担了理解查询字符串,URL,GET / POST请求,HTTP连接等等的负担,并且在很大程度上隐藏了几行代码背后的复杂性。 这恰恰是我们熟悉和期待的。 不幸的是,神经网不是那样的。 它们不是“现成的”技术,第二个与训练ImageNet分类器略有不同。 我试图在我的帖子“是的你应该理解反向传播”中通过反向传播的例子说明这一点,并将其称为“漏洞抽象”,但不幸的是情况更加可怕。 Backprop + SGD并没有神奇地让你的网络正常工作。 Batch Norm也不会神奇地使其收敛得更快。RNN不会神奇地让你“插入”文本。 只是因为你可以将你的问题采用增强学习来建模,并不意味着你应该如此。 如果您坚持使用该技术而不了解其工作原理,则可能会失败。 这让我想到......
2. 神经网络训练无声地失败
当您写错或错误配置代码时,您通常会遇到某种异常。你输入了一个整数,但这本来应该是一个字符串的!某函数只需要3个参数!导入失败!键值不存在!两个列表中的元素个数不相等。此外,通常不会为特定功能创建单元测试。
解决了这些问题,也只是训练神经网络的开始。一切都可以在语法上正确,但整个训练没有妥善安排,而且很难说清楚。 “可能的错误面”是很大的,逻辑(与语法相反)层面的问题,并且对单元测试非常棘手。例如,在数据增强期间左右翻转图像时,您可能忘记翻转数据标签。您的网络仍然可以(令人震惊地)工作得非常好,因为您的网络可以在内部学习检测翻转的图像,然后左右翻转其预测。或许你的自回归模型会因为一个偶然错误而意外地将它想要预测的东西作为输入。或者你想要修剪你的梯度但是修剪了损失,导致在训练期间异常样本被忽略。或者您从预训练模型初始化了您的权重,但没有使用原始均值。或者你只是用错了正则化权重,学习率,衰减率,模型大小等。因此,错误设置的神经网络只有在你运气好的时候才会抛出异常;大部分时间它会训练,但默默地输出看起来有点糟糕的结果。
因此,“快速和激烈”方法训练的神经网络并不能发挥其作用,我们只会遭受其痛苦。 现在,痛苦是让神经网络运作良好的一个非常自然的过程,但可以通过对训练过程中的所有细节了然于胸来减轻训练过程的折磨。 在我的经验中,与深度学习成功最相关的品质是耐心和对细节的关注。
秘方
鉴于上述两个事实,我已经为自己开发了一个特定的过程,使我能够将神经网络应用于新问题。稍后我会竭力描述如何做到的。 你会发现它非常重视上述两个原则。 特别是,它遵循从简单到复杂的规律,并且在每一步我们对将要发生的事情做出具体假设,然后通过实验验证它们或进行检查直到我们发现了问题。 我们试图防止的是同时引入了许多复杂“未经验证的”问题,这必然会导致需要花很多时间去查找的错误/错误配置。 如果编写您的神经网络代码就像训练一样,您需要使用非常小的学习速率并猜测,然后在每次迭代后评估整个的测试集。
1.了解你的数据
训练神经网络的第一步是根本不是接触任何神经网络代码,而是从彻底检查数据开始。这一步至关重要。我喜欢花费大量时间(以小时为单位)扫描数千个示例,了解它们的分布并寻找模式。幸运的是,你的大脑非常擅长这一点。有一次我发现了数据中包含重复的例子。另一次我发现了错误的图像/标签对。我通常会寻找不均衡的数据,也会关注自己对数据的分类过程,这些过程暗示了我们最终会尝试的各种架构。例如 -我们需要局部特征还是全局上下文?数据有多少变化,这些变化采取什么形式?什么变化是假的,是可以预处理的?空间位置是否重要,或者我们是否想要将其平均化?细节有多重要,我们可以在多大程度上对图像进行下采样?标签有多少噪声?
此外,由于神经网络实际上可以看作压缩/编译的数据集,因此您将能够查看网络的(错误)预测并了解它们的来源。如果你的网络给你的预测看起来与你在数据中看到的内容不一致,那么就会有所收获。
一旦获得定性的感知,编写一些简单的代码来搜索/过滤/排序也是一个好主意,无论你能想到什么(例如标签的类型、大小、数量等),并可视化它们的分布,和沿任何坐标轴的异常值。异常值尤其能揭示数据质量或预处理中的一些错误。
2.设置端到端的训练/评估框架+获得基准
当我们了解了数据之后,我们可以采用我们超级精彩的多尺度ASPP FPN ResNet训练牛X的模型吗? 答案是不。 这是一条充满痛苦的道路。 我们的下一步是建立一个完整的训练+评估框架,并通过一系列实验验证其正确性。在这个阶段,最好选择一些你能正确使用的简单模型 - 例如 线性分类器,或非常小的ConvNet。 我们希望对其进行训练,可视化损失,任何其他指标(例如准确度),模型预测,并在此过程中使用明确的假设进行一系列实验。
这个阶段的需要注意的地方和建议主要包括:
- 设置固定的随机种子。始终使用固定的随机种子,以保证当您运行两遍代码两次时,可以获得相同的结果。这消除了随机因素,并将帮助您保持理智。
- 简化。确保禁用任何不必要的尝试。例如,在此阶段肯定要关闭任何数据扩展。数据扩充是一种我们可能在以后合并的正规化策略,但是此刻它也可能引入一些愚蠢错误。
- 在评估时尽量准确。在绘制测试损失时,对整个(大)测试集进行评估。不要只是在批量上绘制测试损失,然后依靠在Tensorboard中平滑它们。我们追求正确,并且非常愿意放弃保持理智的时间。
- 验证初始的损失。验证您的损失函数是否以正确的损失值开始。例如。如果正确初始化最后一层,则应在初始化时查看softmax上的-log(1 / n_classes),这默认值可以为L2回归,Huber损失等。
- 正确初始化。正确初始化每层权重。例如,如果你正在回归一些平均值为50的值,那么将偏差初始化为50。如果你有一个正负样本比例为1:10的不平衡数据集,请在你的数据上设置偏差,使网络初始化时就可以预测为0.1。正确设置这些将加速收敛并消除“曲棍球棒”损失曲线,因为在最初的几次迭代中,您的网络基本上只是学习偏差。
- 人工评测。设置损失之外,人工可以查验和解释的的指标(例如准确性)。尽可能评估您自己(人类)的对该问题的准确性并与之进行比较。或者,对测试数据进行两次标注,并且对于每个示例,分别标注预测值和真实值。
- 指定和数据无关的标准。训练一个独立于输入数据的标准,(例如,最简单的方法是将所有输入设置为零)。这应该比实际插入数据时更糟糕,而不会将其清零。这可以吗?即你的模型是否学会从输入中提取任何信息?
- 在少量数据上过拟合。看看模型是否能够在仅仅少数几个数据(例如少至两个)上过拟合。为此,我们增加了模型的容量(例如添加层或过滤器)并验证我们可以达到可实现的最低损失(例如零)。我还想在同一个图中同时显示标签和预测,并确保一旦达到最小损失,它们就会完美重合。如果他们不这样做,那么某个地方就会出现一个错误,我们无法继续下一个阶段。
- 验证减少训练损失。在这个阶段,你希望模型在数据集上欠拟合,因为你正在使用玩具模型。尝试稍微增加容量,看看您的训练损失是否应该下降?
- 在训练模型前再次查看数据。在您y_hat = model(x)(或tf中的sess.run)之前,查看数据是否正确。也就是说 - 您希望确切地了解将要网络中的内容,将原始张量的数据和标签解码并将其可视化。这是唯一的“事实来源”。我无法计算这个多少次节省了我的时间,并揭示了数据预处理和扩充中的问题。
- 预测过程动态可视化。我喜欢在训练过程中对固定测试数据上的预测结果进行可视化。这些预测如何动态发生的,将为您提供令人难以置信的直觉,使您能够了解训练的进展情况。很多时候,如果网络以某种方式摆动太多,显示出不稳定性,就有可能感觉网络难以拟合您的数据。学习率非常低或非常高,抖动量也很明显。
- 使用反向传递来绘制依赖关系。您的深度学习代码通常包含复杂的,矢量化的和广播的操作。我遇到的一个相对常见的错误是人们弄错了(例如他们使用视图而不是某处的转置/置换)并且无意中混合了batch的维度信息。不巧的是,您的网络通常仍然可以正常训练,因为它会学会忽略其他示例中的数据。调试此问题(以及其他相关问题)的一种方法是将某个示例的损失设置为1.0,将反向传递一直运行到输入,并确保仅在该样本上获得非零梯度。更一般地说,梯度为您提供了网络中数据的依赖关系信息,这对调试很有用。
- 设置一个特例。这是一个更通用的编码技巧,但我经常看到人们因为贪心,从头开始编写相对一般的功能,而导入bug。我喜欢为我现在正在做的事情编写一个专门的函数,让它工作,然后在确保我得到相同的结果再将其重构成一般功能的函数。这通常适用于矢量化代码,其中我几乎总是首先写出完全循环的版本,然后一次一个循环地将它转换为矢量化代码。
posted on 2019-04-26 18:10 xueliangliu 阅读(977) 评论(0) 编辑 收藏 举报