代码生成模型指南

注意

从代码生成的模型在 MLflow 2.12.2 及以上版本中可用。如果您使用的版本早于支持此功能的版本,则需要使用 自定义 Python 模型 文档中概述的传统序列化方法。

备注

从代码生成的模型仅适用于 LangChainLlamaIndex 和自定义 ``pyfunc``(PythonModel 实例)模型。如果您直接使用其他库,建议使用特定模型风格中提供的保存和记录功能。

代码模型功能是对定义、存储和加载自定义模型以及不依赖于序列化模型权重的特定风格实现(如 LangChainLlamaIndex)过程的全面重构。

这些模型在传统序列化与代码生成模型方法之间的关键区别在于模型在序列化过程中如何表示。

在传统方法中,序列化是通过使用 cloudpickle``(自定义 pyfunc LangChain)或自定义序列化器在模型对象上完成的,后者对底层包中的所有功能覆盖不完整(在 LlamaIndex 的情况下)。对于自定义 pyfunc,使用 ``cloudpickle 序列化对象实例会创建一个二进制文件,该文件在加载时用于重建对象。

在代码模型中,对于支持的模型类型,会保存一个包含自定义pyfunc或风格接口定义的简单脚本(例如,在LangChain的情况下,我们可以在脚本中直接定义并标记一个LCEL链作为模型)。

使用代码中的模型进行自定义 pyfunc 和支持库实现的最大收益在于减少了在实现过程中可能发生的重复试错调试。下图展示了在为自定义模型开发解决方案时,这两种方法的比较:

代码比较中的模型与传统序列化

与传统序列化的差异

在自定义模型的传统模式中,您的子类实例 mlflow.pyfunc.PythonModel 在调用 log_model 时被提交。当通过对象引用调用时,MLflow 将使用 cloudpickle 尝试序列化您的对象。

LangChain 的原生风格序列化中,使用 cloudpickle 来存储对象引用。然而,由于外部状态引用或API中使用了lambda函数,只有一部分可以在 LangChain 中使用的对象类型可以被序列化。另一方面, LlamaIndex 在其原生实现的风格中使用了一个自定义序列化器,但由于需要支持库中边缘案例功能而实现过于复杂,因此不能覆盖库的所有可能用途。

在代码模型中,您将简单地传递一个包含模型定义的脚本的路径引用,而不是将对象引用传递给自定义模型的实例。当使用此模式时,MLflow 将在执行环境中简单地执行此脚本(以及在运行主脚本之前的任何 code_paths 依赖项),并实例化您在调用 mlflow.models.set_model() 时定义的任何对象,将该对象分配为推理目标。

在这个过程中,没有任何时候依赖于 picklecloudpickle 这样的序列化库,从而消除了这些序列化包所具有的广泛限制,例如:

  • 可移植性和兼容性: 在不同于序列化对象时使用的Python版本中加载pickle或cloudpickle文件不能保证兼容性。

  • 复杂对象序列化:文件句柄、套接字、外部连接、动态引用、lambda 函数和系统资源无法进行序列化。

  • 可读性: Pickle 和 CloudPickle 都将它们序列化的对象存储为二进制格式,这种格式人类无法阅读。

  • 性能: 对象序列化和依赖检查可能会非常慢,特别是对于具有许多代码引用依赖的复杂实现。

使用代码模型的核心要求

在使用代码模型功能时,有一些重要的概念需要注意,因为在通过脚本记录模型时执行的操作可能并不立即显而易见。

  • 导入:从代码中导入的模型不会捕获非pip可安装包的外部引用,就像旧的 cloudpickle 实现一样。如果你有外部引用(见下面的例子),你必须通过 code_paths 参数定义这些依赖关系。

  • 记录期间的执行:为了验证您正在记录的脚本文件是否有效,代码将在写入磁盘之前执行,与其他模型记录方法完全相同。

  • 需求推断:在您定义的模型脚本顶部导入的包,如果可以从 PyPI 安装,则将被推断为需求,无论您是否在模型执行逻辑中使用它们。

小技巧

如果你定义了在你的脚本中从未使用的导入语句,这些仍然会被包含在需求列表中。建议在编写实现时使用能够确定未使用导入语句的代码检查工具,这样你就不会包含无关的包依赖。

警告

当从代码中记录模型时,请确保您的代码不包含任何敏感信息,例如API密钥、密码或其他机密数据。代码将以纯文本形式存储在MLflow模型工件中,任何有权访问该工件的人都可以查看代码。

在 Jupyter Notebook 中使用代码中的模型

Jupyter (IPython Notebooks) 是处理AI应用和建模的非常方便的方式。它们的一个小限制在于基于单元格的执行模型。由于它们的定义和运行方式的性质,代码模型功能不直接支持将笔记本定义为模型。相反,此功能要求模型被定义为Python脚本(文件扩展名**必须以’.py’结尾**)。

幸运的是,维护 Jupyter 使用的核心内核(IPython)的人们创建了许多可以在笔记本中使用的魔法命令,以增强笔记本作为 AI 从业者开发环境的可用性。在基于 IPython 的任何笔记本环境中(如 JupyterDatabricks Notebooks 等),最有用的魔法命令之一是 %%writefile 命令。

%%writefile 魔法命令,当作为笔记本单元格的第一行时,将捕获单元格的内容(请注意,不是整个笔记本,只是当前单元格的范围),除了魔法命令本身之外,并将这些内容写入您定义的文件中。

例如,在笔记本中运行以下内容:

%%writefile "./hello.py"

print("hello!")

将生成一个文件,该文件位于与您的笔记本相同的目录中,其中包含:

print("hello!")

备注

