📑 本页目录(点开跳转)
16 · 重训练
⏱ 46 分钟 | ⭐ 什么时候训、用什么数据训
🎯 一句话
「定期重训」听起来天经地义,但它有三个必须先回答的问题: 什么时候训、用多长的数据训、以及 —— 训出来的怎么知道更好。
🚨 一、先问一句:这次真的该重训吗
流程图
💀 再强调一次 ①:管道坏了时重训,会把错误数据学进模型, 把「临时故障」变成「永久损伤」。 重训流水线里应该有一道自动闸门:数据质量检查不通过就不许训。
⏰ 二、什么时候触发重训
| 触发方式 | 说明 | 适合 |
|---|---|---|
| 定时 | 每天/每周/每月固定训 | 简单,大多数场景够用 ⭐ |
| 性能触发 | 指标跌破阈值才训 | 需要有及时标签 |
| 漂移触发 | PSI 超阈值才训 | 不需要标签 ⭐ |
| 数据量触发 | 新增 N 条样本就训 | 冷启动期、数据稀疏时 |
⭐ 实践建议:定时为主 + 漂移触发为辅。 定时保证基线(不会忘),漂移触发处理突发情况。 纯性能触发的问题是:等指标掉下来时,损失已经发生了。
频率怎么定
流程图
from sklearn.metrics import roc_auc_score
import pandas as pd
def decay_curve(df, train_end_week, model_fn, feats, horizons=(1, 2, 4, 8, 12)):
"""冻结一个在 train_end_week 之前训好的模型,看它之后 k 周的效果衰减
df 需要有 week 列;model_fn(训练集) 返回一个已 fit 的模型"""
m = model_fn(df[df.week <= train_end_week]) # ⭐ 训完就冻住,之后不再更新
rows = []
for k in horizons:
seg = df[(df.week > train_end_week) & (df.week <= train_end_week + k)]
auc = roc_auc_score(seg.y, m.predict_proba(seg[feats])[:, 1])
rows.append({"上线后第几周": k, "AUC": round(auc, 4)})
return pd.DataFrame(rows)
# 典型输出:
# 上线后第几周 AUC
# 1 0.8410
# 2 0.8395
# 4 0.8352
# 8 0.8218 ← ⭐ 相对起点掉了 0.019,接近可接受上限
# 12 0.8079
💡 这个实验只需要历史数据,不用上线,半天就能做完 —— 但它能把「凭感觉每周训一次」变成有依据的决策。
⚠️ 一定要用至少三个不同的
train_end_week各跑一次: 单个起点的衰减速度可能是那段时间碰巧发生了什么。 三条曲线形状一致,结论才可信。
📅 三、用多长的数据训(训练窗口)
信息关系
三种策略:
| 策略 | 做法 | 适合 |
|---|---|---|
| 固定窗口 | 只用最近 N 天 | 概念漂移快(如时效性内容)⭐ |
| 全量累积 | 用所有历史数据 | 规律稳定(如 OCR、图像分类) |
| 加权衰减 ⭐ | 全都用,但老样本权重低 | 大多数业务场景,兼顾两者 |
# 🧩 骨架:`df` 来自你自己的代码,这一段只看写法
def time_decay_weight(days_ago, half_life=30):
"""指数衰减:半衰期 30 天 → 30 天前的样本权重是今天的一半"""
return 0.5 ** (days_ago / half_life)
sample_weight = time_decay_weight(df["days_ago"].values, half_life=30)
model.fit(X, y, sample_weight=sample_weight) # ⭐ 大多数库都支持
⭐ 半衰期怎么定:用上面那个衰减实验的结果 —— 如果效果 6 周就明显退化,半衰期设 3~4 周比较合理。 这两个决策是同一个数据支撑的。
⚠️ 一个反直觉的发现
信息关系
💡 一定要把「训练窗口」当成一个超参数去调,而不是默认用全量。
import pandas as pd
def tune_window(df, valid_start, model_fn, feats,
windows=(30, 60, 90, 180, 365, 10**9)):
"""把训练窗口当超参数调。⭐ 验证集必须在【时间上靠后】,不能随机切"""
va = df[df.day >= valid_start]
rows = []
for w in windows:
tr = df[(df.day < valid_start) & (df.day >= valid_start - w)]
m = model_fn(tr)
rows.append({"窗口(天)": w, "训练样本": len(tr),
"AUC": round(roc_auc_score(va.y, m.predict_proba(va[feats])[:, 1]), 4)})
return pd.DataFrame(rows)
# 典型输出(⭐ 注意 AUC 是【先涨后跌】的,最优窗口在中间):
# 窗口(天) 训练样本 AUC
# 30 62,000 0.7902 ← 样本太少,方差大
# 60 124,000 0.8071
# 90 186,000 0.8118 ← ⭐ 最优
# 180 371,000 0.8043
# 365 742,000 0.7961
# 全量 1,510,000 0.7834 ← 💀 默认做法,反而最差
⭐ 务必换 3–5 个
valid_start滚动跑一遍(滚动验证): 单个时间点选出来的"最优窗口"很可能是碰巧。 看的是"哪个窗口在多数时间点上都靠前",不是"哪个窗口拿了一次第一"。
🔬 四、训出来的怎么验证
重训不是「跑通就上」,它和首次上线一样要走完整流程:
结果对照
⭐ 第 3 条是个很好用的早期警报: 如果重训后特征重要性排名大变,多半不是"模型进步了", 而是某个特征的数据出问题了。 值得在上线前查一下。
🚧 一定要有的兜底:模型回退基线
因果链
🔁 五、自动重训流水线:五道闸门
结果对照
⭐ 全自动重训必须有这五道闸,否则它会自动地、定期地把问题推上线 —— 自动化会放大你的错误,而不只是放大你的效率。
闸 1 长什么样(大多数团队做得太浅):
def gate_data_quality(new, ref, feats, psi_max=0.25, row_drop_max=0.2):
"""闸1:⭐ 不能只看行数和缺失率 —— 那两项是最低标准,挡不住"单位变了"这类故障"""
issues = []
if len(new) < len(ref) * (1 - row_drop_max):
issues.append(f"行数骤降:{len(new)} vs 基准 {len(ref)}")
for f in feats:
miss = new[f].isna().mean()
if miss > ref[f].isna().mean() + 0.05:
issues.append(f"{f} 缺失率异常:{miss:.1%}")
p = psi(new[f], ref[f]) # 第 6 章的 PSI
if p > psi_max:
issues.append(f"{f} 分布突变:PSI={p:.2f}") # ⭐ 真正挡住事故的是这一行
return (len(issues) == 0), issues
💀 一次自动重训,把 26 小时的脏数据变成三周的损失
对照
系统:风控模型,每周一凌晨自动重训,训练窗口 90 天
周六:上游埋点发版,把 device_age_days 的单位从【天】改成【小时】
26 小时后被发现并回滚 —— 线上模型当时靠降级扛过去了 ✅
周一:自动重训照常跑
五道闸门为什么全没拦住:
| 闸 | 检查了什么 | 为什么没拦住 |
|---|---|---|
| 闸1 数据质量 | 行数 ✅、缺失率 ✅ | 没查分布 —— 那 26 小时的数据"完整而正常" 💀 |
| 闸2 训练健康 | loss 收敛 ✅ | 模型确实好好地学完了 —— 它学的是错的东西 |
| 闸3 效果对比 | 整体 AUC 0.842 → 0.836 | 只掉 0.006,通过了 −0.01 的阈值 ⭐ |
| 闸4 一致性 | 训练/推理特征比对 ✅ | 两边都用了同一批脏数据,当然一致 |
| 闸5 灰度指标 | 整体拦截率 ✅ | 整体没动 |
真正发生的事:
信息关系
⭐ 这个案例最该记住的一句话: 脏数据占比小 ≠ 影响小。 那 26 小时在 90 天窗口里只占 1.2% —— 但它是系统性错误,不是随机噪声。 随机噪声会被平均掉,系统性错误会被认真地学进去。
三条改进(都很便宜):
操作步骤
- 闸1 加 PSI:拿本次训练数据和上次训练数据逐特征对比 ⭐
- 闸3 加【分人群】对比:整体不差还不够,每个关键人群都不能明显差 ⭐
- 维护一张【数据事故时间表】,重训时自动剔除或降权这些时间段的样本
- 💡 ③ 是很多团队没想到的:
- 线上事故修好了,但那段时间的数据【永远留在了训练集里】
🧊 六、几个特殊情况
| 情况 | 注意 |
|---|---|
| 冷启动期 | 数据少时频繁重训会剧烈震荡,先稳一段 |
| 有强季节性 | 别用去年同期以外的数据当窗口;或显式加季节特征 |
| 模型被反馈闭环污染 | 重训会强化偏差 → 需要随机流量数据纠偏(第 7 章)⭐ |
| 增量训练 vs 全量重训 | 增量快但会累积漂移,建议定期全量重训一次做基准 |
⭐ 第三行值得展开:在推荐/风控这类有反馈闭环的系统里, 无脑重训会让模型越来越窄 —— 因为训练数据是它自己筛出来的。 必须掺入那部分随机流量/随机放行的数据。 🔗 推荐算法 02、第 7 章、第 14 章的拒绝推断
增量训练 vs 全量重训,摊开说:
| 增量训练 | 全量重训 | |
|---|---|---|
| 单次耗时 | 分钟级 ⭐ | 小时级 |
| 对新分布的响应 | 快 | 慢(老数据稀释) |
| 误差累积 | 会,且很难察觉 💀 | 不会 |
| 可复现性 | 差(依赖历史每一步)⭐ | 好 |
| 出事时的回滚 | 难 —— 要回到哪一步? | 简单 |
⭐ 务实的组合:增量为主 + 定期(如每月)全量重训一次做基准。 每次全量重训后,把增量模型和全量模型在同一验证集上比一下 —— 两者差距如果在扩大,说明增量已经跑偏了。 这是一个几乎零成本的漂移探针。
⚠️ 七、一张坑表
| 坑 | 后果 | 怎么修 |
|---|---|---|
| 管道坏了还照常重训 💀 | 临时故障变成永久损伤 | 闸1 拦住;PSI 是关键,不是行数和缺失率 ⭐ |
| 闸门只看整体指标 ⭐ | 就是那个 61%→38% 的案例 | 闸3 加分人群对比 |
| 事故时段的数据留在训练集里 | 修好了线上,却把错误学进了下个模型 | 维护数据事故时间表,重训自动剔除 |
| 默认用全量数据训 | 老数据有害,AUC 反而更低(0.7834 vs 0.8118) | 把窗口当超参数调,滚动验证 |
| 验证集随机切 | 泄漏未来信息,离线好线上崩 | 时间切分(ML基础 05) |
| 只和自己比,不和现役模型比 | "比上一版好"不等于"比线上那个好" | 闸3 必须在同一批数据上对比现役模型 |
| 纯性能触发重训 | 等指标掉下来,损失已经发生 | 定时为主 + 漂移触发为辅 |
| 重训完直接全量 | 没有影子和灰度,出事就是全量出事 | 走影子 → 灰度(第 3 章) |
| 只保留最新一个模型版本 | 问题可能是几周前那次重训引入的 | 至少保留 3 个版本 |
| 有反馈闭环还无脑重训 | 模型越来越窄,探索能力消失 | 掺入随机流量数据纠偏 ⭐ |
🔗 八、和站内其他章的关系
| 相关的地方 | 和这一章的关系 |
|---|---|
| 第 6 章四种漂移 | 决定该不该重训 |
| 第 3 章灰度 | 重训后的上线流程 |
| 第 4 章一致性 | 闸门 4 |
| 第 7 章随机流量 | 纠正反馈闭环 |
| ML基础 05时间切分 | 验证集不能随机切 |
✅ 检查点
- 四种漂移分别对应什么处理动作?哪种绝对不能重训?
- 重训触发方式有哪几种?实践建议是什么?纯性能触发的问题是什么?
- 怎么用数据算出该多久重训一次?这个实验有什么必须注意的地方?
- 训练窗口的三种策略?加权衰减的半衰期怎么定?调窗口时验证集有什么要求?
- 那个反直觉的发现是什么?说明了什么?
- 重训后验证清单里,哪一条是很好用的早期警报?为什么?
- 自动重训流水线的五道闸分别挡什么?闸1 只查行数和缺失率够吗?
- 那个「26 小时脏数据」的案例里,五道闸为什么全都没拦住?后果是什么?「脏数据占比小 ≠ 影响小」的道理是什么?该怎么改?
- 增量训练和全量重训各自的代价是什么?务实的组合是什么?有反馈闭环时无脑重训会怎样?
👀 答案
- ①管道坏了→绝不重训,先修管道 ②数据漂移→重训 ③概念漂移→重训+调训练窗口 ④标签漂移→可能只需重新校准。①绝对不能重训,会把错误数据学进去,临时故障变永久损伤。
- 定时、性能触发、漂移触发、数据量触发。建议定时为主+漂移触发为辅。纯性能触发的问题是:等指标掉下来时损失已经发生了。
- 冻结一个历史模型,用它预测之后 1/2/4/8/12 周的数据,画效果衰减曲线,找到"掉到不可接受"的点,重训周期设成它的一半。只需历史数据,半天能做完。注意:要换至少三个不同的起点各跑一次,单个起点的衰减速度可能是那段时间碰巧发生了什么,三条曲线形状一致结论才可信。
- 固定窗口(概念漂移快)、全量累积(规律稳定)、加权衰减(大多数场景)。半衰期用同一个衰减实验的结果定——效果 6 周退化则设 3~4 周。调窗口时验证集必须在时间上靠后,不能随机切;而且要换 3–5 个验证起点滚动跑,看的是"哪个窗口在多数时间点都靠前",不是"哪个拿了一次第一"。
- 用最近 3 个月训(AUC 0.81)比用全部 2 年训(0.78)更好 ——老数据不是没用,是有害。典型的窗口扫描结果是 AUC 先涨后跌、最优在中间(如 90 天 0.8118,而全量只有 0.7834)。说明训练窗口要当超参数调,不能默认全量。
- 特征重要性排序有没有剧烈变化。因为剧变多半不是"模型进步了",而是某个特征的数据出问题了。
- 闸1数据质量(挡住"管道坏了还硬训")、闸2训练健康、闸3效果对比、闸4训练/推理一致性、闸5灰度指标。闸1 只查行数和缺失率远远不够——那是最低标准,挡不住"单位变了"这类故障;真正管用的是逐特征 PSI,拿本次训练数据和上次训练数据对比。
- 埋点发版把
device_age_days的单位从天改成小时,26 小时后回滚。闸1 只查了行数和缺失率(那 26 小时的数据"完整而正常");闸2 loss 确实收敛了(模型好好地学完了,只是学的是错的东西);闸3 整体 AUC 0.842→0.836,只掉 0.006,通过了 −0.01 的阈值;闸4 两边用的是同一批脏数据,当然一致;闸5 整体拦截率没动。后果:新设备用户的欺诈召回率 61% → 38%,因为新设备欺诈是低频场景,整体指标上看不出来,三周后才由业务方发现。道理:那 26 小时在 90 天窗口里只占 1.2%,但它是系统性错误不是随机噪声——随机噪声会被平均掉,系统性错误会被认真地学进去。改法:①闸1 加 PSI ②闸3 加分人群对比 ③维护数据事故时间表,重训时自动剔除或降权那些时段的样本。 - 增量:快(分钟级)、响应新分布快,但会累积误差且很难察觉、可复现性差、出事难回滚。全量:慢,但无累积误差、可复现、好回滚。务实组合是增量为主 + 每月全量重训一次做基准,并且把增量模型和全量模型在同一验证集上比——差距在扩大就说明增量跑偏了,这是零成本的漂移探针。有反馈闭环时无脑重训会让模型越来越窄(训练数据是它自己筛出来的),要掺入随机流量/随机放行的数据纠偏。
🛑 可以停在这里
⚡ 走神救援
重训之前先定性(第 6 章那个四分法):⭐ 管道坏了绝不重训——会把错误学进去,把临时故障变成永久损伤;数据漂移可重训;概念漂移要重训并调窗口;标签漂移可能只需要重新校准。
触发方式:定时为主、漂移触发为辅。纯性能触发的问题是等指标掉下来,损失已经发生了。
⭐ 频率能算出来:冻结一个历史模型,看它之后几周的效果衰减曲线,找到越线点,周期设成它的一半。训练窗口是最被忽略也最有价值的决策——反直觉的实测结果是「用最近三个月训比用全部两年训更好」:老数据不是没用,是有害,要把窗口当超参调。
⚠️ 有反馈闭环时无脑重训会让模型越来越窄(训练数据是它自己筛的)→ 必须掺随机流量数据纠偏。
💀 那个值得记一辈子的案例:埋点发版把一个特征的单位从天改成小时,26 小时后回滚;周一自动重训照常跑,五道闸全部放行——查行数缺失率的那道觉得数据「完整而正常」,看 loss 的那道确实收敛了(模型好好地学完了,只是学的是错的东西),整体 AUC 只掉了一点点没触发阈值。后果是某个人群的召回率几乎腰斩,三周后才被业务方发现。
⭐ 那句该记住的话:脏数据占比小 ≠ 影响小。 它在窗口里只占百分之一点几,但它是系统性错误不是随机噪声——随机噪声会被平均掉,系统性错误会被认真地学进去。所以闸门要加逐特征 PSI 和分人群对比,并维护一张数据事故时间表,重训时自动剔除那些时段。
下一节 👉 17-版本回溯与可复现.md