📑 本页目录(点开跳转)
10 · SHAP:能做什么、不能做什么
⏱ 42 分钟 | ⭐ 线上排查的主力工具
🎯 一句话
SHAP 把一条预测拆成「基准值 + 每个特征的贡献」,而且这些贡献严格加起来等于预测值。 它是唯一能解释单条预测、又不需要标签的工具 —— 这两条让它成为线上排查的主力。
🧮 一、它在算什么
$$\text{预测值} = \underbrace{\phi_0}_{\text{基准(全体平均预测)}} + \sum_{i} \phi_i$$
💡 人话:
「这个用户的评分是 0.83。全体平均是 0.40, 其中 +0.25 来自他的会员身份,+0.15 来自浏览时长,−0.02 来自地域…… 加起来正好 0.83。」
⭐ 这个「严格加和」的性质叫【可加性】,是 SHAP 最值钱的地方:
你可以把一条预测【完整分解】,不多不少
φᵢ 怎么来的(Shapley 值的直觉)
来自合作博弈论:一群人合作赚了钱,怎么公平分?
Shapley 的答案:
看每个人【加入团队时带来的边际贡献】,
对【所有可能的加入顺序】取平均 ⭐
映射到模型:
「特征」= 人,「预测值」= 赚的钱
φᵢ = 把特征 i 加进来时,预测值平均变化多少
💡 为什么要「对所有顺序取平均」:因为特征之间有交互。 「会员身份」在已知「浏览时长很高」时的边际贡献, 和在什么都不知道时是不一样的。取平均是为了公平分配交互带来的收益。
⚠️ 代价:所有顺序有 n! 种,精确计算是指数级的。 实际都用近似 —— 这正是下一节要说的。
🔧 二、三种实现,别用错
| 实现 | 适用模型 | 速度 | 精确性 |
|---|---|---|---|
| TreeSHAP ⭐ | 树模型(XGBoost/LightGBM/RF) | 快(多项式时间) | 精确 ⭐⭐ |
| KernelSHAP | 任意模型(黑盒) | 很慢 | 近似 |
| DeepSHAP | 神经网络 | 中 | 近似 |
⭐ 一个关键事实:只有 TreeSHAP 是精确且快的。 树模型能利用树结构在多项式时间内精确算出 Shapley 值 —— 这是 SHAP 在工业界(大量用 GBDT)流行的直接原因。
⚠️ 如果你用 KernelSHAP 解释一个大模型,它可能要跑几分钟一条 —— 线上排查用不了。
import numpy as np
import shap
# 树模型:首选
explainer = shap.TreeExplainer(model)
sv = explainer.shap_values(X_sample) # ⭐ 快且精确
# 验证可加性(值得跑一次确认没用错)
recon = explainer.expected_value + sv.sum(axis=1)
assert np.allclose(recon, model.predict(X_sample, raw_score=True), atol=1e-6)
⚠️ 一个高频错误:分类模型要注意你解释的是 logit 还是概率。 TreeSHAP 默认在 raw score(logit)空间做分解 —— 可加性在 logit 上成立,在概率上不成立(因为中间隔了个 sigmoid)。
✅ 三、SHAP 能做的四件事
① 解释单条预测 ⭐⭐ —— 线上排查的核心用法
用户投诉:「为什么我的额度被降了?」
→ 拉出这条预测的 SHAP 值
→ 「主要是因为近 30 天查询征信 5 次(−0.18)
和负债率从 40% 升到 65%(−0.12)」
⭐ 这是其他方法都做不到的
② 找出异常预测的原因
线上发现一批预测值异常高
→ 算这批样本的 SHAP,和正常样本对比
→ 看是哪个特征的贡献变了 ⭐
→ 通常直接指向那个出问题的特征
🔗 这是第 11 章排查手册里最有效的一步。
③ 依赖图:看特征的真实作用形态
横轴:特征取值 纵轴:该特征的 SHAP 值
⭐ 能看出【非线性】和【拐点】:
「收入低于 5 万时影响很大,超过 10 万后基本没影响」
⭐ 用颜色标第二个特征,还能看出【交互】
④ 全局重要性(取绝对值平均)
$$\text{全局重要性}_i = \frac{1}{n}\sum_{j=1}^{n}|\phi_i^{(j)}|$$
⚠️ 注意要取绝对值:否则正负会抵消,一个强特征可能显示为 0。
❌ 四、SHAP 不能做的三件事(边界)
① 它不是因果 ⭐⭐
SHAP 说「负债率贡献了 −0.12」的意思是:
「在【这个模型】里,这个特征值把预测拉低了 0.12」
它【不】意味着:
· 现实中降低负债率就能提高额度 ← 那要看模型是否真的因果正确
· 负债率是原因 ← 可能是共同原因的结果
🔑 一句话记住:SHAP 解释的是模型,不是世界。 模型学错了,SHAP 会忠实地把那个错误解释给你听。 🔗 因果问题 → 第 13 章
② 特征相关时,归因会「串味」
f1 和 f2 高度相关时,SHAP 会把贡献【分摊】给两者
→ 看起来两个都有中等贡献
→ 但实际可能只有一个是真正起作用的
⚠️ 更糟的是:TreeSHAP 在计算时会用到
「特征独立」这个假设的某种形式,
相关性强时结果可能偏离直觉
对策:结合相关矩阵一起看,高相关的特征当成一组解读(和第 9 章同一个建议)。
③ 它解释不了「模型没学到的东西」
如果一个重要因素【根本不在特征里】
→ SHAP 永远不会提到它
→ 你会得到一个"完整"但实际上遗漏了关键的解释 ⭐
🚀 五、线上用 SHAP 的三个工程注意
| 问题 | 做法 |
|---|---|
| 算不过来 | 不是每条都算;只对异常样本和抽样样本算 ⭐ |
| 要存起来 | 排查时要回溯,SHAP 值应随预测日志一起落盘(至少抽样落) |
| 背景数据集怎么选 ⭐ | expected_value 依赖背景集;背景集要能代表当前流量,用训练集会让基准偏移 |
⭐ 第三条容易被忽略:如果你的背景集是半年前的训练集, 而线上流量已经漂移了,那个「基准值」就不再有意义 —— 所有 SHAP 值都会有一个系统性的偏移。 定期更新背景集。
🔨 六、把"对比异常批次"写成代码
第三节说这是排查里最有效的一步。它值得一段完整的实现。
import numpy as np, pandas as pd, shap
def shap_diff(model, X_normal, X_abnormal, top=10):
"""找出「异常批次」相对「正常批次」是哪些特征的贡献变了。
⭐ 这是线上排查最有效的一步:它直接指向出问题的特征"""
ex = shap.TreeExplainer(model)
sv_n = ex.shap_values(X_normal)
sv_a = ex.shap_values(X_abnormal)
rows = []
for i, c in enumerate(X_normal.columns):
dn, da = sv_n[:, i].mean(), sv_a[:, i].mean() # ⭐ 不取绝对值:方向很重要
rows.append({
"特征": c,
"正常均贡献": round(dn, 4),
"异常均贡献": round(da, 4),
"贡献变化": round(da - dn, 4), # ⭐ 排查看的是【这一列】
"特征值变化": round(X_abnormal[c].mean() - X_normal[c].mean(), 4),
})
df = pd.DataFrame(rows)
df["排序键"] = df["贡献变化"].abs()
return df.sort_values("排序键", ascending=False).head(top).drop(columns="排序键")
一次真实排查的输出(预测值整体偏低的那批样本):
特征 正常均贡献 异常均贡献 贡献变化 特征值变化
user_7d_click_cnt +0.142 −0.031 −0.173 −11.8 ⭐⭐
item_ctr_30d +0.088 +0.081 −0.007 −0.001
user_reg_days −0.021 −0.019 +0.002 +0.3
...
⭐ 一眼定位:预测值下降的 87% 来自 user_7d_click_cnt 一个特征,
而它的均值掉了 11.8 —— 去查这个特征的上游就对了
⭐⭐ 注意最后两列要一起看: - 贡献变化大 + 特征值变化大 → 这个特征的数据出问题了(查上游) - 贡献变化大 + 特征值几乎没变 → 💀 模型换版本了,或者特征顺序错位(第 4 章)
这两种情况的处理完全不同,而只看其中一列区分不出来。
⚡ 性能:什么规模下用什么
| 实现 | 模型规模 | 单条耗时(量级) | 1 万条要多久 |
|---|---|---|---|
| TreeSHAP | 500 棵树 × 200 特征 | ~0.3 ms | ~3 秒 ✅ |
| TreeSHAP | 2000 棵树 × 500 特征 | ~3 ms | ~30 秒 ✅ |
| KernelSHAP | 同上,nsamples=2048 | ~2–6 秒 💀 | ~10 小时 |
| DeepSHAP | 中等 MLP | ~5 ms | ~1 分钟 |
⭐ 差了【四个数量级】。这不是"慢一点",是"能不能用"的区别。
实践中的三种用法:
· 线上实时解释(每条都算) → 只有 TreeSHAP 撑得住
· 排查时批量算(几千条) → TreeSHAP 秒级,KernelSHAP 要过夜
· 做全局分析(抽样几万条) → 都行,但 Kernel 要抽样到几千条
💀 七、SHAP 最常见的误用:被当成因果拿去做产品决策
第四节的边界①很抽象。这是它在现实里长的样子:
某电商的复购预测模型,SHAP 全局分析显示:
「30 天内加购次数」贡献排第 2,方向为正 ✅ 这个结论是对的
产品团队的行动:
→ 在商品页加了三个"加入购物车"的引导入口
→ 首页新增"我的购物车"红点提醒
→ 加购次数 +34% 🎉
三个月后:
→ 复购率 −0.4% 💀
→ 加购到下单的转化率从 23% 掉到 16%
为什么会这样:
| 这个事故的三层损失 | 说明 |
|---|---|
| 产品资源浪费 | 三个月的迭代 |
| 直接指标下降 | 复购 −0.4%、加购转化 23%→16% |
| 模型被自己污染 ⭐⭐ | 「加购次数」这个特征的预测力被人为削弱了,下次重训后它会掉出前十 |
⭐⭐ 第三层最容易被忽略,也最深: 你按 SHAP 的结论去改产品,会改变数据分布,从而让那个结论失效。 这和第 7 章的完播率事故是同一个机制(Goodhart), 只不过这次是人在优化那个指标,而不是模型。
🔑 一句可以直接用的话术(在会议上很有用): 「SHAP 告诉我们模型在用什么,不告诉我们改变什么会有用。 想知道后者,只有做实验。」 🔗 能做实验 → 第 12 章;不能做 → 第 13 章。
⚠️ SHAP 使用的七个坑:
| 坑 | 后果 | 怎么修 |
|---|---|---|
| 在概率空间上假设可加性 | 分解对不上,解释是错的 | 在 logit 空间做,最后再转 ⭐ |
| 背景集用了过期的训练集 | 基准偏移,所有 SHAP 值系统性偏 | 定期更新背景集 |
| 全局重要性忘了取绝对值 | 强特征显示成 0 | np.abs(sv).mean(0) |
| 对相关特征逐个解读 | 贡献被分摊,误判谁在起作用 | 按相关组解读(第 9 章)⭐ |
| 用 KernelSHAP 解释大模型 | 一条几秒,线上完全用不了 | 换 TreeSHAP,或抽样做离线分析 |
| 拿 SHAP 当因果 | 💀 反向的产品决策(本节) | 只做实验才能回答因果 ⭐⭐ |
| 只解释单条不看批次对比 | 单条 SHAP 看不出"和平时比变了没有" | 永远和正常批次对照 ⭐ |
💡 最后一条值得展开:单条 SHAP 只告诉你"这条预测是怎么构成的", 不告诉你"这个构成正不正常"。 用户问"我为什么被拒"时,前者就够; 但排查线上问题时,你要的永远是"和平时比变了什么"。
🧭 八、和第 9 章合起来:一张选择表
| 你要回答的问题 | 用什么 |
|---|---|
| 训练时模型用了哪些特征 | 内建重要性(粗筛) |
| 删掉哪些特征不影响性能 | 置换重要性 |
| 这一条预测为什么是这个结果 | SHAP ⭐⭐ |
| 这批异常样本是哪个特征导致的 | SHAP 对比 ⭐⭐ |
| 这个特征的作用是线性的吗、有拐点吗 | SHAP 依赖图 |
| 改变这个特征会不会真的改变结果 | ❌ 都不行 → 第 13 章 |
🔗 九、和站内其他章的关系
| 相关的地方 | 和这一章的关系 |
|---|---|
| 第 9 章三类重要性 | SHAP 是第三类 |
| Kaggle 01 | 那里提过 SHAP,这里讲边界 |
| ⭐⭐ 博弈论与集体决策 23 · Shapley 值 | SHAP 的那个 φᵢ 就是 1953 年的 Shapley 值,一个字都没改。那一章讲它的来历(联盟博弈里「怎么分合作创造的价值」)、⭐ 为什么"平均边际贡献"是唯一满足四条公理的分法,以及 n=3 的手算。⚠️ 顺便解释了本章那个 $2^n$ 从哪来 —— 它是「枚举所有联盟」的代价 |
| 博弈论与集体决策 22 · 联盟博弈与核心 | Shapley 值只是分法之一。核心(Core) 是另一套判据(没有哪伙人想退出去单干),两者常常给出不同答案 |
| 第 11 章 | SHAP 对比是排查主力 |
✅ 检查点
- SHAP 的可加性是什么?为什么它值钱?
- Shapley 值的直觉是什么?为什么要对所有顺序取平均?
- 三种实现里哪个是精确且快的?这解释了什么现象?
- 分类模型用 SHAP 时最容易犯什么错?
- SHAP 能做的四件事?哪两件是排查时最有用的?
- 为什么说「SHAP 解释的是模型,不是世界」?
- 特征相关时 SHAP 会怎样?
- 背景数据集选错了会有什么后果?
- 做异常批次 SHAP 对比时,为什么「贡献变化」和「特征值变化」两列要一起看?各自指向什么?
- TreeSHAP 和 KernelSHAP 的速度差多少?这决定了什么?
- 「加购次数」那个事故里,真实的因果结构是什么?三层损失分别是什么?
- 为什么说「你按 SHAP 的结论改产品,会让那个结论失效」?
- 「只解释单条不看批次对比」为什么是个坑?
👀 答案
- 预测值 = 基准值 + 各特征贡献之和,严格相等。值钱是因为能把一条预测完整分解,不多不少。
- 来自合作博弈论:看每个特征加入时的边际贡献,对所有可能的加入顺序取平均。取平均是因为特征之间有交互——某特征在已知另一特征时的边际贡献不同,取平均是为了公平分配交互收益。
- TreeSHAP(树模型,多项式时间、精确)。这解释了SHAP 在工业界流行——因为工业界大量用 GBDT。KernelSHAP 很慢,线上排查用不了。
- 搞混 logit 和概率空间。TreeSHAP 默认在 raw score(logit) 空间分解,可加性在 logit 上成立、在概率上不成立(中间隔了 sigmoid)。
- ①解释单条预测 ②找异常预测的原因 ③依赖图看非线性和拐点 ④全局重要性(要取绝对值否则正负抵消)。排查时最有用的是 ①和②。
- 因为它解释的是模型内部的运算,模型学错了 SHAP 会忠实地把那个错误解释给你听。它不能告诉你现实中改变这个特征会不会改变结果。
- 会把贡献分摊给相关的几个特征("串味"),看起来都有中等贡献但实际可能只有一个真正起作用。对策是结合相关矩阵,把高相关特征当一组解读。
expected_value(基准值)会偏移。如果背景集是半年前的训练集而线上已漂移,基准值不再有意义,所有 SHAP 值都有系统性偏移。要定期更新背景集。- 因为两种情况处理完全不同:贡献变化大 + 特征值变化大 → 这个特征的数据出问题了(去查上游);贡献变化大 + 特征值几乎没变 → 模型换版本了,或者特征顺序错位(第 4 章)。只看其中一列区分不出来。
- 差四个数量级:TreeSHAP 单条 ~0.3ms(1 万条约 3 秒),KernelSHAP 单条 ~2–6 秒(1 万条约 10 小时)。这决定的不是"慢一点",而是能不能用——线上实时解释只有 TreeSHAP 撑得住,排查时批量算 KernelSHAP 要过夜。
- 真实因果是 「购买意愿」同时导致「加购多」和「复购多」,加购和复购是同一个原因的两个结果;人为提高加购不会提高购买意愿。三层损失:①产品资源浪费(三个月迭代)②直接指标下降(复购 −0.4%、加购转化 23%→16%)③⭐⭐模型被自己污染——「加购次数」的预测力被人为削弱,下次重训后会掉出前十。
- 因为改产品会改变数据分布,从而让那个结论本身失效。这和第 7 章完播率事故是同一个机制(Goodhart),只不过这次是人在优化那个指标而不是模型。可以直接用的话术:「SHAP 告诉我们模型在用什么,不告诉我们改变什么会有用;想知道后者只有做实验。」
- 因为单条 SHAP 只告诉你"这条预测是怎么构成的",不告诉你"这个构成正不正常"。用户问"我为什么被拒"时单条就够,但排查线上问题时你要的永远是"和平时比变了什么"。
🛑 可以停在这里
⚡ 走神救援
⭐SHAP = 把一条预测拆成「基准 + 每个特征的贡献」,且严格加起来等于预测值(可加性)。它是唯一能解释单条、又不需要标签的工具 → 线上排查主力。Shapley 来自博弈论:看每个特征加入时的边际贡献,对所有加入顺序取平均(因为有交互);代价是 n! 种顺序,精确计算指数级。⭐三种实现里只有 TreeSHAP 精确且快(树结构能多项式时间精确算)——这就是 SHAP 在工业界流行的直接原因(大家都用 GBDT);KernelSHAP 一条要几分钟,线上用不了。⚠️高频错误:分类模型要分清 logit 和概率空间——可加性只在 logit 上成立。能做四件事:⭐解释单条预测、⭐对比异常批次找出是哪个特征变了(排查最有效的一步)、依赖图看非线性和拐点、全局重要性(要取绝对值否则正负抵消)。❌三个边界:⭐⭐它不是因果——SHAP 解释的是模型不是世界,模型学错了它会忠实地把错误解释给你听;特征相关时归因会串味;没在特征里的因素它永远不会提到(你会得到一个"完整"但遗漏关键的解释)。工程注意:只对异常和抽样样本算、SHAP值要随日志落盘、⭐背景集要能代表当前流量(用半年前的训练集会让基准偏移)。🔨排查最有效的一步是「异常批次 vs 正常批次的 SHAP 对比」——⭐贡献变化和特征值变化两列要一起看:都变了 = 数据出问题去查上游;贡献变了但特征值没变 = 💀模型换版本或特征顺序错位。⚡性能差四个数量级:TreeSHAP 单条 0.3ms(1万条3秒),KernelSHAP 单条 2–6 秒(1万条 10 小时)——这是"能不能用"的区别不是"快不快"。💀最常见的误用:把 SHAP 当因果做产品决策——"加购次数贡献第2"→加了三个加购入口→加购 +34% 但复购 −0.4%、加购转化 23%→16%;真相是购买意愿同时导致加购和复购,两者是同一个原因的两个结果。⭐⭐第三层损失最深:你按 SHAP 改产品会改变数据分布,从而让那个结论本身失效(和第7章完播率是同一个 Goodhart 机制,只是这次是人在优化)。话术:「SHAP 告诉我们模型在用什么,不告诉我们改变什么会有用。」 七坑里最后一条:⭐只看单条不看批次对比——单条只说"怎么构成的",排查要的永远是"和平时比变了什么"。
下一节 👉 11-从告警到根因.md ⭐