在DevOps领域,持续集成和交付(CI/CD)是一个备受追捧的话题。在MLOps(机器学习+运营)领域,我们有另一种形式的持续--持续评估和再培训。MLOps系统根据世界的变化而发展,而这通常是由数据/概念漂移引起的。因此,为了迎合数据的变化,我们需要不断评估我们部署的ML模型,并在必要时重新训练和重新部署。
在这篇博文中,我们介绍了一个项目,它实现了一个结合批量预测和模型评估的工作流程,用于持续评估再训练,以捕捉数据的变化。我们将首先讨论项目的一般设置。然后,我们将继续讨论关键组件(批量预测、新的数据跨度、再训练等),这些组件对于持续评估ML模型,然后在需要时重新训练它非常重要。我们不会讨论项目的技术实现细节,而是保持高层次的讨论,这样我们就会专注于理解基础概念。
该项目是用TensorFlow Extended(TFX)、Keras和谷歌云平台提供的各种服务实现的。你可以在GitHub上找到该项目。
概述
这个项目展示了如何建立两个独立的管道,共同创建一个CI/CD工作流,以应对数据的变化。第一条流水线用于模型训练,第二条流水线用于基于批量预测结果的模型评估,如图1所示:
| 图1.项目结构概览(原文) |
模型训练管道是通过将标准的TFX组件(如ImportExampleGen 和Trainer)与自定义的TFX组件(如VertexUploader和VertexDeployer)结合起来建立的。由于我们在做这个项目时,Pusher标准组件出现了问题,所以我们从之前的项目Dual Deployments中带来了自定义组件。
关于ImportExampleGen 如何处理要输入模型的数据集,有一个重要的实现细节。我们设计了我们的项目,将来自不同分布的数据集保存在不同的文件夹中,文件系统的路径标明跨度号。例如,初始训练和测试数据集可以存储在SPAN-1/train和SPAN-2/test,而漂移数据集可以分别存储在SPAN-2/train和SPAN-2/test,如图2所示。
为了谷歌云存储(GCS)的版本功能,你可能认为我们不需要以这种方式管理数据集。然而,我们认为我们的方式使数据集更容易管理。例如,你可能想从SPAN-1和SPAN-2或者SPAN-1和SPAN-3中挑选数据来训练模型,这取决于情况。另外,属于同一分布的数据集仍然可以从GCS的版本功能中获益。
| 图2.数据集的管理方式(原文) |
批量评估管道并没有利用任何标准的TFX组件。相反,它由五个定制的TFX组件组成,分别是FileListGen 、BatchPredictionGen 、PerformanceEvaluator 、SpanPreparator 、PipelineTrigger 。这些组件在这里可以作为独立的模块使用。
| 图3.批量评估管道中的自定义TFX组件(原始)。 |
FileListGen 生成一个文本文件,由当前部署的模型在顶点AI上查找,根据顶点预测所要求的格式进行批量预测。然后 ,简单地根据 ,准备好的文本文件执行Vertex Prediction,并输出一组包含批量预测结果的文件。 ,根据批量预测结果计算平均精度,如果小于阈值,则输出False。如果输出为True,管道将被终止。或者如果输出为False, ,通过压缩原始数据列表准备TFRecord文件,然后将这些TFRecord放入一个新的文件夹,其名称包含连续的跨度编号,如 。最后, ,通过传递数据的跨度号来触发模型训练管道,这些数据应该包括在模型训练中,通过 。BatchPredictionGen FileListGen PerformanceEvaluator SpanPreparator span-2 PipelineTrigger RuntimeParameter
一般设置
在这一节中,我们将介绍项目的关键组成部分,并留下一些关于我们用来实现它们的工具的说明。
准备好初始模型
我们专注于概念,并考虑以最小的方式实现它们,以便我们的实现尽可能的可复制和可访问。考虑到这一点,我们使用CIFAR-10训练集作为我们的训练数据,并对ResNet50模型进行微调以适应这些数据。我们的训练管道在这本笔记本中得到了展示。
模拟数据漂移和标记新数据
为了模拟数据漂移的情况,我们从互联网上收集了一堆与CIFAR-10类相匹配的图片。为了便于操作,我们在Colab笔记本中实现了这个工作流程,在这里可以找到。这个工作流程还包括上传和部署训练好的模型,作为顶点人工智能平台上的一项服务。
连续评估与批量推理
然后我们用上述步骤中训练好的模型对这些图像进行推理。我们进行批量推理而不是在线推理来获得结果。我们使用顶点AI的批量预测服务来实现这一点。在实践中,通常在这一步之后,模型测试图像和模型预测被发送给领域专家进行审核。他们也会在测试图像上提供预期的地面真实标签。只有在这之后,我们才能验证预测结果。但在本项目中,我们取消了这一步骤,并假设地面真实标签已经存在。因此,一旦有了批量预测结果,我们就对其进行评估。整个工作流程将在本笔记本中涵盖。
我们部署了一个云功能来监测谷歌云存储(GCS)桶内的特定位置。如果该位置有足够数量的新测试图像,我们就触发批量预测管道。我们在这个笔记本中介绍了这个工作流程。这就是我们如何实现我们项目的 "持续评估 "方面。
不过,还有其他方法来捕捉数据的漂移。例如,使用JS-Divergence,我们可以比较新的可用数据和训练数据之间的分布。你可以关注Robert Crowe的这个Coursera讲座,深入了解这些技术。
模型再训练
在对批量预测进行评估后,下一步是确定我们是否需要根据预定的性能阈值重新训练模型,该阈值一般取决于业务背景和许多其他因素。我们在项目中把这个阈值设置为0.9。如果我们需要重新训练,那么我们就触发相同的模型训练管道(如本笔记本所示),但要把新的可用数据添加到CIFAR-10训练集中。我们可以从以前的检查点热启动我们的模型,也可以使用所有可用的训练数据从头开始训练模型。在这个项目中,我们采用了后者。
在下面的章节中,我们将介绍我们的实现中的一些非微不足道的组件,并讨论它们的动机和技术特点。作为提醒,我们的实现在这里是完全开源的。
用跨度数字管理数据集的实施细节
在本节中,我们将介绍项目中一些关键方面的实现细节。请翻阅项目库并查看所有的笔记本以了解更多信息。
最初的CIFAR-10数据集分别存储在{bucket-name}/span-1/train 和 {bucket-name}/span-1/test GCS位置。这个步骤是通过第一个笔记本完成的。然后,我们通过使用Bing图像下载器下载更多与CIFAR-10中相同类别的图像。这些图片被调整为32x32的大小,以使其与CIFAR-10数据集兼容,并将其存储在一个单独的GCS桶中,如{bucket-batch-prediction}/2021-10/ 。
请注意,我们使用YYYY-MM ,作为存储图像的名称。这是因为由云调度器启动的云功能将寻找最新的GCS位置来启动批量评估管道,如下所示:
def get_latest_directory(storage_client, bucket):
blobs = storage_client.list_blobs(bucket)
folders = list(
set(
[
os.path.dirname(blob.name)
for blob in blobs
if bool(
re.match(
"[1-9][0-9][0-9][0-9]-[0-1][0-9]", os.path.dirname(blob.name)
)
)
is True
]
)
)
folders.sort(key=lambda date: datetime.strptime(date, "%Y-%m"))
return folders[0]
正如你所看到的,它只寻找与YYYY-MM 格式完全匹配的GCS位置。云功能通过传递哪个GCS位置来启动批评估管道,以通过RuntimeParameter 。下面的代码片断显示了如何在云端函数一侧以data_gcs_prefix 的名义传递给管道:
from kfp.v2.google.client import AIPlatformClient
api_client = AIPlatformClient(project_id=project, region=region)
response = api_client.create_run_from_job_spec(
...
parameter_values={"data_gcs_prefix": latest_directory},
)
管道识别data_gcs_prefix 是一种类型的RuntimeParameter ,它被用于 [FileListGen](https://github.com/deep-diver/Continuous-Adaptation-for-Machine-Learning-System-to-Data-Changes/blob/main/custom_components/file_list_gen.py)组件中使用,该组件准备了一个所需格式的文本文件来执行顶点AI批量预测:
def _create_pipeline(
data_gcs_prefix: data_types.RuntimeParameter,
...
) -> Pipeline:
filelist_gen = FileListGen(
...
gcs_source_bucket=data_gcs_bucket,
gcs_source_prefix=data_gcs_prefix,
).with_id("filelist_gen")
....
让我们跳过由组件执行的批量预测: [BatchPredictionGen](https://github.com/deep-diver/Continuous-Adaptation-for-Machine-Learning-System-to-Data-Changes/blob/main/custom_components/batch_prediction_vertex.py)组件进行的批量预测:
当 [PerformanceEvaluator](https://github.com/deep-diver/Continuous-Adaptation-for-Machine-Learning-System-to-Data-Changes/blob/main/custom_components/batch_pred_evaluator.py)组件确定应根据来自 [BatchPredictionGen](https://github.com/deep-diver/Continuous-Adaptation-for-Machine-Learning-System-to-Data-Changes/blob/main/custom_components/batch_prediction_vertex.py)组件的结果,该 [SpanPreparator](https://github.com/deep-diver/Continuous-Adaptation-for-Machine-Learning-System-to-Data-Changes/blob/main/custom_components/span_preparator.py)准备一个带有新收集的图像的TFRecord文件,将其移动到{bucket-name}/span-1/train 和{bucket-name}/span-2/test ,训练管道正在为模型训练摄取数据,并将新收集的图像所在的GCS位置更名为{bucket-batch-prediction}/YYYY-MM_old/ 。
我们添加了_old 的后缀,这样云功能就会忽略重命名的GCS位置。如果重新训练的模型没有显示出足够好的性能指标,那么你可以有机会收集更多的数据,并将它们与_old GCS位置中的图像合并。
在 [PipelineTrigger](https://github.com/deep-diver/Continuous-Adaptation-for-Machine-Learning-System-to-Data-Changes/blob/main/custom_components/training_pipeline_trigger.py)组件在批量评估管道的末端将通过传递要寻找的跨度数字来触发训练管道,以便进行模型训练。这些数据将被ImportExampleGen消耗,基于glob模式匹配功能。例如,如果span-1和span-2的数据应该被用于模型训练,那么训练数据集的glob模式可能是span-[12]/train/*.tfrecord 。下面的代码片段清楚地显示了这个想法的一般化版本:
response = api_client.create_run_from_job_spec(
...
parameter_values={
"input-config": json.dumps(
{
"splits": [
{
"name": "train",
"pattern": f"span-[{int(latest_span)-1}{latest_span}]/train/*.tfrecord",
},
{
"name": "val",
"pattern": f"span-[{int(latest_span)-1}{latest_span}]/test/*.tfrecord",
},
]
}
),
"output-config": json.dumps({}),
},
)
我们之所以以这种方式形成 [RuntimeParameter](https://www.tensorflow.org/tfx/api_docs/python/tfx/v1/dsl/experimental/RuntimeParameter)的原因是parameter_values ,因为ImportExampleGen 组件的模式匹配特征应该在input-config 和output-config 参数中指定。对于我们的目的,我们不需要output-config 参数,但是当把input-config 参数作为RuntimeParameter 传递时,它是需要的。这就是为什么output-config 参数留空。注意,在为标准的TFX组件使用RuntimeParameter 时,你必须以协议缓冲区的格式形成参数。下面的代码显示了传递的input-config 和output-config 如何被组件消耗。 [ImportExampleGen](https://www.tensorflow.org/tfx/api_docs/python/tfx/v1/components/ImportExampleGen)组件消耗:
example_gen = tfx.components.ImportExampleGen(
input_base=data_root, input_config=input_config, output_config=output_config
)
值得注意的是,如果后端环境是Kubeflow Pipeline v1,你可以利用TFX支持的标准组件的滚动窗口功能。 下面的代码片段显示了如何用 [CsvExampleGen](https://www.tensorflow.org/tfx/api_docs/python/tfx/v1/components/CsvExampleGen?hl=zh-cn)组件和一个 [Resolver](https://www.tensorflow.org/tfx/api_docs/python/tfx/v1/dsl/Resolver?hl=th)节点:
examplegen_range_config = proto.RangeConfig(
static_range=proto.StaticRange(
start_span_number=2, end_span_number=2))
example_gen = tfx.components.CsvExampleGen(
input_base=data_root,
input_config=examplegen_input_config,
range_config=examplegen_range_config)
resolver_range_config = proto.RangeConfig(
rolling_range=proto.RollingRange(num_spans=2))
examples_resolver = tfx.dsl.Resolver(
strategy_class=tfx.dsl.experimental.SpanRangeStrategy,
config={
'range_config': resolver_range_config
},
examples=tfx.dsl.Channel(
type=tfx.types.standard_artifacts.Examples,
producer_component_id=example_gen.id)).with_id('span_resolver')
这是一个更好的方法,因为它重用了之前的ExampleGens ,而当前的管道运行只负责新跨度的数据。然而不幸的是,基于Kubeflow Pipeline v2的Vertex AI Pipeline不支持这一功能。 我们与TFX团队就此进行了广泛的讨论,这就是为什么我们想出了一个不同于标准方法的方法。
成本
顶点AI训练是一项独立于Pipeline的服务。我们需要单独支付顶点AI管道的费用,在写这篇文章的时候,每条管道运行的费用大约是0.03美元。每个TFX组件的计算实例类型是e2-standard-4,每小时费用约为0.134美元。由于整个管道的完成时间不到一个小时,我们可以估计,顶点AI管道运行的总成本约为0.164美元。
定制模型训练的成本取决于机器的类型和小时数。另外,你必须考虑到你要分别支付服务器和加速器的费用。对于这个项目,我们选择了n1-standard-4 机器类型,其价格为每小时0.19美元,NVIDIA_TESLA_K80 加速器类型,其价格为每小时0.45美元。每个模型的训练在不到一小时内完成,所以总共花费了大约1.28美元。因此,按照我们的估计,所产生的成本的上限不应超过5美元。
成本只源于顶点人工智能,因为其余的组件,如Pub/Sub,云功能等,使用量非常小。因此,即使我们为这些费用增加一个小的估计,这个项目的总费用的上限也不应该超过5美元。请参考官方文件中的价格。顶点人工智能价格参考,云端构建价格参考。
在任何情况下,你都应该使用这个GCP价格计算器来更好地了解你的GCP服务成本可能有什么不同。
总结
在这篇博文中,我们谈到了对机器学习系统进行持续评估和再训练的想法,以及实现这些系统所需的工具。还有一种更传统的CI/CD形式,用于ML系统,以应对代码的变化,包括超参数、模型架构的变化等。我们有一个单独的项目来演示这个用例。鼓励你在这里查看它们。第一部分和第二部分。