复杂模式侵蚀边界
传统的软件工程实践表明,使用 "封装"(en- capsulation)和模块化设计来划分严格的抽象 边界,有助于创建可维护的代码,从而便于进行孤立的更改和改进。严格的抽象边界有助 于表达给定组件的信息输入和输出的不变性和逻辑一致性。
遗憾的是,很难通过规定特定的预期行为来为机器学习系统设定严格的抽象边界。事实上 ,当所需行为无法在不依赖外部数据的情况下用软件逻辑有效表达时,就需要使用机器学 习。现实世界并不适合整齐的封装。在此,我们将从几个方面探讨由此造成的边界侵蚀可 能会显著增加 ML 系统的技术债务。
机器学习系统会将信号混合在一起,使它们相互纠缠,无法实现等效改进。例如 ,考虑一个在模型中使用特征 x1 , ...xn 的系统。如果我们改变 x1 中值的输入分布,其 余 n - 1 个特征的重要性、权重或使用情况都可能发生变化。不管是以批处理方式对模型 进行全面重新训练,还是让模型以在线方式进行调整,情况都是如此。添加新的特征 xn+1 也会引起类似的变化,删除任何特征 xj 也是如此。没有任何输入是真正独立的。我们 将此称为 CACE 原则:任何改变都会改变一切。CACE 不仅适用于输入信号,也适用于超 参数、学习设置、采样方法、收敛阈值、数据选择以及其他所有可能的调整。
数据依赖的成本高于代码依赖
不稳定的数据依赖。为了快速移动,使用其他系统产生的信号作为输入特征往往很方便。 然而,有些输入信号是不稳定的,这意味着它们的行为会随着时间的推移而发生质或量的 变化。当输入信号来自另一个随时间更新的机器学习模型本身,或一个依赖于数据的查找 表(如用于计算 TF/IDF 分数或语义映射)时,这种情况就会隐性发生。当输入信号的工程 所有权与使用输入信号的模型的工程所有权分离时,也可能出现显式情况。在这种情况下 ,可以随时对输入信号进行更新。这是很危险的,因为即使是对输入信号的 "改进",也可 能对消耗系统产生任意的有害影响,而诊断和处理这些影响的成本是很高的。例如,考虑 到输入信号之前被错误校准的情况。使用它的模型很可能与这些错误校准相匹配,而纠正 信号的无声更新将对模型产生突然的影响。
数据依赖性静态分析。在传统代码中,编译器和构建系统会对依赖关系图进行静态分析。 用于数据依赖关系静态分析的工具则少见得多,但对于错误检查、追踪消费者以及强制迁 移和更新却至关重要。自动特征管理系统就是这样一种工具,它可以对数据 源和特征进行注释。然后可以运行自动检查,确保所有依赖关系都有适当的注释,依赖关 系树也能得到完全解决。这种工具可以使迁移和删除在实践中更加安全。
ML 系统反模式
在许多 ML 系统中,真正用于学习或预测的代码只占很小一部分,剩下的大部分代码可以被描述为 "管道"。 不幸的是,采用机器学习方法的系统最终都会采用高负债设计模式。
ML 研究人员倾向于以独立软件包的形式开发通用解决方案。在网站上,有各种各样的开源软件包,也有内部代码、专有软件包和云平台。 使用通用软件包往往会导致胶水代码系统设计模式,在这种模式下,需要编写大量的辅助 代码,以便将数据输入和输出通用软件包。从长远来看,胶水代码的成本很高,因为它往 往会使系统冻结在特定软件包的特殊性上;测试替代方案的成本可能会高得令人望而却步 。这样一来,使用通用软件包就会阻碍系统的改进,因为这样就很难利用特定领域的特性 或调整目标函数来实现特定领域的目标。由于一个成熟的系统最终可能只有(最多)5% 的 机器学习代码和(至少)95% 的胶水代码,因此创建一个简洁的本地解决方案可能比重复 使用通用软件包的成本更低。
在软件工程中,设计气味可能预示着组件或系统的潜在问题。 下面是ML 系统的一些气味,它们并不是硬性规定,而是主观指标。
• 普通数据类型的味道。ML 系统使用和生成的丰富信息通常都是用原始浮点数和整 数等普通数据类型编码的。在一个强大的系统中,模型参数应该知道自己是对数乘 数还是决策阈值,预测结果应该知道产生预测结果的模型的各种信息,以及应该如 何使用这些信息。
• 多语言气味。用一种语言编写系统的某一特定部分往往很有诱惑力,特别是当这种 语言有方便的库或语法来完成手头的任务时。然而,使用多种语言往往会增加有效 测试的成本,并增加将所有权转移给其他人的难度。
• 原型气味。通过原型对新创意进行小规模测试很方便。然而,经常依赖原型环境可 能表明,全面系统很脆弱,难以改变,或者可以从改进的抽象和界面中受益。维护 原型环境本身也需要成本,而且时间压力可能会促使原型系统被用作生产解决方案 ,这种危险性很大。此外,小规模试验的结果很少能反映大规模试验的实际情况。
配置债务
机器学习系统的配置是另一个可能累积债务的惊人领域。任何大型系统都有大量可配置选 项,包括使用哪些特征、如何选择数据、各种特定算法的学习设置、潜在的预处理或后处 理、验证方法等。我们注意到,研究人员和工程师都可能把配置(以及配置的扩展)当作 事后的想法。事实上,对配置的验证或测试甚至可能并不重要。在一个正在积极开发的成 熟系统中,配置的行数可能远远超过传统代码的行数。每一行配置都有可能出错。
请看下面的例子。特征 A 在 9/14 至 9/17 期间被错误记录。10/7 日之前的数据没有特征 B。 由于记录格式发生变化,用于计算特征 C 的代码必须针对 11/1 之前和之后的数据进行更改 。生产中不提供特征 D,因此在实时设置中查询模型时必须使用替代特征 D′ 和 D′′ 。如 果使用特征 Z,则必须为用于训练的作业提供查找表所需的额外内存,否则训练效率会很 低。由于延迟限制,特征 Q 排除了特征 R 的使用。
所有这些混乱都使得配置难以正确修改,也难以推理。然而,配置中的错误可能代价高昂 ,导致严重的时间损失、计算资源浪费或生产问题。因此,我们提出了以下良好配置系统 的原则:
• 只需对以前的配置稍作改动,就能轻松指定配置。
• 应该很难出现人工错误、遗漏或疏忽。
• 从视觉上应该很容易看出两个型号在配置上的区别。
• 自动断言和验证配置的基本事实应该很容易:使用的功能数量、数据依赖关系的传 递封闭性等。
• 应该可以检测到未使用或多余的设置。
• 配置应经过全面的代码审查,并检查到存储库中。
相关的其他债务领域
数据测试债务。如果说在智能语言系统中,数据取代了代码,而代码应该接受测试,那么 很显然,对输入数据进行一定程度的测试对系统的良好运行至关重要。基本的正确性检查 以及监控输入分布变化的更复杂测试都很有用。
可重复性之债。作为科学家,我们必须能够重新进行实验并获得相似的结果,但由于随机 算法、并行学习中固有的非确定性、对初始条件的依赖以及与外部世界的相互作用,设计 现实世界中的系统以实现严格的可重复性变得十分困难。
流程管理债务。本文中描述的大多数用例都谈到了维护单个模型的成本,但成熟的系统可 能会同时运行数十或数百个模型。这就提出了一系列重要问题,包括如何安全自动 地更新许多类似模型的许多配置、如何在具有不同业务优先级的模型之间管理和分配资源 ,以及如何可视化和检测生产流水线中数据流的阻塞。开发工具以帮助从生产事故中恢复 也至关重要。需要避免的一个重要系统级问题是具有许多手动步骤的常见流程。
文化债务。有时,人工智能研究与工程设计之间的界限很严格,但这对系统的长期健康发 展可能会适得其反。重要的是要创建一种团队文化,奖励删除功能、降低复杂性、提高可 重复性、稳定性和监控能力,就像重视提高准确性一样。根据我们的经验,这最有可能发 生在同时具备人工智能研究和工程优势的异质团队中。
总结
技术债务是一个有用的比喻,但遗憾的是,它并没有提供一个可以长期跟踪的严格指标。 我们该如何衡量系统中的技术债务,或评估这种债务的全部成本?仅仅注意到一个团队仍 然能够快速移动,这本身并不能证明债务少或实践好,因为债务的全部成本只有随着时间 的推移才会显现出来。事实上,快速行动往往会带来技术债务。我们可以考虑以下几个有用的问题,
• 一种全新的算法方法如何能轻易地进行全面测试?
• 什么是所有数据依赖关系的传递闭包?
• 如何精确测量系统新变化的影响?
• 改进一个模型或信号是否会降低其他模型或信号的性能?
• 如何快速让团队中的新成员跟上节奏?