跳至主内容

从代码构建模型

attention

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

note

从代码生成模型仅适用于LangChainLlamaIndex以及使用pyfunc编写的自定义Python智能体或GenAI应用。对于其他用例(例如使用xgboost的传统机器学习),如果您直接使用ML库,建议使用相应模型风格中的保存和日志记录功能。

代码生成模型功能是对定义、存储和加载自定义模型及不依赖序列化模型权重的特定flavor实现(如LangChainLlamaIndex)流程的全面革新。如果您正在编写自定义Python模型或GenAI智能体/应用,您应该使用代码生成模型。

代码模型与传统模型序列化的关键区别在于模型在序列化过程中的表示方式。

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

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

使用代码中的模型来实现自定义pyfunc和支持库的最大优势在于减少了重复试错调试的工作量,这在开发实现过程中经常发生。下方展示的工作流程说明了这两种方法在开发自定义模型解决方案时的对比情况:

Models from code comparison with legacy serialization

与传统序列化的差异

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

LangChain的原生序列化实现中,使用cloudpickle来存储对象引用。然而,由于外部状态引用或在API中使用lambda函数,只有部分能在LangChain中使用的对象类型可以进行序列化。另一方面,LlamaIndex在其原生实现中使用了一个自定义序列化器,由于需要极其复杂的实现来支持库中的边缘情况功能,该序列化器并未涵盖库的所有可能用途。

在基于代码的模型中,您无需传递对自定义模型实例的对象引用,只需传递包含模型定义的脚本路径引用。当采用此模式时,MLflow会在执行环境中先运行该脚本(以及code_paths依赖项),然后在调用mlflow.models.set_model()时实例化您定义的任何对象,并将该对象指定为推理目标。

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

  • 可移植性与兼容性: 在不同Python版本中加载pickle或cloudpickle文件时,若该版本与序列化对象时使用的Python版本不一致,则无法保证兼容性。
  • 复杂对象序列化: 文件句柄、套接字、外部连接、动态引用、lambda函数和系统资源无法进行pickle序列化。
  • 可读性: Pickle和CloudPickle都将它们的序列化对象存储在人类无法读取的二进制格式中。
  • 性能: 对象序列化和依赖项检查可能非常缓慢,特别是对于具有许多代码引用依赖项的复杂实现。

使用代码模型的核心要求

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

  • 导入: 从代码生成的模型不会捕获非pip可安装包的外部引用,就像传统的cloudpickle实现一样。如果您有外部引用(请参阅下面的示例),必须通过code_paths参数定义这些依赖项。
  • 记录期间的执行: 为了验证您正在记录的脚本文件是有效的,代码将在写入磁盘之前被执行,与其他模型记录方法完全一致。
  • 依赖推断: 在您定义的模型脚本顶部导入的包,如果可以从PyPI安装,无论您是否在模型执行逻辑中使用它们,都将被推断为依赖项。
tip

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

warning

在记录来自代码的模型时,请确保您的代码不包含任何敏感信息,例如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!")
note

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

从代码中使用模型的示例

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

从代码构建简单模型

在本示例中,我们将定义一个非常基础的模型,当通过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="arithmetic_model",
input_example=[42.0, 24.0],
)

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

The MLflow UI showing the stored model code as a serialized python script

当我们通过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运行记录。您可以通过用户界面或delete_run API来完成此操作。
  2. 删除与该运行关联的工件。您可以通过mlflow gc命令行工具完成此操作。
  3. 通过生成新密钥并从源系统管理界面删除泄露的密钥来轮换您的敏感密钥。
  4. 将模型重新记录到新的运行中,确保不在模型定义脚本中设置敏感密钥。

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

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

其他资源

如需了解更多与MLflow"从代码构建模型"功能相关的背景知识,建议查阅MLflow文档中的以下章节: