实验追踪与模型注册:MLflow、W&B 与可复现性工程

没有实验追踪的机器学习项目最终会退化成手工表格与凭记忆复现,本文系统讲解实验追踪的数据模型(Run/参数/指标/产物)、MLflow 的 Tracking 与 Model Registry 架构、W&B 的实时可视化与协作能力、指标与产物管理的工程规范、Optuna/Ray Tune 超参搜索与早停策略、模型注册的版本与阶段流转、可复现性的代码数据环境三要素、实验对比与统计显著性判断,以及与 CI/CD 流水线的集成与生产踩坑清单。

机器学习项目最容易失控的地方不是模型,而是实验。同一个想法改三次超参、换两版数据、跑五个随机种子,两周后没人说得清哪个 checkpoint 对应哪次运行。实验追踪就是把这件事变成可查询、可复现、可审计的工程系统。

为什么需要实验追踪

先看没有追踪时会发生什么。

手工管理的失效模式

  • Excel 记账:跑一次记一行,超参列越加越多,最后没人愿意维护。
  • 文件名编码:model_lr3e4_bs32_v2_final_final.pt——命名规则迟早失控。
  • 凭记忆复现:三周后想复现最好的结果,记不清当时改了什么。
  • 无法对比:想知道「是数据变了还是超参变了导致指标提升」,无从回答。

这些问题的本质是:实验的状态没有单一可信来源。

追踪系统的四类价值

价值具体表现
可复现从 Run 记录还原完整环境与参数
可对比并行曲线对齐,一眼看出差异
可审计哪个模型上的线、用了哪份数据
可协作团队共享结果,不靠口口相传

最小可用标准

一个团队不需要一上来就上平台,但至少要满足:

  1. 每次运行有唯一 ID。
  2. 参数、指标、产物三者绑定在同一 ID 下。
  3. 代码版本(Git commit)记录在案。
  4. 数据版本可追溯。

做到这四条,就已经避开了绝大多数灾难。

实验追踪的核心数据模型

所有主流工具的数据模型高度一致,理解这套模型后换工具几乎没有学习成本。

Run 与 Experiment

  • Run:一次训练执行,是追踪的最小单元。每个 Run 有唯一 ID、开始结束时间、状态(running/finished/failed)。
  • Experiment:一组相关 Run 的集合,通常对应一个项目或一个研究问题。

层级关系是 Project → Experiment → Run → (Params, Metrics, Artifacts)。

参数、指标与产物

三类数据各有特性:

类型特性存储方式
参数(Params)一次 Run 内不变键值对,可检索
指标(Metrics)随时间变化,多步时间序列
产物(Artifacts)大文件对象存储 + 引用

一个常见的错误是把所有东西都当参数记录:数据集路径、超参、模型结构、环境变量混在一起。正确的做法是分类:

  • 超参:学习率、batch size、层数——用于对比和搜索。
  • 配置:数据路径、随机种子——用于复现。
  • 标签:实验阶段、负责人、目的——用于筛选。

标签与分组

给 Run 打标签(Tag)是团队协作的关键。建议固定几类标签:

stage: baseline | ablation | tuning | final
owner: alice
dataset: v3-2026-09
purpose: test-scaling-law

有了标签,才能回答「只看数据 v3 上 stage=ablation 的所有运行」这类问题。

MLflow 的架构与用法

MLflow 是开源生态里事实标准的实验追踪方案,四大组件各司其职。

四大组件

  • Tracking:记录参数、指标、产物。
  • Projects:打包可复现的运行环境(含 conda 环境与入口点)。
  • Models:统一模型格式,支持多种推理后端。
  • Model Registry:模型版本与阶段管理。

自动记录

最省事的用法是 autolog,一行代码接管主流框架的记录:

import mlflow
import mlflow.pytorch

mlflow.set_tracking_uri("http://mlflow.internal:5000")
mlflow.set_experiment("recsys-ranking")

