训练与实验跟踪
这一课你将能做到
- 把训练代码组织成「加载数据 → 按时间切分 → 构造防泄漏特征 → 训练 → 评估」的结构化管道
- 用 MLflow 自动记录每次训练的超参、指标、模型文件,并在 UI 里对比实验
- 给训练脚本加
--smoke模式,为第 7 课的 CI 训练冒烟测试做准备
为什么要学
训练本身不难,难的是三个月后回答:「线上这个模型是哪天、用哪份数据、什么参数训出来的?F1 多少?为什么不用另一次实验?」没有实验跟踪的团队靠聊天记录和 Excel 找答案,这是 MLOps 里最典型的混乱源。
MLflow Tracking 是事实标准的轻量方案:每次训练成为一个 run,参数、指标、模型 artifact 自动入库,一个网页看全所有实验。人话:把「调参日记」从脑子和小本本里搬进数据库,还能一键翻旧账。
先建立图像
Experiment: timeout-prediction(一个业务问题一个实验)
├─ Run #1 params: {max_iter=200, lr=0.1} metrics: {f1=0.87, auc=0.93}
│ artifacts: model/ (模型文件+签名) tags: {git_sha=abc123, data=judge_clean@md5}
├─ Run #2 params: {max_iter=400, lr=0.05} metrics: {f1=0.89, auc=0.94}
└─ Run #3 ...
一次 run = 一份完整档案:代码版本 + 数据版本 + 参数 + 指标 + 模型文件
跟着做
步骤 1:安装并初始化 MLflow
pip install mlflow
步骤 2:写出结构化训练脚本
数据沿用第 3 课产出的 data/processed/judge_clean.parquet。重点看两处纪律:按时间切分(不能随机切,否则用未来数据预测过去)和防泄漏历史特征(shift(1) 把当前行排除)。
# src/train.py
import argparse, subprocess
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import f1_score, roc_auc_score
import mlflow
from mlflow.models import infer_signature
FEATURES = ["code_lines", "lang_java", "rank", "problem_tle_hist"]
def load_data() -> pd.DataFrame:
df = pd.read_parquet("data/processed/judge_clean.parquet")
df = df.sort_values("submit_time").reset_index(drop=True)
df["is_tle"] = (df["verdict"] == "TLE").astype(int)
df["lang_java"] = (df["lang"] == "Java").astype(int)
# 防泄漏:每题历史 TLE 率,shift(1) 排除当前提交自己
df["problem_tle_hist"] = (df.groupby("problem_id")["is_tle"]
.transform(lambda s: s.shift(1).expanding().mean())
.fillna(0.15))
return df
def git_sha() -> str:
try:
return subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"], text=True).strip()
except Exception:
return "unknown"
def main(smoke: bool, max_iter: int, learning_rate: float):
df = load_data()
if smoke:
df = df.sample(500, random_state=42)
split = int(len(df) * 0.8) # 前 80% 训练,后 20% 测试
X_tr, y_tr = df[FEATURES][:split], df["is_tle"][:split]
X_te, y_te = df[FEATURES][split:], df["is_tle"][split:]
mlflow.set_experiment("timeout-prediction")
with mlflow.start_run() as run:
mlflow.log_params({
"max_iter": max_iter, "learning_rate": learning_rate,
"train_rows": len(X_tr), "test_rows": len(X_te),
})
mlflow.set_tags({"git_sha": git_sha(), "data": "judge_clean.parquet"})
model = HistGradientBoostingClassifier(
max_iter=max_iter, learning_rate=learning_rate, random_state=42)
model.fit(X_tr, y_tr)
proba = model.predict_proba(X_te)[:, 1]
mlflow.log_metrics({
"f1": f1_score(y_te, proba > 0.5),
"roc_auc": roc_auc_score(y_te, proba),
})
mlflow.sklearn.log_model(
model, name="model",
signature=infer_signature(X_tr.head(5)),
input_example=X_tr.head(5))
print("run_id =", run.info.run_id)
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument("--smoke", action="store_true", help="小样本冒烟训练,供 CI 使用")
p.add_argument("--max-iter", type=int, default=200)
p.add_argument("--lr", type=float, default=0.1)
a = p.parse_args()
main(a.smoke, a.max_iter, a.lr)
步骤 3:跑两次对比实验
python src/train.py --max-iter 200 --lr 0.1
python src/train.py --max-iter 400 --lr 0.05
步骤 4:打开 MLflow UI 对比
mlflow ui --backend-store-uri sqlite:///mlflow.db --port 5000
# 浏览器打开 http://127.0.0.1:5000
在 Experiments 页面:按 roc_auc 排序所有 run;勾选两个 run 点 Compare,看参数与指标的并排表格和散点图;点进任意 run 能下载模型 artifact、查看签名。这就是「实验可翻旧账」的完整体验。
步骤 5:验证冒烟模式
python src/train.py --smoke
# 几秒内跑完,run 正常入库——CI 流水线要的就是这个速度
讲清楚要点
- Train/Val/Test 纪律:测试集只能最后碰一次,用它选参数等于考试前偷看卷子;调参用验证集(或交叉验证),且时序数据必须按时间切。
- 特征泄漏是最隐蔽的坑:
problem_tle_hist若不加shift(1),就把「这道题今天 TLE 了」直接喂给模型预测「它会 TLE」,线下 AUC 虚高、线上原形毕露。任何聚合特征都要问一句:算这个值时,包含当前样本吗? - run 的完整性 = 可复现性:参数、指标、git_sha、数据标识、模型文件、输入签名一个都不能少。半年后你能凭 run_id 一键定位一切。
- signature 与 input_example 不是装饰:签名声明模型的输入输出 schema,上线部署和注册中心校验都靠它拦截特征错位——这是训练阶段能做、上线阶段救命的事。
- 深度学习补充:换 PyTorch 后要点变为
torch.device管理、early stopping、checkpoint 断点续训、mlflow.pytorch.log_model;大参数量时记录 GPU 型号与显存占用,因为复现实验要匹配资源。可用mlflow.autolog()快速起步,但生产脚本建议显式记录关键项。
你可能会懵的地方
- 验证集分数很高,线上效果差,最可能是什么? 优先怀疑三件事:数据泄漏(含目标信息的特征混进来)、时间泄漏(随机切分导致用未来预测过去)、train/serve skew(线上特征口径与训练不同)。用「按时间切 + 历史特征 shift(1) + 共享特征代码」三板斧排查。
- MLflow 把东西存哪了?能自定义吗?
默认在当前目录
mlruns/。元数据(参数/指标/标签)是 backend store,可用 SQLite/PostgreSQL;模型文件是 artifact store,可指向 S3/OSS。生产用mlflow server集中托管,第 5 课展开。 - Experiment 和 Run 什么关系?怎么划分? 一个业务问题/一条管道一个 Experiment,一次训练一个 Run。不要把不同问题塞进同一实验,也不要为每点小改动开新实验,否则对比失去意义。
- 怎么从几十个 run 里选出「最佳模型」?
先定业务指标(如高召回下的精确率),再按它排序过滤(如
metrics.roc_auc > 0.9),最后人工检查该 run 的数据版本与参数合理性。自动化择优在下一课用代码实现。 - autolog 一行搞定,为什么还要手写 log_param? autolog 省事但记录的是框架默认项,团队口径不统一、缺业务上下文(如数据版本 tag)。约定:探索期用 autolog,进入生产管道前改为显式记录。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 用测试集反复调参 | 指标虚高,线上打脸 | 测试集只评估一次,调参走验证集 |
| 时序数据 train_test_split 随机切 | 用未来预测过去 | 按时间顺序切分 |
| 聚合特征不排除当前样本 | 数据泄漏,线下虚高 | groupby + shift(1) 再聚合 |
| 只记 accuracy 一个指标 | 类别不平衡时毫无意义 | 同时记 F1/AUC/精确率/召回率 |
| log_model 不带 signature | 部署时特征错位静默出错 | 统一传 signature 与 input_example |
卡点急救
- mlflow ui 打不开或报端口占用:换
--port 5001;确认用的是与训练相同的--backend-store-uri,否则看不到 run。 - log_model 报 warning 找不到 flavor:
pip install scikit-learn环境要与训练一致;mlflow.sklearn.log_model需要 sklearn 已安装。 - UI 里 run 是空的:训练脚本里没写
mlflow.set_experiment/start_run就不会记录;检查是否在with mlflow.start_run()块内执行了训练。
面试常问
- MLflow Tracking 的架构是怎样的?
三层:客户端(训练脚本调 API);Backend Store 存元数据(实验/run/参数/指标/标签,SQLite 或 PostgreSQL);Artifact Store 存大文件(模型、图表,本地盘或 S3/OSS)。生产部署
mlflow server集中托管,团队共享同一实验库。 - 如何系统性地组织实验管理? 命名规范(experiment 按业务问题、run 名有意义)+ 必记字段清单(git_sha、数据版本、切分方式)+ 指标口径统一 + 定期归档失败 run。让「翻旧账」变成 30 秒的事而不是半天的考古。
- 什么是数据泄漏?举一个 ML 场景的例子并说明排查方法。 训练特征里混入了预测时刻拿不到的信息。例:按 problem_id 全量统计 TLE 率再回填到每条样本,当前提交的标签已包含在统计里。排查:对比线下/线上指标差距;审查聚合特征是否 shift;做时间外推验证(用旧数据训、新数据测)。
自测三问
- 为什么时序判题数据必须按时间切分而不是随机切?
- 一次完整的 MLflow run 至少要记录哪五类信息?
problem_tle_hist为什么必须shift(1)?不 shift 会发生什么?
小结一句话
训练工程 = 纪律(切分、防泄漏、可复现)+ 记录(MLflow 把参数、指标、数据与代码版本、模型文件绑成一份档案)。
延伸
- MLflow 官方文档:Tracking——run 生命周期、autolog 与各 flavor 的权威说明。
- scikit-learn:模型评估与交叉验证——指标选型与时间序列切分的官方指引。
- 下一课:MLflow 与血缘——模型注册中心、staging 到 production 的晋升流程与数据血缘。