如何从scikit-learn决策树中提取决策规则?

209

我能从已训练的决策树中提取出底层的决策规则(或“决策路径”),并将其以文本列表形式呈现吗?

类似这样:

if A>0.4 then if B<0.2 then if C>0.8 then class='X'

4
可能是重复的问题:“如何在使用scikit-learn时找到决策树分裂的属性?” - lejlot
你有没有找到这个问题的答案?我必须以SAS数据步骤格式导出决策树规则,几乎与您列出的完全相同。 - Zelazny7
1
你可以使用 sklearn-porter 包将决策树(还包括随机森林和提升树)导出和转译成 C、Java、JavaScript 等其他语言。 - Darius
你可以查看这个链接- https://www.kdnuggets.com/2017/05/simplifying-decision-tree-interpretation-decision-rules-python.html - yogesh agrawal
我在我的文章中总结了从决策树中提取规则的3种方法。其中一种方法基于'paulkernfeld'代码,可以生成易于人类理解的文本规则格式。 - pplonski
25个回答

181

我认为这个答案比其他答案更正确:

from sklearn.tree import _tree

def tree_to_code(tree, feature_names):
    tree_ = tree.tree_
    feature_name = [
        feature_names[i] if i != _tree.TREE_UNDEFINED else "undefined!"
        for i in tree_.feature
    ]
    print "def tree({}):".format(", ".join(feature_names))

    def recurse(node, depth):
        indent = "  " * depth
        if tree_.feature[node] != _tree.TREE_UNDEFINED:
            name = feature_name[node]
            threshold = tree_.threshold[node]
            print "{}if {} <= {}:".format(indent, name, threshold)
            recurse(tree_.children_left[node], depth + 1)
            print "{}else:  # if {} > {}".format(indent, name, threshold)
            recurse(tree_.children_right[node], depth + 1)
        else:
            print "{}return {}".format(indent, tree_.value[node])

    recurse(0, 1)

这会打印出一个有效的Python函数。以下是一棵试图返回其输入(介于0到10之间的数字)的树的示例输出。

def tree(f0):
  if f0 <= 6.0:
    if f0 <= 1.5:
      return [[ 0.]]
    else:  # if f0 > 1.5
      if f0 <= 4.5:
        if f0 <= 3.5:
          return [[ 3.]]
        else:  # if f0 > 3.5
          return [[ 4.]]
      else:  # if f0 > 4.5
        return [[ 5.]]
  else:  # if f0 > 6.0
    if f0 <= 8.5:
      if f0 <= 7.5:
        return [[ 7.]]
      else:  # if f0 > 7.5
        return [[ 8.]]
    else:  # if f0 > 8.5
      return [[ 9.]]

以下是其他回答中存在的一些问题:

  1. 使用 tree_.threshold == -2 判断一个节点是否为叶子节点不是一个好主意。如果它是一个具有阈值为-2的真正决策节点怎么办?相反,您应该查看 tree.featuretree.children_*
  2. 代码行 features = [feature_names[i] for i in tree_.feature] 会导致我的 sklearn 版本崩溃,因为一些 tree.tree_.feature 的值为-2(特别是对于叶子节点)。
  3. 递归函数中没有必要有多个 if 语句,只需要一个即可。

1
这段代码非常适合我。然而,我有500多个feature_names,因此输出的代码几乎不可能为人类所理解。有没有一种方法让我只将我感兴趣的feature_names输入到函数中? - user3768495
3
我同意前面的评论。如果我理解正确,应该将print "{}return {}".format(indent, tree_.value[node])更改为print "{}return {}".format(indent, np.argmax(tree_.value[node][0])),以便函数返回类索引。 - soupault
2
@paulkernfeld 哦,是的,我看到您可以循环遍历RandomForestClassifier.estimators_,但我无法想出如何组合估算器的结果。 - Nathan Lloyd
7
我无法在Python 3中使其正常工作,_tree 部分似乎永远不会工作,而 TREE_UNDEFINED 没有定义。这个链接帮了我。虽然导出的代码不能直接在Python中运行,但它类似于C语言并且很容易翻译到其他语言:https://web.archive.org/web/20171005203850/http://www.kdnuggets.com/2017/05/simplifying-decision-tree-interpretation-decision-rules-python.html - Josiah
3
@Josiah,在Python3中,将()添加到print语句中使其正常工作。例如:print "bla" => print("bla") - Nir
显示剩余4条评论