with mlflow.start_run(run_name="din-v2-lr1e-4") as run:
    mlflow.log_params({
        "lr": 1e-4, "batch_size": 512, "epochs": 10,
        "hidden_dim": 256, "num_layers": 3, "seed": 42,
    })
    mlflow.log_param("git_commit", get_git_commit())

    for epoch in range(10):
        train_loss = train_one_epoch(model, loader)
        val_auc = evaluate(model, val_loader)
        mlflow.log_metrics(
            {"train_loss": train_loss, "val_auc": val_auc}, step=epoch)

    mlflow.log_artifact("configs/train.yaml")
    mlflow.pytorch.log_model(model, "model")
    print(f"run_id = {run.info.run_id}")

step 参数让指标形成时间序列,MLflow UI 会把它们画成曲线。

模型注册与阶段流转

Model Registry 把「一堆 checkpoint 文件」变成「有版本的模型资产」:

import mlflow.pytorch
from mlflow.tracking import MlflowClient

client = MlflowClient()

# 注册模型:同名模型自动累加版本号
model_uri = f"runs:/{run_id}/model"
mv = mlflow.register_model(model_uri, "recsys-ranking")
print(f"注册版本:{mv.version}")

# 阶段流转:None → Staging → Production → Archived
client.transition_model_version_stage(
    name="recsys-ranking", version=mv.version, stage="Staging")

阶段流转的语义:

阶段含义谁在用
None刚注册,未验证无
Staging预发验证中测试环境
Production线上服务中推理服务
Archived已下线无

关键纪律是:推理服务只加载 Production 阶段的模型,绝不硬编码文件路径。这样上线与回滚都变成一次 transition_model_version_stage 调用。

自建服务的取舍

MLflow 可以本地跑(mlflow ui),也可以部署到服务器配数据库与对象存储:

mlflow server \
  --backend-store-uri postgresql://user:pw@db:5432/mlflow \
  --default-artifact-root s3://ml-artifacts/ \
  --host 0.0.0.0 --port 5000
  • backend store:存元数据(Run、参数、指标),用 PostgreSQL 而非默认的 SQLite——SQLite 在并发写入时会锁库。
  • artifact store:存大文件,用 S3/MinIO 而非本地磁盘。

W&B 与可视化对比

Weights and Biases 是商业化程度最高的追踪平台,强项在可视化与协作。

实时曲线与仪表盘

W&B 的曲线是实时推送的,长训练任务可以边跑边看。支持对数轴、平滑、多 Run 叠加对比,是调参时最顺手的工具。

import wandb

wandb.init(
    project="recsys-ranking",
    name="din-v2-lr1e-4",
    config={"lr": 1e-4, "batch_size": 512, "hidden_dim": 256, "seed": 42},
    tags=["ablation", "dataset-v3"],
)

for epoch in range(10):
    train_loss = train_one_epoch(model, loader)
    val_auc = evaluate(model, val_loader)
    wandb.log({"train_loss": train_loss, "val_auc": val_auc}, step=epoch)

wandb.log({"confusion_matrix": wandb.plot.confusion_matrix(
    y_true=labels, preds=preds)})
wandb.finish()

超参面板与 Sweep

W&B 的 Sweep 把超参搜索做成了声明式配置:

sweep_config = {
    "method": "bayes",                 # grid / random / bayes
    "metric": {"name": "val_auc", "goal": "maximize"},
    "parameters": {
        "lr": {"distribution": "log_uniform_values", "min": 1e-5, "max": 1e-3},
        "batch_size": {"values": [128, 256, 512]},
        "hidden_dim": {"values": [128, 256, 512]},
        "dropout": {"distribution": "uniform", "min": 0.1, "max": 0.5},
    },
    "early_terminate": {"type": "hyperband", "min_iter": 3},
}

early_terminate 让表现差的 Run 提前终止,能把搜索成本降低一半以上。

与 MLflow 的选型

