📑 本页目录(点开跳转)
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 能做的四件事
① 解释单条预测 ⭐ —— 线上排查的核心用法
因果链
② 找出异常预测的原因
结果对照
🔗 这是第 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 章
② 特征相关时,归因会「串味」
关键信息
对策:结合相关矩阵一起看,高相关的特征当成一组解读(和第 9 章同一个建议)。
③ 它解释不了「模型没学到的东西」
关键信息
🚀 五、线上用 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 分钟 |
结果对照
💀 七、SHAP 最常见的误用:被当成因果拿去做产品决策
第四节的边界①很抽象。这是它在现实里长的样子:
结果对照
为什么会这样:
| 这个事故的三层损失 | 说明 |
|---|---|
| 产品资源浪费 | 三个月的迭代 |
| 直接指标下降 | 复购 −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 = 把一条预测拆成「基准 + 每个特征的贡献」,且严格加起来等于预测值。 它是唯一能解释单条、又不需要标签的工具,所以是线上排查主力。
三种实现里只有 TreeSHAP 又精确又快——这就是 SHAP 在工业界流行的直接原因(大家都用 GBDT)。KernelSHAP 单条要几秒,线上用不了,两者差四个数量级,这是「能不能用」的区别。
⭐ 排查最有效的一步是「异常批次 vs 正常批次」的 SHAP 对比,而且要同时看两列:贡献变了、特征值也变了 = 输入出问题,去查上游;贡献变了但特征值没变 = 💀 模型换了版本或特征顺序错位。
❌ 三条边界:它不是因果(SHAP 解释的是模型,不是世界——模型学错了它会忠实地把错误解释给你听)、特征相关时归因会串味、没进特征的因素它永远不会提。
💀 最常见的误用是把 SHAP 当因果做产品决策:看到「加购贡献第二」就加了三个加购入口,结果加购涨了、复购反而跌。真相是购买意愿同时导致加购和复购,两者是同一个原因的两个结果。
⭐ 记住这句话:「SHAP 告诉我们模型在用什么,不告诉我们改变什么会有用。」
下一节 👉 11-从告警到根因.md ⭐