Skip to content

필사 모드: ML 模型监控与漂移检测:Evidently AI + MLflow 生产运维指南

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
ML Model Monitoring

1. 引言:生产模型会悄悄退化

ML 模型的准确率在部署那一刻达到峰值。此后,随着现实世界的变化,预测质量会逐渐下降。问题在于,这种劣化是在没有明确错误的情况下发生的。不会抛出 HTTP 500,日志里不会出现 CRITICAL,服务照常正常响应。只是推荐结果慢慢变得离谱,欺诈检测开始漏掉新模式,需求预测开始与现实脱节。

根据 Google 的研究,生产 ML 系统中发生的故障有 60% 以上并非源于模型代码,而是源于数据相关问题。模型本身并没有坏掉,核心原因在于模型所学习的世界与现实世界之间的差距在不断拉大。

本文将介绍如何把开源监控工具 Evidently AI 与实验/模型管理平台 MLflow 结合起来,在生产环境中持续监控 ML 模型的健康状态、检测漂移,并触发自动再训练流水线。

2. 漂移的种类:变化的是什么

漂移(Drift)指的是模型训练时的数据分布与实际服务(serving)时点的数据分布之间的不一致。漂移根据发生位置与性质,大致可分为三类。

数据漂移(Data Drift,Covariate Shift)

指输入特征的分布发生变化的现象。模型的输入空间 P(X) 会随时间推移。例如,在电商推荐模型中用户年龄段分布发生变化,或者购买品类的占比随季节改变,都属于这种情况。目标变量 Y 与特征 X 之间的关系 P(Y|X) 保持不变,而输入本身的统计特性发生了变化。

概念漂移(Concept Drift)

指特征与目标之间的关系本身发生变化的现象,即 P(Y|X) 发生变化。这比数据漂移更严重,因为对相同的输入而言,正确答案本身变了。典型的例子包括新冠(COVID-19)疫情期间需求预测模型彻底失效、金融欺诈检测中欺诈手法不断演进导致既有模式不再有效等。

预测漂移(Prediction Drift)

指模型输出 P(Y_pred) 的分布发生变化的现象。它可能是输入漂移带来的结果,也可能由模型内部问题独立引发。分类模型中特定类别的预测比例突然失衡、回归模型中预测值的均值或方差发生显著变化,都属于此类。

漂移类型变化对象检测难度代表性检测方法再训练紧急度
数据漂移P(X) 输入分布中等PSI、KS 检验、Wasserstein中等
概念漂移P(Y|X) 关系性能指标监控、ADWIN
预测漂移P(Y_pred) 输出输出分布统计、Chi-squared视情况而定
标签漂移P(Y) 目标分布中等标签分布比较

3. Evidently AI 架构与核心功能

Evidently AI 是一个用于 ML 模型监控与数据质量校验的开源库。它运行在 Python 原生环境中,内置了 20 多种统计漂移检测方法。

核心组成

  • Report:一次性数据分析报告,可以以 HTML、JSON、Python 字典等形式输出,适合探索性分析与调试。
  • Test Suite:针对预先定义条件的自动化校验,可集成进 CI/CD 流水线作为数据质量门禁。
  • Metric:单项测量指标。系统内置了 DataDriftTableDatasetSummaryMetricColumnCorrelationsMetric 等数十种指标。
  • Collector/Workspace:Evidently 的服务器模式,将监控结果以时间序列形式存储,并可在仪表盘中查询。

主要漂移检测算法

Evidently 会根据特征类型(数值型/类别型)与数据集规模,自动选择最优的检测算法。

算法适用对象原理优点局限
Kolmogorov-Smirnov (KS)数值型,小规模累积分布函数的最大差异无需分布假设在大规模数据上过于敏感
Population Stability Index (PSI)数值型/类别型两个分布对数比率的加权和行业标准,易于解释对分箱(bin)设置敏感
Wasserstein Distance数值型两个分布间的最小搬运成本反映分布形态差异计算成本高
Jensen-Shannon Divergence数值型/类别型KL 散度的对称版本始终有限、具有对称性对尾部分布变化不敏感
Chi-squared Test类别型观测/期望频数差异对类别型数据直观在低频类别上不稳定
Z-test(比例检验)类别型,大规模比例差异的标准化对大规模数据高效假设正态近似

4. Evidently AI 实战用法

安装与基础设置

# 安装 Evidently AI(包含 MLflow 集成)
# pip install evidently mlflow scikit-learn pandas

import pandas as pd
import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