维度MLflowW&B
部署可完全自建SaaS 为主,支持私有化
可视化基础够用强,实时且丰富
协作一般强,评论与分享
成本自建运维成本按席位收费
生态开源标准,集成广集成也广,SDK 更顺手
数据主权完全可控需评估合规

选型建议:有数据合规要求或想完全自控,选 MLflow 自建;团队小、追求效率、无强合规约束,选 W&B。两者不互斥,也有团队用 W&B 做日常调试、用 MLflow Registry 做上线管理。

指标与产物的管理

追踪系统的价值取决于记录的质量。

记录什么指标

至少记录三类:

  • 训练指标:loss、学习率、梯度范数——用于诊断训练是否健康。
  • 验证指标:任务指标(AUC/F1/BLEU)——用于选模型。
  • 系统指标:GPU 利用率、显存、吞吐——用于发现瓶颈。
import torch

def log_system_metrics(step):
    if torch.cuda.is_available():
        mlflow.log_metrics({
            "gpu_mem_allocated_gb": torch.cuda.memory_allocated() / 1e9,
            "gpu_mem_reserved_gb": torch.cuda.memory_reserved() / 1e9,
            "gpu_util": get_gpu_utilization(),
        }, step=step)

梯度范数尤其值得记:它是最早暴露「梯度爆炸」的信号。

产物管理规范

产物(Artifact)包括模型权重、配置、词表、混淆矩阵图、样本预测。规范:

  • 配置必须存:config.yaml 是复现的第一入口,务必随 Run 保存。
  • 模型存格式而非框架对象:存 state_dict 或 ONNX,别 pickle 整个模型类。
  • 大文件用引用:数据集不要塞进产物系统,存路径与哈希即可。
  • 产物加版本:数据集用 DVC 或 LakeFS 管理版本,追踪系统里只存版本号。
import hashlib, json

def dataset_fingerprint(paths):
    h = hashlib.sha256()
    for p in sorted(paths):
        with open(p, "rb") as f:
            while chunk := f.read(1 << 20):
                h.update(chunk)
    return h.hexdigest()[:16]

超参搜索与调优

实验追踪与超参搜索是天然搭配:搜索产生的每一次尝试都是一个 Run。

搜索策略对比

策略原理适用
网格搜索笛卡尔积维度少(≤3)
随机搜索随机采样维度中等,性价比最高
贝叶斯优化代理模型指导采样评估昂贵,维度中等
Hyperband早停 + 逐次减半训练耗时长
PBT种群训练 + 变异大规模并行

经验结论:随机搜索在 3 维以上几乎总是优于网格搜索,因为高维网格里大部分维度是浪费的。

Optuna 实战

import optuna

def objective(trial):
    lr = trial.suggest_float("lr", 1e-5, 1e-3, log=True)
    hidden = trial.suggest_categorical("hidden_dim", [128, 256, 512])
    dropout = trial.suggest_float("dropout", 0.1, 0.5)

    with mlflow.start_run(nested=True):
        mlflow.log_params({"lr": lr, "hidden_dim": hidden, "dropout": dropout})
        model = build_model(hidden_dim=hidden, dropout=dropout)
        best_auc = 0.0
        for epoch in range(20):
            train_one_epoch(model, lr)
            auc = evaluate(model)
            mlflow.log_metric("val_auc", auc, step=epoch)
            best_auc = max(best_auc, auc)
            trial.report(auc, epoch)
            if trial.should_prune():          # 中位数剪枝
                raise optuna.TrialPruned()
        return best_auc

study = optuna.create_study(
    direction="maximize",
    sampler=optuna.samplers.TPESampler(seed=42),
    pruner=optuna.pruners.MedianPruner(n_startup_trials=5),
)
study.optimize(objective, n_trials=100, n_jobs=4)
print(study.best_params, study.best_value)

早停与资源分配

搜索的最大成本是把算力浪费在注定失败的配置上。剪枝策略:

  • 中位数剪枝:若某 Trial 在 epoch k 的指标低于已完成 Trial 同期的中位数,直接终止。
  • Hyperband:多轮逐次减半,把预算集中在有希望的配置上。
  • 固定小预算预热:先在小数据集/少 epoch 上筛一轮,再对前 20% 配置做全量训练。

模型注册与阶段流转

模型注册是实验追踪与生产部署的交接面。

版本与别名的设计

除了数字版本号,主流做法是引入别名(Alias):

client.set_registered_model_alias(
    name="recsys-ranking", alias="champion", version=7)
client.set_registered_model_alias(
    name="recsys-ranking", alias="challenger", version=8)

别名让推理服务加载 models:/recsys-ranking@champion,切换模型只需改别名,不需要改代码或重启配置。这是蓝绿发布与灰度的基础。

模型卡与元数据

每个注册版本应附带模型卡(Model Card):

client.update_model_version(
    name="recsys-ranking", version=7,
    description=(
        "训练数据:行为日志 v3 (2026-08~2026-09),2.1 亿样本。"
        "离线 AUC 0.812,线上 CTR +2.3%。"
        "已知限制:新用户冷启动样本不足,长尾类目召回偏低。"
    ),
)

模型卡是事故复盘时的第一手资料,也是合规审计的必需项。

上线与回滚流程

标准流程:

  1. 训练完成,Run 记录完整 → 注册新版本,阶段设为 Staging。
  2. 预发环境加载 @challenger,跑回归测试与影子流量。
  3. 指标达标 → 别名切到 @champion,阶段设为 Production。
  4. 线上异常 → 别名切回上一版本,回滚是秒级的。

可复现性的三要素

可复现性是实验追踪的终极目的,它依赖三个要素同时被固定。

代码、数据、环境

可复现 = 固定的代码版本 + 固定的数据版本 + 固定的运行环境 + 固定随机种子

四者缺一不可:

  • 代码:记录 Git commit hash,并确保工作区无未提交改动。
  • 数据:记录数据集指纹(哈希)或版本号,而非仅路径。
  • 环境:记录依赖版本,用锁文件(requirements.lock / poetry.lock)而非宽松范围。
  • 随机种子:Python、NumPy、PyTorch、CUDA 全部设种子。
import os, random
import numpy as np
import torch

def set_seed(seed=42):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    os.environ["PYTHONHASHSEED"] = str(seed)
    # 注意:完全确定性会损失性能,按需开启
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False

确定性的代价

严格确定性会禁用 cuDNN 的自动算法选择,训练速度可能下降 10%~30%。工程上的折中是:

  • 实验阶段:开确定性,保证对比公平。
  • 生产训练:关确定性,换速度,但记录种子以便事后排查。

另外,多卡训练的浮点归约顺序不确定,即使固定种子也无法逐位复现。此时应把可复现性定义为「指标在容差内一致」,而非「逐位相同」。

环境快照

# 记录完整环境快照
import subprocess, platform, torch

env_info = {
    "python": platform.python_version(),
    "torch": torch.__version__,
    "cuda": torch.version.cuda,
    "gpu": torch.cuda.get_device_name(0) if torch.cuda.is_available() else "cpu",
    "pip_freeze": subprocess.check_output(["pip", "freeze"]).decode(),
}
mlflow.log_dict(env_info, "environment.json")

pip freeze 的快照在排查「昨天还好今天不行」类问题时价值极高。

实验对比与决策方法

有了追踪数据,还需要正确的方法论才能做出正确决策。

公平对比的前提

对比两个 Run 之前,必须确认只有想对比的变量不同:

必须一致的项常见失误
数据版本悄悄换了数据集
评估脚本指标口径变了
随机种子数只跑一次就下结论
硬件与精度FP16 对比 FP32
训练步数一个训得久一个训得短

统计显著性

单次运行的指标差异不能说明问题。正确做法:

  • 多种子:至少 3~5 个种子,报告均值与标准差。
  • 置信区间:用 t 检验或 bootstrap 估计差异的置信区间。
  • 看曲线而非终值:终值可能受偶然波动影响,曲线的整体走势更可靠。
