前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Google 年度顶级论文】机器学习系统,隐藏多少技术债?

【Google 年度顶级论文】机器学习系统,隐藏多少技术债?

作者头像
GavinZhou
发布2018-01-02 15:11:52
1.2K0
发布2018-01-02 15:11:52
举报

原文在此:google原文 1. 介绍

随着机器学习(ML)社群持续积累了几年对于活跃系统(live systems)的经验,一种让人不舒服的趋势广泛地浮出水面:研发和部署机器学习系统相对来说是既快速又便宜的,但维护它们却很困难,并且成本昂贵。

这里写图片描述
这里写图片描述

这种对立可以用“技术债”的框架来理解。1992年WardCunningham引入了这个比喻,用来帮助解释在软件工程领域里因为进展快速而带来的长期成本。就像欠下财政债务一样,欠下技术债务也总是有充分的战略性原因。并不是所有的债务都是不好的,但是,所有的债务都需要付出利息。技术债可能通过:

重构代码(refactoringcode)、改进单元测试(improvingunit tests)、删除无用代码(deletingdead code)、降低依赖(reducing dependencies)、精简应用程序接口(tightening APIs)、以及改进说明(improving documentation)的方式来逐渐偿还。

这些行动的目标并不是为了增加全新的功能,而是为了使未来能够继续改进,减少报错,以及提高维护性。推迟偿还只会带来越来越高昂的成本。因为技术债会高速增长,所以它是非常危险的。

在这篇论文中,我们提出,机器学习系统有其特有的、带来技术债的能力,因为它们不仅有传统代码所有的维护问题,还要额外加上机器学习的一些特有问题。这种债务可能很难被察觉到,因为它存在于系统层面而不是代码层面。鉴于数据会影响机器学习系统的行为表现,传统的抽象化和边界可能悄悄崩溃(corrupted)或是失效(invalidated)。偿还代码层面技术债务的典型方法在系统层面的机器学习特有的技术债面前是不够的。

这篇论文并不提供全新的机器学习算法,而是寻求提高机器学习社群对于机器学习系统技术债的意识;从长远来看,实际操作中这些艰难的权衡必须得到考虑。我们关注于系统层面的交互和接口,因为这是一个可能会快速积累机器学习技术债的领域。在系统层面上,一个机器学习模型可能会严重侵蚀抽象化边界(abstractionboundaries)。多次利用输入信号(re-use)或连锁输入信号(chain)非常吸引人,但可能会无意中将几个系统组合起来(combine),也可能会令组合系统脱节(disjoint)。机器学习可能会被当作是黑匣,导致大量的“胶水代码”(glue code)或是大量校准层(calibration layers)。外部世界的变化可能会在无意中影响系统行为。在设计不够仔细的时候,即使是监控机器学习系统的行为都可能会使一件困难的事情。我们探索了一些机器学习特有的需要在设计系统时考虑到的风险因素。这些包括了:

(1)边界侵蚀(boundary erosion) (2)纠缠(entanglement) (3)隐藏反馈循环(hiddenfeedback loops) (4)未申明的访问者(undeclared consumers) (5)数据依赖(data dependencies) (6)配置问题(configuration issues) (7)外部世界变化(changesin the external world) (8)系统层面的反面模式(anti-patterns)

  1. 复杂模型侵蚀边界

传统的软件设计实践表明,使用封装和模块设计的强抽象化边界有助于创造便于维护的代码,因为这让独立更改和改进变得很简单。给定元素中信息输入输出中,严格的抽象化边界有助于表达不变条件(invariants)以及逻辑一致性。

不幸的是,通过预先设置特殊的行为来迫使机器学习系统拥有严格的抽象化边界是非常困难的。事实上,机器学习恰恰是被应用在相反的情况中:当不依赖于外部数据、期望的行为便无法在软件逻辑里被有效表达时。真实世界并不能被干净整齐地封装起来。在这里,我们检验了一些会导致边界侵蚀、进而使机器学习系统中的技术债显著提高的情况。 纠缠