from evidently.report import Report
from evidently.metric_preset import DataDriftPreset, DataQualityPreset
from evidently.metrics import (
    DatasetDriftMetric,
    DataDriftTable,
    ColumnDriftMetric,
)

# 准备参考数据 / 当前数据
data = load_iris(as_frame=True)
df = data.frame
df.columns = ["sepal_length", "sepal_width", "petal_length", "petal_width", "target"]

reference_data = df.sample(frac=0.5, random_state=42)
current_data = df.drop(reference_data.index)

# 生成带有数据漂移的模拟数据
current_drifted = current_data.copy()
current_drifted["sepal_length"] = current_drifted["sepal_length"] + np.random.normal(2.0, 0.5, len(current_drifted))
current_drifted["petal_width"] = current_drifted["petal_width"] * 1.8

# 生成漂移报告
drift_report = Report(metrics=[
    DatasetDriftMetric(),
    DataDriftTable(),
])

drift_report.run(
    reference_data=reference_data,
    current_data=current_drifted,
)

# 将结果提取为字典(供程序化使用)
result = drift_report.as_dict()
dataset_drift = result["metrics"][0]["result"]["dataset_drift"]
drift_share = result["metrics"][0]["result"]["share_of_drifted_columns"]

print(f"数据集漂移检测: {dataset_drift}")
print(f"漂移列比例: {drift_share:.2%}")

# 保存为 HTML 报告
drift_report.save_html("drift_report.html")

用 Test Suite 实现自动化数据质量校验

from evidently.test_suite import TestSuite
from evidently.test_preset import DataDriftTestPreset, DataQualityTestPreset
from evidently.tests import (
    TestColumnDrift,
    TestShareOfDriftedColumns,
    TestNumberOfMissingValues,
    TestShareOfOutRangeValues,
    TestMeanInNSigmas,
)

# 配置数据漂移 + 质量测试套件
monitoring_suite = TestSuite(tests=[
    # 漂移测试:全部列中有 30% 以上漂移时判定失败
    TestShareOfDriftedColumns(lt=0.3),

    # 校验各个关键特征的漂移
    TestColumnDrift(column_name="sepal_length"),
    TestColumnDrift(column_name="petal_width"),

    # 数据质量测试
    TestNumberOfMissingValues(eq=0),

    # 数值范围校验:sepal_length 需在参考数据 ±3 个标准差之内
    TestMeanInNSigmas(column_name="sepal_length", n=3),
])

monitoring_suite.run(
    reference_data=reference_data,
    current_data=current_drifted,
)

# 以程序化方式检查测试结果
suite_result = monitoring_suite.as_dict()
all_passed = all(
    test["status"] == "SUCCESS"
    for test in suite_result["tests"]
)

print(f"全部测试是否通过: {all_passed}")
for test in suite_result["tests"]:
    status_icon = "PASS" if test["status"] == "SUCCESS" else "FAIL"
    print(f"  [{status_icon}] {test['name']}: {test['status']}")

# 在 CI/CD 流水线中作为 exit code 使用
if not all_passed:
    print("ALERT: 检测到数据漂移或质量异常。需要触发再训练流水线。")
    # sys.exit(1)  # 在 CI 中判定构建失败

5. MLflow 模型注册表与监控集成

MLflow 提供实验追踪、模型打包与模型注册表功能。通过将 Evidently 的漂移检测结果记录到 MLflow,可以在同一平台上追踪各模型版本的性能历史与漂移状态。

把漂移指标记录到 MLflow

import mlflow
from evidently.report import Report
from evidently.metrics import (
    DatasetDriftMetric,
    DataDriftTable,
    ColumnDriftMetric,
)
import json
from datetime import datetime

# 配置 MLflow 追踪服务器
mlflow.set_tracking_uri("http://mlflow.internal:5000")
mlflow.set_experiment("model-monitoring/fraud-detection-v2")

