从 SKlearn 决策树中检索决策边界线(x,y 坐标格式)

Posted

技术标签:

【中文标题】从 SKlearn 决策树中检索决策边界线(x,y 坐标格式)【英文标题】:Retrieve Decision Boundary Lines (x,y coordinate format) from SKlearn Decision Tree 【发布时间】:2017-10-11 06:45:48 【问题描述】:

我正在尝试在外部可视化平台上创建曲面图。我正在使用 sklearn decision tree documentation page 上的 iris 数据集。我也使用相同的方法来创建我的决策曲面图。我的最终目标不是 matplot lib 视觉对象,所以我从这里将数据输入到我的可视化软件中。为此,我在xxyyZ 上调用了flatten()tolist(),并编写了一个包含这些列表的JSON 文件。

问题是当我尝试绘制它时,我的可视化程序崩溃了。事实证明数据太大了。展平后,列表的长度 > 86,000。这是因为步长/绘图步长非常小.02。因此,根据模型的预测,它本质上是在数据的最小值和最大值的域中迈出小步,并在进行过程中进行绘图/填充。它有点像像素网格;我将大小缩小到只有 2000 个数组,并注意到坐标只是来回移动的线(最终包含整个坐标平面)。

问题:我可以检索决策边界线本身的 x,y 坐标(而不是遍历整个平面)吗?理想情况下,列表仅包含每条线的转折点。或者,是否有其他完全不同的方法来重新创建此图,从而提高计算效率?

这可以通过将contourf() 调用替换为countour() 来形象化:

我只是不确定如何检索管理这些行的数据(通过xxyyZ 或可能的其他方式?)。

注意:只要计算效率高,我对包含行格式的列表/或数据结构的确切格式并不挑剔。例如,对于上面的第一幅图,一些红色区域实际上是预测空间中的孤岛,所以这可能意味着我们必须像处理它自己的线一样处理它。我猜只要类与 x,y 坐标相结合,使用多少个数组(包含坐标)来捕获决策边界并不重要。

【问题讨论】:

这将是添加到sklearn 的一个很棒的功能,如果你知道的话:) 是的,我希望有一些内置的东西可以解决问题,但如果没有,我也认为它会成为一个很好的新功能。我想现在我对 matplotlib 的强大有了新的认识,因为它可以处理这些巨大的数据结构,但是对于我们这些需要更简洁的数据表示以在其他平台上进行可视化的人来说,这样的功能将非常有用。跨度> 【参考方案1】:

决策树没有很好的边界。它们有多个边界,将特征空间分层划分为矩形区域。

在我的Node Harvest 实现中,我编写了解析 scikit 决策树并提取决策区域的函数。对于这个答案,我修改了部分代码以返回对应于树决策区域的矩形列表。使用任何绘图库都应该很容易绘制这些矩形。下面是一个使用 matplotlib 的例子:

n = 100
np.random.seed(42)
x = np.concatenate([np.random.randn(n, 2) + 1, np.random.randn(n, 2) - 1])
y = ['b'] * n + ['r'] * n
plt.scatter(x[:, 0], x[:, 1], c=y)

dtc = DecisionTreeClassifier().fit(x, y)
rectangles = decision_areas(dtc, [-3, 3, -3, 3])
plot_areas(rectangles)
plt.xlim(-3, 3)
plt.ylim(-3, 3)

只要不同颜色的区域相遇,就会有一个决策边界。我想可以通过适度的努力仅提取这些边界线,但我会将其留给任何感兴趣的人。

rectangles 是一个 numpy 数组。每行对应一个矩形,列为[left, right, top, bottom, class]


更新:应用到 Iris 数据集

Iris 数据集包含三个类,而不是示例中的 2 个。所以我们必须为plot_areas 函数添加另一种颜色:color = ['b', 'r', 'g'][int(rect[4])]。 此外,数据集是 4 维的(它包含四个特征),但我们只能在 2D 中绘制两个特征。我们需要选择要绘制的特征并告诉decision_area 函数。该函数有两个参数xy——它们分别是x 和y 轴上的特征。默认值为x=0, y=1,它适用于具有多个特征的任何数据集。但是,在 Iris 数据集中,第一个维度并不是很有趣,因此我们将使用不同的设置。

