理解 SHAP 值:如何根据模型性质正确解释 XGBoost 与随机森林的结果
背景
在机器学习的世界里,模型解释性工具的需求日益增加,SHAP作为一种强大的解释方法,已被广泛应用,然而,许多初学者和甚至一些经验丰富的从业者可能会忽略一个关键的细节,shap值的解释需要根据模型性质来进行解释如:不同模型在SHAP力图中显示的 f(x) 和使用的模型相关从而导致含义并不相同,本文将通过一个实际案例,深入剖析这一差异,尤其是当你使用XGBoost与随机森林(RF)分类模型时,SHAP力图中 f(x) 所代表的内容如何发生变化,这一差异不仅影响理解模型的输出,还直接影响对模型预测结果的解释与决策,通过阅读本文,你将能够更好地掌握SHAP力图解释的核心概念,避免在模型分析中的常见误区。
差异对比
二分类XGBoost
这是二分类XGBoost部署的APP输出的shap力图,可以发现f(X)=-2.50很明显这不是模型属于某个类别的概率,具体解读会在接下来根据代码进行解释。
二分类随机森林RF
这里是一篇医学柳叶刀顶刊部署的APP,使用的模型为随机森林RF,它这里的解释为f(X)为该预测类别的概率,和XGBoost的力图输出f(X)存在很大差异,于是我们利用数据分别实现这两个模型来探讨一下到底是因为为什么,出现了这种情况。
代码实现
数据读取处理
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
df = pd.read_csv('Dataset.csv')
# 划分特征和目标变量
X = df.drop(['target'], axis=1)
y = df['target']
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
random_state=42, stratify=df['target'])
df.head()
读取数据,划分出特征和目标变量,然后将数据集按照80%训练集和20%测试集的比例进行分割,同时确保目标变量的类别分布在训练集和测试集中保持一致。
XGBoost模型构建
import xgboost as xgb
from sklearn.model_selection import GridSearchCV
# XGBoost模型参数
params_xgb = {
'learning_rate': 0.02, # 学习率,控制每一步的步长,用于防止过拟合。典型值范围:0.01 - 0.1
'booster': 'gbtree', # 提升方法,这里使用梯度提升树(Gradient Boosting Tree)
'objective': 'binary:logistic', # 损失函数,这里使用逻辑回归,用于二分类任务
'max_leaves': 127, # 每棵树的叶子节点数量,控制模型复杂度。较大值可以提高模型复杂度但可能导致过拟合
'verbosity': 1, # 控制 XGBoost 输出信息的详细程度,0表示无输出,1表示输出进度信息
'seed': 42, # 随机种子,用于重现模型的结果
'nthread': -1, # 并行运算的线程数量,-1表示使用所有可用的CPU核心
'colsample_bytree': 0.6, # 每棵树随机选择的特征比例,用于增加模型的泛化能力
'subsample': 0.7, # 每次迭代时随机选择的样本比例,用于增加模型的泛化能力
'eval_metric': 'logloss' # 评价指标,这里使用对数损失(logloss)
}
# 初始化XGBoost分类模型
model_xgb = xgb.XGBClassifier(**params_xgb)
# 定义参数网格,用于网格搜索
param_grid = {
'n_estimators': [100, 200, 300, 400, 500], # 树的数量
'max_depth': [3, 4, 5, 6, 7], # 树的深度
'learning_rate': [0.01, 0.02, 0.05, 0.1], # 学习率
}
# 使用GridSearchCV进行网格搜索和k折交叉验证
grid_search = GridSearchCV(
estimator=model_xgb,
param_grid=param_grid,
scoring='neg_log_loss', # 评价指标为负对数损失
cv=5, # 5折交叉验证
n_jobs=-1, # 并行计算
verbose=1 # 输出详细进度信息
)
# 训练模型
grid_search.fit(X_train, y_train)
# 输出最优参数
print("Best parameters found: ", grid_search.best_params_)
print("Best Log Loss score: ", -grid_search.best_score_)
# 使用最优参数训练模型
best_model = grid_search.best_estimator_
使用XGBoost分类器通过网格搜索和5折交叉验证来寻找最佳模型参数,并在训练集上进行训练,同时输出最佳参数和对应的最优对数损失分数。
随机森林RF模型构建
from sklearn.ensemble import RandomForestClassifier
# 使用随机森林建模
model_rf = RandomForestClassifier(n_estimators=100, criterion='gini', bootstrap=True, max_depth=3, random_state=8)
model_rf.fit(X_train, y_train
使用随机森林分类器(指定了100棵树、基尼系数作为分裂标准、引导抽样、最大深度为3,以及随机种子8)在训练集上进行模型训练。。
shap力图
XGBoost力图
import shap
explainer = shap.TreeExplainer(best_model)
shap_values = explainer.shap_values(X_test)
print("基准值:",explainer.expected_value)
print("shap值维度:",shap_values.shape)
基准值:-0.17231837是模型在没有任何特征输入时的预测输出。
SHAP值维度:(60, 13)表明测试集中有60个样本,每个样本有13个特征。
sample_index = 0
shap.force_plot(explainer.expected_value, shap_values[sample_index], X_test.iloc[sample_index], matplotlib=True)
XGBoost的力图会显示基准值(通常是模型的平均输出)以及绘制样本各特征的具体数值,还有就是f(X)它是XGBoost模型根据这些特征实际的预测值,但是它并不是模型预测类别的概率值,而是输出一个经过Sigmoid函数处理前的对数几率(log-odds)值。
Log-Odds: 在二分类问题中,XGBoost的输出 f(X) 实际上是一个对数几率(log-odds)值,它表示类别为1的对数几率:
这个值可以是任何实数,可能会大于1,也可能小于0。
随机森林RF力图
explainer = shap.TreeExplainer(model_rf)
shap_values = explainer.shap_values(X_test)
print("基准值:",explainer.expected_value)
print("shap值维度:",shap_values.shape)
基准值 [0.54063291, 0.45936709] 对应于每个类别的基准概率。如果不知道任何特征信息,模型会预测类别 0 的概率为 0.5406,类别 1 的概率为 0.4594。
SHAP 值数组的维度是 (60, 13, 2),60: 测试集 X_test 中的样本数量,13: 数据集中的特征数量,2: 模型中的类别数量(这里是二分类问题)。
可以发现这里随机森林RF模型和XGBoost模型的shap结果输出已经出现不一样了,虽然使用的是同一个shap解释器TreeExplainer。
sample_index = 0
shap.force_plot(explainer.expected_value[0], shap_values[sample_index,:,0], X_test.iloc[sample_index], matplotlib=True)
可视化 X_test 中第 0 个样本的第 0 类别的 SHAP 值,展示各个特征对该样本在第 0 类别上的预测的贡献情况,explainer.expected_value[0]:类别 0 的基准值(即模型在不知道任何特征时对类别 0 的平均预测值),shap_values[sample_index,:,0]:第 0 个样本的所有特征对类别 0 预测的 SHAP 值,X_test.iloc[sample_index]:第 0 个样本的特征值,这里的f(X)=0.91实际上就是随机森林RF模型预测这一个样本为0这一类的概率。
sample_index = 0
shap.force_plot(explainer.expected_value[1], shap_values[sample_index,:,1], X_test.iloc[sample_index], matplotlib=True)
可视化 X_test 中第 0 个样本的第 1 类别的 SHAP 值,展示各个特征对该样本在第 1 类别上的预测贡献情况,explainer.expected_value[1]:类别 1 的基准值(即模型在不知道任何特征时对类别 1 的平均预测值),shap_values[sample_index,:,1]:第 0 个样本的所有特征对类别 1 预测的 SHAP 值,X_test.iloc[sample_index]:第 0 个样本的特征值,这里的f(X)=0.09实际上就是随机森林RF模型预测这一个样本为1这一类的概率。
最后可以发现对这两个在随机森林RF下绘制的力图f(X)相加为1(如果你进一步研究还会发现同样本不同类别下的shap值和为0),根据这个概率也可以确定这个样本在模型中是91%的概率预测为0类,也就符合顶刊中的解释,但是如果采用XGBoost模型就不会存在这种解释。
总结
差异原因:
模型输出的本质:
- 随机森林模型通常直接输出概率,因此其 SHAP 值表示的是对最终概率的贡献
- XGBoost 默认输出的是一个经过 logit 变换的对数几率(log-odds)值,log-odds 值并不是概率,而是需要通过 sigmoid 函数将其转换为概率,这就导致 SHAP 值在 log-odds 空间中的解释并不是直接反映最终的概率。
SHAP 值的计算方式:
- 在随机森林模型的二分类问题中,SHAP 值可以分别给出每个样本对于两个类别的 SHAP 值,这是因为随机森林模型直接输出两个类别的概率,因此,对于每个样本,SHAP 值会计算每个特征对两个类别概率的贡献,最终形成一个三维数组 (样本数, 特征数, 类别数)
- 在 XGBoost 的二分类问题中,模型输出的是一个 log-odds 值(对数几率),而不是直接输出类别的概率,log-odds 是一个单一的值,用于描述类别 1 的可能性相对于类别 0 的几率,因此每个样本和每个特征只会对应一个 SHAP 值,SHAP 值的维度是 (样本数, 特征数),即一个二维数组。
这个现象说明不同类型的模型在 SHAP 值解释方面的不同之处,对于像随机森林这样直接输出概率的模型,SHAP 值可以直接反映最终的预测概率,因此 f(X) 相加为 1 并且可以直观地解释为概率。
而对于 XGBoost 这种输出 log-odds 值的模型,SHAP 值解释的是对 log-odds 的贡献,而不是直接的概率,因此在这种情况下,你无法通过简单相加 SHAP 值来得到概率,这种解释方式与顶刊中的标准解释方法有所不同。
这提醒我们,在使用 SHAP 解释模型时,需要根据具体模型的性质正确理解和解释 SHAP 值的含义。