def log_drift_to_mlflow(
    reference_data,
    current_data,
    model_name: str,
    model_version: str,
    batch_id: str,
):
    """将漂移分析结果记录到 MLflow 的函数"""

    # 生成 Evidently 漂移报告
    drift_report = Report(metrics=[
        DatasetDriftMetric(),
        DataDriftTable(),
    ])
    drift_report.run(
        reference_data=reference_data,
        current_data=current_data,
    )

    result = drift_report.as_dict()
    drift_result = result["metrics"][0]["result"]

    # 以 MLflow Run 的形式记录
    with mlflow.start_run(run_name=f"drift-check-{batch_id}") as run:
        # 基础漂移指标
        mlflow.log_metric("dataset_drift_detected", int(drift_result["dataset_drift"]))
        mlflow.log_metric("drifted_columns_share", drift_result["share_of_drifted_columns"])
        mlflow.log_metric("number_of_drifted_columns", drift_result["number_of_drifted_columns"])
        mlflow.log_metric("total_columns", drift_result["number_of_columns"])

        # 记录各列的漂移分数
        column_drift = result["metrics"][1]["result"]["drift_by_columns"]
        for col_name, col_info in column_drift.items():
            safe_col_name = col_name.replace(" ", "_").replace("/", "_")
            mlflow.log_metric(
                f"drift_score_{safe_col_name}",
                col_info.get("drift_score", 0.0),
            )
            mlflow.log_metric(
                f"drift_detected_{safe_col_name}",
                int(col_info.get("column_drift", False)),
            )

        # 以标签形式记录元数据
        mlflow.set_tags({
            "monitoring.type": "drift_detection",
            "monitoring.model_name": model_name,
            "monitoring.model_version": model_version,
            "monitoring.batch_id": batch_id,
            "monitoring.timestamp": datetime.utcnow().isoformat(),
            "monitoring.reference_size": str(len(reference_data)),
            "monitoring.current_size": str(len(current_data)),
        })

        # 将 HTML 报告保存为工件
        report_path = f"/tmp/drift_report_{batch_id}.html"
        drift_report.save_html(report_path)
        mlflow.log_artifact(report_path, artifact_path="drift_reports")

        # 将 JSON 结果也保存为工件
        json_path = f"/tmp/drift_result_{batch_id}.json"
        with open(json_path, "w") as f:
            json.dump(result, f, indent=2, default=str)
        mlflow.log_artifact(json_path, artifact_path="drift_reports")

        print(f"漂移结果已记录到 MLflow。Run ID: {run.info.run_id}")
        return drift_result["dataset_drift"], drift_result["share_of_drifted_columns"]


# 使用示例
is_drifted, drift_share = log_drift_to_mlflow(
    reference_data=reference_data,
    current_data=current_drifted,
    model_name="fraud-detector",
    model_version="3",
    batch_id="2026-03-06-batch-001",
)

基于模型注册表别名(Alias)的管理

从 MLflow 2.x 开始,官方推荐使用基于别名(Alias)的模型管理,而不是传统的 Stage(Staging/Production/Archived)方式。可以据此制定一套策略,根据漂移检测结果自动切换模型别名。

from mlflow import MlflowClient

client = MlflowClient(tracking_uri="http://mlflow.internal:5000")

MODEL_NAME = "fraud-detector"

def handle_drift_detection(
    is_drifted: bool,
    drift_share: float,
    model_name: str = MODEL_NAME,
    drift_threshold_warn: float = 0.2,
    drift_threshold_critical: float = 0.5,
):
    """根据漂移检测结果执行模型注册表操作"""

    # 确认当前生产模型版本
    try:
        prod_version = client.get_model_version_by_alias(model_name, "production")
        current_version = prod_version.version
        print(f"当前生产模型版本: {current_version}")
    except Exception as e:
        print(f"查询生产模型别名失败: {e}")
        return

    if not is_drifted:
        print("未检测到漂移。维持当前模型。")
        client.set_model_version_tag(
            model_name, current_version,
            key="last_drift_check",
            value="passed",
        )
        return

    if drift_share >= drift_threshold_critical:
        # 临界漂移:立即切换到 fallback 模型 + 触发再训练
        print(f"CRITICAL: 漂移比例 {drift_share:.1%} - 切换 fallback 模型并触发再训练")
        client.set_model_version_tag(
            model_name, current_version,
            key="drift_status", value="critical",
        )
        # 如果存在 fallback 模型,则切换
        try:
            fallback = client.get_model_version_by_alias(model_name, "fallback")
            client.set_registered_model_alias(model_name, "production", fallback.version)
            print(f"已切换到 fallback 模型版本 {fallback.version}")
        except Exception:
            print("WARNING: 没有可用的 fallback 模型。需在维持当前模型的同时进行紧急再训练。")

        # 触发再训练(调用外部系统)
        trigger_retraining(model_name, reason="critical_drift")

    elif drift_share >= drift_threshold_warn:
        # 警告级别漂移:记录标签 + 告警
        print(f"WARNING: 漂移比例 {drift_share:.1%} - 加强监控并安排再训练")
        client.set_model_version_tag(
            model_name, current_version,
            key="drift_status", value="warning",
        )
        # 加入调度再训练队列
        schedule_retraining(model_name, priority="normal")


