第2章 SHAP理论基础:Shapley值、博弈论解释、加性特征归因
好,这一章咱们聊聊SHAP到底是怎么算出来的。很多人一开始接触SHAP,看到一堆数学符号就头疼。别急,我会尽量用大白话讲清楚。我自己刚入行的时候,也是被那个Shapley值的公式吓一跳,后来发现,核心逻辑其实非常简单,就是一个“公平分配”的问题。
1. 博弈论里的“公平分配”
SHAP的名字来源于Shapley值,而Shapley值最早是博弈论里的概念。简单说,就是一群玩家合作赚了一笔钱,怎么分才算公平?每个人应该拿多少?Shapley值的答案很简单:拿你贡献的平均值。
举个例子:假设三个玩家A、B、C组队干活,挣了100块。A单独干能挣30,B单独干能挣20,C单独干能挣10。两两组合呢?A+B能挣70,A+C能挣50,B+C能挣40。三人一起挣100。问:A该分多少?Shapley值的做法是,遍历所有可能的加入顺序,算A在每个人加入时带来的“边际贡献”,最后取平均。
关键理解:Shapley值就是每个玩家在所有可能合作顺序下的平均边际贡献。放到模型里,每个特征就是“玩家”,预测结果就是“总收益”。
我在项目中遇到过一个问题:一个客户问,为什么某个特征对预测结果的贡献是负数?其实很好解释,Shapley值允许负贡献,代表该特征在当前组合下“拖后腿”了——就像团队里有人干活少还分钱多,Shapley值会算出他的实际贡献是负的。这一点在实际业务中非常重要,能帮你精准定位“坏特征”。
2. Shapley值的数学公式——其实不复杂
好了,我们来看正经公式。虽然符号多,但拆开看很直观:
φᵢ(v) = Σ_{S ⊆ N\{i}} [ |S|! (|N|-|S|-1)! / |N|! ] × [ v(S∪{i}) - v(S) ]
这里:
- N是所有特征集合(玩家总集合)
- S是不包含特征i的任意子集
- v(S)是只用子集S里的特征时模型的预测值
- v(S∪{i})是加上特征i后的预测值
- 前面的分数是一个权重,保证每个子集大小出现的概率均衡
说白了,就是对所有可能的特征组合,计算加入特征i前后预测值的变化,然后按不同组合的大小加权平均。为什么加权重?因为不同大小的子集出现的概率不一样,Shapley值要保证“公平”——每个玩家在每一轮加入时被考虑的机会均等。
我习惯用一个表格来直观对比不同子集大小的贡献:
| 子集大小(|S|) | 包含特征i的预测值 | 不包含特征i的预测值 | 边际贡献 | 权重 |
|---|---|---|---|---|
| 0 | v({i}) | v(∅) | Δ₁ | 1/|N| |
| 1 | v({i, j}) | v({j}) | Δ₂ | 1/(|N|·(|N|-1)) |
| 2 | v({i, j, k}) | v({j, k}) | Δ₃ | … |
| … | … | … | … | … |
经验提醒:实际工程中我们不会手动算这个公式——指数级复杂度不可行。而是用KernelSHAP或TreeSHAP近似。但理解原理能帮你判断近似方法的可靠性。
3. 加性特征归因——SHAP为什么好用?
SHAP把Shapley值直接嫁接到模型解释上,提出了一个“加性特征归因”框架。简单说,模型对某个样本的预测值,可以分解为:
预测值 = base_value + Σ shap_values_i
其中base_value是所有特征取平均时模型的预测(相当于“基准分”),每个shap_values_i就是特征i对该样本预测的贡献。这个分解形式跟线性回归一模一样,但SHAP能处理任何黑箱模型。
为什么强调“加性”?因为一旦分解成这个形式,你就可以直接相加减。方便做局部解释、全局特征重要性排序,还能做交互作用分析。我在做ADMET毒性预测时,用SHAP一眼看出某个分子片段(比如硝基)对肝毒性贡献特别大,而另一个片段(比如羧基)贡献为负——这直接指导了后续的分子优化。
你想想看,如果一个模型预测某个化合物毒性高,你不光知道结果,还知道是哪个结构片段导致的——这对药物化学家来说,简直是指路明灯。
4. 实战:用SHAP解释一个简单模型
光说不练假把式。下面是一个完整的Python示例,用SHAP解释一个随机森林回归模型(以波士顿房价数据为例,注意该数据已弃用,这里仅作演示):
import shap
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import load_boston
# 加载数据
data = load_boston()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = data.target
# 训练模型
model = RandomForestRegressor(n_estimators=100, random_state=42)
model.fit(X, y)
# 创建SHAP解释器(TreeSHAP算法)
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X)
# 显示单个样本的解释
shap.initjs()
shap.force_plot(explainer.expected_value, shap_values[0,:], X.iloc[0,:])
运行这段代码,你会看到一个力场图(force plot):底部是base_value,两侧是各个特征的推拉效果。红色代表正向推动,蓝色代表负向推动——非常直观。
注意:TreeSHAP假设特征之间相互独立,但实际数据几乎都有相关性。如果相关性很强,SHAP值可能会失真。我在处理高度共线的ADMET描述符时,会先用PCA降维再解释,或者直接改用H2O的SHAP实现,它对相关性更鲁棒一些。
5. 知识图谱:SHAP从博弈论到模型解释的完整路径
为了帮你理清思路,我画了一张流程图,把整个逻辑串起来:
总结本章核心:Shapley值从博弈论出发,为每个特征分配一个公平的贡献值;SHAP将其包装成加性归因框架,让黑箱模型变得可解释。理解这个链条后,你就能自信地使用SHAP解读任何ADMET模型了。