模型可解释性:SHAP、LIME 与注意力归因

模型给出预测却说不清原因,在风控、医疗、信贷等场景无法落地。本文系统讲解可解释性的全局与局部视角、排列重要性与 SHAP(KernelSHAP/TreeSHAP)、LIME 局部近似、梯度类归因(Integrated Gradients)、注意力归因的陷阱,以及忠实度与稳定性评估和合规落地。

一个信贷模型拒绝了申请,用户有权知道「为什么」。一个医疗模型给出高风险评分,医生需要判断它依据的是真实病理信号还是数据里的伪相关。当模型开始影响真实决策,可解释性(Interpretability) 就从「锦上添花」变成「合规刚需」。

可解释性不是单一技术,而是一族方法,各自回答不同的问题、有不同的假设与陷阱。本文按「问什么 → 用什么 → 怎么信」的顺序,把主流方法讲清楚,并指出它们最常被误用的地方。

先分清问的是什么

可解释性问题至少要区分两个维度:

维度全局(Global)局部(Local)
问题模型整体依赖哪些特征这一条预测为什么是这样
典型方法排列重要性、全局 SHAPLIME、单样本 SHAP
使用者建模者、审计者终端用户、客服

另一条正交维度是模型无关(Model-agnostic)与模型特定(Model-specific):

  • 模型无关:把模型当黑盒,只用「输入 → 输出」采样来推断。通用但慢、方差大(LIME、KernelSHAP、排列重要性)。
  • 模型特定:利用模型内部结构,快且精确(TreeSHAP、梯度类方法、注意力权重)。

选型第一问永远是:是给谁看、回答哪个问题。给终端用户解释单条决策,和给数据科学家看全局特征重要性,用的是完全不同的工具。

排列重要性:最朴素的全局视角

排列重要性(Permutation Importance) 的思路直白:把某一列特征随机打乱,看模型性能掉多少。掉得越多,说明模型越依赖它。

from sklearn.inspection import permutation_importance

result = permutation_importance(
    model, X_val, y_val,
    n_repeats=10,            # 重复 10 次取均值,降低随机性
    random_state=42,
    scoring="roc_auc",
)
for name, imp in sorted(zip(X_val.columns, result.importances_mean),
                        key=lambda x: -x[1])[:5]:
    print(f"{name}: {imp:.4f}")

它有三个必须知道的坑:

  • 相关特征会互相「抢功」:两个高度相关的特征,打乱其一,另一个仍能提供信息,于是两者重要性都被低估。对策是用分组排列或条件排列。
  • 在训练集上算会失真:模型在训练集上过拟合,打乱特征后性能下降幅度无法反映真实依赖。必须在留出集上算。
  • 尺度无关但方向不明:只知道「重要」,不知道「正相关还是负相关」——后者要靠 SHAP 或部分依赖图。

SHAP:基于 Shapley 值的加性归因

SHAP 把博弈论里的 Shapley 值搬到特征归因:把一个特征的贡献定义为「它在所有特征子集中带来的边际贡献的平均值」。理论上,它是唯一同时满足局部准确性、缺失性、一致性三条公理的归因方法。

直观上,SHAP 把一个预测拆成「基准值 + 各特征贡献之和」:

f(x) = base_value + φ₁ + φ₂ + ... + φₙ

每个 φᵢ 就是特征 i 的 SHAP 值,正负表示该特征把预测推高还是拉低。

三种常用实现

实现适用模型复杂度特点
KernelSHAP任意(黑盒)高(采样近似)通用但慢,方差较大
TreeSHAP树模型(XGBoost/LightGBM)多项式,很快精确,工业界首选
DeepSHAP神经网络中基于 DeepLIFT,需背景样本

树模型上优先用 TreeSHAP,它是精确解且速度快到可以逐样本解释:

import shap
import xgboost as xgb

model = xgb.XGBClassifier().fit(X_train, y_train)

# TreeSHAP:精确、快
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)

# 全局:特征重要性(按 |SHAP| 均值排序)
shap.summary_plot(shap_values, X_test)

# 局部:解释第 0 条预测
shap.force_plot(explainer.expected_value,
                shap_values[0], X_test.iloc[0])

黑盒模型则用 KernelSHAP,但要控制采样量:

# KernelSHAP:用 100 个背景样本,nsamples 越大越准越慢
explainer = shap.KernelExplainer(model.predict_proba, X_train.sample(100))
shap_values = explainer.shap_values(X_test.iloc[:10], nsamples=200)

nsamples 是精度与速度的直接旋钮;对特征数多的问题,KernelSHAP 可能慢到不可用,此时应考虑 DeepSHAP 或改用代理模型。

SHAP 的坑

  • 特征相关性:SHAP 假设特征可独立缺失,强相关时归因会被分摊得不稳定。可用条件 SHAP 缓解。
  • 背景数据敏感:KernelSHAP/DeepSHAP 的结果依赖背景样本集,换一批背景,数值会变。
  • 不是因果:SHAP 只说明「模型用了这个特征」,不等于「这个特征在现实中导致结果」。把它当因果会犯大错。

LIME:局部线性近似

LIME(Local Interpretable Model-agnostic Explanations) 的做法是:在待解释样本附近扰动生成邻居样本,用模型预测这些邻居,再拟合一个简单的线性模型(或决策树)去近似黑盒在该点附近的行为,线性模型的系数就是解释。

from lime.lime_tabular import LimeTabularExplainer

explainer = LimeTabularExplainer(
    X_train.values,
    feature_names=X_train.columns.tolist(),
    class_names=["拒绝", "通过"],
    mode="classification",
    discretize_continuous=True,
)

exp = explainer.explain_instance(
    X_test.iloc[0].values,
    model.predict_proba,
    num_features=6,          # 只展示贡献最大的 6 个特征
)
exp.show_in_notebook()

文本场景有 LimeTextExplainer,图像场景有 LimeImageExplainer,思路一致:用可解释的简单模型在局部拟合复杂模型。

LIME 的优势是快、模型无关、解释直观;代价是稳定性差——同一份数据多跑几次,解释可能不同,因为扰动采样有随机性。生产中若要用 LIME,务必固定随机种子并对多次结果取共识。

梯度类归因:给神经网络用

对可微模型,可以直接用梯度回答「输入哪个部分影响最大」。

显著性图(Saliency)

最简单:输出对输入的梯度绝对值。梯度大,说明该输入微小变化会显著改变输出。

import torch

x = x.requires_grad_(True)
out = model(x)
out[0, target_class].backward()
saliency = x.grad.abs().squeeze().cpu().numpy()

集成梯度(Integrated Gradients)

显著性图对噪声敏感。集成梯度沿「基线 → 输入」的直线路径积分梯度,满足敏感性与完备性(归因之和等于输出减基线),更稳定:

def integrated_gradients(model, x, baseline, target, steps=50):
    alphas = torch.linspace(0, 1, steps).view(-1, *([1] * (x.dim() - 1)))
    grads = []
    for a in alphas:
        xi = (baseline + a * (x - baseline)).requires_grad_(True)
        out = model(xi)[0, target]
        out.backward()
        grads.append(xi.grad.detach())
    avg_grad = torch.stack(grads).mean(dim=0)
    return (x - baseline) * avg_grad      # 归因图

基线(baseline)的选取很关键:图像常用全黑或全灰,文本常用零嵌入或 <pad>。基线不同,归因结果会变——这是必须在报告里说明的前提。

注意力归因与它的陷阱

Transformer 的注意力权重看起来像「模型在看哪里」,于是很多人直接拿注意力当解释。这是一个常见但危险的误解。

注意力权重反映的是信息路由的相对分配,不直接等于对最终预测的贡献。原因至少有三:

  • 注意力经过多层、多头的非线性组合,单层单头权重与输出之间没有简单对应。
  • 层归一化、残差连接会改变权重的实际影响。
  • 多头的平均会掩盖个别头的作用,而某些头可能对结果几乎无影响。

正确的做法是:把注意力当线索,而不是结论。要用它做归因,需配合注意力展开(Attention Rollout)、梯度加权注意力(如 Grad-CAM 类方法),或直接用集成梯度等有公理保证的方法交叉验证。对文本分类,集成梯度和 SHAP 通常比裸注意力更可信。

部分依赖与累积局部效应

归因回答「哪个特征重要」,但回答不了「特征如何影响预测」——是线性上升、还是先升后降?部分依赖图(Partial Dependence Plot, PDP) 直接画特征取值与预测的关系:固定其他特征,让目标特征扫过一段区间,观察平均预测的变化。