机器学习系统将信号混在一起,使它们互相纠缠,让独立更新变成一件不可能的事。比如,想象一下一个在模型中使用了特征x1,…xn的系统。如果我们改变了x1中输入值的分布,那么剩下n-1个特征的重要性、权重、或是用法(use)可能都会随之发生变化。增加一个新特征xn+1会导致相似的改变,就像移除其中任何一个特征一样。不存在真正意义上独立的输入。我们在这里称它为CACE法则:改变任何东西都意味着改变一切(ChangingAnything Changes Everything)。CACE不仅适用于输入信号,也适用于超参数(hyper-parameters)、学习情景(learningsettings)、抽样方法(samplingmethods)、收敛阈值(convergencethresholds)、数据选择(dataselection)、以及其他在本质上是微小改动的部分。

一种可能的缓解策略是,将模型分离开、转而提供集成模型。这个方法在分离式多层环境之类的情景下(例如在[14]中)是有用的,因为这样的环境下,这个方法能够让纠缠导致的各种问题都自然而然地消解。然而,在许多例子里,集成模型的效果非常好是因为它的组成模型各自的误差是不相关的。依赖于这样的组合会导致严重的纠缠问题:改进一个组成模型可能实际上让整个系统的准确度变得糟糕了——如果余下的误差与其它组成模型的误差相关性变得更强烈。

第二种可能的策略是,关注预测行为发生的变化。[12]中提出,用一个高维度可视化工具来让研究者们可以快速看到许多维度和切片上的效果。以切片为基础进行操作的矩阵可能也会非常有用。

嵌套修正

经常会出现一种情况:针对问题A的模型ma已经有了,但现在需要一个针对稍有不同的问题A’的模型。在这样的情况下,将ma作为输入信息、并学习一个小改变的模型是非常诱人的,它可以很快地解决问题。 然而,这种修正模型带来了新的对于ma的系统依赖,让未来分析改进这个模型的代价变得更为昂贵。当修正模型多层嵌套——针对问题A’’的模型是基于m’a习得的之类——来适应几个有些许不同的测试分布时,成本会变得更为高昂。一旦存在,嵌套修正模型会让改进陷入僵局,因为改进任何一个单独的组成部分的准确性事实上都会带来系统层面的损害。缓解策略是扩充ma,通过增加辨别特征让ma直接在它所在的模型里习得修正;或者接受为A’单独创造一个模型的成本。

未声明的访问者

许多时候,来自一个机器学习模型ma的预测能被许多程序访问的,或者是在运行时、或者是通过写入文件或是日志来让其他系统可以在之后访问。不对访问进行控制的话,一些访问者可能是未声明的(undeclared),悄无声息地将一个模型的输出结果输入给另一个系统。在更经典的软件工程领域,这个问题被称为可见性债务(visibilitydebt)。

未声明的访问者往好了说会带来昂贵的成本,往坏了说更是危险的,因为它们在模型ma与堆栈其他部分之间创建了一个隐藏的紧耦合。对ma的改变将很有可能也影响到这些其它的部分,也许通过无意的、尚未被理解的、会造成损伤的方式。实际操作中,这种紧耦合会逐渐增加对ma做任何改变的成本和难度,即使这些改变是为了提升性能。不仅如此,未声明的访问者可能会创建隐藏的反馈循环,在第4部分我们会更详细地讨论这个问题。

未声明的访问者可能很难被发现,除非系统在设计时就特意针对这个问题作了防护,比如限制访问或是有严格的服务级别协议(strictservice-level agreements, SLAs)。在缺乏限制的情况下,工程师们自然会使用手头最方便的信号,特别是当需要面对截止时间的压力时。

  1. 数据依赖比代码依赖成本更高

在经典的软件工程环境中,依赖债务(dependencydebt)被认为是代码复杂程度和科技债的重要贡献者。我们已经发现,机器学习系统中的数据依赖具有相似的形成债务的能力,但可能更难被察觉到。代码依赖可以通过编译器和链接的静态分析被鉴定出来。由于缺乏相似的工具用来鉴定数据依赖,形成大型的、难以拆解的数据依赖链是一件很容易发生的事情。 依赖于不稳定的数据

