理解简单模型的Tree SHAP

特征的 SHAP 值是通过在所有特征顺序中逐个引入特征时,对该特征进行条件化所导致的模型输出的平均变化。虽然这很容易说明,但计算起来却很有挑战性。因此,本笔记本旨在提供几个简单的示例,在这些示例中我们可以看到这对于非常小的树是如何运作的。对于任意大的树,仅通过观察树来直观地猜测这些值是非常困难的。

[1]:
import graphviz
import numpy as np
import pandas as pd
from sklearn.tree import DecisionTreeRegressor, export_graphviz

import shap

单次分割示例

[2]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: N // 2, 0] = 1
y[: N // 2] = 1

# fit model
single_split_model = DecisionTreeRegressor(max_depth=1)
single_split_model.fit(X, y)

# draw model
dot_data = export_graphviz(
    single_split_model,
    out_file=None,
    filled=True,
    rounded=True,
    special_characters=True,
)
graph = graphviz.Source(dot_data)
graph
[2]:
../../../_images/example_notebooks_tabular_examples_tree_based_models_Understanding_Tree_SHAP_for_Simple_Models_3_0.svg

解释模型

注意,偏置项是模型在训练数据集上的预期输出(0.5)。对于模型中未使用的特征,SHAP值始终为0,而对于 \(x_0\) ,其SHAP值仅是预期值与模型输出之间的差异。

[3]:
xs = [np.ones(M), np.zeros(M)]
df = pd.DataFrame()
for idx, x in enumerate(xs):
    index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
    df = pd.concat(
        [
            df,
            pd.DataFrame(
                [x, shap.TreeExplainer(single_split_model).shap_values(x)],
                index=index,
                columns=["x1", "x2", "x3", "x4"],
            ),
        ]
    )
df
[3]:
x1 x2 x3 x4
Example 0 x 1.0 1.0 1.0 1.0
shap_values 0.5 0.0 0.0 0.0
Example 1 x 0.0 0.0 0.0 0.0
shap_values -0.5 0.0 0.0 0.0

两个功能与示例

在这个例子中,我们使用了两个特性。如果特性 \(x_{0} = 1\)\(x_{1} = 1\),目标值为1,否则为0。因此我们称这个为AND模型。

[4]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: 1 * N // 4, 1] = 1
X[: N // 2, 0] = 1
X[N // 2 : 3 * N // 4, 1] = 1
y[: 1 * N // 4] = 1

# fit model
and_model = DecisionTreeRegressor(max_depth=2)
and_model.fit(X, y)

# draw model
dot_data = export_graphviz(
    and_model, out_file=None, filled=True, rounded=True, special_characters=True
)
graph = graphviz.Source(dot_data)
graph
[4]:
../../../_images/example_notebooks_tabular_examples_tree_based_models_Understanding_Tree_SHAP_for_Simple_Models_8_0.svg

解释模型

注意,偏置项是模型在训练数据集上的预期输出(0.25)。未使用的特征 \(x_2\)\(x_3\) 的SHAP值始终为0。对于 \(x_0\)\(x_1\),它只是预期值(0.25)与模型输出之间的差值,平均分配给它们(因为它们对AND函数的贡献相等)。

[5]:
xs = np.array([np.ones(M), np.zeros(M)])
# np.array([np.ones(M), np.zeros(M), np.array([1, 0, 1, 0]), np.array([0, 1, 0, 0])]   # you can also check these examples
df = pd.DataFrame()
for idx, x in enumerate(xs):
    index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
    df = pd.concat(
        [
            df,
            pd.DataFrame(
                [x, shap.TreeExplainer(and_model).shap_values(x)],
                index=index,
                columns=["x1", "x2", "x3", "x4"],
            ),
        ]
    )
df
[5]:
x1 x2 x3 x4
Example 0 x 1.000 1.000 1.0 1.0
shap_values 0.375 0.375 0.0 0.0
Example 1 x 0.000 0.000 0.0 0.0
shap_values -0.125 -0.125 0.0 0.0
[6]:
y.mean()
[6]:
0.25

以下是如何获取示例1的Shap值:偏置项(y.mean())为0.25,目标值为1。这留下了1 - 0.27 = 0.75在相关特征之间进行分配。由于只有 \(x_1\)\(x_2\) 对目标值有贡献(并且贡献程度相同),因此它们之间平分,即每个0.375。

两个功能 或 示例

我们对上面的例子做了一点变化。如果 \(x_{0} = 1\)\(x_{1} = 1\),目标值为 1,否则为 0。你能猜出 SHAP 值而不往下滚动吗?

[7]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: N // 2, 0] = 1
X[: 1 * N // 4, 1] = 1
X[N // 2 : 3 * N // 4, 1] = 1
y[: N // 2] = 1
y[N // 2 : 3 * N // 4] = 1

# fit model
or_model = DecisionTreeRegressor(max_depth=2)
or_model.fit(X, y)

# draw model
dot_data = export_graphviz(
    or_model, out_file=None, filled=True, rounded=True, special_characters=True
)
graph = graphviz.Source(dot_data)
graph
[7]:
../../../_images/example_notebooks_tabular_examples_tree_based_models_Understanding_Tree_SHAP_for_Simple_Models_15_0.svg

解释模型

注意,偏置项是模型在训练数据集上的期望输出(0.75)。对于模型中未使用的特征,SHAP值始终为0,而对于 \(x_0\)\(x_1\) ,它只是期望值与模型输出之间的差值,平均分配给它们(因为它们对OR函数的贡献相等)。

[8]:
xs = np.array([np.ones(M), np.zeros(M)])
# np.array([np.ones(M), np.zeros(M), np.array([1, 0, 1, 0]), np.array([0, 1, 0, 0])]   # you can also check these examples
df = pd.DataFrame()
for idx, x in enumerate(xs):
    index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
    df = pd.concat(
        [
            df,
            pd.DataFrame(
                [x, shap.TreeExplainer(or_model).shap_values(x)],
                index=index,
                columns=["x1", "x2", "x3", "x4"],
            ),
        ]
    )
df
[8]:
x1 x2 x3 x4
Example 0 x 1.000 1.000 1.0 1.0
shap_values 0.125 0.125 0.0 0.0
Example 1 x 0.000 0.000 0.0 0.0
shap_values -0.375 -0.375 0.0 0.0

两个特征的异或示例

[9]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: N // 2, 0] = 1
X[: 1 * N // 4, 1] = 1
X[N // 2 : 3 * N // 4, 1] = 1
y[1 * N // 4 : N // 2] = 1
y[N // 2 : 3 * N // 4] = 1

# fit model
xor_model = DecisionTreeRegressor(max_depth=2)
xor_model.fit(X, y)

# draw model
dot_data = export_graphviz(
    xor_model, out_file=None, filled=True, rounded=True, special_characters=True
)
graph = graphviz.Source(dot_data)
graph
[9]:
../../../_images/example_notebooks_tabular_examples_tree_based_models_Understanding_Tree_SHAP_for_Simple_Models_19_0.svg

解释模型

注意,偏置项是模型在训练数据集上的预期输出(0.5)。对于模型中未使用的特征,SHAP值始终为0,而对于 \(x_0\)\(x_1\),它只是预期值与模型输出之间的差值,平均分配给它们(因为它们对XOR函数的贡献相等)。

[10]:
xs = np.array([np.ones(M), np.zeros(M)])
# np.array([np.ones(M), np.zeros(M), np.array([1, 0, 1, 0]), np.array([0, 1, 0, 0])]   # you can also check these examples
df = pd.DataFrame()
for idx, x in enumerate(xs):
    index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
    df = pd.concat(
        [
            df,
            pd.DataFrame(
                [x, shap.TreeExplainer(xor_model).shap_values(x)],
                index=index,
                columns=["x1", "x2", "x3", "x4"],
            ),
        ]
    )
df
[10]:
x1 x2 x3 x4
Example 0 x 1.00 1.00 1.0 1.0
shap_values -0.25 -0.25 0.0 0.0
Example 1 x 0.00 0.00 0.0 0.0
shap_values -0.25 -0.25 0.0 0.0

两个功能 AND + 功能增强示例

[11]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: N // 2, 0] = 1
X[: 1 * N // 4, 1] = 1
X[N // 2 : 3 * N // 4, 1] = 1
y[: 1 * N // 4] = 1
y[: N // 2] += 1

# fit model
and_fb_model = DecisionTreeRegressor(max_depth=2)
and_fb_model.fit(X, y)

# draw model
dot_data = export_graphviz(
    and_fb_model, out_file=None, filled=True, rounded=True, special_characters=True
)
graph = graphviz.Source(dot_data)
graph
[11]:
../../../_images/example_notebooks_tabular_examples_tree_based_models_Understanding_Tree_SHAP_for_Simple_Models_23_0.svg

解释模型

需要注意的是,偏置项是模型在训练数据集上的预期输出(0.75)。对于模型中未使用的特征,SHAP值始终为0,而对于 \(x_0\)\(x_1\),其SHAP值是预期值与模型输出之间的差值,平均分配给它们(因为它们对AND函数的贡献相等),再加上 \(x_0\) 额外的0.5影响,因为它本身具有 \(1.0\) 的效果(如果它开启则为+0.5,关闭则为-0.5)。

[12]:
xs = np.array([np.ones(M), np.zeros(M)])
# np.array([np.ones(M), np.zeros(M), np.array([1, 0, 1, 0]), np.array([0, 1, 0, 0])]   # you can also check these examples
df = pd.DataFrame()
for idx, x in enumerate(xs):
    index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
    df = pd.concat(
        [
            df,
            pd.DataFrame(
                [x, shap.TreeExplainer(and_fb_model).shap_values(x)],
                index=index,
                columns=["x1", "x2", "x3", "x4"],
            ),
        ]
    )
df
[12]:
x1 x2 x3 x4
Example 0 x 1.000 1.000 1.0 1.0
shap_values 0.875 0.375 0.0 0.0
Example 1 x 0.000 0.000 0.0 0.0
shap_values -0.625 -0.125 0.0 0.0