如何将DeepSHAP应用于神经网络
深层神经网络(DNN)的模型可解译性一直是使用案例的一个限制因素,需要对模型中所涉及的特征进行解释,许多行业如金融服务就是这种情况。金融机构无论是通过监管还是通过选择,都更喜欢人类容易理解的结构模型,这就是为什么这些行业内的深度学习模式的采用速度缓慢的原因。关键用例的一个例子是风险模型,其中银行通常更喜欢经典统计方法,如广义线性模型,贝叶斯模型和传统的机器学习模型,如基于树的模型,可以根据人类直觉轻松解释。
深度学习中的模型可解释性一直是一个重要的研究领域,因为深度学习模型可以实现高精度,但代价是高抽象(即准确性与可解释性问题)。这也是因为Trust非常重要,因为不受信任的模型是不会被使用的模型。
为了理解这个问题,想象一下简单的多层DNN,这些层试图使用多个特征变量预测项目价格,并自问:
- 在解释结果方面,每个连接的权重意味着什么?
- 哪个权重集在预测中起着最重要的作用?
- 知道权重的大小是否会告诉我有关输入变量重要性的信息吗?
首先,神经网络中的权重是每个神经元之间每个连接的强度的度量。因此,查看DNN中的第一个dense层,可以看出第一层神经元与输入之间的连接有多强。其次,在第一层之后,你失去了一对多的关系,变成了一个多对多的了。这意味着一层中的神经元可能与更远的一些其他神经元方式相关(即神经元由于反向传播而经历非局部效应)。最后,权重确实告诉了有关输入的故事,但是在应用激活函数(即非线性)之后,它们在神经元中压缩的信息使得它很难解码。难怪为什么深度学习模型被称为黑盒子。
用于解释神经网络的方法通常属于两大类(1)显著性方法,(2)特征归因(FA)。
显著性方法善于可视化网络内部的内容并回答诸如(1)在给定输入的情况下激活哪些权重的问题?或者(2)特定卷积层检测图像的哪些区域?。在我们的例子中,这不是我们所追求的,因为它确实没有告诉我们在描述我们的最终预测时哪个特征“最佳”。
FA方法是一种尝试将结构模型拟合到数据子集上的方法,以便找出每个变量在输出变量中具有的解释力。S. Lundberg, S. Lee,在2017年NIPS论文(见论文)中表示,所有模型如LIME,DeepLIFT和Layer-Wise Relevance Propagation都是一系列名为Additive Feature Attribution(AFA)方法的方法的一部分。
附加特征归因方法 有一个解释模型,它是二元变量的线性函数:
其中z'是{0,1} ^ M的子集,其中M是简化特征的数量,Phi_i是权重(即贡献或效果)。
LIME,DeepLIFT和Layer-Wise Relevance Propagation都试图将目标函数最小化为近似g(z')。所以它们都是AFA系列方法的一部分。
在同一篇论文中,作者提出了一种基于名为Shapley值的博弈论来估计该线性模型的贡献(Phi_i)的方法。为什么将博弈论引入呢?
有一个游戏理论的分支,研究协作游戏,其目标是预测所有共同努力实现共同结果的所有参与者中最公平的财富分配(即支付)。这恰好是作者提出的建议:使用Shapley值作为特征变量对神经网络模型输出预测贡献的度量。
作者表明,Shapley值是特定类型协作游戏的最佳解决方案,AFA方法也是其中的一部分(参见文章http://papers.nips.cc/paper/7062-a-unified-approach-to-interpreting-model-predictions.pdf)。最后,他们提出了SHAP(Shapley Additive Explanation)值作为特征重要性的统一度量,以及一些有效生成它们的方法,例如:
- Kernel SHAP(线性LIME + Shapley值)
- Deep SHAP(DeepLift + Shapley值)
- Tree SHAP(Tree explainer + Shapley值)
在本文中,我将向您展示如何将DeepSHAP应用于神经网络,但在我们开始之前,您必须安装以下python库:
- shap:https://github.com/slundberg/shap(> pip install shap)
- seaborn
- pandas
- keras + tensorflow
- Jupyter notebook是必需的(因为shap使用了一些有用的javascript plots)
DeepSHAP示例:
我们将使用seaborn附带的diamonds数据集。这是一个回归问题,我们正试图根据钻石质量测量来估算钻石的价格。
import pandas as pd import seaborn as sns import keras import shap #let's load the diamonds dataset df=sns.load_dataset(name='diamonds') print(df.head()) print(df.describe())
我们的数据看起来像
让我们获取目标和特征变量并对分类变量进行编码:
# get target and feature variables X = df.drop('price',axis=1) Y = df.price #set them as categorical X[['cut','color','clarity']] = X[['cut','color','clarity']].astype('category') # get the maps to the encoding cut_cat_map = [x for x in zip(X.cut.cat.categories,X.cut.cat.codes.unique() )] color_cat_map = [x for x in zip(X.color.cat.categories,X.cut.cat.codes.unique() )] clarity_cat_map = [x for x in zip(X.clarity.cat.categories,X.cut.cat.codes.unique() )] # replace categorical classes to their encoding X[['cut','color','clarity']] = X[['cut','color','clarity']].apply(lambda c : c.cat.codes) print('cut_cat_map:' , cut_cat_map) print('color_cat_map: ',color_cat_map) print('clarity_cat_map: ', clarity_cat_map)
这为我们提供了以后使用的映射
现在让我们来看看我们的训练和测试设置:
# separate train/test set import sklearn X_train, X_test, Y_train, Y_test = model_selection.train_test_split(X, Y, test_size = 0.33, random_state = 5) print('Y_train: ', Y_train.shape) print('Y_test: ', Y_test.shape)
现在让我们使用一个简单的前馈全连接网络(with early stopping)来建立一个神经网络模型
from keras.layers import Input, Dense, BatchNormalization from keras.models import Model from keras.callbacks import EarlyStopping # set input layer inputs = Input(shape=(X_train.shape[1],), name='input') # normalized the batches x = BatchNormalization(name='input_bn')(inputs) # add the fully connected layers x = Dense(X_train.shape[1], activation='relu', name='first')(x) x = Dense(64, activation='relu',name='second')(x) x = Dense(X_train.shape[1], activation='elu',name='last')(x) # get the final result predictions = Dense(1, activation='relu', name='ouput')(x) # This creates a model that includes # the Input layer and three Dense layers model = Model(inputs=inputs, outputs=predictions) model.compile(optimizer='adam', loss='mape') model.fit(X_train, Y_train, epochs = 100, batch_size = 1000, validation_data = (X_test,Y_test), shuffle=True, callbacks=[EarlyStopping(monitor='val_loss', patience=1,)]) #let's get the training and validation histories for plotting val_loss = model.history.history['val_loss'] loss = model.history.history['loss'] print(model.summary()) # let's plot the performance curve import matplotlib.pyplot as plt plt.figure() plt.plot(val_loss, label='val_loss') plt.plot(loss, label = 'loss') plt.legend() plt.show()
它运行得非常快,如果你没有GPU只是根据你的内存限制增加你的batch size。所以,15秒后我们得到:
最终的mape损失约为15%,这足以满足我们的目的。凸起可能是大批量。
DeepSHAP:
使用DeepSHAP非常简单,这归功于shap python库。设置如下:
import shap #initialize js methods for visualization shap.initjs() # create an instance of the DeepSHAP which is called DeepExplainer explainer_shap = shap.DeepExplainer(model=model, data=X_train) # Fit the explainer on a subset of the data (you can try all but then gets slower) shap_values = explainer_shap.shap_values(X=X_train.values[:500], ranked_outputs=True)
方法shap_values(X)使用shapley采样对基于深度提升的SHAP算法进行拟合。选择的数据越多,需要的时间越长,所以要小心。
现在,让我们看看模型推断出的一些个别属性:
# now let's inspect some individual explanations inferred by DeepSHAP shap.force_plot(explainer_shap.expected_value, shap_values[0][0], feature_names=X_train.columns) shap.force_plot(explainer_shap.expected_value, shap_values[0][0][1], X_train.values[:500][0], feature_names=X_train.columns,) shap.force_plot(explainer_shap.expected_value, shap_values[0][0][1], X_train.values[:500][0], feature_names=X_train.columns,)
图1.Force plot for one record (item) in the dataset
要了解这些force plots,您需要参考论文作者的论文:
... SHAP(SHapley Additive exPlanation)将每个要素的属性值定义为在调整该特征时预期模型预测的变化。他们解释了如果我们不知道当前输出f(x)的任何特征,如何从base value E [f(z)]得到预测值。此图显示了单个排序。然而,当模型是非线性的或输入特征不是独立的时,特征被添加到期望的顺序很重要,SHAP值来自于对所有可能的排序求平均值。
请注意,这个base value是DeepSHAP计算的预期值(即E [f(z)]),它只是您不知道任何特征时预测的值。输出值(所有特征贡献和base value的总和),是实际模型的预测。然后,SHAP值只是告诉您每个特征为从base value到输出值添加了多少贡献。
对于图1,您可以理解如下:
# to get the output value and base value record = 1 # this is just to pick one record in the dataset base_value = explainer_2.expected_value output= base_value + np.sum(shap_values[0][0][record]) print('base value: ',base_value) print('output value: ',output) #sanity check that the ouput value is equal to the actual prediction print(np.round(output,decimals=1) == np.round(model.predict(X_train.values)[record],decimals=1)) # to get the shape values or each feature shap_df = pd.DataFrame(list(dict(zip(X_train.columns.values,base_value)).items()), columns=['features','shapvals']).sort_values(by='shapvals', ascending=True) print(shap_df)
图2.来自shap_df的SHAP值
这些是从base value到记录的输出值(即数据集中的单个项目)获得的所有单个特征贡献,显示为图1中的force plot。您可以看到存在不同大小的正面和负面贡献。在这种情况下,x,y,z,carat, clarity, cut和depth都有助于增加从base value到模型输出预测的值,而color和table有助于减小从base value到预测的幅度。预测。另请注意,SHAP值的单位与实际目标值(Price)相同。
以下是获取输出值的方法:
图2.如何获取base value和输出值
尝试添加它们,您将看到输出值等于实际模型预测值。这是一种非常强大且富有洞察力的解释方法,可帮助您了解其贡献
现在,如果您想查看每个特征变量的总体贡献,您只需执行以下操作:
# get the ovearall mean contribution of each feature variable shap.summary_plot(shap_values[0], X_train.values[:500], feature_names=X_train.columns)
绘制贡献值的平均值得到了图3,它显示了每个特征变量对整个平均模型输出的平均贡献。
图3.每个特征对总体预测幅度的平均影响
LIME和Submodular Pick(SP)-LIME
- LIME是一篇由 “Why Should I Trust You?”的论文引入的算法:解释 Marco Tulio Ribeiro等人对任何分类器的预测(2016年)。
- LIME是一种聪明的算法,通过使用可解释的模型在个体预测周围执行局部逼近(即本文情况下,它是一个线性模型等),实现任何黑盒分类器或回归器的可解释性。也就是说,它回答了这样一个问题:我是否完全相信个体预测,可以基于它采取行动吗?因此,检查或调试个体预测是一种很好的算法。
- SP-LIME是一种试图回答问题的算法,我信任模型吗?它通过将问题构建为子模块优化问题来实现。也就是说,它选择的一系列实例模型和相应的预测的方式代表整个模型的性能。这些挑选以这样的方式执行,即解释更多不同实例的输入特征具有更高的重要性分数。
LIME的结果:
import lime import lime.lime_tabular from sklearn import linear_model # set up the LIME explainer explainer = lime.lime_tabular.LimeTabularExplainer(X_train, training_labels = Y_train.values, feature_names = X_train.columns, mode = 'regression', discretize_continuous = False) # you need to modify the output since keras outputs a tensor and LIME takes arrays def predict(x): return model.predict(x).flatten() # compute the explainer. Chose Huber for its robustness against outliers exp = explainer.explain_instance(X_train.values[record], predict, num_features=len(X_train.columns), distance_metric='euclidean', num_samples=1000, model_regressor = linear_model.HuberRegressor()) # generate plot for one item exp.show_in_notebook(show_table=True, predict_proba=True, show_predicted_value=True)
图4.Result of LIME for the same individual record as before
我们可以看到,特征x,y,z,carat,clarity, cut,depth对LIME模型都有积极贡献,尽管与SHAP的顺序不同(但它们非常相似)。此外,Table和color对LIME模型有负面贡献,也证实了SHAP值结果。
SP-LIME的结果:
要计算我们必须使用SP-LIME的总体贡献。由于我们必须计算解释矩阵并获得每个特征的均值,因此这有点繁琐。我将sample_size设置为20,因为SP优化算法在ram中占用了很多
from lime import submodular_pick #set up sp lime with 20 samples. The more amount of samples time increases dramatically sp_obj = submodular_pick.SubmodularPick(explainer, X_train.values[:500], predict, sample_size=20, num_features=9, num_exps_desired=5) #get explanation matrix W_matrix = pd.DataFrame([dict(this.as_list()) for this in sp_obj.explanations]) #get overall mean explanation for each feature matrix_mean = W_matrix.mean() plt.figure(figsize=(14,6)) matrix_mean.sort_values(ascending=False).plot.bar()
图5. SP-LIME的结果
请记住,我没有优化SP-LIME的参数。如果我使用更大的样本量并且增加算法运行的实验数量,它很可能将符合SHAP,因为LIME被证明是AFA类型算法并且是近似Shapley值。
结论:
我希望您已经学到了新的东西,并鼓励您在自己的数据集中尝试不同类型的问题。它还适用于分类和不同类型的数据,如图像数据和文本数据。