为了获得快速进展,将其他系统生成的信号当作输入的特征是非常便利的。然而,一些输入的信号是不稳定的(unstable),这意味着它们的行为随着时间会发生数量上的变化(quantitatively)或是质量上的变化(qualitatively)。这可能发生得非常隐蔽,比如这个输入信号来自于另一个自身会随时间更新的机器学习模型,或者一个依赖于数据的、用来计算TF/IDF分数或是语义映射的查找表(lookuptable)。这也可能发生得非常明显,比如当输入信号的工程所有权(engineeringownership)独立于使用这个输入信号的模型的工程所有权。在这样的情况下,可以在任何时候对输入信号进行更新。这是非常危险的,因为即使是对输入信号的“改进”也可能会对使用这个信号的系统产生不确定的危害作用,要诊断这样的问题需要很高的成本。比如,想象一下这样的情况:一个输入信号之前的校准是错误的(mis-calibrated),使用这个信号的模型很可能拟合这些错误的校准;对信号作了更正以后,整个模型都会突然发生变化。

针对依赖于不稳定数据的问题,一种常见的缓解策略是,对于一个给定信号创建一份标记版本的副本。比如,与其允许一个词汇(words)到主题(topics)的语义映射聚合随时间一直变化,创建这个映射的冻结副本(frozenversion)、一直使用到更新版本被彻底检查确认完毕是更好的选择。然而,创建每个版本的副本会带来它自己的成本,例如可能的冗余以及同一信号随时间而产生的不同版本的维持成本。

依赖于未充分使用的数据

在代码层面,未被充分使用的依赖指的是大部分时候非必要的包(Packages)。相似的,依赖于未充分使用的数据指的是几乎不能为模型提供性能提升的输入信号。这会导致机器学习系统在变化的时候容易出现问题,有时候还会造成灾难,即使这些依赖可以被无损移除。

例如,假如要简化从老编号方案到新编号方案的过渡,那么两种方案都作为特征留在系统里。新的产品只有新的编号,但老的产品会有新、老编号,而模型在某些产品上,会持续依赖于老的编号。一年后,停止将老编号填充进数据库的代码被删除了。这对于机器学习系统的维修者来说不会是一个愉快的日子。 依赖于未充分使用的数据,这个问题可以通过多种方式蔓延进入模型中。

遗留特征:很常见的例子是,特征 F 在早期的开发中存在,但随着开发进行,在新的特征中 F 变成多余了,但并没有被发觉。

捆绑特征:很多时候,一组特征被评估为有用的。但是由于截止期限的压力或其他类似原因,这组特征都被加入到新模型了,其中就很可能包括一些没有价值的特征。

є特征:作为机器学习研究者,总是会想方设法改善模型的精确程度,哪怕这个精确度的提升是非常的小,或者带来的复杂度会很高。

相关特征:有时候两个特征是相关性很高,但其中一个特征有更直接的因果作用。很多机器学习方法很难识别出来区别,而是把这两者等同看待,甚至可能选择了没有因果关系的那个特征。当真实世界的行为改变了特征之间的相关性,结果就会变得很脆弱、不够稳健。

依赖于未充分使用的数据这一问题可以通过完全把一个特征丢弃以评估检测。这项工作需要定期进行,以识别并移除多余的特征。

这里写图片描述
这里写图片描述

在传统的代码中,编译器和编译系统可以用依赖图(dependencygraphs)做静态分析。数据依赖的静态分析工具非常少,但它们对于错误检查、访问者追踪、迁移和更新(migrationand updates)来说都很必要。这类工具其中之一就是自动特征管理系统(automatedfeature management system),它让数据源和特征可以被标注,然后运行自动检查来确保所有的依赖都有恰当的标注,而依赖树(dependencytree)可以被完全解开(fullyresolved)。这类工具在实践中能让迁移和删除操作变得安全得多。

  1. 反馈循环

