机器学习项目最容易失控的地方不是模型,而是实验。同一个想法改三次超参、换两版数据、跑五个随机种子,两周后没人说得清哪个 checkpoint 对应哪次运行。实验追踪就是把这件事变成可查询、可复现、可审计的工程系统。
为什么需要实验追踪
先看没有追踪时会发生什么。
手工管理的失效模式
- Excel 记账:跑一次记一行,超参列越加越多,最后没人愿意维护。
- 文件名编码:
model_lr3e4_bs32_v2_final_final.pt——命名规则迟早失控。 - 凭记忆复现:三周后想复现最好的结果,记不清当时改了什么。
- 无法对比:想知道「是数据变了还是超参变了导致指标提升」,无从回答。
这些问题的本质是:实验的状态没有单一可信来源。
追踪系统的四类价值
| 价值 | 具体表现 |
|---|---|
| 可复现 | 从 Run 记录还原完整环境与参数 |
| 可对比 | 并行曲线对齐,一眼看出差异 |
| 可审计 | 哪个模型上的线、用了哪份数据 |
| 可协作 | 团队共享结果,不靠口口相传 |
最小可用标准
一个团队不需要一上来就上平台,但至少要满足:
- 每次运行有唯一 ID。
- 参数、指标、产物三者绑定在同一 ID 下。
- 代码版本(Git commit)记录在案。
- 数据版本可追溯。
做到这四条,就已经避开了绝大多数灾难。
实验追踪的核心数据模型
所有主流工具的数据模型高度一致,理解这套模型后换工具几乎没有学习成本。
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 的选型
| 维度 | MLflow | W&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%。"
"已知限制:新用户冷启动样本不足,长尾类目召回偏低。"
),
)
模型卡是事故复盘时的第一手资料,也是合规审计的必需项。
上线与回滚流程
标准流程:
- 训练完成,Run 记录完整 → 注册新版本,阶段设为
Staging。 - 预发环境加载
@challenger,跑回归测试与影子流量。 - 指标达标 → 别名切到
@champion,阶段设为Production。 - 线上异常 → 别名切回上一版本,回滚是秒级的。
可复现性的三要素
可复现性是实验追踪的终极目的,它依赖三个要素同时被固定。
代码、数据、环境
可复现 = 固定的代码版本 + 固定的数据版本 + 固定的运行环境 + 固定随机种子
四者缺一不可:
- 代码:记录 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 提供更强的实时可视化与协作;方法上,超参搜索要配剪枝、模型对比要多随机种子并做显著性检验。模型注册把实验与生产连起来,别名机制让上线与回滚变成秒级操作。而这一切的底线是可复现性三要素(代码、数据、环境)加固定种子——没有可复现性,追踪系统就只是一堆好看但无用的曲线。记住一句话:实验追踪的投入产出比,与团队的实验规模成正比;但即使只有一个人,从第一天起就该记录。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。