🏠 总目录📚 本教程 16 · 重训练
📑 本页目录(点开跳转)

16 · 重训练

46 分钟 | ⭐ 什么时候训、用什么数据训


🎯 一句话

「定期重训」听起来天经地义,但它有三个必须先回答的问题: 什么时候训、用多长的数据训、以及 —— 训出来的怎么知道更好。


🚨 一、先问一句:这次真的该重训吗

   ⭐ 按第 6 章的四分法先定性:

   ① 管道坏了       → 【绝不重训】,先修管道  💀
   ② 数据漂移       → 重训有效  ✅
   ③ 概念漂移       → 重训有效,但要调训练窗口  ✅⭐
   ④ 标签漂移       → 可能只需重新校准,比重训便宜  ⭐

💀 再强调一次 ①管道坏了时重训,会把错误数据学进模型, 把「临时故障」变成「永久损伤」。 重训流水线里应该有一道自动闸门:数据质量检查不通过就不许训。


⏰ 二、什么时候触发重训

触发方式 说明 适合
定时 每天/每周/每月固定训 简单,大多数场景够用
性能触发 指标跌破阈值才训 需要有及时标签
漂移触发 PSI 超阈值才训 不需要标签 ⭐
数据量触发 新增 N 条样本就训 冷启动期、数据稀疏时

实践建议:定时为主 + 漂移触发为辅。 定时保证基线(不会忘),漂移触发处理突发情况。 纯性能触发的问题是:等指标掉下来时,损失已经发生了。

频率怎么定

   ⭐ 一个能算的判据:看【模型效果的衰减速度】

   ① 拿一个历史模型,冻结它
   ② 用它去预测之后 1、2、4、8 周的数据
   ③ 画出效果衰减曲线
   ④ 找到「掉到不可接受」的那个点 → 重训周期设成它的一半

   例:8 周后 AUC 掉 0.02(可接受上限是 0.015)
       → 大约 6 周越线 → 重训周期设 3 周
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、图像分类)
加权衰减 全都用,但老样本权重低 大多数业务场景,兼顾两者
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 周比较合理。 这两个决策是同一个数据支撑的。

⚠️ 一个反直觉的发现

   很多人默认「数据越多越好」,但在概念漂移的场景下:

   用最近 3 个月 的数据训   →  AUC 0.81
   用全部 2 年 的数据训     →  AUC 0.78   ⭐ 更差

   → 老数据不是没用,是【有害】

💡 一定要把「训练窗口」当成一个超参数去调,而不是默认用全量。

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 章),不要直接全量

第 3 条是个很好用的早期警报如果重训后特征重要性排名大变,多半不是"模型进步了", 而是某个特征的数据出问题了。 值得在上线前查一下。

🚧 一定要有的兜底:模型回退基线

   每次重训后,把【新模型】和【上一版】都保留
   → 新模型表现不如预期时,能立刻切回

   ⭐ 而且要保留【至少 3 个版本】——
      因为问题可能是几周前那次重训引入的

🔁 五、自动重训流水线:五道闸门

   数据 ──[闸1]──→ 训练 ──[闸2]──→ 评估 ──[闸3]──→ 影子 ──[闸4]──→ 灰度 ──[闸5]──→ 全量

   闸1 数据质量:行数、缺失率、分布是否正常     ⭐ 挡住"管道坏了还硬训"
   闸2 训练健康:loss 收敛、无 NaN、耗时正常
   闸3 效果对比:在同一验证集上不差于现役模型   ⭐
   闸4 一致性  :训练/推理特征比对通过(第 4 章)⭐⭐
   闸5 灰度指标:小流量上业务指标不恶化

全自动重训必须有这五道闸,否则它会自动地、定期地把问题推上线 —— 自动化会放大你的错误,而不只是放大你的效率。

闸 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 灰度指标 整体拦截率 ✅ 整体没动

真正发生的事

   模型学到了「device_age_days 取到 1000+ 也是正常的」
   → 它降低了这个特征的整体权重

   分人群看才看得见:
   · 新设备用户的欺诈召回率:61%  →  38%   💀
   · 而新设备欺诈是低频场景,整体指标上几乎看不出

   → 三周后由业务方发现("最近新号薅羊毛的变多了")

⭐⭐ 这个案例最该记住的一句话脏数据占比小 ≠ 影响小。 那 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. 四种漂移分别对应什么处理动作?哪种绝对不能重训?
  2. 重训触发方式有哪几种?实践建议是什么?纯性能触发的问题是什么?
  3. 怎么用数据算出该多久重训一次?这个实验有什么必须注意的地方?
  4. 训练窗口的三种策略?加权衰减的半衰期怎么定?调窗口时验证集有什么要求?
  5. 那个反直觉的发现是什么?说明了什么?
  6. 重训后验证清单里,哪一条是很好用的早期警报?为什么?
  7. 自动重训流水线的五道闸分别挡什么?闸1 只查行数和缺失率够吗?
  8. 那个「26 小时脏数据」的案例里,五道闸为什么全都没拦住?后果是什么?「脏数据占比小 ≠ 影响小」的道理是什么?该怎么改?
  9. 增量训练和全量重训各自的代价是什么?务实的组合是什么?有反馈闭环时无脑重训会怎样?
