作为PyTorch Lightning的核心维护者,我对测试在软件开发中的价值有了强烈的认识。由于我在工作中一直在酝酿一个新项目,我已经花了相当多的时间来思考我们应该如何测试机器学习系统。几周前,我的一位同事给我寄来了一篇关于这个主题的有趣的论文,这激发了我深入挖掘,收集我的想法,并写下这篇博客文章。
在这篇博客文章中,我们将讨论传统软件开发的测试是什么样子,为什么测试机器学习系统可以不同,并讨论为机器学习系统编写有效测试的一些策略。我们还将澄清作为模型开发过程的一部分的评估和测试这两个密切相关的角色之间的区别。在这篇博客文章的最后,我希望你相信有效测试机器学习系统所需的额外工作和做这些工作的价值。
在传统的软件系统中,人类编写与数据交互的逻辑以产生所需的行为。我们的软件测试有助于确保此书面逻辑与实际预期行为一致。
然而,在机器学习系统中,人类在训练期间提供期望的行为作为示例,并且模型优化过程产生系统的逻辑。我们如何确保这种习得的逻辑能够始终如一地产生我们想要的行为呢?
让我们先来看看测试传统软件系统和开发高质量软件的最佳实践。
在代码库的原子片段上操作并且可以在开发期间快速运行的单元测试,
集成测试通常是观察利用代码库中的多个组件的高级行为的较长时间运行的测试,
在提供错误修复时,请确保编写一个测试来捕获错误并防止将来出现倒退。
当我们针对新代码运行测试套件时,我们将得到一份关于我们已针对其编写测试的特定行为的报告,并验证我们的代码更改不会影响系统的预期行为。如果测试失败,我们将知道哪些特定行为与我们的预期输出不再一致。我们还可以查看此测试报告,通过查看代码覆盖率等度量来了解我们的测试有多广泛。
让我们将其与开发机器学习系统的典型工作流程进行对比。培训新模型后,我们通常会生成一份评估报告,其中包括:
在同一数据集上进行评估时,仅升级可改进现有模型(或基线)的模型。
当审查新的机器学习模型时,我们将检查总结验证数据集上模型性能的指标和曲线图。我们能够比较多个模型之间的性能并做出相关判断,但我们不能立即确定特定模型行为的特征。例如,找出模型出现故障的地方通常需要额外的调查工作;这里的一种常见做法是查看验证数据集上最严重的模型错误列表,并手动对这些故障模式进行分类。
假设我们为我们的模型编写行为测试(下面讨论),那么还有一个问题是我们是否有足够的测试!虽然传统的软件测试有运行测试时覆盖的代码行等度量,但当您将应用程序逻辑从代码行转移到机器学习模型的参数时,这就变得很难量化了。我们是否希望根据输入数据分布来量化我们的测试覆盖率?或者可能是模型内部可能的激活?
Odena等人。引入一种可能的覆盖度量,在该度量中,我们跟踪所有测试示例的模型日志,并量化这些激活向量周围的径向邻域覆盖的区域。然而,我的看法是,作为一个行业,我们在这里没有一个久负盛名的惯例。事实上,感觉机器学习系统的测试还处于早期阶段,以至于这个测试覆盖率的问题并没有被很多人提出。
虽然报告评估指标肯定是模型开发期间质量保证的良好实践,但我认为这还不够。如果没有具体行为的细粒度报告,我们就不能立即理解如果我们切换到新模型,行为可能会如何变化的细微差别。此外,我们将无法跟踪(和防止)先前已解决的特定故障模式的行为回归。
这对于机器学习系统来说可能特别危险,因为故障通常是在静默情况下发生的。例如,您可能会改进总体评估度量,但在关键数据子集上引入回归。或者,您可以在培训期间通过包含新的数据集,在不知不觉中向模型添加性别偏见。我们需要更多模型行为的细微差别报告来识别此类情况,这正是模型测试可以提供帮助的地方。
对于机器学习系统,我们应该并行运行模型评估和模型测试。
模型评估涵盖总结验证或测试数据集性能的度量和曲线图。
在实践中,大多数人将两者结合起来,自动计算评估指标,并通过错误分析(即,对故障模式进行分类)手动完成某种级别的模型测试。开发机器学习系统的模型测试可以为错误分析提供一种系统的方法。
在我看来,有两类模型测试是我们想要编写的。
训练前测试使我们能够在早期识别一些错误,并缩短培训工作的时间。
训练后测试使用经过训练的模型构件来检查我们定义的各种重要场景的行为。
检查模型输出的形状并确保其与数据集中的标注对齐。
检查输出范围并确保其符合我们的预期(例如,分类模型的输出应该是分类概率总和为1的分布)
确保一批数据上的单个渐变步长可以减少您的损失。
这里的主要目标是及早发现一些错误,这样我们就可以避免浪费培训工作。
然而,为了使我们能够理解模型行为,我们需要针对经过训练的模型工件进行测试。这些测试的目的是询问在培训期间学到的逻辑,并为我们提供模型性能的行为报告。
上述文章的作者提出了三种不同类型的模型测试,我们可以使用它们来理解行为属性。
不变性测试允许我们描述一组我们应该能够在不影响模型输出的情况下对输入进行的扰动。我们可以使用这些扰动来产生输入示例对(原始的和扰动的),并检查模型预测的一致性。这与数据增强的概念密切相关,在数据增强的概念中,我们在训练期间对输入应用扰动,并保留原始标签。
我们预计,简单地更改主题的名称不会影响模型预测。
另一方面,方向性预期测试允许我们定义一组对输入的扰动,这些扰动应该对模型输出具有可预测的影响。
增加浴室的数量(保持所有其他功能不变)应该不会导致价格下降。
降低房子的面积(保持所有其他功能不变)应该不会导致价格上涨。
让我们来考虑这样一个场景:一个模型没有通过第二次测试--从我们的验证数据集中随机抽取一行,然后减少特性house_sq_ft,会产生比原始标签更高的预测价格。这是令人惊讶的,因为它不符合我们的直觉,所以我们决定更深入地研究它。我们意识到,在没有针对房屋的社区/位置设置功能的情况下,我们的模型了解到较小的单元往往更贵;这是因为我们数据集中的较小单元在价格普遍较高的城市中更为普遍。在这种情况下,我们对数据集的选择以意想不到的方式影响了模型的逻辑-这不是我们能够简单地通过检查验证数据集的性能来识别的东西。
正如软件单元测试旨在隔离和测试代码库中的原子组件一样,数据单元测试允许我们量化在数据中发现的特定情况下的模型性能。
这使您可以确定预测错误会导致严重后果的关键场景。您还可以决定为您在错误分析期间发现的故障模式编写数据单元测试;这允许您在未来的模型中自动搜索此类错误。
Snorkel还通过他们的切片函数概念引入了非常类似的方法。这些是允许我们识别满足特定标准的数据集的子集的编程函数。例如,您可以编写一个切片函数来识别少于5个单词的句子,以评估模型在短文本上的执行情况。
在传统的软件测试中,我们通常组织测试以反映代码库的结构。然而,这种方法不能很好地转化为机器学习模型,因为我们的逻辑是由模型的参数构成的。
上面链接的核对表论文的作者建议您围绕我们期望模型在学习执行给定任务时获得的技能来组织您的测试。
对于图像识别模型,我们可能希望该模型学习以下概念:
把所有这些放在一起,我们可以修改我们的模型开发过程图,以包括培训前和培训后测试。这些测试输出可以与模型评估报告一起显示,以便在管道的最后一步期间进行审查。根据模型培训的性质,您可以选择自动批准符合某些指定标准的模型。
机器学习系统更难测试,因为我们并没有显式地编写系统的逻辑。然而,自动化测试仍然是开发高质量软件系统的重要工具。这些测试可以为我们提供训练模型的行为报告,这可以作为错误分析的系统方法。
在这篇博客文章中,我将传统软件开发和机器学习模型开发作为两个独立的概念进行了介绍。这种简化使得讨论与测试机器学习系统相关的独特挑战变得更容易;不幸的是,现实世界更加混乱。开发机器学习模型还依赖于大量的传统软件开发,以便处理数据输入、创建特征表示、执行数据扩充、协调模型训练、向外部系统公开接口等等。因此,机器学习系统的有效测试既需要传统的软件测试套件(用于模型开发基础设施),也需要模型测试套件(用于训练的模型)。
如果您有测试机器学习系统的经验,请联系并分享您所学到的东西!
谢谢吴欣欣给我寄来的论文,启发了我写这篇文章!此外,我还要感谢约翰·赫夫曼、乔什·托宾和安德鲁·奈特阅读了这篇文章的早期草稿,并提供了有用的反馈。