【问题标题】:Why I get different expected_value when I include the training data in TreeExplainer?当我在 TreeExplainer 中包含训练数据时,为什么会得到不同的 expected_value?
【发布时间】:2020-11-13 13:30:05
【问题描述】:

在 SHAP TreeExplainer 中包含训练数据会在 scikit-learn GBT Regressor 中给出不同的expected_value

可重现的示例(在 Google Colab 中运行):

from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingRegressor
import numpy as np
import shap

shap.__version__
# 0.37.0

X, y = make_regression(n_samples=1000, n_features=10, random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

gbt = GradientBoostingRegressor(random_state=0)
gbt.fit(X_train, y_train)

# mean prediction:
mean_pred_gbt = np.mean(gbt.predict(X_train))
mean_pred_gbt
# -11.534353657511172

# explainer without data
gbt_explainer = shap.TreeExplainer(gbt)
gbt_explainer.expected_value
# array([-11.53435366])

np.isclose(mean_pred_gbt, gbt_explainer.expected_value)
# array([ True])

# explainer with training data
gbt_data_explainer = shap.TreeExplainer(model=gbt, data=X_train) # specifying feature_perturbation does not change the result
gbt_data_explainer.expected_value
# -23.564797322079635

因此,包含训练数据 gbt_data_explainer.expected_value 时的预期值与未提供数据时计算的预期值 (gbt_explainer.expected_value) 完全不同。

当与(明显不同的)各自的shap_values 一起使用时,这两种方法都是相加且一致的:

np.abs(gbt_explainer.expected_value + gbt_explainer.shap_values(X_train).sum(1) - gbt.predict(X_train)).max() < 1e-4
# True

np.abs(gbt_data_explainer.expected_value + gbt_data_explainer.shap_values(X_train).sum(1) - gbt.predict(X_train)).max() < 1e-4
# True

但我想知道为什么他们不提供相同的expected_value,以及为什么gbt_data_explainer.expected_value 与预测的平均值如此不同。

我在这里错过了什么?

【问题讨论】:

  • 部分解释:尝试从gbt_data_explainer 提取解释失败并显示有用的消息。运行gbt_explainer.model.fully_defined_weighting, gbt_data_explainer.model.fully_defined_weighting 会得到True, False:shap 认为X_train 不会到达每棵树的每一片叶子。
  • 代码的相关位:github.com/slundberg/shap/blob/… 我不知道为什么 shap 会使用它的 C 代码得到不同的预测 (github.com/slundberg/shap/blob/…); sklearn 在中点(特征值之间)分裂,所以我不认为舍入错误是罪魁祸首。
  • 我的理解[背景]data是减少运行时间的帮手。从背景数据中采样扰动依赖于特征独立性的假设,而这个特定数据集并非如此,因此采样会产生不切实际的特征组合。见hereherehereherehere
  • 这是我基于半小时研究的最佳选择。这个解释还有很多问题:为什么TreeExplainer对feature_perturbation参数不敏感?它是一个错误吗? TreeExplainer 真的是一种精确的形状值估计方法吗?如果是背景数据,需要多少样本才能获得准确的结果?
  • 到达那里:gbt_explainer.data.shape 只是(100, 10)...但我找不到它在代码中的哪个位置(下采样?)。因此,当它执行先前链接的步骤“如果给定数据,则重新计算通过每个节点的样本数”时,它没有整个集合,并且无法填充所有叶子,如前所述。另见解释者的.model.node_sample_weight 列表。

标签: machine-learning scikit-learn shap


【解决方案1】:

data 被传递时,显然shap 子集为100 行,然后通过树运行这些行以重置每个节点的样本计数。所以报告的-23.5... 是这 100 行的平均模型输出。

data 被传递给 Independent 掩码器,该掩码器执行子采样:
https://github.com/slundberg/shap/blob/v0.37.0/shap/explainers/_tree.py#L94
https://github.com/slundberg/shap/blob/v0.37.0/shap/explainers/_explainer.py#L68
https://github.com/slundberg/shap/blob/v0.37.0/shap/maskers/_tabular.py#L216

跑步

from shap import maskers

another_gbt_explainer = shap.TreeExplainer(
    gbt,
    data=maskers.Independent(X_train, max_samples=800),
    feature_perturbation="tree_path_dependent"
)
another_gbt_explainer.expected_value

回到

-11.534353657511172

【讨论】:

    【解决方案2】:

    虽然@Ben 在挖掘data 如何通过Independent 掩码器方面做得很好,但他的回答并没有准确显示(1)如何计算基值以及我们从哪里获得不同的基值和(2)如何选择/降低max_samples参数

    不同值的来源

    masker 对象有一个data 属性,用于保存屏蔽处理后的数据。获取gbt_explainer.expected_value中显示的值:

    from shap.maskers import Independent
    gbt = GradientBoostingRegressor(random_state=0)
    
    # mean prediction:
    mean_pred_gbt = np.mean(gbt.predict(X_train))
    mean_pred_gbt
    # -11.534353657511172
    
    # explainer without data
    gbt_explainer = shap.TreeExplainer(gbt)
    gbt_explainer.expected_value
    # array([-11.53435366])
    
    gbt_explainer = shap.TreeExplainer(gbt, Independent(X_train,100))
    gbt_explainer.expected_value
    # -23.56479732207963
    

    需要做的:

    masker = Independent(X_train,100)
    gbt.predict(masker.data).mean()
    # -23.56479732207963
    

    选择max_samples怎么样?

    max_samples 设置为原始数据集长度似乎也适用于其他解释器:

    import sklearn
    from sklearn.feature_extraction.text import TfidfVectorizer
    from sklearn.model_selection import train_test_split
    import shap
    from shap.maskers import Independent
    from scipy.special import logit, expit
    
    corpus,y = shap.datasets.imdb()
    corpus_train, corpus_test, y_train, y_test = train_test_split(corpus, y, test_size=0.2, random_state=7)
    
    vectorizer = TfidfVectorizer(min_df=10)
    X_train = vectorizer.fit_transform(corpus_train)
    
    model = sklearn.linear_model.LogisticRegression(penalty="l2", C=0.1)
    model.fit(X_train, y_train)
    
    explainer = shap.Explainer(model
                               ,masker = Independent(X_train,100)
                               ,feature_names=vectorizer.get_feature_names()
                              )
    explainer.expected_value
    # -0.18417413671991964
    

    这个值来自:

    masker=Independent(X_train,100)
    logit(model.predict_proba(masker.data.mean(0).reshape(1,-1))[...,1])
    # array([-0.18417414])
    

    max_samples=100 对于true base_value 似乎有点偏离(仅提供功能意味着):

    logit(model.predict_proba(X_train.mean(0).reshape(1,-1))[:,1])
    array([-0.02938039])
    

    通过增加max_samples 可能会合理地接近true 基线,同时保持较低的样本数量:

    masker = Independent(X_train,1000)
    logit(model.predict_proba(masker.data.mean(0).reshape(1,-1))[:,1])
    # -0.05957302658674238
    

    因此,要获得感兴趣的解释器的基值 (1) 通过您的模型传递 explainer.data(或 masker.data)并 (2) 选择 max_samples 以便采样数据上的 base_value 足够接近真实值基值。您也可以尝试观察形状重要性的值和顺序是否收敛。

    有些人可能会注意到,为了获得基值,有时我们平均特征输入 (LogisticRegression) 和有时输出 (GBT)

    【讨论】:

      猜你喜欢
      • 2020-02-19
      • 2021-04-03
      • 2020-06-04
      • 2020-11-08
      • 2021-12-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多