机器学习模型的可解释性
本文介绍了SHAP可解释性方法,将其作为理解机器学习“黑箱”模型预测背后原因的一种途径。文中还提供了一个简单的Jupyter笔记本,您可以使用并修改它,从而对这些概念进行实践操作:
https://www.kaggle.com/code/jorgeivnjh/explainability-in-ml-models
https://github.com/JorgeIvanJH/Explainability-in-ML-models
我们将利用这些概念,在未来将它们应用到我们的持续培训管道中:https://community.intersystems.com/post/complementing-iris-mlflow-continuous-training-ct-pipeline
在本笔记本中,我们将探讨黑盒模型可解释性的直观理解。黑盒模型是指复杂程度超出人类直接理解范围的模型,例如神经网络以及梯度提升(如 XGBoost、LightGBM、CatBoost)等集成学习方法。
在开始之前,有必要澄清可解释模型与可解释性模型之间的区别:
可解释模型是指仅通过观察模型本身,我们就能直接理解输入的变化如何影响输出的模型。 线性回归便是如此,其中每个变量都关联一个系数,该系数表明该变量如何影响预测结果。单棵决策树也是如此,通过追踪其分支,我们可以准确理解预测是如何得出的。
相比之下,诸如随机森林和梯度提升(例如 XGBoost、LightGBM)等由多棵决策树组合而成的模型,或是拥有数千乃至数百万个参数的神经网络,其复杂程度已超出此类直接解释的范围。 在这些情况下,我们需要借助可解释性方法,来理解模型是如何利用输入特征生成预测结果的。
为了使此类模型具备可解释性,我们通常会选取一个已训练好的模型,并分析输入特征的变化如何影响其输出。实现这一目标的方法有很多(例如偏依赖图、ICE图、LIME),但其中应用最广泛且具有坚实数学基础的方法之一是SHAP。
SHAP(Shapley Additive exPlanations)基于博弈论,通过计算夏普利值来量化每个特征对模型预测的贡献程度。这些贡献既可以从全局(整个数据集)角度分析,也可以从局部(单个预测)角度分析。
在本笔记本中,我们将使用 SHAP Python 库来探索这些概念。我们将从一个简单且可解释的模型(线性回归)开始,然后过渡到一个更复杂的模型(LightGBM)。在此过程中,我们将介绍一些最常用的图表,用于解释模型的行为。
注意:要在 Kaggle 上运行此笔记本,您必须已登录账户并连接互联网(设置 - 开启互联网)
import numpy as np
import pandas as pd
import sklearn
import shap
import matplotlib.pyplot as plt
import plotly.express as px
import lightgbm as lgb
import optuna # for a quick lightgbm hyperparameter tuning
import os
for dirname, _, filenames in os.walk('/kaggle/input'):
for filename in filenames:
print(os.path.join(dirname, filename))
在本练习中,我们将加州住房数据集作为真实标签。该数据集包含与房价相关的以下变量:
- MedInc(浮点数):街区收入中位数
- HouseAge(浮点数):街区内的房屋年龄中位数
- AveRooms(浮点数):住宅的平均房间数
- AveBedrms(浮点数):住宅的平均卧室数
- Population (float):街区人口
- AveOccup (float):房屋平均入住率
- 纬度 (float):住宅街区的纬度
- 经度 (float):住宅街区经度
要预测目标变量:
- MedHouseVal(浮点数):房屋价值中位数,以10万美元为单位(例如,数值4.526代表452,600美元)
X, y = shap.datasets.california(n_points=1000) # The dataset
X, X_valid, y, y_valid = sklearn.model_selection.train_test_split(X, y, test_size=0.1, random_state=42)
线性回归(可解释模型)
线性回归是一种简单而强大的模型,且特别易于解释。这是因为,在模型拟合完成后,每个变量都会与一个参数(系数)相关联,该参数会与该变量相乘,从而将其值转换为目标变量的单位。 所有这些转换后变量的总和,加上一个偏移量(截距),即得出了最终的预测结果。
例如,通过分析一个已拟合的房价预测模型的参数,我们可以了解数据集中每个变量如何影响房价:
HousePrice = MedInc * (0.4) + AveBedrms * (5000) + Latitude * (-0.5) + 50000
- 该偏移量表明,当所有变量均为零时,房屋的基准价格为 50,000。
- 该街区的中位收入(MedInc)会以 0.4 的倍率推高房价(收入每增加 1 美元,预测房价就增加 0.4 美元),而每增加一间卧室,房价则增加 5,000 美元。
- 纬度系数为负,表明随着向北移动(纬度增加),房价会下降。由于纬度向北增加,该负系数意味着对价格产生下行影响。
这种初步解读很有用;然而,存在一个重要的局限性:如果没有上下文,参数的量级可能会产生误导。如果变量未经过标准化处理,数值量级较大的变量往往会具有较小的系数,反之亦然。
例如,如果房屋年龄中位数(“HouseAge”)以秒为单位而非年为单位,其系数会小得多,仅仅是因为输入值要大得多。这可能会给人一种误导性的印象,即“HouseAge”比其他变量不重要,而实际上这种差异仅仅是由于计量单位的不同所致。 相比之下,像“平均卧室数”(AveBedrms)这样的变量,其系数可能较大,仅仅是因为其数值范围较小。
现在来看我们数据集上的实际模型:
model = sklearn.linear_model.LinearRegression()
model.fit(X, y)
print("Model coefficients:\n")
for i in range(X.shape[1]):
print(X.columns[i], "=", model.coef_[i].round(5))
print("Intercept = ", model.intercept_.round(5))
print("\nModel performance metrics:\n")
r2 = sklearn.metrics.r2_score(y_valid, model.predict(X_valid))
mae = sklearn.metrics.mean_absolute_error(y_valid, model.predict(X_valid))
print("r2 score: ",r2)
print("mae: ",mae)
Model coefficients:
MedInc = 0.41174
HouseAge = 0.00932
AveRooms = -0.109
AveBedrms = 0.63208
Population = 4e-05
AveOccup = -0.25315
Latitude = -0.46534
Longitude = -0.46173
Intercept = -37.84145
Model performance metrics:
r2 score: 0.6822806366957364
mae: 0.5473856962442348
我们进行的分析效果良好,但它仅提供了各变量在所有样本中如何影响输出的静态、全局性理解。它未能捕捉变量之间的交互作用,且主要局限于线性模型。 如果我们使用更复杂的模型(例如神经网络或基于树的模型),这种解释方式将不足以理解所学习到的关系。
此时,SHAP 便派上了用场。SHAP(夏普利值)是一种基于博弈论的方法,它能帮助我们理解每个特征对模型预测的边际贡献。 我们在此省略其背后的理论,但直观而言,它告诉我们每个特征如何使预测结果偏离模型的平均预测值。为了便于理解,我们可以将其与线性回归进行比较。在线性回归中,截距充当基准线,每个特征与其系数相乘后,会使预测结果偏离该基准线。 在 SHAP 中,基线是模型在整个数据集上的平均预测值(即期望值),而每个特征的夏普利值则代表该特征在将给定样本的预测值从该基线偏移至最终预测值的过程中所作出的贡献。
与仅提供单一全局解释的线性回归系数不同,SHAP 允许我们计算针对每个样本的解释,展示每个特征如何影响单个预测结果,而不仅仅是数据集的平均情况。
下面,我们从数据中提取一个子样本作为比较的参考数据集(虽然也可以使用完整数据集,但这会消耗大量计算资源)。 然后,我们为已训练的线性回归模型创建一个解释器对象,计算 SHAP 值(代表每个特征对每个预测的贡献),并选择一个特定样本(样本 #20)来分析模型所捕捉的关系。
注:要了解SHAP算法的原理,请参阅:https://christophm.github.io/interpretable-ml-book/shapley.html
X100 = shap.utils.sample(X, 100) # Subsample to use as background dataset (SHAP needs one for its internal algorithm)
explainer = shap.Explainer(model.predict, X100)
shap_values = explainer(X)
sample_ind = 20
ExactExplainer explainer: 901it [00:12, 30.31it/s]
计算出 SHAP 值后,我们可以绘制不同的图表,从而更好地理解模型预测背后的原因。
依赖性图(+ ICE 线)
依赖性图有助于我们理解随着特征值的变化,模型输出如何变化,以及该特征的不同值在数据中出现的频率。更具体地说,它展示了模型的预测值在特征取值范围内如何演变,同时也能让我们直观地了解这些值的普遍程度。 这不仅有助于我们观察特征的影响,还能了解这种影响在实际中的相关性。因此,那些影响显著但出现频率较低的值,其整体重要性可能不如那些影响较弱但出现频率较高的值。
在下图中,我们将依赖图与个体条件期望(ICE)线叠加显示,每条线对应一个样本,展示当某个特征值变化时该样本的预测值如何变化。我们展示了变量“纬度”的行为:
- 特征“纬度”的平均值(灰色垂直虚线)约为 36
- 房屋价格的模型平均预测值(灰色水平虚线),约为 2($200.000)。
- 粗蓝线代表随着“纬度”在整个取值范围内变化时模型预测的平均值(即偏依赖,也就是整体趋势)
- 较浅的蓝色线代表在改变纬度时模型对单个样本的预测(ICE曲线)。每条线对应一个样本,其中仅改变纬度,其余特征保持不变
- 红色垂直线段标记了所选样本(sample_ind)。它显示了该特定样本的预测值相对于基线(期望值)的偏移情况,突显了纬度对该实例的影响
所有蓝色曲线均为线性,因为底层模型是线性回归。我们可以清楚地看到一种负相关关系:随着纬度增加(向北移动),所有样本的预测房价均呈下降趋势。
ICE 线的分散程度并不表示特征的重要性,而是反映其效果在各样本间的一致性。如果线条紧密聚集,说明该特征在整个数据集中的效果相似;如果线条分散,则表明该特征与其他变量存在交互作用,其效果取决于具体样本。
shap.partial_dependence_plot(
"Latitude",
model.predict,
X100,
ice=True, # Change to false to see only general trend
model_expected_value=True,
feature_expected_value=True,
shap_values=shap_values[sample_ind : sample_ind + 1, :],
)

散点图
另一种可视化变量如何影响模型输出的方法是使用散点图。在此图中,我们将特征值置于x轴,相应的SHAP值置于y轴,从而展示特征的变化如何影响预测结果。
通过将 shap_values 传递给“color”参数,SHAP 会自动选择与所选特征的 SHAP 值相关性最强(或交互作用最强)的特征,并用其为数据点着色。
在本例中,我们分析“纬度”(Latitude),SHAP 识别出“经度”(Longitude)是与其关联最密切的特征,并通过颜色标尺将其呈现出来。(请注意,这与上文 ICE 图中因与另一变量交互而导致的线段分布范围较宽的情况是一致的)
在此图中,我们观察到:
- 纬度较低(位于更南)的点往往具有较高的经度值(红色),这意味着它们也位于更东侧
- 纬度较高的点(位于更北处)往往具有较低的经度值(蓝色),这意味着它们也位于更西侧
这种模式与加利福尼亚州两大主要人口中心——旧金山(西北部)和洛杉矶(东南部)——周边的房屋密度相吻合。
SHAP 值呈现出明显的负线性趋势(这与线性模型的预期一致),表明随着纬度的增加,其对预测的贡献度会降低。
这意味着:
- 东南部的房屋(低纬度、高经度)往往对预测价格有正向贡献
- 西北地区(高纬度、低经度)的房屋对预测价格的贡献往往为负
换言之,在此数据集中,模型将东南部位置与更高的预测价格相关联,而将西北部位置与更低的预测价格相关联。
注:可能的情况是,洛杉矶的房屋比旧金山的更贵,而这两座城市的地理位置正是导致我们观察到的这种模式的原因。然而,本分析纯属观察性研究,我们在此并未进行任何假设检验或因果推断。
shap.plots.scatter(shap_values[:, "Latitude"], color=shap_values)

瀑布图
该图以单个样本为单位解释了模型的预测结果,展示了每个变量如何影响单个观测值的最终输出结果,而非该变量在整个数据集中的表现。
在下图中,我们可以看到每个特征如何推动模型的期望值(所有样本的平均预测值,E[f(X)])向特定样本(sample_ind)的最终预测值靠拢。
从预期房价 E[f(X)] 开始,每个变量都会在此基准值上进行加减,直至得出该样本的最终预测值 f(x)。我们可以通过比较该特定样本的模型预测值与图表中 f(x) 处显示的预测值来验证这一点:
model.predict(X.iloc[[sample_ind], :])
array([1.9473549])
从图表中,我们观察到:
- 蓝色显示的变量会使预测值向下偏移
- 红色标注的变量则会使预测值向上偏移
每根柱状图的大小代表该特征对该特定样本预测值的贡献程度。
这些单独的贡献就是我们所说的SHAP值:它们量化了每个特征将预测结果从基线E[f(X)]偏移到最终输出结果的程度。
shap.plots.waterfall(shap_values[sample_ind], max_display=14)

注:比较瀑布图和依赖图,观察“纬度”变量的SHAP值在两者中如何保持一致。
蜂群图
该图展示了所有样本中每个变量的 SHAP 值。每个点代表一个样本,其位置根据其 SHAP 值(对模型输出的影响)确定,而颜色则表示特征的数值(红色 = 高值,蓝色 = 低值)。
这使我们能够同时理解:
- 特征值如何影响预测结果
- 不同影响的普遍程度(区域越密集,表示具有相似贡献的样本越多)
以下是我们加利福尼亚州住房数据集的蜂群图:
shap.plots.beeswarm(shap_values)

通过分析该图,我们可以观察到:
- MedInc 和 AveBedrms: 这两个特征的 SHAP 值均呈现右偏分布,其中少数样本的正向贡献值非常大。特别是,较高数值(红色)对应着较大的正 SHAP 值,这意味着更高的收入水平和更多的卧室数量往往会显著推高预测的房价。 这些高值观测值虽然相对罕见,但对模型的预测具有显著影响。
- AveOccup 和 AveRooms: 这些特征的 SHAP 值大多集中在零附近,表明对于大多数样本而言,它们对模型预测的影响有限。 然而,部分高值离群点(红色点)显示出强烈的负SHAP值,这意味着异常高的居住人数或房间数量会显著降低预测的房价。
- HouseAge 和 Population: 这些特征的 SHAP 值紧密聚集在零附近,表明它们对模型的整体预测影响甚微或几乎没有影响。
- 纬度(南北): 存在一个明显的规律:纬度值越高(红色,即更靠北的地区),SHAP值往往越负,这意味着它们会降低预测的房价。 纬度较低的地区(蓝色,即更靠南的地区)往往具有正的 SHAP 值,这会提高预测值。 这表明,在我们的数据集中,越靠北的房屋价格往往越低,而越靠南的房屋价格往往越高。
- 经度(东–西): 我们观察到两个主要的值群。较低的经度值(蓝色,更西侧的地点)往往具有正的SHAP值,而较高的经度值(红色,更东侧的地点)往往具有负的SHAP值。
这种纬度-经度的规律与加利福尼亚州主要人口中心的地理分布相一致。如果我们将数据绘制在地图上,可以清楚地看到两个密集的聚类:一个位于西北部(旧金山地区),另一个位于东南部(洛杉矶地区)。
使用密度图:
df = X.copy()
df["Price"] = y
meanlat = df.Latitude.mean()
meanlon = df.Longitude.mean()
fig = px.density_map(df, lat='Latitude', lon='Longitude', z="Price", radius=3,
center=dict(lat=meanlat, lon=meanlon),
zoom=4.5,map_style="open-street-map")
fig.show()

LightGBM(可解释模型)
现在我们转向一个更复杂的模型——基于梯度提升的LightGBM,它应该能够检测出数据集中更多隐藏的和非线性的模式。为了快速找到最佳超参数,我们将使用“Optuna”机器学习自动超参数优化框架。
我们可以看到,随着该模型捕捉非线性模式和特征间交互关系的能力增强,性能指标也得到了提升。
optuna.logging.set_verbosity(optuna.logging.WARNING)
best_model = None
best_score = float("-inf")
def objective(trial):
global best_model, best_score
train_data = lgb.Dataset(X, label=y, free_raw_data=True)
valid_data = lgb.Dataset(X_valid, label=y_valid, reference=train_data, free_raw_data=True)
param = {
"objective": "regression",
"metric": "mean_squared_error",
"boosting_type": "gbdt",
"verbosity": -1,
'boosting_type': 'gbdt',
'lambda_l1': trial.suggest_float('lambda_l1', 1e-8, 10.0, log=True),
'lambda_l2': trial.suggest_float('lambda_l2', 1e-8, 10.0, log=True),
'num_leaves': trial.suggest_int('num_leaves', 2, 256),
'feature_fraction': trial.suggest_float('feature_fraction', 0.4, 1.0),
'bagging_fraction': trial.suggest_float('bagging_fraction', 0.4, 1.0),
'bagging_freq': trial.suggest_int('bagging_freq', 1, 7),
'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
}
model = lgb.train(param, train_data, valid_sets=[valid_data],callbacks=[lgb.log_evaluation(0)])
preds = model.predict(X_valid)
r2 = sklearn.metrics.r2_score(y_valid, preds)
if r2 > best_score:
best_score = r2
best_model = model
return r2
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)
model = best_model
print("\nModel performance metrics:\n")
preds = model.predict(X_valid)
print("r2 score:", sklearn.metrics.r2_score(y_valid, preds))
print("mae:", sklearn.metrics.mean_absolute_error(y_valid, preds))
Model performance metrics:
r2 score: 0.8105733216454589
mae: 0.39733995113249015
explainer = shap.Explainer(model.predict, X100)
shap_values = explainer(X)
ExactExplainer explainer: 901it [00:13, 16.52it/s]
依赖图(含 ICE 线)。
通过这个新模型,我们观察到加利福尼亚州的纬度与房价之间存在着更加丰富且复杂的关系。
不同纬度区间之间存在明显的差异:
- 在较低纬度区域(南部地区,如洛杉矶周边),ICE曲线往往位于基准线之上,表明其对预测房价持续产生正向影响。
- 对于较高纬度值(北部地区,旧金山及以北),ICE曲线往往位于基线下方,表明其对预测房价产生了负面影响。
- 在中等纬度范围内,曲线聚集在基线附近,这表明该地区的房屋对预测几乎没有或完全没有影响,房价保持在数据集平均水平附近。
总体而言,与线性模型相比,该图表显示纬度的影响不再是严格的线性关系,而是因地区而异,从而捕捉到了更细微的地理模式。
shap.partial_dependence_plot(
"Latitude",
model.predict,
X100,
ice=True, # Change to false to see only general trend
model_expected_value=True,
feature_expected_value=True,
shap_values=shap_values[sample_ind : sample_ind + 1, :],
)

散点图。
我们再次更清晰地看到了数据中的非线性关系和交互作用。在此散点图中,SHAP识别出经度是与纬度SHAP值相关性最强的特征,并利用该特征对数据点进行着色。
从图中,我们可以观察到:
- 东南方向的点(低纬度、高经度——显示为红色)聚集得更紧密,且对房价有正向影响。
- 西北方向的数据点(高纬度、低经度——显示为蓝色)分布更为分散,对房价的贡献为负。
我们还观察到明显的非线性转变:
在纬度34–35度附近,纬度的影响从以正向为主转变为中性/负向 在纬度38度附近,出现更明显的下降,此后纬度对预测房价产生显著的负面影响
在纬度约38度的附近存在一个小区域,其中某些数据点显示出微弱的正贡献,但总体而言,该范围内的主导效应为负。
shap.plots.scatter(shap_values[:, "Latitude"], color=shap_values)

瀑布图。
在瀑布图中,我们可以看到,尽管大多数变量的影响方向保持一致,但与线性模型相比,其贡献的大小发生了显著变化。
在此情况下,我们观察到,此前在预测中占据主导地位的纬度和经度的影响,现在已更多地分散到其他变量上:
- 纬度仍然具有正向贡献,但其影响明显小于线性模型中的情况
- 经度仍然具有负向贡献,但其影响程度也有所减弱,且已不再是影响第二大的变量
此外,该模型还捕捉到了线性回归无法识别的影响:
- 平均住房入住率(AveOccup)在该样本中现在表现出强烈的负向影响,而在线性模型中这一影响要弱得多
总体而言,该模型将影响分散到更多特征上,反映出输入变量与预测房价之间存在更为复杂的关系。
shap.plots.waterfall(shap_values[sample_ind], max_display=14)

蜂群图。
在此蜂群图中,我们观察到特征影响的总体方向与线性回归模型中的结果大致一致,但其影响分布已发生显著变化。
我们可以看到,LightGBM 将特征值的影响在数据集上分布得更为均匀。与线性回归不同,我们不再观察到少数极端离群值主导预测结果的情况。这反映了线性模型的局限性之一,即其对离群值极为敏感。
与此同时,主要变量的相对重要性保持不变,纬度(Latitude)、中位数收入(MedInc)和经度(Longitude)等特征仍然发挥着主导作用。
然而,我们现在发现,一些在线性模型中此前影响甚微的特征,如今对预测做出了更有意义的贡献。例如 HouseAge、AveBedrms 和 Population 等变量,它们的 SHAP 值分布范围现在更为广泛。
进一步分析:
- 以Population为例,较低的值现在既可能产生正向贡献,也可能产生负向贡献,这表明其影响取决于具体情境(即与其他特征的交互作用)
- “AveRooms”和“Longitude”也呈现出类似的模式,根据样本的不同,其高值和低值都会产生不同的影响
这突显了其与线性回归的关键区别:模型不再为每个特征赋予单一的固定效应,而是捕捉变量之间的非线性关系和交互作用。
shap.plots.beeswarm(shap_values)

结语
请务必注意,不要从因果关系的角度来解读这些图表(即不要据此得出过于绝对的结论)。 我们所看到的图表展示了训练好的模型如何响应不同的输入,但它们既不能真实地反映现实,也无法直接解释模型内部究竟发生了什么。相反,它们提供了一种外部近似,帮助我们理解那些原本过于复杂而难以解读的行为。
本笔记本的内容主要参考了 SHAP 文档: https://shap.readthedocs.io/en/latest/example_notebooks/overviews/An%20introduction%20to%20explainable%20AI%20with%20Shapley%20values.html
如需更深入地了解可解释性背后的理论,请参阅: https://christophm.github.io/interpretable-ml-book/
欢迎根据需要修改上文图表中使用的代码,以更详细地分析其他变量,并得出您自己的结论。