🏠 总目录📚 本教程 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 涨多少?
⭐ 没有通用换算公式。这条连接【必须自己实测建立】

建立这条连接的做法:

操作步骤

  1. 前几次 A/B,同时记录【模型指标变化】和【业务指标变化】
  2. 攒够 5-10 组,画个散点图
  3. 得到你这个场景的经验换算率
  4. 例:「历史上 AUC +0.005 大致对应 CTR +1%」
  5. ⭐ 有了它,以后离线看到 AUC +0.001 就知道:
  6. 不值得上线,提升会被噪声淹没

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


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

信息关系

数据侧
[ ] 评估集按时间切分,无未来信息
[ ] 评估人群 = 上线目标人群
[ ] 有一份随机流量评估集(对抗幸存者偏差)⭐
指标侧
[ ] 用的是位置敏感指标(如果是排序场景)
[ ] 如果分数要用于决策,看了校准曲线 ⭐
[ ] 知道模型指标→业务指标的大致换算率
工程侧
[ ] 训练/推理特征一致性已验证(第 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——直到有人拿分数去出价。

🛑 可以停在这里

⚡ 走神救援

离线好不等于线上好,中间有五道坎:评估数据不是线上流量、排序指标不是用户看到的、模型指标不是业务指标、你不是唯一变量、短期不是长期。

⭐ 第一道坎的核心是幸存者偏差:评估集全是旧模型曝光过的样本,而新模型想推的东西根本不在里面。务实解法是留一小份随机流量当评估集——代价是这部分体验差一点,换一把没被污染的尺子。

⭐ 第二道坎:AUC 关心整个列表的排序,而用户只看前几个——把第一和第三对调,前几名的指标明显掉,AUC 几乎不动。⚠️ AUC 对分数的绝对值也不敏感:两组差距悬殊的分数可以有一样的 AUC,但要拿分数去出价或卡阈值时,其中一组是灾难。

⭐ 第三道坎没有通用换算公式:模型指标到业务指标的兑换率必须自己用几次 A/B 实测建立——有了它才能在占用实验位之前判断值不值得做。

⭐ 清单里最容易漏的一条:实验开始前就写下失败标准——否则跑出负向时人会找理由继续跑,结论就不可信了。

💀 那次完整复盘最值得记:离线涨、线上跌,真因是评估集 SQL 里一句三年前的过滤条件。⭐ 拆开看是新模型在活跃用户上更好、在低活用户上更差,而低活占大多数,加权之后其实是负的。三条教训:每一个 WHERE 都要有人能解释、永远同时看全量和分人群、⭐ 先查最便宜的假设(打开那句 SQL 十分钟,而前两步各要一天)。

⭐ 校准什么时候致命:卡阈值、算期望收益、广告出价——真实案例里预估值整体高估,消耗和曝光这些中间指标全是好的,而投入产出比腰斩,一个月后财务才发现。

⚠️ 两个坑:在训练集上校准(线上完全不成立)、⭐ 负采样后忘了修正——概率被整体放大,而采样只影响概率不影响 AUC,所以它能一路通过全部离线评估。

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

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