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