有一个可选的 -a 追加命令,可以与 %%writefile 魔法命令一起使用。此选项将 追加 单元格内容到目标文件中,该文件用于保存单元格内容。由于在包含多个模型定义逻辑副本的脚本中创建难以调试的覆盖的可能性,不推荐 使用此选项。建议使用 %%writefile 的默认行为,即每次执行单元格时覆盖本地文件,以确保单元格内容的状态始终反映在保存的脚本文件中。

从代码中使用模型的示例

这些示例中的每一个都会在脚本定义单元块的顶部展示 %%writefile 魔法命令的使用,以便模拟在单个笔记本内定义模型代码或其他依赖项。如果你在IDE或文本编辑器中编写实现,请不要将此魔法命令放在脚本顶部。

Building a simple Models From Code model


在这个例子中,我们将定义一个非常基本的模型,当通过 predict() 调用时,将使用输入的浮点值作为数字 2 的指数。第一个代码块,代表一个离散的笔记本单元格,将在与笔记本相同的目录中创建一个名为 basic.py 的文件。该文件的内容将是模型定义 BasicModel,以及导入语句和 MLflow 函数 set_model,该函数将实例化此模型的一个实例以用于推理。

# If running in a Jupyter or Databricks notebook cell, uncomment the following line:
# %%writefile "./basic.py"

import pandas as pd
from typing import List, Dict
from mlflow.pyfunc import PythonModel
from mlflow.models import set_model


class BasicModel(PythonModel):
    def exponential(self, numbers):
        return {f"{x}": 2**x for x in numbers}

    def predict(self, context, model_input) -> Dict[str, float]:
        if isinstance(model_input, pd.DataFrame):
            model_input = model_input.to_dict()[0].values()
        return self.exponential(model_input)


# Specify which definition in this script represents the model instance
set_model(BasicModel())

下一节展示另一个包含日志逻辑的单元格。

import mlflow

mlflow.set_experiment("Basic Model From Code")

model_path = "basic.py"

with mlflow.start_run():
    model_info = mlflow.pyfunc.log_model(
        python_model=model_path,  # Define the model as the path to the script that was just saved
        artifact_path="arithemtic_model",
        input_example=[42.0, 24.0],
    )

在 MLflow UI 中查看这个存储的模型,我们可以看到第一个单元格中的脚本被记录为运行的工件。

显示存储模型代码的MLflow UI,作为序列化的Python脚本

当我们通过 mlflow.pyfunc.load_model() 加载这个模型时,这个脚本将被执行,并且会构造一个 BasicModel 的实例,将 predict 方法作为我们推理的入口点,就像使用自定义模型的替代传统模式记录一样。

my_model = mlflow.pyfunc.load_model(model_info.model_uri)
my_model.predict([2.2, 3.1, 4.7])

# or, with a Pandas DataFrame input
my_model.predict(pd.DataFrame([5.0, 6.0, 7.0]))

代码模型的常见问题解答

在使用代码功能记录模型时,有几个方面是你应该注意的。虽然其行为与使用传统模型序列化的行为相似,但在开发流程和代码架构上,你需要做一些显著的调整。

依赖管理与需求

正确管理依赖项和需求对于确保您的模型可以在新环境中加载或部署至关重要。

为什么在从保存的脚本加载模型时会遇到 NameError?

在定义脚本(或在笔记本中开发时定义单元格)时,确保所有必需的导入语句都在脚本中定义。未能包含导入依赖项不仅会导致名称解析错误,而且需求依赖项也不会包含在模型的 requirements.txt 文件中。

加载我的模型时出现了ImportError。

如果你的模型定义脚本有在 PyPI 上不可用的外部依赖,你必须在使用 code_paths 参数记录或保存模型时包含这些引用。在记录模型时,你可能需要手动将这些外部脚本的导入依赖添加到 extra_pip_requirements 参数中,以确保在加载模型时所有必需的依赖项都可用。

为什么我的 requirements.txt 文件中充满了我的模型没有使用的包?

MLflow 会根据代码脚本中的模块级导入语句构建需求列表。没有检查过程来验证您的模型逻辑是否需要所有声明的导入。强烈建议在这些脚本中修剪您的导入,仅包含模型运行所需的最小导入语句。导入大型包会导致加载或部署模型时安装延迟,以及在部署的推理环境中增加内存压力。

使用代码中的模型进行日志记录

当从定义的Python文件中记录模型时,您将会遇到一些在提供对象引用时旧模型序列化过程之间的细微差异。

我不小心在脚本中包含了一个API密钥。我该怎么办?

由于代码特征库中的模型将您的脚本定义以纯文本形式存储,完全可见于MLflow UI的工件查看器中,包括访问密钥或其他基于授权的敏感数据,这存在安全风险。如果您在记录模型时意外地在脚本中直接定义了敏感密钥,建议您:

  1. 删除包含泄露密钥的 MLflow 运行。你可以通过 UI 或使用 delete_run API 来完成此操作。

  2. 删除与运行相关的工件。您可以通过 mlflow gc cli 命令来执行此操作。

  3. 通过生成新密钥并从源系统管理界面删除泄露的密钥来轮换您的敏感密钥。

  4. 将模型重新记录到一个新的运行中,确保不在模型定义脚本中设置敏感键。

为什么我在记录模型时它会执行?

为了验证代码在定义模型的Python文件中是否可执行,MLflow 将在 set_model API 中实例化定义为模型的对象。如果在模型初始化期间进行了外部调用,这些调用将被执行以确保在记录之前代码是可执行的。如果这些调用需要对服务进行身份验证访问,请确保您从中记录模型的环境已配置了适当的身份验证,以便您的代码可以运行。

附加资源

为了进一步了解MLflow的“从代码生成模型”功能的相关上下文主题,可以考虑探索MLflow文档中的以下部分: