🏠 总目录📚 本教程 02 · 离线好≠线上好
📑 本页目录(点开跳转)

02 · 离线好不等于线上好

38 分钟 | ⭐ 上线前的最后一道关


🎯 一句话

离线评估回答的是「在过去的数据上,它比旧模型准」; 线上要回答的是「在未来的流量上,它比旧模型好」。 这两句话之间有五道坎,这一章逐个拆。

完美校准过度自信过度保守模型说的概率实际发生的比例模型说「70% 会点」,那批人里真的有 70% 点了吗?说 80% 实际只有 60%说 40% 实际有 60%⭐ AUC 只管排序 —— 这两条曲线的 AUC 可以【完全一样】但要拿分数去出价、算期望收益、卡阈值时,校准错了就全错修法:Platt scaling / isotonic regression 做一次后校准
模型说「70% 会点」,那批人里真的有 70% 点了吗?⭐ 这两条曲线的 AUC 可以完全一样 —— 因为 AUC 只管排序。但要拿分数去出价、算期望收益、卡阈值时,校准错了就全错。

🕳️ 一、五道坎

   离线 AUC 更高
      ↓ ① 评估数据和线上流量不是一回事
   在线上流量的分布下也更准
      ↓ ② 排序指标 ≠ 用户实际看到的
   用户看到的那几个更好了
      ↓ ③ 模型指标 ≠ 业务指标
   业务指标涨了
      ↓ ④ 你的模型不是唯一变量
   涨的确实是你造成的
      ↓ ⑤ 短期涨 ≠ 长期好
   长期也是好的  ✅

每一步都可能断。 ①②③ 是这一章;④ 是第 12–14 章;⑤ 是第 15 章


⚠️ 二、坎 ①:评估数据和线上流量不是一回事

最常见的三种不一致

不一致 后果 怎么发现
时间不一致 用了未来数据(ML基础 05的泄漏) 严格按时间切分
样本选择偏差 评估集里全是旧模型曝光过的样本 见下
人群不一致 评估用全量,上线只给新用户 按上线人群切一份评估

🔑 把第二条讲透:幸存者偏差

   你的评估集是怎么来的?
   → 线上日志:用户看到了什么、点了什么

   但「用户看到了什么」是【旧模型决定的】  ⭐

   → 评估集里全是旧模型认为「值得曝光」的样本
   → 新模型想推的那些东西,评估集里【根本没有】
   → 你在旧模型划定的范围里比较两个模型  💀

🔗 这就是推荐算法 02 反馈闭环在评估上的后果。

务实的缓解办法(不需要改架构): 留一小份随机流量(比如 1%)—— 这部分用户看到的东西不由模型决定, 用它做评估集,就没有幸存者偏差。 代价是这 1% 的体验会差一点,换来的是一把没被污染的尺子。


📏 三、坎 ②:排序指标 ≠ 用户实际看到的

   AUC = 「随机取一正一负,正的排在前面的概率」
   → 它关心【整个列表】的排序

   但用户只看前 3 个(信息流甚至只看 1 个)
   → 第 800 名和第 900 名谁在前面,【对用户毫无意义】

换成位置敏感的指标

指标 关心什么 什么时候用
AUC 全局排序 粗排、召回;或样本极不平衡时
NDCG@K 前 K 个的排序质量,位置有折扣 精排、搜索
Recall@K 前 K 个里捞回了多少 召回阶段推荐算法 18
MRR 第一个正确答案排第几 问答、只需一个答案的场景
Precision@1 第一个对不对 单坑位(开屏、语音助手)

💡 一个实测过的数字推荐算法 12 里跑过):

NDCG@3 完美排序 = 1.0;把第 1 和第 3 交换,NDCG@3 就掉到 0.92 —— 而 AUC 几乎不动同一个改动,两个指标给出完全不同的评价。

⚠️ AUC 还有一个陷阱:它对分数的绝对值不敏感。

   模型 A 预测:[0.90, 0.10]   真实:[1, 0]
   模型 B 预测:[0.51, 0.49]   真实:[1, 0]

   AUC 完全一样(都是 1.0)✅
   但如果你要用分数做【出价】或【阈值决策】——B 是灾难 💀

   ⭐ 这时候要看【校准度】而不是排序:
      预测 0.7 的那批样本,实际正例率是不是真的 70%

🔗 需要精确概率的场景(广告出价、风控、期望收益计算) 必须额外看校准曲线,AUC 高不代表概率对。 常用修法:Platt scaling / isotonic regression 做后校准。


💰 四、坎 ③:模型指标 ≠ 业务指标

   AUC 涨 2% ──────?──────→ CTR 涨多少?

   ⭐ 没有通用换算公式。这条连接【必须自己实测建立】