👀 答案
  1. ①管道坏了→绝不重训,先修管道 ②数据漂移→重训 ③概念漂移→重训+调训练窗口 ④标签漂移→可能只需重新校准。①绝对不能重训,会把错误数据学进去,临时故障变永久损伤。
  2. 定时、性能触发、漂移触发、数据量触发。建议定时为主+漂移触发为辅纯性能触发的问题是:等指标掉下来时损失已经发生了。
  3. 冻结一个历史模型,用它预测之后 1/2/4/8/12 周的数据,画效果衰减曲线,找到"掉到不可接受"的点,重训周期设成它的一半。只需历史数据,半天能做完。注意:要换至少三个不同的起点各跑一次,单个起点的衰减速度可能是那段时间碰巧发生了什么,三条曲线形状一致结论才可信
  4. 固定窗口(概念漂移快)、全量累积(规律稳定)、加权衰减(大多数场景)。半衰期用同一个衰减实验的结果定——效果 6 周退化则设 3~4 周。调窗口时验证集必须在时间上靠后,不能随机切;而且要换 3–5 个验证起点滚动跑,看的是"哪个窗口在多数时间点都靠前",不是"哪个拿了一次第一"。
  5. 用最近 3 个月训(AUC 0.81)比用全部 2 年训(0.78)更好 ——老数据不是没用,是有害。典型的窗口扫描结果是 AUC 先涨后跌、最优在中间(如 90 天 0.8118,而全量只有 0.7834)。说明训练窗口要当超参数调,不能默认全量。
  6. 特征重要性排序有没有剧烈变化。因为剧变多半不是"模型进步了",而是某个特征的数据出问题了
  7. 闸1数据质量(挡住"管道坏了还硬训")、闸2训练健康、闸3效果对比、闸4训练/推理一致性、闸5灰度指标。闸1 只查行数和缺失率远远不够——那是最低标准,挡不住"单位变了"这类故障;真正管用的是逐特征 PSI,拿本次训练数据和上次训练数据对比
  8. 埋点发版把 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 加分人群对比 ③维护数据事故时间表,重训时自动剔除或降权那些时段的样本。
  9. 增量:快(分钟级)、响应新分布快,但会累积误差且很难察觉、可复现性差、出事难回滚。全量:慢,但无累积误差、可复现、好回滚。务实组合是增量为主 + 每月全量重训一次做基准,并且把增量模型和全量模型在同一验证集上比——差距在扩大就说明增量跑偏了,这是零成本的漂移探针。有反馈闭环时无脑重训会让模型越来越窄(训练数据是它自己筛出来的),要掺入随机流量/随机放行的数据纠偏

🛑 可以停在这里

走神救援

重训前先按第6章四分法定性:⭐管道坏了绝不重训(会把错误学进去,临时故障变永久损伤——重训流水线要有自动闸门)、数据漂移可重训、概念漂移重训+调窗口、标签漂移可能只需重新校准触发:⭐定时为主+漂移触发为辅纯性能触发的问题是等指标掉下来损失已经发生)。⭐频率能算出来:冻结一个历史模型看它在之后1/2/4/8周的效果衰减曲线,找到越线点,周期设成它的一半(半天能做完)。⭐⭐训练窗口是最被忽略也最有价值的决策:固定窗口/全量累积/加权衰减(半衰期用同一个衰减实验定);⭐反直觉发现:用最近3个月训(0.81) 比用全部2年训(0.78) 更好——老数据不是没用是有害,要把窗口当超参调。验证清单里⭐特征重要性排序剧变是个好用的早期警报(多半不是模型进步,是某特征数据出问题)。自动流水线五道闸:数据质量/训练健康/效果对比/训练推理一致性/灰度指标 —— ⭐自动化会放大你的错误而不只是效率。⚠️有反馈闭环时无脑重训会让模型越来越窄(训练数据是它自己筛的)→ 必须掺随机流量数据纠偏。💀真实案例(值得记一辈子):埋点发版把 device_age_days 的单位从天改成小时,26 小时后回滚;周一自动重训照常跑,五道闸全部放行——闸1 只查行数和缺失率(那批数据"完整而正常")、闸2 loss 确实收敛(模型好好地学完了,只是学的是错的东西)、闸3 整体 AUC 0.842→0.836 只掉 0.006,通过了 −0.01 阈值、闸4 两边同一批脏数据当然一致、闸5 整体拦截率没动。真实后果:新设备用户的欺诈召回率 61%→38%,三周后才由业务方发现。⭐⭐那句该记住的话:脏数据占比小 ≠ 影响小——26 小时在 90 天窗口里只占 1.2%,但它是系统性错误不是随机噪声随机噪声会被平均掉,系统性错误会被认真地学进去。三条便宜的改进:闸1 加逐特征 PSI(拿本次和上次训练数据比)、闸3 加分人群对比维护一张数据事故时间表,重训自动剔除那些时段衰减曲线和窗口扫描都要换 3–5 个起点滚动跑,单点结论可能是碰巧;窗口扫描的典型形状是 AUC 先涨后跌,90 天 0.8118 而全量只有 0.7834增量 vs 全量:增量快但会累积误差且难察觉、可复现性差、难回滚 → ⭐增量为主 + 每月全量重训做基准,两者在同一验证集上比,差距扩大就说明增量跑偏了(零成本的漂移探针)。

下一节 👉 17-版本回溯与可复现.md

打卡记录保存在你的浏览器里,首页能看到总进度