现在机器学习系统的主要特征之一即是,如果系统随时间不断进行更新,这些系统最终会影响它们自身的行为。这就导致了某种形式的分析债,即在一个系统未发布之前很难预测一个给定模型的行为。这些反馈循环可以有不同的形式,但如果所有这些反馈循环是随时间而逐渐发生的话,它们是很难被检测到的,当模型更新不频繁时就是这样。 直接反馈循环

一个模型可能直接影响到它自身未来训练数据的选择。尽管理论上正确的解决方法是赌博机算法,但是采用标准的监督算法已经是一种很普遍的做法。问题是赌博机算法(例如语境赌博机算法)实质上不能很好的地缩放到现实世界中要求的动作空间的尺度大小。因而,可以通过采用一些随机化的操作或者隔离受到给定模型影响的某部分数据等方式来缓解上述的不良影响。

隐藏反馈循环

直接反馈循环分析起来代价是非常昂贵的,但是它们至少提出来了一个机器学习研究者们很自然地会去琢磨的统计挑战。相比之下,更困难的情况是隐藏反馈循环,即两个系统在世界范围内间接地互相影响。

一个例子可能是,如果两个系统单独地决定一个网页的某些方面,例如一个系统对产品进行挑选并展示,另一个系统挑选相关的评论。改进一个系统可能导致另一个系统行为的变化,因为作为对于改变的反应,用户开始更多或更少地点击其他部分。需要注意的是,在两个完全不相交的系统中也可能会存在隐藏反馈循环。可以从来自于两个不同的投资公司的两个股票市场预测模型的案例加以考虑,如果对其中的一个模型做出某些改进(或者,bug),则另一个模型的投标和购买行为相应地也会受到一定程度的影响。

  1. 机器学习系统的反面模式 对于学术领域的众多研究者而言,了解到在许多的机器学习系统中仅仅只有一小部分的代码用来进行学习或者预测可能会让他们感到非常惊讶(如图1所示)。在Lin和Rayboy语言中,除了仅有的小部分代码用于学习或者预测,大部分剩余代码被称为“管道工程”。

不幸的是,对于一个融合了机器学习算法并最终债台高筑的系统来说,这很普遍。在这个章节中,我们检测了数个在许多的机器学习系统中显露的反面模式系统设计,以及在有可能的时候哪些应该被避免或重构。

粘合代码

机器学习研究者倾向于开发普遍适用的解决方案作为自给自足的包(packages)。在像mloss.org、in-house code、专有软件包、基于云平台等地方,有许多这种包以开源包的形式存在。

采用通用软件包经常会导致粘合代码的系统设计模式,在这种系统设计模式中,包含了大量支持数据写入通用软件包或者数据从通用软件包中输出的代码。粘合代码的代价从长远来看是很高的,因为这将系统局限于(freeze)一个特定包的特点,如果要测试其他方法,成本就会变得不可避免的昂贵。因此,采用通用包会抑制改进,因为以粘合代码的形式使得利用特定领域的特性或者调整目标函数实现特定领域的目标变得更加困难,。由于一个成熟的机器学习系统可能最终会包含(最多)5%的学习代码和(最少)95%的粘合代码,创建一个干净纯粹的解决方案比重复使用通用软件包成本要更低。

对抗粘合代码的重要策略之一就是,将黑盒包(black-box packages)包装进普通的应用程序接口。这让周边支持的基础设施能被更多地重复利用,降低更换包的成本。

管道丛林

作为粘合代码的一种特殊情况,“管道丛林”(pipeline jungles)现象经常出现在数据预备阶段。随着新信号逐渐被鉴定、新信息资源逐渐被添加,管道丛林也有机地发展起来。一不小心,在机器学习系统友好的格式下,预备数据的结果系统可能会成为一个充满碎片、联结部分以及采样步骤的丛林,经常也会有中间输出文件在其中。管理这些管道、检测错误、以及从失效中修复等全部都是非常困难和代价昂贵的事情。测试这样的管道通畅要求端到端的整合测试。上述所有情况增加了系统的技术债,并且使得未来创新的成本更加昂贵。 只有从整体上考虑数据收集和特征提取的过程,才能够避免“管道丛林”现象的发生。用从头再来的方式消除管道丛林,以及从最底层开始重新进行设计,确实是一项工程上的重大投资,但是其也是一项能够大幅减少持续增的成本并加速未来创新的工程。