建立这条连接的做法

   ① 前几次 A/B,同时记录【模型指标变化】和【业务指标变化】
   ② 攒够 5-10 组,画个散点图
   ③ 得到你这个场景的经验换算率
      例:「历史上 AUC +0.005 大致对应 CTR +1%」

   ⭐ 有了它,以后离线看到 AUC +0.001 就知道:
      不值得上线,提升会被噪声淹没

💡 这是个高回报的一次性投入: 它让你在做实验之前就能判断「这个改动值不值得占一个实验位」—— 而实验位通常是团队最稀缺的资源。


🚦 五、上线前的检查清单(可以照抄)

   数据侧
   [ ] 评估集按时间切分,无未来信息
   [ ] 评估人群 = 上线目标人群
   [ ] 有一份随机流量评估集(对抗幸存者偏差)⭐

   指标侧
   [ ] 用的是位置敏感指标(如果是排序场景)
   [ ] 如果分数要用于决策,看了校准曲线 ⭐
   [ ] 知道模型指标→业务指标的大致换算率

   工程侧
   [ ] 训练/推理特征一致性已验证(第 4 章)⭐⭐
   [ ] 延迟、QPS、内存压测通过
   [ ] 降级路径明确:模型挂了返回什么

   实验侧
   [ ] 样本量够(第 12 章会算)
   [ ] 明确了实验时长和停止条件
   [ ] 想清楚了「什么结果算失败、失败了怎么办」⭐

最后一条最容易漏,也最重要在实验开始前就写下失败标准。 否则实验跑出负向时,人会本能地找理由继续跑 —— 这在统计上叫 peeking,会让你的结论完全不可信(第 12 章)。


💀 六、一个完整的翻车复盘:离线 +0.02,线上 −1.5%

这是坎 ① 最典型的形态,值得逐步看。

   背景:某电商的「猜你喜欢」精排模型换代
   离线:AUC 0.812 → 0.831(+0.019),所有人都觉得稳了
   上线 A/B 一周:CTR −1.5%,人均下单量 −0.8%   💀

排查过程(三天,走了两条弯路)

第几步 猜测 验证结果
1 训练/推理不一致? 逐字段比对,全部一致
2 延迟变高导致超时降级? P99 从 38ms 到 41ms,降级次数 0 ❌
3 评估集怎么来的? 找到了
   评估集的构造 SQL 里有一句:
       WHERE user_click_cnt_7d > 0      ← 「过滤掉无行为用户」

   加这句的原因是三年前有人觉得「无行为用户是噪声」
   → 从此评估集里全是活跃用户(占流量 34%)
   → 新模型在活跃用户上确实更好(AUC 0.84 → 0.87)
   → 但在剩下 66% 的低活用户上,AUC 从 0.79 掉到 0.74  💀
   → 加权到全量:真实 AUC 是【下降】的
人群 流量占比 旧模型 AUC 新模型 AUC
有 7 日点击 34% 0.84 0.87 +0.03
无 7 日点击 66% 0.79 0.74 −0.05 💀
全量加权 100% 0.807 0.784 −0.023

三条可以带走的教训: 1. 离线评估集的构造 SQL,每一个 WHERE 都要有人负责解释。 一句三年前的过滤条件,能让所有后续实验的结论都失真。 2. 永远同时看「全量」和「分人群」的离线指标。 只看一个数是危险的。 🔗 这就是第 14 章辛普森悖论在离线评估上的形态。 3. 排查时先查最便宜的假设:这次前两步各花了一天, 而"打开评估集的 SQL 看一眼"只要十分钟。 🔗 第 11 章会给一个排序好的排查清单。


📐 七、校准:什么时候必须看它,怎么看

第三节说了 AUC 不管分数的绝对值。什么时候这件事真的会要命

你拿分数干什么 校准重要吗 说明
只做排序(推荐列表、搜索) 🟡 不太重要 只要顺序对就行
卡阈值做决策(风控通过/拒绝) 重要 阈值 0.7 的含义必须稳定
算期望收益p × 客单价 很重要 概率错了,期望全错
广告出价bid = pCTR × pCVR × 价值 致命 ⭐⭐ 见下

💡 一个有具体数字的例子

   某广告模型换代后:AUC 0.78 → 0.80(涨了)
   但 pCTR 系统性高估 1.8 倍(预测 5%,实际 2.8%)

   → 出价 = pCTR × 价值,出价整体高 1.8 倍
   → 抢到更多曝光,消耗涨 40%   ← 看起来是好事 🎉
   → 但实际点击没跟上
   → ROI 从 3.2 掉到 1.7        ← 💀 一个月后财务才发现

