所有文章 > AI驱动 > 从基础到进阶:优化SHAP力图,让样本解读更直观

从基础到进阶:优化SHAP力图,让样本解读更直观

背景

机器学习模型的解释过程中,SHAP力图(SHAP Force Plot)被广泛用于展示单样本各个特征对模型预测结果的贡献,然而,标准的SHAP力图有时可能难以直观地传达关键信息,尤其是在特征数量较多或特征值之间存在较大差异的情况下,为了让模型的解释更具可读性和准确性,我们有必要对SHAP力图进行优化。

在本篇文章中,我们将详细探讨如何通过调整SHAP力图的显示阈值、特征标注和视觉呈现等方面来优化图表,使其更易于解读,通过这些优化,我们不仅能够突出最重要的特征,还可以减少信息冗余,从而帮助我们更清晰地理解模型的决策逻辑。这些优化步骤不仅提升了图表的美观性,还增强了它在实际应用中的实用性。

代码实现

模型构建

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = 'SimHei'
plt.rcParams['axes.unicode_minus'] = False
df = pd.read_excel('california.xlsx')

from sklearn.model_selection import train_test_split

X = df.drop(['price'],axis=1)
y = df['price']

X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 然后将训练集进一步划分为训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.125, random_state=42) # 0.125 x 0.8 = 0.1

import lightgbm as lgb

# LightGBM模型参数
params_lgb = {
'learning_rate': 0.02, # 学习率,控制每一步的步长,用于防止过拟合。典型值范围:0.01 - 0.1
'boosting_type': 'gbdt', # 提升方法,这里使用梯度提升树(Gradient Boosting Decision Tree,简称GBDT)
'objective': 'mse', # 损失函数
'metric': 'rmse', # 评估指标
'num_leaves': 127, # 每棵树的叶子节点数量,控制模型复杂度。较大值可以提高模型复杂度但可能导致过拟合
'verbose': -1, # 控制 LightGBM 输出信息的详细程度,-1表示无输出,0表示最少输出,正数表示输出更多信息
'seed': 42, # 随机种子,用于重现模型的结果
'n_jobs': -1, # 并行运算的线程数量,-1表示使用所有可用的CPU核心
'feature_fraction': 0.8, # 每棵树随机选择的特征比例,用于增加模型的泛化能力
'bagging_fraction': 0.9, # 每次迭代时随机选择的样本比例,用于增加模型的泛化能力
'bagging_freq': 4 # 每隔多少次迭代进行一次bagging操作,用于增加模型的泛化能力
}

model_lgb = lgb.LGBMRegressor(**params_lgb)
model_lgb.fit(X_train, y_train, eval_set=[(X_val, y_val)],

使用LightGBM模型对房价进行预测,并通过划分训练集、验证集和测试集的方式来训练模型、验证其性能,以确保模型的泛化能力,接下来也将对这个模型进行力图的绘制,数据集是一个包含了房价预测相关的特征信息数据,如中位收入、房龄、平均房间数、平均卧室数、人口数、平均家庭人口数、纬度和经度,以及对应的房价。

基础力图绘制

import shap
# 构建 shap解释器
explainer = shap.TreeExplainer(model_lgb)
# 计算测试集的shap值
shap_values = explainer.shap_values(X_test)

# 绘制单个样本的SHAP解释(Force Plot)
sample_index = 7 # 选择一个样本索引进行解释
shap.force_plot(explainer.expected_value, shap_values[sample_index], X_test.iloc[sample_index], matplotlib=True)
plt.savefig("SHAP力图_1.pdf", format='pdf', bbox_inches='tight')

这是一个基础的SHAP力图,目前在许多文章中采用了类似的展示形式,然而,这种样式仍有许多值得我们深入探讨的地方,例如,当特征数量过多时,如果我们只想展示部分关键特征,该如何实现?又如,特征带有度量单位时,如何在力图上清晰地展示?这些都是可以改进的方向,作者在一篇文献中发现了类似的SHAP力图如下:

接下来我们通过一步步解读参数,根据原理去绘制类似的力图。

explainer.expected_value

explainer.expected_value 是SHAP解释器中的一个关键参数,它代表了模型在没有任何特征输入时的预测值,即模型输出的基准值或平均值,在回归问题中,这通常是模型在整个训练数据集上的平均预测值,这个值作为SHAP力图中的基础线,所有特征的SHAP值都相对于这个基准值进行调整,从而显示出每个特征对最终预测的贡献。

shap_values[sample_index]

shap_values[sample_index] 表示SHAP解释器对第 sample_index 个样本计算出的各个特征的SHAP值。

X_test.iloc[sample_index]

X_test.iloc[sample_index] 返回的是测试集 X_test 中第 sample_index 个样本的特征值也就是前文shap对应的样本,可以发现它本身是不带度量单位的,接下来根据这个原理去绘制带度量单位的shap力图。

带度量单位的shap力图

base_value = explainer.expected_value  # 基础值,一般是模型在训练集上的平均输出
shap_values = shap_values[sample_index] # 每个特征的SHAP值
features = np.array([
'MedInc=-0.096140 ($1000)', # 中位收入,单位为千美元
'HouseAge=0.749907 (years)', # 房龄,单位为年
'AveRooms=-0.195315 (rooms/house)', # 每个家庭的平均房间数
'AveBedrms=-0.111900 (bedrooms/house)', # 每个家庭的平均卧室数
'Population=-0.367422 (people)', # 人口数
'AveOccup=0.053259 (people/house)', # 每个家庭的平均人口数
'Latitude=-0.807184 (degrees)', # 纬度,单位为度
'Longitude=0.749004 (degrees)'])# 经度,单位为度 特征名称和数值
# 创建力图
shap.force_plot(base_value, shap_values, features,
matplotlib=True, # 使用Matplotlib来显示图像
show=False)

plt.savefig("SHAP力图_2.pdf", format='pdf', bbox_inches='tight')

根据特定样本的特征值和SHAP值生成并保存一个解释模型预测的力图,可以发现该力图对于每个特征都存在一个其对应的单位,但是存在折叠,或许在实际应用中我们只希望展示部分特征就可以了,而不是每个特征都进行展示,只需要添加参数调整阈值就好了。

特征展示控制

# 创建力图
shap.force_plot(base_value, shap_values, features,
matplotlib=True, # 使用Matplotlib来显示图像
show=False,
contribution_threshold=0.1) # 调整阈值以控制显示哪些特征

plt.savefig("SHAP力图_3.pdf", format='pdf', bbox_inches='tight')

通过添加 contribution_threshold=0.1 参数,我们可以调整SHAP力图中的显示阈值,对比两幅力图后可以发现,特征 HouseAge 已不在当前的力图中显示,因为它对预测结果的贡献较小。这种调整并未改变图表的整体信息表达,同时也使图表更加简洁,读者可以根据具体需求自行调整阈值,以更好地控制特征的展示效果。

文章转自微信公众号@Python机器学习AI