Press "Enter" to skip to content

观远AI实战 | 机器学习系统的工程实践

 

「观远AI实战」 栏目文章由观远算法天团倾力打造,观小编整理编辑。这里将不定期推送关于机器学习,数据挖掘,特征重要性等干货分享。

 

本文8千多字,约需要16分钟阅读时间。

 

机器学习作为时下最为火热的技术之一受到了广泛的关注。我们每天打开公众号都能收到各种前沿进展、论文解读、最新教程的推送。这些文章中绝大多数内容都跟酷炫的新模型、高大上的数学推导有关。但是Peter Norvig说过,“We don’t have better algorithms. We just have more data.”。在实际机器学习应用中,对最终结果起到决定性作用的往往是精心收集处理的高质量数据。

 

从表面看好像也不难,但略微深究就会发现机器学习系统与传统的软件工程项目有着非常大的差异。除了广受瞩目的模型算法,良好的工程化思考与实现是最终达到机器学习项目成功的另一大关键因素。

 

谷歌在2015年发表的论文《Hidden Technical Debt in Machine Learning Systems》中就很好的总结了 机器学习工程中的各种不佳实践导致的技术债问题 。主要有以下几种:

 

系统边界模糊

 

在传统的软件工程中,一般会进行细致的设计和抽象,对于系统的各个组成部分进行良好的模块划分,这样整个系统的演进和维护都会处于一个比较可控的状态。但机器学习系统天然就与数据存在一定程度的耦合,加上天然的交互式、实验性开发方式,很容易就会把数据清洗、特征工程、模型训练等模块耦合在一起,牵一发而动全身,导致后续添加新特征,做不同的实验验证都会变得越来越慢,越来越困难。

 

数据依赖难以管理

 

传统的软件工程开发中,可以很方便的通过编译器,静态分析等手段获取到代码中的各种依赖关系,快速发现不合理的耦合设计,然后借助于单元测试等手段快速重构改进。在机器学习系统中这类代码耦合分析同样不可或缺。除此之外还多了数据依赖问题。

 

比如销售预测系统可能会对接终端POS系统数据,也会引入市场部门的营销数据,还有仓储、运输等等多种数据来源。在大多数情况下这些数据源都是不同部门维护的,不受数据算法团队的控制,指不定哪天就悄悄做了一个变更。如果变更很大,可能在做数据处理或者模型训练时会直接抛出错误,但大多数情况下你的系统还是能正常运行,而得到的训练预测结果很可能就有问题了。

 

在一些复杂业务系统中,这些数据本身还会被加工成各种中间数据集,同时被几个数据分析预测任务共享,形成复杂的依赖关系网,进一步加大了数据管理的难度。

 

机器学习系统的反模式

 

胶水代码:随着各种开源项目的百花齐放,很多机器学习项目都会调用各种开源库来做数据处理、模型训练、参数调优等环节。于是自然而然在整个项目中大量的代码都是为了把这些不同的开源库粘合在一起的胶水代码,同样会导致之前提到过的边界模糊,与特定的库紧耦合,难以替换模块快速演进等问题。

 

流水线丛林:在数据处理特征工程与模型调优的迭代演进过程中,稍不注意你的整个系统流水线就会变得无比冗长,各种中间结果的写入和前后依赖极其复杂。这时候想添加一个新特征,或是调试某个执行失败都变得如此困难,逐渐迷失在这混乱的丛林中……如果只具备机器学习知识而缺少工程经验,用这种做实验的方式来开发系统显然是不靠谱的,必须有良好的工程化思维,从总体上把控代码模块结构,才能更好的平衡实验的灵活性与系统开发效率,保证整体的高效运作。

 

失效的实验性代码路径:这一点也是承接前面,很多时候如果以跑实验的心态来给系统“添砖加瓦”,很可能到后面各种小径交叉的代码库就这幺出现了,谁都搞不清楚哪些代码有用哪些是不会执行到的。如何复现别人的实验结果,要用哪些数据集和特征,设置哪些变量、做哪些微调都会成为难解之谜。

 

