第29章 上线监控与再训练
小白一句话
模型上线不是终点,是值班的开始。每天三件事:把今天的分按时跑出来 (链路),盯着分有没有悄悄变坏 (监控),该重训就重训(节奏)。这一章把这套值班流程走一遍。
这一章在干嘛
第17章把"训练 → 评估 → 评分"跑通过一次,但那是演示性质的------跑完一次就完事。上线后是每天都跑,这一章把"每天跑"变成一套可运维的流程,讲三件事:
- 每日评分链路:结果表长什么样、跑挂了怎么补、数据门禁怎么卡;
- 标签成熟回填与在线推理契约:今天的分什么时候才有答案、评分接口要带什么;
- 监控与重训节奏:看什么指标报警、多久重训一次。
每日评分链路:评分是可运维的流程,不是一次性脚本
"每天打分"和"跑一次脚本"的区别,全在结果表上。上线后的每日评分表要带全契约列,谁都能看懂、出了问题能追溯。示例数据上,07-23 给 57 个有历史的用户打分,结果表长这样(前 8 行):
| uid | 评分日 | 模型版本 | 回访分 | 持续分 | 风险档 | 兜底分 |
|---|---|---|---|---|---|---|
| U0001 | 07-23 | v2.3.0 | 93 | 79 | 观察 | 91 |
| U0002 | 07-23 | v2.3.0 | 93 | 64 | 正常 | 100 |
| U0003 | 07-23 | v2.3.0 | 91 | 49 | 正常 | 70 |
| U0005 | 07-23 | v2.3.0 | 49 | 11 | 正常 | 9 |
| U0006 | 07-23 | v2.3.0 | 93 | 81 | 复核 | 100 |
| U0007 | 07-23 | v2.3.0 | 87 | 62 | 正常 | 88 |
| U0008 | 07-23 | v2.3.0 | 65 | 14 | 正常 | 56 |
每一列都不是可有可无的:uid (谁)、评分日 (哪天的分)、模型版本 (哪个模型出的,重训后版本号变,才能追溯)、回访分/持续分 (第26章的双目标分)、风险档 (第28章,拦自动动作)、兜底分(评分卡,模型挂了它顶上)。U0005 这种回访分 49 的照常进表------分低也是结果,名单要的是全集。
围绕这张表,三件事是每天的固定动作:
- 失败补数与回滚:今天的任务跑挂了,不能干等明天。当天补跑(补数),补跑也失败就回滚到昨天的结果表,用昨天版本的分先顶着------分可能是旧的,但总比没有强。
- 增量更新底座:明细每天都在涨,不用每天全量重算特征。底座按"上次的截至日期 + 新明细"增量更新,只有切分、标准化这些口径变化才全量重算。
- 每日数据门禁:跑分之前先检查今天的明细到齐了没有(第5章那套质量门禁的思路)。明细没到齐就打分,打出来的是缺数据的分,比不打好不了多少。
在线推理契约:评分接口不是裸模型
打分不是"模型.predict(特征)"就完事,接口要带上让分数可复现的所有东西:
- 特征版本与字段顺序冻结 :第15章那条"
FEATS训练和评分用同一份"的规矩,上线后升级成版本契约------特征列表本身带版本号,改特征要走版本升级,不能悄悄改。 - 预处理参数随模型一起存:标准化用的均值和标准差、编码用的词典,是模型的一部分。模型存档的时候它们得跟着存,评分时用同一个 scaler 处理------第15章演示过,漏了测试集的标准化,AUC 掉 0.17。
- 降级策略 :模型服务真挂了,评分卡(第18章的规则分)顶上。示例数据上验证过:模型分 top-10 名单回访 7/10,评分卡 top-10 回访 10/10------兜底出的名单质量不输主模型,够撑到模型修好。这也是第18章那个"故障降级"用途在上线后的正式用法。
模型怎么搬到线上:序列化、一致性校验与 Golden Case
上面说的是"接口要带什么契约"。还有一个更底层的问题没回答:模型本身怎么从训练环境搬到线上服务?这道"搬运"不是复制文件那么简单------训练时模型住在内存里,要上线得先序列化(存成文件)、传过去、再加载(反序列化)。中间任何一步出错(框架版本不一致、环境缺依赖、文件被换),线上跑的就是一个"悄悄变了的模型",不报错,只是分数不对。
所以发布模型要验三件事(附件/08_上线篇/activity_model_serialize.py):
-
一致性 :加载出来的模型,对同一批样本的输出要和训练时内存里的输出逐样本一致。示例里 GBDT 用 joblib 序列化,重新加载后 57 个测试样本的最大绝对差是 0.000000------一致。真实项目这一步常常是 PyTorch → ONNX 导出,然后校验"训练框架的输出 vs 推理引擎的输出"的 logit 是否一致(一致到小数点后几位),再锁死算子版本。
-
Golden Case 回归:发布前固定挑一批样本(本例用 uid + 评分日做 hash,稳定挑出 8 个),把"应该输出什么"存成一张黄金表。每次发布后拿这批样本重跑,输出对不上就是发布坏了------这是回归测试,专门防"搬的过程中悄悄变了"。本例 8 条全过:
| uid | 标签 | 训练内存输出 | 加载后输出 |
|---|---|---|---|
| U0006 | 1 | 0.999504 | 0.999504 |
| U0028 | 1 | 0.958206 | 0.958206 |
| U0044 | 1 | 0.999504 | 0.999504 |
| U0019 | 0 | 0.999734 | 0.999734 |
| U0043 | 0 | 0.999043 | 0.999043 |
- 模型指纹 :模型文件算一个 sha256,记进发布清单。文件被人动过一个字节,指纹立刻变。本例模型文件指纹是
6cf581648fc9,随便改了文件一下变成517ac720f009------发布脚本比对指纹就能发现文件被动过。
一句话:在线推理契约管"接口长什么样",序列化与一致性管"模型到没到、还是不是那个模型"。两个一起,分数才可复现。
新模型先影子跑:只记录,不决策
模型训练完、验完一致性,下一步不是直接切流量,而是影子部署 :新模型(v2)和线上模型(v1)对同一批真实用户并行打分,但只有 v1 的输出进运营名单、真正触达用户;v2 的输出只写进影子表,什么都不影响(附件/08_上线篇/activity_shadow.py)。三步:
- 并行打分:v1、v2 都对 07-22 这 57 人出分,运营名单用 v1 的;
- 只记录不决策:v2 的结果只落影子表,不改任何线上输出------"看得到、摸不到";
- 事后对照:标签成熟回填后(就是下面那节),比 v1 vs v2,再决定切不切。
示例里 v1 是全特征模型,v2 是去掉 f_trend 的变体(第21章那个熟面孔)。标签成熟后对比:
| 模型 | PR-AUC | top-10 精确率 | top-10 lift |
|---|---|---|---|
| v1 线上(全特征) | 0.827 | 0.700 | 0.95 |
| v2 影子(去 f_trend) | 0.862 | 0.800 | 1.09 |
v2 影子分数更高,看起来可以转正------但两个提醒:
- 这是 57 人上的一个数。第21章讲过,"去趋势变体略好"在 bootstrap 里和 0 分不开。所以"影子更好"只是拿到了候选资格,真要切,还得过第30章那套同窗配对 + bootstrap 的显著性验收。
- 名单会变。v2 的 top-10 名单和 v1 只有 5/10 重合------转正后一半人的名单会换。运营要提前知道名单动得多大,别上线当天才发现触达对象全换了。
影子部署的本质是"拿真实流量做对照,但把决策权留到最后"------比离线评估更接近真实,又比直接切换安全。第30章转正验收里,影子积累的对比就是验收的输入。
标签成熟回填:今天的分,7 天后才有答案
标签是"未来 7 天",所以今天的评分,今天永远没有答案:
| 评分日 | 标签要看哪段行为 | 标签成熟日 |
|---|---|---|
| 07-15 | 07-16 ~ 07-22 | 07-22 |
| 07-22 | 07-23 ~ 07-29 | 07-29 |
| 07-23 | 07-24 ~ 07-30 | 07-30 |
回填机制很简单:每天把 7 天前那批标签写回训练表。07-30 晚上,07-23 那批评分就有了标签,第二天(07-31)重训就能用到新样本。标签回填是重训的燃料------不回填,重训永远吃旧标签,等于拿着半年前的标准判断今天的人。
每日监控:眼睛不能闭
上线后要盯的指标,前面各章都备好了,这里只是把它们排进值班表:
| 监控项 | 阈值/规则 | 出处 |
|---|---|---|
| PSI(当日 vs 训练期) | > 0.25 报警 | 第13、22章(f_trend 3.60 那种量级) |
| Brier / ECE | 逐日记录,连续 3 天恶化报警 | 第19章 |
| 层级一致率 | 应保持 100%,出现 >0 报警 | 第27章 |
| 观察档升级率 | 观察档大量升级复核 → 风险规则在变松 | 第28章 |
报警之后先别急着动模型------第22章那句话:"先看是不是口径又变了(数据晚到、窗口算错),再看是不是用户真变了"。PSI 高不一定是模型坏了,可能是上游表晚了。
重训节奏:什么时候重训
重训不是"想起了就训"。先定节奏,再用监控决定要不要提前:
- 起步每周重训:行为聚类(第9-11章)和活跃度模型一起,每周用回填好的最新标签重训一次;
- 稳定后放慢:连续几周评估都稳(PSI 平静、Brier/ECE 没恶化),改成双周、再改月度------省算力,也少一次"重训后分数变化"的运营困惑;
- 监控触发提前 :PSI 报警、Brier 连续恶化、AUC 明显下滑,任何一条命中,不等周计划,立刻重训。触发条件是"数据变了"的信号,不是固定日历。
重训后还有一件小事:模型版本号要 bump(v2.3.0 → v2.4.0),结果表里跟着变------第28章说的"降级升级都要有迹可循",版本号就是那个迹。
重训之外:在线学习什么时候用
每周重训是"全量重来一遍"。还有一条路------在线学习:新数据来了,模型只做增量更新(partial_fit),不重训旧数据。示例数据上演示一下(线性模型 SGD 支持 partial_fit;树模型没有这个方法,这里用 SGD 展示"在线更新长什么样"):
- 全量训练(105 行一次性 fit):PR-AUC 0.855 、ROC-AUC 0.710
- 在线更新(7 批逐批喂,模拟每天来一批):PR-AUC 0.885 、ROC-AUC 0.769
两份接近(差 0.03~0.06,57 人测试集上属噪声)------同样的数据分批喂,收敛到差不多的位置。顺带提醒一个坑:partial_fit 每批都在内部迭代,批数少了迭代不够会欠收敛(批数从 7 改成 2,AUC 掉到 0.46 附近)------在线学习也要喂够步数。
但"能在线更新"不等于"该在线更新":
- 好处:新数据到了立刻生效,不用等周重训;训练成本摊到每天;内存只存新一批,不用全量历史;
- 代价:模型可能"忘记"旧数据(忘了历史模式);更新过程难审计;版本追溯麻烦------模型天天在变,指纹对不上一个版本(第30章那套指纹、门禁全派不上用场);
- 适合的场景:风控、推荐这类实时性要求极高、数据分钟级到账的系统。
本项目是日级评分 + 标签要 7 天才成熟(回填机制),数据不是分钟级实时------在线更新的收益很小、审计成本很高,所以走重训就够。而且 GBDT、随机森林压根没有 partial_fit 这个方法------scikit-learn 用"没有这个方法"直接告诉你:树模型的世界里,增量就是滚动重训。
换了基线口径,得回放重训验一遍
重训还有一类隐蔽的坑:换口径 。上线后经常发生------离线训练用的是"滚动基线"(每个评分日 D 用 D-13, D-1 窗口算业务基线),线上服务却长期固定一个基线工件(某天定下来就不动了)。基线一变,相对单位奖励特征(第7章补充一)、行为聚类、预测全跟着变。这时候只重训还不够,得回放重训:把新口径的基线套回历史评分日,整条链路重训重评,看模型是否还成立。
最容易踩的雷是:固定基线里混进了未来信息 (附件/08_上线篇/activity_baseline_replay.py)。示例里做两个方案:
- 方案 A(滚动,无泄露):每个评分日 D 用 D-13, D-1 窗口算基线;
- 方案 C(固定,有泄露):统一用 07-15 ~ 07-21 窗口算基线------对 07-08、07-15 两个训练评分日来说,这个基线用了"未来"的数据(真实项目里就是"线上定死了某个较晚日期的基线工件,拿它回算历史")。
换基线重训后,测试集(07-22)指标:
| 方案 | PR-AUC | Brier |
|---|---|---|
| A 滚动基线(无泄露) | 0.803 | 0.215 |
| C 固定基线(含未来) | 0.854 | 0.196 |
C 的指标明显更好------但这正是问题所在:C 偷看了未来,指标虚高,是"泄露的甜头" 。如果拿 C 的评估数字去决定"这套固定基线能不能上线",等于拿开卷考试的成绩当闭卷成绩。而且换基线后特征分布也变了:A vs C 的相对单位奖励特征 PSI = 0.524,远超 0.25 的明显漂移线。
所以回放重训的定位要摆正:
- 它是兼容性压力测试:看换口径会不会让模型崩(指标掉、分布炸),不是无泄露的模型验证;
- 要判断"能不能用这套固定基线上线",必须用早于评分日的基线重新评估,或等固定基线日期之后的标签成熟再做无泄露验证;
- 判定门禁照旧:关键指标在冻结的容忍阈值内、特征分布 PSI 不超线、高风险人群切片无异常------任一条不过,说明口径切换破坏了模型,不能直接切。
它和前后章节的关系
- 第17章把评分跑通一次,本章把它变成每天可运维的流程;
- 第18章的评分卡在这里当降级兜底;第28章的风险分档进结果表拦自动动作;
- 第13/22章的 PSI、第19章的 Brier/ECE、第27章的层级一致率,在这里合并成一张监控值班表;
- 第24章的双标签靠本章的"回填"机制才有新样本可学------回填是双目标持续运作的前提;
- 新模型从本章的影子部署(先跑着看)到第30章的转正验收(指纹 + 同窗配对 + bootstrap),是一条完整的发布链------影子先给候选资格,验收过了才切流量;
- 第30章把前面所有步骤串成一条完整可复跑的最小流水线。
动手
- 跑
附件/08_上线篇/activity_monitor.py,对照正文:07-23 评分表契约列、标签成熟回填表、降级对照(模型 7/10 vs 评分卡 10/10)。 - 把脚本里评分日从 07-23 改成 07-22 重跑,看结果表的变化------评分日不同,分数不同,但契约列不变,这就是"流程固定、内容每天变"。
- 在脚本里把"回填"的窗口从 7 天改成 3 天(
timedelta(days=7)→3)重跑,看标签成熟日提前到哪天、07-23 的分最早哪天有答案------想一下窗口缩短后标签还准不准。 - 想一个场景:PSI 报警了,但你查了数据没问题、口径没问题。这种情况多半是"用户真变了"的信号------先跑一版滚动评估确认,再决定是调阈值、提前重训还是补特征,而不是直接动线上模型。
- 跑
附件/08_上线篇/activity_online_learning.py,对照"重训之外:在线学习"那节:全量 0.855 vs 在线更新 0.885。把批数从 7 改成 2 重跑,看 AUC 掉到哪------感受"迭代不够会欠收敛"。 - 跑
附件/08_上线篇/activity_model_serialize.py,对照"模型怎么搬到线上"那节。改一下脚本里joblib.load(model_path)换成加载前先改模型文件(参考第 3 部分的做法),看 Golden Case 会不会当场抓住------这就是回归测试的意义。 - 跑
附件/08_上线篇/activity_shadow.py,对照"新模型先影子跑"那节。把 v2 从"去 f_trend"换成"去 f_recency"(改feat_v2那行),看影子对比和名单重合度怎么变------想想名单变化多大才值得让运营重新评估。 - 跑
附件/08_上线篇/activity_baseline_replay.py,对照"换了基线口径,得回放重训验一遍"那节。把 C 方案的固定窗口改成 07-01 ~ 07-07(对训练评分日无泄露),看指标还虚不虚高------体会"泄露的甜头"只在基线用了未来时才出现。
本章配套脚本
附件/08_上线篇/activity_monitor.py(07-23 每日评分契约表 + 标签成熟回填表 + 降级对照 + 监控阈值汇总)、附件/08_上线篇/activity_online_learning.py(全量训练 vs partial_fit 在线更新的收敛对照 + 什么时候值得走在线更新的取舍)、附件/08_上线篇/activity_model_serialize.py(序列化 + 加载一致性 + Golden Case 回归 + 模型指纹防换文件)、附件/08_上线篇/activity_shadow.py(影子部署三步:并行打分 / 只记录不决策 / 标签成熟后对照)、附件/08_上线篇/activity_baseline_replay.py(换基线口径的回放重训:无泄露滚动基线 vs 含未来固定基线,看"泄露甜头"和特征 PSI)。用 scikit-learn 与标准库,依赖在第4章附件/00_公共/requirements.txt。双目标模型口径与第26章一致,评分卡规则与第18章一致。