def trigger_retraining(model_name: str, reason: str):
    """触发紧急再训练(调用 Airflow DAG、Kubeflow Pipeline 等)"""
    print(f"再训练已触发: model={model_name}, reason={reason}")
    # requests.post("http://airflow.internal/api/v1/dags/retrain/dagRuns", ...)


def schedule_retraining(model_name: str, priority: str):
    """注册到调度再训练队列"""
    print(f"再训练调度已注册: model={model_name}, priority={priority}")


# 执行
handle_drift_detection(
    is_drifted=True,
    drift_share=0.55,
    model_name=MODEL_NAME,
)

6. 构建自动再训练流水线

从漂移检测到再训练的自动化流水线,由以下几个阶段构成。

整体流水线流程

  1. 调度器:在批量推理之后,或按固定周期(日/周)触发漂移检查
  2. 漂移分析器:用 Evidently 对参考数据与当前数据进行分析
  3. 判断引擎:基于漂移阈值决定是否需要再训练
  4. 再训练编排器:在 Airflow/Kubeflow 上执行训练 Job
  5. Champion/Challenger 评估:将新模型与现有模型进行对比评估
  6. 部署门禁:性能达标时自动部署,未达标时回滚

与 Airflow DAG 的集成模式

# Airflow DAG 示例:漂移检查 + 条件式再训练
# dag_drift_monitor.py

from airflow import DAG
from airflow.operators.python import PythonOperator, BranchPythonOperator
from airflow.operators.empty import EmptyOperator
from airflow.utils.dates import days_ago
from datetime import timedelta
import pandas as pd

default_args = {
    "owner": "ml-platform",
    "retries": 2,
    "retry_delay": timedelta(minutes=5),
    "execution_timeout": timedelta(minutes=30),
}

dag = DAG(
    dag_id="ml_drift_monitor_fraud_detection",
    default_args=default_args,
    description="每日漂移监控与条件式再训练",
    schedule_interval="0 6 * * *",  # 每天早上 6 点
    start_date=days_ago(1),
    catchup=False,
    tags=["ml-monitoring", "drift-detection"],
)


def fetch_data(**context):
    """加载参考数据与最近 24 小时的服务数据"""
    from sqlalchemy import create_engine
    engine = create_engine("postgresql://reader:password@db.internal/features")

    reference = pd.read_sql(
        "SELECT * FROM fraud_features_reference", engine
    )
    current = pd.read_sql(
        """SELECT * FROM fraud_features_serving
           WHERE created_at >= NOW() - INTERVAL '24 hours'""",
        engine,
    )

    # 通过 XCom 传递路径(大体量数据存放在 S3)
    ref_path = "/tmp/reference_data.parquet"
    cur_path = "/tmp/current_data.parquet"
    reference.to_parquet(ref_path)
    current.to_parquet(cur_path)

    context["ti"].xcom_push(key="reference_path", value=ref_path)
    context["ti"].xcom_push(key="current_path", value=cur_path)
    context["ti"].xcom_push(key="current_size", value=len(current))


def run_drift_check(**context):
    """执行 Evidently 漂移分析并记录到 MLflow"""
    from evidently.report import Report
    from evidently.metrics import DatasetDriftMetric, DataDriftTable
    import mlflow

    ti = context["ti"]
    ref_path = ti.xcom_pull(key="reference_path")
    cur_path = ti.xcom_pull(key="current_path")

    reference = pd.read_parquet(ref_path)
    current = pd.read_parquet(cur_path)

    # 校验最小样本数
    if len(current) < 100:
        print(f"当前数据样本数不足: {len(current)}。跳过漂移检查。")
        ti.xcom_push(key="drift_action", value="skip")
        return "skip_retraining"

    report = Report(metrics=[DatasetDriftMetric(), DataDriftTable()])
    report.run(reference_data=reference, current_data=current)
    result = report.as_dict()

    drift_detected = result["metrics"][0]["result"]["dataset_drift"]
    drift_share = result["metrics"][0]["result"]["share_of_drifted_columns"]

    # 记录到 MLflow
    mlflow.set_tracking_uri("http://mlflow.internal:5000")
    mlflow.set_experiment("monitoring/fraud-detection")
    with mlflow.start_run(run_name=f"drift-{context['ds']}"):
        mlflow.log_metric("drift_detected", int(drift_detected))
        mlflow.log_metric("drift_share", drift_share)

    ti.xcom_push(key="drift_detected", value=drift_detected)
    ti.xcom_push(key="drift_share", value=drift_share)


