深度学习调试指南
深度学习故障诊断难点
- 运行时的bug。
- 超参数选择。
- 模型跟数据不匹配。例如使用ImageNet数据预训练模型,拟合自动驾驶汽车图像。
- 数据构建问题。包括:数据少,数据标签错误,类别不平衡,训练集和测试集数据分布不同。
神经网络调试策略
深度学习故障排除的关键思想是:由于消除错误很困难,所以最好从简单的工作开始,然后逐渐增加复杂性。
从简单模型开始
选择一个简单的架构
要开始简单的工作,首先要选择一个简单的架构。简单的架构很容易实现,并且很可能在不引入太多bug的情况下解决问题。
- 如果数据是图像,可以从类似LeNet的架构开始,当你的代码成熟时,可以考虑使用类似ResNet的模型。
- 如果数据是序列,可以从包含一个隐藏层和(或)时间(经典)卷积的LSTM开始。当问题成熟时,可以转而使用Attention模型或WaveNet类的模型。
- 对于其他的任务,可以从包含一个隐藏层的全连接神经网络开始,之后再根据问题选择更先进的网络。
实际上,很多时候,输入数据包含以上内容的多个。那么如何在神经网络中处理多种输入模式呢?以下是我们推荐的三步策略: - 首先,将每一种形式的数据都映射到一个低维特征空间。在上面的例子中,图像通过卷积神经网络映射,单词通过LSTM映射。
- 然后,将这些网络的输出扁平化,以得到模型中每一个输入的单个向量。拼接这些输入。
- 随后,将它们输入到全连接层得到输出。
使用合适的默认值
在选择了一个简单的架构之后,接下来要做的就是选择合理的超参数默认值作为开始。以下是我们推荐的默认值:
- Adam优化器,学习率3e-4。
- 全连接神经网络使用Relu激活函数,LSTM模型使用Tanh激活函数。
- ReLU激活函数使用He初始化,Tanh激活函数使用Glorot初始化。
- 不适用正则化和数据标准化。
输入标准化
下一步是将输入数据标准化,减去平均值,除以方差。注意,对于图像,可以将值缩放为[0,1]或[-0.5,0.5](例如,除以255)。
简化问题
最后一件事是考虑简化问题本身。如果你有一个需要处理大量数据和大量类别的复杂问题,那么你应该考虑:
- 处理大约10,000个样本的小训练集。
- 使用固定数量的对象,类别,输入尺寸,等等。
- 构建一个简单的合成训练集。
这很重要,因为(1)可以有合理的信心相信模型应该能够求解,并且(2)迭代速度将会提高。
运行和调试
下面是5个常见的深度学习bug:
- 神经网络的张量尺寸不正确。
- 预处理输入不正确。例如,忘记将输入标准化,过度的输入预处理(过度标准化和过度数据增强)。
- 模型损失函数输入错误。例如,损失函数需要logits,输入的却是softmax。
- 忘记设置正确的网络训练模式。例如,切换 训练/评估 模式,控制 batch-norm 依赖关系。
- 数值不稳定。例如,输出中包含 ‘inf’ 或 ‘NaN’ 。这个bug通常是由于在代码的某个地方使用了指数、log或除法操作。
下面是实现你的模型的三个一般建议:
- 从轻量级的实现开始。模型的第一个版本的新代码行数尽可能少。经验法则是少于200行,不包括经过测试的基础架构组件或TensorFlow/PyTorch代码。
- 使用现成的组件。比如Keras,因为Keras中的大部分内容都可以开箱即用。如果必须使用TensorFlow,尽量使用内置函数,不要自己计算。这可以避免很多数值不稳定的问题。
- 稍后再构建数据管道。这些对于大规模ML系统很重要,但是不应该从它们开始,因为数据管道本身可能是bug的一大来源。只需要从一个可以加载到内存中的数据集开始。
下面的图表简洁地总结了如何实现和调试深度神经网络:
让模型跑起来
实现无bug深度学习模型的第一步是让模型完全运行,下面是一些可能出现的问题:
- 尺寸不匹配。要解决这类问题,应该在调试器中逐步完成模型创建和推理,检查张量的正确形状和数据类型。
- 内存不足问题。可以逐个缩减内存密集型操作。例如,可以减少大型矩阵的维数或将批处理大小减半。
- 其它问题。Google或Stack Overflow。
下面探讨在调试器中逐步创建模型的过程,并讨论用于深度学习代码的调试器:
- 在PyTorch中,可以使用ipdb—它导出函数来访问交互式IPython调试器。
- 在TensorFlow中,就比较复杂了。TensorFlow中,创建图和执行图的过程是分离的。可以尝试以下三个选项:(1)逐步完成图形创建本身并检查每个张量层,(2)逐步进入训练循环并评估张量层,或(3)使用TensorFlow Debugger (tfdb),它会自动执行选项1和2。
过拟合一批数据
在模型可以运行之后,下一件事是过拟合一批数据。这是一种启发式方法,可以捕获大量的bug。这实际上意味着将训练损失无限的接近于0。
当你尝试过拟合单个批次数据时,有一些事情会导致失败:
- 误差上升:通常,这是由于在损失函数/梯度中的某个符号错误翻转。
- 误差激增:这通常是数值问题或高学习率引起。
- 误差震荡:可以降低学习率,检查是否有打乱的标签或不正确的数据增强。
- 误差平稳:可以提高学习率,取消正则化。然后检查损失函数和数据管道。
与已知结果比较
模型在单个批处理中过拟合之后,仍然可能存在其他一些导致bug的问题。最后一步是将结果与已知结果进行比较。
- 最有用的结果来自于相似数据集的官方模型的实现。可以逐行遍历两个模型中的代码,并确保模型具有相同的输出,确保模型性能符合期望。
- 如果无法在类似的数据集上找到官方实现,可以与基准数据集上评估的官方模型实现的结果进行比较。逐行遍历代码,并确保具有相同的输出。
- 如果没有官方的实现,与非官方模型实现的结果进行比较。可以像以前一样检查代码,但可信度更低(因为几乎GitHub上的所有非官方实现都有bug)。
- 然后,可以比较没有代码的论文结果(以确保性能达到预期)、基准数据集上模型的结果(以确保模型在更简单的设置下性能良好)、以及来自类似数据集上的类似模型的结果(帮助你大致了解可以期望什么样的性能)。
- 最后可以比较的结果来自于简单的基线(例如,输出的平均值或线性回归),这可以帮助确保模型可以学习到东西。
评估
偏差-方差分解(Bias-Variance Decomposition)
为了评估模型并对模型开发的下一步进行优先级排序,可以应用偏差-方差分解。偏差-方差分解是模型拟合的折衷,测试误差公式中有四个项:
Test error = irreducible error + bias + variance + validation overfitting
- 不可约误差(Irreducible error)是指基线误差,可以通过强基线评估,比如人类的表现。
- 可避免的偏差(Bias),可以作为欠拟合程度的度量,是训练误差与不可约误差的差值。
- 方差(Variance),可以作为过拟合程度的度量,是验证误差与训练误差的差值。
- 验证集过拟合(Validation overfitting)是测试误差与验证误差的差值。
考虑下面的学习曲线和误差图表。利用偏差和方差的测试误差公式,可以计算出测试误差的各个分量,并根据其值进行决策。例如,我们可以避免的偏差相当低(只有2分),而方差则高得多(5分)。有了这些知识,我们应该优先考虑防止过拟合的方法,比如正则化。
分布偏移
显然,对测试误差的偏差-方差分解的应用已经帮助我们为模型开发的下一步设定了优先级。然而,到目前为止,我们一直假设这些样本(训练、验证、测试)都来自相同的分布。在实际的ML情况下,这种分布偏移经常发生。在制造自动驾驶汽车时,一个常见的情况可能是使用来自一个分布(例如白天驾驶视频)的样本进行训练,但测试或推断来自一个完全不同的分布(例如夜间驾驶)的样本。
在我们的假设中处理这个问题的一个简单方法是创建两个验证集:一个来自训练分布,一个来自测试分布。即使对于非常小的测试集,这也很有帮助。如果使用这种方法,我们实际上可以估计分布偏移,这就是测试验证误差和测试误差之间的区别。加入这个新项,偏差和方差的检验误差公式更新为:
Test error = irreducible error + bias + variance + distribution shift + validation overfitting
改进模型和数据
使用上一节中更新的公式,我们将能够为模型的每个迭代决定正确的下一步并确定优先级。我们将遵循如下步骤:
Step 1:解决欠拟合
我们将从解决欠拟合(即减少偏差)开始。在这种情况下,首先要尝试的是让你的模型更大(例如,添加层,每个层添加更多的神经元)。接下来,考虑减少正则化,因为它可以防止模型与数据紧密匹配。其他选项包括误差分析、选择不同的模型体系结构(例如,更先进的模型)、调优超参数或添加特征。一些注意事项:
- 选择不同的架构,特别是SOTA(state of the art)架构,可能非常有帮助,但也有风险。在实现过程中很容易引入bug。
- 与传统机器学习相比,添加特征在深度学习中并不常见。我们通常希望网络能够自动学习特征。
Step 2:解决过拟合
在解决了欠拟合之后,继续解决过拟合。类似地,有一系列推荐的方法可以按顺序尝试。收集更多训练数据(如果可能的话)是解决过度拟合的最佳方法,尽管可能很难。接下来,像标准化、数据增强和正则化这样的改进会有所帮助。调优超参数、选择不同的网络结构或误差分析也是有用的。最后,如果过拟合相当棘手,那么有一系列不太推荐的方法,比如早停、移除特征和减少模型尺寸。
Step 3:解决分布偏移
首先手动查看测试验证集中的误差。将这些误差背后的潜在逻辑与训练验证集的结果进行比较,并使用这些误差指导进一步的数据收集。数据是模型本质上存在分布偏移的原因,进一步收集数据是最基本的处理分布偏移的方法,尽管实际上也是最具挑战性的方法。如果无法收集更多数据来解决这些误差,可以尝试合成数据。此外,您可以尝试领域适应(domain adaption)。
ERROR ANALYSIS
手动评估误差以理解模型性能通常是确定如何改进模型的一种高产出的方法。系统地执行误差分析过程,并把误差分解为不同的误差类型,可以帮助优化模型。例如,在一个自动驾驶汽车用例中,错误类型包括难以看到的行人、反光、夜间场景,分解每个误差的贡献以及它发生的地方(train-val vs. test-val)可以产生一组明确的优先级操作项。
DOMAIN ADAPTION
领域自适应是一类仅使用无标记数据或有限标记数据训练“源”分布并推广到另一个“目标”分布的技术。当测试分布有标记的数据有限,但是类似的数据是丰富的时,应该使用域自适应。有几种不同类型的域自适应:
- 有监督的域自适应:在这种情况下,我们有来自目标域的有限数据来适应。这一概念的一些示例应用包括对预先训练的模型进行微调或向训练集添加目标数据。
- 无监督域自适应:在这种情况下,我们有大量来自目标域的未标记数据。一些技术是CORAL、域混淆和CycleGAN。
有监督的领域自适应工作的很好,无监督域自适应还需要进一步研究。
Step 4:重新平衡数据集
如果测试验证集的性能开始看起来比测试性能要好得多,那么模型可能已经过拟合验证集。这通常发生在小的验证集或大量的超参数训练中。如果发生这种情况,请从测试分布中重新取样验证集,并重新评估性能。
超参调优
超参数优化的核心挑战之一是**应该调优哪些超参数?**当我们考虑这个基本问题时,需要考虑以下几点:
- 模型对某些超参数比其他超参数更敏感。这意味着我们应该把精力集中在更有影响力的超参数上。
- 然而,哪个超参数最重要,在很大程度上取决于我们对模型的选择。
- 某些经验规则可以帮助指导我们的最初思维。
- 敏感度总是相对于默认值,如果使用良好的默认设置,可能会从一个好的地方开始。
下表是超参数对模型的大致影响程度:
超参数调优技术
- 手动超参调优。这种方法的工作原理是手动、详细地查看算法,考虑哪个超参数会产生最大的不同。在找出这些参数之后,可以使用对算法的直觉来训练、评估和猜测一个更好的超参数值。虽然看起来有点过时,但这种方法与其他方法结合得很好(例如,为超参数设置一个范围的值),如果使用得当,它的主要好处是减少计算时间和成本。这可能是耗时和具有挑战性的,但它可以是一个好的开始。
- 网格搜索。想象每一个参数在一个网格上绘制,可以从这个网格中统一抽样测试值。对于每一个点,你都要进行一次训练并评估效果。它的优点是非常简单,通常可以产生良好的效果。但是,它的效率非常低,因为必须运行超参数的每个组合。由于我们必须手动设置值的范围,因此通常还需要先验的超参数知识。
- 随机搜索。建议在网格搜索中使用此方法。不是从网格中均匀地采样超参数的值,而是在网格中随机抽取n个点。实践证明,该方法比网格搜索的结果更好。但是,结果可能有些难以解释,因为某些超参数返回了意外的值。
- 由粗到精搜索。首先,定义一个非常大的范围来进行随机搜索。在结果池中,您可以找到N个最好的结果,根据对应的超参数值选择不断缩小搜索范围。
- 贝叶斯超参数优化。这是一个相当复杂的方法,可以在这里和这里了解更多。在高层次上,从参数分布的先验估计开始。随后,建立超参数值与模型性能关系的概率模型。这是选择超参数的一种自动的、有效的方法。然而,从头实现这些技术可能相当具有挑战性。随着库和基础架构的成熟,将这些方法集成到训练中将变得更加容易。
总之,应该由从粗到细的随机搜索开始,并随着代码库的成熟和对模型更加确定而转向贝叶斯方法。
总结
这里有更多的资源,可以去了解更多:
- Andrew Ng’s “Machine Learning Yearning” book.
- This Twitter thread from Andrej Karpathy.
- BYU’s “Practical Advice for Building Deep Neural Networks” blog post.