🏠 总目录📚 本教程 10 · SHAP 能与不能
📑 本页目录(点开跳转)

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 对比是排查主力

✅ 检查点

  1. SHAP 的可加性是什么?为什么它值钱?
  2. Shapley 值的直觉是什么?为什么要对所有顺序取平均?
  3. 三种实现里哪个是精确且快的?这解释了什么现象?
  4. 分类模型用 SHAP 时最容易犯什么错?
  5. SHAP 能做的四件事?哪两件是排查时最有用的?
  6. 为什么说「SHAP 解释的是模型,不是世界」?
  7. 特征相关时 SHAP 会怎样?
  8. 背景数据集选错了会有什么后果?
  9. 做异常批次 SHAP 对比时,为什么「贡献变化」和「特征值变化」两列要一起看?各自指向什么?
  10. TreeSHAP 和 KernelSHAP 的速度差多少?这决定了什么?
  11. 「加购次数」那个事故里,真实的因果结构是什么?三层损失分别是什么?
  12. 为什么说「你按 SHAP 的结论改产品,会让那个结论失效」?
  13. 「只解释单条不看批次对比」为什么是个坑?
👀 答案
  1. 预测值 = 基准值 + 各特征贡献之和,严格相等。值钱是因为能把一条预测完整分解,不多不少
  2. 来自合作博弈论:看每个特征加入时的边际贡献,对所有可能的加入顺序取平均。取平均是因为特征之间有交互——某特征在已知另一特征时的边际贡献不同,取平均是为了公平分配交互收益。
  3. TreeSHAP(树模型,多项式时间、精确)。这解释了SHAP 在工业界流行——因为工业界大量用 GBDT。KernelSHAP 很慢,线上排查用不了。
  4. 搞混 logit 和概率空间。TreeSHAP 默认在 raw score(logit) 空间分解,可加性在 logit 上成立、在概率上不成立(中间隔了 sigmoid)。
  5. ①解释单条预测 ②找异常预测的原因 ③依赖图看非线性和拐点 ④全局重要性(要取绝对值否则正负抵消)。排查时最有用的是 ①和②
  6. 因为它解释的是模型内部的运算模型学错了 SHAP 会忠实地把那个错误解释给你听。它不能告诉你现实中改变这个特征会不会改变结果。
  7. 会把贡献分摊给相关的几个特征("串味"),看起来都有中等贡献但实际可能只有一个真正起作用。对策是结合相关矩阵,把高相关特征当一组解读。
  8. expected_value(基准值)会偏移。如果背景集是半年前的训练集而线上已漂移,基准值不再有意义,所有 SHAP 值都有系统性偏移。要定期更新背景集。
  9. 因为两种情况处理完全不同:贡献变化大 + 特征值变化大 → 这个特征的数据出问题了(去查上游);贡献变化大 + 特征值几乎没变 → 模型换版本了,或者特征顺序错位(第 4 章)。只看其中一列区分不出来。
  10. 四个数量级:TreeSHAP 单条 ~0.3ms(1 万条约 3 秒),KernelSHAP 单条 ~2–6 秒(1 万条约 10 小时)。这决定的不是"慢一点",而是能不能用——线上实时解释只有 TreeSHAP 撑得住,排查时批量算 KernelSHAP 要过夜。
  11. 真实因果是 「购买意愿」同时导致「加购多」和「复购多」,加购和复购是同一个原因的两个结果;人为提高加购不会提高购买意愿。三层损失:①产品资源浪费(三个月迭代)②直接指标下降(复购 −0.4%、加购转化 23%→16%)③⭐⭐模型被自己污染——「加购次数」的预测力被人为削弱,下次重训后会掉出前十。
  12. 因为改产品会改变数据分布,从而让那个结论本身失效。这和第 7 章完播率事故是同一个机制(Goodhart),只不过这次是在优化那个指标而不是模型。可以直接用的话术:「SHAP 告诉我们模型在用什么,不告诉我们改变什么会有用;想知道后者只有做实验。」
  13. 因为单条 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

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