def decide_action(**context):
    """根据漂移水平决定是否再训练"""
    ti = context["ti"]
    drift_detected = ti.xcom_pull(key="drift_detected")
    drift_share = ti.xcom_pull(key="drift_share")

    if drift_share is None or drift_share < 0.2:
        return "skip_retraining"
    elif drift_share >= 0.5:
        return "trigger_emergency_retrain"
    else:
        return "trigger_scheduled_retrain"


fetch_task = PythonOperator(
    task_id="fetch_data", python_callable=fetch_data, dag=dag,
)
drift_task = PythonOperator(
    task_id="run_drift_check", python_callable=run_drift_check, dag=dag,
)
branch_task = BranchPythonOperator(
    task_id="decide_action", python_callable=decide_action, dag=dag,
)
skip_task = EmptyOperator(task_id="skip_retraining", dag=dag)
scheduled_retrain = EmptyOperator(task_id="trigger_scheduled_retrain", dag=dag)
emergency_retrain = EmptyOperator(task_id="trigger_emergency_retrain", dag=dag)

fetch_task >> drift_task >> branch_task >> [skip_task, scheduled_retrain, emergency_retrain]

再训练触发阈值指南

漂移水平drift_share 范围建议措施响应时间
正常0% ~ 15%维持监控-
注意15% ~ 30%发送告警,开始原因分析48 小时内
警告30% ~ 50%加入调度再训练队列24 小时内
危险50% 以上立即再训练 + 切换 fallback 模型立即

注意:阈值应根据领域与模型特性进行调整。像金融欺诈检测这类漏检成本很高的领域,适合采用更低的阈值(10~20%);而像推荐系统这类容忍范围较宽的领域,适合采用更高的阈值(30~50%)。

7. 监控工具对比:Evidently vs NannyML vs WhyLabs vs Alibi Detect

生产环境 ML 监控工具有多种选择。下面对比各工具的优势与劣势。

标准Evidently AINannyMLWhyLabsAlibi Detect
许可证Apache 2.0 (OSS)BSD-3 (OSS)SaaS + 免费层BSD-3 (OSS)
核心优势通用数据/模型监控无标签性能估计 (CBPE)实时流式数据画像高级漂移检测算法
漂移检测方法数20+10+15+15+
无标签性能估计有限核心功能(CBPE、DLE)不支持不支持
实时监控Collector 模式不支持(批处理)原生支持不支持(批处理)
可视化内置 HTML/仪表盘内置 HTMLWeb 仪表盘(SaaS)基础可视化
CI/CD 集成Test Suite(原生)有限基于 API需手动配置
Prometheus 集成官方支持需自定义内置需自定义
MLflow 集成简单(Python 原生)手动配置API 集成手动配置
学习曲线中等低(SaaS)
生产使用场景通用标签延迟环境大规模实时研究/高级检测

选型指南

  • 无法立即获得标签的环境(例如金融欺诈检测中,标签确认要经过数月):NannyML 基于 CBPE(Confidence-Based Performance Estimation) 的性能估计是唯一可行的选择。
  • 优先开源 + 快速落地:Evidently AI 提供最广的功能范围,且上手难度最低。
  • 大规模实时流式场景:WhyLabs 的数据画像针对每秒数万条的处理量做了优化。
  • 需要高级统计检测的研究环境:Alibi Detect 的深度核 MMD、Learned Kernel 漂移检测方法比较合适。

8. Grafana/Prometheus 仪表盘搭建

下面来看如何把 Evidently 的监控结果暴露为 Prometheus 指标,并在 Grafana 仪表盘上以时间序列形式可视化。

Prometheus 指标导出

# prometheus_drift_exporter.py
from prometheus_client import start_http_server, Gauge, Counter, Histogram
from evidently.report import Report
from evidently.metrics import DatasetDriftMetric, DataDriftTable
import pandas as pd
import time
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# 定义 Prometheus 指标
DRIFT_DETECTED = Gauge(
    "ml_model_drift_detected",
    "数据集是否检测到漂移(0/1)",
    ["model_name", "model_version"],
)
DRIFT_SHARE = Gauge(
    "ml_model_drift_column_share",
    "检测到漂移的列比例",
    ["model_name", "model_version"],
)
COLUMN_DRIFT_SCORE = Gauge(
    "ml_model_column_drift_score",
    "各列的漂移分数",
    ["model_name", "model_version", "column_name"],
)
DRIFT_CHECK_TOTAL = Counter(
    "ml_model_drift_checks_total",
    "漂移检查执行次数",
    ["model_name"],
)
DRIFT_CHECK_DURATION = Histogram(
    "ml_model_drift_check_duration_seconds",
    "漂移检查耗时",
    ["model_name"],
    buckets=[0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0],
)