注意这个事故的形状排序变好了,业务变差了, 而且中间指标(消耗、曝光)全是涨的 —— 这让它特别难被发现。

🔨 怎么量校准:ECE + 校准表

import numpy as np, pandas as pd

def calibration_table(p, y, bins=10):
    """p: 预测概率, y: 0/1 真实标签。返回分箱校准表 + ECE"""
    # ⭐ 按【分位数】切箱,不是等宽——等宽切在长尾场景会有空箱
    edges = np.unique(np.quantile(p, np.linspace(0, 1, bins + 1)))
    idx = np.clip(np.digitize(p, edges[1:-1]), 0, len(edges) - 2)
    rows, ece, n = [], 0.0, len(p)
    for b in range(len(edges) - 1):
        m = idx == b
        if m.sum() == 0:
            continue
        pred, real, cnt = p[m].mean(), y[m].mean(), int(m.sum())
        rows.append((b, cnt, round(pred, 4), round(real, 4), round(pred - real, 4)))
        ece += cnt / n * abs(pred - real)          # ⭐ ECE = 按样本量加权的平均偏差
    tbl = pd.DataFrame(rows, columns=["箱", "样本数", "平均预测", "实际正例率", "偏差"])
    return tbl, ece
   怎么读这张表:
   · 偏差列【全是同号】     → 系统性高估或低估 → 后校准能修  ✅
   · 偏差在高分段特别大     → 头部样本不可信(广告/风控最要命)⭐
   · ECE < 0.01            → 基本可用
     ECE 0.01 ~ 0.05       → 排序可用,别拿去算期望
     ECE > 0.05            → 💀 不要用它做任何数值决策

修法(按代价从低到高)

方法 做法 适合
Platt scaling 在验证集上拟合一个 sigmoid 偏差是单调、平滑的
Isotonic regression 拟合一个单调阶梯函数 偏差形状复杂;要求验证集够大(>10k)
分人群分别校准 每个人群一套校准器 不同人群偏差方向不同时 ⭐
重新训练 换损失函数 / 改采样 前面都不行时

⚠️ 两个高频坑