缺乏好的系统抽象:个人觉得sklearn的各种API设计还算蛮好的,现在很多其它库的高层API都参考了这个准业界标准来实现。文中主要提到在分布式训练中缺乏一个很好的业界标准,比如MapReduce显然是不行的,Parameter Server看起来还算靠谱但也还未成为标准。没有好的抽象标准也就导致了各种库在功能、接口设计时不一致,从而有了以上提到的一系列边界模糊,胶水代码等问题。

 

配置项技术债

 

相对于传统软件系统,机器学习系统的配置项往往会更多更复杂。比如要使用哪些特征、各种数据选择的规则、复杂的预处理和后置处理、模型本身的各种参数设置等等。因此除了工程代码外,配置项的精心设计、评审也成了一个不容忽视的点。否则很容易造成系统在实际运行中频繁出错,难以使用。

 

变化无常的外部世界

 

机器学习系统很多时候都是直接与外部世界的数据做交互,而外部世界总是变化无常。而且机器学习系统本身的输出也会影响到外部世界,从而进一步回馈到机器学习系统的输入中来。比如推荐系统给用户展示的内容会影响用户的点击行为,而这些点击浏览行为又会成为训练数据输入到推荐系统来。如何获取到外部世界变化的信息,进而及时改进甚至自动更新算法模型就成了一个非常重要的问题。

 

在谷歌的这篇原始论文中对各种坑都给了一些解决的建议,归纳总结一下,总体上来说就是要转变团队整体的文化氛围,强调良好的工程思维和实践。一个设计良好的机器学习项目系统中往往真正跟机器学习相关的代码只占了很小的一部分。

 

 

新模型固然酷炫,但是机器学习工程实践的总结与推广与它在整个项目中扮演的角色的重要性显然是不成正比的。所以今天我们要着重来讲一下这个方面:机器学习系统的最佳工程实践是什幺样的?

 

这时候就要请出谷歌的另外一篇论文《The ML Test Score》了。前一篇论文在具体实践落地方面缺乏细节,在这篇论文里, 谷歌总结了28个非常具体的机器学习系统相关工程实践准则 ,可谓是干货满满,十分接地气。

 

文中给出的28个建议都是针对机器学习系统的,没有包含通用软件工程里那些单元测试,发布流程等内容,在实践中这些传统最佳实践也同样非常重要。这些实践在谷歌内部团队广泛使用,但没有一个团队执行的覆盖率超过80%,因此这些测试点都是非常值得关注并有一定的实践难度的。

 

特征与数据测试

特征期望值编写到schema中: 很多特征的分布情况或数值期望是有一些先验知识可以去校验的。比如一般人身高都在0-3米的范围内、英语中最常见的词是”the”、整体的词频一般服从幂律分布等。我们可以把这些先验领域知识,或是从训练集中计算出的数学期望值编写在数据schema文件中,后续对于新的输入数据,构建完特征后的模型训练数据以及最终上线使用模型时都能进行自动化的检查,避免因为数据不符合预期而导致的错误预测情况。

确保所有的特征都是有用的:在之前的机器学习技术债论文中也有提到研发人员总是倾向于不断往系统中添加新的特征,尤其在上线时间比较紧迫的情况下,缺少细致的特征选择和有效性验证工作。这会导致特征数量越来越多,构建训练集需要花费的时间也越来越长,后续的维护成本也会更高。所以跟业务代码一样,没有帮助的特征也要及时清理,轻装前行。文中给出的方法基本是常见的特征选择法,比如计算特征相关度,使用单独或小批量特征来跑模型看是否有预测能力等。

 