MODEL_NAME = "fraud-detector"
MODEL_VERSION = "3"


def run_periodic_drift_check(
    reference_path: str,
    current_query_fn,
    interval_seconds: int = 300,
):
    """周期性漂移检查并更新 Prometheus 指标"""
    reference = pd.read_parquet(reference_path)

    while True:
        try:
            start_time = time.time()

            # 加载最近数据
            current = current_query_fn()
            if current is None or len(current) < 50:
                logger.warning(f"当前数据不足: {len(current) if current is not None else 0} 条")
                time.sleep(interval_seconds)
                continue

            # 仅过滤特征列(排除目标列与元数据列)
            feature_cols = [c for c in reference.columns if c not in ["target", "id", "timestamp"]]
            ref_features = reference[feature_cols]
            cur_features = current[feature_cols]

            # 漂移分析
            report = Report(metrics=[DatasetDriftMetric(), DataDriftTable()])
            report.run(reference_data=ref_features, current_data=cur_features)
            result = report.as_dict()

            drift_result = result["metrics"][0]["result"]
            column_results = result["metrics"][1]["result"]["drift_by_columns"]

            # 更新 Prometheus 指标
            DRIFT_DETECTED.labels(MODEL_NAME, MODEL_VERSION).set(
                int(drift_result["dataset_drift"])
            )
            DRIFT_SHARE.labels(MODEL_NAME, MODEL_VERSION).set(
                drift_result["share_of_drifted_columns"]
            )

            for col_name, col_info in column_results.items():
                COLUMN_DRIFT_SCORE.labels(MODEL_NAME, MODEL_VERSION, col_name).set(
                    col_info.get("drift_score", 0.0)
                )

            DRIFT_CHECK_TOTAL.labels(MODEL_NAME).inc()

            duration = time.time() - start_time
            DRIFT_CHECK_DURATION.labels(MODEL_NAME).observe(duration)

            logger.info(
                f"漂移检查完成: drift={drift_result['dataset_drift']}, "
                f"share={drift_result['share_of_drifted_columns']:.2%}, "
                f"duration={duration:.1f}s"
            )

        except Exception as e:
            logger.error(f"漂移检查失败: {e}", exc_info=True)

        time.sleep(interval_seconds)


if __name__ == "__main__":
    # 启动 Prometheus 指标 HTTP 服务器(端口 8000)
    start_http_server(8000)
    logger.info("Prometheus 指标导出器已启动(端口 8000)")

    # 启动周期性漂移检查(间隔 5 分钟)
    run_periodic_drift_check(
        reference_path="/data/reference/fraud_features_v3.parquet",
        current_query_fn=lambda: pd.read_parquet("/data/serving/latest_batch.parquet"),
        interval_seconds=300,
    )

Grafana 仪表盘组成

在 Grafana 中配置以下面板,综合监控 ML 模型的健康状态。

面板指标可视化类型告警规则
漂移状态ml_model_drift_detectedStat(最新值)值为 1 时触发 Critical 告警
漂移列比例趋势ml_model_drift_column_shareTime Series超过 30% 时触发 Warning
各列漂移分数ml_model_column_drift_scoreHeatmap高亮超过阈值的列
检查耗时ml_model_drift_check_duration_secondsHistogram超过 60 秒时触发 Warning
检查执行次数rate(ml_model_drift_checks_total[1h])Time Series为 0 时告警检查已停止

Alertmanager 告警规则示例

# prometheus-alerts.yaml
groups:
  - name: ml_model_drift_alerts
    rules:
      - alert: MLModelDriftDetected
        expr: ml_model_drift_detected == 1
        for: 5m
        labels:
          severity: warning
          team: ml-platform
        annotations:
          summary: 'ML 模型数据漂移检测'
          description: '模型 {{ $labels.model_name }} v{{ $labels.model_version }} 检测到数据漂移。漂移列比例: {{ $value }}'

      - alert: MLModelCriticalDrift
        expr: ml_model_drift_column_share > 0.5
        for: 0m
        labels:
          severity: critical
          team: ml-platform
        annotations:
          summary: 'ML 模型临界漂移 - 需要立即处理'
          description: '模型 {{ $labels.model_name }} 的漂移列比例为 {{ $value | humanizePercentage }}。需要立即再训练或切换 fallback 模型。'

      - alert: MLDriftCheckStalled
        expr: rate(ml_model_drift_checks_total[1h]) == 0
        for: 30m
        labels:
          severity: warning
          team: ml-platform
        annotations:
          summary: 'ML 漂移检查已中断'
          description: '模型 {{ $labels.model_name }} 的漂移检查已超过 30 分钟未执行。需要检查监控流水线。'