52

我创建了自己的函数,从sklearn创建的决策树中提取规则:

import pandas as pd
import numpy as np
from sklearn.tree import DecisionTreeClassifier

# dummy data:
df = pd.DataFrame({'col1':[0,1,2,3],'col2':[3,4,5,6],'dv':[0,1,0,1]})

# create decision tree
dt = DecisionTreeClassifier(max_depth=5, min_samples_leaf=1)
dt.fit(df.ix[:,:2], df.dv)

这个函数首先从节点开始(由子数组中的-1标识),然后递归地查找父节点。我称之为节点的“血统”。在此过程中,我获取了创建if/then/else SAS逻辑所需的值:

def get_lineage(tree, feature_names):
     left      = tree.tree_.children_left
     right     = tree.tree_.children_right
     threshold = tree.tree_.threshold
     features  = [feature_names[i] for i in tree.tree_.feature]

     # get ids of child nodes
     idx = np.argwhere(left == -1)[:,0]     

     def recurse(left, right, child, lineage=None):          
          if lineage is None:
               lineage = [child]
          if child in left:
               parent = np.where(left == child)[0].item()
               split = 'l'
          else:
               parent = np.where(right == child)[0].item()
               split = 'r'

          lineage.append((parent, split, threshold[parent], features[parent]))

          if parent == 0:
               lineage.reverse()
               return lineage
          else:
               return recurse(left, right, parent, lineage)

     for child in idx:
          for node in recurse(left, right, child):
               print node
下面的元组集合包含了我创建SAS if/then/else语句所需的所有内容。我不喜欢在SAS中使用do代码块,这就是为什么我会描述一个节点整个路径的逻辑。元组后面的单个整数是路径中终端节点的ID。所有前面的元组组合起来创建了那个节点。
In [1]: get_lineage(dt, df.columns)
(0, 'l', 0.5, 'col1')
1
(0, 'r', 0.5, 'col1')
(2, 'l', 4.5, 'col2')
3
(0, 'r', 0.5, 'col1')
(2, 'r', 4.5, 'col2')
(4, 'l', 2.5, 'col1')
5
(0, 'r', 0.5, 'col1')
(2, 'r', 4.5, 'col2')
(4, 'r', 2.5, 'col1')
6

示例树的GraphViz输出


这种树的类型是否正确,因为col1再次出现,一个是col1<=0.50000,另一个是col1<=2.5000。如果是,这是库中使用的任何递归类型吗? - jayant singh
右分支将具有介于(0.5,2.5]之间的记录。这些树是使用递归分区构建的。没有任何阻止变量被多次选择的限制。 - Zelazny7
好的,你能解释一下递归部分发生了什么吗?因为我在我的代码中使用了它,并且看到了类似的结果。 - jayant singh
如何修改此代码以获取类和规则,并以数据框的形式呈现? - Soumya Boral

46

Scikit learn在2019年5月的0.21版本中引入了一种美味的新方法,称为export_text,用于从树中提取规则。这里是文档链接。现在不再需要创建自定义函数。

一旦您拟合了模型,您只需要两行代码。首先导入export_text

from sklearn.tree import export_text

其次, 创建一个包含规则的对象。为了使规则更易读,使用feature_names参数并传递一个特征名列表。例如,如果您的模型名称为model,并且您的特征以数据帧X_train的形式命名,您可以创建一个名为tree_rules的对象:

tree_rules = export_text(model, feature_names=list(X_train.columns))

然后只需打印或保存tree_rules。您的输出将如下所示:

|--- Age <= 0.63
|   |--- EstimatedSalary <= 0.61
|   |   |--- Age <= -0.16
|   |   |   |--- class: 0
|   |   |--- Age >  -0.16
|   |   |   |--- EstimatedSalary <= -0.06
|   |   |   |   |--- class: 0
|   |   |   |--- EstimatedSalary >  -0.06
|   |   |   |   |--- EstimatedSalary <= 0.40
|   |   |   |   |   |--- EstimatedSalary <= 0.03
|   |   |   |   |   |   |--- class: 1

39

