理解简单模型的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]:
解释模型
注意,偏置项是模型在训练数据集上的预期输出(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]:
解释模型
注意,偏置项是模型在训练数据集上的预期输出(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]:
解释模型
注意,偏置项是模型在训练数据集上的期望输出(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]:
解释模型
注意,偏置项是模型在训练数据集上的预期输出(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]:
解释模型
需要注意的是,偏置项是模型在训练数据集上的预期输出(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 |