你的生产型ML复现不了,可能是工作流程出了问题

在机器学习社区,越来越多的人开始讨论研究的可复现性,但这些讨论大部分局限于学术环境。如何确保生产环境的ML可复现?近日,机器学习开发服务提供商 maiot.io 的 CTO Benedikt Koller 发布一篇博客文章,介绍了他基于自身经验总结的开发可复现生产级机器学习所要注意的 12 个要素。

你的生产型ML复现不了,可能是工作流程出了问题

过去二十年来,我们对软件开发的理解有了大幅提升。其中一大部分原因是 DevOps 概念的出现及其在软件开发行业的广泛应用。

领先的软件公司都遵循着同样的模式:首先是在软件开发过程中快速迭代,然后进行持续集成、持续交付、持续部署。每个特性都要经过测试,看其提供价值的能力如何,而且软件始终要处于就绪的状态,并且通过自动化方法进行部署。

机器学习这个领域虽不同于传统的软件开发,但我们也能从软件开发行业汲取很多实用的经验教训。过去几年里,我们一直在开发生产型机器学习项目。我们的目标并不只是概念验证,而是与软件开发一样的可复现能力(reproducibility)。因此,我们构建了一套流程协调器、强大的自动化能力并建立了一套用于实现该目标的工作流程。

为什么不直接使用 Jupyter Notebook?从头开始构建一组包含所有处理步骤的笔记需要多长时间?为团队纳入新成员的难易程度如何?你现在可以复现两个月前的结果吗?能以多快的速度复现?你能将今天的结果和历史结果进行对比吗?你能在训练过程中关注到数据的出处吗?如果你的模型过时了又会发生什么?

我们遇到过所有这些问题。现在,我们将这些经验进行了归纳总结,得到了成功构建生产型机器学习的 12 个要素(类似于软件开发中的十二要素应用/12 factor app)。

1. 版本控制

对软件工程师来说,版本控制基本上是理所当然需要做的,但是这一方法论还尚未被数据科学家广泛接受。让我引述一下 Gitlab 上一些人的说法:

版本控制可促进整个软件开发团队之间的协调、共享和协作。版本控制软件让团队可以在分布式和异步环境中工作、管理代码和文件的修改和版本以及解决合并冲突和相关异常。

简单来说,版本控制能让你安全地管理软件开发中会变化的部分。

机器学习其实是一种特殊的软件开发,有着自己特定的要求。首先,机器学习中会变化的部分不止一种,而是两种:代码和数据。其次,模型训练的方式是(快速)迭代,并且代码中的差异会很大(比如拆分、预处理、模型)。

只要数据发生更改,就需要保存一个版本,这样才能保证能复现结果以及重复执行实验和训练模型。简单粗暴的版本控制(硬拷贝)具有很大的改进空间,不过尤其是在团队共享的情况下,能够保持不变的版本控制是至关重要的。

代码的版本控制还要更加重要。除了上面引述的内容,预处理代码不仅在训练阶段很重要,而且在服务阶段也很重要,需要与模型有保持不变的相关性。为了在数据科学家的工作流程和投入生产的要求之间建立一种中台,一种方便的方法是提供无服务器的功能。

总结:你需要对代码进行版本控制,也需要对数据进行版本控制。

2. 明确的特征依赖关系

在理想世界中,产生你的输入数据的东西应该总是会产生同样的数据,至少结构上是这样。但这个世界并不是完美的,你从上游服务获取的数据也是由人类构建的,因此可能会发生变化。最终,特征也可能发生改变。最好的情况是你的模型会直接故障报错,但还有最坏的情况:你的模型悄悄继续工作,但得到的结果都是垃圾。

明确定义的特征依赖关系能够尽快揭示出失败案例。如果系统设计得好,还能在服务时进行持续训练,然后调整依赖关系并加以适应。

总结:明确代码中的特征依赖关系。

3. 描述性的训练和预处理

优良的软件都有优良的描述和注释——让人无需阅读每一行代码就能轻松阅读和理解代码功能。

尽管机器学习是一类特殊的软件开发,但它并不鼓励实践者背离已有的代码书写准则。在代码书写标准中,最基本的一条是能让人在短时间内不费力地阅读。

预处理和模型的代码都应该遵循 PEP8 规范。代码中应当使用有意义的对象名并包含有助于理解的注释。遵循 PEP8 规范可提升代码的可读性,降低复杂度并加快调试速度。SOLID 之类的编程范式提供了经过深思熟虑的框架,可让代码在未来用例中的可维护性、可理解性和灵活性都得到改善。

配置应该与代码分离。不要将数据分配比例硬编码到代码之中,而是通过配置方式提供,以便在运行时修改。人们在超参数调节方面已经熟知这一点了:使用分离的配置文件可以显著加快迭代速度,并且让代码库可以重复使用。

总结:提升代码可读性并且将代码和配置分开。

4. 训练结果的可复现性

如果你不能复现训练结果,那么这个结果就是不可信的。尽管这是本文的主题,但在可复现性方面有一些细节需要说明。不仅是你自己需要能复现训练结果,你的整个团队都要能做到这一点。不管是在 PC 还是在 AWS 虚拟机上,模糊处理 Jupyter Notebook 中的训练结果都与可复现性背道而驰。

通过设定训练的工作流程,整个团队都可以透明地访问已执行的实验和已运行的训练。通过绑定可复用的代码库以及分离的配置文件,每个人都可在任何时间成功重新训练。

总结:使用管道式工作流程和自动化。

5. 测试

测试的形式有很多。举两个例子:

1)单元测试是原子层面上的测试——基于各自的标准单独测试每个函数和功能。

