EBM 内部原理 - 回归#

这是描述 EBM 内部原理以及如何进行预测的 3 部分系列中的第 1 部分。对于第 2 部分,请点击此处。对于第 3 部分,请点击此处

在第 1 部分中,我们将介绍最简单实用的 EBM:不包含交互项、缺失值或其他复杂情况的回归模型。

EBM 的核心是广义加性模型,其中来自单个特征和交互项的分数贡献相加得出预测结果。每个独立的分数贡献都通过查阅表来确定。在进行查阅之前,我们需要先对连续特征进行离散化,并为类别特征分配分箱索引。

回归是 EBM 模型最简单的形式,因为最终的总和就是实际预测值,无需逆链接函数。

# boilerplate
from interpret import show
from interpret.glassbox import ExplainableBoostingRegressor
import numpy as np

from interpret import set_visualize_provider
from interpret.provider import InlineProvider
set_visualize_provider(InlineProvider())
# make a dataset composed of a nominal categorical, and a continuous feature 
X = [["Peru", 7.0], ["Fiji", 8.0], ["Peru", 9.0]]
y = [450.0, 550.0, 350.0]

# Fit a regression EBM without interactions
# Eliminate the validation set to handle the small dataset
ebm = ExplainableBoostingRegressor(
    interactions=0, 
    validation_size=0, outer_bags=1, min_samples_leaf=1, min_hessian=1e-9)
ebm.fit(X, y)
show(ebm.explain_global())





让我们来看看 ExplainableBoostingRegressor 中一些最重要的属性。

print(ebm.feature_types_in_)
['nominal', 'continuous']

由于我们没有向 ExplainableBoostingRegressor 的 __init__ 函数传递 feature_types 参数,因此系统分配了一些合理的特征类型猜测。这些猜测记录在 ebm.feature_types_in_ 中。我们支持以下基本特征类型:'continuous'(连续)、'nominal'(名义)和 'ordinal'(有序)。出于评估目的,'nominal' 和 'ordinal' 可以视为相同,因为它们都是类别型,并且在模型中的表示方式相同。'nominal' 和 'ordinal' 在训练期间的处理方式不同,但这里我们只关注预测。

print(ebm.bins_)
[[{'Fiji': 1, 'Peru': 2}], [array([7.5, 8.5])]]

ebm.bins_ 定义了如何对类别特征('nominal' 和 'ordinal')和 'continuous'(连续)特征进行分箱。

对于类别特征,我们使用一个字典,将类别字符串映射到分箱索引。在这个例子中,“Fiji”被分配到分箱 #1,“Peru”被分配到分箱 #2。

连续特征的分箱通过一系列分割点来定义,这些点将连续范围划分成多个区域。在这个例子中,连续特征的数据集包含 3 个唯一值:7.0、8.0 和 9.0。要将这 3 个值分成 3 个分箱,需要 2 个分割点。EBM 选择了 7.5 和 8.5 作为分割点,但它也可以选择介于 7.0 到 8.0 之间以及 8.0 到 9.0 之间的任何分割点值。

在涉及连续特征进行预测时,我们接收到的特征值可以在从负无穷到正无穷的连续范围内的任意位置。因此在这个例子中,我们的两个分割点定义了 3 个分箱区域:分箱 #1 是 [-inf, 7.5),分箱 #2 是 [7.5, 8.5),分箱 #3 是 [8.5, +inf)。

如果任何特征值等于分箱的分割点值,它们将被放入靠上的分箱选择。要将连续特征转换为分箱,我们可以使用 numpy.digitize 函数,并进行我们在下面将看到的微调。

EBM 还包括 2 个特殊的分箱:缺失分箱和未见分箱。缺失分箱是当存在任何缺失特征值时使用的分箱,例如 NaN 或 ‘None’。未见分箱用于预测时看到训练集中不存在的类别值。例如,如果我们的测试数据集包含类别值“越南”或“巴西”,那么在此示例中我们将使用未见分箱,因为这些国家未出现在训练集中。

缺失分箱总是位于索引 0,未见分箱总是位于最后一个索引。

print(ebm.term_scores_[0])
[  0.          84.55036163 -42.27518082   0.        ]