去除性价比低的特征:计算添加任何一个特征都需要消耗资源,包括生成和训练模型开销,模型预测开销,甚至还要考虑到对上游数据的依赖,额外的库函数引入,特征本身的不稳定性等等。对于任何一个特征的添加,都要综合考虑这些开销与它能带来的性能提升来决定是否引入。如果只是非常有限的效果提升,我们应该果断放弃那些过于复杂的特征。

 

特征必须遵循业务规范需求:不同的项目对机器学习系统可以使用的数据可能有不同的规范需求,比如可能有些业务禁止我们使用从用户数据中推演出来的特征。所以我们也需要从代码工程层面把这些规范需求进行实现,以避免训练与线上特征出现不一致或违反了业务规范等问题。

 

数据流水线必须有完善的隐私控制:与上一条类似,机器学习系统从数据源获取用户相关隐私数据时已经通过了相应的控制校验,后续在系统内部流水线做处理时我们也要时刻注意对隐私数据的访问控制。比如各种中间数据集,数据字典的存放与访问控制,上游系统的用户数据删除能够级联更新到机器学习系统的整个链路中,诸如此类需要特别注意的问题。

 

能够快速开发新特征:一个新特征从提出到实现,测试,上线的整个流程所需要花费的时间决定了整个机器系统迭代演进,响应外部变化的速度。要实现这一点,良好的工程结构、不同模块的抽象设计都是非常重要的。文中没有给具体的例子,不过我们可以借鉴sklearn中pipeline模块设计的思想,以及类似FeatureHub这样的开源系统的实现来不断优化完善特征工程实践。

 

为特征工程代码写相应的测试: 在实验探索阶段,我们经常会写完一个特征之后,粗略地取样一些数据,大致验证通过后就认为这个特征基本没有问题了。但这其中可能就隐藏了不少bug,而且不像业务代码中的错误,要发现这些bug极其困难。所以必须养成良好的习惯,在特征开发阶段就写好相应的测试代码,确保特征的正确性,后续应对各种系统变更也都能很快通过测试来进行快速验证。

 

 

模型开发测试

 

模型说明必须通过review并记录在案:随着机器学习模型技术的发展,各种复杂模型,大量的参数配置往往让模型训练和执行变得无比复杂。加上在多人协同的项目中,很多时候需要使用不同的数据,或者做一些特定的调整来重新评估模型效果,这时候有详细的模型说明记录就显得尤为重要了。除了简单的文本记录,市面上也有不少开源项目(比如ModelDB,MLflow等)专注于帮助开发者管理模型,提高实验的可复现性。

 

模型优化指标与业务指标一致:很多机器学习的应用业务中,实际的业务指标并不能直接拿来作为目标函数优化,比如业务营收,用户满意度等等。因此大多数模型在优化时都会选择一个“代理指标”,比如用户点击率的logloss之类。因此在建模,评估过程中必须要考虑到这个代理指标与真实业务指标是否有比较强的正相关性。我们可以通过各种A/B测试来进行评估,如果代理指标的改进无法提升真正的业务指标,就需要及时进行调整。

 

调优模型超参数: 这点相信大家都会做,毕竟各种机器学习教程中都会有很大篇幅讲解如何进行调参来提升模型效果。值得注意的是除了暴力的网格搜索,随机搜索同样简单而效果往往更好。另外还有许多更高级的算法例如贝叶斯优化,SMAC等也可以尝试使用。对于同一个数据集,在使用不同的特征组合,数据抽样手段的时候理论上来说都应该进行参数调优以达到最佳效果。这部分的工作也是比较容易通过自动化工具来实现的。

 

对模型时效性有感知: 对于很多输入数据快速变化的业务例如推荐系统,金融应用等,模型的时效性就显得极其重要了。如果没有及时训练更新模型的机制,整个系统的运行效果可能会快速下降。我们可以通过保留多个版本的旧模型,使用A/B测试等手段来推演模型效果与时间推移的关系,并以此来制定整体模型的更新策略。

 

模型应该优于基准测试: 对于我们开发的复杂模型,我们应该时常拿它与一些简单的基准模型进行测试比较。如果需要花费大量精力调优的模型效果相比简单的线性模型甚至统计预测都没有明显提升的话,我们就要仔细权衡一下使用复杂模型或做进一步研发改进的必要性了。

 

模型效果在不同数据切片上表现都应达标:在验证模型效果时,我们不能仅依赖于一个总体的验证集或是交叉验证指标。应该在不同维度下对数据进行切分后分别验证,便于我们对模型效果有更细粒度上的理解。否则一些细粒度上的问题很容易被总体统计指标所掩盖,同时这些更细粒度的模型问题也能指导我们做进一步细化模型的调优工作。例如将预测效果根据不同国家的用户,不同使用频率的用户,或者各种类别的电影等方式来分组分析。具体划分方式可以根据业务特点来制定,并同时考察多个重要的维度。我们可以把这些测试固化到发布流程中,确保每次模型更新不会在某些数据子集中有明显的效果下降。

 

将模型的包容性列入测试范围:近些年来也有不少关于模型包容性,公平性相关问题的研究。例如我们在做NLP相关问题时通常会使用预训练的word embedding表达,如果这些预训练时使用的语料与真实应用的场景有偏差,就会导致类似种族,性别歧视的公平性问题出现。我们可以检查输入特征是否与某些用户类别有强关联性,还可以通过模型输出的切分分析,去判断是否对某些用户组别有明显的差异对待。当发现存在这个问题时,我们可以通过特征预处理(例如文中提到的embedding映射转换来消除例如性别维度的差异),模型开发中的各种后置处理,收集更多数据以确保模型能学习到少数群体的特性等方式来解决这个问题。

 

 

机器学习基础设施测试

 

模型训练是可复现的:在理想情况下,我们对同一份数据以同样的参数进行模型训练应该能获取到相同的模型结果。这样对于我们做特征工程重构验证等都会非常有帮助。但在实际项目中做到稳定复现却非常困难。例如模型中用到的随机数种子,模型各部分的初始化顺序,多线程/分布式训练导致的训练数据使用顺序的不确定性等都会导致无法稳定复现的问题。因此我们在实际工程中对于这些点都要额外注意。比如凡是用到了随机数的地方都应该暴露接口方便调用时进行随机数种子的设置。除了尽量写能确定性运行的代码外,模型融合也能在一定程度上减轻这个问题。

 

模型说明代码需要相应测试:虽然模型说明代码看起来很像“配置文件”,但其中也可能存在bug,导致模型训练没有按预期方式执行。而且要测试这种模型说明代码也非常困难,因为模型训练往往牵涉到非常多的外部输入数据,而且通常耗时较长。文中提到谷歌将会开源一些相关的框架来帮助做相关的测试,一些具体的测试方法如下:

 

1. 用工具去生成一些随机数据,在模型中执行一个训练步骤,检验梯度更新是否符合预期。模型需要支持从任意checkpoint中恢复,继续执行训练。

 

2. 针对算法特性做简单的检验,比如RNN在运行过程中每次应该会接受输入序列中的一个元素来进行处理。

 

3. 执行少量训练步骤,观察training loss变化,确定loss是按预期呈现不断下降的趋势。

 

4. 用简单数据集让模型去过拟合,迅速达到在训练集上100%的正确率,证明模型的学习能力。

 

5. 尽量避免传统的”golden tests”,就是把之前一个模型跑的结果存下来,以此为基准去测试后面新模型是否达到了预期的效果。从长期来看由于输入数据的变化,训练本身的不稳定性都会导致这个方法的维护成本很高。即使发现了性能下降,也难以提供有用的洞察。

 