粘合代码和“管道丛林”是集成问题表现出来的症状,根源可能在于“研究”和“工程”角色过于分离。当机器学习包是在象牙塔的环境中被开发出来时,对于在实践中部署这些包的团队来说结果可能看上去像一个黑盒。工程师和研究者处于同一个团队的混合研究方法(事实上,很多时候研究者和工程师是同一个人)能够显著地减少源头的摩擦。

失效的实验代码路径

粘合代码或是“管道丛林”的一个常见结果是,在主要的生成代码中,通过执行实验代码路径作为条件分支来演示具有选择性方法的实验过程,从短期看来是极具吸引力的。对于任何单独的改变,以这种方式来进行实验,成本是相对较低的——周边的基础设备无需再返工。然而,随着时间的推移,由于后台兼容性的维护愈加困难以及循环复杂度(cyclomatic complexity)呈指数增长,这些日渐积聚的代码路径将会导致技术债的持续增加。对代码路径间所有可能的交互做测试变得非常困难,或者说不可能。这种危险的一个著名例子就是Knight Capital的系统在45分钟里损失了4.65亿美元,由于从废弃的实验代码路径产生了未被预料到的行为。 和传统软件领域中的“死旗”(dead flags)一样,周期性地重复检查每个实验分支,从而纠察出哪个实验分支可以舍弃的方法是非常有益的。一般仅仅是一小部分分支能够被实际应用到,其它许多实验分支可能被测试一次后就遭到舍弃。

抽象化债务

上述的问题强调了这样的一个事实:明显缺少强抽象来支持机器学习系统。Zheng最近对机器学习抽象状态与数据库技术状态做了令人信服的比较,得到这样的一个结论:机器学习领域的文献中,没有哪一篇将相关数据库(relational database)作为基本抽象(basic abstraction)的论文得到的结果能达到接近成功的地步。什么样的接口才是描述一个数据流、一个模型、或者一个预测的正确接口?

特别是对于分布式学习来说,仍然缺乏被广泛接受的抽象。可能有人会争论,机器学习中Map-Reduce的广泛应用是由于缺乏强分布式学习抽象。事实上,在最近几年,广泛协议的几个领域之一认为Map-Reduce对于迭代的机器学习算法是一个性能很差的抽象。

参数服务器抽象更具有鲁棒性,但这个基本思想有多个相互竞争的规范。标准抽象的缺乏使得不同组成部分之间的界限太容易被模糊化。

常见异味

在软件工程中,系统设计的异味(smell)指的是在一个系统中或者系统中的一个部件中存在的潜在问题。我们鉴别了一些机器学习系统的异味,这些并不是不可违逆的准则,而只是主观判断的指标。

POD类(Plain-Old-Data)异味:机器学习系统采用和生成的丰富数据信息全部是用浮点数、整数等普通的数据类型进行编码的。在一个鲁棒的系统中,一个模型的参数应该明确自己代表的是一个对数赔率系数、还是一个决策阈值,一个预测应该知道关于产生这个预测的模型的各种片段信息以及它将如何被使用。 多语言异味:用一种特定的语言编写系统中的一部分代码是一件非常具有诱惑力的事情,特别是当这种语言对于手头的任务而言有一个便利的库(library)或脚本。然而,用多种语言编写系统经常会增加测试成本,并且会增加将所有权让渡给其他人的难度。

原型异味:通过原型在小范围内测试新的想法是非常方便的。然而,定期地依赖于一个原型环境暗示着系统的脆弱性、难以改变,并能够从改善的抽象性和接口受益。维护一个原型环境需要承担它的代价,并且有个显著的威胁是,时间压力会促使一个原型系统变成了产品解决方案。除此之外,在小范围内发现的结果不能很好地应用在整个范围内。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2015-12-11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档