2)集成测试则相反,是将代码库的所有元素都放到一起进行测试,同时还会测试上下游服务的克隆版本或模拟版本。

这两种范式都适应于机器学习。预处理代码是预先确定的,直到测试阶段——这样的转换能在不同的输入下都得到正确结果吗?模型是集成测试的一个绝佳案例——在生产环境中提供服务时,你的模型的表现是否与评估时相当?

总结:测试你的代码,测试你的模型。

6. 偏移与持续训练

在生产场景中,任务发生偏移是合理存在的问题。只要数据存在变化的可能性,你就需要考虑偏移的可能性。对于此问题的风险,有两种可以采取的措施:

1)监控生产系统中的数据。建立自动化报告机制,在数据发生变化时通知团队,这种变化甚至可能超过明确定义的特征依赖关系。

2)基于新输入的数据持续训练。良好自动化的管道化流程可以基于新数据重复运行,然后与历史训练结果进行比较,展示性能变化情况以及将训练得到的模型快速投放到生产中,从而让模型表现更好。

总结:如果你的数据会发生变化,那就采用一种持续训练的管道化流程。

7. 跟踪结果

Excel 并非一种跟踪实验结果的好方法。而且还不只是 Excel,任何分散的人工跟踪方法得到的信息都是不够权威的,也因此是不可信的。

正确的做法是以一种中心化的数据存储方式自动记录训练结果。自动化能够保证可靠地跟踪每次训练,从而方便之后比较每次训练的结果。对结果进行中心化存储,能为团队提供透明,实现持续性分析。

总结:通过自动化方法跟踪结果。

8. 实验模型与生产模型

我们需要努力才能理解数据集。通常来说,我们会通过实验来实现理解,尤其是当我们关注的领域具备大量隐含领域知识时。创建一个 Jupyter Notebook,将部分/全部数据导入 Pandas Dataframe,进行几个小时无序研究,训练第一个模型,评估结果——任务完成。但幸运的是,现实并不如此。

在机器学习的生命周期中,实验有自己的目的。这些目的并不是模型,而是理解。基于探索性 Jupyter Notebook 的模型是为了理解,而不是为生产开发的成品。理解之后,还需要进一步开发和适应,才能开始打造用于生产的训练流程。

不过,所有与领域特定的知识无关的理解都可以自动化。你可以基于你使用的每个数据版本生成统计信息,从而可以跳过那些你在 Jupyter Notebook 中做过的一次性的临时探索工作,然后直达第一个管道式流程。你在流程中实验进行得越早,你就能越早地在中间结果上进行协作,也就能更早地实现可投入生产的模型。

总结:笔记不能投入生产,因此要在流程中尽早实验。

9. 训练和服务之间的方法差异

训练和实际服务之间往往存在方法差异,为了正确地将所有数据预处理过程都纳入到模型服务环境中,需要减少这些差异。这当然是正确的,你也需要坚持这一原则。但是,这只是对这一问题的部分解读。

先来简单看一段古老的 DevOps 历史:2006 年,亚马逊的 CTO Werner Vogels 创造了一个说法「You build it, you run it(你构建的东西你要运行)」。这是一个描述性的短语,意思是开发者的责任不只是写程序,还需要运行它们。

机器学习项目也需要类似的机制——理解上游的数据生成以及下游的模型使用都在数据科学家的职责范围内。你训练用的数据是通过什么体系生成的?它会出问题吗?该体系的服务级目标(SLO)是什么?这与实际服务的目标一致吗?你的模型的服务方式是怎样的?运行时环境是怎样的?怎样在服务时对函数进行预处理?这些都是数据科学家需要理解和解答的问题。

总结:正确地将预处理嵌入到服务之中,确保你理解数据的上下游。

10. 可比较性

从为项目引入第二个训练脚本开始,可比较性就成了未来工作的重要组成部分。如果第二个模型的结果无法与第一个模型的结果进行比较,则整个过程就浪费了,其中至少有一个是多余的,甚至可能两个都多余。

根据定义,所有试图解决同一问题的模型训练都需要可以比较,否则它们就不是在解决同一问题。尽管迭代过程可能导致所要比较的东西发生变化,但是在技术上实现模型训练的可比较性需要一开始就作为首要功能内置于训练架构之中。

总结:构建你自己的管道式流程,以便轻松比较各个流程的训练结果。

11. 监控

粗略地说,机器学习的目标应该是通过学习数据来解决问题。为了解决这个问题,需要分配计算资源。首先是分配给模型的训练,然后是分配给模型的服务。负责在训练期间提供资源的不管是人还是部门,都需要负责将这些资源转移给服务。模型在使用过程中可能出现很多性能下降问题。数据可以偏移,模型可能成为整体性能的瓶颈,偏差也是一个真实存在的问题。

效果:数据科学家和团队负责监控他们创建的模型。他们并不一定要负责实施监控,尤其是当组织结构很大时,但他们肯定需要负责监控数据的理解和解释。最低限度上,需要监控的内容包括输入数据、推理次数、资源使用情况(CPU、RAM)和输出数据。

总结:同样,「You build it, you run it(你构建的东西你要运行)」。监控生产过程中的模型是数据科学的部分工作。

12. 模型的可部署性

从技术层面讲,每个模型训练流程都需要得到可部署到生产环境中的成品。毫无疑问,这些模型结果可能很糟糕,但它需要做成可以部署到生产环境的形态。

这是软件开发中的常见模式,也叫做持续交付(Continuous Delivery)。团队需要能够随时部署他们的软件,为了满足这个目标,迭代周期需要足够快。

相关推荐