我修改了由Zelazny7提交的代码来打印一些伪代码:

def get_code(tree, feature_names):
        left      = tree.tree_.children_left
        right     = tree.tree_.children_right
        threshold = tree.tree_.threshold
        features  = [feature_names[i] for i in tree.tree_.feature]
        value = tree.tree_.value

        def recurse(left, right, threshold, features, node):
                if (threshold[node] != -2):
                        print "if ( " + features[node] + " <= " + str(threshold[node]) + " ) {"
                        if left[node] != -1:
                                recurse (left, right, threshold, features,left[node])
                        print "} else {"
                        if right[node] != -1:
                                recurse (left, right, threshold, features,right[node])
                        print "}"
                else:
                        print "return " + str(value[node])

        recurse(left, right, threshold, features, 0)

如果您在相同的示例上调用get_code(dt, df.columns),您将获得:

if ( col1 <= 0.5 ) {
return [[ 1.  0.]]
} else {
if ( col2 <= 4.5 ) {
return [[ 0.  1.]]
} else {
if ( col1 <= 2.5 ) {
return [[ 1.  0.]]
} else {
return [[ 0.  1.]]
}
}
}

1
你能告诉我在上面的输出中,返回语句中的[[1.0]]具体是什么意思吗?虽然我不是Python专家,但正在处理类似的事情。如果您能提供一些详细信息,那对我来说会更容易些。 - Subhradip Bose
1
@user3156186 这意味着在类 '0' 中有一个对象,而在类 '1' 中没有任何对象。 - Daniele
1
@Daniele,你知道类是按什么顺序排列的吗?我猜应该是按字母数字顺序,但我在任何地方都没有找到确认。 - IanS
谢谢!对于边界情况,即阈值实际上为-2的情况,我们可能需要将(threshold[node] != -2)更改为(left[node] != -1)(类似于下面获取子节点ID的方法)。 - tlingf
1
@Daniele,你有什么办法可以让你的函数“get_code”返回一个值而不是打印它,因为我需要将其发送到另一个函数吗? - RoyaumeIX
@Daniele,您能否检查一下这个问题:https://dev59.com/Sanka4cB1Zd3GeqPKDKu - user9238790

19

0.18.0 版本中,有一个新的 DecisionTreeClassifier 方法 decision_path。开发人员提供了一份详细(有文档说明)演示文稿

演示文稿中打印树结构的第一部分代码似乎没问题。然而,我修改了第二部分代码来查询一个样本。我的更改用 # <-- 标记出来。

编辑:#8653#10951 的推送请求指出错误后,下面代码中用 # <-- 标记的更改已在演示文稿链接中得到更新。现在更容易跟随了。

sample_id = 0
node_index = node_indicator.indices[node_indicator.indptr[sample_id]:
                                    node_indicator.indptr[sample_id + 1]]

print('Rules used to predict sample %s: ' % sample_id)
for node_id in node_index:

    if leave_id[sample_id] == node_id:  # <-- changed != to ==
        #continue # <-- comment out
        print("leaf node {} reached, no decision here".format(leave_id[sample_id])) # <--

    else: # < -- added else to iterate through decision nodes
        if (X_test[sample_id, feature[node_id]] <= threshold[node_id]):
            threshold_sign = "<="
        else:
            threshold_sign = ">"

        print("decision id node %s : (X[%s, %s] (= %s) %s %s)"
              % (node_id,
                 sample_id,
                 feature[node_id],
                 X_test[sample_id, feature[node_id]], # <-- changed i to sample_id
                 threshold_sign,
                 threshold[node_id]))

Rules used to predict sample 0: 
decision id node 0 : (X[0, 3] (= 2.4) > 0.800000011921)
decision id node 2 : (X[0, 2] (= 5.1) > 4.94999980927)
leaf node 4 reached, no decision here
< p >将< code >sample_id< /code >更改为其他样本以查看决策路径。在处理示例时,我没有向开发人员询问这些更改,只是更符合直觉。< /p >