9. 运维注意事项

假阳性(False Positive)漂移管理

统计漂移检测最常见的陷阱就是假阳性。尤其在以下情况下,即便实际没有问题也可能被误判为漂移。

样本量效应:当前数据样本量非常大时,KS 检验或 Chi-squared 检验会把统计学上显著、但实际意义不大的差异也判定为漂移。应同时使用 PSI、Wasserstein 距离等基于效应量(effect size)的指标,以验证其实际意义上的显著性。

季节性(Seasonality):电商领域中黑色星期五期间的购买模式与平时明显不同。如果把这种情况判定为漂移,每年同一时期都会出现不必要的告警风暴。应将参考数据设置为同期的历史数据,或者应用季节性调整逻辑。

特征间相关性:仅在单个特征层面检测漂移,无法捕捉多变量分布的变化。有些情况下特征 A 与 B 各自的分布都很相似,但 A-B 之间的相关性发生了变化。Evidently 的 DatasetDriftMetric 提供的是整体数据集层面的判断,如果需要显式的多变量检测,应考虑 Alibi Detect 的 MMD(Maximum Mean Discrepancy) 方法。

参考数据管理策略

参考数据是漂移检测的基准线。错误的参考数据会使所有检测结果失效。

策略说明适用情况注意事项
固定训练数据将模型训练所用的数据固定为参考数据稳定领域、变化较少的环境随时间推移,参考数据本身会过时
滑动窗口用最近 N 天/N 周的数据更新参考数据渐进式变化属正常的环境有可能错过渐进式漂移
再训练时更新每次模型再训练时更新参考数据具有定期再训练的流水线依赖于再训练周期
双重基线同时对比训练数据与最近稳定期数据需要高准确度的环境管理复杂度增加

关键点:应对参考数据进行版本管理,并与模型版本建立 1:1 对应关系,确保可追溯。建议以 MLflow 工件(artifact)的形式保存参考数据快照。

与特征存储的集成

如果离线训练时点与线上服务时点的特征计算逻辑不一致(Training-Serving Skew),就会出现并非真实漂移、而是由实现不一致造成的假漂移。使用 Feast 之类的特征存储来保证训练/服务之间的特征一致性,才是根本的解决方案。

10. 故障案例与恢复流程

案例 1:静默的模型性能退化(Silent Model Degradation)

情况:某电商推荐模型在 3 个月内逐渐性能退化,CTR 从 12% 降至 7%,但由于漂移监控只设置在单个特征层面,未能检测出来。

原因:用户行为模式发生了多变量变化。单个特征(浏览次数、停留时长、品类占比)各自的分布都没有大幅变化,但特征之间的相关性发生了改变。尤其是"停留时长-购买转化"这一关系,因短视频内容消费模式的变化而被削弱。

恢复流程

  1. 增加多变量漂移检测(比较特征相关性矩阵)
  2. 增加直接监控业务 KPI(CTR、转化率)的概念漂移监控层
  3. 用最近 2 周的数据再训练模型,并通过 A/B 测试部署
  4. 将再训练周期从每月一次缩短为每周一次

教训:仅靠数据漂移难以捕捉概念漂移,必须同时并行监控业务指标。

案例 2:数据管道故障导致的假漂移

情况:某个周五深夜,漂移 Critical 告警集中爆发,3 个模型同时检测到超过 80% 的列漂移。

原因:上游数据管道的 ETL 作业失败,导致服务特征表中的部分列被填充为默认值(0 或 null)。这是一起数据质量问题被误判为漂移的案例。

恢复流程

  1. 在 Evidently TestSuite 中,把 TestNumberOfMissingValuesTestShareOfOutRangeValues 放在漂移检查之前的阶段
  2. 数据质量检查失败时跳过漂移检查,并单独发送数据管道告警
  3. 在上游 ETL 中增加数据完整性校验门禁
  4. 在漂移告警中附上"最近数据质量检查结果"信息

