通过构建端到端可扩展的机器学习(ML)管道,本文介绍了如何充分利用软件工程的架构模式、设计模式和原则来实现一个高可用、可靠并且高效的机器学习系统。
正文
开发模型时,数据科学家在适合统计和机器学习(Python,R 等)的开发环境中工作,并且在一个“沙盒”环境中训练和测试模型,同时编写相对较少的代码。这对于构建能快速推向市场的交互式原型非常有用,但它们还不是可发布的、低延迟的系统!
这是系列文章中的第二部分,将介绍如何构建端到端可扩展的机器学习(ML)管道。
本系列的第一部分,其中介绍了基本的架构风格、设计模式和 SOLID 原则,ML 系统应该尝试解决如下问题:
主要目标是建造一个系统:
▸减少延迟;
▸集成系统的其他部分(例如数据存储,报告,图形用户界面),但松耦合;
▸可以水平和垂直伸缩;
▸消息驱动,即系统通过异步、非阻塞的消息传递进行通信;
▸提供工作负载管理相关的高效计算;
▸容错和自我修复,即故障管理;
▸支持批量和实时处理。
场景设置:现在你已经了解了软件工程的基本概念,已经是一位经验丰富的数据科学家。
不用多说,让我们把它们两(软件工程)两(数据科学)结合在一起。
对于 ML 管道的每个步骤,我将演示如何设计生产级的架构,但不提及任何特定的技术(除了几个演示的例子)。
✏️注意:如果需要重新认识构建 ML 管道的步骤,请查看这个资源。
ML 管道
构建 ML 管道
传统上,管道通过整夜的批处理,来收集数据。通过企业消息总线发送数据,并对其进行处理,以便为第二天的操作提供预先计算的结果和指导。虽然这在某些行业中有效,但在很多行业,尤其是对于 ML 应用程序,它确实是不够的。
下图显示了应用在实时业务问题的 ML 管道,其中的特征和预测是时间敏感的(例如 Netflix 的推荐引擎、Uber 的到达时间估计、LinkedIn 的关联建议、Airbnb 的搜索引擎等)。
实时 ML
它由两块定义明确的组件组成:
在线模型分析:第一行代表应用中的业务组件,用于实时决策。
离线数据发现:下面一行代表学习组件,对历史数据进行分析,用批处理的模式创建 ML 模型。
现在,我们将采用这个简化的图表并展开介绍其内部工作原理。
① 数据摄取(Data Ingestion)
数据采集。
将传入的数据收集到数据存储中,是任何 ML 工作流的第一步。关键是,数据是持久化的,根本不需要进行任何转换,这样我们就有一个不变的原始数据集记录。数据可以从各种数据源获得:可以通过请求(发布/订阅)获得,也可以从其他服务流中获得。
NoSQL 文档类数据库非常适合存储大量快速变化的结构化和/或非结构化数据,因为它们不受模式的限制。它们还提供分布式、可扩展、多副本的数据存储。
离线
在离线层中,数据通过摄取服务(复合编排的服务)流入原始数据存储,该服务封装了数据源和持久化。它在内部采用仓库模式与数据服务交互,数据服务反过来与数据存储进行交互。当数据保存在数据库中时,每个数据集会获得唯一的 batch-id,从而实现高效的查询和端到端的数据沿袭和可追溯性。
为了提高性能,摄取分发有两个特点:
每个数据集都有一个专用的管道,因此所有数据集都是独立和并发处理的
每个管道中的数据被分区,以便同时利用多个服务器内核、处理器甚至服务器。
将准备好的数据分散在多条管道上(水平和垂直),可减少作业完成的总时间。
摄取服务根据计划(每天一次或多次)定期运行或通过触发器触发:按主题将生产者(即数据源)与消费者(本例中的摄取管道)解耦,这样当源数据可用时,生产者系统向代理发布消息,嵌入的通知服务通过触发摄取服务来响应订阅。通知服务还向代理广播源数据已成功处理并保存在数据库中。
在线
在在线层中,在线摄取服务是流式架构的入口,通过提供可靠的、高吞吐量的、低延迟的特性,解耦和管理从数据源到处理和存储组件的信息流,它起到了企业级“ 数据总线 ”的作用。数据保存在长期存在的原始数据存储(Raw Data Store)中,同时也是通往下一个在线流服务的传递层,进行进一步的实时处理。
这里使用的示例技术可以是 Apache Kafka(发布/订阅消息系统)和 Apache Flume(从长期数据库中收集数据),但可能会遇到更多,取决于企业的技术堆栈。
② 数据准备(Data Preparation)
数据探索、数据转换和特征工程。
一旦数据被摄取,将生成一个分布式管道,用于评估数据的状态,查找格式差异、异常值、趋势、不正确、丢失或偏斜的数据,并纠正整个过程中的任何异常。此步骤还包括特征工程过程,特征工程的管道有三个主要阶段:提取、转换和选择。
特征工程操作
这是 ML 项目中最复杂的部分,引入正确的设计模式至关重要。因此,对代码结构来说,明智的做法是,基于一些公共的抽象特征行为,采用工厂方法生成特征,并在运行时选择正确特征的策略模式。特征提取器和转换器的结构应该考虑到组合及高重用。
特征的选择可以留给调用者,也可以自动选择,例如,应用卡方统计检验对概念标签上每个特征的影响进行排序,并在模型训练之前丢弃影响较小的特征,此功能可以通过定义一系列选择器 API 来实现。无论哪种方式,为了确保特征在模型输入和评分时的一致性,会为每个特征集分配唯一的 id。
一般来说,应该把数据准备管道组装成一系列不可变的转换,这样可以很容易地被组合。而测试和高代码覆盖率,是这类项目的重要成功因素。
离线
在离线层中,数据准备服务由摄取服务的完成来触发。它源于原始数据,负责所有特征工程的逻辑,并将生成的特征保存在特征数据存储(Feature Data Store)中。
另外,还可以用分区来处理这个服务(例如,专用管道/并行)。
或者,可以组合来自多个数据源的特征,“join/sync”任务可以聚合所有中间完成的事件,并创建新的组合特征。最后,通知服务向代理广播此过程已完成,并且特征可以使用。
每个数据准备管道完成后,都会把这些特征复制到在线数据存储中,这样可以通过低延迟的查询进行实时预测。
在线
原始数据从摄取管道流入在线数据准备服务。生成的特征存储在内存中的在线特征数据存储中,它可以在预测期间保证较低的读取延迟,也会存储在长期特征数据存储中供将来训练。此外,还可以从长期特征数据存储中加载特征来准备内存中的数据库。
继续前面的技术栈示例,常用的流式引擎是 Apache Spark。
离线钻取:如果我们要深入了解离线摄取和数据准备服务交互,可以通过如下过程:
(1)一个或多个数据生产者将事件发布到消息代理指定的“源数据可用”主题,表示数据已准备好使用。
(2)摄取服务监听该主题。
一旦接收到相应的事件,它将通过以下方式进行处理:(3)获取数据和
(4)在数据存储中以原始格式保存数据。
(5)当处理结束时,它会向“原始数据提取”主题发出一个新的事件,通知原始数据已准备就绪。
(6)数据准备服务监听该主题。
一旦接收到相应的事件,它将通过以下方式进行处理:
(7)获取原始数据,准备和设计新特征,以及
(8)并将特征保存在数据存储中。
(9)当流程结束时,它会向“已生成特征”主题发起一个新的事件,通知特征已生成。
离线数据摄取/准备交互
③ 数据隔离(Data Segregation)
拆分数据子集来训练模型,进一步验证它在新数据上的表现。
ML 系统的基本目标是对非训练数据使用基于模式预测质量的精确模型。因此,把已标定的数据拆分为训练和评估子集,用作将来/未处理数据的代理。
有许多策略可以做到这一点,其中最常见的四种是:
使用默认或自定义比例,将数据按顺序拆分为两个子集,按照在源中显示的顺序,确保没有重叠。例如,前面 70%的数据进行训练,后面 30%的数据进行测试。
使用默认或自定义比例,通过随机种子拆分为两个子集。例如,选择随机 70%的源数据进行训练,选择该随机子集的补集进行测试。
使用上述任一方法(顺序或随机),不过对每个数据集中的记录进行无序排列。
需要显式控制时,使用自定义注入策略来拆分数据。
数据隔离不是一个独立的 ML 管道,因此,必须有一个 API 或服务来促进这项任务。接下来的两个管道(模型训练和评估)必须能够调用此 API 以获取所请求的数据集。在代码结构方面,策略模式是必要的,调用者服务可以在运行时选择正确的算法,显然需要有注入比例或随机种子的能力。此外,API 必须能够返回带有或不带标定/特征的数据,以便分别进行训练和评估。
为了防止调用者将参数固定导致不均匀的数据分布,应触发相应的警告并与数据集一起返回。
④ 模型训练(Model Training)
利用训练数据子集,ML 算法能识别数据中的模式。
模型训练管道只支持离线,执行调度的不同取决于应用是否关键,从每隔几小时到每天一次。除了调度程序外,该服务也可以通过时间或事件来触发。
它由一个模型训练算法库(线性回归、ARIMA、k 均值、决策树等)组成,通过 SOLID 的方式构建,为持续开发新类型的模型并确保它们可以相互替换做好了准备。另外,使用 Facade 模式的包含结构也是集成第三方 API 的关键技术(这也是封装和调用你的个人 Python Jupyter 笔记本的地方)。
实现并行化有几种选项:
最简单的形式是为每个模型指定专用管道,所有模型可以同时运行;
另一个想法是训练数据并行化,即对数据进行分区,每个分区都有一个模型的副本。对那些需要实例的所有字段来执行计算的模型(例如 LDA 和 MF),这是首选的方法;
第三种选择是将模型本身并行化,即对模型进行分区,每个分区负责更新一部分参数。它是线性模型的理想选择,例如 LR 和 SVM;
最后,可以使用混合方法,结合一个或多个选项。(有关更多信息,建议阅读这里)。
模型训练必须在考虑容错的情况下实施,并且应启用训练分区上的数据检查点和故障转移。例如,如果以前由于某些暂时性问题(例如超时)而尝试失败,还可以重新训练每个分区。
既然我们讨论了这个管道的功能,那么让我们来剖析下工作流:模型训练服务从配置服务获得训练的配置参数(例如模型类型、超参数、要使用的特征等),然后通过数据隔离的 API 请求训练数据集。并行发送该数据集到所有模型,一旦完成,模型、原始配置、学习的参数以及训练集和计时的元数据将保存在模型候选数据存储(Model Candidate Data Store)中。
⑤ 候选模型评估(Candidate Model Evaluation)
使用测试数据子集评估模型的性能,以便了解预测的准确程度。
这个管道也是离线的。通过各种度量比较评估数据集上的预测与真值,从而评估模型预测的性能。选择评估子集上的“最佳”模型,对将来/新的实例进行预测。一个包含多个评价器(Evaluator)的库被设计用来提供模型的精度指标(例如 ROC 曲线和 PR 曲线),针对数据存储中的模型被保存下来。同样,前面用到的设计模式在这里也适用,这样可以在评价器之间灵活地组合和转换。
编排(Orchestration)方面,模型评估服务从数据隔离 API 请求评估数据集,对于来自模型候选存储库中的每个模型,应用相关的评价器,评估结果将保存回存储库。这是一个迭代过程,生成最终的模型还会用到超参数优化以及正则化技术。标记好最佳的模型,最后,通知服务广播模型已准备好部署。
这条管道还需要满足所有反应性(reactive)的特性。
⑥ 模型部署(Model Deployment)
所选模型生成后,通常会将其部署并嵌入到决策框架中。
部署模型不是结束,只是开始!
我们为离线(异步)和在线(同步)预测的部署选择最佳模型,可以在任何时候部署多个模型,以实现旧模型和新模型之间的安全过渡,在部署新模型时,所有现有服务需要继续支持预测的请求。
传统上,部署中的一个挑战是,操作模型所需的编程语言与开发模型所用的的编程语言不同。将 Python 或 R 模型移植到像 C++、C 语言或 Java 这样的生产语言中是有挑战性的,通常会导致原模型的性能(速度和准确性)降低。有几种方法可以解决这个问题(没有先后顺序):
用新的语言重写代码(例如 Python 转为 C#)
创建自定义 DSL(领域特定语言)来描述模型
微服务(通过 RESTful API 访问)
API 优先方法
容器化
将模型序列化并加载到内存的键-值存储中
进一步来说:
离线
离线模式下,可以将预测模型部署到一个容器中,作为微服务运行,按需或定期进行预测。
另一种选择是围绕模型创建一个包装器(wrapper),这样可以控制可用的功能。一旦发出批量的预测请求,可以将其作为单独的进程动态加载到内存中,调用预测函数,将其从内存中取出并释放资源(native 句柄)。
最后,还有一种方法是将库包装到 API 中,让调用者直接调用它,或将其包装在服务中,以完全接管预测工具的控制。
可伸缩性方面,可以创建多个并行的管道来调整负载。因为 ML 模型是无状态的,这只需要很少的工作量就可以做到。
在线
在这里,预测模型可以部署到服务集群的容器中,通常分布在一个队列的许多服务器中,以进行负载平衡,确保可伸缩性、低延迟和高吞吐量。客户端可以通过远程过程调用(RPC)来发送预测请求。
或者,采用 key-value 存储(例如 Redis)来支持模型及参数的存储,从而大大地提高性能。
✳️关于实际的模型部署活动,可以通过持续交付实现自动化:打包所需的文件,通过可靠的测试套件来验证模型,最终部署到正在运行的容器中。
测试通过自动构建的管道来执行:首先评估短的、自洽的、无状态的单元测试,如果通过,则在更大的集成或回归测试中评估预测模型的质量。当两个级别的测试都通过时,就可以在服务环境中部署应用。
理想的部署方案是一键式的。
⑦ 模型评分(Model Scoring)
为发现有助于解决业务问题的实际见解,而将 ML 模型应用于新数据集的过程,也称为模型服务(Model Serving)。
✏️ 模型评分和模型服务是在行业中可互换使用的两个术语。阅读此资源后,我发现了评分的真正意义,所以在继续之前,快速介绍下其中的基础知识。
模型评分是给定模型和一些新的输入,进而产生新值的过程。使用通用术语评分(Score)而不是预测(Prediction),因为可能产生不同类型的值:
列表,用于推荐项
数值,用于时间序列模型和回归模型
概率值,表示新的输入属于某个现有类别的可能性
和新的项目最近似的类别或群集的名称
分类模型的预测类别或结果。
模型部署后,就可以根据前面的管道或直接从客户端服务加载的特征数据进行评分。提供预测服务时,模型在离线和在线模式下的行为应该相同。
离线
在离线层中,评分服务针对高吞吐量进行了优化,对大量数据收集采用了用完即弃(fire-and-forget)的预测。应用程序可以发送一个异步请求来启动评分过程,但需要等到批量评分过程完成后才能访问预测结果。评分服务准备数据,生成特征,也从特征数据存储中提取额外的特征。进行评分后,结果将保存在评分数据存储(Score Data Store)中。代理会收到一条评分已完成的通知消息。应用程序侦听此事件,在接到通知时获得评分。
在线
客户端向在线评分服务发送请求,指定要调用的模型的版本,模型路由器会检查请求并将其发送到相应的模型。根据请求,与离线层类似,服务准备数据,生成特征,有选择地从特征数据存储中提取额外特征。进行评分后,结果将保存在评分数据存储中,然后通过网络发送回客户端。
根据用例的不同,评分也可以异步传递给客户端,而与请求无关:
推送:生成评分后,将其作为通知推送给调用者。
轮询:生成评分后,它们将存储在低读取延迟(low-read latency)数据库中,调用者定期轮询数据库以获取可用的预测。
为了最大限度地缩短系统在收到请求时提供评分服务的时间,可以采用以下两种方法:
输入的特征存储在低读取延迟的内存数据存储中;
离线批处理的评分作业中,预先计算的预测被缓存以便于访问(用例的不同,离线预测的结果可能各不相关)。
⑧ 性能监控(Performance Monitoring)
持续监控模型,观察其在现实世界中的行为,并相应地进行校准。
任何 ML 解决方案都需要一个定义明确的性能监控解决方案。对于模型服务应用程序,我们可能希望看到的信息示例如下:
模型标识符;
部署日期/时间;
提供模型服务的次数;
平均/最小/最大模型服务时间;
所用特征的分布;
预测结果与实际/观察结果。
这类元数据在模型评分期间进行计算,然后可以用于监控。
这是另一个离线的管道。当提供新的预测时,会通知性能监控服务,执行性能评估,保存结果并发出相关通知。评估本身是通过将评分与数据管道(训练集)产生的观察结果进行比较来进行的。监控的基本实现可以遵循不同的方法,最流行的是日志记录分析(Kibana、Grafana、Splunk 等)。
为了确保 ML 系统的内置弹性,新模型的速度表现不佳会触发前一个模型生成评分。采用“宁错勿迟”的哲学:如果模型中的一项计算的时间太长,那么模型将被以前部署的模型取代,而不是被阻塞。
此外,当观察到的结果可用时,评分会与观察到的结果相结合,意味着生成了模型的连续精确测量值,并且与速度表现相一致,任何退化的迹象都可以通过恢复到以前的模型来处理。
可以使用责任链模式将不同的版本链接在一起。
模型监控是一个持续的过程: 预测的转变可能导致模型设计的重组。持续提供准确的预测/建议以推动业务发展,这就是 ML 的优势!
交叉问题
结束本文之前,我们必须要提一下横切关注点(Cross Cutting Concern)。与任何其他应用程序一样,ML 应用程序具有跨越层/管道的一些通用功能。即使在单个层中,这些功能也可以跨所有类/服务使用,切入和穿过所有的正常边界。
横切关注点通常集中在一个地方,这增加了应用程序的模块化。通常由组织中的其他团队管理它们,或者是现成的/第三方产品。依赖注入是将这些关注点注入代码中相关位置的最佳方法。
在我们的用例中,要解决的最重要的问题是:
通知
调度
日志记录框架(和警报机制)
异常管理
配置服务
数据服务(在数据存储中公开查询)
审计
数据沿袭
缓存
工具
把它们放在一起
现在你有了一个发布就绪的 ML 系统:
端到端 ML 架构
脚注
祝贺你!你做到了!非常希望你喜欢进入面向数据科学的软件工程领域!
原文链接:https://towardsdatascience.com/architecting-a-machine-learning-pipeline-a847f094d1c7
评论