机器学习pipeline的集成测试: 一个完整的机器学习pipeline一般会包括训练数据的收集,特征生成,模型训练,模型验证,部署和服务发布等环节。这些环节前后都会有一定交互影响,因此需要集成测试来验证整个流程的正确性。这些测试应该包括在持续集成和发布上线环节。为了节约执行时间,对于需要频繁执行的持续集成测试,我们可以选择少量数据进行验证,或者是使用较为简单的模型以便于开发人员快速验证其它环节的修改不会引起问题。而在发布流程中还是需要使用与生产环境尽可能一致的配置来执行整体的集成测试。

 

模型上线前必须验证其效果:这点看起来应该是大家都会遵守的原则。唯一要注意的是需要同时验证模型的长期效果趋势(是否有缓慢性能下降),以及与最近一个版本对比是否有明显的性能下降。

 

模型能够对单个样本做debug:这个就有点厉害了,当你发现一个奇怪的模型输出时,你怎幺去寻找问题的原因?这时候如果有一个方便的工具能让你把这个样本输入到模型中去,单步执行去看模型训练/预测的过程,那将对排查问题带来极大的便利和效率提升。文中提到TensorFlow自带了一个debugger,在实际工作中我们也会使用eli5,LIME之类的工具来做黑盒模型解释,但从方便程度和效果上来说肯定还是比不上框架自带的排查工具。

 

模型上线前的金丝雀测试:这点在传统软件工程中基本也是标配,尽管我们有测试环境,预发布环境的各种离线测试,模型在正式上线时还是需要在生产环境中跑一下简单的验证测试,确保部署系统能够成功加载模型并产出符合预期的预测结果。在此基础上还可以进一步使用灰度发布的方式,逐渐把流量从旧模型迁移到新模型上来,增加一层保障。

 

模型能够快速回滚:与其它传统软件服务一样,模型上线后如果发现有问题应该能够快速,安全回滚。要做到这点必须在开发时就确保各种上下游依赖的兼容性。并且回滚演练本身也应该成为常规发布测试的一环,而不是到出了问题才去手毛脚乱的操作,引出更多意料之外的问题。

 

监控测试

 

依赖变更推送:机器学习系统一般都会大量依赖于各种外部系统提供的数据,如果这些外部系统的数据格式,字段含义等发生了变化而我们没有感知,很容易就会导致模型训练和预测变得不符合预期。因此我们必须订阅这些依赖系统的变更推送,并确保其它系统的维护者知晓我们在使用他们提供的数据。

 

训练与线上输入数据分布的一致性:虽然模型内部运作过程极为复杂,难以直接监控其运行时正确性,但是模型的输入数据这块还是比较容易监控的。我们可以用之前定义的特征数据schema文件来对线上输入数据进行检测,当出现较大偏差时自动触发告警,以便及时发现外部数据的变化情况。

 

训练与线上服务时生成的特征值一致:在机器学习项目中经常会出现同一个特征在训练时和在线上使用时所采用的计算生成方式是不一样的。比如在训练时特征是通过处理之前的历史日志计算出来的,而到了线上的时候同样的特征可能来自于用户实时的反馈。或者是训练时采用的特征生成函数是非常灵活,易于做各种实验尝试的,而到了线上则改用固化的优化计算性能版本以降低服务响应时间。在理想情况下,虽然采用了不同的计算方式,但生成的特征值应该是相同的,否则就会出现训练和预测使用的数据有偏移,导致模型效果变差等问题。所以我们需要通过各种手段来监控线上线下数据的一致性。

 

例如可以通过对线上数据进行采样打标记录,来与训练集中的对应条目进行直接比较,计算出有偏差的特征数量,及这些特征中相应的有偏差的样本占比数量。另外也可以通过计算线上线下各个特征的统计分布的差别来衡量是否有这类问题的产生。

 

模型的时效性:这一点与之前的模型测试中的时效性类似,我们可以直接使用其中获取到的模型效果与时间推移关系来推断理想情况下模型训练更新的频率,并以此来对模型持续运行时间进行监控和告警。要注意过长的更新周期会提升模型的维护难度。另外哪怕模型本身更新比较可控,但是模型依赖的数据源也有类似的时效性问题,我们同样需要对其进行监控以免出现数据过期的问题。

 

