TT Lab
开始
学习 学习路径 课程

上周那个模型更好,可没人找得到

更差的模型上了生产

在 TT Lab 中继续学习

目标

搭建一个有模型版本和别名(champion、challenger)的小型注册表,做出指标下降就拦住晋级的门禁,回滚被无视门禁的晋级,最后把输入分布是否发生移动用数字表示出来。

为什么重要

部署模型不是复制文件,而是 决定指向哪个版本。如果线上直接持有版本号,回滚就成了修改部署配置;而隔上一层别名,回滚就成了改变名字所指向的位置。所以注册表把版本、别名、历史三者放在一起。晋级门禁则在此基础上增加了“自动进行,但不是随时都走”这一点——如果重新训练一结束就直接上线,在某一周,更差的模型就会悄悄上去。门禁拦住了却仍有人强行推过去,这种事确实会发生,所以和拦住同样重要的,是 把拦住这件事和强行推过这件事一起留下来。

步骤

  1. 把现在线上的模型注册为版本 1。
  2. 让 champion 别名指向版本 1。
  3. 把重新训练的结果注册为版本 2,并设为 challenger。
  4. 用代码做出晋级门禁,并留下判定。
  5. 把别名移动的历史留作审计记录。
  6. 重现无视门禁的晋级,并把这件事留在记录里。
  7. 回滚,并写下损失了多少。
  8. 测量输入分布移动了多少,附到重新训练的判断上。

参考

把现在线上的模型注册为版本 1

以 lr 0.1、epochs 40、seed 7 运行训练器生成模型文件,在 /root/registry/registry.json 中把 model 设为 "churn-classifier",把 version 1 注册为 versions 的第一项。每个版本包含 version、run_id、params、metrics、model_path、model_sha256、data_sha256、created_at。model_sha256 是 model_path 所指向文件的哈希,data_sha256 是 /opt/fixtures/mlops/train.csv 的哈希。

训练器的 --out 选项会写出模型文件。每个版本的文件要分开放——如果覆盖了,就没有可回滚的东西了。

用名字而不是编号指向部署对象

把 {"champion": 1} 保存到 /root/registry/aliases.json。这个实验中使用的别名只有 champion 和 challenger 两个。

如果让线上直接看版本号,回滚时就得修改部署配置。隔上一层别名,回滚就成了改这个文件里的一行。

把重新训练的结果注册为挑战者

以 lr 0.3、epochs 30、seed 3 重新训练,把版本 2 注册到 /root/registry/registry.json,并让 /root/registry/aliases.json 的 challenger 指向 2。版本号必须从 1 开始每次加 1,champion 仍然必须是 1。

重新训练不等于部署。新版本先立为挑战者,晋级由下一步的门禁来判断。

让规则而不是人来判断晋级

编写 /root/registry/gate.py,读取 /root/registry/registry.json 和 /root/registry/aliases.json,把 champion_version、challenger_version、champion_metric、challenger_metric、margin、decision、reasons 保存到 /root/registry/gate.json。margin 是 0.005,只有当挑战者的 valid_accuracy 大于等于 champion + margin 时,decision 才是 promote,否则是 block。reasons 是由 10 个字符以上的句子组成的列表。

设置余量的原因是测量噪声。如果因为 0.001 的差距就换掉 champion,下周又得换回来。

留下别名移动的历史

在 /root/registry/audit.jsonl 里,把到目前为止设立别名的事一行一行地留下来。每一行包含 ts、alias、from(第一次时为 null)、to、actor、reason(10 个字符以上)。把记录从头重放,必须得到与 /root/registry/aliases.json 完全相同的状态。

审计记录装的不是“现在是什么”,而是“怎么走到这里的”。写下 from,重放时哪里对不上就会立刻暴露。

看看无视门禁的晋级会留下什么

重现门禁拦住了、人却仍把 champion 升到版本 2 的局面。把 /root/registry/aliases.json 的 champion 改为 2,并在 /root/registry/audit.jsonl 中追加一行 alias 为 champion、from 为 1、to 为 2 的记录,在这一行里用 gate_decision 键原样写下 gate.json 的判定。

光是拦住还不够。必须留下强行推过的人当时知道什么,下次才能就同样的事进行讨论。

回滚,并写下损失了多少

把 champion 重新回滚到版本 1,并把这次变更也留在 /root/registry/audit.jsonl 里(记录总共要有 4 行以上,最后一行必须是 champion 2→1)。然后把 restored_version、bad_version、detected_by(10 个字符以上)、metric_delta(版本 1 的 valid_accuracy − 版本 2 的值,四舍五入到小数点后第四位)保存到 /root/registry/rollback.json。

回滚就是移动别名。如果删除模型文件或版本,发生过什么也会随之消失。

把输入分布是否移动用数字表示出来

以 /opt/fixtures/mlops/train.csv 为基准,测量 /opt/fixtures/mlops/week2.csv 的五个特征(tenure_months、monthly_fee、support_tickets、late_payments、usage_hours)移动了多少,把 baseline、current、features、max_abs_shift_sigma、alert、retrain_required 保存到 /root/registry/drift.json。features 的每一项包含 train_mean、new_mean、pstdev(基准数据的总体标准差)、shift_sigma((new_mean − train_mean) ÷ pstdev),空值或不是数字的值不计入。alert 和 retrain_required 表示 max_abs_shift_sigma 是否超过 1.0。

分布移动不是违反契约——即使值全部在允许范围内,平均值也可能移动。所以要另外去测。