MLFlow 大规模深度学习实践指南(二)

原文:annas-archive.org/md5/0801c480d77054d281bf58617eeac06d

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:第六章:大规模运行超参数调优

超参数调优超参数优化HPO)是一种在合理的计算资源约束和时间框架内,找到最佳深度神经网络结构、预训练模型类型以及模型训练过程的程序。这里的超参数是指在机器学习训练过程中无法改变或学习的参数,例如深度神经网络中的层数、预训练语言模型的选择,或训练过程中的学习率、批量大小和优化器。在本章中,我们将使用 HPO 作为超参数调优和优化过程的简称。HPO 是生成高性能机器学习/深度学习模型的关键步骤。由于超参数的搜索空间非常大,因此在大规模上高效地运行 HPO 是一个主要挑战。与经典的机器学习模型相比,深度学习模型的评估复杂性和高成本进一步加剧了这些挑战。因此,我们需要学习最先进的 HPO 方法和实现框架,实现越来越复杂和可扩展的 HPO 方法,并通过 MLflow 跟踪它们,以确保调优过程是可复现的。在本章结束时,你将能够熟练地实现适用于深度学习模型管道的可扩展 HPO。

在本章中,首先,我们将概述不同的自动化 HPO 框架以及深度学习模型调优的应用。此外,我们还将了解应该优化哪些内容以及何时选择使用哪些框架。我们将比较三种流行的 HPO 框架:HyperOptOptunaRay Tune。我们将展示在大规模运行 HPO 时,哪一种框架是最合适的选择。接着,我们将重点学习如何创建适合 HPO 的深度学习模型代码,这些代码可以使用 Ray Tune 和 MLflow。之后,我们将展示如何轻松切换到使用不同的 HPO 算法,并以 Optuna 为主要示例进行说明。

在本章中,我们将涵盖以下主题:

  • 了解深度学习管道的自动化 HPO

  • 使用 Ray Tune 和 MLflow 创建适合 HPO 的深度学习模型

  • 使用 MLflow 运行第一个 Ray Tune HPO 实验

  • 使用 Optuna 和 HyperBand 运行 Ray Tune HPO

技术要求

要理解本章的示例,以下是所需的关键技术要求:

理解深度学习管道中的自动超参数优化

自动超参数优化(HPO)已经研究了超过二十年,自 1995 年首次发布有关该主题的已知论文以来(www.sciencedirect.com/science/article/pii/B9781558603776500451)。人们普遍理解,调优机器学习模型的超参数可以提升模型的性能——有时,甚至能显著提升。近年来,深度学习(DL)模型的崛起催生了一股新的创新浪潮,并推动了新框架的开发,以应对深度学习管道的超参数优化问题。这是因为,深度学习模型管道带来了许多新的、大规模的优化挑战,而这些挑战不能轻易通过以往的超参数优化方法解决。需要注意的是,与模型训练过程中可以学习到的模型参数不同,超参数集必须在训练之前设定。

超参数优化与迁移学习微调的区别

在本书中,我们一直专注于一种成功的深度学习方法——迁移学习(有关详细讨论,请参阅第一章深度学习生命周期与 MLOps 挑战)。迁移学习过程中的关键步骤是用一些任务和领域特定的标注数据来微调一个预训练模型,以获得一个良好的任务特定深度学习模型。然而,微调步骤仅仅是模型训练过程中的一种特殊训练方式,它同样涉及许多超参数需要优化。这就是超参数优化发挥作用的地方。

超参数类型及其挑战

有几种类型的超参数可以用于深度学习管道:

  • 深度学习模型类型与架构:在迁移学习的情况下,选择使用哪些预训练模型是一个可能的超参数。例如,Hugging Face模型库中有超过 27,000 个预训练模型(huggingface.co/models),包括BERTRoBERTa等。对于特定的预测任务,我们可能会尝试其中的几个模型,决定哪个模型是最适合的。

  • 学习和训练相关的参数:这些包括不同类型的优化器,如随机梯度下降SGD)和Adam(你可以在machinelearningknowledge.ai/pytorch-optimizers-complete-guide-for-beginner/查看 PyTorch 优化器的完整列表)。它还包括相关的参数,如学习率和批量大小。建议在适用时,首先根据其重要性顺序调整神经网络模型的以下参数:学习率、动量、迷你批量大小、隐藏层数量、学习率衰减和正则化(arxiv.org/abs/2003.05689)。

  • 数据和管道配置:深度学习管道可能包括数据处理和转换步骤,这些步骤可能会影响模型训练。例如,如果我们想比较带有或不带有签名文本主体的电子邮件消息的分类模型的性能,那么就需要一个超参数来决定是否包含电子邮件签名。另一个例子是当我们没有足够的数据或数据的变化时,我们可以尝试使用各种数据增强技术,这些技术会为模型训练提供不同的输入集(neptune.ai/blog/data-augmentation-nlp)。

提醒一下,并非所有超参数都是可调的或需要调整的。例如,在深度学习模型中,训练轮次不需要调整。这是因为当准确度指标停止提升或不再有改善的可能时,训练应停止。这被称为早停或剪枝,是支撑一些近期最先进超参数优化(HPO)算法的关键技术之一(关于早停的更多讨论,请参考databricks.com/blog/2019/08/15/how-not-to-scale-deep-learning-in-6-easy-steps.html)。

请注意,所有这三类超参数可以进行混合搭配,整个超参数空间的配置可能非常庞大。例如,如果我们希望选择一个预训练模型的类型作为超参数(例如,选择可能是BERTRoBERTa),再加上两个与学习相关的参数(如学习率和批处理大小),以及两种不同的 NLP 文本数据增强技术(如随机插入和同义词替换),那么我们就有五个超参数需要优化。请注意,每个超参数可能有许多不同的候选值可以选择,如果每个超参数有 5 个不同的值,那么我们总共有 55 = 3125 种超参数组合需要尝试。在实际应用中,通常需要尝试几十个超参数,每个超参数可能有几十个选择或分布可供采样。这很容易导致维度灾难问题(insaid.medium.com/automated-hyperparameter-tuning-988b5aeb7f2a)。这种高维搜索空间的挑战由于 DL 模型训练和评估成本高昂而变得更加复杂;我们知道,即使是一个小型 BERT 的 1 个周期(我们在前几章中尝试过),使用一个小规模的训练和验证数据集也可能需要 1 到 2 分钟。现在想象一个实际的生产级 DL 模型,若要进行 HPO,可能需要数小时、数天,甚至数周,如果没有高效执行的话。通常,以下是需要大规模应用高性能 HPO 的主要挑战:

  • 超参数的高维搜索空间

  • 随着 DL 模型越来越大,模型训练和评估时间的高成本

  • 用于生产环境中 DL 模型的生产时间和部署

    同时进行模型训练和 HPO

    在训练过程中是可以动态改变超参数的。这是一种混合方法,它同时进行模型训练和 HPO,例如基于种群的训练PBTdeepmind.com/blog/article/population-based-training-neural-networks)。然而,这并不改变这样一个事实:当开始新的训练周期时,一组超参数需要预先定义。PBT 是尝试减少搜索高维超参数空间和深度学习(DL)模型训练成本的创新之一。感兴趣的读者可以查阅进一步阅读部分,深入了解这个话题。

现在我们已经了解了优化超参数的一般挑战和类别,接下来让我们看看 HPO 是如何工作的,以及如何选择适合我们使用的框架。

HPO 是如何工作的,以及如何选择

有不同的方式理解 HPO 的工作原理。经典的 HPO 方法包括网格搜索和随机搜索,其中会选择一组具有候选值范围的超参数。每个超参数组合独立运行,直到完成,然后根据我们找到的最佳模型性能指标,从我们运行的试验中挑选出最佳的超参数配置。虽然这种搜索方法易于实现,甚至可能不需要复杂的框架来支持,但它本质上是低效的,且由于 HPO 的非凸性质,可能找不到最佳的超参数配置。非凸的意思是存在多个局部最小值或最大值,优化方法可能无法找到全局最优(即最小值或最大值)。简单来说,现代的 HPO 需要做两件事:

  • 超参数的自适应采样(也称为配置选择CS):这意味着需要通过利用先前的知识来选择要尝试的超参数集。这主要是通过使用不同变种的贝叶斯优化方法,根据先前的试验以顺序方式自适应地识别新的配置。已证明这种方法优于传统的网格搜索和随机搜索方法。

  • 超参数集的自适应评估(也称为配置评估CE):这些方法专注于自适应地将更多资源分配给有前景的超参数配置,同时迅速去除效果不佳的配置。资源可以以不同的形式存在,如训练数据集的大小(例如,仅使用训练数据集的一小部分)或迭代次数(例如,仅使用少量迭代来决定哪些任务需要终止,而不是运行到收敛)。有一类方法称为多臂赌博机算法,例如异步成功缩减算法ASHA)。在这里,所有试验从一个初始预算开始,然后去除最差的一半,调整剩余试验的预算,这个过程会重复进行,直到只剩下一个试验。

实际上,我们希望使用以下五个标准来选择一个合适的 HPO 框架:

  • 与 MLflow 的回调集成

  • 可扩展性和对 GPU 集群的支持

  • 易于使用和灵活的 API

  • 与前沿 HPO 算法的集成(CSCE

  • 深度学习框架的支持

在本书中,比较了三种框架,结果总结在图 6.1中:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_06_01.jpg

图 6.1:Ray Tune、Optuna 和 HyperOpt 的比较

图 6.1所示,Ray Tunedocs.ray.io/en/latest/tune/index.html)的表现优于Optunaoptuna.org/)和HyperOpthyperopt.github.io/hyperopt/)。接下来,我们将依次解释以下五个标准:

  • 与 MLflow 的回调集成:Optuna 对 MLflow 回调的支持仍然是一个实验性功能,而 HyperOpt 根本不支持回调,这就让用户需要额外管理每次试验运行的 MLflow 跟踪。

只有 Ray Tune 支持 Python 混合装饰器和与 MLflow 的回调集成。Python 混合装饰器是一种模式,允许在需要时将独立的函数混合进来。在这种情况下,MLflow 的功能通过mlflow_mixin装饰器在模型训练期间自动混合进来。这可以将任何训练函数转变为 Ray Tune 可训练的函数,自动配置 MLflow 并在与每次 Tune 试验相同的进程中创建运行。然后,你可以在训练函数内部使用 MLflow API,结果会自动报告到正确的运行中。此外,它还支持 MLflow 的自动日志记录,这意味着所有的 MLflow 跟踪信息将被记录到正确的试验中。例如,以下代码片段展示了如何将我们之前的深度学习微调函数转换为一个mlflow_mixin的 Ray Tune 函数:

@mlflow_mixin
def train_dl_model():
    mlflow.pytorch.autolog()
    trainer = flash.Trainer(
        max_epochs=num_epochs,
        callbacks=[TuneReportCallback(
            metrics, on='validation_end')])
    trainer.finetune()

请注意,当我们定义训练器时,可以将TuneReportCallback作为回调之一添加,这将把指标反馈给 Ray Tune,而 MLflow 的自动日志记录会同时完成所有跟踪结果的记录。在下一节中,我们将展示如何将上一章中微调深度学习模型的示例转变为 Ray Tune 可训练函数。

  • 可扩展性和 GPU 集群支持:尽管 Optuna 和 HyperOpt 支持并行化,但它们都依赖于一些外部数据库(如关系数据库或 MongoDB)或 SparkTrials。只有 Ray Tune 通过 Ray 分布式框架原生支持并行和分布式 HPO,而且在这三种框架中,它也是唯一支持在 GPU 集群上运行的。

  • API 的易用性和灵活性:在这三个框架中,只有 Optuna 支持按运行时定义的 API,这允许您以 Pythonic 编程风格动态定义超参数,包括循环和分支(optuna.readthedocs.io/en/stable/tutorial/10_key_features/002_configurations.html)。这与 Ray Tune 和 HyperOpt 支持的定义-运行API 形成对比,其中搜索空间在评估目标函数之前由预定义的字典定义。这两个术语按运行时定义定义-运行实际上是由 DL 框架开发社区创造的。在早期,当 TensorFlow 1.0 最初发布时,神经网络首先需要定义,然后稍后惰性执行,这称为定义-运行。这两个阶段,1)神经网络构建阶段和 2)评估阶段,是按顺序执行的,神经网络结构在构建阶段之后不能更改。更新的 DL 框架,如 TensorFlow 2.0(或 TensorFlow 的急切执行版本)和 PyTorch,支持按运行时定义神经网络计算。没有用于构建和评估神经网络的两个单独阶段。用户可以在计算过程中直接操作神经网络。虽然 Optuna 提供的按运行时定义 API 可以用于动态定义超参数搜索空间,但它确实有一些缺点。主要问题是在运行时不知道参数并发性,这可能会使优化方法的实现复杂化。这是因为事先了解参数并发性对于许多采样方法是有很好的支持的。因此,在本书中,我们更喜欢使用定义-运行API。还请注意,Ray Tune 可以通过与 Optuna 的集成支持按运行时定义API(您可以在 Ray Tune 的 GitHub 存储库中看到一个示例,位于github.com/ray-project/ray/blob/master/python/ray/tune/examples/optuna_define_by_run_example.py#L35)。

  • 与前沿 HPO 算法集成CS 和 CE):在CS方面,这三种框架中,HyperOpt 在支持或集成最新的前沿 HPO 采样和搜索方法方面开发活跃度最低。其主要的搜索方法是树结构帕森估计器TPE),这是一种贝叶斯优化变体,特别适用于混合分类和条件超参数搜索空间。同样,Optuna 的主要采样方法也是 TPE。相反,Ray Tune 支持包括以下内容的所有前沿搜索方法:

此外,Ray Tune 还通过与 Optuna 和 HyperOpt 的集成支持 TPE。

CE 方面,HyperOpt 不支持任何修剪或调度器来停止不 promising 的超参数配置。Optuna 和 Ray Tune 都支持相当多的修剪器(在 Optuna 中)或调度器(在 Ray Tune 中)。然而,只有 Ray Tune 支持 PBT。考虑到 Ray Tune 活跃的开发社区和灵活的 API,Ray Tune 很有可能会继续及时集成并支持任何新兴的调度器或修剪器。

  • 深度学习框架的支持:HyperOpt 并非专为任何深度学习框架设计或集成。这并不意味着不能使用 HyperOpt 调优深度学习模型。然而,HyperOpt 不提供任何修剪或调度器支持来对不 promising 的超参数配置进行早停,这是 HyperOpt 用于深度学习模型调优的一个主要缺点。Ray Tune 和 Optuna 都与流行的深度学习框架如 PyTorch Lightning 和 TensorFlow/Keras 集成。

除了我们刚才讨论的主要标准,Ray Tune 还拥有最佳的文档、广泛的代码示例和充满活力的开源开发者社区,这也是我们在本章中偏向使用 Ray Tune 进行学习的原因。在接下来的部分中,我们将学习如何使用 Ray Tune 和 MLflow 创建适合超参数优化的深度学习模型。

使用 Ray Tune 和 MLflow 创建适合超参数优化的深度学习模型