from sklearn.inspection import PartialDependenceDisplay

# 年龄对预测概率的影响
PartialDependenceDisplay.from_estimator(
    model, X_train, features=["age", "income"],
    kind="average", grid_resolution=50,
)

PDP 的致命缺陷是假设特征独立。当特征相关时(如「年龄」与「工作年限」高度相关),PDP 会构造出现实中不存在的样本组合,得到误导性曲线。此时应改用累积局部效应(Accumulated Local Effects, ALE),它只在数据实际存在的邻域内计算局部差分,对相关性更稳健:

from alibi.explainers import ALE
ale = ALE(model.predict_proba, feature_names=X_train.columns.tolist())
exp = ale.explain(X_train.values)

一句话选择:特征独立用 PDP,特征相关用 ALE。这是很多解释报告出错却没人发现的隐蔽点。

代理模型与规则抽取

当需要「一句话说清模型逻辑」时,可以训练一个代理模型(Surrogate Model):用黑盒模型对大量样本的预测作为标签,训练一棵浅决策树,再把它打印成规则。

from sklearn.tree import DecisionTreeClassifier, export_text

# 用黑盒预测当标签,训练浅树
surrogate = DecisionTreeClassifier(max_depth=3, random_state=42)
surrogate.fit(X_train, model.predict(X_train))

print(export_text(surrogate, feature_names=list(X_train.columns)))

输出的规则形如「若 income > 5 且 age < 30 则拒绝」,业务方能直接读懂。代理模型的评估指标是保真度(Fidelity):代理在留出集上与黑盒预测一致的比例。保真度低说明代理没学到黑盒的真实逻辑,规则不可信。

对神经网络,还可以用「规则抽取」把某类样本的共同特征归纳成 IF-THEN 规则,但要警惕规则过度拟合训练分布。

文本与图像的可解释性

不同模态的可解释性方法差异很大。

文本上,归因的对象是 token。常用做法是逐 token 遮挡(Occlusion):依次把每个词替换为 <unk> 或删除,看预测概率下降多少,下降最多的词就是关键。也可用集成梯度直接算 token 嵌入的梯度。

def token_occlusion(model, text, tokenizer):
    tokens = tokenizer.tokenize(text)
    base = model(text).prob
    scores = {}
    for i in range(len(tokens)):
        masked = tokens[:i] + ["[MASK]"] + tokens[i+1:]
        scores[tokens[i]] = base - model(tokenizer.convert_tokens_to_string(masked)).prob
    return sorted(scores.items(), key=lambda x: -x[1])[:5]

图像上,除了集成梯度与 Grad-CAM,还有超像素遮挡(如 SLIC 分割后逐块遮挡)。Grad-CAM 用目标类梯度加权最后一层特征图,得到一张热力图,直观且计算便宜:

# Grad-CAM:最后一层卷积特征图的加权和
weights = grad.mean(dim=(2, 3), keepdim=True)      # 每个通道的平均梯度
cam = (weights * feature_map).sum(dim=1)           # 加权求和
cam = torch.relu(cam)                               # 只保留正向贡献

图像解释的陷阱是热力图好看但可能不忠实:高亮区域有时是模型关注的相关背景,而非真正的判别依据。仍要用删除/保留实验验证。

如何判断一个解释靠不靠谱

解释本身也需要被评估。两个核心指标:

忠实度(Faithfulness)

解释是否真实反映了模型的决策依据。常用检验是删除/保留实验:

  • 删除最重要特征:若模型预测显著改变,说明解释忠实。
  • 只保留最重要特征:若仍能得到相近预测,说明解释忠实。
def faithfulness_delete(model, x, attributions, k=5):
    topk = attributions.abs().argsort(descending=True)[:k]
    x_masked = x.clone()
    x_masked[topk] = 0                  # 抹掉最重要的 k 个特征
    p_before = model(x)[0].item()
    p_after = model(x_masked)[0].item()
    return abs(p_before - p_after)      # 变化越大越忠实

稳定性(Stability)

相似输入应得到相似解释。对样本加微小噪声,重复计算归因,看结果的一致性。LIME 常在此项失分,SHAP(尤其 TreeSHAP)表现更好。

一个实用经验:任何用于对外解释的方法,上线前都要做忠实度与稳定性测试,否则可能给出「看起来合理但完全错误」的解释,比不给解释更糟。

大模型时代的可解释性

对 LLM,经典的特征归因方法不再适用——输入是 token 序列,输出是序列,且模型有上百亿参数。业界转向机制可解释性(Mechanistic Interpretability),试图理解内部计算:

  • 探针(Probing):在某一层激活上训练一个简单分类器,检测该层是否编码了某种属性(如「这句话是否是疑问句」)。探针准确率高说明信息在该层可线性解码。
  • 激活修补(Activation Patching):把某层某个位置的激活替换成另一组输入的激活,看输出如何变化,从而定位「哪一层哪个位置」对某个行为负责。
  • 稀疏自编码器(Sparse Autoencoder, SAE):把叠加(superposition)的激活分解成稀疏、可解释的特征方向,是当前识别「概念神经元」的主流手段。
  • 思维链(CoT):让模型显式输出推理步骤。它提供的是过程可见性,但需注意 CoT 未必忠实反映真实计算——模型可能「编」一段看似合理但与其实际决策无关的理由。

对业务落地,务实的做法是:用 CoT 做用户可读的解释,用机制分析做内部的可靠性审计,两者目的不同,不能互相替代。

一个完整例子:信贷拒绝解释

把上面的方法拼成一条可落地的流水线——对一条被拒的申请,输出结构化的拒绝理由:

import shap

explainer = shap.TreeExplainer(model)

def explain_rejection(x_row, top_k=3):
    sv = explainer.shap_values(x_row)[0]
    pairs = list(zip(x_row.columns, sv))
    # 只看把「拒绝」概率推高的特征(正贡献)
    positives = [(f, v) for f, v in pairs if v > 0]
    positives.sort(key=lambda t: -t[1])

    reasons = []
    for feat, contrib in positives[:top_k]:
        reasons.append({
            "feature": feat,
            "value": float(x_row[feat].iloc[0]),
            "contribution": round(float(contrib), 4),
            "template": REASON_TEMPLATES.get(feat, "{feature} 偏高"),
        })
    return {"decision": "reject", "reasons": reasons}

配合一张「特征 → 业务话术」模板表,就能把数值贡献翻译成用户能读的理由:

特征业务话术
debt_ratio当前负债收入比偏高
recent_inquiries近期征信查询次数较多
delinquency_12m近 12 个月存在逾期记录

关键工程点:限制理由条数(通常 3 条)、用业务语言而非特征名、对每条理由给出可核验的数据来源。这样既满足合规要求,也避免让用户淹没在几十个归因数值里。

工程落地与合规

  • 对齐监管要求:金融、医疗等受监管行业通常要求「对拒绝决策给出主要理由」。实践中常用「SHAP Top-3 特征 + 业务语言模板」组合成用户可读的解释。
  • 解释即产品:把解释封装成接口,返回结构化的 [{feature, contribution, direction}],前端再渲染成文案,避免把原始数值直接丢给用户。
  • 性能与批处理:TreeSHAP 可批量计算;KernelSHAP 需异步或缓存,避免阻塞在线请求。
  • 可解释性与公平性联动:解释能暴露模型是否依赖了敏感属性(性别、地域),是审计偏差的入口,常与 公平性与偏差缓解 的方法配合使用。
  • 别把相关当因果:解释反映的是模型内部逻辑,要回答「特征是否真正导致结果」,需要专门的因果方法。

小结

可解释性方法按「问什么」选:全局看排列重要性与全局 SHAP,局部看 LIME 与单样本 SHAP,神经网络看集成梯度,树模型优先 TreeSHAP。使用时牢记三条边界:SHAP 不是因果、注意力不是解释、LIME 不稳定。任何对外解释上线前都要过忠实度与稳定性测试。可解释性是评估体系的一环,与 模型评估 的指标设计互补;若要进一步追问「特征与结果之间是否存在因果关系」,则需要 因果推断 的工具;更完整的可解释性方法与工具栈可参考 模型可解释性 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ai」更多文章

  1. 排序学习与搜索召回排序系统
  2. 数据版本控制与血缘:DVC 与 LakeFS
  3. 多智能体协作与编排