An introduction to explainable AI with Shapley values
- 训练模型
- 检查模型系数
- 使用部分依赖图的更完整的图片
- 从部分相关性图中读取SHAP值
- Shapley值的可加性
- 解释additive regression模型
- 解释non-additive boosted tree模型
- 解释线性逻辑回归模型
- 解释non-additive boosted tree逻辑回归模型
- 处理相关特征
- transformers NLP模型的解释
用到的环境是python3.7(基于上一篇文章的环境),再装了个interpret、transformers、datasets。。。
官方的代码在https://github.com/shap/shap/blob/master/docs/example_notebooks/overviews/An%20introduction%20to%20explainable%20AI%20with%20Shapley%20values.ipynb
训练模型
这是用Shapley值解释机器学习模型的介绍。Shapley值是合作博弈论中一种广泛使用的方法,具有理想的性质。
在使用Shapley值来解释复杂模型之前,了解它们如何适用于简单模型是很有帮助的。最简单的模型类型之一是标准线性回归,因此下面我们在加州住房数据集上训练线性回归模型。该数据集由1990年加利福尼亚州20640栋房屋组成,我们的目标是从8个不同的特征预测房价的中位数的自然对数。
8个特征:
- MedInc - median income in block group
- HouseAge - median house age in block group
- AveRooms - average number of rooms per household
- AveBedrms - average number of bedrooms per household
- Population - block group population
- AveOccup - average number of household members
- Latitude - block group latitude
- Longitude - block group longitude
首先获取california数据集,随机抽取1000条数据,X100就是从这1000条数据中抽取100条数据,模型使用sklearn的LinearRegression()。
import pandas as pd
import shap
import sklearn
# a classic housing price dataset
X,y = shap.datasets.california(n_points=1000)
X100 = shap.utils.sample(X, 100) # 100 instances for use as the background distribution
# a simple linear model
model = sklearn.linear_model.LinearRegression()
model.fit(X, y)
又是上次那个警告,,,忽略。。。
输出:
LinearRegression()
检查模型系数
理解线性模型最常见的方法是检查为每个特征学习的系数。当我们改变每个输入特征时,这些系数告诉我们模型输出的变化程度:
print("Model coefficients:\n")
for i in range(X.shape[1]):
print(X.columns[i], "=", model.coef_[i].round(5))
model.coef_[i]
获取特征的系数,round(5)
保留5位小数
model.intercept_
获取模型截距
y=系数1 * 特征1+系数2 * 特征2+…+截距
输出:
Model coefficients:
MedInc = 0.45769
HouseAge = 0.01153
AveRooms = -0.12529
AveBedrms = 1.04053
Population = 5e-05
AveOccup = -0.29795
Latitude = -0.41204
Longitude = -0.40125
虽然系数可以很好地告诉我们当我们改变输入特征的值时会发生什么,但就其本身而言,它们并不是衡量特征整体重要性的好方法。这是因为每个系数的值取决于输入特征的尺度。例如,如果我们以分钟而不是年来衡量房屋的年龄,那么HouseAge特征的系数将变为0.0115/(3652460)=2.18e-8。很明显,房子建成后的年数并不比分钟数更重要,但它的系数值要大得多。这意味着系数的大小不一定是线性模型中特征重要性的良好衡量标准。
使用部分依赖图的更完整的图片
https://christophm.github.io/interpretable-ml-book/pdp.html
部分依赖图(partial dependence plots,PDP)
显示了目标函数(即我们的机器学习模型)和一组特征之间的依赖关系,并边缘化其他特征的值(也就是补充特征)。 它们是通过将模型应用于一组数据、改变感兴趣特征的值同时保持补充特征的值不变可以分析模型输出来计算特征变量对模型预测结果影响的函数关系:例如近似线性关系、单调关系或者更复杂的关系。
为了理解特征在模型中的重要性,有必要了解更改该特征如何影响模型的输出,以及该特征值的分布。为了将其可视化为线性模型,我们可以构建一个经典的部分依赖图,并将特征值的分布显示为x轴上的直方图:
shap.partial_dependence_plot(
"MedInc", model.predict, X100, ice=False,
model_expected_value=True, feature_expected_value=True
)
输出:
上图中的灰色水平线表示模型应用于加州住房数据集时的预期值。垂直灰线表示收入中位数特征的平均值。请注意,蓝色的部分依赖性图线(当我们将中值收入特征固定为给定值时,它是模型输出的平均值)总是穿过两条灰色期望值线的交叉点。我们可以将该交点视为关于数据分布的部分依赖图的“中心”。当我们接下来转向Shapley值时,这种中心化的影响将变得显而易见。
从部分相关性图中读取SHAP值
https://christophm.github.io/interpretable-ml-book/shapley.html
机器学习模型基于Shapley值
的解释背后的核心思想是使用合作博弈论的公平分配结果来分配模型输出的信用𝑓(𝑥) 在其输入特征中。为了将博弈论与机器学习模型联系起来,既需要将模型的输入特征与游戏中的玩家相匹配,也需要将模型函数与游戏规则相匹配。由于在博弈论中,玩家可以加入或不加入游戏,我们需要一种功能“加入”或“不加入”模型的方法。定义特征“加入”模型意味着什么的最常见方法是,当我们知道该特征的值时,说该特征“加入了模型”,当我们不知道该特征值时,它没有加入模型。评估现有模型𝑓 当只有一个子集𝑆 的特征是模型的一部分,我们使用条件期望值公式来整合其他特征。该构想可以采取两种形式:
E [ f ( X ) ∣ X S = x S ] E[f(X) \mid X_S = x_S] E[f(X)∣XS=xS]
E [ f ( X ) ∣ d o ( X S = x S ) ] E[f(X) \mid do(X_S = x_S)] E[f(X)∣do(XS=xS)]
在第一种形式中,我们知道S中特征的值,因为我们观察到了它们。在第二种形式中,我们知道S中特征的值,因为我们设置了它们。一般来说,第二种形式通常更可取,因为它告诉我们,如果我们干预并改变其输入,模型将如何表现,也因为它更容易计算。在本教程中,我们将完全关注第二个公式。我们还将使用更具体的术语“SHAP值”来指代应用于机器学习模型的条件期望函数的Shapley值。
SHAP值的计算可能非常复杂(通常是NP-hard),但线性模型非常简单,我们可以从部分依赖图中读取SHAP值。当我们在解释一个预测𝑓(𝑥)时,特定特征 𝑖 的SHAP值只是预期模型输出和在部分依赖图上特征值 x i x_i xi之间的差值:
# compute the SHAP values for the linear model
explainer = shap.Explainer(model.predict, X100)
shap_values = explainer(X)
# make a standard partial dependence plot
sample_ind = 20
shap.partial_dependence_plot(
"MedInc", model.predict, X100, model_expected_value=True,
feature_expected_value=True, ice=False,
shap_values=shap_values[sample_ind:sample_ind+1,:]
)
输出:
经典部分依赖图和SHAP值之间的紧密对应意味着,如果我们在整个数据集上绘制特定特征的SHAP值,我们将准确地追踪出该特征的部分依赖图的以平均值为中心的版本:
shap.plots.scatter(shap_values[:,"MedInc"])
Shapley值的可加性
Shapley值的一个基本性质是,它们总是总和为所有玩家都在场时的游戏结果和没有玩家在场时的比赛结果之间的差异。对于机器学习模型,这意味着对于所解释的预测,所有输入特征的SHAP值将总是总和为基线(预期)模型输出和当前模型输出之间的差。最简单的方法是通过瀑布图,从我们对房价的背景预期开始𝐸[𝑓(𝑋)],然后一次添加一个功能,直到达到当前模型输出𝑓(𝑥):
# the waterfall_plot shows how we get from shap_values.base_values to model.predict(X)[sample_ind]
shap.plots.waterfall(shap_values[sample_ind], max_display=14)
解释additive regression模型
线性模型的部分依赖图与SHAP值有如此密切的联系的原因是,模型中的每个特征都是独立于其他特征处理的(效果只是相加在一起)。我们可以在放宽直线的线性要求的同时保持这种相加性质。这导致了众所周知的一类广义可加性模型(GAM)。虽然有很多方法可以训练这些类型的模型(比如将XGBoost模型设置为depth-1),但我们将使用专门为此设计的可解释的解释器。
pip install interpret
又是红凸凸的,习惯就好。。。
# fit a GAM model to the data
import interpret.glassbox
model_ebm = interpret.glassbox.ExplainableBoostingRegressor(interactions=0)
model_ebm.fit(X, y)
# explain the GAM model with SHAP
explainer_ebm = shap.Explainer(model_ebm.predict, X100)
shap_values_ebm = explainer_ebm(X)
# make a standard partial dependence plot with a single SHAP value overlaid
fig,ax = shap.partial_dependence_plot(
"MedInc", model_ebm.predict, X100, model_expected_value=True,
feature_expected_value=True, show=False, ice=False,
shap_values=shap_values_ebm[sample_ind:sample_ind+1,:]
)
输出:
shap.plots.scatter(shap_values_ebm[:,"MedInc"])
输出:
# the waterfall_plot shows how we get from explainer.expected_value to model.predict(X)[sample_ind]
shap.plots.waterfall(shap_values_ebm[sample_ind])
输出:
# the waterfall_plot shows how we get from explainer.expected_value to model.predict(X)[sample_ind]
shap.plots.beeswarm(shap_values_ebm)
输出:
解释non-additive boosted tree模型
# train XGBoost model
import xgboost
model_xgb = xgboost.XGBRegressor(n_estimators=100, max_depth=2).fit(X, y)
# explain the GAM model with SHAP
explainer_xgb = shap.Explainer(model_xgb, X100)
shap_values_xgb = explainer_xgb(X)
# make a standard partial dependence plot with a single SHAP value overlaid
fig,ax = shap.partial_dependence_plot(
"MedInc", model_xgb.predict, X100, model_expected_value=True,
feature_expected_value=True, show=False, ice=False,
shap_values=shap_values_xgb[sample_ind:sample_ind+1,:]
)
输出:
shap.plots.scatter(shap_values_xgb[:,"MedInc"])
输出:
shap.plots.scatter(shap_values_xgb[:,"MedInc"], color=shap_values)
输出:
解释线性逻辑回归模型
# a classic adult census dataset price dataset
X_adult,y_adult = shap.datasets.adult()
# a simple linear logistic model
model_adult = sklearn.linear_model.LogisticRegression(max_iter=10000)
model_adult.fit(X_adult, y_adult)
def model_adult_proba(x):
return model_adult.predict_proba(x)[:,1]
def model_adult_log_odds(x):
p = model_adult.predict_log_proba(x)
return p[:,1] - p[:,0]
这。。。有点烦。。。可能网太烂了。。。
老规矩,翻源码。。。C:\Users\gxx\anaconda3\envs\tf-py37\Lib\site-packages\shap。。。
改成自己保存在本地的路径,保存,重启notebook。。。
注意,解释线性逻辑回归模型的概率在输入中不是线性的。
# make a standard partial dependence plot
sample_ind = 18
fig,ax = shap.partial_dependence_plot(
"Capital Gain", model_adult_proba, X_adult, model_expected_value=True,
feature_expected_value=True, show=False, ice=False
)
输出:
如果我们使用SHAP来解释线性逻辑回归模型的概率,我们会看到强烈的相互作用效应。这是因为线性逻辑回归模型在概率空间中不是可加性的。
# compute the SHAP values for the linear model
background_adult = shap.maskers.Independent(X_adult, max_samples=100)
explainer = shap.Explainer(model_adult_proba, background_adult)
shap_values_adult = explainer(X_adult[:1000])
shap.plots.scatter(shap_values_adult[:,"Age"])
输出:
如果我们解释模型的对数几率输出,我们会看到模型输入和模型输出之间存在完美的线性关系。重要的是要记住你所解释的模型的单位是什么,并且解释不同的模型输出可能会导致对模型行为的不同看法。
# compute the SHAP values for the linear model
explainer_log_odds = shap.Explainer(model_adult_log_odds, background_adult)
shap_values_adult_log_odds = explainer_log_odds(X_adult[:1000])
shap.plots.scatter(shap_values_adult_log_odds[:,"Age"])
输出:
# make a standard partial dependence plot
sample_ind = 18
fig,ax = shap.partial_dependence_plot(
"Age", model_adult_log_odds, X_adult, model_expected_value=True,
feature_expected_value=True, show=False, ice=False
)
输出:
解释non-additive boosted tree逻辑回归模型
# train XGBoost model
model = xgboost.XGBClassifier(n_estimators=100, max_depth=2).fit(X_adult, y_adult*1, eval_metric="logloss")
# compute SHAP values
explainer = shap.Explainer(model, background_adult)
shap_values = explainer(X_adult)
# set a display version of the data to use for plotting (has string values)
shap_values.display_data = shap.datasets.adult(display=True)[0].values
默认情况下,SHAP条形图将取数据集所有实例(行)上每个特征的平均绝对值。
shap.plots.bar(shap_values)
输出:
但是,平均绝对值并不是创建特征重要性全局度量的唯一方法,我们可以使用任何数量的变换。在这里,我们展示了使用最大绝对值如何突出资本收益和资本损失特征,因为它们具有罕见但高幅度的影响。
shap.plots.bar(shap_values.abs.max(0))
输出:
如果我们愿意处理更多的复杂性,我们可以使用蜂群图来总结每个特征的SHAP值的整个分布。
shap.plots.beeswarm(shap_values)
输出:
通过取绝对值并使用纯色,我们在条形图和全蜂群图的复杂性之间取得了折衷。请注意,上面的条形图只是以下蜂群图中所示值的汇总统计数据。
shap.plots.beeswarm(shap_values.abs, color="shap_red")
输出:
shap.plots.heatmap(shap_values[:1000])
输出:
shap.plots.scatter(shap_values[:,"Age"])
输出:
shap.plots.scatter(shap_values[:,"Age"], color=shap_values)
输出:
shap.plots.scatter(shap_values[:,"Age"], color=shap_values[:,"Capital Gain"])
输出:
shap.plots.scatter(shap_values[:,"Relationship"], color=shap_values)
输出:
处理相关特征
clustering = shap.utils.hclust(X_adult, y_adult)
shap.plots.bar(shap_values, clustering=clustering)
输出:
shap.plots.bar(shap_values, clustering=clustering, clustering_cutoff=0.8)
输出:
shap.plots.bar(shap_values, clustering=clustering, clustering_cutoff=1.8)
输出:
transformers NLP模型的解释
这展示了SHAP如何应用于具有高度结构化输入的复杂模型类型。
conda install transformers
conda install datasets
import transformers
import datasets
import torch
import numpy as np
import scipy as sp
# load a BERT sentiment analysis model
tokenizer = transformers.DistilBertTokenizerFast.from_pretrained("distilbert-base-uncased")
model = transformers.DistilBertForSequenceClassification.from_pretrained(
"distilbert-base-uncased-finetuned-sst-2-english"
).cuda()
# define a prediction function
def f(x):
tv = torch.tensor([tokenizer.encode(v, padding='max_length', max_length=500, truncation=True) for v in x]).cuda()
outputs = model(tv)[0].detach().cpu().numpy()
scores = (np.exp(outputs).T / np.exp(outputs).sum(-1)).T
val = sp.special.logit(scores[:,1]) # use one vs rest logit units
return val
# build an explainer using a token masker
explainer = shap.Explainer(f, tokenizer)
# explain the model's predictions on IMDB reviews
imdb_train = datasets.load_dataset("imdb")["train"]
shap_values = explainer(imdb_train[:10], fixed_context=1, batch_size=2)
bug又来了,,,估计是版本不兼容无法导入pyarrow。。。
当前pyarrow版本8.0.0
尝试安装pyarrow==4.0.1
pip install pyarrow==4.0.1
尝试安装pyarrow==6.0.0
pip install --user pyarrow==6.0.0
又又又有新bug。。。
莫非这需要魔法???
。。。
好的,这部分跳过。。。