import numpy as np
from scipy import stats

def compare_runs(a_scores, b_scores):
    """对比两组多种子结果,返回差异与 p 值"""
    t, p = stats.ttest_ind(a_scores, b_scores, equal_var=False)
    diff = np.mean(a_scores) - np.mean(b_scores)
    return {"diff": diff, "p_value": p,
            "significant": p < 0.05,
            "a_mean": np.mean(a_scores), "b_mean": np.mean(b_scores)}

经验法则:AUC 提升小于 0.002、准确率提升小于 0.5% 的差异,在单种子下基本不可信。

消融实验的设计

消融实验要回答「哪个组件真正起作用」。设计要点:

  • 一次只改一个变量,否则无法归因。
  • 保留完整基线:所有消融都相对同一个基线比较。
  • 记录负结果:无效的尝试同样有价值,避免团队重复踩坑。

与 CI/CD 和特征平台的集成

实验追踪不是孤岛,要和上下游打通。

训练流水线集成

典型流水线:数据校验 → 训练 → 评估 → 注册 → 部署。

# 训练流水线的关键步骤(伪配置)
# 1) 数据校验:检查分布漂移与缺失率,不通过则中止
# 2) 训练:自动记录 Run,产出指标与模型
# 3) 评估:与 champion 模型对比,未显著优于则不上线
# 4) 注册:指标达标才注册新版本并设为 challenger
# 5) 部署:预发验证通过后切 champion

「未显著优于现有模型就不上线」 这条门禁极其重要,它把「实验对比的纪律」固化进了流水线。

特征与数据版本

实验追踪必须能回答「这次训练用的是哪版特征」。与特征平台集成的做法:

  • 特征定义纳入 Git 管理,与模型代码同版本。
  • 训练时记录特征视图版本号与时间范围。
  • 在线推理使用同一特征定义,避免训练服务不一致。

告警与监控联动

追踪系统还可以充当训练监控:设置「loss 超过阈值」或「Run 超过预期时长」的告警,训练异常时主动通知,而不是等人发现。

生产踩坑清单

  • SQLite 后端扛不住并发:多个训练任务同时写会锁库。生产必须用 PostgreSQL。
  • 产物存本地磁盘:容器重启后产物全丢。必须用对象存储。
  • 只记最终指标不记曲线:无法诊断训练过程,也看不出是否过拟合。
  • 超参记录不全:漏记一个 seed 或数据版本,就永远复现不出来。
  • 模型文件硬编码路径:上线与回滚都要改代码。用 Registry 别名。
  • 实验数据无标签无组织:几百个 Run 混在一起,找不出有效信息。
  • 不记录评估脚本版本:指标口径变了却不知道,历史对比全部失效。
  • 单种子下结论:把噪声当成了改进。
  • 产物无限增长:从不清理旧 Run 的大文件,存储成本失控。设保留策略。
  • 追踪系统与生产环境网络不通:训练集群访问不到 tracking server,只能关掉记录。上线前先打通。

总结

实验追踪的工程主线是「让每一次运行都有唯一身份、完整记录、可被检索与复现」:数据模型上,Run 绑定参数、指标、产物三类数据;工具上,MLflow 提供可自建的 Tracking 与 Model Registry,W&B 提供更强的实时可视化与协作;方法上,超参搜索要配剪枝、模型对比要多随机种子并做显著性检验。模型注册把实验与生产连起来,别名机制让上线与回滚变成秒级操作。而这一切的底线是可复现性三要素(代码、数据、环境)加固定种子——没有可复现性,追踪系统就只是一堆好看但无用的曲线。记住一句话:实验追踪的投入产出比,与团队的实验规模成正比;但即使只有一个人,从第一天起就该记录。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ai」更多文章

  1. GPU 共享与调度:MPS、MIG 与多租户隔离
  2. 异构推理硬件:ROCm、Intel 与国产 NPU 适配实践
  3. 前缀缓存与语义缓存:KV 复用与重复计算消除