MLflow 签名游乐场笔记本

欢迎来到 MLflow 签名游乐场!这个交互式 Jupyter 笔记本旨在引导您了解 MLflow 生态系统中的 模型签名 基础概念。随着您在笔记本中的进展,您将获得定义、执行和利用模型签名的实际经验——这是增强模型管理可重复性、可靠性和易用性的关键方面。

为什么模型签名很重要

在机器学习领域,精确地定义模型的输入和输出是确保操作顺畅的关键。模型签名作为模型期望和生成的数据的架构定义,为模型开发者和用户提供了蓝图。这不仅明确了期望,还促进了自动验证检查,简化了从模型训练到部署的过程。

签名执行在行动中

通过探索本笔记本中的代码单元格,您将亲眼见证模型签名如何强制执行数据完整性、防止常见错误,并在出现差异时提供描述性反馈。这对于维护模型输入的质量和一致性至关重要,尤其是在模型在生产环境中运行时。

深入理解的实用示例

该笔记本包含一系列示例,展示了从简单标量到复杂嵌套字典的不同数据类型和结构。这些示例展示了如何推断、记录和更新签名,为您提供对签名生命周期的全面理解。当您与提供的 PythonModel 实例交互并调用其 predict 方法时,您将学习如何处理各种输入场景——包括必需和可选的数据字段——以及如何更新现有模型以包含详细的签名。无论您是希望优化模型管理实践的数据科学家,还是将 MLflow 集成到工作流程中的开发者,这个笔记本都是您掌握模型签名的沙盒。让我们深入探索 MLflow 签名的强大功能吧!

注意:本笔记本中展示的几个功能仅在 MLflow 2.10.0 及以上版本中可用。特别是,ArrayObject 类型的支持在 2.10.0 之前的版本中不可用。

[1]:
import numpy as np
import pandas as pd

import mlflow
from mlflow.models.signature import infer_signature, set_signature


def report_signature_info(input_data, output_data=None, params=None):
    inferred_signature = infer_signature(input_data, output_data, params)

    report = f"""
The input data: \n\t{input_data}.
The data is of type: {type(input_data)}.
The inferred signature is:\n\n{inferred_signature}
"""
    print(report)

MLflow 签名中的标量支持

在本教程的这一部分中,我们探讨了标量数据类型在MLflow模型签名中的关键作用。标量类型,如字符串、整数、浮点数、双精度数、布尔值和日期时间,是定义模型输入和输出模式的基础。准确表示这些类型对于确保模型正确处理数据至关重要,这直接影响到预测的可靠性和准确性。

通过检查各种标量类型的示例,本节展示了MLflow如何推断和记录数据的结构和性质。我们将看到MLflow签名如何适应不同的标量类型,确保输入模型的数据符合预期格式。这种理解对于任何机器学习从业者都至关重要,因为它有助于准备和验证数据输入,从而实现更顺畅的模型操作和更可靠的结果。

通过实际示例,包括字符串、浮点数和其他类型的列表,我们展示了MLflow的 infer_signature 函数如何准确推断数据格式。这种能力是MLflow处理多样数据输入的基石,并为机器学习模型中更复杂的数据结构奠定了基础。在本节结束时,您将对标量数据在MLflow签名中的表示方式及其对您的ML项目的重要性有一个清晰的认识。

[2]:
# List of strings

report_signature_info(["a", "list", "of", "strings"])

The input data:
        ['a', 'list', 'of', 'strings'].
The data is of type: <class 'list'>.
The inferred signature is:

inputs:
  [string (required)]
outputs:
  None
params:
  None


[3]:
# List of floats

report_signature_info([np.float32(0.117), np.float32(1.99)])

The input data:
        [0.117, 1.99].
The data is of type: <class 'list'>.
The inferred signature is:

inputs:
  [float (required)]
outputs:
  None
params:
  None


[4]:
# Adding a column header to a list of doubles
my_data = pd.DataFrame({"input_data": [np.float64(0.117), np.float64(1.99)]})
report_signature_info(my_data)

The input data:
           input_data
0       0.117
1       1.990.
The data is of type: <class 'pandas.core.frame.DataFrame'>.
The inferred signature is:

inputs:
  ['input_data': double (required)]
outputs:
  None
params:
  None


[5]:
# List of Dictionaries
report_signature_info([{"a": "a1", "b": "b1"}, {"a": "a2", "b": "b2"}])

The input data:
        [{'a': 'a1', 'b': 'b1'}, {'a': 'a2', 'b': 'b2'}].
The data is of type: <class 'list'>.
The inferred signature is:

inputs:
  ['a': string (required), 'b': string (required)]
outputs:
  None
params:
  None


[6]:
# List of Arrays of strings
report_signature_info([["a", "b", "c"], ["d", "e", "f"]])

The input data:
        [['a', 'b', 'c'], ['d', 'e', 'f']].
The data is of type: <class 'list'>.
The inferred signature is:

inputs:
  [Array(string) (required)]
outputs:
  None
params:
  None


[7]:
# List of Arrays of Dictionaries
report_signature_info(
    [[{"a": "a", "b": "b"}, {"a": "a", "b": "b"}], [{"a": "a", "b": "b"}, {"a": "a", "b": "b"}]]
)

The input data:
        [[{'a': 'a', 'b': 'b'}, {'a': 'a', 'b': 'b'}], [{'a': 'a', 'b': 'b'}, {'a': 'a', 'b': 'b'}]].
The data is of type: <class 'list'>.
The inferred signature is:

inputs:
  [Array({a: string (required), b: string (required)}) (required)]
outputs:
  None
params:
  None


理解类型转换:从整数到长整数

在本教程的这一部分中,我们观察到 MLflow 的架构推断中类型转换的一个有趣方面。当报告整数列表的签名信息时,你可能会注意到推断的数据类型是 long 而不是 int。这种从 int 到 long 的转换不是错误或漏洞,而是 MLflow 架构推断机制中有效且有意的类型转换。

为什么整数被推断为长整型

  • 更广泛的兼容性: 转换为 long 确保了在各种平台和系统上的兼容性。由于整数 (int) 的大小可能因系统架构而异,使用 ``long``(具有更一致的大小规范)可以避免潜在的不一致性和数据溢出问题。

  • 数据完整性: 通过将整数推断为长整型,MLflow 确保了较大的整数值,这些值可能超过 int 的典型容量,能够被准确表示和处理,而不会丢失数据或发生溢出。

  • 机器学习模型中的一致性: 在许多机器学习框架中,尤其是涉及较大数据集或计算的框架,长整数通常是数值操作的标准数据类型。这种推断模式中的标准化与机器学习社区中的常见做法相一致。

[8]:
# List of integers
report_signature_info([1, 2, 3])

The input data:
        [1, 2, 3].
The data is of type: <class 'list'>.
The inferred signature is:

inputs:
  [long (required)]
outputs:
  None
params:
  None


/Users/benjamin.wilson/repos/mlflow-fork/mlflow/mlflow/types/utils.py:378: UserWarning: Hint: Inferred schema contains integer column(s). Integer columns in Python cannot represent missing values. If your input data contains missing values at inference time, it will be encoded as floats and will cause a schema enforcement error. The best way to avoid this problem is to infer the model schema based on a realistic data sample (training dataset) that includes missing values. Alternatively, you can declare integer columns as doubles (float64) whenever these columns may have missing values. See `Handling Integers With Missing Values <https://www.mlflow.org/docs/latest/models.html#handling-integers-with-missing-values>`_ for more details.
  warnings.warn(
[9]:
# List of Booleans
report_signature_info([True, False, False, False, True])

The input data:
        [True, False, False, False, True].
The data is of type: <class 'list'>.
The inferred signature is:

inputs:
  [boolean (required)]
outputs:
  None
params:
  None


[10]:
# List of Datetimes
report_signature_info([np.datetime64("2023-12-24 11:59:59"), np.datetime64("2023-12-25 00:00:00")])

The input data:
        [numpy.datetime64('2023-12-24T11:59:59'), numpy.datetime64('2023-12-25T00:00:00')].
The data is of type: <class 'list'>.
The inferred signature is:

inputs:
  [datetime (required)]
outputs:
  None
params:
  None


[11]:
# Complex list of Dictionaries
report_signature_info([{"a": "b", "b": [1, 2, 3], "c": {"d": [4, 5, 6]}}])

The input data:
        [{'a': 'b', 'b': [1, 2, 3], 'c': {'d': [4, 5, 6]}}].
The data is of type: <class 'list'>.
The inferred signature is:

inputs:
  ['a': string (required), 'b': Array(long) (required), 'c': {d: Array(long) (required)} (required)]
outputs:
  None
params:
  None


[12]:
# Pandas DF input

data = [
    {"a": "a", "b": ["a", "b", "c"], "c": {"d": 1, "e": 0.1}, "f": [{"g": "g"}, {"h": 1}]},
    {"b": ["a", "b"], "c": {"d": 2, "f": "f"}, "f": [{"g": "g"}]},
]
data = pd.DataFrame(data)

report_signature_info(data)

The input data:
             a          b                   c                       f
0    a  [a, b, c]  {'d': 1, 'e': 0.1}  [{'g': 'g'}, {'h': 1}]
1  NaN     [a, b]  {'d': 2, 'f': 'f'}            [{'g': 'g'}].
The data is of type: <class 'pandas.core.frame.DataFrame'>.
The inferred signature is:

inputs:
  ['a': string (optional), 'b': Array(string) (required), 'c': {d: long (required), e: double (optional), f: string (optional)} (required), 'f': Array({g: string (optional), h: long (optional)}) (required)]
outputs:
  None
params:
  None


签名强制执行

在本教程的这一部分,我们重点介绍MLflow中签名强制的实际应用。签名强制是一个强大的功能,确保提供给模型的数据与定义的输入模式一致。这一步骤对于防止因数据不匹配或格式错误而导致的错误和不一致性至关重要。

通过动手示例,我们将观察MLflow如何在运行时强制数据符合预期的签名。我们将使用``MyModel``类,一个简单的Python模型,来演示MLflow如何检查输入数据与模型签名的兼容性。这个过程有助于保护模型免受不兼容或错误输入的影响,从而增强模型预测的健壮性和可靠性。

本节还强调了精确数据表示在 MLflow 中的重要性及其对模型性能的影响。通过测试不同类型的数据,包括那些不符合预期模式的数据,我们将看到 MLflow 如何验证数据并提供有用的反馈。这种签名执行的方面对于调试数据问题和优化模型输入是无价的,使其成为任何参与部署机器学习模型的人的关键技能。

[13]:
class MyModel(mlflow.pyfunc.PythonModel):
    def predict(self, context, model_input, params=None):
        return model_input
[14]:
data = [{"a": ["a", "b", "c"], "b": "b", "c": {"d": "d"}}, {"a": ["a"], "c": {"d": "d", "e": "e"}}]

report_signature_info(data)

The input data:
        [{'a': ['a', 'b', 'c'], 'b': 'b', 'c': {'d': 'd'}}, {'a': ['a'], 'c': {'d': 'd', 'e': 'e'}}].
The data is of type: <class 'list'>.
The inferred signature is:

inputs:
  ['a': Array(string) (required), 'b': string (optional), 'c': {d: string (required), e: string (optional)} (required)]
outputs:
  None
params:
  None


[15]:
# Generate a prediction that will serve as the model output example for signature inference
model_output = MyModel().predict(context=None, model_input=data)

with mlflow.start_run():
    model_info = mlflow.pyfunc.log_model(
        python_model=MyModel(),
        artifact_path="test_model",
        signature=infer_signature(model_input=data, model_output=model_output),
    )

loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)
prediction = loaded_model.predict(data)

prediction
/Users/benjamin.wilson/miniconda3/envs/mlflow-dev-env/lib/python3.8/site-packages/_distutils_hack/__init__.py:30: UserWarning: Setuptools is replacing distutils.
  warnings.warn("Setuptools is replacing distutils.")
[15]:
a b c
0 [a, b, c] b {'d': 'd'}
1 [a] NaN {'d': 'd', 'e': 'e'}

我们可以直接从调用 log_model() 返回的已记录模型信息中检查推断的签名。

[16]:
model_info.signature
[16]:
inputs:
  ['a': Array(string) (required), 'b': string (optional), 'c': {d: string (required), e: string (optional)} (required)]
outputs:
  ['a': Array(string) (required), 'b': string (optional), 'c': {d: string (required), e: string (optional)} (required)]
params:
  None

我们还可以快速验证记录的输入签名是否与签名推断匹配。与此同时,我们也可以生成输出签名。

注意:建议在您的模型中同时记录输入和输出签名。

[17]:
report_signature_info(data, prediction)

The input data:
        [{'a': ['a', 'b', 'c'], 'b': 'b', 'c': {'d': 'd'}}, {'a': ['a'], 'c': {'d': 'd', 'e': 'e'}}].
The data is of type: <class 'list'>.
The inferred signature is:

inputs:
  ['a': Array(string) (required), 'b': string (optional), 'c': {d: string (required), e: string (optional)} (required)]
outputs:
  ['a': Array(string) (required), 'b': string (optional), 'c': {d: string (required), e: string (optional)} (required)]
params:
  None


[18]:
# Using the model while not providing an optional input (note the output return structure and the non existent optional columns)

loaded_model.predict([{"a": ["a", "b", "c"], "c": {"d": "d"}}])
[18]:
a c
0 [a, b, c] {'d': 'd'}
[19]:
# Using the model while omitting the input of required fields (this will raise an Exception from schema enforcement,
# stating that the required fields "a" and "c" are missing)

loaded_model.predict([{"b": "b"}])
---------------------------------------------------------------------------
MlflowException                           Traceback (most recent call last)
~/repos/mlflow-fork/mlflow/mlflow/pyfunc/__init__.py in predict(self, data, params)
    469             try:
--> 470                 data = _enforce_schema(data, input_schema)
    471             except Exception as e:

~/repos/mlflow-fork/mlflow/mlflow/models/utils.py in _enforce_schema(pf_input, input_schema)
    939                 message += f" Note that there were extra inputs: {extra_cols}"
--> 940             raise MlflowException(message)
    941     elif not input_schema.is_tensor_spec():

MlflowException: Model is missing inputs ['a', 'c'].

During handling of the above exception, another exception occurred:

MlflowException                           Traceback (most recent call last)
/var/folders/cd/n8n0rm2x53l_s0xv_j_xklb00000gp/T/ipykernel_97464/1628231496.py in <cell line: 4>()
      2 # stating that the required fields "a" and "c" are missing)
      3
----> 4 loaded_model.predict([{"b": "b"}])

~/repos/mlflow-fork/mlflow/mlflow/pyfunc/__init__.py in predict(self, data, params)
    471             except Exception as e:
    472                 # Include error in message for backwards compatibility
