代码生成模型指南
注意
从代码生成的模型在 MLflow 2.12.2 及以上版本中可用。如果您使用的版本早于支持此功能的版本,则需要使用 自定义 Python 模型 文档中概述的传统序列化方法。
备注
从代码生成的模型仅适用于 LangChain、LlamaIndex 和自定义 ``pyfunc``(PythonModel 实例)模型。如果您直接使用其他库,建议使用特定模型风格中提供的保存和记录功能。
代码模型功能是对定义、存储和加载自定义模型以及不依赖于序列化模型权重的特定风格实现(如 LangChain 和 LlamaIndex)过程的全面重构。
这些模型在传统序列化与代码生成模型方法之间的关键区别在于模型在序列化过程中如何表示。
在传统方法中,序列化是通过使用 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()
时定义的任何对象,将该对象分配为推理目标。
在这个过程中,没有任何时候依赖于 pickle 或 cloudpickle 这样的序列化库,从而消除了这些序列化包所具有的广泛限制,例如:
可移植性和兼容性: 在不同于序列化对象时使用的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 的任何笔记本环境中(如 Jupyter
、Databricks 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.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]))
Using Models from Code with code_paths dependencies
在这个示例中,我们将探讨一个更复杂的场景,展示如何使用多个Python脚本并利用MLflow中的``code_paths``功能进行模型管理。具体来说,我们将定义一个包含执行基本算术运算函数的简单脚本,然后在另一个脚本中使用这个函数定义一个自定义的``AddModel`` PythonModel
。这个模型将被记录在MLflow中,使我们能够使用存储的模型进行预测。
要了解更多关于 MLflow 中的
code_paths
功能,请参阅 这里的使用指南。
本教程将向您展示如何:
从 Jupyter 笔记本中创建多个 Python 文件。
使用 MLflow 记录一个自定义模型,该模型依赖于在另一个文件中定义的外部代码。
使用
code_paths
功能在记录模型时包含额外的脚本,确保在加载模型进行推理时所有依赖项都可用。
Defining a dependent code script
在第一步中,我们在名为 calculator.py
的文件中定义我们的 add
函数,如果在笔记本单元中运行,则包括魔法命令 %%writefile
:
# If running in a Jupyter or Databricks notebook cell, uncomment the following line:
# %%writefile "./calculator.py"
def add(x, y):
return x + y
Defining the model as a Python file
接下来,我们创建一个新文件 math_model.py
,其中包含 AddModel
类。该脚本将负责从外部脚本导入 add
函数,定义我们的模型,执行预测,并验证输入数据类型。predict 方法将利用 add
函数来执行输入的两个数字的加法。
以下代码块将 AddModel
类定义写入 math_model.py
:
# If running in a Jupyter or Databricks notebook cell, uncomment the following line:
# %%writefile "./math_model.py"
from mlflow.pyfunc import PythonModel
from mlflow.models import set_model
from calculator import add
class AddModel(PythonModel):
def predict(self, context, model_input, params=None):
return add(model_input["x"], model_input["y"])
set_model(AddModel())
该模型通过检查输入的存在性和类型来引入错误处理,确保了鲁棒性。它作为一个实际的例子,展示了如何在利用外部依赖的同时,将自定义逻辑封装在MLflow模型中。
Logging the Model from Code
一旦定义了 AddModel
自定义 Python 模型,我们就可以使用 MLflow 记录它。这个过程包括指定 math_model.py
脚本的路径,并使用 code_paths
参数将 calculator.py
作为依赖项包含进来。这确保了当模型在不同的环境中或另一台机器上加载时,所有必要的代码文件都可用以正确执行。
以下代码块演示了如何使用 MLflow 记录模型:
import mlflow
mlflow.set_experiment("Arithemtic Model From Code")
model_path = "math_model.py"
with mlflow.start_run():
model_info = mlflow.pyfunc.log_model(
python_model=model_path, # The model is defined as the path to the script containing the model definition
artifact_path="arithemtic_model",
code_paths=[
"calculator.py"
], # dependency definition included for the model to successfully import the implementation
)
此步骤将 AddModel
模型注册到 MLflow,确保主模型脚本及其依赖项都作为工件存储。通过在 code_paths
参数中包含 calculator.py
,我们确保模型可以可靠地重新加载并用于预测,无论其在何种环境中部署。
Loading and Viewing the model
在记录模型后,可以将其加载回笔记本或任何有权访问MLflow跟踪服务器的其他环境中。当模型被加载时,``calculator.py``脚本将与``math_model.py``脚本一起执行,确保``add``函数可用于``ArithmeticModel``脚本的导入语句。
以下代码块展示了如何加载模型并进行预测:
my_model_from_code = mlflow.pyfunc.load_model(model_info.model_uri)
my_model_from_code.predict({"x": 42, "y": 9001})
这个例子展示了模型处理不同数值输入、执行加法并保持计算历史的能力。这些预测的输出包括算术运算的结果和历史日志,这对于审计和追踪模型执行的计算非常有用。
在 MLflow UI 中查看存储的模型,你可以看到 math_model.py
和 calculator.py
脚本都被记录为运行中的工件。这种全面的日志记录不仅允许你跟踪模型的参数和指标,还可以跟踪定义其行为的代码,使其可以直接在 UI 中可见和可调试。
MLflow's native LangChain Models from Code support
在这个稍微高级的示例中,我们将探讨如何使用 MLflow LangChain 集成 来定义和管理一个 AI 模型的操作链。这个链将基于特定的区域和区域输入生成景观设计建议。该示例展示了如何定义自定义提示,使用大型语言模型(LLM)生成响应,并使用 MLflow 的跟踪功能将整个设置记录为模型。
本教程将引导您完成:
编写一个脚本来定义一个自定义的 LangChain 模型,该模型处理输入数据以生成景观设计建议。
使用 langchain 集成通过 MLflow 记录模型,确保捕获整个操作链。
在不同情境下加载并使用记录的模型进行预测。
Defining the Model with LCEL
首先,我们将创建一个名为 mfc.py
的 Python 脚本,该脚本定义了生成景观设计建议的操作链。此脚本利用了 LangChain 库以及 MLflow 的 autolog
功能,以启用 捕获跟踪。
在这个脚本中:
自定义函数 (get_region 和 get_area): 这些函数从输入数据中提取特定信息(区域和面积)。
提示模板:
PromptTemplate
被定义为构建语言模型输入的结构,指定模型将执行的任务和上下文。模型定义:我们使用
ChatOpenAI
模型根据结构化的提示生成响应。链创建: 链是通过连接输入处理、提示模板、模型调用和输出解析步骤来创建的。
以下代码块将此链定义写入 mfc.py 文件:
# If running in a Jupyter or Databricks notebook cell, uncomment the following line:
# %%writefile "./mfc.py"
import os
from operator import itemgetter
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnableLambda
from langchain_openai import ChatOpenAI
import mlflow
def get_region(input_data):
default = "Virginia, USA"
if isinstance(input_data[0], dict):
return input_data[0].get("content").get("region", default)
return default
def get_area(input_data):
default = "5000 square feet"
if isinstance(input_data[0], dict):
return input_data[0].get("content").get("area", default)
return default
prompt = PromptTemplate(
template="You are a highly accomplished landscape designer that provides suggestions for landscape design decisions in a particular"
" geographic region. Your goal is to suggest low-maintenance hardscape and landscape options that involve the use of materials and"
" plants that are native to the region mentioned. As part of the recommendations, a general estimate for the job of creating the"
" project should be provided based on the square footage estimate. The region is: {region} and the square footage estimate is:"
" {area}. Recommendations should be for a moderately sophisticated suburban housing community within the region.",
input_variables=["region", "area"],
)
model = ChatOpenAI(model="gpt-4o", temperature=0.95, max_tokens=4096)
chain = (
{
"region": itemgetter("messages") | RunnableLambda(get_region),
"area": itemgetter("messages") | RunnableLambda(get_area),
}
| prompt
| model
| StrOutputParser()
)
mlflow.models.set_model(chain)
此脚本封装了使用 LangChain 表达式语言 (LCEL) 构建完整链所需的逻辑,以及链将用于输入处理的定制默认逻辑。然后,使用 set_model
函数将定义的链指定为模型的接口对象。
Logging the model using Models from Code
一旦在 mfc.py
中定义了链,我们使用 MLflow 记录它。这一步包括指定包含链定义的脚本路径,并使用 MLflow 的 langchain
集成来确保捕获链的所有方面。
提供给日志记录函数的 input_example
作为模板,展示了如何调用模型。此示例也作为日志记录模型的一部分存储,使得理解和复制模型的使用场景变得更加容易。
以下代码块展示了如何使用 MLflow 记录 LangChain 模型:
import mlflow
mlflow.set_experiment("Landscaping")
chain_path = "./mfc.py"
input_example = {
"messages": [
{
"role": "user",
"content": {
"region": "Austin, TX, USA",
"area": "1750 square feet",
},
}
]
}
with mlflow.start_run():
info = mlflow.langchain.log_model(
lc_model=chain_path, # Defining the model as the script containing the chain definition and the set_model call
artifact_path="chain",
input_example=input_example,
)
在这一步中,从输入处理到AI模型推理的整个操作链被记录为一个单一的、连贯的模型。通过使用代码中的模型功能,避免了与定义的链组件对象序列化相关的潜在复杂性,确保在部署应用程序时执行的正是用于开发和测试链的确切代码和逻辑,而不会面临序列化能力不完整或不存在风险。
Loading and Viewing the Model
在记录模型之后,可以将其加载回您的环境中进行推理。此步骤演示了如何加载链并使用它根据新输入数据生成景观设计建议。
以下代码块展示了如何加载模型并运行预测:
# Load the model and run inference
landscape_chain = mlflow.langchain.load_model(model_uri=info.model_uri)
question = {
"messages": [
{
"role": "user",
"content": {
"region": "Raleigh, North Carolina USA",
"area": "3850 square feet",
},
},
]
}
response = landscape_chain.invoke(question)
此代码块演示了如何使用新数据调用加载的链,生成一个响应,该响应提供根据指定地区和面积定制的景观设计建议。
模型记录后,您可以在 MLflow UI 中探索其详细信息。界面将显示脚本 mfc.py
作为记录模型的工件,以及链定义和相关元数据。这使您可以轻松查看模型的组件、输入示例和其他关键信息。
当你使用 mlflow.langchain.load_model()
加载此模型时,mfc.py
中定义的整个链将被执行,模型将按预期运行,生成用于景观设计的 AI 驱动推荐。
代码模型的常见问题解答
在使用代码功能记录模型时,有几个方面是你应该注意的。虽然其行为与使用传统模型序列化的行为相似,但在开发流程和代码架构上,你需要做一些显著的调整。
依赖管理与需求
正确管理依赖项和需求对于确保您的模型可以在新环境中加载或部署至关重要。
为什么在从保存的脚本加载模型时会遇到 NameError?
在定义脚本(或在笔记本中开发时定义单元格)时,确保所有必需的导入语句都在脚本中定义。未能包含导入依赖项不仅会导致名称解析错误,而且需求依赖项也不会包含在模型的 requirements.txt
文件中。
加载我的模型时出现了ImportError。
如果你的模型定义脚本有在 PyPI 上不可用的外部依赖,你必须在使用 code_paths
参数记录或保存模型时包含这些引用。在记录模型时,你可能需要手动将这些外部脚本的导入依赖添加到 extra_pip_requirements
参数中,以确保在加载模型时所有必需的依赖项都可用。
为什么我的 requirements.txt 文件中充满了我的模型没有使用的包?
MLflow 会根据代码脚本中的模块级导入语句构建需求列表。没有检查过程来验证您的模型逻辑是否需要所有声明的导入。强烈建议在这些脚本中修剪您的导入,仅包含模型运行所需的最小导入语句。导入大型包会导致加载或部署模型时安装延迟,以及在部署的推理环境中增加内存压力。
使用代码中的模型进行日志记录
当从定义的Python文件中记录模型时,您将会遇到一些在提供对象引用时旧模型序列化过程之间的细微差异。
我不小心在脚本中包含了一个API密钥。我该怎么办?
由于代码特征库中的模型将您的脚本定义以纯文本形式存储,完全可见于MLflow UI的工件查看器中,包括访问密钥或其他基于授权的敏感数据,这存在安全风险。如果您在记录模型时意外地在脚本中直接定义了敏感密钥,建议您:
删除包含泄露密钥的 MLflow 运行。你可以通过 UI 或使用 delete_run API 来完成此操作。
删除与运行相关的工件。您可以通过 mlflow gc cli 命令来执行此操作。
通过生成新密钥并从源系统管理界面删除泄露的密钥来轮换您的敏感密钥。
将模型重新记录到一个新的运行中,确保不在模型定义脚本中设置敏感键。
为什么我在记录模型时它会执行?
为了验证代码在定义模型的Python文件中是否可执行,MLflow 将在 set_model
API 中实例化定义为模型的对象。如果在模型初始化期间进行了外部调用,这些调用将被执行以确保在记录之前代码是可执行的。如果这些调用需要对服务进行身份验证访问,请确保您从中记录模型的环境已配置了适当的身份验证,以便您的代码可以运行。
附加资源
为了进一步了解MLflow的“从代码生成模型”功能的相关上下文主题,可以考虑探索MLflow文档中的以下部分: