ML 模型解释工具:模型解释是什么、为什么需要模型解释以及如何解释模型

443 阅读19分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第16天,点击查看活动详情

解释的字面意思是解释或展示你自己对某事的理解。

当您创建一个 ML 模型时,它只不过是一种可以学习模式的算法,对于其他项目利益相关者来说(有时甚至对你),它可能感觉就像一个黑盒子。

这就是我们拥有模型解释工具的原因。

什么是模型解释?

一般来说,ML 模型必须获得预测,并使用这些预测和最终的见解来解决一系列问题。我们已经可以提出几个后续问题:

  • 这些预测的可信度如何?
  • 他们是否足够可靠以做出重大决定?

模型解释将您的注意力从“结论是什么?”转向“为什么得出这个结论?”。您可以了解模型的决策过程,即究竟是什么驱动模型正确或错误地对数据点进行分类。

为什么模型解释很重要?

考虑一个哈士奇与狼(狗品种)分类器的示例,其中一些哈士奇被错误分类为狼。使用可解释的机器学习,您可能会发现这些错误分类主要是由于图像中的雪,分类器将其用作预测狼的特征。

这是一个简单的示例,但您已经可以看到为什么模型解释很重要。它至少在几个方面对您的模型有帮助:

  • 公平——公司用来决定加薪和晋升的可解释模型可以告诉你为什么任何特定的人得到或没有得到晋升。
  • 可靠——输入的微小变化不会导致多米诺骨牌效应,也不会大幅改变输出。
  • 因果关系——只有因果关系对决策有用。
  • 信任——所有项目利益相关者(尤其是非技术方面的利益相关者)更容易信任可以用外行术语解释的模型。

如何解释 ML 模型?

机器学习模型的复杂程度和性能各不相同。没有放之四海皆准的解决方法。因此,有不同的方式来解释它们。首先,这些方法可以分类为:

特定于模型/与模型无关

  • 特定于模型的方法是特定于某些模型的,它们依赖于模型的内部机制来得出某些结论。这些方法可能包括广义线性模型 (GLM) 中系数权重的解释,或神经网络情况下的权重和偏差的解释。
  • 与模型无关的方法可用于任何模型。它们通常在训练后应用。它们通常通过分析特征输入-输出对之间的关系来工作,并且无法访问模型的内部机制,例如:权重或假设。

局部/全局范围

  • 局部范围仅涵盖单个预测,仅捕获指定预测背后的原因。
  • 全局范围超出了单个数据点,涵盖了模型的一般行为。

下面我们创建一个模型来进行解释。我们将简要介绍模型创建步骤,然后我们将专注于不同的与模型无关的工具和框架来解释创建的模型,而不是解决实际问题。

模型创建

1.加载数据集

image.png

数据集Schema:

image.png

预测标签: image.png

2. 进行探索性数据分析和数据预处理

  1. 填充空值。
  2. 删除时间 (GMT) 等冗余特征。
  3. 通过删除所有字母数字字符来清理文本数据。
  4. 通过标签编码(Label Encoding)分类值属性。
  5. 处理某些属性中存在的错误值。
  6. 基于文本的特征的词形还原和 TF-IDF 向量化。

推荐阅读

3. 特征工程

  • 将所有文本特征组合成单个文本特征以弥补缺失值。
  • 将数据映射到工作日。
  • 将时间映射到小时。
  • 根据对话文本的长度创建另一个特征。

探索工具

4. 最终的训练和测试数据集

可以看出,TF-IDF 向量化以及特征工程导致训练和测试数据集中的属性数量增加

image.png

5. 训练分类器

尽管可以使用一系列模型来完成这项任务,但我们将使用随机森林分类器,因为它很复杂,所以不容易解释。 我们希望使用许多工具和框架来使其可解释。

from sklearn.ensemble import RandomForestClassifier 
rfc = RandomForestClassifier() rfc.fit(train_X,y)

6. 对测试数据集进行预测

testpred = rfc.predict(test_X)

Model interpretation_dataset

7. 模型性能评估

由于我们没有来自测试数据集的正确标签,下面我们看看如何通过训练分类报告和 K 折交叉验证分数看看我们的模型的表现。

训练分类报告:

Model interpretation_training classification

K 折交叉验证分数:

Model interpretation_scores

推荐阅读

模型解释工具

现在我们已经建立了一个模型,现在是时候开始使用解释工具来解释我们模型的预测了。

我们将从最流行的工具之一ELI5开始。

1. ELI5

ELI5 是一个流行的 Python 库,因为它易于使用。 它主要用于:

  • 了解在预测中发挥关键作用的重要特征。
  • 分析某个个体的预测,看看究竟是什么导致了该模型的预测。

ELI5 更多地以局部/全局范围的方式解释模型,而不是我们上面讨论的特定于模型/与模型无关的方式。

目前,ELI5 只能用于少数几个著名的模型——Sklearn 广义线性模型 (GLM) 和基于树的模型、Keras 模型、LightGBM、XGBoost、CatBoost

推荐阅读

查看如何跟踪Sklearn、Keras、LightGBM和XGBoost模型训练元数据

如何安装 Eli5

Eli5 可以使用 pip 命令安装。

pip install eli5

或者,它可以使用 conda 命令安装。

conda install -c conda-forge eli5

在我们的模型上使用 Eli5

我们先导入需要的依赖:

import eli5

A:特征权重和重要性

让我们从一个标准的函数调用开始。

eli5.show_weights(rfc)

image.png

“Weight”列包含相关特征的权重,这些特征存在于“Feature”列中。

现在您可能会想,如果特征是数字,您将如何理解呢? 这些见解很难解释。为了解决这个问题,我们需要对原来的函数调用进行一些更改。

columns = ['Source','weekday','hour','text_num_words']
feature_names = list(vect.get_feature_names()) + columns
eli5.show_weights(rfc, feature_names = feature_names)

在这里,我们使用 vect(TF-IDF 向量器)和包含工程特征的变量列来获取特征名称。 然后将特征名称作为 **kwargs 传递到同一函数中。 这就是我们现在得到的。

image.png

由于我们有 5000 个 TF-IDF 特征,因此看到很多单词被赋予了高重要性是有道理的。 除了单词之外,还有一个特征text_num_words,是经过特征工程获得的。 它在总体上被列为第 12 位最重要的特征。 这可以是一个迭代过程,您可以看到工程属性的活力,如果需要,可以再次重新设计它们。

我们还可以:

  • 使用top参数指定我们是想要所有特征还是只想要前n个。
  • 使用 features_refeatures_filter 参数仅获取符合我们条件和约束的那些特征。

现在让我们看看 Eli5 的另一个用例。

B:单独的预测分析

孤立地分析某些预测可能非常有价值。 您可以通俗地向所有利益相关者解释获得的预测背后的数学机制。

让我们首先检查一个真阳性(True Positive)的情况。

eli5.show_prediction(rfc,
    train_X.toarray()[1],
    feature_names=feature_names, 
    top=20, 
    show_feature_values=True)

这就是导致模型预测该数据点属于患者的原因。

image.png

因此,该模型得出结论,该数据点属于患者,因为enlarged hearthospital heartdizzycancer等特征是说的有道理的。 <BIAS>是模型基于训练集分布输出的预期平均分,起着最关键的作用。

现在让我们看一下真阴性(True Negative)的情况。 image.png

与第一张图像相比,我们可以清楚地看到,与健康/医疗状况相关的词语太少了。尽管diagnosedcongestive heart等几个特征是最主要的贡献因素,但它们的相关贡献和价值为零。

2. LIME

LIME 代表局部可解释与模型无关的解释。让我们更深入地了解一下这个名字:

术语“局部”是指对个体预测的分析。 LIME 让我们深入了解某个预测背后发生的事情。

它与模型无关,这意味着它将每个模型都视为一个黑匣子,因此它可以在不访问模型内部的情况下进行解释,从而使其能够与各种模型一起工作。

就像名字一样直观,其解释逻辑背后的想法也同样直观:

  • LIME 基本上测试一旦模型提供了输入数据的某些变化,预测会发生什么。
  • 为了测试这一点,LIME 在一个新数据集上训练一个可解释的模型,该数据集由扰动样本和黑盒模型的相应预测组成。
  • 这个新的学习模型需要是一个很好的局部近似(对于某个个体预测),但不一定是一个很好的全局近似。它可以在数学上表示为:

image.png

解释模型,例如 x,是由 LIME 创建的可解释模型 g,最小化损失函数 L,衡量解释与原始模型 f 的预测的接近程度;而模型复杂度 Ω(g) 保持较低(更少的特征)。 G 是可能的解释族,即所有 GLM。

如何安装 LIME ?

和 ELI5 一样,它也可以使用简单的 pip 命令安装。

pip install lime

或者,它也可以使用 conda 命令安装。

conda install -c conda-forge lime

在我们的模型上使用Lime

LIME主要提供三种解释方法,三种方法都处理不同类型的数据:

  • 表格解释,
  • 文本解释,
  • 图像解释。

在我们的5004个可训练属性中,有5000个是基于Tf-Idf的特征,而这些特征只不过是单词。因此,我们将采用Lime的文本解释方法。为此,我们必须对训练做一些改变。

rfc.fit(vectorized_train_text,y)

如您所见,我们现在只使用矢量化文本特征进行建模。

正如我们所知,LIME准备了一个新的数据集,在这个数据集上它可以训练自己的可解释模型。但LIME如何在文本数据的情况下做到这一点呢?这就是如何:

  • 新文本是通过切换随机选择的单词的存在/不存在来创建的,这些单词存在于原始文本中。
  • 如果包含相应的单词,则特征为1;如果删除,则特征值为0,从而使其成为二进制表示。

理论部分讲够了,让我们看看LIME的实际应用。

导入所需的依赖项:

import lime
from sklearn.pipeline import make_pipeline

进行所需的函数调用:

explainer = lime.lime_text.LimeTextExplainer(
    class_names=['Not Patient', 'Patient'])
pl = make_pipeline(vect, rfc)

首先,我们创建一个Text Explainer实例。然后,由于我们处理的是文本特征,我们使用sklearn的流水线方法将向量化器(vect)与模型(rfc)相结合,以便将要输入的文本特征可以矢量化,并且可以进行预测。

就像ELI5一样,让我们先检查真阳性(True Positive)实例。

exp = explainer.explain_instance(
train['combined_text'][689], pl.predict_proba)

让我们绘制实例结果,看看我们得到了什么。

exp.show_in_notebook()

image.png

读完这篇文章后,很明显,它是在谈论一些急需心脏移植后生命得以挽救的患者,同时也在感谢医护人员。我们可以看到,突出显示的单词出现在“Patient”列中,因此负责将此数据点正确分类为“Patient”。

现在让我们看看真阴性(True Negative)实例的解释图是什么样子的。

image.png 第一眼看到突出显示的关键字,如coldfatburn等,这个数据点就好像与Patient类相关。在实际阅读时,我们了解到文本是在谈论冷水淋浴的好处,显然,我们的模型也理解这一点。

到现在为止,一直都还不错。但是,如果我们的特征是表格格式的连续值,或者是图像的像素值,该怎么办?

为此,我们只需要记住上面讨论的内容:

Data typeFunction
Textlime.lime_text.LimeTextExplainer()
Tabularlime.lime_tabular.limeTabularExplainer()
Imagelime.lime_image.limeImageExplainer()

好的,接下来是下一个模型解释工具——SHAP。

3. SHAP

SHapley Additive exPlanations是一种博弈论方法,用于解释任何机器学习模型的输出。SHAP通过计算每个特征对预测的贡献来解释实例的预测。它使用Shapley值。

Shapley值是什么?

  • Shapley值:一种来自合作博弈论的方法,告诉我们如何在特征之间分配“payout”。
  • 因此,可以通过假设实例的每个特征值是游戏中的“玩家”来解释预测,其中预测是payout。

如何计算Shapely值?

  • Shapely值是特征值在所有可能合作中的平均边际贡献。
  • 假设我们有一个形状为N x M的数据集,其中N是样本数,M是特征数,例如:5 – A、B、C、D和E。
  • E是具有连续值的从属属性,而A、B、C、D是我们的分类值预测因子。
  • 现在,假设我们要计算特征A的贡献,即计算其形状值。
  • 我们通过从数据集中随机选取一个实例并使用其特征D的值来模拟只有A、B和C处于合作中。然后,我们预测这个组合的E,假设它是X。
  • 现在,我们将特征A的值替换为从A的域中随机抽取的值(如果不同的话),并再次预测E,假设这次它显示为Y。
  • X-Y之间的差异,无论是正的还是负的,都是特征A在预测中的贡献。
  • A值的这个采样步骤会反复进行,并对贡献进行平均,以获得A的Shapely值。

SHAP解释可以用数学方式表示为:

g(z)=ϕ0+j=1Mϕjzjg(z^\prime)= {\phi}_0 + \sum_{j = 1}^{M} \phi_j z_j^\prime

其中,g是解释模型,z{0,1}Mz^\prime \in \{0,1\}^M 是联盟向量(coalition vector),M是最大联盟规模(coalition size),ϕjR\phi^j \in R 是特征jj的的特征归属,Shapley值。

足够的理论之后,让我们看看SHAP在模型上的表现。

如何安装SHAP

就像其他库一样,它可以用pip命令安装,只需确保pip版本超过19.0。

pip install shape

如果遇到pip错误,可以始终使用conda进行安装。

conda install -c conda-forge shap

在我们的模型上使用SHAP

导入所需的依赖项:

import shap

根据您的模型,您可以使用SHAP中提供的不同解释程序。因为我们正在处理一个随机森林分类器,所以我们将使用SHAP的树解释程序。

explainer = shap.TreeExplainer(rfc)

让我们计算特征的形状值。请记住,由于我们的大多数特征都是基于文本的,我们将利用它们来理解我们的模型,就像我们对LIME所做的那样。

shap_values = explainer.shap_values(
    vectorized_train_text.toarray(), 
    check_additivity=False
)    

shap_values显示为一个列表,其中包含2个数组作为元素,对应于数据集中的2个类。因此,我们可以从两个类的角度来解释预测。

按照我们的方法,让我们先从解释真阳性(True Positive)实例开始。为了显示一些一致性,我们将检查与LIME检查相同的数据点。

shap.initjs() 
shap.force_plot(
    explainer.expected_value[1],
    shap_values = shap_values[1][689], 
    features = vectorized_train_text.toarray()[0:][689],
    feature_names = vect.get_feature_names()
)

绘制图如下:

image.png

现在,代码中的索引以及绘制图本身似乎有点势不可挡。让我们一步一步地把它分解。

与“Patient”类关联的值出现在expected_value和shap_Values的第一个索引中;因此,此图是从“Patient'”类的角度绘制的。

关于绘制图:

  • 所有特征值的预测分数为0.74,以粗体显示。
  • 基值=0.206是模型在训练中的所有输出值的平均值。
  • 粉红色(红色)的特征值会影响对类别1(患者)的预测,而蓝色的特征值则会将结果拖向类别0(非患者)。
  • 彩色块的大小表示特征重要性的大小。
  • 由于我们的预测分数(0.74)大于基础值(0.206),该数据点已被positively分类,即分类=患者。

如果我们从另一个类的角度来看这个实例,会发生什么?

shap.initjs() 
shap.force_plot(
    explainer.expected_value[1], 
    shap_values = shap_values[1][689], 
    features = vectorized_train_text.toarray()[0:][689], 
    feature_names = vect.get_feature_names()
)    

image.png

我们很容易理解这里发生的事情:

  • 我们将expected_value和shap_values的索引从1切换到0,因为我们希望将透视图从“Patient”改为“Not Patient“。
  • 因此,所有以粉红色(红色)显示的特征都变为蓝色,现在对“非患者”类别的预测产生了负面影响。
  • 虽然预测分数(0.27)小于基础值(0.794),但我们从相反的角度来看,因此该数据点属于“患者”类。

现在让我们检查真阴性(True Negative)实例。

shap.initjs() 
shap.force_plot(
    explainer.expected_value[1], 
    shap_values = shap_values[1][120], 
    features = vectorized_train_text.toarray()[0:][120], 
    feature_names = vect.get_feature_names()
)                  

绘制图如下:
image.png

由于预测分数小于基本值,因此被归类为“无患者”。

除了局部解释外,SHAP还可以通过全局解释解释模型的一般行为。

shap.summary_plot(
    shap_values = shap_values[1], 
    features = vectorized_train_text.toarray(), 
    feature_names = vect.get_feature_names()
)

image.png

从代码中可以明显看出,我们是从“Patient”类的角度绘制的,我们可以看到像“congestive heart”、“congestive heart failure”和“trouble”这样的特征向红色光谱延伸;因此,对“Patiety”类起着不可或缺的作用。

SHAP最初提供的一系列特征可能会让人有点不知所措,但一旦掌握了窍门,就再也没有比这更直观的了。

我们将查看另一个解释库MLXTEND。

4. MLXTEND

MLxtend或Machine Learning Extensions是一个用于日常数据科学和机器学习任务的有用工具库。它提供了广泛的功能。

到目前为止,我们只分析了文本特性和其他特性。这一次,让我们使用MLXTEND看看其余的特性。

如何安装MLXTEND

它可以用一个简单的pip命令安装。

pip install mlxtend

或者,您可以从以下位置手动下载软件包:https://pypi.python.org/pypi/mlxtend,将其解压缩,进入到包中,然后使用以下命令:

python setup.py install

在模型上使用MLXTEND

导入所需的依赖项:

import mlxtend

MLXTEND提供不同的功能,如:

1.PCA相关圆

  • 通过主成分分析可以看到一种有趣的结果。
  • MLXTEND允许您使用plot_PCA_correlation_graph函数绘制PCA相关圆。
  • 我们基本上计算了特征和主成分之间的相关性。
  • 然后将这些相关性绘制为单位圆上的向量,其轴为主分量。
  • 特定的主要组件可以作为元组传递给维度函数参数。
  • 相关圆轴显示了相应主成分的方差百分比。

让我们为剩下的特性绘制这个相关圆,看看我们得到了什么。

from mlxtend.plotting import plot_pca_correlation_graph 
from sklearn.preprocessing import StandardScaler
    
X = StandardScaler().fit_transform(
    train[['text_num_words','weekday','hour','Source']].values
)
    
fig, corr_matrix = plot_pca_correlation_graph( 
    X, 
    ['text_num_words', 'weekday','hour','Source'], 
    dimensions=(1, 2), 
    figure_axis_size=6 
)    

image.png

第一主成分解释总方差的31.7%,第二主成分解释25.8%

text_num_wordsSource与第一PC更为一致,而hourweekday与第二PC更一致。

2.偏差方差分解

  • 每个人都知道偏差-方差权衡,这困扰着所有的机器学习项目。
  • 通常,目标是在两者之间找到一个最佳点,通过保持低偏差来避免拟合不足,并通过保持低方差来避免过拟合。
  • 很难得到任何预测模型的偏差方差分数,但MLXTEND可以将模型的泛化误差分解为偏差、方差和误差分数。

让我们尝试为我们的随机森林分类器计算此分数。

from mlxtend.evaluate import bias_variance_decomp
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    vectorized_train_text, 
    y,
    test_size=0.25,
    random_state=1,
    shuffle=True,
    stratify=y
)

这里必须将我们的数据集分割成训练和测试,以便进行此计算。

avg_expected_loss, avg_bias, avg_var = bias_variance_decomp(
        clf2, X_train, y_train, X_test, y_test, 
        loss='mse',
        num_rounds=50,
        random_seed=1
)
print(f"Average expected loss: {avg_expected_loss.round(3)}")
print(f"Average bias: {avg_bias.round(3)}")
print(f"Average variance: {avg_var.round(3)}")

这就是结果。

image.png

  • 我们针对本练习的文本特征对模型进行了训练。
  • 根据我们得到的分数,我们可以推断出我们的模型可能善于泛化,即它没有过拟合。
  • 由于它具有相对较高的偏差,这可能意味着它在一定程度上对我们的数据集不足。

3.绘制模型的决策边界和区域

使用MLXTEND,我们还可以在二维中查看模型的决策边界,并查看模型如何区分不同类的数据点。

然而,这种解释技术有一个缺点。对于这个可视化,一次只能使用两个特征。因此,我们将只在这里使用我们的非文本特征,两个一组。

让我们看看这里有什么样的决策边界。

进行所需导入:

from mlxtend.plotting import plot_decision_regions
from mlxtend.classifier import EnsembleVoteClassifier
import matplotlib.gridspec as gridspec
import itertools
import matplotlib.pyplot as plt

实例化模型:

clf = RandomForestClassifier(random_state=1)
value=1.5
width=0.75
gs = gridspec.GridSpec(1,2)
fig = plt.figure(figsize=(15,8))
labels = ['Random Forest']

现在, 绘制图:

for clf, lab, grd in zip([clf2], labels, itertools.product([0],[0])):
    clf.fit(train[['Source', 'weekday']].values, y)
    ax = plt.subplot(gs[grd[0], grd[1]])
    fig = plot_decision_regions(
        X=train[['Source', 'weekday']].values, 
        y=y, 
        clf=clf)
    plt.title(lab)

这就是我们得到的:

image.png

我们可以清楚地看到,这是一个非常糟糕的决策边界。输入特征不是很好的微分器,因此,巩固了我们使用其他工具获得的结果。

总结

随着越来越复杂的架构,模型解释是你现在必须要做的事情。 我希望现在你对如何做到这一点有了一些想法。

我们在本文中探讨的工具并不是唯一可用的工具,有很多方法可以理解模型预测。 其中一些方法可能包括工具或框架,而其他方法可能不包括,我鼓励您探索它们。

原文链接:ML Model Interpretation Tools: What, Why, and How to Interpret