教训:漂移检测流水线之前必须放置数据质量校验环节。区分数据质量问题与真实的分布变化,是运维的核心。

案例 3:参考数据污染

情况:模型再训练后,用新数据更新了参考数据。此后完全检测不到任何漂移,导致监控形同虚设。

原因:用于再训练的数据本身已经含有漂移,而这份被污染的数据成为了新的参考数据。结果就是漂移被"正常化",基准线被重新设定成了错误的状态。

恢复流程

  1. 在更新参考数据时,自动对比新旧参考数据之间的漂移
  2. 增加门禁:当漂移比例超过一定水平时,阻止参考数据更新
  3. 将参考数据的变更历史以 MLflow 工件形式做版本管理
  4. 定期与黄金数据集(经人工校验的高质量数据)进行比较

教训:参考数据是监控系统的基准线,任何变更都必须经过校验流程。

11. 生产监控检查清单

部署前检查清单

  • 参考数据是否与模型版本一起做了版本管理
  • Evidently Report/TestSuite 是否已集成进部署流水线
  • 漂移阈值是否根据领域特性做了调整
  • 数据质量校验是否放置在漂移检测之前的阶段
  • fallback 模型是否已在注册表中注册
  • Grafana 仪表盘与告警规则是否已配置

运行中检查清单

  • 漂移检查是否按正常周期执行("监控者的监控")
  • 假阳性比例是否处于可控水平(建议每月不超过 5 起)
  • 参考数据是否在合适的时点得到更新
  • 再训练触发是否正常运作,是否进行了 Champion/Challenger 评估
  • 业务 KPI 与模型性能指标是否被一并追踪
  • 收到告警后的平均响应时间(MTTR)是否在 SLA 范围内

概念漂移应对检查清单

  • 是否搭建了标签获取流水线(含延迟标签)
  • 是否在监控模型性能指标(Accuracy、F1、AUC)的时间序列走势
  • 是否为没有标签的时间段定义了代理指标(proxy metric)
  • A/B 测试基础设施是否已就绪

12. 参考资料

  1. Evidently AI - Data Drift 官方指南 - 涵盖数据漂移的概念、检测方法论与实际案例的综合指南。
  2. Evidently AI GitHub 仓库 - 开源代码、示例笔记本、社区讨论。
  3. MLflow Model Registry 官方文档 - 模型注册表 API、别名系统、部署工作流指南。
  4. Evidently AI 官方文档 - Report、TestSuite、Metric 的完整 API 参考与教程。
  5. Advanced ML Model Monitoring: Drift Detection, Explainability, and Automated Retraining - 漂移检测与自动再训练流水线的高级模式。
  6. Google - ML Technical Debt (Hidden Technical Debt in Machine Learning Systems) - 关于 ML 系统技术债务与监控必要性的奠基性论文。
  7. NannyML - Estimating Model Performance without Ground Truth - 无需标签即可估计模型性能的 CBPE 方法论。

测验

Q1:《ML 模型监控与漂移检测:Evidently AI + MLflow 生产运维指南》一文主要讨论的内容是什么?

一份综合指南,涵盖用 Evidently AI 与 MLflow 构建 ML 模型生产监控流水线、数据/概念漂移检测、自动再训练触发, 直至运维故障排查。

Q2:漂移的种类:变化的是什么,指的是什么? 漂移(Drift)指的是模型训练时的数据分布与实际服务(serving)时点的数据分布之间的不一致。漂移根据发生位置与 性质,大致可分为三类。

Q3:请说明 Evidently AI 的架构与核心功能。 Evidently AI 是一个用于 ML 模型监控与数据质量校验的开源库。它运行在 Python 原生环境中,内置了 20 多种 统计漂移检测方法。核心组成 Report:一次性数据分析报告。

Q4:Evidently AI 实战用法有哪些要点? 安装与基础设置 用 Test Suite 实现自动化数据质量校验

Q5:MLflow 模型注册表与监控集成是如何运作的? MLflow 提供实验追踪、模型打包与模型注册表功能。通过将 Evidently 的漂移检测结果记录到 MLflow,可以在 同一平台上追踪各模型版本的性能历史与漂移状态。

현재 단락 (1/529)

ML 模型的准确率在部署那一刻达到峰值。此后,随着现实世界的变化,预测质量会逐渐下降。问题在于,这种劣化是在没有明确错误的情况下发生的。不会抛出 HTTP 500,日志里不会出现 CRITICAL,服...

작성 글자: 0원문 글자: 21,607작성 단락: 0/529