为了在超参数优化中使用 Ray Tune 与 MLflow,我们将使用我们在 第五章 中的深度学习管道示例中的微调步骤,看看需要设置什么内容以及我们需要做哪些代码更改。在开始之前,首先让我们回顾一下几个与我们使用 Ray Tune 特别相关的关键概念:

  • 目标函数:目标函数可以是最小化或最大化给定超参数配置的某个指标值。例如,在深度学习模型训练和微调的场景中,我们希望最大化 NLP 文本分类器的 F1 分数。这一目标函数需要被包装成一个可训练的函数,Ray Tune 可以进行超参数优化。在接下来的部分中,我们将演示如何包装我们的 NLP 文本情感分析模型。

  • tune.report 用于报告模型指标 (docs.ray.io/en/latest/tune/api_docs/trainable.html#function-api)。基于类的 API 要求模型训练函数(trainable)是 tune.Trainable 的子类 (docs.ray.io/en/latest/tune/api_docs/trainable.html#trainable-class-api)。基于类的 API 提供了更多控制 Ray Tune 如何控制模型训练过程的方式。如果你开始编写神经网络模型的新架构,这可能非常有用。然而,当使用预训练的基础模型进行微调时,使用基于函数的 API 会更容易,因为我们可以利用像 PyTorch Lightning Flash 这样的包来进行 HPO。

  • tune.run,在这里 Ray Tune 将协调超参数优化(HPO)过程。

  • tune.loguniform) 或来自某些类别变量(例如,tune.choice(['a', 'b' ,'c']) 允许你均匀选择这三个选项)。通常,这个搜索空间被定义为一个名为 config 的 Python 字典变量。

  • tune.suggest API (docs.ray.io/en/latest/tune/api_docs/suggestion.html#tune-search-alg)。

  • tune.suggest API 提供了用于搜索的优化算法,但它不提供早期停止或修剪功能,以便在仅经过几次迭代后停止明显不太可能成功的实验。由于早期停止或修剪可以显著加速 HPO 过程,因此强烈建议你结合搜索器使用调度器。Ray Tune 通过其调度器 API (tune.schedulers) 提供了许多流行的调度器,如 ASHA、HyperBand 等。(请访问 docs.ray.io/en/latest/tune/api_docs/schedulers.html#trial-schedulers-tune-schedulers.)

在回顾了 Ray Tune 的基本概念和 API 后,在下一节中,我们将设置 Ray Tune 和 MLflow 来运行 HPO 实验。

设置 Ray Tune 和 MLflow

现在我们理解了 Ray Tune 的基本概念和 API,让我们看看如何设置 Ray Tune 来执行之前的 NLP 情感分类器的微调步骤的 HPO。你可能想要下载本章的代码 (github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter06/),以便跟随这些说明:

  1. 通过在你的 conda 虚拟环境 dl_model_hpo 中输入以下命令来安装 Ray Tune:

    pip install ray[tune]==1.9.2
    
  2. 这将会在你启动 DL 模型微调的 HPO 任务时,在虚拟环境中安装 Ray Tune。请注意,我们还提供了完整的requirements.txt文件,位于本章的 GitHub 仓库中(github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter06/requirements.txt),你应该能够运行以下安装命令:

    pip install -r requirements.txt
    
  3. 位于同一文件夹中的README.md文件包含完整的说明,如果你需要了解如何设置合适的虚拟环境,它将为你提供更多的指导。

  4. 对于 MLflow 设置,假设你已经设置好了完整的 MLflow 跟踪服务器,唯一需要注意的是确保你正确配置了环境变量,以便访问 MLflow 跟踪服务器。在你的终端中运行以下命令来设置这些变量。或者,你也可以通过在 Python 代码中调用os.environ["environmental_name"]=value来覆盖你的环境变量。提醒一下,我们已经在终端会话中展示了以下可以设置的环境变量:

    export MLFLOW_TRACKING_URI=http://localhost
    export MLFLOW_S3_ENDPOINT_URL=http://localhost:9000
    export AWS_ACCESS_KEY_ID="minio"
    export AWS_SECRET_ACCESS_KEY="minio123"
    
  5. 运行download_data步骤将原始数据下载到chapter06父文件夹下的本地文件夹:

    mlflow run . -P pipeline_steps='download_data' --experiment-name dl_model_chapter06
    

当前面的执行完成后,你应该能够在**chapter06/data/**文件夹下找到 IMDB 数据。

现在我们准备创建一个 HPO 步骤,以微调我们之前构建的 NLP 情感分析模型。

为 DL 模型创建 Ray Tune 可训练对象

我们需要做多个更改,才能让 Ray Tune 运行 HPO 任务来微调我们在前几章中开发的 DL 模型。我们将逐步演示这些步骤,如下所示:

  1. 首先,让我们列出在之前的微调代码中可能的超参数(包括可调和不可调的)。回想一下,我们的微调代码看起来如下(这里只显示关键代码行;完整代码可以在 GitHub 仓库的chapter05中找到,地址为github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter05/pipeline/fine_tuning_model.py#L19):

    datamodule = TextClassificationData.from_csv(
        input_fields="review",
        target_fields="sentiment",
        train_file=f"{data_path}/imdb/train.csv",
        val_file=f"{data_path}/imdb/valid.csv",
        test_file=f"{data_path}/imdb/test.csv")
    classifier_model = TextClassifier(
        backbone= "prajjwal1/bert-tiny",
        num_classes=datamodule.num_classes, 
        metrics=torchmetrics.F1(datamodule.num_classes))
    trainer = flash.Trainer(max_epochs=3)
    trainer.finetune(classifier_model, 
        datamodule=datamodule, strategy="freeze") 
    

前面的代码包含四个主要部分:

为了学习目的,我们将选择learning_ratebatch_size作为两个需要调优的超参数,因为这两个参数对于优化深度学习模型至关重要。一旦你完成这一章,你应该能够轻松地将更多的超参数添加到优化候选列表中。

  1. Ray Tune 要求传入一个可训练的函数到tune.run。这意味着我们需要创建一个可训练的函数。默认情况下,训练函数只接受一个必需的输入参数config,它包含超参数和其他参数的键值对字典,并用于标识执行环境,如 MLflow 跟踪 URL。然而,Ray Tune 提供了一个包装函数,叫做tune.with_parameters,它允许你传递额外的任意参数和对象(docs.ray.io/en/latest/tune/tutorials/overview.html#how-can-i-pass-further-parameter-values-to-my-trainable)。首先,让我们创建一个名为finetuning_dl_model的函数,将我们刚才检查的微调步骤逻辑封装起来,并使用mlflow_mixin装饰器。这样可以确保每次调用该函数时,MLflow 会自动初始化。

    @mlflow_mixin
    def finetuning_dl_model(config, data_dir=None,
                            num_epochs=3, num_gpus=0):
    

该函数接受一个config字典作为输入,其中可以传入一组超参数和 MLflow 配置。此外,我们为函数签名添加了三个额外的参数:data_dir表示目录的位置,num_epochs表示每个试验的最大训练轮数,num_gpus表示每个试验使用的 GPU 数量(如果有的话)。

  1. 在这个mlflow_mixin装饰的函数中,如果需要,我们可以使用所有 MLflow 跟踪 API,但从 MLflow 版本 1.22.0 开始,由于 MLflow 的自动日志记录支持不再是实验性特性,而是一个成熟的生产级特性(github.com/mlflow/mlflow/releases/tag/v1.22.0),因此我们应该直接在代码中使用自动日志记录,如下所示:

    mlflow.pytorch.autolog()
    

这样做是高效的,并且无需进行任何更改。然而,batch_size超参数不会被自动记录,因此我们需要在微调完成后再添加一个日志记录语句,如下所示:

mlflow.log_param('batch_size',config['batch_size'])
  1. finetuning_dl_model函数的其余实现部分,大部分代码与之前相同。这里有一些更改。在datamodule变量赋值语句中,我们添加了batch_size=config['batch_size'],以便训练数据的迷你批量大小可以调整,如下所示:

    datamodule = TextClassificationData.from_csv(
        input_fields="review",
        target_fields="sentiment",
        train_file=f"{data_dir}/imdb/train.csv",
        val_file=f"{data_dir}/imdb/valid.csv",
        test_file=f"{data_dir}/imdb/test.csv",
        batch_size=config['batch_size'])
    
  2. 在定义classifier_model变量时,不再使用默认的超参数集值,而是需要传入config字典来分配这些值:

    classifier_model = TextClassifier(
        backbone=config['foundation_model'],
        learning_rate=config['lr'],
        optimizer=config['optimizer_type'],
        num_classes=datamodule.num_classes,
        metrics=torchmetrics.F1(datamodule.num_classes))
    
  3. 接下来,我们需要修改训练器赋值代码。在这里,我们需要做两件事:首先,我们需要定义一个度量标准的键值字典,以便从 PyTorch Lightning 传递到 Ray Tune。该字典中的键是 Ray Tune 试验运行中要引用的名称,而该字典中键的值是 PyTorch Lightning 报告的相应度量标准名称。

    PyTorch Lightning 验证步骤中的度量标准名称

    在将 metrics 传递给 Ray Tune 时,首先我们需要了解在 PyTorch Lightning 中验证步骤期间使用的指标名称,因为 HPO 仅使用验证数据进行评估,而不使用保留的测试数据集。实际上,PyTorch Lightning 有一个硬编码的约定,所有指标的名称都会以相应的训练、验证和测试步骤名称及下划线为前缀。名为f1的指标将在 PyTorch Lightning 中作为train_f1在训练步骤中报告,在验证步骤中报告为val_f1,在测试步骤中报告为test_f1。(你可以在github.com/PyTorchLightning/lightning-flash/blob/8b244d785c5569e9aa7d2b878a5f94af976d3f55/flash/core/model.py#L462查看 PyTorch Lightning 的代码逻辑)。在我们的示例中,我们可以选择cross_entropyf1作为验证步骤中的指标,它们分别命名为val_cross_entropyval_f1,并将它们作为lossf1传递回 Ray Tune。这意味着,在 Ray Tune 的试验运行中,我们将这两个指标引用为lossf1

因此,在这里我们定义了两个希望从 PyTorch Lightning 验证步骤中传递到 Ray Tune 的指标,分别是val_cross_entropyval_f1,它们分别作为lossf1传递:

metrics = {"loss":"val_cross_entropy", "f1":"val_f1"}

现在,我们可以将这个 metrics 字典传递给 trainer 赋值,如下所示:

trainer = flash.Trainer(max_epochs=num_epochs,
    gpus=num_gpus,
    progress_bar_refresh_rate=0,
    callbacks=[TuneReportCallback(metrics, 
        on='validation_end')])

注意,metrics 字典会在validation_end事件发生时通过TuneReportCallBack传递。这意味着当 PyTorch Lightning 中的验证步骤完成时,它会自动触发 Ray Tune 报告函数,将指标列表反馈给 Ray Tune 进行评估。有关TuneReportCallback可以使用的有效事件的支持列表,请参考 Ray Tune 与 PyTorch Lightning 的集成源代码(github.com/ray-project/ray/blob/fb0d6e6b0b48b0a681719433691405b96fbea104/python/ray/tune/integration/pytorch_lightning.py#L170)。

  1. 最后,我们可以调用trainer.finetune来执行微调步骤。在这里,我们可以将finetuning_strategies作为可调超参数传递给参数列表:

    trainer.finetune(classifier_model,
        datamodule=datamodule,
        strategy=config['finetuning_strategies'])
    
  2. 这完成了原始微调 DL 模型函数的更改。现在我们有一个新的finetuning_dl_model函数,准备被with_parameters包装以成为 Ray Tune 的可训练函数。它应该如下所示调用:

    trainable = tune.with_parameters(finetuning_dl_model, data_dir, num_epochs, num_gpus)
    
  3. 请注意,无需传递config参数,因为默认假设它是finetuning_dl_model的第一个参数。其他三个参数需要传递给tune.with_parameters包装器。同时,确保创建 Ray Tune 可训练对象的语句放在finetuning_dl_model函数外部。

在下一部分,它将被放入 Ray Tune 的 HPO 运行函数中,名为run_hpo_dl_model

创建 Ray Tune HPO 运行函数

现在,让我们创建一个 Ray Tune HPO 运行函数来执行以下五个步骤:

  • 定义 MLflow 运行时配置参数,包括追踪 URI 和实验名称。

  • 使用 Ray Tune 的随机分布 API 定义超参数搜索空间(docs.ray.io/en/latest/tune/api_docs/search_space.html#random-distributions-api),以采样我们之前确定的超参数列表。

  • 使用tune.with_parameters定义 Ray Tune 的可训练对象,如前一小节末尾所示。

  • 调用tune.run。这将执行 HPO 运行,并在完成时返回 Ray Tune 的实验分析对象。

  • 在整个 HPO 运行完成后,记录最佳配置参数。

让我们一起查看实现过程,看看这个函数是如何实现的:

  1. 首先,让我们定义超参数的config字典,如下所示:

    mlflow.set_tracking_uri(tracking_uri)
    mlflow.set_experiment(experiment_name)
    

这将接受tracking_uriexperiment_name作为 MLflow 的输入参数,并正确设置它们。如果这是你第一次运行,MLflow 还将创建该实验。

  1. 然后,我们可以定义config字典,其中可以包括可调和不可调的参数,以及 MLflow 的配置参数。如前所述,我们将调整learning_ratebatch_size,但也会包括其他超参数用于记账和未来的调优目的:

    config = {
            "lr": tune.loguniform(1e-4, 1e-1),
            "batch_size": tune.choice([32, 64, 128]),
            "foundation_model": "prajjwal1/bert-tiny",
            "finetuning_strategies": "freeze",
            "optimizer_type": "Adam",
            "mlflow": {
                "experiment_name": experiment_name,
                "tracking_uri": mlflow.get_tracking_uri()
            },
        }
    

config字典中可以看到,我们调用了tune.loguniform来在1e-41e-1之间采样一个对数均匀分布,以选择学习率。对于批量大小,我们调用了tune.choice从三个不同的值中均匀选择。对于其余的键值对,它们是不可调的,因为它们没有使用任何采样方法,但在运行试验时是必需的。

  1. 使用tune.with_parameters定义可训练对象,包含除config参数之外的所有额外参数:

    trainable = tune.with_parameters(
        finetuning_dl_model,
        data_dir=data_dir,
        num_epochs=num_epochs,
        num_gpus=gpus_per_trial)
    

在下一个语句中,这将被称为 tune.run函数。

  1. 现在我们准备通过调用tune.run来运行 HPO,如下所示:

    analysis = tune.run(
        trainable,
        resources_per_trial={
            "cpu": 1,
            "gpu": gpus_per_trial
        },
        metric="f1",
        mode="max",
        config=config,
        num_samples=num_samples,
        name="hpo_tuning_dl_model")
    

在这里,目标是找到一组超参数,使得所有实验中的 F1-score 最大化,因此模式是 max,而指标是 f1。请注意,这个指标名称 f1 来自我们在之前的 finetuning_dl_model 函数中定义的 metrics 字典,其中我们将 PyTorch Lightning 的 val_f1 映射到 f1。然后,这个 f1 值会在每次实验验证步骤结束时传递给 Ray Tune。trainable 对象作为第一个参数传递给 tune.run,这个函数将根据 num_samples 的参数被执行多次。接下来,resources_per_trial 定义了每个实验使用的 CPU 和 GPU。请注意,在前面的示例中,我们没有指定任何搜索算法,这意味着它将默认使用 tune.suggest.basic_variant,这是一种网格搜索算法。我们也没有定义调度器,因此默认情况下不会进行早期停止,所有实验将并行运行,且使用执行机器上允许的最大 CPU 数量。当运行结束时,会返回一个 analysis 变量,包含找到的最佳超参数以及其他信息。

  1. 记录找到的最佳超参数配置。这可以通过使用从 tune.run 返回的 analysis 变量来完成,代码如下:

    logger.info("Best hyperparameters found were: %s", analysis.best_config)
    

就这样。现在我们可以试试看。如果你从本章的 GitHub 仓库下载完整代码,你应该能在 pipeline 文件夹下找到 hpo_finetuning_model.py 文件。

通过上述更改,我们现在已经准备好运行我们的第一个 HPO 实验。

使用 MLflow 运行第一个 Ray Tune HPO 实验

现在我们已经设置好了 Ray Tune、MLflow,并创建了 HPO 运行函数,我们可以尝试运行我们的第一个 Ray Tune HPO 实验,如下所示:

python pipeline/hpo_finetuning_model.py

几秒钟后,你将看到以下屏幕,图 6.2,显示所有 10 次实验(即我们为 num_samples 设置的值)正在并行运行:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_06_02.jpg

图 6.2 – Ray Tune 在本地多核笔记本上并行运行 10 次实验

大约 12-14 分钟后,你会看到所有实验都已完成,并且最佳超参数将显示在屏幕上,如下所示(由于随机性、样本数量有限以及网格搜索的使用,结果可能会有所不同,且网格搜索不保证全局最优解):

Best hyperparameters found were: {'lr': 0.025639008922511797, 'batch_size': 64, 'foundation_model': 'prajjwal1/bert-tiny', 'finetuning_strategies': 'freeze', 'optimizer_type': 'Adam', 'mlflow': {'experiment_name': 'hpo-tuning-chapter06', 'tracking_uri': 'http://localhost'}}

你可以在结果日志目录下找到每个实验的结果,默认情况下该目录位于当前用户的 ray_results 文件夹中。从 图 6.2 中我们可以看到,结果位于 /Users/yongliu/ray_results/hpo_tuning_dl_model

你将在屏幕上看到最佳超参数的最终输出,这意味着你已经完成了第一次 HPO 实验!你可以看到所有 10 次试验都已记录在 MLflow 跟踪服务器中,并且可以使用 MLflow 跟踪服务器提供的平行坐标图来可视化和比较所有 10 次运行。你可以通过进入 MLflow 实验页面,选择你刚完成的 10 次试验,然后点击页面顶部附近的Compare按钮来生成该图(参见图 6.3)。这将带你进入并排比较页面,页面底部会显示绘图选项:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_06_03.jpg

图 6.3 – 点击“Compare”以比较 MLflow 实验页面上所有 10 次试验运行

你可以点击平行坐标图菜单项,这将允许你选择要绘制的参数和指标。在这里,我们选择lrbatch_size作为参数,val_f1val_cross_entropy作为指标。绘图显示在图 6.4中:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_06_04.jpg

图 6.4 – 用于比较 HPO 实验结果的平行坐标图

正如你在图 6.4中看到的,非常容易看出batch_size为 128,lr为 0.02874 时,能够产生最佳的val_f1得分 0.6544 和val_cross_entropy(损失值)为 0.62222。正如前面所提到的,这次 HPO 运行没有使用任何高级搜索算法和调度器,因此让我们看看在接下来的部分中,通过使用提前停止和修剪,我们能否通过更多的实验做得更好。

使用 Optuna 和 HyperBand 通过 Ray Tune 运行 HPO

现在,让我们尝试一些不同的搜索算法和调度器的实验。鉴于 Optuna 是一种非常优秀的基于 TPE 的搜索算法,ASHA 是一种优秀的调度器,可以通过异步并行试验并提前终止那些不太有前景的试验,看看我们需要做多少更改才能使其工作将会很有趣。

结果表明,基于我们在前一部分所做的工作,变化非常微小。这里,我们将展示四个主要的变化:

  1. 安装Optuna包。可以通过运行以下命令来完成:

    pip install optuna==2.10.0
    

这将把 Optuna 安装到我们之前的虚拟环境中。如果你已经运行了pip install -r requirements.text,那么 Optuna 已经被安装,你可以跳过这一步。

  1. 导入与 Optuna 和 ASHA 调度器集成的相关 Ray Tune 模块(在这里,我们使用 ASHA 的 HyperBand 实现)如下:

    from ray.tune.suggest import ConcurrencyLimiter
    from ray.tune.schedulers import AsyncHyperBandScheduler
    from ray.tune.suggest.optuna import OptunaSearch
    
  2. 现在,我们准备好将搜索算法变量和调度器变量添加到 HPO 执行函数run_hpo_dl_model中了,具体如下:

    searcher = OptunaSearch()
    searcher = ConcurrencyLimiter(searcher, max_concurrent=4)
    scheduler = AsyncHyperBandScheduler()
    

请注意,searcher 变量现在使用的是 Optuna,我们将并发运行的最大次数设置为 4,让这个 searcher 变量在 HPO 搜索过程中每次尝试最多同时运行四个试验。调度器初始化时使用 HyperBand 调度器。

  1. 将 searcher 和 scheduler 分配给 tune.run 调用的相应参数,如下所示:

    analysis = tune.run(
        trainable,
        resources_per_trial={
            "cpu": 1,
            "gpu": gpus_per_trial
        },
        metric="f1",
        mode="max",
        config=config,
        num_samples=num_samples,
        search_alg=searcher,
        scheduler=scheduler,
        name="hpo_tuning_dl_model")
    

请注意,searcher 被分配给了 search_alg 参数,scheduler 被分配给了 scheduler 参数。就这样。现在,我们已经准备好在统一的 Ray Tune 框架下使用 Optuna 进行 HPO,并且已经通过 Ray Tune 提供了所有的 MLflow 集成。

我们已经在 pipeline 文件夹中的 hpo_finetuning_model_optuna.py 文件中提供了完整的 Python 代码。我们可以按照以下步骤运行这个 HPO 实验:

python pipeline/hpo_finetuning_model_optuna.py

你将立即注意到控制台输出中的以下内容:

[I 2022-02-06 21:01:27,609] A new study created in memory with name: optuna

这意味着我们现在使用 Optuna 作为搜索算法。此外,你会注意到在屏幕显示的状态输出中有四个并发的试验。随着时间的推移,一些试验会在完成之前的一个或两个迭代(epoch)后被终止。这意味着 ASHA 正在工作,已经淘汰了那些没有前景的试验,以节省计算资源并加快搜索过程。图 6.5 显示了运行过程中的一个输出,其中三个试验仅进行了一个迭代就被终止。你可以在状态输出中找到 num_stopped=3(在图 6.5中的第三行),其中显示 Using AsynHyerBand: num_stopped=3。这意味着 AsyncHyperBand 在试验完成之前就终止了这三个试验:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_06_05.jpg

图 6.5 – 使用 Optuna 和 AsyncHyperBand 在 Ray Tune 上运行 HPO

运行结束时,你将看到以下结果:

2022-02-06 21:11:59,695    INFO tune.py:626 -- Total run time: 632.10 seconds (631.91 seconds for the tuning loop).
2022-02-06 21:11:59,728 Best hyperparameters found were: {'lr': 0.0009599443695046438, 'batch_size': 128, 'foundation_model': 'prajjwal1/bert-tiny', 'finetuning_strategies': 'freeze', 'optimizer_type': 'Adam', 'mlflow': {'experiment_name': 'hpo-tuning-chapter06', 'tracking_uri': 'http://localhost'}}

请注意,总运行时间仅为 10 分钟。与上一节中使用没有提前停止的网格搜索相比,这节省了 2-4 分钟。现在,这看起来可能很短,但请记住,我们这里只使用了一个小型的 BERT 模型,且只有 3 个 epoch。在生产环境中的 HPO 运行中,使用 20 个 epoch 的大型预训练基础模型并不罕见,而结合良好的搜索算法和调度器(如异步 HyperBand 调度器)搜索速度将会显著提升。Ray Tune 提供的 MLflow 集成是免费的,现在我们可以在一个框架下切换不同的搜索算法和/或调度器。

虽然本节仅向您展示了如何在 Ray Tune 和 MLflow 框架中使用 Optuna,但将 Optuna 替换为 HyperOpt 只是一个简单的替换操作。我们可以用 HyperOptSearch 替代 OptunaSearch 来初始化搜索器(您可以参考示例:github.com/ray-project/ray/blob/d6b0b9a209e3f693afa6441eb284e48c02b10a80/python/ray/tune/examples/hyperopt_conditional_search_space_example.py#L80),其他代码保持不变。我们将这个作为练习留给你自行探索。

使用不同的搜索算法和调度器与 Ray Tune 配合

请注意,并不是所有的搜索算法都能与任何调度器配合使用。你选择的搜索算法和调度器取决于模型的复杂性和评估成本。对于深度学习模型,由于每个训练周期的运行成本通常很高,因此非常推荐使用现代搜索算法,如 TPE、Dragonfly 和 BlendSearch,并结合像我们使用的 HyperBand 调度器等 ASHA 类型的调度器。有关选择哪些搜索算法和调度器的更详细指南,您可以查阅 Ray Tune 网站上的以下文档:docs.ray.io/en/latest/tune/tutorials/overview.html#which-search-algorithm-scheduler-should-i-choose

现在我们已经理解了如何使用 Ray Tune 和 MLflow 为深度学习模型进行高并行和高效的 HPO,这为我们将来在大规模环境中进行更高级的 HPO 实验奠定了基础。

小结

在本章中,我们介绍了 HPO 的基本原理和挑战,为什么它对深度学习模型管道非常重要,以及现代 HPO 框架应该支持哪些内容。我们比较了三种流行的框架——Ray Tune、Optuna 和 HyperOpt,并选择了 Ray Tune 作为在大规模运行最先进 HPO 的最佳框架。我们展示了如何使用 Ray Tune 和 MLflow 创建适合 HPO 的深度学习模型代码,并使用 Ray Tune 和 MLflow 运行了我们的第一次 HPO 实验。此外,我们还介绍了在设置好 HPO 代码框架后,如何切换到其他搜索和调度算法,举例说明了如何使用 Optuna 和 HyperBand 调度器。通过本章的学习,您将能够在真实的生产环境中胜任大规模的 HPO 实验,从而以一种具有成本效益的方式产生高性能的深度学习模型。我们还在本章末尾提供了许多参考文献,在进一步阅读部分鼓励您深入学习。

在下一章中,我们将继续学习如何使用 MLflow 构建模型推理管道的预处理和后处理步骤,这是在深度学习模型经过 HPO 调优并准备投入生产后,真实生产环境中的典型场景。

进一步阅读

第四部分 –

大规模部署深度学习管道

在本节中,我们将学习如何实现和部署一个多步骤推理管道,以便用于生产环境。我们将从生产环境中四种推理工作流模式的概述开始。接着,我们将学习如何使用 MLflow PyFunc API,围绕一个经过微调的深度学习DL)模型,实施一个包含预处理和后处理步骤的多步骤推理管道。在这个准备好部署的 MLflow PyFunc 兼容的深度学习推理管道上,我们将了解不同的部署工具和托管环境,以便选择适合特定部署场景的工具。然后,我们将使用 MLflow 的 Spark 用户定义函数UDF)实现并部署一个批量推理管道。之后,我们将专注于使用 MLflow 内置的模型服务工具或 Ray Serve 的 MLflow 部署插件,部署一个 Web 服务。最后,我们将展示一个完整的逐步指南,介绍如何将一个深度学习推理管道部署到管理的 AWS SageMaker 实例中,以便用于生产环境。

本节包括以下章节:

  • 第七章多步骤深度学习推理管道

  • 第八章大规模部署深度学习推理管道

第七章:第七章:多步骤深度学习推理管道

现在我们已经成功运行了HPO超参数优化),并生成了一个经过良好调优、满足业务需求的深度学习模型,是时候迈向下一步,开始使用这个模型进行预测。此时,模型推理管道便发挥了作用,模型将用于在生产环境中预测或评分真实数据,无论是实时模式还是批处理模式。然而,推理管道通常不仅仅依赖单一模型,还需要一些在模型开发阶段可能未见过的预处理和后处理逻辑。预处理步骤的例子包括在将输入数据传递给模型进行评分之前,检测语言区域(英语或其他语言)。后处理可能包括用额外的元数据来丰富预测标签,以满足业务应用的需求。还有一些深度学习推理管道模式,甚至可能涉及多个模型的集成,以解决现实世界中的业务问题。许多机器学习项目往往低估了实现生产环境推理管道所需的工作,这可能导致模型在生产环境中的性能下降,甚至在最糟糕的情况下,整个项目失败。因此,在将模型部署到生产环境之前,了解如何识别不同推理管道的模式并正确实现它们是非常重要的。

到本章结束时,你将能够使用 MLflow 自信地实现多步骤推理管道中的预处理和后处理步骤,并为将来章节中的生产环境使用做好准备。

本章将涵盖以下主要内容:

  • 理解深度学习推理管道的模式

  • 理解 MLflow 模型 Python 函数 API

  • 实现自定义 MLflow Python 模型

  • 在深度学习推理管道中实现预处理和后处理步骤

  • 将推理管道作为主机器学习项目中的新入口点进行实现

技术要求

本章的技术要求如下:

理解深度学习推理管道的模式

随着模型开发进入为即将到来的生产使用实现推理管道的阶段,必须理解,拥有一个调优良好的深度学习模型只是商业 AI 战略成功的一半。另一半包括部署、服务、监控以及模型投入生产后的持续改进。设计和实现深度学习推理管道是迈向故事第二阶段的第一步。尽管模型已经在精心挑选的离线数据集上进行了训练、调优和测试,现在它需要以两种方式处理预测:

  • 批量推理:这通常需要定期或临时执行推理管道,针对一些离线批量的观察数据。生成预测结果的周转时间通常为每天、每周或其他预定的时间安排。

  • 在线推理:这通常需要一个 Web 服务来实时执行推理管道,为输入数据在不到一秒钟,甚至少于 100 毫秒的时间内生成预测结果,具体取决于用户场景。

请注意,由于执行环境和数据特征可能与离线训练和测试环境不同,因此在核心模型逻辑周围会有额外的预处理或后处理步骤,这些逻辑是在模型训练和调优步骤中开发出来的。需要强调的是,任何可以共享的数据预处理步骤应该同时在训练管道和推理管道中使用,但不可避免地,一些业务逻辑将会介入,这会使推理管道具备额外的预处理和后处理逻辑。例如,在深度学习推理管道中,一个非常常见的步骤是使用缓存来存储并返回基于最近输入的预测结果,这样就不必每次都调用昂贵的模型评估过程。在模型开发阶段,训练/测试管道不需要此步骤。

尽管推理管道的模式仍在不断发展,但现在已经普遍认为,现实生产环境中至少有四种模式:

  • 多步骤管道:这是该模型在生产中最典型的使用方式,包括在调用模型逻辑之前的一系列预处理步骤,以及在模型评估结果返回之后的一些后处理步骤。虽然从概念上看这很简单,但实现方式仍然可以有所不同。本章将展示如何使用 MLflow 高效地完成这项工作。

  • 模型集成:这是一个更复杂的场景,其中可以使用多个不同的模型。这些模型可以是相同类型的模型,只是版本不同,用于 A/B 测试,或者是不同类型的模型。例如,在复杂的对话 AI 聊天机器人场景中,需要一个意图分类模型来将用户查询的意图分类到特定类别中。然后,还需要一个内容相关性模型,根据检测到的用户意图检索相关答案并呈现给用户。

  • 业务逻辑和模型:这通常涉及如何以及从哪里获取模型输入的额外业务逻辑,例如从企业数据库查询用户信息和验证,或者在调用模型之前从特征库中检索预先计算的额外特征。此外,后处理业务逻辑还可以将预测结果转化为某些特定应用的逻辑,并将结果存储在后台存储中。虽然这可以是一个简单的线性多步骤管道,但它也可以迅速变成一个DAG有向无环图),在模型调用前后具有多个并行的 fan-in 和 fan-out 任务。

  • 在线学习:这是生产中最复杂的推理任务之一,其中模型会不断地学习并更新其参数,例如强化学习。

虽然理解生产环境中推理管道复杂性的全局图景是必要的,但本章的目的是学习如何通过强大且通用的 MLflow 模型 API 创建可重用的推理管道构建块,这些构建块可以在多个场景中使用,并能封装预处理和后处理步骤与训练好的模型。感兴趣的读者可以通过这篇文章(www.anyscale.com/blog/serving-ml-models-in-production-common-patterns)和进一步阅读部分中的其他参考资料,深入了解生产中的模型模式。

那么,什么是 MLflow 模型 API,如何使用它来实现多步骤推理管道的预处理和后处理逻辑呢?让我们在下一节中了解。

作为 MLflow 模型的多步骤推理管道

在之前的第三章中,跟踪模型、参数和指标,我们介绍了使用 MLflow MLproject的灵活松耦合多步骤管道实现,使我们能够在 MLflow 中显式地执行和跟踪多步骤训练管道。然而,在推理时,希望在已记录的模型库中的训练模型旁边实现轻量级的预处理和后处理逻辑。MLflow 模型 API 提供了一种机制,将训练好的模型与预处理和后处理逻辑封装起来,然后将新封装的模型保存为一个新模型,封装了推理管道逻辑。这统一了使用 MLflow 模型 API 加载原始模型或推理管道模型的方式。这对于使用 MLflow 进行灵活部署至关重要,并为创造性推理管道的构建打开了大门。

了解 MLflow 模型 Python 函数 API

MLflow 模型(www.mlflow.org/docs/latest/models.html#id25)是 MLflow 提供的核心组件之一,用于加载、保存和记录不同类型的模型(例如,MLmodel文件:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_07_01.jpg

图 7.1 – 微调 PyTorch 模型的 MLmodel 内容

图 7.1可以看到,该模型的类型是 PyTorch。还有一些关于模型的其他元数据,例如 conda 环境,它定义了运行该模型的依赖项,以及许多其他内容。凭借这些自包含的信息,应该足以使 MLflow 通过mlflow.pytorch.load_model API 如下所示加载该模型:

logged_model = f'runs:/{run_id}/model'
model = mlflow.pytorch.load_model(logged_model)

这将允许将通过run_id记录的 MLflow 运行模型加载回内存并进行推理。现在假设我们有以下场景,需要添加一些预处理逻辑来检查输入文本的语言类型。这需要加载一个语言检测模型(amitness.com/2019/07/identify-text-language-python/),例如FastText语言检测器(fasttext.cc/)或谷歌的Compact Language Detector v3pypi.org/project/gcld3/)。此外,我们还希望检查是否存在完全相同输入的缓存预测。如果存在,则应该直接返回缓存结果,而不调用耗时的模型预测部分。这是非常典型的预处理逻辑。对于后处理,一个常见的场景是返回预测结果以及有关模型 URI 的一些元数据,以便我们可以调试生产中的任何潜在预测问题。基于这些预处理和后处理逻辑,推理管道现在看起来如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_07_02.jpg

图 7.2 – 多步骤推理管道

图 7.2可以看出,这五个步骤包括以下内容:

  • 一个原始的、经过微调的预测模型(一个 PyTorch 深度学习模型)

  • 一个额外的语言检测模型,这个模型并未包含在我们之前的训练流程中

  • 缓存操作(检查缓存并存储到缓存中)以提高响应性能

  • 一个响应消息组成步骤

与其将这五个步骤拆分成五个不同的入口点放入机器学习项目中(回想一下,机器学习项目中的入口点可以是 Python 中的任意执行代码或其他可执行文件),将这多步骤推理管道组合成一个入口点显得更为优雅,因为这些步骤与模型的预测步骤紧密相关。此外,将这些紧密相关的步骤封装成一个推理管道的优点是,我们可以将推理管道保存并作为 MLmodel 工件加载。MLflow 提供了一种通用方法,将这个多步骤推理管道实现为一个新的 Python 模型,同时不会失去在需要时添加额外预处理和后处理功能的灵活性,正如下面的图所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_07_03.jpg

图 7.3 – 将多步骤的预处理和后处理逻辑封装到一个新的 MLflow Python 模型中

图 7.3可以看出,如果我们将预处理和后处理逻辑封装成一个新的 MLflow 模型,称为 inference_pipeline_model,那么我们可以像加载其他模型一样加载整个推理管道。这还允许我们规范化推理管道的输入和输出格式(称为模型签名),这样任何想要使用这个推理管道的人都不需要猜测输入和输出的格式是什么。

在高层次上实现此机制的方式如下:

  1. 首先,创建一个自定义的 MLflow pyfunc(Python 函数)模型(www.mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#creating-custom-pyfunc-models)来包装现有的训练模型。具体来说,我们需要超越 MLflow 提供的内置模型类型(www.mlflow.org/docs/latest/models.html#built-in-model-flavors),实现一个新的 Python 类,继承自 mlflow.pyfunc.PythonModelwww.mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.PythonModel),并在这个新的 Python 类中定义 predict() 和(可选的)load_context() 方法。

此外,我们可以通过定义模型输入和输出的模式来指定模型签名mlflow.org/docs/latest/models.html#model-signature)。这些模式可以是基于列的,也可以是基于张量的。强烈建议在生产环境中实现这些模式,以便进行自动输入验证和模型诊断。

  1. 然后,在此 MLflow pyfunc中实现预处理和后处理逻辑。这些逻辑可能包括缓存、语言检测、响应消息以及其他所需的逻辑。

  2. 最后,在 ML 项目中实现推理流水线的入口点,以便我们可以像调用单一模型工件一样调用推理流水线。

现在我们已经理解了 MLflow 自定义 Python 模型的基本原理,来表示一个多步骤的推理流水线,接下来让我们看看如何为我们的 NLP 情感分类模型实现它,预处理和后处理步骤在下文的图 7.3中进行了描述。

实现自定义 MLflow Python 模型

首先,让我们描述实现一个自定义 MLflow Python 模型的步骤,而不包含任何额外的预处理和后处理逻辑:

  1. 首先,确保我们有一个训练好的深度学习模型,准备好用于推理。在本章的学习中,我们将本章的训练流水线README文件包含在 GitHub 仓库中,并设置相应的环境变量github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter07/README.md)。然后,在命令行中运行以下命令,在本地 MLflow 跟踪服务器上生成一个微调后的模型:

    mlflow run . --experiment-name dl_model_chapter07 -P pipeline_steps=download_data,fine_tuning_model
    

完成后,您将在 MLflow 跟踪服务器中记录一个微调的深度学习模型。现在,我们将使用记录的模型 URI 作为推理流水线的输入,因为我们将把它封装起来并保存为一个新的 MLflow 模型。记录的模型 URI 类似于以下内容,其中长的随机字母数字字符串是fine_tuning_model MLflow 运行的run_id,您可以在 MLflow 跟踪服务器中找到:

runs:/1290f813d8e74a249c86eeab9f6ed24e/model
  1. 一旦您有了训练/微调后的模型,我们就准备好按如下方式实现一个新的自定义 MLflow Python 模型。您可以查看 GitHub 仓库中的basic_custom_dl_model.pygithub.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter07/notebooks/basic_custom_dl_model.py),以跟随这里概述的步骤:

    class InferencePipeline(mlflow.pyfunc.PythonModel):
        def __init__(self, finetuned_model_uri):
            self.finetuned_model_uri = finetuned_model_uri
        def sentiment_classifier(self, row):
            pred_label = self.finetuned_text_classifier.predict({row[0]})
            return pred_label
        def load_context(self, context):
            self.finetuned_text_classifier = mlflow.pytorch.load_model(self.finetuned_model_uri)
        def predict(self, context, model_input):
            results = model_input.apply(
                        self.sentiment_classifier, axis=1,
                        result_type='broadcast')
            return results
    

让我们看看我们已经实现了什么。首先,InferencePipeline类继承自MLflow.pyfunc.PythonModel模块,并实现了如下四个方法:

  • predict:这是mlflow.pyfunc.PythonModel要求的方法,用于返回预测结果。在这里,model_input参数是一个pandas DataFrame,其中包含一个需要分类的输入文本列。我们利用pandas DataFrame 的apply方法来运行sentiment_classifier方法,对每一行的文本进行评分,结果是一个 DataFrame,其中每一行都是预测标签。由于我们原始的微调模型不接受pandas DataFrame 作为输入(它接受的是文本字符串的列表),我们需要实现一个新的分类器,作为原始模型的封装器。这就是sentiment_classifier方法。另一个context参数是 MLflow 上下文,用于描述模型工件存储的位置。由于我们将传递一个 MLflow 记录的模型 URI,因此context参数在我们的实现中没有使用,因为记录的模型 URI 包含了加载模型所需的所有信息。

  • sentiment_classifier:这是一个封装方法,允许通过调用微调后的 DL 模型的预测函数来为输入的每一行pandas DataFrame 评分。请注意,我们将行的第一个元素包装成一个列表,以便 DL 模型可以正确地将其用作输入。

  • init:这是一个标准的 Python 构造方法。在这里,我们使用它来传入一个之前微调的 DL 模型 URI,finetuned_model_uri,以便我们可以在load_context方法中加载它。请注意,我们不希望直接在init方法中加载模型,因为那样会导致序列化问题(如果你尝试,你会发现直接序列化 DL 模型并不是一件轻松的事情)。由于微调后的 DL 模型已经通过mlflow.pytorch API 进行了序列化和反序列化,我们不需要在这里重新发明轮子。推荐的方法是在load_context方法中加载模型。

  • load_context:此方法在使用mlflow.pyfunc.load_model API 加载 MLflow 模型时调用。在构建 Python 模型后立即执行。在这里,我们通过mlflow.pytorch.load_model API 加载微调后的 DL 模型。请注意,在此方法中加载的任何模型都可以使用相应的反序列化方法。这将为加载其他模型提供可能性,例如语言检测模型,它可能包含不能通过 Python 序列化协议进行序列化的本地代码(例如,C++代码)。这是 MLflow 模型 API 框架提供的一个优点。

  1. 现在,我们有了一个可以接受基于列输入的 MLflow 自定义模型,我们还可以按如下方式定义模型签名:

    input = json.dumps([{'name': 'text', 'type': 'string'}])
    output = json.dumps([{'name': 'text', 'type': 'string'}])
    signature = ModelSignature.from_dict({'inputs': input, 'outputs': output})
    

这个签名定义了一个输入格式,其中包含一个名为text的列,数据类型为string,以及一个输出格式,其中包含一个名为text的列,数据类型为stringmlflow.models.ModelSignature类用于创建这个signature对象。当我们在 MLflow 中记录新的自定义模型时,将使用此签名对象,正如我们将在下一步中看到的。

  1. 接下来,我们可以像使用通用的 MLflow pyfunc模型一样,使用mlflow.pyfunc.log_model API 将这个新的自定义模型记录到 MLflow 中,代码如下:

    MODEL_ARTIFACT_PATH = 'inference_pipeline_model'
    with mlflow.start_run() as dl_model_tracking_run:
        finetuned_model_uri = 'runs:/1290f813d8e74a249c86eeab9f6ed24e/model'
        inference_pipeline_uri = f'runs:/{dl_model_tracking_run.info.run_id}/{MODEL_ARTIFACT_PATH}'
        mlflow.pyfunc.log_model(
          artifact_path=MODEL_ARTIFACT_PATH,
          conda_env=CONDA_ENV,
          python_model=InferencePipeline(
            finetuned_model_uri),
          signature=signature)
    

上述代码将把模型记录到 MLflow 跟踪服务器中,根文件夹名为inference_pipeline_model,这是因为我们定义了MODEL_ARTIFACT_PATH变量并将其值分配给mlflow.pyfunc.log_model方法的artifact_path参数。我们分配给的其他三个参数如下:

  • conda_env:这是定义此自定义模型运行的 conda 环境。在这里,我们可以传递conda.yaml文件的绝对路径,该文件位于本章根文件夹中,由CONDA_ENV变量定义(此变量的详细信息可以在 GitHub 上找到basic_custom_dl_model.py笔记本的源代码中)。

  • python_model:在这里,我们调用了刚刚实现的新的InferencePipeline类,并传入了finetuned_model_uri参数。这样,推理管道就会加载正确的微调模型进行预测。

  • signature:我们还传递了刚刚定义的输入和输出的签名,并将其分配给 signature 参数,以便可以记录模型的输入输出架构并进行验证。

提醒一下,确保你将'runs:/1290f813d8e74a249c86eeab9f6ed24e/model'值替换为你在步骤 1中生成的自己的微调模型 URI,这样代码才能正确加载原始的微调模型。

  1. 如果你按照basic_custom_dl_model.py逐步执行,直到步骤 4,你应该能够在 MLflow 跟踪服务器的Artifacts部分找到一个新记录的模型,正如下面截图所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_07_04.jpg

图 7.4 – 推理 MLflow 模型,带有模型架构和名为 inference_pipeline_model 的根文件夹

正如图 7.4所示,根文件夹的名称(截图左上角)是inference_pipeline_model,这是调用mlflow.pyfunc.log_modelartifact_path参数的指定值。请注意,如果我们没有指定artifact_path参数,默认情况下它的值将是model。你可以通过查看本章早些时候的图 7.1来确认这一点。还要注意,现在在inference_pipeline_model文件夹下有一个MLmodel文件,我们可以看到其完整内容如下:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_07_05.jpg

图 7.5 – inference_pipeline_model的 MLmodel 文件内容

图 7.5中可以看到,在底部附近的signature部分是一个新的部分,相较于图 7.1有所不同。然而,在模型类型方面还有一些更重要的区别。inference_pipeline_model的类型是通用的mlflow.pyfunc.model模型,不再是mlflow.pytorch模型。事实上,如果你将图 7.5图 7.1进行对比,后者是我们的 PyTorch 微调的深度学习模型,你会发现其中有关于pytorch及其model_datapytorch_version的部分,而这些在图 7.5中已经完全消失。对于 MLflow 来说,它并不了解原始的 PyTorch 模型,而只是将其作为新封装的通用 MLflow pyfunc模型。这是一个好消息,因为现在我们只需要一个通用的 MLflow pyfunc API 来加载模型,无论封装的模型多复杂,或者这个通用的pyfunc模型中包含多少额外的预处理和后处理步骤,当我们在下一节中实现时都不成问题。

  1. 我们现在可以使用通用的mlflow.pyfunc.load_model来加载inference_pipeline_model并使用输入的pandas数据框进行预测,如下所示:

    input = {"text":["what a disappointing movie","Great movie"]}
    input_df = pd.DataFrame(input)
    with mlflow.start_run():
        loaded_model = \
        mlflow.pyfunc.load_model(inference_pipeline_uri)
        results = loaded_model.predict(input_df)
    

这里,inference_pipeline_uri步骤 4中生成的 URI,作为inference_pipeline_model的唯一标识符。例如,inference_pipeline_uri的值可能如下所示:

'runs:/6edf6013d2454f7f8a303431105f25f2/inference_pipeline_model'

一旦模型加载完成,我们只需调用predict函数对input_df数据框进行评分。这会调用我们新实现的InferencePipleine类的predict函数,如步骤 2中所述。结果将类似如下:

![图 7.6 – 推理管道在 pandas 数据框格式中的输出]

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_07_06.jpg)

图 7.6 – 推理管道在 pandas 数据框格式中的输出

如果你看到像图 7.6中的预测结果,那么你应该为自己感到骄傲,因为你刚刚实现了一个功能强大的自定义 MLflow Python 模型,这个模型具有巨大的灵活性和能力,使我们能够在不更改任何日志记录和加载模型部分的情况下,实施预处理和后处理逻辑,正如我们将在下一节中看到的那样。

创建一个新的 MLflow 自定义模型类型

正如本章所示,我们可以使用已经训练好的模型构建一个封装的 MLflow 自定义模型进行推理。需要注意的是,也可以为训练目的构建一个全新的 MLflow 自定义模型版本。这在你有一个尚未被内置 MLflow 模型版本支持的模型时是必要的。例如,如果你想基于自己的语料库训练一个全新的 FastText 模型,但截至 MLflow 1.23.1 版本,还没有 FastText 的 MLflow 模型版本,那么你可以构建一个新的 FastText MLflow 模型版本(参见参考:medium.com/@pennyqxr/how-save-and-load-fasttext-model-in-mlflow-format-37e4d6017bf0)。有兴趣的读者还可以在本章末尾的进一步阅读部分找到更多参考资料。

在深度学习推理管道中实现预处理和后处理步骤

现在我们有了一个基本的通用 MLflow Python 模型,可以对输入的 pandas DataFrame 进行预测,并在另一个 pandas DataFrame 中生成输出,我们已经准备好处理之前提到的多步骤推理场景。请注意,尽管上一节中的初始实现看起来可能没有什么突破性,但它为实现之前无法实现的预处理和后处理逻辑打开了大门,同时仍然保持使用通用的 mlflow.pyfunc.log_modelmlflow.pyfunc.load_model 将整个推理管道视为一个通用的 pyfunc 模型的能力,无论原始的深度学习模型有多复杂,或者有多少额外的预处理和后处理步骤。让我们在本节中看看如何做到这一点。你可能想查看 GitHub 上的 VS Code 笔记本中的 multistep_inference_model.py 来跟随本节中的步骤:github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter07/notebooks/multistep_inference_model.py

图 7.3中,我们描述了模型预测之前的两个预处理步骤,以及模型预测之后的两个后处理步骤。那么,在保持整个推理管道作为单个 MLflow 模型的同时,在哪里以及如何添加预处理和后处理逻辑呢?事实证明,主要的改动会发生在上一节实现的 InferencePipeline 类中。让我们在以下小节中一步步梳理实现和变化。

实现语言检测预处理逻辑

首先让我们实现语言检测的预处理逻辑:

  1. 为了检测输入文本的语言类型,我们可以使用 Google 的pyfunc模型。好消息是,MLflow 的load_context方法允许我们加载此模型,而无需担心序列化和反序列化。我们只需在InferencePipeline类的load_context方法中添加两行代码,如下所示,以加载语言检测器模型:

    import gcld3
    self.detector = gcld3.NNetLanguageIdentifier(
        min_num_bytes=0,
        max_num_bytes=1000)
    

上述两行代码被添加到了load_context方法中,此外还有先前用于加载情感分类微调深度学习模型的语句。这样,语言检测器将在InferencePipeline类初始化完成后立即加载。该语言检测器将使用输入的前 1,000 个字节来确定语言类型。一旦语言检测器加载完成,我们就可以在预处理方法中使用它来检测语言。

  1. 在语言检测的预处理方法中,我们将接受每一行输入文本,检测语言,并返回语言类型作为string,如下所示:

    def preprocessing_step_lang_detect(self, row):
        language_detected = \
        self.detector.FindLanguage(text=row[0])
        if language_detected.language != 'en':
            print("found Non-English Language text!")
        return language_detected.language
    

实现非常直接。我们还添加了一个打印语句,用于查看是否有非英语文本输入到控制台。如果您的业务逻辑需要在处理某些特定语言时执行预防性操作,您可以在此方法中添加更多逻辑。在这里,我们只是返回检测到的语言类型。

  1. 然后,在sentiment_classifier方法中,为每一行输入打分,我们可以在预测之前添加一行代码,首先检测语言,如下所示:

    language_detected = self.preprocessing_step_lang_detect(row)
    

然后,我们将language_detected变量传递给响应,如我们将在后处理逻辑实现中看到的那样。

这就是将语言检测作为推理管道中的预处理步骤实现的全部过程。

现在让我们看看如何实现另一个步骤:缓存,这需要同时进行预处理(检测是否存在任何预先匹配的预测结果)和后处理(将输入和预测结果的键值对存储到缓存中)。

实现缓存的预处理和后处理逻辑

让我们看看如何在InferencePipeline类中实现缓存:

  1. 我们可以添加一个新语句,在init方法中初始化缓存存储,因为它没有问题被序列化或反序列化:

    from cachetools import LRUCache
    self.cache = LRUCache(100)
    

这将初始化一个带有 100 个对象的最近最少使用(LRU)缓存。

  1. 接下来,我们将添加一个预处理方法来检测输入是否在缓存中:

    def preprocessing_step_cache(self, row):
        if row[0] in self.cache:
            print("found cached result")
            return self.cache[row[0]]
    

如果它在缓存中找到与输入行完全匹配的键,那么它将返回缓存的值。

  1. sentiment_classifier方法中,我们可以添加预处理步骤来检查缓存,如果找到缓存,它将立即返回缓存的响应,而无需调用昂贵的深度学习模型分类器:

        cached_response = self.preprocessing_step_cache(row)
        if cached_response is not None:
            return cached_response
    

这个预处理步骤应该作为 sentiment_classifier 方法中的第一步,在进行语言检测和模型预测之前放置。当输入中有大量重复项时,这可以显著加快实时预测响应的速度。

  1. 同样在 sentiment_classifier 方法中,我们需要添加一个后处理步骤,将新的输入和预测响应存储在缓存中:

    self.cache[row[0]]=response
    

就是这样。我们已经成功地将缓存添加为 InferencePipeline 类中的预处理和后处理步骤。

实现响应组成的后处理逻辑

现在让我们看看如何在原始深度学习模型预测被调用并返回结果后,作为后处理步骤实现响应组成逻辑。仅仅返回 positivenegative 的预测标签通常是不够的,因为我们希望知道使用的是哪个版本的模型,以及在生产环境中进行调试和诊断时检测到的语言。推理管道对调用者的响应将不再是简单的字符串,而是一个序列化的 JSON 字符串。按照以下步骤实现这个后处理逻辑:

  1. InferencePipeline 类的 init 方法中,我们需要添加一个新的 inference_pipeline_uri 参数,以便捕获该通用 MLflow pyfunc 模型的引用,进行溯源跟踪。finetuned_model_uriinference_pipeline_uri 两个参数将成为响应 JSON 对象的一部分。init 方法现在看起来如下:

    def __init__(self, 
                 finetuned_model_uri,
                 inference_pipeline_uri=None):
        self.cache = LRUCache(100)
        self.finetuned_model_uri = finetuned_model_uri
        self.inference_pipeline_uri = inference_pipeline_uri
    
  2. sentiment_classifier 方法中,添加一个新的后处理语句,以根据检测到的语言、预测标签以及包含 finetuned_model_uriinference_pipeline_uri 的模型元数据来组成新的响应:

    response = json.dumps({
                    'response': {
                        'prediction_label': pred_label
                    },
                    'metadata': {
                        'language_detected': language_detected,
                    },
                    'model_metadata': {
                        'finetuned_model_uri': self.finetuned_model_uri,
                        'inference_pipeline_model_uri': self.inference_pipeline_uri
                    },
                })                    
    

请注意,我们使用 json.dumps 将嵌套的 Python 字符串对象编码为 JSON 格式的字符串,以便调用者可以轻松地使用 JSON 工具解析响应。

  1. mlflow.pyfunc.log_model 语句中,我们需要在调用 InferencePipeline 类时添加一个新的 inference_pipeline_uri 参数:

    mlflow.pyfunc.log_model(
      artifact_path=MODEL_ARTIFACT_PATH,
      conda_env=CONDA_ENV,
      python_model=InferencePipeline(finetuned_model_uri,
      inference_pipeline_uri),
      signature=signature)
    

这将记录一个新的推理管道模型,包含我们实现的所有附加处理逻辑。这完成了图 7.3 中描述的多步骤推理管道的实现。

请注意,一旦模型记录了所有这些新步骤,要使用这个新的推理管道,即加载这个模型,不需要任何代码修改。我们可以像以前一样加载新记录的模型:

loaded_model = mlflow.pyfunc.load_model(inference_pipeline_uri)

如果你已经按照步骤进行到现在,你应该也逐步运行了 multistep_inference_model.py 的 VS Code notebook,直到本小节描述的第 3 步。现在我们可以尝试使用这个新的多步骤推理管道进行测试。我们可以准备一组新的输入数据,其中包含重复项和一个非英语文本字符串,如下所示:

input = {"text":["what a disappointing movie", "Great movie", "Great movie", "很好看的电影"]}
input_df = pd.DataFrame(input)

这个输入包含了两个重复项(Great movie)和一个中文文本字符串(输入列表中的最后一个元素,其中文本含义与Great Movie相同)。现在我们只需要加载模型并像之前一样调用results = loaded_model.predict(input_df)。在执行该预测语句时,你应该能在控制台输出中看到以下两条语句:

found cached result 
found Non-English language text.

这意味着我们的缓存和语言检测器工作正常!

我们也可以通过以下代码打印输出结果,以便再次检查我们的多步骤管道是否正常工作:

for i in range(results.size):
    print(results['text'][i])

这将打印出响应中每一行的完整内容。在这里,我们以最后一行(包含中文文本)作为示例进行展示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_07_07.jpg

图 7.7 – 使用多步骤推理管道处理中文文本字符串输入的 JSON 响应

图 7.7所示,prediction_label包含在响应中(其值为negative)。由于我们在 JSON 响应中的metadata部分使用了language_detected字段,我们看到了字符串"zh",表示中文。这是语言检测器在预处理步骤中生成的结果。此外,model_metadata部分包括了原始的finetuned_model_uriinference_pipeline_model_uri。这些是与 MLflow 追踪服务器相关的 URI,我们可以用它们来唯一地追踪和识别使用了哪个微调模型和推理管道来进行此预测结果。这对于生产环境中的溯源跟踪和诊断分析非常重要。将这个完整的 JSON 输出与图 7.6中的早期预测标签输出进行比较,可以看出它为推理管道的使用者提供了更丰富的上下文信息。

如果你在笔记本中看到类似图 7.7的 JSON 输出,给自己鼓掌,因为你刚刚完成了实现一个可以重用并部署到生产环境中的多步骤推理管道的重大里程碑,适用于现实的商业场景。

将推理管道作为新入口点实现到主 MLproject 中

现在我们已经成功地将多步骤推理管道作为新的自定义 MLflow 模型实现,我们可以更进一步,将其作为主MLproject中的一个新入口点,这样我们就可以运行整个管道的端到端流程(图 7.8)。请查看本章代码,访问 GitHub 以在本地环境中运行管道。

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_07_08.jpg

图 7.8 – 使用 MLproject 的端到端管道

我们可以将新的入口点inference_pipeline_model添加到MLproject文件中。你可以在 GitHub 仓库中查看这个文件(github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter07/MLproject):

inference_pipeline_model:
    parameters:
      finetuned_model_run_id: { type: str, default: None }
    command: "python pipeline/inference_pipeline_model.py --finetuned_model_run_id {finetuned_model_run_id}"

这个入口点或步骤可以独立调用,也可以作为整个管道的一部分,如图 7.8所示。提醒一下,执行 MLflow run命令之前,请确保按照本章README文件中所述,已设置 MLflow 跟踪服务器和后端存储 URI 的环境变量。此步骤会记录并注册一个新的inference_pipeline_model,该模型本身包含多步预处理和后处理逻辑。如果你知道finetuned_model_run_id,可以使用以下命令在chapter07文件夹的根目录下运行此步骤:

mlflow run . -e inference_pipeline_model  --experiment-name dl_model_chapter07 -P finetuned_model_run_id=07b900a96af04037a956c74ef691396e

这不仅会在 MLflow 跟踪服务器中记录一个新的inference_pipeline_model,还会在 MLflow 模型注册表中注册一个新的inference_pipeline_model版本。你可以通过以下链接在本地 MLflow 服务器中找到注册的inference_pipeline_model

http://localhost/#/models/inference_pipeline_model/

作为示例,以下截图展示了注册的inference_pipeline_model版本 6:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_07_09.jpg

图 7.9 – 注册的inference_pipeline_model,版本 6

你也可以按如下方式运行整个端到端管道,如图 7.8所示:

mlflow run . --experiment-name dl_model_chapter07

这将执行这个端到端管道中的所有步骤,并最终在模型注册表中记录并注册inference_pipeline_model

inference_pipeline_model.py的 Python 代码实现,在调用入口点inference_pipeline_model时执行,基本上是复制了我们在 VS Code 笔记本中为multistep_inference_model.py实现的InferencePipeline类,并进行了一些小的修改,具体如下:

  • 添加一个任务函数,作为此步骤的参数化入口点执行:

    def task(finetuned_model_run_id, pipeline_run_name):
    

这个函数的作用是启动一个新的 MLflow 运行,以记录和注册一个新的推理管道模型。

  • 通过如下方式启用在记录时的模型注册:

    mlflow.pyfunc.log_model(
        artifact_path=MODEL_ARTIFACT_PATH,
        conda_env=CONDA_ENV,          
        python_model=InferencePipeline(
            finetuned_model_uri, 
            inference_pipeline_uri),
        signature=signature,
        registered_model_name=MODEL_ARTIFACT_PATH)
    

请注意,我们将MODEL_ARTIFACT_PATH的值(即inference_pipeline_model)分配给registered_model_name。这使得模型可以在 MLflow 模型注册表中以这个名字注册,如图 7.9所示。

这个新入口点的完整代码可以在 GitHub 仓库中找到:github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter07/pipeline/inference_pipeline_model.py

请注意,我们还需要在 main.py 文件中添加一个新部分,以便 inference_pipeline_model 入口点也可以从 main 入口点内调用。实现方法非常简单,类似于之前在 第四章 追踪代码和数据版本控制 中描述的添加其他步骤。感兴趣的读者可以查看 GitHub 上的 main.py 文件,了解实现细节:github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter07/main.py

本章完成了在 MLproject 中添加一个新入口点的实现,以便我们可以使用 MLflow run 命令工具运行多步骤的推理管道创建、日志记录和注册。

总结

本章介绍了使用 MLflow 的自定义 Python 模型方法(即 mlflow.pyfunc.PythonModel)创建多步骤推理管道这一非常重要的主题。

我们讨论了生产环境中四种推理工作流模式,其中通常单一的训练模型不足以完成业务应用需求。很有可能在模型训练和开发阶段没有看到一些预处理和后处理逻辑。这就是为什么 MLflow 的 pyfunc 方法是一种优雅的方式,能够实现一个自定义的 MLflow 模型,该模型可以在训练好的深度学习模型之外,加入额外的预处理和后处理逻辑。

我们成功实现了一个推理管道模型,它将我们的深度学习情感分类器与谷歌的紧凑型语言检测器(Compact Language Detector)、缓存以及除预测标签外的其他模型元数据结合。我们更进一步,将推理管道模型的创建步骤融入到端到端的模型开发工作流中,以便通过一个 MLflow run 命令生成一个注册的推理管道模型。

本章中学习的技能和课程对任何希望使用 MLflow pyfunc 方法实现真实推理管道的人来说都至关重要。这也为支持灵活且强大的生产环境部署打开了大门,相关内容将在下一章中讨论。

进一步阅读

第八章:第八章:在大规模环境下部署深度学习推理管道

部署 深度学习DL)推理管道以供生产使用既令人兴奋又具有挑战性。令人兴奋的部分是,最终深度学习模型管道可以用来对真实生产数据进行预测,为商业场景提供真正的价值。然而,具有挑战性的一点是,有不同的深度学习模型服务平台和托管环境。选择适合的框架来应对合适的模型服务场景并不容易,这需要在最小化部署复杂性的同时提供可扩展、成本效益高的最佳模型服务体验。本章将介绍不同的部署场景和托管环境的概述,然后提供实际操作,学习如何使用 MLflow 部署工具部署到不同的环境,包括本地和远程云环境。到本章结束时,您应该能够自信地将 MLflow 深度学习推理管道部署到各种托管环境中,用于批处理或实时推理服务。

在本章中,我们将讨论以下主要内容:

  • 了解部署和托管环境的全貌

  • 本地部署用于批处理和 Web 服务推理

  • 使用 Ray Serve 和 MLflow 部署插件进行部署

  • 部署到 AWS SageMaker – 完整的端到端指南

技术要求

本章学习所需的项目:

了解不同的部署工具和托管环境

在 MLOps 技术栈中有不同的部署工具,针对不同的目标用例和主机环境来部署不同的模型推断管道。在第七章多步骤深度学习推断管道,我们学习了不同的推断场景和要求,并实现了一个可以部署到模型托管/服务环境中的多步骤 DL 推断管道。现在,我们将学习如何将这样的模型部署到几个特定的模型托管和服务环境中。这在图 8.1中如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_08_01.jpg

图 8.1 – 使用模型部署工具将模型推断管道部署到模型托管和服务环境

如图 8.1 所示,针对不同的模型托管和服务环境可能存在不同的部署工具。在这里,我们列出了三种典型的场景如下:

  • 规模化批量推断:如果我们想要定期进行批量推断,我们可以使用 PySpark 的用户定义函数UDF)加载一个 MLflow 模型格式来执行此操作,因为我们可以利用 Spark 在分布式集群上的可扩展计算方法(mlflow.org/docs/latest/models.html#export-a-python-function-model-as-an-apache-spark-udf)。我们将在下一节中展示如何做到这一点的示例。

  • 规模化流式推断:这通常需要一个托管模型即服务MaaS)的端点。存在许多用于生产级部署和模型服务的工具和框架。在我们开始学习如何在本章中进行这种类型的部署之前,我们将在本节比较几种工具,以了解它们的工作方式及其与 MLflow 集成的情况。

  • 设备上的模型推断:这是一个称为TinyML的新兴领域,它在资源有限的环境中部署 ML/DL 模型,例如移动设备、传感器或边缘设备(www.kdnuggets.com/2021/11/on-device-deep-learning-pytorch-mobile-tensorflow-lite.html)。两个流行的框架是 PyTorch Mobile(pytorch.org/mobile/home/)和 TensorFlow Lite(www.tensorflow.org/lite)。这不是本书的重点。建议您在本章结束时查看一些进一步阅读材料。

现在,让我们看看有哪些工具可用于将模型推断部署为服务,特别是那些支持 MLflow 模型部署的工具。有三种类型的模型部署和服务工具,如下所示:

  • MLflow 内置模型部署:这是 MLflow 发布时自带的功能,包括部署到本地 Web 服务器、AWS SageMaker 和 Azure ML。Databricks 上也有一个托管的 MLflow,支持模型服务,在本书写作时处于公开审阅阶段,我们不会在本书中涵盖该内容,因为它在官方 Databricks 文档中已经有很好的展示(感兴趣的读者可以在此网站查阅有关该 Databricks 功能的官方文档:docs.databricks.com/applications/mlflow/model-serving.html)。不过,在本章中,我们将展示如何使用 MLflow 内置的模型部署功能,将模型部署到本地和远程的 AWS SageMaker。

  • mlflow-torchservgithub.com/mlflow/mlflow-torchserve),mlflow-ray-servegithub.com/ray-project/mlflow-ray-serve),以及mlflow-triton-plugingithub.com/triton-inference-server/server/tree/v2.17.0/deploy/mlflow-triton-plugin)。在本章中,我们将展示如何使用mlflow-ray-serve插件进行部署。

  • 使用mlflow-ray-serve插件部署 MLflow Python 模型。需要注意的是,尽管在本书中我们展示了如何使用 MLflow 自定义插件通过 Ray Serve 等通用的机器学习服务工具进行部署,但重要的是要注意,无论是否使用 MLflow 自定义插件,通用机器学习服务工具都能做更多的事情。

    通过专门的推理引擎优化深度学习推理

    有一些特殊的 MLflow 模型格式,比如ONNXonnx.ai/)和TorchScripthuggingface.co/docs/transformers/v4.17.0/en/serialization#torchscript),它们专门设计用于深度学习模型推理运行时。我们可以将深度学习模型转换为 ONNX 模型格式(github.com/microsoft/onnxruntime)或 TorchScript 服务器(pytorch.org/serve/)。由于 ONNX 和 TorchScript 仍在发展中,并且它们是专门为原始深度学习模型部分设计的,而不是整个推理管道,因此我们在本章中不会涵盖它们。

现在我们已经很好地理解了各种部署工具和模型服务框架,接下来让我们通过具体示例学习如何进行部署。

本地部署用于批量推理和 Web 服务推理

在开发和测试过程中,我们通常需要将模型本地部署以验证其是否按预期工作。我们来看看如何在两种场景下进行部署:批量推理和 Web 服务推理。

批量推理

对于批量推理,请按照以下说明操作:

  1. 请确保您已完成第七章多步骤深度学习推理管道。这将生成一个 MLflow pyfunc深度学习推理模型管道 URI,可以通过标准的 MLflow Python 函数加载。已记录的模型可以通过run_id和模型名称唯一定位,如下所示:

    logged_model = 'runs:/37b5b4dd7bc04213a35db646520ec404/inference_pipeline_model'
    

模型还可以通过模型注册表中的模型名称和版本号进行识别,如下所示:

logged_model = 'models:/inference_pipeline_model/6'
  1. 按照README.md文件中使用 PySpark UDF 函数进行批量推理部分的说明(github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter08/README.md),设置本地虚拟环境、完整的 MLflow 跟踪服务器和一些环境变量,以便我们能够在本地环境中执行代码。

  2. 使用 MLflow 的mlflow.pyfunc.spark_udf API 加载模型,以创建一个 PySpark UDF 函数,如下所示。您可能需要查看 GitHub 上的batch_inference.py文件来跟进:github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter08/batch/batch_inference.py

    loaded_model = mlflow.pyfunc.spark_udf(spark, model_uri=logged_model, result_type=StringType())
    

这将把推理管道封装为一个返回结果类型为String的 PySpark UDF 函数。这是因为我们的模型推理管道具有一个模型签名,要求输出为string类型的列。

  1. 现在,我们可以将 PySpark UDF 函数应用于输入的 DataFrame。请注意,输入的 DataFrame 必须包含一个text列,并且该列的数据类型必须为string,因为这是模型签名的要求:

    df = df.withColumn('predictions', loaded_model())
    

因为我们的模型推理管道已经定义了模型签名,所以如果输入的 DataFrame 中包含text列(在本示例中是df),我们就不需要指定任何列参数。请注意,我们可以使用 Spark 的read API 读取大量数据,支持多种数据格式,如 CSV、JSON、Parquet 等。在我们的示例中,我们从 IMDB 数据集读取了test.csv文件。如果数据量较大,这将利用 Spark 强大的分布式计算在集群上执行。这使得我们可以轻松地进行大规模的批量推理。

  1. 要从头到尾运行批量推理代码,您应该查看存储库中提供的完整代码,地址为:github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter08/batch/batch_inference.py。确保在batch文件夹中运行以下命令之前,将logged_model变量替换为您自己的run_id和模型名称,或注册的模型名称和版本:

    python batch_inference.py
    
  2. 你应该能在屏幕上看到图 8.2中的输出:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_08_02.jpg

图 8.2 – 使用 PySpark UDF 函数进行批量推理

图 8.2中可以看到,我们加载的多步推理管道工作正常,甚至能够检测到非英文文本和重复内容,尽管语言检测器可能会产生一些误报。输出是一个两列的 DataFrame,其中模型预测的 JSON 响应保存在 predictions 列中。请注意,你可以在 Databricks notebook 中使用 batch_inference.py 中提供的相同代码,通过更改输入数据和已记录模型的位置,利用 Spark 集群处理大量的输入数据。

现在我们已经知道如何进行大规模的批量推理,让我们来看看如何将相同的模型推理管道部署到本地 web 服务中。

模型作为 web 服务

我们可以将相同的已记录模型推理管道部署到本地的 web 服务中,并拥有一个接受 HTTP 请求并返回 HTTP 响应的端点。

本地部署非常简单,只需要一条命令。我们可以使用模型 URI 来部署已记录的模型或注册的模型,就像之前的批量推理一样,具体如下:

mlflow models serve -m models:/inference_pipeline_model/6

你应该能够看到如下内容:

2022/03/06 21:50:19 INFO mlflow.models.cli: Selected backend for flavor 'python_function'
2022/03/06 21:50:21 INFO mlflow.utils.conda: === Creating conda environment mlflow-a0968092d20d039088e2875ad04bbaa0f3a75206 ===
± |main U:1 ?:8 X| done
Solving environment: done

这将使用已记录的模型创建 conda 环境,确保其拥有运行所需的所有依赖项。创建完 conda 环境后,你应该会看到如下内容:

2022/03/06 21:52:11 INFO mlflow.pyfunc.backend: === Running command 'source /Users/yongliu/opt/miniconda3/bin/../etc/profile.d/conda.sh && conda activate mlflow-a0968092d20d039088e2875ad04bbaa0f3a75206 1>&2 && gunicorn --timeout=60 -b 127.0.0.1:5000 -w 1 ${GUNICORN_CMD_ARGS} -- mlflow.pyfunc.scoring_server.wsgi:app'
[2022-03-06 21:52:12 -0800] [97554] [INFO] Starting gunicorn 20.1.0
[2022-03-06 21:52:12 -0800] [97554] [INFO] Listening at: http://127.0.0.1:5000 (97554)
[2022-03-06 21:52:12 -0800] [97554] [INFO] Using worker: sync
[2022-03-06 21:52:12 -0800] [97561] [INFO] Booting worker with pid: 97561

现在,模型已经作为 web 服务部署,并准备好接受 HTTP 请求进行模型预测。打开一个新的终端窗口,输入以下命令调用模型 web 服务来获取预测响应:

curl http://127.0.0.1:5000/invocations -H 'Content-Type: application/json' -d '{
    "columns": ["text"],
    "data": [["This is the best movie we saw."], ["What a movie!"]]
}'

我们可以立即看到如下预测响应:

[{"text": "{\"response\": {\"prediction_label\": [\"positive\"]}, \"metadata\": {\"language_detected\": \"en\"}, \"model_metadata\": {\"finetuned_model_uri\": \"runs:/07b900a96af04037a956c74ef691396e/model\", \"inference_pipeline_model_uri\": \"runs:/37b5b4dd7bc04213a35db646520ec404/inference_pipeline_model\"}}"}, {"text": "{\"response\": {\"prediction_label\": [\"positive\"]}, \"metadata\": {\"language_detected\": \"en\"}, \"model_metadata\": {\"finetuned_model_uri\": \"runs:/07b900a96af04037a956c74ef691396e/model\", \"inference_pipeline_model_uri\": \"runs:/37b5b4dd7bc04213a35db646520ec404/inference_pipeline_model\"}}"}]

如果你已经按照步骤操作并看到预测结果,你应该为自己感到非常骄傲,因为你刚刚将一个深度学习模型推理管道部署到了本地 web 服务中!这对于测试和调试非常有用,而且在生产环境的 web 服务器上模型的行为不会发生变化,所以我们应该确保它在本地 web 服务器上正常工作。

到目前为止,我们已经学习了如何使用内置的 MLflow 部署工具。接下来,我们将学习如何使用通用的部署工具 Ray Serve 来部署一个 MLflow 推理管道。

使用 Ray Serve 和 MLflow 部署插件进行部署

更通用的部署方式是使用像 Ray Serve 这样的框架(docs.ray.io/en/latest/serve/index.html)。Ray Serve 有几个优点,例如支持不同的深度学习模型框架、原生 Python 支持以及支持复杂的模型组合推理模式。Ray Serve 支持所有主要的深度学习框架和任何任意的业务逻辑。那么,我们能否同时利用 Ray Serve 和 MLflow 进行模型部署和服务呢?好消息是,我们可以使用 Ray Serve 提供的 MLflow 部署插件来实现这一点。接下来我们将介绍如何使用mlflow-ray-serve插件通过 Ray Serve 进行 MLflow 模型部署(github.com/ray-project/mlflow-ray-serve)。在开始之前,我们需要安装mlflow-ray-serve包:

pip install mlflow-ray-serve

接下来,我们需要首先使用以下两个命令在本地启动一个单节点 Ray 集群:

ray start --head
serve start

这将在本地启动一个 Ray 集群,并且你可以通过浏览器访问它的仪表板,网址是http://127.0.0.1:8265/#/,如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_08_03.jpg

图 8.3 – 本地运行的 Ray 集群

图 8.3展示了一个本地运行的 Ray 集群。然后,你可以执行以下命令将inference_pipeline_model部署到 Ray Serve:

mlflow deployments create -t ray-serve -m runs:/63f101fb3700472ca58975488636f4ae/inference_pipeline_model --name dl-inference-model-on-ray -C num_replicas=1

这将显示以下屏幕输出:

2022-03-20 20:16:46,564    INFO worker.py:842 -- Connecting to existing Ray cluster at address: 127.0.0.1:6379
2022-03-20 20:16:46,717    INFO api.py:242 -- Updating deployment 'dl-inference-model-on-ray'. component=serve deployment=dl-inference-model-on-ray
(ServeController pid=78159) 2022-03-20 20:16:46,784    INFO deployment_state.py:912 -- Adding 1 replicas to deployment 'dl-inference-model-on-ray'. component=serve deployment=dl-inference-model-on-ray
2022-03-20 20:17:10,309    INFO api.py:249 -- Deployment 'dl-inference-model-on-ray' is ready at `http://127.0.0.1:8000/dl-inference-model-on-ray`. component=serve deployment=dl-inference-model-on-ray
python_function deployment dl-inference-model-on-ray is created

这意味着位于http://127.0.0.1:8000/dl-inference-model-on-ray的端点已准备好为在线推理请求提供服务!你可以使用以下提供的 Python 代码在chapter08/ray_serve/query_ray_serve_endpoint.py中测试这个部署:

python ray_serve/query_ray_serve_endpoint.py

这将在屏幕上显示如下结果:

2022-03-20 21:16:45,125    INFO worker.py:842 -- Connecting to existing Ray cluster at address: 127.0.0.1:6379
[{'name': 'dl-inference-model-on-ray', 'info': Deployment(name=dl-inference-model-on-ray,version=None,route_prefix=/dl-inference-model-on-ray)}]
{
    "columns": [
        "text"
    ],
    "index": [
        0,
        1
    ],
    "data": [
        [
            "{\"response\": {\"prediction_label\": [\"negative\"]}, \"metadata\": {\"language_detected\": \"en\"}, \"model_metadata\": {\"finetuned_model_uri\": \"runs:/be2fb13fe647481eafa071b79dde81de/model\", \"inference_pipeline_model_uri\": \"runs:/63f101fb3700472ca58975488636f4ae/inference_pipeline_model\"}}"
        ],
        [
            "{\"response\": {\"prediction_label\": [\"positive\"]}, \"metadata\": {\"language_detected\": \"en\"}, \"model_metadata\": {\"finetuned_model_uri\": \"runs:/be2fb13fe647481eafa071b79dde81de/model\", \"inference_pipeline_model_uri\": \"runs:/63f101fb3700472ca58975488636f4ae/inference_pipeline_model\"}}"
        ]
    ]
}

你应该会看到预期的推理模型响应。如果你按照这些步骤操作至此,恭喜你成功通过mlflow-ray-serve MLflow 部署插件完成了部署!如果你不再需要这个 Ray Serve 实例,可以通过执行以下命令行来停止它:

ray stop

这将停止你本地机器上所有正在运行的 Ray 实例。

使用 MLflow 部署插件进行部署

有几个 MLflow 部署插件。我们刚刚展示了如何使用mlflow-ray-serve部署一个通用的 MLflow Python 模型inference_pipeline_model。这为将 Ray 集群部署到许多目标目的地打开了大门,你可以在任何云服务提供商上启动 Ray 集群。由于本书的范围限制,我们不会在这一章进一步探讨更多细节。如果你感兴趣,可以参考 Ray 的文档了解如何启动云集群(AWS、Azure 和Google Cloud PlatformGCP)):docs.ray.io/en/latest/cluster/cloud.html#:~:text=The%20Ray%20Cluster%20Launcher%20can,ready%20to%20launch%20your%20cluster。一旦 Ray 集群启动,你可以按照相同的流程部署 MLflow 模型。

现在我们知道了几种在本地部署的方法,并且如果需要的话可以进一步部署到云端,使用 Ray Serve,让我们看看如何在下一节中部署到云管理的推理服务 AWS SageMaker,因为它被广泛使用,并且可以提供在现实场景中如何部署的良好教训。

部署到 AWS SageMaker —— 完整的端到端指南

AWS SageMaker 是由 AWS 管理的云托管模型服务。我们将以 AWS SageMaker 为例,展示如何将模型部署到托管 Web 服务的远程云提供商,以服务真实的生产流量。AWS SageMaker 提供了一套包括支持注释和模型训练等在内的 ML/DL 相关服务。在这里,我们展示如何为部署 BYOM(Bring Your Own Model,自带模型)做准备和部署。这意味着您已在 AWS SageMaker 之外训练了模型推理管道,现在只需部署到 SageMaker 进行托管。按照以下步骤准备和部署 DL 情感模型。需要几个先决条件:

  • 您必须在本地环境中运行 Docker Desktop。

  • 您必须拥有一个 AWS 账户。您可以通过免费注册网站 aws.amazon.com/free/ 轻松创建一个免费的 AWS 账户。

一旦满足这些要求,激活 dl-model-chapter08 的 conda 虚拟环境,按照以下几个步骤部署到 SageMaker。我们将这些步骤细分为六个子章节如下:

  1. 构建本地 SageMaker Docker 镜像

  2. 将额外的模型构件层添加到 SageMaker Docker 镜像上

  3. 使用新构建的 SageMaker Docker 镜像进行本地部署测试

  4. 将 SageMaker Docker 镜像推送到 AWS Elastic Container Registry

  5. 部署推理管道模型以创建 SageMaker 终端节点

  6. 查询 SageMaker 终端节点进行在线推理

让我们从第一步开始构建本地 SageMaker Docker 镜像。

第 1 步:构建本地 SageMaker Docker 镜像

我们故意从本地构建开始,而不是推送到 AWS,这样我们可以学习如何在此基础镜像上添加额外的层,并在本地验证一切,避免产生任何云端费用:

mlflow sagemaker build-and-push-container --build --no-push -c mlflow-dl-inference

您将看到大量的屏幕输出,最后会显示类似以下内容:

#15 exporting to image
#15 sha256:e8c613e07b0b7ff33893b694f7759a10 d42e180f2b4dc349fb57dc6b71dcab00
#15 exporting layers
#15 exporting layers 8.7s done
#15 writing image sha256:95bc539b021179e5e87087 012353ebb43c71410be535ef368d1121b550c57bd4 done
#15 naming to docker.io/library/mlflow-dl-inference done
#15 DONE 8.7s

如果您看到镜像名称 mlflow-dl-inference,那么说明您已成功创建了一个符合 SageMaker 标准的 MLflow 模型服务 Docker 镜像。您可以通过运行以下命令来验证:

docker images | grep mlflow-dl-inference

您应该看到类似以下的输出:

mlflow-dl-inference          latest                  95bc539b0211   6 minutes ago   2GB

第 2 步:将额外的模型构件层添加到 SageMaker Docker 镜像上

回想一下,我们的推理管道模型是基于一个微调的深度学习模型构建的,我们通过 MLflow PythonModel API 的 load_context 函数 (www.mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.PythonModel) 加载该模型,而不需要序列化微调后的模型本身。部分原因是因为 MLflow 无法通过 pickle 正确序列化 PyTorch DataLoader (pytorch.org/docs/stable/data.html#single-and-multi-process-data-loading),因为 DataLoader 从目前的文档来看并不实现 pickle 序列化。这为我们提供了一个机会,去学习当一些依赖项无法正确序列化时,我们该如何进行部署,尤其是在处理现实世界的深度学习模型时。

让 Docker 容器访问 MLflow 跟踪服务器的两种方法

有两种方法可以让 Docker 容器(如 mlflow-dl-inference)在运行时访问并加载微调模型。第一种方法是让容器包含 MLflow 跟踪服务器的 URL 和访问令牌。这在企业环境中可能会引发一些安全隐患,因为此时 Docker 镜像中包含了一些安全凭证。第二种方法是直接将所有引用的工件复制到新建的 Docker 镜像中,从而创建一个自给自足的镜像。在运行时,它不需要知道原始 MLflow 跟踪服务器的位置,因为它已将所有模型工件保存在本地。这种自包含的方法消除了任何安全泄露的担忧。在本章中,我们采用了第二种方法进行部署。

本章中,我们将把引用的微调模型复制到一个新的 Docker 镜像中,该镜像基于基础的 mlflow-dl-inference Docker 镜像构建。这将创建一个新的自包含 Docker 镜像,无需依赖任何外部的 MLflow 跟踪服务器。为此,你需要将微调的深度学习模型从模型跟踪服务器下载到当前本地文件夹,或者你可以通过使用本地文件系统作为 MLflow 跟踪服务器后端,直接在本地运行我们的 MLproject 流水线。按照 README.md 文件中的 部署到 AWS SageMaker 部分,重现本地的 MLflow 运行,准备微调模型和本地文件夹中的 inference-pipeline-model。为了学习目的,我们在 GitHub 仓库的 chapter08 文件夹中提供了两个示例 mlruns 工件和 huggingface 缓存文件夹 (github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/tree/main/chapter08),以便我们可以立即使用这些现有的工件开始构建新的 Docker 镜像。

要构建一个新的 Docker 镜像,我们需要创建一个如下所示的 Dockerfile:

FROM mlflow-dl-inference
ADD mlruns/1/meta.yaml  /opt/mlflow/mlruns/1/meta.yaml
ADD mlruns/1/d01fc81e11e842f5b9556ae04136c0d3/ /opt/mlflow/mlruns/1/d01fc81e11e842f5b9556ae04136c0d3/
ADD tmp/opt/mlflow/hf/cache/dl_model_chapter08/csv/ /opt/mlflow/tmp/opt/mlflow/hf/cache/dl_model_chapter08/csv/

第一行表示它从现有的mlflow-dl-inference Docker 镜像开始,接下来的三行ADD将会复制一个meta.yaml文件和两个文件夹到 Docker 镜像中的相应位置。请注意,如果您已经按照README文件中的步骤生成了自己的运行实例,则无需添加第三行。需要注意的是,默认情况下,当 Docker 容器启动时,它会自动进入/opt/mlflow/工作目录,因此所有内容都需要复制到这个文件夹中以便于访问。此外,请注意/opt/mlflow目录需要超级用户权限,因此您需要准备好输入本地机器的管理员密码(通常,在您的个人笔记本电脑上,密码就是您自己的密码)。

将私有构建的 Python 包复制到 Docker 镜像中

还可以将私有构建的 Python 包复制到 Docker 镜像中,这样我们就可以在conda.yaml文件中直接引用它们,而无需走出容器。举例来说,我们可以将一个私有的 Python wheel 包cool-dl-package-1.0.py3-none-any.whl复制到/usr/private-wheels/cool-dl-package/cool-dl-package-1.0-py3-none-any.whl Docker 文件夹中,然后我们可以在conda.yaml文件中指向这个路径。这使得 MLflow 模型工件能够成功加载这些本地可访问的 Python 包。在当前的示例中,我们没有使用这种方法,因为我们没有使用任何私有构建的 Python 包。如果您有兴趣探索这个方法,未来参考时会有用。

现在,您可以运行以下命令,在chapter08文件夹中构建一个新的 Docker 镜像:

docker build . -t mlflow-dl-inference-w-finetuned-model

这将基于mlflow-dl-inference构建一个新的 Docker 镜像mlflow-dl-inference-w-finetuned-model。您应该能看到以下输出(为了简洁起见,仅展示第一行和最后几行):

[+] Building 0.2s (9/9) FINISHED
 => [internal] load build definition from Dockerfile                                                      0.0s
…………
=> => naming to docker.io/library/mlflow-dl-inference-w-finetuned-model

现在,您已经有了一个名为mlflow-dl-inference-w-finetuned-model的新 Docker 镜像,其中包含微调后的模型。现在,我们准备使用这个新的、兼容 SageMaker 的 Docker 镜像来部署我们的推理管道模型。

步骤 3:使用新构建的 SageMaker Docker 镜像测试本地部署

在我们将部署到云端之前,让我们先使用这个新的 SageMaker Docker 镜像在本地进行部署测试。MLflow 提供了一种方便的方式来使用以下命令在本地进行测试:

mlflow sagemaker run-local -m runs:/dc5f670efa1a4eac95683633ffcfdd79/inference_pipeline_model -p 5555 -i mlflow-dl-inference-w-finetuned-model

这个命令将会在本地启动mlflow-dl-inference-w-finetuned-model Docker 容器,并将运行 ID 为dc5f670efa1a4eac95683633ffcfdd79的推理管道模型部署到该容器中。

修复潜在的 Docker 错误

请注意,您可能会遇到一个 Docker 错误,提示路径/opt/mlflow/mlruns/1/dc5f670efa1a4eac95683633ffcfdd79/artifacts/inference_pipeline_model 未从主机共享,Docker 无法识别该路径。您可以通过配置共享路径,进入Docker | Preferences… | Resources | File Sharing 来解决此 Docker 错误。

我们已经在 GitHub 仓库中提供了这个推理管道模型,所以当你在本地环境中检出仓库时,应该可以开箱即用。Web 服务的端口是5555。命令运行后,你会看到很多屏幕输出,最终你应该能看到以下内容:

[2022-03-18 01:47:20 +0000] [552] [INFO] Starting gunicorn 20.1.0
[2022-03-18 01:47:20 +0000] [552] [INFO] Listening at: http://127.0.0.1:8000 (552)
[2022-03-18 01:47:20 +0000] [552] [INFO] Using worker: gevent
[2022-03-18 01:47:20 +0000] [565] [INFO] Booting worker with pid: 565
[2022-03-18 01:47:20 +0000] [566] [INFO] Booting worker with pid: 566
[2022-03-18 01:47:20 +0000] [567] [INFO] Booting worker with pid: 567
[2022-03-18 01:47:20 +0000] [568] [INFO] Booting worker with pid: 568
[2022-03-18 01:47:20 +0000] [569] [INFO] Booting worker with pid: 569
[2022-03-18 01:47:20 +0000] [570] [INFO] Booting worker with pid: 570

这意味着服务已经启动并正在运行。你可能会看到一些关于 PyTorch 版本不兼容的警告,但这些可以安全地忽略。一旦服务启动并运行,你就可以在另一个终端窗口中通过发出curl网页请求来进行测试,像我们之前尝试过的那样:

curl http://127.0.0.1:5555/invocations -H 'Content-Type: application/json' -d '{
    "columns": ["text"],
    "data": [["This is the best movie we saw."], ["What a movie!"]]
}'

请注意,本地主机的端口号是5555。然后,你应该能看到如下响应:

[{"text": "{\"response\": {\"prediction_label\": [\"positive\"]}, \"metadata\": {\"language_detected\": \"en\"}, \"model_metadata\": {\"finetuned_model_uri\": \"runs:/d01fc81e11e842f5b9556ae04136c0d3/model\", \"inference_pipeline_model_uri\": \"runs:/dc5f670efa1a4eac95683633ffcfdd79/inference_pipeline_model\"}}"}, {"text": "{\"response\": {\"prediction_label\": [\"negative\"]}, \"metadata\": {\"language_detected\": \"en\"}, \"model_metadata\": {\"finetuned_model_uri\": \"runs:/d01fc81e11e842f5b9556ae04136c0d3/model\", \"inference_pipeline_model_uri\": \"runs:/dc5f670efa1a4eac95683633ffcfdd79/inference_pipeline_model\"}}"}]

你可能会想知道这和之前章节的本地推理模型 Web 服务有什么不同。区别在于,这一次我们使用的是 SageMaker 容器,而之前只是没有 Docker 容器的本地 Web 服务。在本地测试 SageMaker 容器非常重要,以避免浪费时间和金钱将失败的模型服务部署到云端。

接下来,我们准备将这个容器部署到 AWS SageMaker。

第 4 步:将 SageMaker Docker 镜像推送到 AWS Elastic Container Registry

现在,你可以将新构建的mlflow-dl-inference-w-finetuned-model Docker 镜像推送到 AWS Elastic Container Registry (ECR),使用以下命令。确保你的 AWS 访问令牌和访问 ID 已正确设置(使用真实的,而不是本地开发的)。一旦你拥有访问密钥 ID 和令牌,运行以下命令以设置对真实 AWS 的访问:

aws configure

执行命令后回答所有问题,你就准备好继续了。现在,你可以运行以下命令将mlflow-dl-inference-w-finetuned-model Docker 镜像推送到 AWS ECR:

mlflow sagemaker build-and-push-container --no-build --push -c mlflow-dl-inference-w-finetuned-model

确保在命令中不包含--no-build选项来构建新的镜像,因为我们只需要推送镜像,而不是构建新的镜像。你将看到以下输出,显示镜像正在被推送到 ECR。请注意,在以下输出中,AWS 账户被xxxxx掩盖。你将看到你的账户编号出现在输出中。确保你拥有写入 AWS ECR 存储的权限:

2022/03/18 17:36:05 INFO mlflow.sagemaker: Pushing image to ECR
2022/03/18 17:36:06 INFO mlflow.sagemaker: Pushing docker image mlflow-dl-inference-w-finetuned-model to xxxxx.dkr.ecr.us-west-2.amazonaws.com/mlflow-dl-inference-w-finetuned-model:1.23.1
Created new ECR repository: mlflow-dl-inference-w-finetuned-model
2022/03/18 17:36:06 INFO mlflow.sagemaker: Executing: aws ecr get-login-password | docker login  --username AWS  --password-stdin xxxxx.dkr.ecr.us-west-2.amazonaws.com;
docker tag mlflow-dl-inference-w-finetuned-model xxxxx.dkr.ecr.us-west-2.amazonaws.com/mlflow-dl-inference-w-finetuned-model:1.23.1;
docker push xxxxx.dkr.ecr.us-west-2.amazonaws.com/mlflow-dl-inference-w-finetuned-model:1.23.1
Login Succeeded
The push refers to repository [xxxxx.dkr.ecr.us-west-2.amazonaws.com/mlflow-dl-inference-w-finetuned-model]
447db5970ca5: Pushed
9d6787a516e7: Pushed
1.23.1: digest: sha256:f49f85741bc2b82388e85c79f6621f4 d7834e19bdf178b70c1a6c78c572b4d10 size: 3271

完成后,如果你访问 AWS 网站(例如,如果你使用的是us-west-2区域的数据中心,网址是us-west-2.console.aws.amazon.com/ecr/repositories?region=us-west-2),你应该能在 ECR 中找到你新推送的镜像,并且会看到一个名为mlflow-dl-inference-w-finetuned-model的文件夹。然后,你会在该文件夹中找到以下镜像(图 8.4):

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_08_04.jpg

图 8.4 – 带有 mlflow-dl-inference-w-finetuned-model 镜像标签 1.23.1 的 AWS ECR 存储库

请注意镜像标签号Copy URI选项。它将如下所示(AWS 账户号被屏蔽为xxxxx):

xxxxx.dkr.ecr.us-west-2.amazonaws.com/mlflow-dl-inference-w-finetuned-model:1.23.1

您将在下一步中需要此镜像 URI 来部署到 SageMaker。现在让我们将模型部署到 SageMaker,创建一个推理端点。

第 5 步:将推理管道模型部署到 SageMaker 以创建 SageMaker 端点

现在,是时候使用我们刚刚推送到 AWS ECR 注册表的镜像 URI 将推理管道模型部署到 SageMaker 了。我们在 GitHub 仓库的chapter08文件夹中包含了sagemaker/deploy_to_sagemaker.py代码。您需要使用正确的 AWS 角色进行部署。您可以在您的账户中创建一个新的AWSSageMakerExecutionRole角色,并将两个权限策略AmazonS3FullAccessAmazonSageMakerFullAccess分配给该角色。在实际场景中,您可能希望将权限收紧到更受限的策略,但为了学习目的,这种设置是可以的。下图显示了创建角色后的屏幕:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_08_05.jpg

图 8.5 – 创建一个可以在 SageMaker 中用于部署的角色

您还需要为 SageMaker 创建一个 S3 存储桶,以便 SageMaker 可以上传模型工件并将其部署到 SageMaker。在我们的示例中,我们创建了一个名为dl-inference-deployment的存储桶。当我们执行部署脚本时,如下所示,待部署的模型首先会上传到dl-inference-deployment存储桶中,然后再部署到 SageMaker。我们已经在chapter08/sagemaker/deploy_to_sagemaker.py GitHub 仓库中提供了完整的部署脚本,您可以下载并按如下方式执行(提醒一下,在运行此脚本之前,请确保将环境变量MLFLOW_TRACKING_URI重置为空,如export MLFLOW_TRACKING_URI=):

sudo python sagemaker/deploy_to_sagemaker.py

此脚本执行以下两个任务:

  1. 将本地mlruns文件夹下的内容复制到本地的/opt/mlflow文件夹中,以便 SageMaker 部署代码能够识别inference-pipeline-model并进行上传。由于/opt路径通常是受限的,因此我们使用sudo(超级用户权限)来执行此复制操作。此操作将提示您在笔记本电脑上输入用户密码。

  2. 使用mlflow.sagemaker.deployAPI 来创建一个新的 SageMaker 端点,dl-sentiment-model

代码片段如下:

mlflow.sagemaker.deploy(
    mode='create',
    app_name=endpoint_name,
    model_uri=model_uri,
    image_url=image_uri,
    execution_role_arn=role,
    instance_type='ml.m5.xlarge',
    bucket = bucket_for_sagemaker_deployment,
    instance_count=1,
    region_name=region
)

这些参数需要一些解释,以便我们能完全理解所需的所有准备工作:

  • model_uri:这是推理管道模型的 URI。在我们的示例中,它是runs:/dc5f670efa1a4eac95683633ffcfdd79/inference_pipeline_model

  • image_url:这是我们上传到 AWS ECR 的 Docker 镜像。在我们的示例中,它是xxxxx.dkr.ecr.us-west-2.amazonaws.com/mlflow-dl-inference-w-finetuned-model:1.23.1。请注意,您需要将被屏蔽的 AWS 账户号xxxxx替换为您的实际账户号。

  • execution_role_arn:这是我们创建的角色,允许 SageMaker 进行部署。在我们的示例中,它是arn:aws:iam::565251169546:role/AWSSageMakerExecutionRole。再次提醒,你需要将xxxxx替换为你的实际 AWS 账户号码。

  • bucket:这是我们创建的 S3 桶,允许 SageMaker 上传模型并进行实际部署。在我们的示例中,它是dl-inference-deployment

其余的参数不言自明。

执行部署脚本后,你将看到如下输出(其中xxxxx是隐藏的 AWS 账户号码):

2022/03/18 19:30:47 INFO mlflow.sagemaker: Using the python_function flavor for deployment!
2022/03/18 19:30:47 INFO mlflow.sagemaker: tag response: {'ResponseMetadata': {'RequestId': 'QMAQRCTJT36TXD2H', 'HostId': 'DNG57U3DJrhLcsBxa39zsjulUH9VB56FmGkxAiMYN+2fhc/rRukWe8P3qmBmvRYbMj0sW3B2iGg=', 'HTTPStatusCode': 200, 'HTTPHeaders': {'x-amz-id-2': 'DNG57U3DJrhLcsBxa39zsjulUH9VB56FmGkxAiMYN+2fhc/rRukWe8P3qmBmvRYbMj0sW3B2iGg=', 'x-amz-request-id': 'QMAQRCTJT36TXD2H', 'date': 'Sat, 19 Mar 2022 02:30:48 GMT', 'server': 'AmazonS3', 'content-length': '0'}, 'RetryAttempts': 0}}
2022/03/18 19:30:47 INFO mlflow.sagemaker: Creating new endpoint with name: dl-sentiment-model ...
2022/03/18 19:30:47 INFO mlflow.sagemaker: Created model with arn: arn:aws:sagemaker:us-west-2:xxxxx:model/dl-sentiment-model-model-qbca2radrxitkujn3ezubq
2022/03/18 19:30:47 INFO mlflow.sagemaker: Created endpoint configuration with arn: arn:aws:sagemaker:us-west-2:xxxxx:endpoint-config/dl-sentiment-model-config-r9ax3wlhrfisxkacyycj8a
2022/03/18 19:30:48 INFO mlflow.sagemaker: Created endpoint with arn: arn:aws:sagemaker:us-west-2:xxxxx:endpoint/dl-sentiment-model
2022/03/18 19:30:48 INFO mlflow.sagemaker: Waiting for the deployment operation to complete...
2022/03/18 19:30:48 INFO mlflow.sagemaker: Waiting for endpoint to reach the "InService" state. Current endpoint status: "Creating"

这可能需要几分钟时间(有时超过 10 分钟)。你可能会看到一些关于 PyTorch 版本兼容性的警告信息,就像在进行本地 SageMaker 部署测试时看到的那样。你也可以直接访问 SageMaker 网站,在那里你将看到端点的状态从Creating开始,最终变成绿色的InService状态,如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_08_06.jpg

图 8.6 – AWS SageMaker dl-sentiment-model 端点 InService 状态

如果你看到InService状态,那么恭喜你!你已成功将一个 DL 推理管道模型部署到 SageMaker,现在你可以将它用于生产流量!

现在服务状态为 InService,你可以在下一步使用命令行进行查询。

步骤 6:查询 SageMaker 端点进行在线推理

要查询 SageMaker 端点,你可以使用以下命令行:

aws sagemaker-runtime invoke-endpoint --endpoint-name 'dl-sentiment-model' --content-type 'application/json; format=pandas-split' --body '{"columns":["text"], "data": [["This is the best movie we saw."], ["What a movie!"]]}' response.json

然后你将看到如下输出:

{
    "ContentType": "application/json",
    "InvokedProductionVariant": "dl-sentiment-model-model-qbca2radrxitkujn3ezubq"
}

实际的预测结果存储在本地的response.json文件中,你可以通过运行以下命令查看响应内容:

cat response.json

这将显示如下内容:

[{"text": "{\"response\": {\"prediction_label\": [\"positive\"]}, \"metadata\": {\"language_detected\": \"en\"}, \"model_metadata\": {\"finetuned_model_uri\": \"runs:/d01fc81e11e842f5b9556ae04136c0d3/model\", \"inference_pipeline_model_uri\": \"runs:/dc5f670efa1a4eac95683633ffcfdd79/inference_pipeline_model\"}}"}, {"text": "{\"response\": {\"prediction_label\": [\"negative\"]}, \"metadata\": {\"language_detected\": \"en\"}, \"model_metadata\": {\"finetuned_model_uri\": \"runs:/d01fc81e11e842f5b9556ae04136c0d3/model\", \"inference_pipeline_model_uri\": \"runs:/dc5f670efa1a4eac95683633ffcfdd79/inference_pipeline_model\"}}"}]

这是我们推理管道模型的预期响应模式!你也可以通过 Python 代码查询 SageMaker 推理端点,我们已在 GitHub 仓库中的chapter08/sagemaker/query_sagemaker_endpoint.py文件中提供了该代码。核心代码片段使用了SageMakerRuntime客户端的invoke_endpoint进行查询,如下所示:

client = boto3.client('sagemaker-runtime') 
response = client.invoke_endpoint(
    EndpointName=app_name, 
    ContentType=content_type,
    Accept=accept,
    Body=payload
    )

invoke_endpoint的参数需要一些解释:

  • EndpointName:这是推理端点的名称。在我们的示例中,它是dl-inference-model

  • ContentType:这是请求体中输入数据的 MIME 类型。在我们的示例中,我们使用application/json; format=pandas-split

  • Accept:这是推理响应体中期望的 MIME 类型。在我们的示例中,我们期望返回text/plain字符串类型。

  • Body:这是我们希望使用 DL 模型推理服务预测情感的实际文本。在我们的示例中,它是{"columns": ["text"],"data": [["This is the best movie we saw."], ["What a movie!"]]}

完整的代码已提供在 GitHub 仓库中,你可以在命令行中按照以下方式运行它:

python sagemaker/query_sagemaker_endpoint.py

你将在终端屏幕上看到如下输出:

Application status is: InService
[{"text": "{\"response\": {\"prediction_label\": [\"positive\"]}, \"metadata\": {\"language_detected\": \"en\"}, \"model_metadata\": {\"finetuned_model_uri\": \"runs:/d01fc81e11e842f5b9556ae04136c0d3/model\", \"inference_pipeline_model_uri\": \"runs:/dc5f670efa1a4eac95683633ffcfdd79/inference_pipeline_model\"}}"}, {"text": "{\"response\": {\"prediction_label\": [\"negative\"]}, \"metadata\": {\"language_detected\": \"en\"}, \"model_metadata\": {\"finetuned_model_uri\": \"runs:/d01fc81e11e842f5b9556ae04136c0d3/model\", \"inference_pipeline_model_uri\": \"runs:/dc5f670efa1a4eac95683633ffcfdd79/inference_pipeline_model\"}}"}]

这就是我们期望从推理管道模型的响应中得到的结果!如果你已经按照本章内容完成学习,恭喜你成功将推理管道模型部署到 AWS SageMaker 上的远程云主机中!完成本章内容后,请务必删除端点,以避免不必要的费用。

让我们总结一下我们在本章中学到的内容。

总结

在本章中,我们学习了不同的方法来部署一个 MLflow 推理管道模型,适用于批量推理和在线实时推理。我们从对不同模型服务场景(批处理、流式、设备端)的简要调查开始,并研究了三种不同类别的 MLflow 模型部署工具(MLflow 内置部署工具、MLflow 部署插件,以及可以与 MLflow 推理模型配合使用的通用模型推理服务框架)。然后,我们介绍了几种本地部署场景,使用 PySpark UDF 函数进行批量推理,并使用 MLflow 本地部署进行 Web 服务部署。接着,我们学习了如何结合使用 Ray Serve 和mlflow-ray-serve插件,将 MLflow Python 推理管道模型部署到本地 Ray 集群。这为我们打开了部署到任何云平台的大门,比如 AWS、Azure ML 或 GCP,只要我们能在云中设置 Ray 集群。最后,我们提供了一个完整的端到端指南,讲解如何部署到 AWS SageMaker,重点介绍了 BYOM(Bring Your Own Model)常见场景,在这个场景中,我们有一个在 AWS SageMaker 外部训练的推理管道模型,现在需要将其部署到 AWS SageMaker 以提供托管服务。我们的逐步指南应该能帮助你信心满满地将 MLflow 推理管道模型部署到真实生产环境中。

请注意,部署深度学习推理管道模型的领域仍在发展,我们刚刚学到了一些基础技能。我们鼓励你在进一步阅读部分探索更多高级主题。

现在我们已经知道如何部署和托管深度学习推理管道,接下来我们将在下一章学习如何进行模型可解释性,这对于许多现实场景中可信赖和可解释的模型预测结果至关重要。

进一步阅读

第五部分 – 大规模深度学习模型可解释性

在本节中,我们将学习可解释性的基础概念以及可解释人工智能XAI),并了解如何使用 MLflow 实现深度学习DL)可解释性。我们将从概述可解释性的八个维度开始,然后学习如何使用SHapley 加法解释SHAP)和Transformer 解释来执行自然语言处理NLP)管道的可解释性。此外,我们还将学习并分析当前 MLflow 与 SHAP 的集成,以理解其权衡并避免潜在的实现问题。接下来,我们将展示如何使用 MLflow 的日志 API 实现 SHAP。最后,我们将学习如何将 SHAP 解释器作为 MLflow Python 模型进行实现,并将其加载为 Spark UDF 进行批量解释,或作为在线解释即服务EaaS)的 Web 服务。

本节包括以下章节:

  • 第九章深度学习可解释性的基础

  • 第十章使用 MLflow 实现深度学习可解释性

第九章:第九章:深度学习可解释性基础

可解释性是为自动化系统提供选择性人类可理解的决策解释。在本书的背景下,在深度学习DL)开发的整个生命周期中,应将可解释性视为与数据、代码和模型这三大支柱一样重要的产物。这是因为不同的利益相关者和监管者、模型开发者以及模型输出的最终消费者可能对了解数据如何使用以及模型为何产生特定预测或分类有不同的需求。如果没有这样的理解,将难以赢得模型输出消费者的信任,或者在模型输出结果漂移时诊断可能出现的问题。这也意味着可解释性工具不仅应用于解释在生产中部署的模型的预测结果或离线实验期间的情况,还应用于理解离线模型训练中使用的数据特性和在线模型操作中遇到的数据集之间的差异。

此外,在许多高度受监管的行业,如自动驾驶、医疗诊断、银行业和金融业,还有法律法规要求任何个体都有权利获取算法输出的解释的要求。最后,最近的一项调查显示,超过 82%的 CEO 认为基于 AI 的决策必须是可解释的,以便作为企业加速其投资于开发和部署基于 AI 的倡议的信任基础(cloud.google.com/blog/topics/developers-practitioners/bigquery-explainable-ai-now-ga-help-you-interpret-your-machine-learning-models)。因此,学习可解释性的基础知识和相关工具是很重要的,这样我们就知道在何时为何种观众使用何种工具来提供相关、准确和一致的解释。

通过本章结束时,您将能够自信地知道什么是良好的解释,以及存在哪些工具用于不同的可解释性目的,并且将获得使用两个解释性工具箱来解释深度学习情感分类模型的实际经验。

在本章中,我们将涵盖以下主要话题:

  • 理解解释性的类别和受众

  • 探索 SHAP 可解释性工具包

  • 探索 Transformers Interpret 工具箱

技术要求

完成本章学习需要满足以下要求:

理解可解释性的类别和受众

正如本章开头所述,深度学习系统的可解释性变得越来越重要,有时在一些高度监管的行业中,如金融、法律、政府和医疗领域,甚至是强制性的。一个因缺乏机器学习可解释性而导致的示例诉讼是B2C2 诉 Quoine 案(www.scl.org/articles/12130-explainable-machine-learning-how-can-you-determine-what-a-party-knew-or-intended-when-a-decision-was-made-by-machine-learning),其中自动化的 AI 交易算法错误地以市场价格的 250 倍下单购买比特币。深度学习模型在生产中的成功应用促进了对可解释性领域的积极研究和开发,因为我们需要理解深度学习模型是如何运作的。你可能听说过可解释人工智能XAI)这个术语,它由美国国防高级研究计划局DARPA)于 2015 年为其 XAI 计划提出,旨在帮助最终用户更好地理解、信任和有效管理 AI 系统(onlinelibrary.wiley.com/doi/epdf/10.1002/ail2.61)。然而,可解释性的概念早在 1980 年代或更早期的专家系统时代就已出现(wires.onlinelibrary.wiley.com/doi/full/10.1002/widm.1391),而近期对于可解释性话题的关注高潮,只是突显了它的重要性。

那么,什么是解释?事实证明,这仍然是机器学习(ML)、深度学习(DL)和人工智能(AI)领域的一个活跃研究课题。从实际应用的角度来看,解释的精确定义取决于谁在什么时间、出于什么目的,基于机器学习/深度学习/人工智能的生命周期需求进行解释(dl.acm.org/doi/abs/10.1145/3461778.3462131)。因此,可解释性可以被定义为为受众提供适当的、易于理解的解释,阐明模型为什么以及如何给出某些预测结果的能力。这也可能包括数据可解释性的方面,涉及数据是如何通过溯源追踪被使用的,数据的特征是什么,或是数据是否由于意外事件而发生了变化。例如,由于突如其来的新冠疫情,销售和营销邮件发生了变化(www.validity.com/resource-center/disruption-in-email/)。这种数据的变化将意外改变模型预测结果的分布。在解释模型漂移时,我们需要考虑到这种数据的变化。这意味着,解释的复杂性需要根据接收受众进行定制和选择,而不会提供过多的信息。例如,包含许多技术术语的复杂解释,如激活,可能不如使用商业友好的术语进行简单文本总结的效果好。这进一步表明,可解释性也是一个人机界面/交互HCI)话题。

要全面了解可解释性类别及其对应的受众,我们考虑了图 9.1中展示的八个维度:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_09_001.jpg

图 9.1 – 理解可解释性的八个维度

图 9.1可以看出,可解释性的复杂性可以从八个维度进行理解。这不一定是一个详尽的分类,而是帮助理解来自 HCI、人工智能/机器学习/深度学习完整生命周期以及不同技术方法的不同视角的指南。在接下来的讨论中,我们将重点介绍与深度学习应用最相关的维度及其相互关系,因为本章的重点是深度学习的可解释性。

受众:谁需要知道

正如最近一项研究指出的(dl.acm.org/doi/abs/10.1145/3461778.3462131),了解谁在什么阶段需要知道什么类型的解释是非常重要的,这将在整个 AI 项目生命周期中产生影响。这也将影响解释的输出格式。另一项早期研究(arxiv.org/pdf/1702.08608.pdf)也指出,根据是否有领域专家参与实际应用任务(例如,诊断癌症的医学专家),验证一个解释的成本可能也很高,因为这需要一个实际的人类在实际工作环境中参与。

对于当前的实际深度学习项目,我们需要根据目标受众(如数据科学家、机器学习工程师、业务利益相关者、**用户体验(UX)**设计师或最终用户)定制解释方法和展示方式,因为没有一种通用的方法适用于所有情况。

阶段:在深度学习生命周期中何时提供解释

阶段通常指的是在模型开发生命周期中可以提供解释的时机。对于像决策树这样的模型,由于它是一个白盒模型,我们说我们可以提供事前可解释性。然而,目前大多数深度学习(DL)模型通常被视为黑盒模型,尽管自解释的深度学习模型正在逐渐开发,且具有事前可解释性(arxiv.org/abs/2108.11761)。因此,对于当前的实际深度学习应用,需要事后可解释性。此外,当模型开发阶段处于训练、验证或生产时,解释的范围可以是全局的、群体的或局部的,即使使用相同的事后可解释性工具(towardsdatascience.com/a-look-into-global-cohort-and-local-model-explainability-973bd449969f)。

范围:哪些预测需要解释

范围指的是我们是否能够为所有预测、部分预测或仅仅一个特定预测提供解释,即使我们为黑盒深度学习模型使用相同的事后工具。最常见的全局可解释性是描述特征重要性,并允许用户了解哪些特征对整体模型性能最有影响。局部可解释性则是关于特定预测实例的特征归因。特征归因与特征重要性的区别在于,特征归因不仅量化了特征影响的排名和大小,还量化了影响的方向(例如,一个特征是正向还是负向地影响了预测)。

许多后期工具对于深度学习模型在局部可解释性方面表现优秀。群体可解释性有助于识别一些特定群体(如年龄或种族群体)可能存在的模型偏差。对于深度学习模型,如果我们想要得到全局解释,通常需要使用替代模型(如决策树模型)来模拟深度学习模型的行为(towardsdatascience.com/explainable-ai-xai-methods-part-5-global-surrogate-models-9c228d27e13a)。然而,这种方法并不总是奏效,因为很难判断替代模型是否足够准确地逼近原始黑箱模型的预测。因此,在实际操作中,深度学习模型通常使用局部可解释性工具,如SHapley 加法解释SHAP),我们将在方法维度中对此进行解释。

输入数据格式:什么是输入数据的格式

输入数据格式指的是在开发和使用模型时,我们处理的输入数据类型。一个简单的模型可能只关注单一类型的输入数据格式,例如文本,而许多复杂的模型可能需要结合结构化的表格数据和非结构化数据,如图像或文本。此外,还需要单独理解输入数据的潜在偏差(在模型训练和验证期间)或漂移(在生产环境中)。因此,这是一个相当复杂的话题。数据解释也可以用于监控生产环境中的数据异常和漂移。这适用于所有类型的机器学习/深度学习模型。

输出数据格式:什么是输出解释的格式

输出解释格式指的是我们如何向目标受众呈现解释。通常,图像解释可能是一个条形图,显示特征的重要性及其得分,或者是一个显著性图,突出显示每个图像中某一特定类别的空间支持,适用于与图像相关的机器学习问题。对于文本输出,它可能是一个英语句子,解释为何某个信用申请因几个可理解的因素而被拒绝。自然语言处理(NLP)模型的可解释性也可以通过交互式探索实现,使用显著性图、注意力机制以及其他丰富的可视化手段(参见 Google 的语言可解释性工具LIT)示例:ai.googleblog.com/2020/11/the-language-interpretability-tool-lit.html)。由于没有一劳永逸的解释方法适用于这些复杂的输出格式,因此,满足受众对解释的需求、经验和期望是至关重要的。

问题类型:什么是机器学习问题类型

问题类型广泛地指代所有种类的机器学习/人工智能问题,但就实际应用而言,目前商业上成功的主要问题大多集中在分类、回归和聚类。强化学习和推荐系统在行业中的应用也越来越成功。深度学习模型现在常常应用于所有这些类型的问题,或者至少正在被评估作为潜在的候选模型。

目标类型:解释的动机或目标是什么

目标类型指的是在人工智能/机器学习项目中使用可解释性的动机。人们认为,可解释性的首要目标是通过提供足够的理解来获得信任,揭示 AI 系统行为中的脆弱性、偏差和缺陷。另一个动机是从输入和输出预测中推断因果关系。其他目标包括通过更好地理解 AI/ML 系统的内部工作原理来提高模型的准确性,以及在可能涉及严重后果时,通过透明的解释来为模型行为和决策提供正当理由。甚至可能通过解释揭示基于解释的未知见解和规则(www.tandfonline.com/doi/full/10.1080/10580530.2020.1849465)。总的来说,打破黑箱非常重要,以便在真实的生产系统中使用 AI/ML 模型和系统时,能够以信心进行使用。

方法类型:使用的具体后验解释方法是什么

**方法类型(后验分析)**指的是与深度学习模型密切相关的后验分析方法。后验分析方法主要分为两类:基于扰动的方法和基于梯度的方法。最近的研究开始尝试将这两种方法统一起来,尽管这种统一方法尚未广泛应用于实际中(teamcore.seas.harvard.edu/publications/towards-unification-and-robustness-perturbation-and-gradient-based)。以下是对这两种方法的简要讨论:

请注意,还有一些其他类型的方法,比如反事实方法 (christophm.github.io/interpretable-ml-book/counterfactual.html) 或基于原型的方法 (christophm.github.io/interpretable-ml-book/proto.html),这些方法我们在本书中不会涉及。

在讨论了可解释性的多个维度后,重要的是要知道 XAI(可解释人工智能)仍然是一个新兴领域(fairlyaccountable.org/aaai-2021-tutorial/doc/AAAI_slides_final.pdf),有时甚至很难在应用到同一数据集或模型时找到不同可解释性方法之间的一致性(请参见关于可解释机器学习中的分歧问题的最新研究,站在从业者的角度:arxiv.org/abs/2202.01602)。最终,确实需要进行一些实验,才能找出哪些可解释性方法提供了经过人工验证的解释,并满足现实世界中某个特定预测任务的需求。

在本章的接下来的两个部分中,我们将重点介绍一些流行的、正在兴起的工具包,并通过一些实践实验来学习如何进行可解释性分析。

探索 SHAP 可解释性工具箱

为了我们的学习目的,让我们在实验一些示例时回顾一些流行的可解释性工具箱。根据 GitHub 的星标数量(截至 2022 年 4 月为 16,000 个,github.com/slundberg/shap),SHAP 是最广泛使用和集成的开源模型可解释性工具箱。它也是与 MLflow 集成的基础解释工具。在这里,我们将进行一个小实验,亲身体验这种工具是如何工作的。让我们使用一个情感分析 NLP 模型,探索 SHAP 如何用于解释模型的行为:

  1. 在从 GitHub 检查本章的代码后,在本地环境中设置虚拟环境。运行以下命令将创建一个名为dl-explain的新虚拟环境:

    conda env create -f conda.yaml
    

这将安装 SHAP 及其相关依赖项,例如matplotlib,并将它们添加到该虚拟环境中。创建此虚拟环境后,通过运行以下命令来激活该虚拟环境:

conda activate dl-explain

现在,我们准备好使用 SHAP 进行实验了。

  1. 您可以查看shap_explain.ipynb笔记本,跟随其中的实验。该笔记本的第一步是导入相关的 Python 库:

    import transformers
    import shap
    from shap.plots import *
    

这些导入将允许我们使用 Hugging Face 的 transformers 管道 API,获取一个预训练的 NLP 模型并使用 SHAP 函数。

  1. 然后,我们使用 transformers 管道 API 创建dl_model来进行sentiment_analysis。请注意,这是一个预训练的管道,因此我们可以在不进行额外微调的情况下使用它。该管道中使用的默认转换器模型是distilbert-base-uncased-finetuned-sst-2-english(https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english):

    dl_model = transformers.pipeline(
        'sentiment-analysis', return_all_scores=True)
    

这将生成一个准备好预测输入句子情感(正面或负面)的模型。

  1. 尝试用两个输入句子测试这个dl_model,看看输出是否有意义:

    dl_model(
        ["What a great movie! ...if you have no taste.", 
         "Not a good movie to spend time on."])
    

这将输出每句话的标签和概率分数,如下所示:

[[{'label': 'NEGATIVE', 'score': 0.00014734962314832956}, {'label': 'POSITIVE', 'score': 0.9998526573181152}], [{'label': 'NEGATIVE', 'score': 0.9997993111610413}, {'label': 'POSITIVE', 'score': 0.00020068213052581996}]]

看起来第一句话被预测为POSITIVE的概率很高,而第二句话则被预测为NEGATIVE的概率很高。现在,如果我们仔细观察第一句话,我们可能会认为模型的预测是错误的,因为句子的后半部分(no taste)带有微妙的负面情绪。因此,我们想知道模型为何做出这样的预测。这就是模型可解释性发挥作用的地方。

  1. 现在,让我们使用 SHAP API,shap.Explainer,获取我们感兴趣的两句话的 Shapley 值:

    explainer = shap.Explainer(dl_model) 
    shap_values = explainer(["What a great movie! ...if you have no taste.", "Not a good movie to spend time on."])
    
  2. 一旦我们有了shap_values,就可以使用不同的可视化技术来展示 Shapley 值。第一种方法是使用shap.plot.text来可视化第一句话的 Shapley 值,当预测标签为POSITIVE时:

    shap.plots.text(shap_values[0, :, "POSITIVE"])
    

这将生成如下图表:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_09_002.jpg

图 9.2 – 句子 1 的 SHAP 可视化,带有正向预测

图 9.2所示,词语great的 SHAP 值非常大,主导了最终预测的影响,而词语no对最终预测的影响较小。这导致了最终的POSITIVE预测结果。那么,第二句话带有NEGATIVE预测的情况如何呢?运行以下命令将生成一个类似的图表:

shap.plots.text(shap_values[1, :, "NEGATIVE"])

该命令将创建如下图表:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_09_003.jpg

图 9.3 – 句子 2 的 SHAP 可视化,带有负向预测

图 9.3所示,词语Not对最终预测有很强的影响,而词语good的影响非常小,导致最终预测为NEGATIVE情感。这是非常有道理的,对模型行为的解释很到位。

  1. 我们还可以使用不同的图表来可视化shap_values。一种常见的方式是条形图,它显示特征对最终预测的贡献。运行以下命令将为第一句话生成一个图表:

    bar(shap_values[0, :,'POSITIVE'])
    

这将生成如下的条形图:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_09_004.jpg

图 9.4 – 句子 1 的 SHAP 条形图,带有正向预测

图 9.4中可以看出,该图表按重要性对特征进行了从上到下的排名,其中对最终预测有正面影响的特征绘制在x轴的正侧,而负面贡献则绘制在x轴的负侧。x轴表示每个标记或单词的 SHAP 值,并带有符号(+ 或 -)。这清楚地表明,单词great是一个强正面因素,影响了最终预测,而have no taste有一定的负面影响,但不足以改变最终预测的方向。

类似地,我们可以为第二个句子绘制如下的条形图:

bar(shap_values[1, :,'NEGATIVE'])

这将生成以下的条形图:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_09_005.jpg

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_09_005.jpg)

图 9.5 – 带有负面预测的第二个句子的 SHAP 条形图

图 9.5中可以看出,单词Not对最终预测有很强的贡献,而单词good排在第二位。这两个词对最终预测的影响是相反的,但显然,单词Not的影响要强得多,SHAP 值也大得多。

如果你已经跟着这个例子走,并在你的笔记本中看到了 SHAP 图表,那么恭喜你!这意味着你已经成功地运行了 SHAP 可解释性工具,为 NLP 文本情感分析中的深度学习转换器模型提供了解释。

让我们进一步探索另一种流行的可解释性工具,看看它们如何提供不同的解释。

探索 Transformers Interpret 工具箱

如我们在本章的第一节中回顾的那样,有两种主要方法:基于扰动的和基于梯度的事后可解释性工具。SHAP 属于基于扰动的方法家族。现在,让我们来看一个基于梯度的工具箱,名为Transformers Interpretgithub.com/cdpierse/transformers-interpret)。这是一个相对较新的工具,但它建立在一个名为Captumgithub.com/pytorch/captum)的统一模型可解释性和理解库之上,该库为 PyTorch 提供了统一的 API,可以使用基于扰动或梯度的工具(arxiv.org/abs/2009.07896)。Transformers Interpret 进一步简化了 Captum 的 API,使我们能够快速探索基于梯度的可解释性方法,从而获得实践经验。

首先,请确保您已经根据前一节的描述,设置并激活了dl-explain虚拟环境。然后,我们可以使用相同的 Hugging Face 转换器情感分析模型,探索一些 NLP 情感分类示例。接下来,我们可以执行以下步骤,学习如何使用 Transformers Interpret 进行模型解释。您可能想查看gradient_explain.ipynb笔记本,以跟随说明操作:

  1. 按如下方式将相关包导入笔记本:

    from transformers import AutoModelForSequenceClassification, AutoTokenizer
    from transformers_interpret import SequenceClassificationExplainer
    

这将使用 Hugging Face 的 transformer 模型和分词器,以及来自transformers_interpret的可解释性功能。

  1. 使用与前一节相同的预训练模型创建模型和分词器,该模型是distilbert-base-uncased-finetuned-sst-2-english模型:

    model_name = "distilbert-base-uncased-finetuned-sst-2-english"
    model = AutoModelForSequenceClassification.from_pretrained(model_name)
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    

现在我们已经有了模型和分词器,我们可以使用SequenceClassificationExplainer API 创建一个可解释性变量。

  1. 创建一个解释器并给出示例句子,以获取来自解释器的word归因:

    cls_explainer = SequenceClassificationExplainer(model, tokenizer)
    word_attributions = cls_explainer("Not a good movie to spend time on.")
    
  2. 我们也可以在检查word归因之前,通过运行以下命令获取预测标签:

    cls_explainer.predicted_class_name
    

这将产生Negative的结果,意味着预测为负面情绪。那么,让我们看看解释器是如何为这个预测提供解释的。

  1. 我们可以仅显示word_attributions值,或者我们可以将其可视化。word_attributions的值如下:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_09_006.jpg

图 9.6 – 使用分层集成梯度的词归因值,结果为负面预测

图 9.6可以看出,使用分层集成梯度方法,这是当前解释器在 Transformers Interpret 库中实现的默认方法,not这个词对最终的预测结果产生了积极的影响,最终的预测是负面情绪。这是有道理的。请注意,其他一些词,如to spend time on,对最终预测也有较强的积极影响。考虑到交叉注意力机制,似乎模型正试图提取not to spend time on作为最终预测的主要归因。请注意,我们还可以像下面这样可视化这些word归因:

cls_explainer.visualize("distilbert_viz.html")

这将产生以下图表:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_09_007.jpg

图 9.7 – 使用分层集成梯度的词归因值,结果为负面预测

图 9.7可以看出,它突出了not to spend time on这个词对最终负面预测结果的积极影响。

现在我们已经实验了基于扰动和梯度的可解释性方法,我们已成功完成了使用可解释性工具进行事后本地解释的动手探索。

接下来,我们将总结在本章中学到的内容。

总结

在本章中,我们通过八维分类回顾了 AI/ML 中的可解释性。虽然这不一定是一个全面或详尽的概述,但它为我们提供了一个大致的框架,包括谁需要解释、不同阶段和范围的解释、各种输入输出格式的解释、常见的 ML 问题和目标类型,以及最后的不同后验可解释性方法。接着,我们提供了两个具体的练习来探索 SHAP 和 Transformers 可解释工具箱,它们可以为 NLP 文本情感 DL 模型提供扰动和基于梯度的特征归因解释。

这为我们使用 DL 模型的可解释性工具奠定了坚实的基础。然而,考虑到 XAI 的积极发展,这仅仅是将 XAI 应用于 DL 模型的开始。其他可解释性工具箱,如 TruLens(github.com/truera/trulens)、Alibi(github.com/SeldonIO/alibi)、微软的负责任 AI 工具箱(github.com/microsoft/responsible-ai-toolbox)和 IBM 的 AI 可解释性 360 工具包(github.com/Trusted-AI/AIX360)都在积极开发中,值得进一步研究和学习。深入阅读部分还提供了额外的链接,帮助你继续学习这一主题。

现在我们已经了解了可解释性的基础知识,在下一章中,我们将学习如何在 MLflow 框架中实现可解释性,从而为在 MLflow 框架内提供统一的解释方式。

深入阅读

第十章:第十章:使用 MLflow 实现深度学习可解释性

深度学习DL)可解释性的重要性在前一章中已有充分讨论。为了在实际项目中实现深度学习可解释性,最好像其他模型工件一样,将解释器和解释作为工件记录在 MLflow 服务器中,这样我们就可以轻松跟踪和重现解释。将 SHAP(github.com/slundberg/shap)等深度学习可解释性工具与 MLflow 集成,可以支持不同的实现机制,理解这些集成如何应用于我们的深度学习可解释性场景是非常重要的。本章将探讨通过使用不同的 MLflow 功能将 SHAP 解释集成到 MLflow 中的几种方法。由于可解释性工具和深度学习模型都在快速发展,我们还将重点介绍使用 MLflow 实现深度学习可解释性时的当前限制和解决方法。到本章结束时,你将能够使用 MLflow API 实现 SHAP 解释和解释器,从而实现可扩展的模型可解释性。

本章将涵盖以下主要内容:

  • 理解当前的 MLflow 可解释性集成

  • 使用 MLflow 工件日志记录 API 实现 SHAP 解释

  • 使用 MLflow pyfunc API 实现 SHAP 解释器

技术要求

完成本章所需的以下要求:

理解当前的 MLflow 可解释性集成

MLflow 支持多种可解释性集成方式。在实现可解释性时,我们提到两种类型的工件:解释器和解释:

  • 解释器是一个可解释性模型,常见的模型是 SHAP 模型,其中可能有多种 SHAP 解释器,如 TreeExplainerKernelExplainerPartitionExplainershap.readthedocs.io/en/latest/generated/shap.explainers.Partition.html)。为了提高计算效率,我们通常选择 PartitionExplainer 来处理深度学习模型。

  • 解释是一个产物,它展示了某种形式的输出,可能是文本、数值或图表。解释可以发生在离线训练或测试中,或者发生在在线生产中。因此,如果我们想知道模型为何给出某些预测,我们应该能够提供一个离线评估的解释器或一个在线查询的解释器端点。

在这里,我们简要概述了截至 MLflow 版本 1.25.1(pypi.org/project/mlflow/1.25.1/)的当前功能。使用 MLflow 进行可解释性的方式有四种,如下所示:

  • 使用 mlflow.log_artifact API(www.mlflow.org/docs/latest/python_api/mlflow.html#mlflow.log_artifact)记录相关的解释产物,如条形图和 Shapley 值数组。这为记录解释提供了最大的灵活性。无论是离线批处理处理,还是在线自动记录某一预测的 SHAP 条形图,都可以使用该功能。需要注意的是,在在线生产场景中为每个预测记录解释是非常昂贵的,因此我们应为按需查询提供一个单独的解释 API。

  • 使用 mlflow.pyfunc.PythonModel API(www.mlflow.org/docs/latest/python_api/mlflow.pyfunc.html#mlflow.pyfunc.PythonModel)来创建一个解释器,该解释器可以通过 MLflow 的 pyfunc 方法进行记录和加载,mlflow.pyfunc.log_model 用于记录,mlflow.pyfunc.load_modelmlflow.pyfunc.spark_udf 用于加载解释器。这为我们提供了最大灵活性,可以将自定义解释器作为 MLflow 通用的 pyfunc 模型创建,并且可以用于离线批处理解释或在线提供解释即服务EaaS)。

  • 使用mlflow.shap API(www.mlflow.org/docs/latest/python_api/mlflow.shap.html)。该 API 存在一些限制。例如,mlflow.shap.log_explainer方法仅支持 scikit-learn 和 PyTorch 模型。mlflow.shap.log_explanation方法仅支持shap.KernelExplainershap-lrjball.readthedocs.io/en/latest/generated/shap.KernelExplainer.html)。这非常消耗计算资源,因为计算时间会随着特征数量的增加呈指数增长;因此,甚至对于中等规模的数据集,也无法计算解释(请参阅已发布的 GitHub 问题github.com/mlflow/mlflow/issues/4071)。MLflow 提供的现有示例仅适用于 scikit-learn 包中的经典机器学习模型,如线性回归或随机森林,并没有提供深度学习模型的解释示例(github.com/mlflow/mlflow/tree/master/examples/shap)。我们将在本章后面的章节中展示,当前该 API 不支持基于 transformers 的 SHAP 解释器和解释,因此在本章中我们将不使用该 API。我们将在讲解本章示例时突出一些问题。

  • 使用mlflow.evaluate API(www.mlflow.org/docs/latest/python_api/mlflow.html#mlflow.evaluate)。该 API 可以在模型训练和测试之后用于评估。这是一个实验性功能,未来可能会有所变化。它支持 MLflow pyfunc模型。然而,它有一些限制:评估数据集的标签值必须是数字或布尔值,所有特征值必须是数字,每个特征列必须只包含标量值(www.mlflow.org/docs/latest/models.html#model-evaluation)。同样,MLflow 提供的现有示例仅适用于 scikit-learn 包中的经典机器学习模型(github.com/mlflow/mlflow/tree/master/examples/evaluation)。我们可以使用该 API 仅记录 NLP 情感模型的分类器指标,但该 API 会自动跳过解释部分,因为它要求特征列包含标量值(NLP 模型的输入是文本输入)。因此,这不适用于我们需要的深度学习模型可解释性。所以,我们在本章中将不使用该 API。

鉴于某些 API 仍处于实验阶段并且还在不断发展,用户应注意限制和解决方法,以便成功实现 MLflow 的可解释性。在本章中,我们将学习如何实现深度学习模型的可解释性,使用 MLflow 来实现这一点相当具有挑战性,因为 MLflow 与 SHAP 的集成仍在进行中(截至 MLflow 版本 1.25.1)。在接下来的章节中,我们将学习何时以及如何使用这些不同的 API 来实现解释,并记录和加载深度学习模型的解释器。

使用 MLflow 工件记录 API 实现 SHAP 解释

MLflow 有一个通用的跟踪 API,可以记录任何工件:mlflow.log_artifact。然而,MLflow 文档中给出的示例通常使用 scikit-learn 和表格型数值数据进行训练、测试和解释。在这里,我们希望展示如何使用mlflow.log_artifact来记录与 NLP 情感分析深度学习模型相关的工件,如 Shapley 值数组和 Shapley 值条形图。您可以查看本章 GitHub 仓库中的 Python VS Code 笔记本shap_mlflow_log_artifact.pygithub.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter10/notebooks/shap_mlflow_log_artifact.py),以便按照步骤操作:

  1. 请确保您已准备好相关的前提条件,包括本地完整的 MLflow 服务器和 conda 虚拟环境。请按照README.md文件中的说明(github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter10/README.md)中的步骤,确保这些内容已就绪,文件位于 第十章 文件夹中。

  2. 在开始运行本章任何代码之前,请确保按照如下方式激活chapter10-dl-explain虚拟环境:

    conda activate chapter10-dl-explain
    
  3. 在笔记本开头导入相关库,如下所示:

    import os
    import matplotlib.pyplot as plt
    import mlflow
    from mlflow.tracking import MlflowClient
    from mlflow.utils.file_utils import TempDir
    import shap
    import transformers
    from shap.plots import *
    import numpy as np
    
  4. 下一步是设置一些环境变量。前三个环境变量用于本地 MLflow URI,第四个用于禁用由于已知 Hugging Face 标记化问题而产生的 Hugging Face 警告:

    os.environ["AWS_ACCESS_KEY_ID"] = "minio"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "minio123"
    os.environ["MLFLOW_S3_ENDPOINT_URL"] = "http://localhost:9000"
    os.environ["TOKENIZERS_PARALLELISM"] = "False"
    
  5. 我们还需要设置 MLflow 实验,并将 MLflow 实验 ID 作为输出显示在屏幕上:

    EXPERIMENT_NAME = "dl_explain_chapter10"
    mlflow.set_tracking_uri('http://localhost')
    mlflow.set_experiment(EXPERIMENT_NAME)
    experiment = mlflow.get_experiment_by_name(EXPERIMENT_NAME)
    print("experiment_id:", experiment.experiment_id)
    

如果您已经运行了该笔记本,应该能看到类似如下的输出:

experiment_id: 14

这意味着实验名称为dl_explain_chapter10的 MLflow 实验 ID 是14。请注意,您也可以按照如下方式将 MLflow 跟踪 URI 设置为环境变量:

export MLFLOW_TRACKING_URI=http://localhost

在这里,我们使用 MLflow 的mlflow.set_tracking_uri API 来定义 URI 位置。两种方式都可以。

  1. 现在我们可以创建一个 DL 模型,使用 Hugging Face 的 transformer pipeline API 将一句话分类为正面或负面情感。由于这个模型已经进行过微调,我们将重点放在如何获取该模型的解释器和解释内容,而不是如何训练或微调模型:

    dl_model = transformers.pipeline('sentiment-analysis', return_all_scores=False)
    explainer = shap.Explainer(dl_model)
    shap_values = explainer(["Not a good movie to spend time on.", "This is a great movie."])
    

这些代码片段创建了一个情感分析模型dl_model,然后为该模型创建一个 SHAP explainer。接着,我们为这个解释器提供了一个包含两句话的列表,以获取 shap_values 对象。该对象将用于在 MLflow 中记录。

给定 shap_values 对象,我们现在可以开始一个新的 MLflow 运行,并记录 Shapley 值和我们在上一章中看到的条形图(第九章*,深度学习可解释性基础*)。第一行代码确保所有活跃的 MLflow 运行都已结束。如果我们想多次交互性地重新运行这段代码,这非常有用:

mlflow.end_run()

然后我们定义两个常量。一个是artifact_root_path,用于 MLflow 工件存储中的根路径,将用于存储所有 SHAP 解释对象。另一个是shap_bar_plot,用于工件文件名,将用于条形图图形:

artifact_root_path = "model_explanations_shap"
artifact_file_name = 'shap_bar_plot'
  1. 然后我们开始一个新的 MLflow 运行,在该运行中,我们将生成并记录三个 SHAP 文件到 MLflow 工件存储中,路径为 model_explanations_shap

    with mlflow.start_run() as run:
       with TempDir() as temp_dir:
            temp_dir_path = temp_dir.path()
            print("temp directory for artifacts: {}".format(temp_dir_path))
    

我们还需要一个临时的本地目录,如前面的代码片段所示,用来先保存 SHAP 文件,然后再将这些文件记录到 MLflow 服务器。如果你已经运行到此步骤,你应该在输出中看到一个类似以下的临时目录:

temp directory for artifacts: /var/folders/51/whxjy4r92dx18788yp11ycyr0000gp/T/tmpgw520wu1
  1. 现在我们准备生成并保存 SHAP 文件。第一个是条形图,这个稍微有点复杂,保存和记录起来有点难度。让我们一起看看以下代码,了解我们是如何做到的:

    try:
         plt.clf()
         plt.subplots_adjust(bottom=0.2, left=0.4)
         shap.plots.bar(shap_values[0, :, "NEGATIVE"],
                        show=False)
         plt.savefig(f"{temp_dir_path}/{artifact_file_name}")
    finally:
         plt.close(plt.gcf())
    mlflow.log_artifact(f"{temp_dir_path}/{artifact_file_name}.png", artifact_root_path)
    

请注意,我们使用了 matplotlib.pyplot,并将其导入为 plt,首先通过 plt.clf() 清除图形,然后创建一个具有一些调整的子图。在这里,我们定义了 bottom=0.2,意味着子图底部边缘的位置位于图形高度的 20%。同样,我们调整了子图的左边缘。然后,我们使用 shap.plots.bar SHAP API 绘制第一句话的特征贡献的条形图,但将 show 参数设置为 False。这意味着我们在交互式运行中看不到该图,但图形会存储在 pyplot plt 变量中,然后可以使用 plt.savefig 将其保存到本地临时目录,文件名前缀为 shap_bar_plotpyplot 会在文件保存后自动添加文件扩展名 .png。因此,这会将名为 shap_bar_plot.png 的本地图像文件保存到临时文件夹中。最后的语句调用了 MLflow 的 mlflow.log_artifact,将此 PNG 文件上传到 MLflow 跟踪服务器的工件存储的根文件夹 model_explanations_shap。我们还需要确保通过调用 plt.close(plt.gcf()) 来关闭当前图形。

  1. 除了将 shap_bar_plot.png 日志记录到 MLflow 服务器外,我们还希望将 Shapley base_values 数组和 shap_values 数组作为 NumPy 数组记录到 MLflow 跟踪服务器中。这可以通过以下语句实现:

    np.save(f"{temp_dir_path}/shap_values", 
            shap_values.values)
    np.save(f"{temp_dir_path}/base_values", 
            shap_values.base_values)
            mlflow.log_artifact(
                f"{temp_dir_path}/shap_values.npy", 
                artifact_root_path)
            mlflow.log_artifact(
                f"{temp_dir_path}/base_values.npy", 
                artifact_root_path)      
    

这将首先在本地临时文件夹中保存 shap_values.npybase_values.npy 的本地副本,然后将其上传到 MLflow 跟踪服务器的工件存储中。

  1. 如果你按照笔记本的步骤操作到这里,你应该能够在本地 MLflow 服务器中验证这些工件是否成功存储。前往本地主机上的 MLflow UI – http://localhost/,然后找到实验 dl_explain_chapter10。你应该能够找到刚刚运行的实验。它应该类似于 图 10.1,在那里你可以在 model_explanations_shap 文件夹中找到三个文件:base_values.npyshap_bar_plot.pngshap_values.npy图 10.1 显示了句子 Not a good movie to spend time on 的预测结果的不同标记或词汇的特征贡献条形图。此实验页面的 URL 类似于以下内容:

    http://localhost/#/experiments/14/runs/10f0655189f740aeb813a015f1f6e115
    

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_10_001.jpg

图 10.1 – MLflow log_artifact API 将 SHAP 条形图作为图像保存到 MLflow 跟踪服务器

或者,您也可以使用代码以编程方式下载存储在 MLflow 跟踪服务器中的这些文件并在本地查看。我们在笔记本的最后一个单元格中提供了这样的代码。

  1. 如果你运行笔记本代码中的最后一个单元格,该单元格用于从我们刚才保存的 MLflow 服务器下载这三个文件并打印它们,你应该能够看到以下输出,如图 10.2所示。从 MLflow 跟踪服务器下载工件的机制是使用MlflowClient().download_artifacts API,你需要提供 MLflow 运行 ID(在我们的示例中是10f0655189f740aeb813a015f1f6e115)和工件根路径model_explanations_shap作为 API 的参数:

    downloaded_local_path = MlflowClient().download_artifacts(run.info.run_id, artifact_root_path)
    

这将把model_explanations_shap文件夹中的所有文件从 MLflow 跟踪服务器下载到本地路径,返回的变量为downloaded_local_path

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_10_002.jpg

图 10.2 – 从 MLflow 跟踪服务器下载 SHAP 的 base_values 和 shap_values 数组到本地路径并显示它们

要显示这两个 NumPy 数组,我们需要调用 NumPy 的load API 来加载它们,然后打印它们:

base_values = np.load(os.path.join(downloaded_local_path, "base_values.npy"), allow_pickle=True)
shap_values = np.load(os.path.join(downloaded_local_path, "shap_values.npy"), allow_pickle=True)

请注意,当调用np.load API 时,我们需要将allow_pickle参数设置为True,以便 NumPy 可以正确地将这些文件加载回内存。

虽然你可以在 VS Code 环境中交互式运行这个笔记本,但你也可以按如下方式在命令行中运行它:

python shap_mlflow_log_artifact.py

这将产生所有控制台输出,并将所有工件记录到 MLflow 服务器中,正如我们在交互式运行笔记本时所看到的。

如果你已经运行了到目前为止的代码,恭喜你成功实现了使用 MLflow 的mlflow.log_artifact API 将 SHAP 解释记录到 MLflow 跟踪服务器!

尽管记录所有解释的过程看起来有些冗长,但这种方法确实有一个优点,即不依赖于使用何种解释器,因为解释器是在 MLflow 工件日志 API 之外定义的。

在下一节中,我们将看到如何使用内置的mlflow.pyfunc.PythonModel API 将 SHAP 解释器记录为 MLflow 模型,然后将其部署为端点或像通用的 MLflow pyfunc模型一样在批处理模式中使用。

使用 MLflow pyfunc API 实现 SHAP 解释器

正如我们在前一节中了解到的,SHAP 解释器可以在需要时离线使用,只需通过 SHAP API 创建一个新的解释器实例。然而,由于底层的深度学习模型通常会被记录到 MLflow 服务器中,因此希望将相应的解释器也记录到 MLflow 服务器中,这样我们不仅可以跟踪深度学习模型,还能跟踪它们的解释器。此外,我们还可以使用通用的 MLflow pyfunc 模型日志和加载 API 来处理解释器,从而统一访问深度学习模型及其解释器。

在本节中,我们将一步一步学习如何将 SHAP 解释器实现为一个通用的 MLflow pyfunc 模型,并如何将其用于离线和在线解释。我们将把过程分为三个小节:

  • 创建并记录 MLflow pyfunc 解释器

  • 部署 MLflow pyfunc 解释器用于 EaaS

  • 使用 MLflow pyfunc 解释器进行批量解释

让我们从创建和记录 MLflow pyfunc 解释器的第一小节开始。

创建并记录 MLflow pyfunc 解释器

为了跟随本节内容,请查看 GitHub 仓库中的nlp_sentiment_classifier_explainer.py文件(github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter10/pipeline/nlp_sentiment_classifier_explainer.py):

  1. 首先,通过子类化mlflow.pyfunc.PythonModel,我们可以创建一个定制的 MLflow 模型,该模型封装了一个 SHAP 解释器。因此,按如下方式声明此类:

    class SentimentAnalysisExplainer(mlflow.pyfunc.PythonModel):
    
  2. 接下来,我们需要实例化一个解释器。我们将不在这个类的init方法中创建解释器,而是使用load_context方法为 Hugging Face NLP 情感分析分类器加载一个 SHAP 解释器,如下所示:

    def load_context(self, context):
      from transformers import pipeline
      import shap
      self.explainer = shap.Explainer(pipeline('sentiment-analysis', return_all_scores=True))
    

这将在每次执行SentimentAnalysisExplainer类时创建一个 SHAP 解释器。请注意,情感分类器是一个 Hugging Face 管道对象,return_all_scores参数设置为True。这意味着它将返回每个输入文本的正负情感标签和概率得分。

避免 SHAP 解释器的运行时错误

如果我们在此类的init方法中实现self.explainer,则会遇到与 SHAP 包的_masked_model.py文件相关的运行时错误,该错误抱怨init方法将被 MLflow 序列化,因此很明显,这个运行时错误来自 MLflow 的序列化。然而,在load_context函数中实现self.explainer可以避免 MLflow 的序列化,并且在运行时调用这个解释器时能正常工作。

  1. 然后我们将实现sentiment_classifier_explanation方法,该方法接收一个 pandas DataFrame 行作为输入,并生成一个已保存的shap_values输出,作为单行文本输入的解释:

    def sentiment_classifier_explanation(self, row):
      shap_values = self.explainer([row['text']])
      return [pickle.dumps(shap_values)]
    

请注意,我们需要使用一对方括号将 row['text'] 的值括起来,使其成为一个列表,而不仅仅是单一的值。这是因为这个 SHAP 解释器期望的是一个文本列表,而不是单一的字符串。如果我们不将值放入方括号中,解释器会按字符拆分整个字符串,将每个字符当作一个单词,这不是我们想要的。一旦我们从解释器获得了作为 shap_values 的 Shapley 值输出,我们就需要使用 pickle.dumps 对其进行序列化,然后再返回给调用方。MLflow pyfunc 模型的输入输出签名不支持未序列化的复杂对象,因此这一步的 pickling 确保了模型输出签名符合 MLflow 的要求。稍后我们将在 第 5 步 中看到这个 MLflow pyfunc 解释器的输入输出签名的定义。

  1. 接下来,我们需要为这个类实现所需的 predict 方法。这将应用 sentiment_classifier_explanation 方法到整个输入的 pandas DataFrame,如下所示:

    def predict(self, context, model_input):
      model_input[['shap_values']] = model_input.apply(
        self.sentiment_classifier_explanation, axis=1, 
        result_type='expand')
      model_input.drop(['text'], axis=1, inplace=True)
      return model_input
    

这将为输入 pandas DataFrame 中的 text 列的每一行生成一个新的列,命名为 shap_values。然后我们删除 text 列,返回一个单列的 shap_values DataFrame 作为最终的预测结果:在这种情况下,解释结果以 DataFrame 的形式呈现。

  1. 现在我们已经实现了 SentimentAnalysisExplainer 类,可以使用标准的 MLflow pyfunc 模型日志记录 API 将此模型记录到 MLflow 跟踪服务器中。在进行 MLflow 日志记录之前,让我们确保声明此解释器的模型签名,如下所示:

    input = json.dumps([{'name': 'text', 'type': 'string'}])
    output = json.dumps([{'name': 'shap_values', 'type': 'string'}])
    signature = ModelSignature.from_dict({'inputs': input, 'outputs': output})
    

这些语句声明了输入是一个包含单一 string 类型 text 列的 DataFrame,输出是一个包含单一 string 类型 shap_values 列的 DataFrame。回想一下,这个 shap_values 列是一个经过 pickled 序列化的字节串,包含了 Shapley 值对象。

  1. 最后,我们可以在任务方法中使用 mlflow.pyfunc.log_model 方法实现解释器日志记录步骤,如下所示:

    with mlflow.start_run() as mlrun:          
      mlflow.pyfunc.log_model(
        artifact_path=MODEL_ARTIFACT_PATH, 
        conda_env=CONDA_ENV,                           
        python_model=SentimentAnalysisExplainer(), 
        signature=signature)
    

我们在 log_model 方法中使用了四个参数。MODEL_ARTIFACT_PATH 是 MLflow 跟踪服务器中存储解释器的文件夹名称。这里,值在你查看的 Python 文件中定义为 nlp_sentiment_classifier_explainerCONDA_ENV 是本章根目录中的 conda.yaml 文件。python_model 参数是我们刚刚实现的 SentimentAnalysisExplainer 类,signature 是我们定义的解释器输入输出签名。

  1. 现在我们已经准备好按如下方式在命令行运行整个文件:

    python nlp_sentiment_classifier_explainer.py
    

假设你已经根据 GitHub 仓库中本章的 README.md 文件正确设置了本地 MLflow 跟踪服务器和环境变量,那么在控制台输出中将生成以下两行:

2022-05-11 17:49:32,181 Found credentials in environment variables.
2022-05-11 17:49:32,384 finished logging nlp sentiment classifier explainer run_id: ad1edb09e5ea4d8ca0332b8bc2f5f6c9

这意味着我们已经成功地将解释器记录到本地的 MLflow 跟踪服务器中。

  1. 在 Web 浏览器中访问 http://localhost/,然后点击 dl_explain_chapter10 实验文件夹。你应该能在 nlp_sentiment_classifier_explainer 下的 Artifacts 文件夹中找到此运行和记录的解释器,它应该如图 10.3所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_10_003.jpg

图 10.3 – SHAP 解释器作为 MLflow pyfunc 模型被记录

请注意,图 10.3 中显示的 MLmodel 元数据与我们之前记录为 MLflow pyfunc 模型的普通 DL 推理流水线差别不大,除了 artifact_path 名称和 signature。这就是使用这种方法的优势,因为现在我们可以使用通用的 MLflow pyfunc 模型方法来加载此解释器或将其作为服务部署。

mlflow.shap.log_explainer API 的问题

正如我们之前提到的,MLflow 提供了一个 mlflow.shap.log_explainer API,它提供了一种记录解释器的方法。然而,这个 API 不支持我们的 NLP 情感分类器解释器,因为我们的 NLP 流水线不是 MLflow 当前支持的已知模型类型。因此,即使 log_explainer 能将该解释器对象写入跟踪服务器,当通过 mlflow.shap.load_explainer API 将解释器加载回内存时,它会因以下错误消息而失败:mlflow.shap.log_explainer API 在本书中的问题。

现在我们有了已记录的解释器,可以通过两种方式使用它:将其部署到 Web 服务中,以便我们可以创建一个端点来建立 EaaS,或者直接通过 MLflow pyfunc 的 load_modelspark_udf 方法使用 MLflow 的 run_id 加载解释器。让我们从设置本地 Web 服务来开始部署。

部署 MLflow pyfunc 解释器以进行 EaaS

由于现在 SHAP 解释器就像一个通用的 MLflow pyfunc 模型,我们可以按标准 MLflow 方式设置一个本地 EaaS。请按照以下步骤查看如何在本地实现:

  1. 运行以下 MLflow 命令,以为我们刚刚记录的解释器设置本地 Web 服务。此示例中的 run_idad1edb09e5ea4d8ca0332b8bc2f5f6c9

    mlflow models serve -m runs:/ ad1edb09e5ea4d8ca0332b8bc2f5f6c9/nlp_sentiment_classifier_explainer
    

这将产生以下控制台输出:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_10_004.jpg

图 10.4 – SHAP EaaS 控制台输出

请注意,在图 10.4中,默认的预训练语言模型在 gunicore HTTP 服务器启动并运行后加载。这是因为我们实现的解释器位于 load_context 方法中,这正是预期的行为:在 Web 服务启动并运行后立即加载解释器。

  1. 在另一个终端窗口中,输入以下命令以调用本地主机端口 5000 上的解释器 Web 服务,并输入两个示例文本:

    curl -X POST -H "Content-Type:application/json; format=pandas-split" --data '{"columns":["text"],"data":[["This is meh weather"], ["This is great weather"]]}' http://127.0.0.1:5000/invocations
    

这将产生以下输出:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_10_005.jpg

图 10.5 - 调用我们的 SHAP EaaS 后 DataFrame 中的响应

请注意,在图 10.5中,列名为 shap_values,而其值为 pickle 序列化的字节十六进制数据。这些数据并不易于阅读,但可以在调用方使用pickle.loads方法将其转换回原始的 shap_values。因此,如果您看到类似图 10.5的响应输出,恭喜您已经设置了本地 EaaS!您可以像其他 MLflow 服务部署一样部署此解释器服务,正如第八章**,在规模上部署 DL 推理管道中描述的那样,因为此解释器现在可以像通用的 MLflow pyfunc 模型服务一样调用。

接下来,我们将看到如何使用 MLflow pyfunc 解释器进行批量解释。

使用 MLflow pyfunc 解释器进行批量解释

有两种方法可以使用 MLflow pyfunc 解释器实现离线批量解释:

  • 将 pyfunc 解释器加载为 MLflow pyfunc 模型,以解释给定的 pandas DataFrame 输入。

  • 将 pyfunc 解释器加载为 PySpark UDF 以解释给定的 PySpark DataFrame 输入。

让我们从将解释器作为 MLflow pyfunc 模型加载开始。

将 MLflow pyfunc 解释器加载为 MLflow pyfunc 模型

正如我们之前提到的,消费 MLflow 记录的解释器的另一种方法是直接在本地 Python 代码中使用 MLflow 的 pyfunc load_model方法加载解释器,而不是将其部署为 Web 服务。这非常直接,我们将向您展示如何完成。您可以在 GitHub 存储库中的shap_mlflow_pyfunc_explainer.py文件中查看代码(github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter10/notebooks/shap_mlflow_pyfunc_explainer.py):

  1. 第一步是将记录的解释器加载回内存。以下代码使用 mlflow.pyfunc.load_model 和解释器 run_id URI 来实现这一点:

    run_id = "ad1edb09e5ea4d8ca0332b8bc2f5f6c9"
    logged_explainer = f'runs:/{run_id}/nlp_sentiment_classifier_explainer'
    explainer = mlflow.pyfunc.load_model(logged_explainer)
    

这应该加载解释器,就像它只是一个通用的 MLflow pyfunc 模型一样。我们可以通过运行以下代码打印解释器的元数据:

explainer

这将显示以下输出:

mlflow.pyfunc.loaded_model: artifact_path: nlp_sentiment_classifier_explainer flavor: mlflow.pyfunc.model run_id: ad1edb09e5ea4d8ca0332b8bc2f5f6c9

这意味着这是一个mlflow.pyfunc.model风格,这是个好消息,因为我们可以使用相同的 MLflow pyfunc API 来使用这个解释器。

  1. 接下来,我们将获取一些示例数据来测试新加载的解释器:

    import datasets
    dataset = datasets.load_dataset("imdb", split="test")
    short_data = [v[:500] for v in dataset["text"][:20]]
    df_test = pd.DataFrame (short_data, columns = ['text'])
    

这将加载 IMDb 测试数据集,将每个评论文本截断为 500 个字符,并选择前 20 行,以便为下一步的解释创建一个 pandas DataFrame。

  1. 现在,我们可以按如下方式运行解释器:

    results = explainer.predict(df_test)
    

这将为输入 DataFrame df_test 运行 SHAP 分区解释器。当运行时,它将显示 DataFrame 的每一行的以下输出:

Partition explainer: 2it [00:38, 38.67s/it]

结果将是一个包含单列shap_values的 pandas DataFrame。这可能需要几分钟时间,因为它需要对每一行进行分词、执行解释器并序列化输出。

  1. 一旦解释器执行完毕,我们可以通过反序列化行内容来检查结果。以下是检查第一行输出的代码:

    results_deserialized = pickle.loads(results['shap_values'][0])
    print(results_deserialized)
    

这将打印出第一行的shap_values图 10.6展示了shap_values输出的部分截图:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_10_006.jpg

图 10.6 – 解释中反序列化shap_values的部分输出

正如在图 10.6中所看到的,shap_values的输出与我们在*第九章**《深度学习可解释性基础》*中学到的没有什么不同,当时我们没有使用 MLflow 记录和加载解释器。我们还可以生成 Shapley 文本图,以突出文本对预测情感的贡献。

  1. 在笔记本中运行以下语句以查看 Shapley 文本图:

    shap.plots.text(results_deserialized[:,:,"POSITIVE"])
    

这将生成一个如图 10.7所示的图:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_10_007.jpg

图 10.7 – 使用我们通过 MLflow 记录的解释器反序列化shap_values生成的 Shapley 文本图

图 10.7所示,这条评论具有正面情感,贡献于预测情感的关键词或短语包括goodlove以及其他一些以红色标记的短语。当你看到这个 Shapley 文本图时,应该为自己鼓掌,因为你已经学会了如何使用 MLflow 记录的解释器来生成批量解释。

如在这一批量解释的逐步实现中提到的,使用这种 pyfunc 模型方法进行大规模批量解释会稍微慢一些。幸运的是,我们还有另一种方法可以使用 PySpark UDF 函数实现批量解释,我们将在下一个子章节中进行解释。

加载 pyfunc 解释器作为 PySpark UDF

对于可扩展的批量解释,我们可以利用 Spark 的分布式计算能力,这通过将 pyfunc 解释器作为 PySpark UDF 加载来支持。使用此功能不需要额外的工作,因为 MLflow pyfunc API 已经通过mlflow.pyfunc.spark_udf方法提供了这一功能。我们将一步步向你展示如何实现这种大规模解释:

  1. 首先,确保你已经完成了README.md文件的操作(github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter10/README.md),以安装 Spark,创建并激活chapter10-dl-pyspark-explain虚拟环境,并在运行 PySpark UDF 代码进行大规模解释之前设置所有环境变量。

  2. 然后,您可以开始运行 VS Code 笔记本shap_mlflow_pyspark_explainer.py,您可以在 GitHub 仓库中查看:github.com/PacktPublishing/Practical-Deep-Learning-at-Scale-with-MLFlow/blob/main/chapter10/notebooks/shap_mlflow_pyspark_explainer.py。在chapter10/notebooks/目录下运行以下命令:

    python shap_mlflow_pyspark_explainer.py
    

您将会得到显示在图 10.8中的最终输出,前面有相当多行输出在这些最后几行之前:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/prac-dl-scl-mlflow/img/B18120_10_008.jpg

图 10.8 – PySpark UDF 解释器输出的前两行文本的 shap_values 以及它们的输入文本

正如在图 10.8中所示,PySpark UDF 解释器的输出是一个 PySpark DataFrame,包含两列:textshap_valuestext列是原始输入文本,而shap_values列包含了像我们在前面子节中使用 pandas DataFrame 时看到的 Shapley 值的 pickled 序列化形式。

现在让我们看看代码中发生了什么。我们将解释shap_mlflow_pyspark_explainer.py文件中的关键代码块。由于这是一个 VS Code 笔记本,您可以像我们刚刚在命令行中运行或在 VS Code IDE 窗口中交互式地运行它。

  1. 第一个关键代码块是使用mflow.pyfunc.spark_udf方法加载解释器,如下所示:

    spark = SparkSession.builder.appName("Batch explanation with MLflow DL explainer").getOrCreate()
    run_id = "ad1edb09e5ea4d8ca0332b8bc2f5f6c9"
    logged_explainer = f'runs:/{run_id}/nlp_sentiment_classifier_explainer'
    explainer = mlflow.pyfunc.spark_udf(spark, model_uri=logged_explainer, result_type=StringType())
    

第一条语句是初始化一个SparkSession变量,然后使用run_id将记录的解释器加载到内存中。运行解释器以获取元数据如下:

explainer

我们将得到以下结果:

<function mlflow.pyfunc.spark_udf.<locals>.udf(iterator: Iterator[Tuple[Union[pandas.core.series.Series, pandas.core.frame.DataFrame], ...]]) -> Iterator[pandas.core.series.Series]>

这意味着我们现在将 SHAP 解释器包装为 Spark UDF 函数。这允许我们在下一步直接对输入的 PySpark DataFrame 应用 SHAP 解释器。

  1. 我们像以前一样加载 IMDb 测试数据集,以获得short_data列表,然后为测试数据集的前 20 行创建一个 PySpark DataFrame 以进行解释:

    df_pandas = pd.DataFrame (short_data, columns = ['text'])
    spark_df = spark.createDataFrame(df_pandas)
    spark_df = spark_df.withColumn('shap_values', explainer())
    

注意最后一条语句,它使用 PySpark 的withColumn函数将一个新的shap_values列添加到输入 DataFrame spark_df中,该 DataFrame 最初只包含一个列text。这是使用 Spark 的并行和分布式计算能力的自然方式。如果您已经运行了使用 MLflow pyfunc load_model方法和当前 PySpark UDF 方法的前面的非 Spark 方法,您会注意到 Spark 方法即使在本地计算机上也运行得更快。这使我们能够为许多输入文本实例规模化地进行 SHAP 解释。

  1. 最后,为了验证结果,我们展示了spark_df DataFrame 的前两行,这在图 10.8中有所说明。

现在,通过 MLflow 的 pyfunc Spark UDF 包装的 SHAP 解释器,我们可以自信地进行大规模批量解释。恭喜!

让我们在接下来的部分总结本章所学内容。

摘要

在本章中,我们首先回顾了可以用于实现可解释性的 MLflow API 中的现有方法。两个现有的 MLflow API,mlflow.shapmlflow.evaluate,存在一定的局限性,因此不能用于我们需要的复杂深度学习模型和管道的可解释性场景。随后,我们重点介绍了在 MLflow API 框架内实现 SHAP 解释和解释器的两种主要方法:使用 mlflow.log_artifact 记录解释内容,以及使用 mlflow.pyfunc.PythonModel 记录 SHAP 解释器。使用 log_artifact API 可以将 Shapley 值和解释图记录到 MLflow 跟踪服务器中。而使用 mlflow.pyfunc.PythonModel 则可以将 SHAP 解释器作为 MLflow pyfunc 模型记录,从而为将 SHAP 解释器作为 Web 服务部署并创建 EaaS 端点提供了可能。同时,它也为通过 MLflow pyfunc 的 load_modelspark_udf API 执行大规模离线批量解释提供了可能。这使我们能够在大规模应用中为深度学习模型实现可解释性,增加了实现的信心。

随着可解释性领域的不断发展,MLflow 与 SHAP 及其他可解释性工具箱的集成也将持续改进。我们鼓励有兴趣的读者通过进一步阅读部分提供的链接,继续他们的学习之旅。祝愿大家持续学习和成长!

进一步阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值