函数decision_areas 也不知道数据集的范围。决策树通常具有向无穷大扩展的开放决策范围(例如,只要 sepal length 小于 xyz,它就是 B 类)。在这种情况下,我们需要人为地缩小绘图范围。我为示例数据集选择了-3..3,但对于 iris 数据集,其他范围是合适的(从来没有负值,一些特征超出 3)。

在这里,我们在 0..7 和 0..5 的范围内绘制最后两个特征的决策区域:

from sklearn.datasets import load_iris
data = load_iris()
x = data.data
y = data.target
dtc = DecisionTreeClassifier().fit(x, y)
rectangles = decision_areas(dtc, [0, 7, 0, 5], x=2, y=3)
plt.scatter(x[:, 2], x[:, 3], c=y)
plot_areas(rectangles)

请注意左上角的红色和绿色区域如何奇怪地重叠。发生这种情况是因为树在四个维度上做出决策,但我们只能显示两个。没有真正干净的方法解决这个问题。高维分类器在低维空间中通常没有很好的决策边界。

因此,如果您对分类器更感兴趣,那就是您所得到的。您可以沿各种维度组合生成不同的视图,但表示的有用性是有限的。

但是,如果您对数据比对分类器更感兴趣,则可以在拟合之前限制维度。在这种情况下,分类器仅在二维空间中做出决策,我们可以绘制出不错的决策区域:

from sklearn.datasets import load_iris
data = load_iris()
x = data.data[:, [2, 3]]
y = data.target
dtc = DecisionTreeClassifier().fit(x, y)
rectangles = decision_areas(dtc, [0, 7, 0, 3], x=0, y=1)
plt.scatter(x[:, 0], x[:, 1], c=y)
plot_areas(rectangles)


最后,实现如下:

import numpy as np
from collections import deque
from sklearn.tree import DecisionTreeClassifier
from sklearn.tree import _tree as ctree
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle


class AABB:
    """Axis-aligned bounding box"""
    def __init__(self, n_features):
        self.limits = np.array([[-np.inf, np.inf]] * n_features)

    def split(self, f, v):
        left = AABB(self.limits.shape[0])
        right = AABB(self.limits.shape[0])
        left.limits = self.limits.copy()
        right.limits = self.limits.copy()

        left.limits[f, 1] = v
        right.limits[f, 0] = v

        return left, right


def tree_bounds(tree, n_features=None):
    """Compute final decision rule for each node in tree"""
    if n_features is None:
        n_features = np.max(tree.feature) + 1
    aabbs = [AABB(n_features) for _ in range(tree.node_count)]
    queue = deque([0])
    while queue:
        i = queue.pop()
        l = tree.children_left[i]
        r = tree.children_right[i]
        if l != ctree.TREE_LEAF:
            aabbs[l], aabbs[r] = aabbs[i].split(tree.feature[i], tree.threshold[i])
            queue.extend([l, r])
    return aabbs


def decision_areas(tree_classifier, maxrange, x=0, y=1, n_features=None):
    """ Extract decision areas.

    tree_classifier: Instance of a sklearn.tree.DecisionTreeClassifier
    maxrange: values to insert for [left, right, top, bottom] if the interval is open (+/-inf) 
    x: index of the feature that goes on the x axis
    y: index of the feature that goes on the y axis
    n_features: override autodetection of number of features
    """
    tree = tree_classifier.tree_
    aabbs = tree_bounds(tree, n_features)

    rectangles = []
    for i in range(len(aabbs)):
        if tree.children_left[i] != ctree.TREE_LEAF:
            continue
        l = aabbs[i].limits
        r = [l[x, 0], l[x, 1], l[y, 0], l[y, 1], np.argmax(tree.value[i])]
        rectangles.append(r)
    rectangles = np.array(rectangles)
    rectangles[:, [0, 2]] = np.maximum(rectangles[:, [0, 2]], maxrange[0::2])
    rectangles[:, [1, 3]] = np.minimum(rectangles[:, [1, 3]], maxrange[1::2])
    return rectangles

def plot_areas(rectangles):
    for rect in rectangles:
        color = ['b', 'r'][int(rect[4])]
        print(rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1])
        rp = Rectangle([rect[0], rect[2]], 
                       rect[1] - rect[0], 
                       rect[3] - rect[2], color=color, alpha=0.3)
        plt.gca().add_artist(rp)

【讨论】:

很好的例子和自定义函数!因此,如果我想将矩形 x,y 坐标导出到 json,rectangles 将是一个嵌套数组,其中包含所有矩形,对吗? @ArashHowaida 我不确定您是否可以将 numpy 数组直接转换为 json,但您可以调用 rectangles.tolist() 来获取 Python 的 json 工具导出到嵌套 json 数组的嵌套列表。 即使域严格为正,我的 x/y 坐标有时也会得到负值。这可能是由于输入的 maxrange 不够准确还是其他用户错误? 我刚刚注意到在您的原始随机数据示例中,实际上预测类有第五个条目。我想当我为 iris 数据集调整它时,我搞砸了。为了稳健性,您能否包含一个简短的设置来演示 iris 数据如何与您的函数交互?也许只是第一对:x = iris.data[:,[0,1]] @ArashHowaida 当你绘制的一个特征根本没有被树使用时,就会发生这种情况。我已更新 decision_areas 函数以接受 n_features 参数。将其设置为数据集中的特征数(即 4 与 Iris),你应该没问题。【参考方案2】:

@kazemakase 的方法是“正确”的方法。为了完整起见,这里有一个简单的方法来获取Z 中作为决策边界的每个“像素”:

steps = np.diff(Z,axis=0)[:,1:] + np.diff(Z,axis=1)[1:,:]
is_boundary = steps != 0
x,y = np.where(is_boundary)
# rescale to convert pixels into into original units
x = x.astype(np.float) * plot_step
y = y.astype(np.float) * plot_step

is_boundary 的图(放大以便可以看到所有非零条目):

【讨论】:

我还注意到 is_boundary 是一个布尔列表,我不确定 matplotlib 是如何绘制的。也许是因为在 python 中,没有变量,只有数据和名称链接?无论如何,我的外部可视化平台不会以这种方式工作。当我在导出到 json 之前运行以下命令测试坐标时:plt.plot(x,y) plt.show(),结果与原来的不一样。你也可以试试这样我们可以弄清楚吗?谢谢。 x,y 是标记边界的像素位置。它们不是线段(因为它们不是按顺序排列的)。如果您想用plt.plot 绘制线段,则必须从“图像”is_boundary 中提取它们。否则,您可以只用plt.imshow 绘制is_boundary【参考方案3】:

对于那些感兴趣的人,我最近也不得不为高维数据实现这个,代码如下:

number_of_leaves = (tree.tree_.children_left == -1).sum()
features = x.shape[1]
boundaries = np.zeros([number_of_leaves, features, 2])
boundaries[:,:,0] = -np.inf
boundaries[:,:,1] = np.inf

locs = np.where(tree.tree_.children_left == -1)[0]

for k in range(locs.shape[0]):
    idx = locs[k]
    idx_new = idx

    while idx_new != 0:
        i_check = np.where(tree.tree_.children_left == idx_new)[0]
        j_check = np.where(tree.tree_.children_right == idx_new)[0]

        if i_check.shape[0] == 1:
            idx_new = i_check[0]
            feat_ = tree.tree_.feature[idx_new]
            val_ = tree.tree_.value[idx_new]
            boundaries[k,feat_, 0] = val_
        elif j_check.shape[0] == 1:
            idx_new = j_check[0]
            feat_ = tree.tree_.feature[idx_new]
            val_ = tree.tree_.value[idx_new]
            boundaries[k,feat_, 1] = val_ 
        else: 
            print('Fail Case') # for debugging only - never occurs

基本上我建立了一个 n*d*2 张量,其中 n 是树的叶子数,d 是空间的维数,第三维保存最小值和最大值。叶子存储在 tree.tree_.children_left / tree.tree_.children_right 为 -1,然后我向后循环查找导致分裂到叶子上的分支并将分裂标准添加到决策边界。

【讨论】:

以上是关于从 SKlearn 决策树中检索决策边界线(x,y 坐标格式)的主要内容,如果未能解决你的问题,请参考以下文章

决策树中特定类的 Sklearn 决策规则

如何在 sklearn 决策树中显示特征名称?

如何获取决策树中的所有基尼指数?

我如何从决策树中预测 x_train 的位置获取叶子的节点号?

如何在 python 中的 SVM sklearn 数据中绘制决策边界?

clf.score(X_train,Y_train) 在决策树中评估啥?