MLflow 实验追踪与模型注册:从实验到生产的可复现工作流

文章目录

    • 一、实验管理的混乱
    • [二、MLflow Tracking:每一次实验都有据可查](#二、MLflow Tracking:每一次实验都有据可查)
      • [2.1 Tracking 的核心概念](#2.1 Tracking 的核心概念)
      • [2.2 基础追踪代码](#2.2 基础追踪代码)
      • [2.3 自动记录(Autologging)](#2.3 自动记录(Autologging))
      • [2.4 产物记录(Artifacts)](#2.4 产物记录(Artifacts))
      • [2.5 嵌套运行与超参搜索](#2.5 嵌套运行与超参搜索)
    • [三、MLflow Model Registry:模型版本的生命周期管理](#三、MLflow Model Registry:模型版本的生命周期管理)
      • [3.1 Model Registry 的核心概念](#3.1 Model Registry 的核心概念)
      • [3.2 模型注册与版本管理](#3.2 模型注册与版本管理)
      • [3.3 阶段流转与审批工作流](#3.3 阶段流转与审批工作流)
      • [3.4 模型加载与部署](#3.4 模型加载与部署)
    • [四、MLflow Projects:可复现的环境打包](#四、MLflow Projects:可复现的环境打包)
      • [4.1 项目配置文件](#4.1 项目配置文件)
      • [4.2 运行项目](#4.2 运行项目)
    • [五、与 sklearn Pipeline 的集成:端到端可复现](#五、与 sklearn Pipeline 的集成:端到端可复现)
      • [5.1 完整 Pipeline 的追踪与注册](#5.1 完整 Pipeline 的追踪与注册)
      • [5.2 带签名的模型加载与推理](#5.2 带签名的模型加载与推理)
    • 六、从实验到生产的完整工作流
      • [6.1 完整工作流架构](#6.1 完整工作流架构)
      • [6.2 完整工作流代码](#6.2 完整工作流代码)
    • [七、MLflow 的部署模式](#七、MLflow 的部署模式)
      • [7.1 三种部署方式](#7.1 三种部署方式)
    • 八、小结

一、实验管理的混乱

一个典型的机器学习项目会经历数十甚至上百次实验:调整学习率、更换模型架构、尝试不同的特征组合、修改数据预处理方式。每次实验都产生一组参数、一组指标、一个模型文件------但这些信息分散在 Notebook 的单元格中、散落在不同版本的脚本里、保存在各个目录下。三周后,当需要复现某个结果时,发现已经记不清"那次最好的实验到底用了什么参数"。

MLflow 解决的核心问题就是机器学习实验的可追踪、可复现、可管理。它提供了一套完整的工具链:Tracking(记录参数和指标)、Model Registry(管理模型版本)、Projects(打包可复现环境)、Models(标准化部署格式)。本文从实验追踪、模型注册和可复现工作流三个维度,建立一套从开发到生产的完整 MLOps 链路。

二、MLflow Tracking:每一次实验都有据可查

2.1 Tracking 的核心概念

#mermaid-svg-znItdof58vSZGJ7v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-znItdof58vSZGJ7v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-znItdof58vSZGJ7v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-znItdof58vSZGJ7v .error-icon{fill:#552222;}#mermaid-svg-znItdof58vSZGJ7v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-znItdof58vSZGJ7v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-znItdof58vSZGJ7v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-znItdof58vSZGJ7v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-znItdof58vSZGJ7v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-znItdof58vSZGJ7v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-znItdof58vSZGJ7v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-znItdof58vSZGJ7v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-znItdof58vSZGJ7v .marker.cross{stroke:#333333;}#mermaid-svg-znItdof58vSZGJ7v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-znItdof58vSZGJ7v p{margin:0;}#mermaid-svg-znItdof58vSZGJ7v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-znItdof58vSZGJ7v .cluster-label text{fill:#333;}#mermaid-svg-znItdof58vSZGJ7v .cluster-label span{color:#333;}#mermaid-svg-znItdof58vSZGJ7v .cluster-label span p{background-color:transparent;}#mermaid-svg-znItdof58vSZGJ7v .label text,#mermaid-svg-znItdof58vSZGJ7v span{fill:#333;color:#333;}#mermaid-svg-znItdof58vSZGJ7v .node rect,#mermaid-svg-znItdof58vSZGJ7v .node circle,#mermaid-svg-znItdof58vSZGJ7v .node ellipse,#mermaid-svg-znItdof58vSZGJ7v .node polygon,#mermaid-svg-znItdof58vSZGJ7v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-znItdof58vSZGJ7v .rough-node .label text,#mermaid-svg-znItdof58vSZGJ7v .node .label text,#mermaid-svg-znItdof58vSZGJ7v .image-shape .label,#mermaid-svg-znItdof58vSZGJ7v .icon-shape .label{text-anchor:middle;}#mermaid-svg-znItdof58vSZGJ7v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-znItdof58vSZGJ7v .rough-node .label,#mermaid-svg-znItdof58vSZGJ7v .node .label,#mermaid-svg-znItdof58vSZGJ7v .image-shape .label,#mermaid-svg-znItdof58vSZGJ7v .icon-shape .label{text-align:center;}#mermaid-svg-znItdof58vSZGJ7v .node.clickable{cursor:pointer;}#mermaid-svg-znItdof58vSZGJ7v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-znItdof58vSZGJ7v .arrowheadPath{fill:#333333;}#mermaid-svg-znItdof58vSZGJ7v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-znItdof58vSZGJ7v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-znItdof58vSZGJ7v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-znItdof58vSZGJ7v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-znItdof58vSZGJ7v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-znItdof58vSZGJ7v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-znItdof58vSZGJ7v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-znItdof58vSZGJ7v .cluster text{fill:#333;}#mermaid-svg-znItdof58vSZGJ7v .cluster span{color:#333;}#mermaid-svg-znItdof58vSZGJ7v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-znItdof58vSZGJ7v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-znItdof58vSZGJ7v rect.text{fill:none;stroke-width:0;}#mermaid-svg-znItdof58vSZGJ7v .icon-shape,#mermaid-svg-znItdof58vSZGJ7v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-znItdof58vSZGJ7v .icon-shape p,#mermaid-svg-znItdof58vSZGJ7v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-znItdof58vSZGJ7v .icon-shape .label rect,#mermaid-svg-znItdof58vSZGJ7v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-znItdof58vSZGJ7v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-znItdof58vSZGJ7v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-znItdof58vSZGJ7v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MLflow Tracking
Experiment

实验
Run

单次运行
Artifact

产物
一个项目对应一个 Experiment
如:房价预测 / 欺诈检测
一次训练对应一个 Run
包含:参数 / 指标 / 产物
自动记录:开始时间 / 持续时间 / 状态
模型文件

pickle / ONNX
图表

ROC 曲线 / 特征重要性
数据

训练集快照

MLflow Tracking 的数据模型分为三层:

  • Experiment(实验):一个项目的容器,例如"房价预测_v2"。每个 Experiment 包含多个 Run。
  • Run(单次运行):一次模型训练的执行记录,包含参数(parameters)、指标(metrics)、产物(artifacts)、标签(tags)和起始/结束时间。
  • Artifact(产物):Run 产生的文件,如模型文件、图表、配置文件等。

2.2 基础追踪代码

python 复制代码
import mlflow
import mlflow.sklearn
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score
import pandas as pd

# 设置实验名称
mlflow.set_experiment("credit_fraud_detection")

# 开始一次运行
with mlflow.start_run(run_name="rf_baseline"):
    # 加载数据
    df = pd.read_csv('creditcard.csv')
    X = df.drop('Class', axis=1)
    y = df['Class']
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y
    )

    # 模型参数
    params = {
        'n_estimators': 100,
        'max_depth': 10,
        'min_samples_split': 2,
        'random_state': 42
    }

    # 记录参数
    mlflow.log_params(params)

    # 训练模型
    model = RandomForestClassifier(**params)
    model.fit(X_train, y_train)

    # 预测与评估
    y_pred = model.predict(X_test)
    acc = accuracy_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)

    # 记录指标
    mlflow.log_metric("accuracy", acc)
    mlflow.log_metric("f1_score", f1)

    # 记录模型
    mlflow.sklearn.log_model(model, "model")

    print(f"Run ID: {mlflow.active_run().info.run_id}")
    print(f"Accuracy: {acc:.4f}, F1: {f1:.4f}")

运行上述代码后,MLflow 会在当前目录下创建 mlruns/ 目录,其中保存了所有实验的完整记录。启动 MLflow UI 即可查看:

bash 复制代码
mlflow ui
# 访问 http://localhost:5000

2.3 自动记录(Autologging)

MLflow 为多个主流框架提供了自动记录功能,无需手动调用 log_paramlog_metric

python 复制代码
import mlflow

# 启用 sklearn 自动记录
mlflow.sklearn.autolog()

# 启用 XGBoost 自动记录
mlflow.xgboost.autolog()

# 启用 PyTorch 自动记录
mlflow.pytorch.autolog()

# 启用 LightGBM 自动记录
mlflow.lightgbm.autolog()

with mlflow.start_run():
    # 训练代码------参数和指标会自动记录
    model.fit(X_train, y_train)
    # 不需要手动 log_param / log_metric

自动记录会捕捉的默认内容包括:

框架 自动记录的参数 自动记录的指标 自动记录的产物
sklearn 模型超参 --- 模型文件、特征重要性
XGBoost booster 参数 train/valid 评估指标 模型文件、特征重要性
LightGBM 训练参数 train/valid 评估指标 模型文件、特征重要性
PyTorch 优化器参数、网络结构 训练/验证损失 模型 checkpoint

2.4 产物记录(Artifacts)

除了参数和指标,MLflow 还可以记录任意文件作为产物。

python 复制代码
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import joblib

with mlflow.start_run(run_name="rf_with_artifacts"):
    # ... 训练代码 ...

    # 1. 记录混淆矩阵图
    cm = confusion_matrix(y_test, y_pred)
    disp = ConfusionMatrixDisplay(confusion_matrix=cm)
    disp.plot()
    plt.savefig("confusion_matrix.png")
    mlflow.log_artifact("confusion_matrix.png")

    # 2. 记录特征重要性图
    importances = pd.DataFrame({
        'feature': X.columns,
        'importance': model.feature_importances_
    }).sort_values('importance', ascending=False)
    importances.to_csv("feature_importance.csv", index=False)
    mlflow.log_artifact("feature_importance.csv")

    # 3. 记录预处理 Pipeline
    joblib.dump(preprocessor, "preprocessor.pkl")
    mlflow.log_artifact("preprocessor.pkl")

    # 4. 记录训练数据快照(小数据集适用)
    X_train.to_csv("train_data.csv", index=False)
    mlflow.log_artifact("train_data.csv")

2.5 嵌套运行与超参搜索

在超参搜索过程中,每次 trial 对应一个子运行(child run),主运行(parent run)汇总所有子运行的结果。

python 复制代码
import mlflow
import optuna

mlflow.set_experiment("optuna_xgboost")

def objective(trial):
    with mlflow.start_run(nested=True):
        params = {
            'max_depth': trial.suggest_int('max_depth', 3, 10),
            'learning_rate': trial.suggest_float('learning_rate', 1e-3, 1e-1, log=True),
            'n_estimators': trial.suggest_int('n_estimators', 100, 1000),
            'subsample': trial.suggest_float('subsample', 0.5, 1.0)
        }

        mlflow.log_params(params)

        model = XGBClassifier(**params, random_state=42)
        model.fit(X_train, y_train)

        y_proba = model.predict_proba(X_test)[:, 1]
        pr_auc = average_precision_score(y_test, y_proba)

        mlflow.log_metric("pr_auc", pr_auc)
        mlflow.sklearn.log_model(model, "model")

        return pr_auc

with mlflow.start_run(run_name="optuna_study"):
    study = optuna.create_study(direction='maximize')
    study.optimize(objective, n_trials=50)

    # 记录最优结果到父运行
    mlflow.log_metric("best_pr_auc", study.best_value)
    mlflow.log_params(study.best_params)

三、MLflow Model Registry:模型版本的生命周期管理

3.1 Model Registry 的核心概念

#mermaid-svg-dzrnM6yQPEwoR0EF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dzrnM6yQPEwoR0EF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dzrnM6yQPEwoR0EF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dzrnM6yQPEwoR0EF .error-icon{fill:#552222;}#mermaid-svg-dzrnM6yQPEwoR0EF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dzrnM6yQPEwoR0EF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dzrnM6yQPEwoR0EF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dzrnM6yQPEwoR0EF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dzrnM6yQPEwoR0EF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dzrnM6yQPEwoR0EF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dzrnM6yQPEwoR0EF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dzrnM6yQPEwoR0EF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dzrnM6yQPEwoR0EF .marker.cross{stroke:#333333;}#mermaid-svg-dzrnM6yQPEwoR0EF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dzrnM6yQPEwoR0EF p{margin:0;}#mermaid-svg-dzrnM6yQPEwoR0EF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dzrnM6yQPEwoR0EF .cluster-label text{fill:#333;}#mermaid-svg-dzrnM6yQPEwoR0EF .cluster-label span{color:#333;}#mermaid-svg-dzrnM6yQPEwoR0EF .cluster-label span p{background-color:transparent;}#mermaid-svg-dzrnM6yQPEwoR0EF .label text,#mermaid-svg-dzrnM6yQPEwoR0EF span{fill:#333;color:#333;}#mermaid-svg-dzrnM6yQPEwoR0EF .node rect,#mermaid-svg-dzrnM6yQPEwoR0EF .node circle,#mermaid-svg-dzrnM6yQPEwoR0EF .node ellipse,#mermaid-svg-dzrnM6yQPEwoR0EF .node polygon,#mermaid-svg-dzrnM6yQPEwoR0EF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dzrnM6yQPEwoR0EF .rough-node .label text,#mermaid-svg-dzrnM6yQPEwoR0EF .node .label text,#mermaid-svg-dzrnM6yQPEwoR0EF .image-shape .label,#mermaid-svg-dzrnM6yQPEwoR0EF .icon-shape .label{text-anchor:middle;}#mermaid-svg-dzrnM6yQPEwoR0EF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dzrnM6yQPEwoR0EF .rough-node .label,#mermaid-svg-dzrnM6yQPEwoR0EF .node .label,#mermaid-svg-dzrnM6yQPEwoR0EF .image-shape .label,#mermaid-svg-dzrnM6yQPEwoR0EF .icon-shape .label{text-align:center;}#mermaid-svg-dzrnM6yQPEwoR0EF .node.clickable{cursor:pointer;}#mermaid-svg-dzrnM6yQPEwoR0EF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dzrnM6yQPEwoR0EF .arrowheadPath{fill:#333333;}#mermaid-svg-dzrnM6yQPEwoR0EF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dzrnM6yQPEwoR0EF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dzrnM6yQPEwoR0EF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dzrnM6yQPEwoR0EF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dzrnM6yQPEwoR0EF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dzrnM6yQPEwoR0EF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dzrnM6yQPEwoR0EF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dzrnM6yQPEwoR0EF .cluster text{fill:#333;}#mermaid-svg-dzrnM6yQPEwoR0EF .cluster span{color:#333;}#mermaid-svg-dzrnM6yQPEwoR0EF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dzrnM6yQPEwoR0EF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dzrnM6yQPEwoR0EF rect.text{fill:none;stroke-width:0;}#mermaid-svg-dzrnM6yQPEwoR0EF .icon-shape,#mermaid-svg-dzrnM6yQPEwoR0EF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dzrnM6yQPEwoR0EF .icon-shape p,#mermaid-svg-dzrnM6yQPEwoR0EF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dzrnM6yQPEwoR0EF .icon-shape .label rect,#mermaid-svg-dzrnM6yQPEwoR0EF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dzrnM6yQPEwoR0EF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dzrnM6yQPEwoR0EF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dzrnM6yQPEwoR0EF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Model Registry
Registered Model

注册模型
Model Version

版本
Model Stage

阶段
如:credit_fraud_model
包含多个版本
版本号:1, 2, 3...
每个版本关联一个 Run
None → Staging → Production → Archived
支持自定义阶段

Model Registry 是 MLflow 的模型版本管理中枢,解决"生产环境用的是哪个版本的模型"这一问题。

3.2 模型注册与版本管理

python 复制代码
import mlflow
from mlflow.tracking import MlflowClient

client = MlflowClient()

# 1. 注册模型(如果不存在)
try:
    client.create_registered_model("credit_fraud_model")
except mlflow.exceptions.MlflowException:
    pass  # 模型已存在

# 2. 从已有 Run 创建模型版本
run_id = "abc123def456"  # 某次训练的运行 ID
model_uri = f"runs:/{run_id}/model"

mv = client.create_model_version(
    name="credit_fraud_model",
    source=model_uri,
    run_id=run_id
)
print(f"Registered version: {mv.version}")

# 3. 将版本推进到 Staging 阶段
client.transition_model_version_stage(
    name="credit_fraud_model",
    version=mv.version,
    stage="Staging"
)

# 4. 添加版本说明
client.update_model_version(
    name="credit_fraud_model",
    version=mv.version,
    description="PR-AUC 0.92, 使用 SMOTE + XGBoost"
)

# 5. 添加标签
client.set_model_version_tag(
    name="credit_fraud_model",
    version=mv.version,
    key="validated",
    value="true"
)

3.3 阶段流转与审批工作流

#mermaid-svg-dXu5kv7g10UlKSwF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dXu5kv7g10UlKSwF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dXu5kv7g10UlKSwF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dXu5kv7g10UlKSwF .error-icon{fill:#552222;}#mermaid-svg-dXu5kv7g10UlKSwF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dXu5kv7g10UlKSwF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dXu5kv7g10UlKSwF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dXu5kv7g10UlKSwF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dXu5kv7g10UlKSwF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dXu5kv7g10UlKSwF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dXu5kv7g10UlKSwF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dXu5kv7g10UlKSwF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dXu5kv7g10UlKSwF .marker.cross{stroke:#333333;}#mermaid-svg-dXu5kv7g10UlKSwF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dXu5kv7g10UlKSwF p{margin:0;}#mermaid-svg-dXu5kv7g10UlKSwF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dXu5kv7g10UlKSwF .cluster-label text{fill:#333;}#mermaid-svg-dXu5kv7g10UlKSwF .cluster-label span{color:#333;}#mermaid-svg-dXu5kv7g10UlKSwF .cluster-label span p{background-color:transparent;}#mermaid-svg-dXu5kv7g10UlKSwF .label text,#mermaid-svg-dXu5kv7g10UlKSwF span{fill:#333;color:#333;}#mermaid-svg-dXu5kv7g10UlKSwF .node rect,#mermaid-svg-dXu5kv7g10UlKSwF .node circle,#mermaid-svg-dXu5kv7g10UlKSwF .node ellipse,#mermaid-svg-dXu5kv7g10UlKSwF .node polygon,#mermaid-svg-dXu5kv7g10UlKSwF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dXu5kv7g10UlKSwF .rough-node .label text,#mermaid-svg-dXu5kv7g10UlKSwF .node .label text,#mermaid-svg-dXu5kv7g10UlKSwF .image-shape .label,#mermaid-svg-dXu5kv7g10UlKSwF .icon-shape .label{text-anchor:middle;}#mermaid-svg-dXu5kv7g10UlKSwF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dXu5kv7g10UlKSwF .rough-node .label,#mermaid-svg-dXu5kv7g10UlKSwF .node .label,#mermaid-svg-dXu5kv7g10UlKSwF .image-shape .label,#mermaid-svg-dXu5kv7g10UlKSwF .icon-shape .label{text-align:center;}#mermaid-svg-dXu5kv7g10UlKSwF .node.clickable{cursor:pointer;}#mermaid-svg-dXu5kv7g10UlKSwF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dXu5kv7g10UlKSwF .arrowheadPath{fill:#333333;}#mermaid-svg-dXu5kv7g10UlKSwF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dXu5kv7g10UlKSwF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dXu5kv7g10UlKSwF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dXu5kv7g10UlKSwF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dXu5kv7g10UlKSwF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dXu5kv7g10UlKSwF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dXu5kv7g10UlKSwF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dXu5kv7g10UlKSwF .cluster text{fill:#333;}#mermaid-svg-dXu5kv7g10UlKSwF .cluster span{color:#333;}#mermaid-svg-dXu5kv7g10UlKSwF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dXu5kv7g10UlKSwF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dXu5kv7g10UlKSwF rect.text{fill:none;stroke-width:0;}#mermaid-svg-dXu5kv7g10UlKSwF .icon-shape,#mermaid-svg-dXu5kv7g10UlKSwF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dXu5kv7g10UlKSwF .icon-shape p,#mermaid-svg-dXu5kv7g10UlKSwF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dXu5kv7g10UlKSwF .icon-shape .label rect,#mermaid-svg-dXu5kv7g10UlKSwF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dXu5kv7g10UlKSwF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dXu5kv7g10UlKSwF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dXu5kv7g10UlKSwF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 训练完成
QA 验证通过
验证失败
新模型上线
性能回退
None
回滚到 Staging
Production
Archived

生产环境中的典型模型生命周期:

  1. 训练完成:模型自动注册为版本,阶段为 None
  2. QA 验证:在验证集上评估,通过后推进到 Staging
  3. 灰度发布:Staging 阶段模型接入 5% 流量,观察指标
  4. 全量发布:灰度通过后推进到 Production
  5. 归档:新版本上线后,旧版本推进到 Archived
python 复制代码
def promote_model(model_name, version, from_stage, to_stage):
    """模型阶段流转(含验证检查)"""
    client = MlflowClient()

    # 检查当前阶段
    current = client.get_model_version(model_name, version)
    if current.current_stage != from_stage:
        raise ValueError(f"版本 {version} 当前阶段为 {current.current_stage},不是 {from_stage}")

    # 阶段流转
    client.transition_model_version_stage(
        name=model_name,
        version=version,
        stage=to_stage
    )

    print(f"模型 {model_name} v{version} 已从 {from_stage} 推进到 {to_stage}")

# 使用示例
promote_model("credit_fraud_model", 3, "Staging", "Production")

3.4 模型加载与部署

python 复制代码
import mlflow.pyfunc

# 按阶段加载模型(推荐)
model = mlflow.pyfunc.load_model("models:/credit_fraud_model/Production")
predictions = model.predict(X_new)

# 按版本加载模型(用于回滚)
model_v2 = mlflow.pyfunc.load_model("models:/credit_fraud_model/2")
predictions_v2 = model_v2.predict(X_new)

# 按 Run ID 加载模型(调试时)
model_run = mlflow.pyfunc.load_model("runs:/abc123def456/model")

生产环境最佳实践:部署脚本中始终使用 models:/model_name/Production 而非硬编码版本号。这样当模型在 Registry 中更新阶段时,部署系统会自动加载新模型,无需修改代码。

四、MLflow Projects:可复现的环境打包

4.1 项目配置文件

MLflow Projects 通过 MLproject 文件定义项目的入口点和依赖,确保实验在任意环境中都能复现。

yaml 复制代码
# MLproject
name: credit_fraud_training

conda_env: conda.yaml

entry_points:
  main:
    parameters:
      n_estimators: {type: int, default: 100}
      max_depth: {type: int, default: 5}
      learning_rate: {type: float, default: 0.1}
      data_path: {type: str, default: "data/creditcard.csv"}
    command: "python train.py --n_estimators={n_estimators} --max_depth={max_depth}
              --learning_rate={learning_rate} --data_path={data_path}"

  evaluate:
    parameters:
      model_uri: {type: str}
      test_data: {type: str, default: "data/test.csv"}
    command: "python evaluate.py --model_uri={model_uri} --test_data={test_data}"
yaml 复制代码
# conda.yaml
name: fraud_training
channels:
  - conda-forge
dependencies:
  - python=3.11
  - pip
  - pip:
    - mlflow==2.14.0
    - scikit-learn==1.5.0
    - xgboost==2.0.0
    - pandas==2.0.0
    - numpy==1.26.0

4.2 运行项目

bash 复制代码
# 本地运行
mlflow run . -P n_estimators=200 -P max_depth=7

# 从 Git 仓库运行
mlflow run https://github.com/username/fraud-model.git -P max_depth=10

# 指定实验名称
mlflow run . --experiment-name fraud_v2 -P learning_rate=0.05

mlflow run 会自动:

  1. 解析 MLproject 文件
  2. 根据 conda.yaml 创建/复用 Conda 环境
  3. 在隔离环境中执行入口命令
  4. 自动记录所有参数、指标和产物到 Tracking

五、与 sklearn Pipeline 的集成:端到端可复现

5.1 完整 Pipeline 的追踪与注册

python 复制代码
import mlflow
import mlflow.sklearn
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
import pandas as pd

mlflow.set_experiment("fraud_pipeline_v1")

with mlflow.start_run(run_name="full_pipeline"):
    # 特征工程 Pipeline
    numeric_features = ['amount', 'time', 'v1', 'v2']
    categorical_features = ['merchant_category']

    preprocessor = ColumnTransformer([
        ('num', StandardScaler(), numeric_features),
        ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features)
    ])

    # 完整 Pipeline
    pipeline = Pipeline([
        ('preprocess', preprocessor),
        ('classifier', RandomForestClassifier(n_estimators=200, random_state=42))
    ])

    # 记录参数
    mlflow.log_params({
        'model_type': 'RandomForest',
        'n_estimators': 200,
        'cv_folds': 5
    })

    # 交叉验证
    scores = cross_val_score(
        pipeline, X_train, y_train,
        cv=5, scoring='average_precision', n_jobs=-1
    )

    mlflow.log_metric('cv_pr_auc_mean', scores.mean())
    mlflow.log_metric('cv_pr_auc_std', scores.std())

    # 在全量训练集上训练
    pipeline.fit(X_train, y_train)

    # 测试集评估
    from sklearn.metrics import average_precision_score
    y_proba = pipeline.predict_proba(X_test)[:, 1]
    test_pr_auc = average_precision_score(y_test, y_proba)
    mlflow.log_metric('test_pr_auc', test_pr_auc)

    # 记录签名(输入/输出 schema)
    from mlflow.models.signature import infer_signature
    signature = infer_signature(X_train, pipeline.predict(X_train))

    # 记录完整 Pipeline(含预处理)
    mlflow.sklearn.log_model(
        pipeline,
        artifact_path="pipeline_model",
        signature=signature,
        input_example=X_train.head(5)
    )

    print(f"CV PR-AUC: {scores.mean():.4f} ± {scores.std():.4f}")
    print(f"Test PR-AUC: {test_pr_auc:.4f}")

5.2 带签名的模型加载与推理

python 复制代码
import mlflow.pyfunc

# 加载模型
model = mlflow.pyfunc.load_model("models:/fraud_pipeline_v1/Production")

# 推理------Pipeline 自动处理预处理
# 输入原始数据,输出预测结果
predictions = model.predict(X_new)
probabilities = model.predict_proba(X_new)

# 模型签名保证了输入格式的一致性
print(model.metadata.signature)
# 输出示例:
# inputs:
#   ['amount': double, 'time': double, 'v1': double, ...]
# outputs:
#   ['predicted_class': long]

Pipeline + MLflow 的组合价值:训练时的预处理步骤(StandardScaler、OneHotEncoder)与模型一起被打包保存。推理时无需手动预处理,模型自动处理输入数据。这消除了"训练-推理不一致"的风险。

六、从实验到生产的完整工作流

6.1 完整工作流架构

#mermaid-svg-jPgtl9MPzVv99xou{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jPgtl9MPzVv99xou .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jPgtl9MPzVv99xou .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jPgtl9MPzVv99xou .error-icon{fill:#552222;}#mermaid-svg-jPgtl9MPzVv99xou .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jPgtl9MPzVv99xou .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jPgtl9MPzVv99xou .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jPgtl9MPzVv99xou .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jPgtl9MPzVv99xou .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jPgtl9MPzVv99xou .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jPgtl9MPzVv99xou .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jPgtl9MPzVv99xou .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jPgtl9MPzVv99xou .marker.cross{stroke:#333333;}#mermaid-svg-jPgtl9MPzVv99xou svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jPgtl9MPzVv99xou p{margin:0;}#mermaid-svg-jPgtl9MPzVv99xou .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jPgtl9MPzVv99xou .cluster-label text{fill:#333;}#mermaid-svg-jPgtl9MPzVv99xou .cluster-label span{color:#333;}#mermaid-svg-jPgtl9MPzVv99xou .cluster-label span p{background-color:transparent;}#mermaid-svg-jPgtl9MPzVv99xou .label text,#mermaid-svg-jPgtl9MPzVv99xou span{fill:#333;color:#333;}#mermaid-svg-jPgtl9MPzVv99xou .node rect,#mermaid-svg-jPgtl9MPzVv99xou .node circle,#mermaid-svg-jPgtl9MPzVv99xou .node ellipse,#mermaid-svg-jPgtl9MPzVv99xou .node polygon,#mermaid-svg-jPgtl9MPzVv99xou .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jPgtl9MPzVv99xou .rough-node .label text,#mermaid-svg-jPgtl9MPzVv99xou .node .label text,#mermaid-svg-jPgtl9MPzVv99xou .image-shape .label,#mermaid-svg-jPgtl9MPzVv99xou .icon-shape .label{text-anchor:middle;}#mermaid-svg-jPgtl9MPzVv99xou .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jPgtl9MPzVv99xou .rough-node .label,#mermaid-svg-jPgtl9MPzVv99xou .node .label,#mermaid-svg-jPgtl9MPzVv99xou .image-shape .label,#mermaid-svg-jPgtl9MPzVv99xou .icon-shape .label{text-align:center;}#mermaid-svg-jPgtl9MPzVv99xou .node.clickable{cursor:pointer;}#mermaid-svg-jPgtl9MPzVv99xou .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jPgtl9MPzVv99xou .arrowheadPath{fill:#333333;}#mermaid-svg-jPgtl9MPzVv99xou .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jPgtl9MPzVv99xou .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jPgtl9MPzVv99xou .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jPgtl9MPzVv99xou .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jPgtl9MPzVv99xou .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jPgtl9MPzVv99xou .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jPgtl9MPzVv99xou .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jPgtl9MPzVv99xou .cluster text{fill:#333;}#mermaid-svg-jPgtl9MPzVv99xou .cluster span{color:#333;}#mermaid-svg-jPgtl9MPzVv99xou div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jPgtl9MPzVv99xou .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jPgtl9MPzVv99xou rect.text{fill:none;stroke-width:0;}#mermaid-svg-jPgtl9MPzVv99xou .icon-shape,#mermaid-svg-jPgtl9MPzVv99xou .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jPgtl9MPzVv99xou .icon-shape p,#mermaid-svg-jPgtl9MPzVv99xou .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jPgtl9MPzVv99xou .icon-shape .label rect,#mermaid-svg-jPgtl9MPzVv99xou .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jPgtl9MPzVv99xou .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jPgtl9MPzVv99xou .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jPgtl9MPzVv99xou :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 生产环境
验证环境
开发环境
通过
失败
性能下降
Jupyter Notebook

探索性实验
MLflow Tracking

记录参数/指标
Optuna

超参搜索
模型注册到

Model Registry
Staging 阶段

QA 验证
Production 阶段
返回开发
部署服务

加载 Production 模型
监控指标

漂移检测
触发重训练

6.2 完整工作流代码

python 复制代码
"""
完整的 MLOps 工作流:从数据到部署
"""
import mlflow
import mlflow.sklearn
from mlflow.tracking import MlflowClient
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from xgboost import XGBClassifier
from sklearn.metrics import average_precision_score
import optuna

# 配置
EXPERIMENT_NAME = "fraud_detection_prod"
MODEL_NAME = "fraud_classifier"
DATA_PATH = "data/creditcard.csv"

def load_and_split_data(path):
    """加载并分割数据"""
    df = pd.read_csv(path)
    X = df.drop('Class', axis=1)
    y = df['Class']
    return train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)

def train_baseline(X_train, y_train, X_test, y_test):
    """训练基线模型并记录"""
    mlflow.set_experiment(EXPERIMENT_NAME)

    with mlflow.start_run(run_name="baseline"):
        pipeline = Pipeline([
            ('scale', StandardScaler()),
            ('model', XGBClassifier(random_state=42))
        ])

        pipeline.fit(X_train, y_train)

        y_proba = pipeline.predict_proba(X_test)[:, 1]
        pr_auc = average_precision_score(y_test, y_proba)

        mlflow.log_params({'model': 'XGBClassifier', 'scale': True})
        mlflow.log_metric('pr_auc', pr_auc)
        mlflow.sklearn.log_model(pipeline, 'model')

        return mlflow.active_run().info.run_id, pr_auc

def hyperparameter_search(X_train, y_train, X_test, y_test, n_trials=50):
    """超参搜索并记录最优模型"""
    mlflow.set_experiment(EXPERIMENT_NAME)

    def objective(trial):
        with mlflow.start_run(nested=True):
            params = {
                'max_depth': trial.suggest_int('max_depth', 3, 10),
                'learning_rate': trial.suggest_float('learning_rate', 1e-3, 1e-1, log=True),
                'n_estimators': trial.suggest_int('n_estimators', 100, 1000),
                'subsample': trial.suggest_float('subsample', 0.5, 1.0),
                'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
                'scale_pos_weight': trial.suggest_float('scale_pos_weight', 1, 100, log=True)
            }

            mlflow.log_params(params)

            model = XGBClassifier(**params, random_state=42)
            model.fit(X_train, y_train)

            y_proba = model.predict_proba(X_test)[:, 1]
            pr_auc = average_precision_score(y_test, y_proba)
            mlflow.log_metric('pr_auc', pr_auc)
            mlflow.sklearn.log_model(model, 'model')

            return pr_auc

    with mlflow.start_run(run_name="hyperparameter_search"):
        study = optuna.create_study(direction='maximize')
        study.optimize(objective, n_trials=n_trials, show_progress_bar=True)

        # 记录最优结果
        mlflow.log_params(study.best_params)
        mlflow.log_metric('best_pr_auc', study.best_value)

        # 获取最优 run
        best_run_id = study.best_trial.user_attrs.get('run_id')

        return study.best_value, study.best_params, best_run_id

def register_and_promote(run_id, model_name, metric_value):
    """注册模型并推进到 Production"""
    client = MlflowClient()

    # 创建或获取注册模型
    try:
        client.create_registered_model(model_name)
    except mlflow.exceptions.MlflowException:
        pass

    # 创建新版本
    model_uri = f"runs:/{run_id}/model"
    mv = client.create_model_version(
        name=model_name,
        source=model_uri,
        run_id=run_id
    )

    # 添加说明
    client.update_model_version(
        name=model_name,
        version=mv.version,
        description=f"PR-AUC: {metric_value:.4f}"
    )

    # 先推进到 Staging
    client.transition_model_version_stage(
        name=model_name, version=mv.version, stage="Staging"
    )

    # 验证逻辑(此处简化,实际应有独立验证脚本)
    print(f"模型 {model_name} v{mv.version} 已注册并推进到 Staging")
    print("请运行验证脚本,验证通过后执行以下命令推进到 Production:")
    print(f"  mlflow transition-model-version-stage --name {model_name} "
          f"--version {mv.version} --stage Production")

    return mv.version

def production_inference(model_name, X_new):
    """生产环境推理"""
    model = mlflow.pyfunc.load_model(f"models:/{model_name}/Production")
    return model.predict_proba(X_new)[:, 1]

# 主工作流
if __name__ == "__main__":
    print("=== Step 1: 加载数据 ===")
    X_train, X_test, y_train, y_test = load_and_split_data(DATA_PATH)

    print("\n=== Step 2: 训练基线模型 ===")
    baseline_run_id, baseline_pr_auc = train_baseline(
        X_train, y_train, X_test, y_test
    )
    print(f"基线 PR-AUC: {baseline_pr_auc:.4f}")

    print("\n=== Step 3: 超参搜索 ===")
    best_pr_auc, best_params, best_run_id = hyperparameter_search(
        X_train, y_train, X_test, y_test, n_trials=30
    )
    print(f"最优 PR-AUC: {best_pr_auc:.4f}")
    print(f"最优参数: {best_params}")

    print("\n=== Step 4: 模型注册 ===")
    version = register_and_promote(best_run_id, MODEL_NAME, best_pr_auc)

七、MLflow 的部署模式

7.1 三种部署方式

#mermaid-svg-xPOphsgCTyWHtcXn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xPOphsgCTyWHtcXn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xPOphsgCTyWHtcXn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xPOphsgCTyWHtcXn .error-icon{fill:#552222;}#mermaid-svg-xPOphsgCTyWHtcXn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xPOphsgCTyWHtcXn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xPOphsgCTyWHtcXn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xPOphsgCTyWHtcXn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xPOphsgCTyWHtcXn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xPOphsgCTyWHtcXn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xPOphsgCTyWHtcXn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xPOphsgCTyWHtcXn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xPOphsgCTyWHtcXn .marker.cross{stroke:#333333;}#mermaid-svg-xPOphsgCTyWHtcXn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xPOphsgCTyWHtcXn p{margin:0;}#mermaid-svg-xPOphsgCTyWHtcXn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xPOphsgCTyWHtcXn .cluster-label text{fill:#333;}#mermaid-svg-xPOphsgCTyWHtcXn .cluster-label span{color:#333;}#mermaid-svg-xPOphsgCTyWHtcXn .cluster-label span p{background-color:transparent;}#mermaid-svg-xPOphsgCTyWHtcXn .label text,#mermaid-svg-xPOphsgCTyWHtcXn span{fill:#333;color:#333;}#mermaid-svg-xPOphsgCTyWHtcXn .node rect,#mermaid-svg-xPOphsgCTyWHtcXn .node circle,#mermaid-svg-xPOphsgCTyWHtcXn .node ellipse,#mermaid-svg-xPOphsgCTyWHtcXn .node polygon,#mermaid-svg-xPOphsgCTyWHtcXn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xPOphsgCTyWHtcXn .rough-node .label text,#mermaid-svg-xPOphsgCTyWHtcXn .node .label text,#mermaid-svg-xPOphsgCTyWHtcXn .image-shape .label,#mermaid-svg-xPOphsgCTyWHtcXn .icon-shape .label{text-anchor:middle;}#mermaid-svg-xPOphsgCTyWHtcXn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xPOphsgCTyWHtcXn .rough-node .label,#mermaid-svg-xPOphsgCTyWHtcXn .node .label,#mermaid-svg-xPOphsgCTyWHtcXn .image-shape .label,#mermaid-svg-xPOphsgCTyWHtcXn .icon-shape .label{text-align:center;}#mermaid-svg-xPOphsgCTyWHtcXn .node.clickable{cursor:pointer;}#mermaid-svg-xPOphsgCTyWHtcXn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xPOphsgCTyWHtcXn .arrowheadPath{fill:#333333;}#mermaid-svg-xPOphsgCTyWHtcXn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xPOphsgCTyWHtcXn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xPOphsgCTyWHtcXn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xPOphsgCTyWHtcXn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xPOphsgCTyWHtcXn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xPOphsgCTyWHtcXn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xPOphsgCTyWHtcXn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xPOphsgCTyWHtcXn .cluster text{fill:#333;}#mermaid-svg-xPOphsgCTyWHtcXn .cluster span{color:#333;}#mermaid-svg-xPOphsgCTyWHtcXn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xPOphsgCTyWHtcXn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xPOphsgCTyWHtcXn rect.text{fill:none;stroke-width:0;}#mermaid-svg-xPOphsgCTyWHtcXn .icon-shape,#mermaid-svg-xPOphsgCTyWHtcXn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xPOphsgCTyWHtcXn .icon-shape p,#mermaid-svg-xPOphsgCTyWHtcXn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xPOphsgCTyWHtcXn .icon-shape .label rect,#mermaid-svg-xPOphsgCTyWHtcXn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xPOphsgCTyWHtcXn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xPOphsgCTyWHtcXn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xPOphsgCTyWHtcXn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MLflow 模型
本地推理

mlflow.pyfunc.load_model
MLflow Serving

REST API
云原生部署

Docker + K8s
Python 脚本直接调用

适合批处理
mlflow models serve

自动暴露 REST API

适合实时推理
mlflow models build-docker

打包为 Docker 镜像

适合大规模部署

本地推理

python 复制代码
import mlflow.pyfunc

model = mlflow.pyfunc.load_model("models:/fraud_classifier/Production")
predictions = model.predict_proba(X_batch)

MLflow Serving

bash 复制代码
# 启动 REST 服务
mlflow models serve -m models:/fraud_classifier/Production -p 5001

# 测试请求
curl -X POST http://localhost:5001/invocations \
  -H "Content-Type: application/json" \
  -d '{"dataframe_split": {"columns": ["amount", "time", "v1"], "data": [[100.0, 50000, 0.5]]}}'

Docker 部署

bash 复制代码
# 构建 Docker 镜像
mlflow models build-docker \
  -m models:/fraud_classifier/Production \
  -n fraud-model:v1

# 运行容器
docker run -p 5001:8080 fraud-model:v1

八、小结

MLflow 不是单个工具,而是一个完整的机器学习生命周期管理平台。Tracking 解决了"实验去哪儿了"的问题,Model Registry 解决了"生产用的是哪个模型"的问题,Projects 解决了"这个实验怎么复现"的问题。

核心设计原则:

  • 实验即代码:每次实验都对应一个可执行的代码版本,而非 Notebook 中的某个单元格状态
  • 模型即产物:训练好的模型与参数、指标、环境配置一起打包,形成完整的"模型包"
  • 阶段即契约:Staging → Production → Archived 的阶段流转,是团队协作的标准语言

与 sklearn Pipeline 的集成是生产落地的关键。将预处理步骤(StandardScaler、OneHotEncoder)与模型一起封装为 Pipeline,再用 MLflow 记录和注册,可以确保训练时的预处理逻辑与推理时完全一致------这是避免"训练-推理不一致"的工程底线。

此前专栏关于模型评估与超参调优、特征工程系统方法论以及 XGBoost 实战的文章,为本文提供了从特征构造到模型训练再到实验管理的完整上游支撑。如果本文对 MLflow 实践和 MLOps 工作流有所启发,欢迎点赞、收藏与关注。

相关推荐
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【72】集成 MCP 客户端
java·人工智能·spring
ZHW_AI课题组1 小时前
利用DeepLab在PascalVOC数据集中实现简单物体的多类别分割
人工智能·计算机视觉
直接冲冲冲1 小时前
pytorch-深度学习-引言
人工智能·pytorch·深度学习
曲幽1 小时前
写爬虫时用了代理还被封?Python 代理的那些隐藏坑,我替你踩明白了
python·http·https·proxy·socks·requests·socks5·proxies
SAP上海工博云署1 小时前
生产采购财务一体化ERP选型指南(中小制造/工贸企业适用)
大数据·人工智能·信息可视化·制造·信息与通信
装不满的克莱因瓶1 小时前
掌握多头自注意力机制(Multi-Head Self-Attention)——Transformer 强大表达能力的核心来源
人工智能·python·深度学习·数学·ai·transformer
花间相见1 小时前
【AI工作流搭建n8n】—— Docker + PostgreSQL 生产环境部署全攻略:MCP 集成与 Skills 技能实战
人工智能·docker·postgresql
doiito1 小时前
【Agent Harness实战】我让 Agent 的上下文“瘦身”成功,Token 省了,记忆反而更好了
人工智能·架构
意图共鸣1 小时前
技术视角:意图共鸣科技《AI记忆链商业化白皮书3.0》中的第二大脑与AI参谋架构
人工智能