你,我的朋友,是一个传奇!有什么想法可以为那个特定的样本绘制决策树吗?非常感谢您的帮助。 - user9238790
1
谢谢Victor,最好单独提出这个问题,因为绘图要求可能会根据用户的需求而有所不同。如果您提供想要输出的外观的想法,您可能会得到很好的回应。 - Kevin
嗨,凯文,我创建了这个问题 https://stackoverflow.com/questions/48888893/visualize-the-tree-for-a-specific-sample-in-decision-tree-rather-than-the-whole。 - user9238790
请您帮忙查看以下问题:https://stackoverflow.com/questions/52654280/visualizing-decision-tree-not-using-graphviz-web - Alexander Chervov
请问能否解释一下叫做node_index的部分,我不太理解它的作用是什么? - Anindya Sankar Dey

13
from StringIO import StringIO
out = StringIO()
out = tree.export_graphviz(clf, out_file=out)
print out.getvalue()

您可以看到一个有向图树。然后,clf.tree_.featureclf.tree_.value 分别是节点拆分特征和节点值的数组。您可以从这个github源代码中了解更多细节。


2
是的,我知道如何绘制树——但我需要更多的文本版本——规则。类似这样的东西:http://orange.biolab.si/docs/latest/reference/rst/Orange.classification.tree/#printing-the-tree - Dror Hilman

8

我需要一种更人性化的决策树规则格式。我正在构建开源AutoML Python包,许多MLJAR用户想要查看树中的确切规则。

这就是为什么我基于paulkernfeld的回答实现了一个函数。

def get_rules(tree, feature_names, class_names):
    tree_ = tree.tree_
    feature_name = [
        feature_names[i] if i != _tree.TREE_UNDEFINED else "undefined!"
        for i in tree_.feature
    ]

    paths = []
    path = []
    
    def recurse(node, path, paths):
        
        if tree_.feature[node] != _tree.TREE_UNDEFINED:
            name = feature_name[node]
            threshold = tree_.threshold[node]
            p1, p2 = list(path), list(path)
            p1 += [f"({name} <= {np.round(threshold, 3)})"]
            recurse(tree_.children_left[node], p1, paths)
            p2 += [f"({name} > {np.round(threshold, 3)})"]
            recurse(tree_.children_right[node], p2, paths)
        else:
            path += [(tree_.value[node], tree_.n_node_samples[node])]
            paths += [path]
            
    recurse(0, path, paths)

    # sort by samples count
    samples_count = [p[-1][1] for p in paths]
    ii = list(np.argsort(samples_count))
    paths = [paths[i] for i in reversed(ii)]
    
    rules = []
    for path in paths:
        rule = "if "
        
        for p in path[:-1]:
            if rule != "if ":
                rule += " and "
            rule += str(p)
        rule += " then "
        if class_names is None:
            rule += "response: "+str(np.round(path[-1][0][0][0],3))
        else:
            classes = path[-1][0][0]
            l = np.argmax(classes)
            rule += f"class: {class_names[l]} (proba: {np.round(100.0*classes[l]/np.sum(classes),2)}%)"
        rule += f" | based on {path[-1][1]:,} samples"
        rules += [rule]
        
    return rules

规则按照每个规则分配的训练样本数量进行排序。对于每个规则,都有关于分类任务预测类名和预测概率的信息。对于回归任务,只打印有关预测值的信息。

示例

from sklearn import datasets
from sklearn.tree import DecisionTreeRegressor
from sklearn import tree
from sklearn.tree import _tree

# Prepare the data data
boston = datasets.load_boston()
X = boston.data
y = boston.target

# Fit the regressor, set max_depth = 3
regr = DecisionTreeRegressor(max_depth=3, random_state=1234)
model = regr.fit(X, y)

# Print rules
rules = get_rules(regr, boston.feature_names, None)
for r in rules:
    print(r)

印刷的规则:

if (RM <= 6.941) and (LSTAT <= 14.4) and (DIS > 1.385) then response: 22.905 | based on 250 samples
if (RM <= 6.941) and (LSTAT > 14.4) and (CRIM <= 6.992) then response: 17.138 | based on 101 samples
if (RM <= 6.941) and (LSTAT > 14.4) and (CRIM > 6.992) then response: 11.978 | based on 74 samples
if (RM > 6.941) and (RM <= 7.437) and (NOX <= 0.659) then response: 33.349 | based on 43 samples
if (RM > 6.941) and (RM > 7.437) and (PTRATIO <= 19.65) then response: 45.897 | based on 29 samples
if (RM <= 6.941) and (LSTAT <= 14.4) and (DIS <= 1.385) then response: 45.58 | based on 5 samples
if (RM > 6.941) and (RM <= 7.437) and (NOX > 0.659) then response: 14.4 | based on 3 samples
if (RM > 6.941) and (RM > 7.437) and (PTRATIO > 19.65) then response: 21.9 | based on 1 samples