--> 473                 raise MlflowException.invalid_parameter_value(
    474                     f"Failed to enforce schema of data '{data}' "
    475                     f"with schema '{input_schema}'. "

MlflowException: Failed to enforce schema of data '[{'b': 'b'}]' with schema '['a': Array(string) (required), 'b': string (optional), 'c': {d: string (required), e: string (optional)} (required)]'. Error: Model is missing inputs ['a', 'c'].

更新签名

本教程的这一部分讨论了数据和模型的动态特性,重点介绍了更新 MLflow 模型签名的关键任务。随着数据集的演变和需求的变化,有必要修改模型的签名以适应新的数据结构或输入。这种更新签名的能力是保持模型随时间推移的准确性和相关性的关键。

我们将演示如何识别何时需要更新签名,并详细介绍为现有模型创建和应用新签名的过程。本节突出了MLflow在无需重新保存整个模型的情况下,适应数据格式和结构变化的灵活性。然而,对于MLflow中已注册的模型,更新签名需要重新注册模型以反映注册版本中的更改。

通过探索更新模型签名的步骤,您将学习如何在以下情况下更新模型签名:您手动定义了一个无效的签名,或者在记录时未能定义签名,并且需要使用有效签名更新模型。

[20]:
# Updating an existing model that wasn't saved with a signature


class MyTypeCheckerModel(mlflow.pyfunc.PythonModel):
    def predict(self, context, model_input, params=None):
        print(type(model_input))
        print(model_input)
        if not isinstance(model_input, (pd.DataFrame, list)):
            raise ValueError("The input must be a list.")
        return "Input is valid."


with mlflow.start_run():
    model_info = mlflow.pyfunc.log_model(
        python_model=MyTypeCheckerModel(),
        artifact_path="test_model",
    )

loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)

loaded_model.metadata.signature
[21]:
test_data = [{"a": "we are expecting strings", "b": "and only strings"}, [1, 2, 3]]
loaded_model.predict(test_data)
<class 'list'>
[{'a': 'we are expecting strings', 'b': 'and only strings'}, [1, 2, 3]]
[21]:
'Input is valid.'

MLflow 中模式强制的必要性

在本教程的这一部分,我们解决机器学习模型部署中的一个常见挑战:错误消息的清晰度和可解释性。如果没有模式强制执行,模型通常会返回晦涩或误导性的错误消息。这是因为,在没有明确定义的模式的情况下,模型试图处理可能不符合其预期的输入,从而导致模糊或难以诊断的错误。

为什么模式执行很重要

模式强制执行充当守门员,确保输入模型的数据精确匹配预期格式。这不仅减少了运行时错误的可能性,而且使发生的任何错误更容易理解和纠正。没有这种强制执行,诊断问题变得耗时且复杂,通常需要深入研究模型的内部逻辑。

更新模型签名以获得更清晰的错误消息

为了说明模式强制的价值,我们将更新一个已保存模型的签名以匹配预期的数据结构。这个过程包括定义预期的数据结构,使用 infer_signature 函数生成适当的签名,然后使用 set_signature 将此签名应用于模型。通过这样做,我们确保任何未来的错误都更具信息性,并与我们预期的数据结构一致,从而简化故障排除并提高模型的可靠性。

[22]:
expected_data_structure = [{"a": "string", "b": "another string"}, {"a": "string"}]

signature = infer_signature(expected_data_structure, loaded_model.predict(expected_data_structure))

set_signature(model_info.model_uri, signature)
<class 'list'>
[{'a': 'string', 'b': 'another string'}, {'a': 'string'}]
[23]:
loaded_with_signature = mlflow.pyfunc.load_model(model_info.model_uri)

loaded_with_signature.metadata.signature
[23]:
inputs:
  ['a': string (required), 'b': string (optional)]
outputs:
  [string (required)]
params:
  None
[24]:
loaded_with_signature.predict(expected_data_structure)
<class 'pandas.core.frame.DataFrame'>
        a               b
0  string  another string
1  string             NaN
[24]:
'Input is valid.'

验证模式强制不会允许有缺陷的输入

既然我们已经正确设置了签名并更新了模型定义,让我们确保之前的错误输入类型会引发一个有用的错误消息!

[25]:
loaded_with_signature.predict(test_data)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
~/repos/mlflow-fork/mlflow/mlflow/pyfunc/__init__.py in predict(self, data, params)
    469             try:
--> 470                 data = _enforce_schema(data, input_schema)
    471             except Exception as e:

~/repos/mlflow-fork/mlflow/mlflow/models/utils.py in _enforce_schema(pf_input, input_schema)
    907         elif isinstance(pf_input, (list, np.ndarray, pd.Series)):
--> 908             pf_input = pd.DataFrame(pf_input)
    909

~/miniconda3/envs/mlflow-dev-env/lib/python3.8/site-packages/pandas/core/frame.py in __init__(self, data, index, columns, dtype, copy)
    781                         columns = ensure_index(columns)
--> 782                     arrays, columns, index = nested_data_to_arrays(
    783                         # error: Argument 3 to "nested_data_to_arrays" has incompatible

~/miniconda3/envs/mlflow-dev-env/lib/python3.8/site-packages/pandas/core/internals/construction.py in nested_data_to_arrays(data, columns, index, dtype)
    497
--> 498     arrays, columns = to_arrays(data, columns, dtype=dtype)
    499     columns = ensure_index(columns)

~/miniconda3/envs/mlflow-dev-env/lib/python3.8/site-packages/pandas/core/internals/construction.py in to_arrays(data, columns, dtype)
    831     elif isinstance(data[0], abc.Mapping):
--> 832         arr, columns = _list_of_dict_to_arrays(data, columns)
    833     elif isinstance(data[0], ABCSeries):

~/miniconda3/envs/mlflow-dev-env/lib/python3.8/site-packages/pandas/core/internals/construction.py in _list_of_dict_to_arrays(data, columns)
    911         sort = not any(isinstance(d, dict) for d in data)
--> 912         pre_cols = lib.fast_unique_multiple_list_gen(gen, sort=sort)
    913         columns = ensure_index(pre_cols)

~/miniconda3/envs/mlflow-dev-env/lib/python3.8/site-packages/pandas/_libs/lib.pyx in pandas._libs.lib.fast_unique_multiple_list_gen()

~/miniconda3/envs/mlflow-dev-env/lib/python3.8/site-packages/pandas/core/internals/construction.py in <genexpr>(.0)
    909     if columns is None:
--> 910         gen = (list(x.keys()) for x in data)
    911         sort = not any(isinstance(d, dict) for d in data)

AttributeError: 'list' object has no attribute 'keys'

During handling of the above exception, another exception occurred:

MlflowException                           Traceback (most recent call last)
/var/folders/cd/n8n0rm2x53l_s0xv_j_xklb00000gp/T/ipykernel_97464/2586525788.py in <cell line: 1>()
----> 1 loaded_with_signature.predict(test_data)

~/repos/mlflow-fork/mlflow/mlflow/pyfunc/__init__.py in predict(self, data, params)
    471             except Exception as e:
    472                 # Include error in message for backwards compatibility
--> 473                 raise MlflowException.invalid_parameter_value(
    474                     f"Failed to enforce schema of data '{data}' "
    475                     f"with schema '{input_schema}'. "

MlflowException: Failed to enforce schema of data '[{'a': 'we are expecting strings', 'b': 'and only strings'}, [1, 2, 3]]' with schema '['a': string (required), 'b': string (optional)]'. Error: 'list' object has no attribute 'keys'

总结:从 MLflow 签名游乐场获得的见解和最佳实践

在我们结束MLflow签名游乐场笔记本的旅程时,我们已经获得了关于MLflow生态系统中模型签名复杂性的宝贵见解。本教程为您提供了有效管理和利用模型签名所需的知识和实践技能,确保您的机器学习模型的鲁棒性和准确性。

关键要点包括准确定义标量类型的重要性、为数据完整性强制执行和遵守模型签名的意义,以及MLflow在更新无效模型签名时提供的灵活性。这些概念不仅是理论上的,而且是现实世界中成功部署和管理模型的基础。

无论你是正在优化模型的数据科学家,还是将机器学习集成到应用程序中的开发者,理解和利用模型签名都是至关重要的。我们希望本教程为你提供了MLflow签名方面的坚实基础,使你能够在未来的ML项目中实施这些最佳实践。