后果 怎么修
在训练集上做校准 校准器学到的是过拟合后的分数分布,线上完全不成立 💀 必须用独立的验证集
负采样后忘了修正 训练时把负样本采样到 1/10,预测概率整体被放大约 10 倍 用采样率反推:$p = \frac{p'}{p' + (1-p')/w}$

第二条在广告和推荐里极其常见:负采样是标配,而采样只影响概率、不影响 AUC —— 所以它能一路通过所有离线评估,直到有人拿分数去出价。


🔗 八、和站内其他章的关系

相关的地方 和这一章的关系
ML基础 05数据泄漏 坎 ① 的时间不一致
推荐算法 02反馈闭环 幸存者偏差的根源
推荐算法 12 NDCG / A/B 坎 ② 和坎 ④ 的推荐场景版
Kaggle 01评价指标 竞赛视角的指标选择

| 数据这一关 13 · 评测集也是数据 | 这四处讲的是各自场景怎么评测;评测集本身怎么造、多大够、有没有被污染、评分器准不准、结果该怎么报 在那一章 ⭐ |

✅ 检查点

  1. 从「离线 AUC 更高」到「长期确实更好」,中间有哪五道坎?
  2. 什么是评估里的幸存者偏差?根源是什么?务实的缓解办法是什么?
  3. 为什么排序场景不该只看 AUC?NDCG@3 那个例子说明了什么?
  4. AUC 高但分数不能用来做决策,是什么问题?怎么修?
  5. 模型指标和业务指标的换算率怎么建立?为什么值得做?
  6. 上线前检查清单里最容易漏的是哪条?漏了会怎样?
  7. 「离线 +0.02 线上 −1.5%」那个事故的真因是什么?它给出的三条教训是什么?
  8. 什么场景下校准是致命的?举那个广告出价的数字例子,说明它为什么难被发现。
  9. ECE 怎么算?三档判读标准是什么?
  10. 校准的两个高频坑是什么?为什么负采样那个坑能一路通过所有离线评估?
👀 答案
  1. ①评估数据≠线上流量 ②排序指标≠用户实际看到的 ③模型指标≠业务指标 ④你的模型不是唯一变量 ⑤短期涨≠长期好。
  2. 评估集里全是旧模型曝光过的样本,新模型想推的东西评估集里根本没有 —— 你在旧模型划定的范围里比较。根源是反馈闭环。缓解:留 1% 随机流量做评估集,这部分不由模型决定,是把没被污染的尺子。
  3. 因为 AUC 关心整个列表的排序,而用户只看前几个——第 800 和 900 名谁在前对用户毫无意义。NDCG@3 例子:交换第 1 和第 3 位,NDCG@3 从 1.0 掉到 0.92,而 AUC 几乎不动——同一改动两个指标给出完全不同的评价。
  4. 校准问题:AUC 只看排序、对分数绝对值不敏感。预测 [0.51,0.49] 和 [0.90,0.10] 的 AUC 一样,但用于出价/阈值决策时前者是灾难。修法:看校准曲线 + Platt scaling / isotonic regression 后校准
  5. 前几次 A/B 同时记录模型指标和业务指标的变化,攒 5-10 组画散点,得到经验换算率。值得做是因为能在做实验之前判断改动值不值得占一个实验位,而实验位是最稀缺的资源。
  6. 「在实验开始前就写下失败标准」。漏了的话,实验跑出负向时人会本能找理由继续跑(peeking),结论完全不可信
  7. 真因是评估集构造 SQL 里一句三年前的 WHERE user_click_cnt_7d > 0,把 66% 的低活用户过滤掉了。新模型在活跃用户上 AUC 0.84→0.87,但在低活用户上 0.79→0.74,全量加权后其实是 −0.023。三条教训:①评估集的每一个 WHERE 都要有人负责解释永远同时看全量和分人群指标排查时先查最便宜的假设(前两步各花一天,打开 SQL 看一眼只要十分钟)。
  8. 广告出价bid = pCTR × 价值)最致命。例子:AUC 0.78→0.80 但 pCTR 高估 1.8 倍 → 出价高 1.8 倍 → 消耗涨 40%、曝光涨,看起来全是好事,但点击没跟上,ROI 从 3.2 掉到 1.7,一个月后财务才发现。难发现是因为排序变好了、中间指标全是涨的
  9. 按分位数分箱,每箱算「平均预测」和「实际正例率」,ECE = 按样本量加权的平均绝对偏差。三档:<0.01 基本可用;0.01~0.05 排序可用但别算期望;>0.05 不要做任何数值决策
  10. 在训练集上做校准(学到的是过拟合后的分数分布,线上完全不成立)②负采样后忘了修正(负样本采样到 1/10,预测概率整体放大约 10 倍)。第二个能通过所有离线评估,是因为采样只影响概率、不影响 AUC——直到有人拿分数去出价。

🛑 可以停在这里

走神救援

离线好≠线上好,中间有五道坎:评估数据≠线上流量 → 排序指标≠用户看到的 → 模型指标≠业务指标 → 你不是唯一变量 → 短期≠长期。⭐坎①的核心是幸存者偏差:评估集全是旧模型曝光过的样本,新模型想推的东西里面根本没有 —— 务实解法:留 1% 随机流量做评估集,代价是这部分体验差一点,换一把没被污染的尺子。⭐坎②:AUC 关心全列表排序但用户只看前几个实测:交换第1和第3位,NDCG@3 从1.0掉到0.92而AUC几乎不动。⚠️AUC 对分数绝对值不敏感——[0.51,0.49] 和 [0.90,0.10] 的 AUC 一样,但要用分数出价/做阈值决策时前者是灾难 → 看校准曲线,用 Platt/isotonic 后校准。坎③:AUC→CTR 没有通用换算公式,必须自己用几次 A/B 实测建立,有了它就能在做实验前判断值不值得占实验位。⭐清单里最容易漏的:实验开始前就写下失败标准,否则跑出负向时会找理由继续跑(peeking),结论不可信。💀完整复盘:离线 AUC +0.019、线上 CTR −1.5%,真因是评估集 SQL 里一句三年前的 WHERE 7日点击>0 —— 新模型在 34% 活跃用户上 0.84→0.87,在 66% 低活用户上 0.79→0.74全量加权其实是 −0.023。教训:每一个 WHERE 都要有人能解释永远同时看全量和分人群先查最便宜的假设(打开 SQL 十分钟 vs 前两步各一天)。⭐校准什么时候致命:卡阈值、算期望收益、广告出价——真实案例 pCTR 高估 1.8 倍,消耗涨40%、曝光涨,中间指标全是好的,但 ROI 3.2→1.7,一个月后财务才发现。ECE 判读:<0.01 可用 / 0.01~0.05 只能排序 / >0.05 不要做任何数值决策。修法 Platt(偏差平滑)、isotonic(形状复杂,需验证集>10k)、分人群校准。⚠️两个坑:在训练集上校准(线上完全不成立)、⭐负采样后忘了修正(概率整体放大约10倍,因为采样只影响概率不影响 AUC,所以能一路通过全部离线评估)。

下一节 👉 03-灰度影子与回滚.md

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