我在我的文章中总结了从决策树中提取规则的三种方法:使用Scikit-Learn和Python从决策树中提取规则的3种方法

2
请记得导入:from sklearn.tree import _tree - JP Zhang
和 "import numpy as np" - bhamadicharef
@pplonski...我无法使您的代码适用于xgboost而不是DecisionTreeRegressor。如果您能帮忙,我将非常感激,我是一个开始学习Python的MATLAB人。 - bhamadicharef
@bhamadicharef 这对于xgboost不起作用。xgboost是树的集合。首先,您需要从xgboost中提取所选树。您需要将其存储在sklearn-tree格式中,然后才能使用上面的代码。 - pplonski
@pplonski,我明白你的意思,但是还不太熟悉sklearn-tree格式。如果我有什么有用的东西,我会分享的。我将简单和小规则解析成matlab代码,但是我拥有的模型有3000棵深度为6的树,所以像你这样强大而特别适合递归的方法非常有用。学习中... - bhamadicharef

6

现在您可以使用export_text功能。

from sklearn.tree import export_text

r = export_text(loan_tree, feature_names=(list(X_train.columns)))
print(r)

这是一个来自[sklearn][1]的完整示例:

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.tree import export_text
iris = load_iris()
X = iris['data']
y = iris['target']
decision_tree = DecisionTreeClassifier(random_state=0, max_depth=2)
decision_tree = decision_tree.fit(X, y)
r = export_text(decision_tree, feature_names=iris['feature_names'])
print(r)

5

这是你所需要的代码

我已经修改了最受欢迎的代码,以正确地在Jupyter Notebook Python 3中缩进

import numpy as np
from sklearn.tree import _tree

def tree_to_code(tree, feature_names):
    tree_ = tree.tree_
    feature_name = [feature_names[i] 
                    if i != _tree.TREE_UNDEFINED else "undefined!" 
                    for i in tree_.feature]
    print("def tree({}):".format(", ".join(feature_names)))

    def recurse(node, depth):
        indent = "    " * depth
        if tree_.feature[node] != _tree.TREE_UNDEFINED:
            name = feature_name[node]
            threshold = tree_.threshold[node]
            print("{}if {} <= {}:".format(indent, name, threshold))
            recurse(tree_.children_left[node], depth + 1)
            print("{}else:  # if {} > {}".format(indent, name, threshold))
            recurse(tree_.children_right[node], depth + 1)
        else:
            print("{}return {}".format(indent, np.argmax(tree_.value[node])))

    recurse(0, 1)

4
我一直在研究这个,但我需要以这种格式编写规则。
if A>0.4 then if B<0.2 then if C>0.8 then class='X' 

我采用了@paulkernfeld的答案(感谢),你可以根据自己的需要进行个性化设置。

def tree_to_code(tree, feature_names, Y):
    tree_ = tree.tree_
    feature_name = [
        feature_names[i] if i != _tree.TREE_UNDEFINED else "undefined!"
        for i in tree_.feature
    ]
    pathto=dict()

    global k
    k = 0
    def recurse(node, depth, parent):
        global k
        indent = "  " * depth

        if tree_.feature[node] != _tree.TREE_UNDEFINED:
            name = feature_name[node]
            threshold = tree_.threshold[node]
            s= "{} <= {} ".format( name, threshold, node )
            if node == 0:
                pathto[node]=s
            else:
                pathto[node]=pathto[parent]+' & ' +s

            recurse(tree_.children_left[node], depth + 1, node)
            s="{} > {}".format( name, threshold)
            if node == 0:
                pathto[node]=s
            else:
                pathto[node]=pathto[parent]+' & ' +s
            recurse(tree_.children_right[node], depth + 1, node)
        else:
            k=k+1
            print(k,')',pathto[parent], tree_.value[node])
    recurse(0, 1, 0)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接