作者:王磊
更多精彩分享,欢迎访问和关注:https://www.zhihu.com/people/wldandan
技术债(Technical debt)是指开发人员为了加速软件开发,在应该采用最佳方案时进行了妥协,改用了短期内能加速软件开发的方案,从而在未来给自己带来的额外开发负担。这种技术上的选择,就像一笔债务一样,虽然眼前看起来可以得到好处,但必须在未来偿还。软件工程师必须付出额外的时间和精力持续修复之前的妥协所造成的问题及副作用,或是进行重构,把架构改善为最佳实现方式。
1992年,Ward Cunningham首次引入技术债的概念。
概述
近年来,AI相关技术快速演进,正加快与千行百业深度融合。如何构建一个稳定可靠的机器学习系统至关重要。当前在构建机器学习系统时,开发者们更多关注于算法优化和技术创新,较少关注如何构建稳定的系统。
谷歌的工程师们基于大量的机器学习系统构建和维护经验,于2015年发表了《Hidden Technical Debt in Machine Learning Systems》论文,并列举了构建机器学习系统时,可能存在的Technical Debt(技术债)。
复杂模型侵蚀边界
在传统的软件工程实践中,工程师们会使用封装和模块化设计定义一些抽象边界,提升代码的可维护性。但是,在机器学习系统中,由于需要大量的外部数据才能实现其业务逻辑,因此系统要做到严格的模块抽象边界是比较困难。作者们研究了可能会增加ML系统中的技术债的几种因素。
纠缠(Entanglement):机器学习系统将信号混合在一起,相互纠缠,使得采用隔离的方式改进变得不可能。例如,如果一个机器学习系统中的模型使用了特征x1, x2, ..., xn。当x1的输入分布发生变化时,其余n-1个特征(x2, ..., xn)的权重也会发生改变,这给系统的管理带来了很大的挑战。作者们把其称为CACE原则:Changing Anything Changes Everything(改变任何事物都会改变一切)。该场景下,可能的改进方法为:隔离模型和专注于检测变化(预测会发生变化)。
修正级联(Correction Cascades):假定模型ma用于解决问题A,当有一个与A稍微不同的问题A '时,可能将模型ma稍作改动即可解决A ',这种快速的获得的m'a看起来很诱人。然而,这种修正创建了对ma的新系统依赖,将来分析改进该模型时,会代价高昂。尤其当不断有A ''(基于m'a模型修正)、A '''(基于m''a模型修正)等等,可能会造成改进死锁。该场景下,可能的改进方法为:增加特征来区分模型或者为接受A '单独创建模型的成本。
未声明的消费者(Undeclared Consumers):通常,机器学习模型的预测可以广泛访问,无论运行时、写入文件或者日志,而这些文件或者日志可能会被其他系统使用。如果,没有做访问控制,存在未申明的消费者,使用给定模型输出作为另一个系统的输入。这种隐藏的紧密耦合在后续改进时,会增加高昂的成本和难度。在经典的软件工程中,这些问题常常被称为可见性债务。解决这个问题,需要在系统上做严格的访问限制或采用严格的服务级别协议 (SLA)。
数据依赖比代码依赖成本高
依赖债务被认为是经典软件工程中造成代码复杂度和技术债的关键因素,在机器学习系统中,数据依赖也是如此。传统软件工程中,代码依赖往往能通过静态分析(依靠编译器和链接器)来识别,而数据的依赖关系却没有类似的工具去识别出来,导致系统就很容易产生大量复杂的数据依赖,并且一旦产生就很难再清除掉。
不稳定的数据依赖关系(Unstable Data Dependencies):例如,为了进展迅速,常常会把一个系统的输出作为另一个系统的输入。然而,这些输入可能是不稳定的,任何的修改都会对另一个系统产生深远影响,而且这些影响的诊断和解决成本高昂。针对该问题,作者们给出了可行的方案是对输入进行版本控制,即维护并使用当前稳定的版本,更新数据后,维护一个新版本,待新版本验证过后再进行切换。
多余的数据依赖关系(Underutilized Data Dependencies):多余的数据依赖关系可能通过如下途径引起。
- 遗留功能,例如随着时间推移,新特性变得多余。
- 捆绑功能,为了一个功能引入一个完整的组件,组件包含全部特性。
- ǫ-Features,引入这些依赖明显收益很小且代价高昂,而且额外受到这些数据变化所带来的副作用影响。
- 关联功能,两个功能密切相关,一个更重要,检查时会被认为是一个特性,当选择一个不重要的功能进行修改,会导致系统脆弱不堪。
针对该问题,作者们建议用“leave-one-feature-out”的方法,来定期的检查和去除非必要的功能。
图 1:在实际的机器学习系统中,只有小部分代码用于机器学习,如中间的小黑匣子所示。 其周围所需要的基础设施庞大而复杂。
最后,缺少数据依赖方面的静态分析工具,作者们建议开发相关工具来帮助分析数据依赖。这里给出了一个做依赖管理的例子,《Ad click prediction: a view from the trenches》中给出了一个自动特征管理系统,它允许对数据源和特征进行注释。然后,可以运行自动检查,以确保所有注释完整以及依赖项正常。
反馈循环
一个正常运行的机器学习系统,其有一个关键特征,即其产生的行为可能会反过来影响系统自身。这导致了一种分析债务,在模型发布前很难预测其行为。反馈循环产生的形式多样,且会随着时间推移逐渐显现,因此,这类问题很难检测和解决。
直接反馈循环(Direct Feedback Loops):一个模型可能直接影响它未来训练数据的选择。一般来说,理论上正确的解决方案是采取Bandit算法,但通常会使用标准监督算法。Bandit算法不一定能很好的扩展到实际问题所需的动作空间大小,通过一定数量的随机策略或者数据隔离策略来减轻上述影响。
隐式反馈循环(Hidden Feedback Loops):直接反馈循环虽然分析成本很高,但开发者们通过统计算法可以获得分析结果。而隐式反馈循环,其分析将会更困难,甚至隐式反馈循环可能来源于完全不相关的系统。
机器学习系统反模式
在图1中,我们可以得知在实际的机器学习系统中,只有小部分代码用于机器学习,如中间的小黑匣子所示,大部分代码都属于周边“依赖”。如下给出了几种可能在机器学系统中的设计反模式。
胶水代码(Glue Code): ML 工程师倾向于构建很多的机器学习库,大量通用库的使用通常会导致胶水代码。一个成熟的机器学习系统最终可能是(最多)5% 的机器学习代码和(至少)95% 的胶水代码,重新构建一个纯净的解决方案要比重复使用通用包成本来的低。对抗胶水代码的一个重要策略是将机器学习库封装到公共API中。
管道丛林(Piepline Jungles):管道丛林通常出现在数据准备阶段。随着新的输入信号加入数据准备中,可能导致其变为一个由爬虫、连接和采样,以及各种中间输出件等构成的丛林。只有全面考虑数据收集和特征提取,才能避免管道丛林。胶水代码和管道丛林是继承问题的一类症状,其根源可能是过分区分算法和工程的职责。让工程师和科研工作者更多的合作,譬如在一个团队中,可以有效的减少这种问题。
失效的实验性代码路径(Dead Experimental Codepaths):在机器学习代码开发中,常常伴着大量的实验,因此需要在原有代码中拉取满足各种条件的代码分支来进行功能验证。从短期来看,极具吸引力,生产成本较低。但随着时间的推移,积累的实验性代码越来越多,由于保持向后兼容性的难度越来越大,以及循环复杂度呈指数级增长,这些累积的代码路径会产生越来越多的技术债务。因此,需要定期检查并删除这些失效的代码路径。通常只有一小部分代码在实际中会用到,其它的代码分支在测试一次之后就可以废弃掉。
抽象债务(Abstraction Debt):前面几个问题产生的一个事实是机器学习系统缺乏一个强抽象模型,类似关系数据库抽象理论。例如描述数据流、模型和预测最合理的接口是什么?特别在分布式学习中,仍然缺乏被广泛接受的抽象模型。MapReduce 和Parameter-server都达不到需要抽象的要求。
常见的坏味道(Common Smells):在软件工程中,设计味道(design smell)可能表示组件或系统中的潜在问题。在此,也列举几个机器学习系统中几个常见的坏味道。
- 数据类型味道(Plain-Old-Data Type Smell):机器学习系统使用和生成的信息通常都使用原始数据类型(如原始浮点数和整数)进行编码。在一个健壮的系统中,模型参数应该表明其是计算参数、或者是决策阈值。预测过程应该清楚使用什么信息,产出什么信息。
- 多语言味道(Multiple-Language Smell):用特定的语言编写系统的特定部分通常很方便,尤其是当该语言有方便的库或语法时。但是,使用多种语言通常会增加测试成本和交接成本。
- 原型味道(Prototype Smell):通过原型环境小规模测试新想法很方便。然而,经常依赖原型环境预示着整个系统脆弱且难以更新。维护原型环境需要成本,并且存在一个巨大的风险就是迫于时间压力,可能直接将原型环境直接作为生产环境使用。此外,在小范围内研究结果难以全面反映实际情况。
配置债务
机器学习系统由于其复杂性,往往需要大量的系统配置,例如使用哪些功能、如何选择数据、算法参数设置、预处理或后处理、验证方法等。因此,应加强配置的可维护性和易读性。这里给出以下几个原则:
- 易于找到待变更的配置项。
- 避免人工错误、遗漏或疏忽。
- 方便查看两个配置版本之间的差异。
- 具备基本的自动校验机制。
- 能够检测失效或者冗余配置。
- 应经过代码review,并提交到代码仓中。
应对外部环境变化
机器学习系统需要经常和外部环境进行交互。而外部环境是时刻变化的,这无疑给机器学习系统维护带来了极大的挑战。
动态系统中的固定阈值(Fixed Thresholds in Dynamic Systems):在实际环境中,经常需要选择一个决策阈值来进行决策判断。然而,这些阈值通常是人工设置的,如果用新数据更新模型后,原有的阈值可能不再适用。人工去更新多个模型阈值,不仅会涉及大量工作量,且容易出错。一种缓解策略是通过对要验证的数据作简单评估来获取阈值。
监控和测试(Monitoring and Testing):单个组件的单元测试和运行系统的端到端测试是验证系统很有效的方法。但是面对不断变化的环境,这些测试方法难以保障系统的可靠性。为保障系统的长期可靠性,还需要对系统进行实时监控和预警机制。一般可以通过预测偏差(Prediction Bias)、行为限制(Action Limits)、上游生产者(Up-Stream Producers)等维度设置监控指标。
其它领域债务
数据测试债务(Data Testing Debt):如果数据替换了系统中的代码,则需要对其进行完整的测试。
可复现性债务(Reproducibility Debt):实验可复现性很重要,虽然机器学习系统可能因为随机算法、非确定性并行学习、初始条件依赖,以及与外部环境交互,使得复现很困难。
流程管理债务(Process Management Debt):有效的流程管理在应对同时运行数十或数百个模型时,可以有效的进行配置更新、资源分配、定位数据流中的阻塞。当然,要避免过多的人工干预。
组织文化债务(Cultural Debt):长期区分机器学习中的研究、工程职责也不利于构建良好的机器学习系统。团队文化也很重要,建议将算法研究、技术创新和AI工程放在同等重要的地位,即重视奖励实现剔除失效功能、降低复杂性、提升复现性、稳定性、监控等能力的工作。
总结
论文中并未提出新的算法或者技术,作者们主要聚焦于机器学习系统工程性问题。并基于他们多年的工程经验总结了一系列机器学习系统中的技术债,比如数据依赖、反馈循环和反模式等等,并提出一些改进的方法。
近年来,随着算法、算力和数据的不断突破,AI已经从实验室研究走向产业实践。而AI 要成为企业的生产力,就必须以工程化的技术来解决模型开发、训练、预测等全链路生命周期的问题。AI工程,一门新兴的学科,聚焦工具体系、开发流程、模型管理全生命流程的高效耦合。通俗的讲,AI工程是一系列方法、工具和实践的集合,确保AI模型/软件的高效交付,具备可信、健壮性及可解释性,并持续地为用户创造价值。相信,随着AI工程的不断发展,对于消减AI工程中的技术债会起到重要作用。我们之前已经发布了一系列的AI工程相关文章,欢迎大家查看,详见专栏AI工程与实践。
参考资料
- Ward Cunningham. The WyCash Portfolio Management System .1992-03-26 [2008-09-26]
- D. Sculley, Gary Holt, Daniel Golovin, Eugene Davydov, Todd Phillips, Dietmar Ebner, Vinay Chaudhary, Michael Young, Jean-François Crespo, Dan Denniso. Hidden Technical Debt in Machine Learning Systems. (NIPS 2015)
- H. B. McMahan, G. Holt, D. Sculley, M. Young, D. Ebner, J. Grady, L. Nie, T. Phillips, E. Davydov, D. Golovin, S. Chikkerur, D. Liu, M. Wattenberg, A. M. Hrafnkelsson, T. Boulos, and J. Kubica. Ad click prediction: a view from the trenches. In The 19th ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, KDD 2013, Chicago, IL, USA, August 11-14, 2013, 2013.
说明:严禁转载本文内容,否则视为侵权。