ebm.term_scores_ 包含每个加性项的查阅表。ebm.term_scores_[0] 是此示例中第一个特征的查阅表,该特征是包含国家字符串的类别特征。

由于第一个特征是类别型,我们使用 ebm.bins_[0] 中的字典,即 {‘Fiji’: 1, ‘Peru’: 2},来查阅当这些字符串作为特征值出现时应使用的分箱。如果我们收到 NaN 作为特征值,则将使用索引 0 处的得分值。如果特征值是“Fiji”,将使用索引 1 处的得分值。如果特征值是“Peru”,将使用索引 2 处的得分值。如果特征值是其他任何值,将使用索引 3 处的得分值。

另一点需要注意的是,ebm.term_scores_ 中存储的值与全局解释图(见上文)中显示的值是相同的。这对于类别特征和连续特征都是如此,对于交互项也是如此。这种完全的模型透明性使得 EBM 成为 Glassbox 模型。

print(ebm.term_scores_[1])
[  0.          42.27518082  15.44963837 -57.72481918   0.        ]

ebm.term_scores_[1] 是数据集中连续特征的查阅表。索引 0 再次保留用于缺失值,最后一个索引再次保留用于未见值。在连续特征的上下文中,未见分箱用于任何无法转换为浮点数的值,因此如果我们收到字符串“BAD_VALUE”而不是数字,我们将使用最后一个分箱。未见分箱的得分值可以可选地设置为 NaN,如果你希望它表示错误条件。

中间剩余的 3 个得分对应于 3 个分箱区域,在我们的例子中分别为:分箱 #1 [-numpy.inf, 7.5),分箱 #2 [7.5, 8.5),以及分箱 #3 [8.5, +numpy.inf)。

再次强调,ebm.term_scores_[1] 中的得分与 feature_0001 的全局解释图(见上文)中显示的值匹配。

print(ebm.intercept_)
450.0

ebm.intercept_ 通常应该非常接近基础得分。在这个例子中,截距确实非常接近三个 'y' 值的平均值:numpy.average([450, 550, 350]) == 450。

进行预测时,我们从截距值开始,并加上每个查阅表中的得分。

示例代码

最后,这里有一些代码,将上述考虑因素结合起来形成一个函数,可以在简化场景下进行预测。这段代码不处理交互项、缺失值、未见值或分类等情况。

如果你需要一个适用于所有 EBM 场景的完整即用函数,请参见第 3 部分中的多分类示例,该示例除了处理多分类外,还处理回归和二分类以及所有其他细节。

sample_scores = []
for sample in X:
    # start from the intercept for each sample
    score = ebm.intercept_
    print("intercept: " + str(score))

    # we have 2 features, so add their score contributions
    for feature_idx, feature_val in enumerate(sample):
        bins = ebm.bins_[feature_idx][0]
        if isinstance(bins, dict):
            # categorical feature
            bin_idx = bins[feature_val]
        else:
            # continuous feature. bins is an array of cut points
            # add 1 because the 0th bin is reserved for 'missing'
            bin_idx = np.digitize(feature_val, bins) + 1

        local_score = ebm.term_scores_[feature_idx][bin_idx]

        # local_score is also the local feature importance (see plot below)
        print(ebm.feature_names_in_[feature_idx] + ": " + str(local_score))
        
        score += local_score
    sample_scores.append(score)
    print()

print("PREDICTIONS:")
print(ebm.predict(X))
print(np.array(sample_scores))
intercept: 450.0
feature_0000: -42.275180816747664
feature_0001: 42.275180816747564

intercept: 450.0
feature_0000: 84.55036163349533
feature_0001: 15.449638366504642

intercept: 450.0
feature_0000: -42.275180816747664
feature_0001: -57.72481918325221

PREDICTIONS:
[450. 550. 350.]
[450. 550. 350.]

预测结果与 ExplainableBoostingRegressor 的 predict 函数的预测结果一致。在这个例子中,EBM 几乎完全恢复了原始的 ‘y’ 值 [450, 550, 350]。这并不令人惊讶,因为这个模型是为了说明目的而故意高度过拟合的。

另一个值得注意的有趣之处在于,我们从查阅表中检索到的值(分配给变量 ‘local_score’)与 EBM 局部解释中显示的值是相同的。

show(ebm.explain_local(X, y), 0)