数值稳定性:在模型训练中可能会在没有任何报错的情况下出现奇怪的NaN,inf值,导致非预期的参数更新甚至最终得到不可用的模型。因此我们需要在训练中监控任何训练数据中包含NaN或者inf的情况进行适当的处理。同时对于各模型参数的取值范围,ReLU层后输出为0的单元数量占比等指标进行检测,一旦超出预期范围就进行告警,便于及时定位排查相关问题。

 

模型性能相关监控:机器学习模型的训练速度,预测响应时间,系统吞吐量,以及内存占用等性能指标对于整体系统的可扩展性来说都是至关重要的。尤其是随着数据量的越来越大,越来越复杂模型的引入,更加剧了各种性能问题的出现。在模型演进,数据变化以及基础架构/计算库的更迭中,需要我们谨慎的评估模型性能变化,进行快速响应。在做性能监控时不但要注意不同代码组件,版本的表现,也要关注数据和模型版本的影响。而且除了容易检测到的性能突变,长期,缓慢的性能下降也需要引起足够的重视。

 

模型预测质量的回归问题:总体的目标是希望新部署的模型相对于过去的模型在预测性能上没有下降。但验证集相对于线上的真实情况来说总是有所区别的,只能作为一个性能评估的参考。文中列举了几种手段来做监控:

 

1. 衡量预测的统计偏差,正常情况下模型的预测偏差应该为0,如果不是则说明其中有一定问题。

 

2. 对于能在作出预测后很快得到正确与否反馈的任务类型,我们可以以此进行实时监控,迅速判断模型效果相比之前是否有下降。

 

3. 对于在模型提供服务时无法快速获取到正确标签类型的任务类型,我们可以使用人工标记的验证数据来比较模型效果的变化。

 

最后总结一下前面提到的各种监控,基本上还有两个共通点,一是各种告警的阈值要做精心选择,过低的阈值容易出现警报泛滥导致根本没人去管的情况,而过高的阈值又会掩盖系统中已经存在的各种问题。二是除了短时间内明显的指标急剧下降外,同时也要关注长期的缓慢的下降,后者更难以发现,应该引起足够的重视。

 

 

文章后续还给出了这28个测试指标的具体评分标准,帮助大家在实践中更好的对照,应用这些最佳实践。还有很多在谷歌内部使用这套评分系统的各种反馈,以及他们各个团队的平均得分情况等。

 

 

对于这个平均得分情况,作者强力安利了一把谷歌自家的TFX。

 

 

比如基础架构的集成测试,谷歌内部的得分也很低(满分为1的情况下平均为0.2分)。TFX可以方便的实现整个工程的pipeline,自然也很容易在此基础上完成相应的集成测试了。除了TFX,像Uber的Michelangelo,Facebook的FBLearner Flow也都是类似的机器学习平台,虽然没有开源,但都有比较详细的介绍文章可以学习参考。

 

 

这些框架基本可以看作各家公司做机器学习工程的最佳实践总结,总体架构基本都遵循“数据管理->模型训练->模型测试验证->部署上线”这样的流程。不过他们在具体设计实现上各有千秋,例如文中作者认为“线下线上训练数据分布偏差监控”是最为重要但却鲜有实现的一个测试点,在很多项目组都曾经因为缺少这个监控而出现过多次线上故障。因此TFX提供了数据验证模块来专门解决这个问题。而像Uber的Michelangelo则侧重快速开发特征这个点引入了Feature Store模块。在实际应用中我们可以根据不同的业务特点灵活的选择工程方案来进行实现。

 

参考资料:https://papers.nips.cc/paper/5656-hidden-technical-debt-in-machine-learning-systems.pdf

 

https://ai.google/research/pubs/pub46555

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注