从代码构建模型
从代码生成模型的功能在MLflow 2.12.2及以上版本中可用。如果您使用的版本早于支持此功能的版本,则需要使用自定义Python模型文档中概述的传统序列化方法。
从代码生成模型仅适用于LangChain、LlamaIndex以及使用pyfunc
编写的自定义Python智能体或GenAI应用。对于其他用例(例如使用xgboost的传统机器学习),如果您直接使用ML库,建议使用相应模型风格中的保存和日志记录功能。
代码生成模型功能是对定义、存储和加载自定义模型及不依赖序列化模型权重的特定flavor实现(如LangChain和LlamaIndex)流程的全面革新。如果您正在编写自定义Python模型或GenAI智能体/应用,您应该使用代码生成模型。
代码模型与传统模型序列化的关键区别在于模型在序列化过程中的表示方式。
在传统方法中,模型对象的序列化是通过cloudpickle
(自定义pyfunc和LangChain)或一个覆盖底层包所有功能不完整(在LlamaIndex的情况下)的自定义序列化器完成的。对于自定义pyfunc,使用cloudpickle
序列化对象实例会创建一个二进制文件,该文件在加载时用于重建对象。
在代码生成的模型中,对于受支持的模型类型,会保存一个简单的脚本,其中包含自定义pyfunc或特定flavor接口的定义(例如,在LangChain的情况下,我们可以直接在脚本中定义并将LCEL链标记为模型)。
使用代码中的模型来实现自定义pyfunc
和支持库的最大优势在于减少了重复试错调试的工作量,这在开发实现过程中经常发生。下方展示的工作流程说明了这两种方法在开发自定义模型解决方案时的对比情况:
与传统序列化的差异
在自定义模型的传统模式下,调用log_model
时会提交您子类化的mlflow.pyfunc.PythonModel
实例。当通过对象引用调用时,MLflow将使用cloudpickle
尝试序列化您的对象。
在LangChain
的原生序列化实现中,使用cloudpickle
来存储对象引用。然而,由于外部状态引用或在API中使用lambda函数,只有部分能在LangChain
中使用的对象类型可以进行序列化。另一方面,LlamaIndex
在其原生实现中使用了一个自定义序列化器,由于需要极其复杂的实现来支持库中的边缘情况功能,该序列化器并未涵盖库的所有可能用途。
在基于代码的模型中,您无需传递对自定义模型实例的对象引用,只需传递包含模型定义的脚本路径引用。当采用此模式时,MLflow会在执行环境中先运行该脚本(以及code_paths
依赖项),然后在调用mlflow.models.set_model()
时实例化您定义的任何对象,并将该对象指定为推理目标。
在整个过程中,没有任何环节依赖于pickle或cloudpickle等序列化库,从而消除了这些序列化包所具有的广泛限制,例如:
- 可移植性与兼容性: 在不同Python版本中加载pickle或cloudpickle文件时,若该版本与序列化对象时使用的Python版本不一致,则无法保证兼容性。
- 复杂对象序列化: 文件句柄、套接字、外部连接、动态引用、lambda函数和系统资源无法进行pickle序列化。
- 可读性: 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或文本编辑器中编写实现,请不要在脚本顶部添加此魔法命令。
- 简单示例
- 带有代码路径依赖的模型
- 使用LangChain从代码构建模型
从代码构建简单模型
在本示例中,我们将定义一个非常基础的模型,当通过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中查看这个存储的模型时,我们可以看到第一个单元格中的脚本被记录为该运行的工件。
当我们通过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]))
在代码中使用带有code_paths依赖项的模型
在本示例中,我们将探索一个更复杂的场景,展示如何处理多个Python脚本并利用MLflow中的code_paths
功能进行模型管理。具体而言,我们将定义一个包含基本算术运算函数的简单脚本,然后在一个单独脚本中定义的AddModel
自定义PythonModel
中使用该函数。这个模型将通过MLflow进行记录,使我们能够使用存储的模型执行预测。
要了解更多关于MLflow中code_paths
功能的信息,请参阅此处使用指南。
本教程将向您展示如何:
- 在Jupyter笔记本中创建多个Python文件。
- 使用MLflow记录一个自定义模型,该模型依赖于另一个文件中定义的外部代码。
- 使用
code_paths
功能在记录模型时包含额外脚本,确保在加载模型进行推理时所有依赖项都可用。
定义依赖代码脚本
第一步,我们在名为calculator.py
的文件中定义add
函数,如果是在notebook单元格中运行,还需包含魔法命令%%writefile
:
# If running in a Jupyter or Databricks notebook cell, uncomment the following line:
# %%writefile "./calculator.py"
def add(x, y):
return x + y
将模型定义为Python文件
接下来,我们创建一个新文件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模型中封装自定义逻辑,同时利用外部依赖。
从代码中记录模型
一旦定义了AddModel
自定义Python模型,我们就可以继续使用MLflow记录它。这个过程包括指定math_model.py
脚本的路径,并使用code_paths
参数将calculator.py
作为依赖项包含进来。这样可以确保当模型在不同环境或其他机器上加载时,所有必要的代码文件都可用于正确执行。
以下代码块展示了如何使用MLflow记录模型:
import mlflow
mlflow.set_experiment("Arithmetic 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="arithmetic_model",
code_paths=[
"calculator.py"
], # dependency definition included for the model to successfully import the implementation
)
此步骤将AddModel
模型注册到MLflow中,确保主模型脚本及其依赖项都作为工件存储。通过在code_paths
参数中包含calculator.py
,我们确保无论部署在何种环境下,该模型都能被可靠地重新加载并用于预测。
加载并查看模型
在记录模型后,可以将其加载回笔记本或任何可以访问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原生支持的LangChain代码模型
在这个稍微进阶的示例中,我们将探索如何使用MLflow LangChain集成来定义和管理AI模型的操作链。该操作链将帮助根据特定区域和基于区域的输入生成景观设计建议。本示例展示了如何定义自定义提示、使用大型语言模型(LLM)生成响应,并通过MLflow的跟踪功能将整个设置记录为模型。
本教程将引导您了解:
- 编写一个脚本来定义自定义LangChain模型,该模型处理输入数据以生成景观设计推荐。
- 使用MLflow的langchain集成记录模型,确保捕获整个操作链。
- 加载并使用已记录的模型在不同场景中进行预测。
使用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
函数将定义好的链指定为模型的接口对象。
使用代码中的模型记录模型
一旦在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模型推理的完整操作链将被记录为一个统一的模型。通过使用代码中的模型特性,可以避免定义链式组件时可能遇到的对象序列化复杂性,确保开发和测试链时使用的精确代码与逻辑在部署应用时能完整执行,消除了因序列化功能缺失或不完整带来的风险。
加载并查看模型
记录模型后,可以将其加载回您的环境进行推理。此步骤演示如何加载链并使用它基于新的输入数据生成景观设计推荐。
以下代码块展示了如何加载模型并运行预测:
# 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运行记录。您可以通过用户界面或delete_run API来完成此操作。
- 删除与该运行关联的工件。您可以通过mlflow gc命令行工具完成此操作。
- 通过生成新密钥并从源系统管理界面删除泄露的密钥来轮换您的敏感密钥。
- 将模型重新记录到新的运行中,确保不在模型定义脚本中设置敏感密钥。
为什么在记录模型时它会执行?
为了验证定义模型的Python文件中的代码是可执行的,MLflow会实例化在set_model
API中定义为模型的对象。如果在模型初始化期间进行了外部调用,这些调用将被执行以确保在记录之前代码是可执行的。如果此类调用需要对服务进行身份验证访问,请确保记录模型的环境已配置适当的身份验证,以便代码可以运行。
其他资源
如需了解更多与MLflow"从代码构建模型"功能相关的背景知识,建议查阅MLflow文档中的以下章节: