webentwicklung-frage-antwort-db.com.de

Wie extrahiere ich die Entscheidungsregeln aus dem Entscheidungsbaum von scikit-learn?

Kann ich die zugrunde liegenden Entscheidungsregeln (oder "Entscheidungspfade") aus einem trainierten Baum in einem Entscheidungsbaum als Textliste extrahieren?

So etwas wie: 

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

Danke für Ihre Hilfe.

108
Dror Hilman

Ich glaube, dass diese Antwort korrekter ist als die anderen Antworten hier:

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)

Dies gibt eine gültige Python-Funktion aus. Hier ist eine Beispielausgabe für einen Baum, der versucht, seine Eingabe zurückzugeben, eine Zahl zwischen 0 und 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.]]

Hier sind einige Stolpersteine, die ich in anderen Antworten sehe:

  1. Die Verwendung von tree_.threshold == -2 zur Entscheidung, ob ein Knoten ein Blatt ist, ist keine gute Idee. Was ist, wenn es sich um einen echten Entscheidungsknoten mit einer Schwelle von -2 handelt? Stattdessen sollten Sie sich tree.feature oder tree.children_* anschauen.
  2. Die Zeile features = [feature_names[i] for i in tree_.feature] stürzt mit meiner Version von sklearn ab, da einige Werte von tree.tree_.feature -2 sind (speziell für Blattknoten).
  3. Es müssen nicht mehrere if-Anweisungen in der rekursiven Funktion verwendet werden, nur eine ist in Ordnung.
91
paulkernfeld

Ich habe meine eigene Funktion erstellt, um die Regeln aus den von sklearn erstellten Entscheidungsbäumen zu extrahieren:

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)

Diese Funktion beginnt zunächst mit den Knoten (in den untergeordneten Arrays mit -1 gekennzeichnet) und sucht dann rekursiv die übergeordneten Elemente. Ich nenne das eine "Linie" eines Knotens. Dabei greife ich mir die Werte zu, die ich erstellen muss, wenn/then/else SAS logic:

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

Die folgenden Tupel-Sets enthalten alles, was ich zum Erstellen von SAS if/then/else-Anweisungen benötige. Ich mag es nicht, do-Blöcke in SAS zu verwenden. Deshalb erstelle ich eine Logik, die den gesamten Pfad eines Knotens beschreibt. Die einzelne Ganzzahl nach den Tupeln ist die ID des Endknotens in einem Pfad. Alle vorhergehenden Tupel kombinieren, um diesen Knoten zu erstellen.

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 output of example tree

45
Zelazny7

Ich habe den von Zelazny7 übergebenen Code geändert, um einen Pseudocode zu drucken:

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)

wenn Sie in demselben Beispiel get_code(dt, df.columns) aufrufen, erhalten Sie:

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.]]
}
}
}
33
Daniele

Es gibt eine neue DecisionTreeClassifier Methode, decision_path, in der Version 0.18.0 . Die Entwickler bieten ein umfangreiches (gut dokumentiertes) Walkthrough .

Der erste Codeabschnitt in der exemplarischen Vorgehensweise, der die Baumstruktur druckt, scheint in Ordnung zu sein. Ich habe jedoch den Code im zweiten Abschnitt geändert, um ein Beispiel abzufragen. Meine mit # <-- gekennzeichneten Änderungen

Edit Die Änderungen, die im nachstehenden Code mit # <-- gekennzeichnet sind, wurden seitdem in der exemplarischen Verknüpfung aktualisiert, nachdem die Fehler in den Pull-Anforderungen # 8653 und # 10951 angezeigt wurden. Es ist jetzt viel einfacher zu folgen. 

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

Ändern Sie den sample_id, um die Entscheidungspfade für andere Beispiele anzuzeigen. Ich habe die Entwickler nicht nach diesen Änderungen gefragt, sondern wirkte beim Durcharbeiten des Beispiels intuitiver.

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

Sie können einen Digraphenbaum sehen. Dann sind clf.tree_.feature und clf.tree_.value ein Array von Knoten, die Merkmals- und Array von Knotenwerten aufteilen. Sie können auf weitere Details von diesem github source verweisen. 

10
lennon310

Die folgenden Codes sind mein Ansatz unter Anaconda Python 2.7 sowie der Paketname "pydot-ng" zum Erstellen einer PDF -Datei mit Entscheidungsregeln. Ich hoffe es ist hilfreich.

from sklearn import tree

clf = tree.DecisionTreeClassifier(max_leaf_nodes=n)
clf_ = clf.fit(X, data_y)

feature_names = X.columns
class_name = clf_.classes_.astype(int).astype(str)

def output_pdf(clf_, name):
    from sklearn import tree
    from sklearn.externals.six import StringIO
    import pydot_ng as pydot
    dot_data = StringIO()
    tree.export_graphviz(clf_, out_file=dot_data,
                         feature_names=feature_names,
                         class_names=class_name,
                         filled=True, rounded=True,
                         special_characters=True,
                          node_ids=1,)
    graph = pydot.graph_from_dot_data(dot_data.getvalue())
    graph.write_pdf("%s.pdf"%name)

output_pdf(clf_, name='filename%s'%n)

eine Baumgrafikshow hier

2
TED Zhao

Nur weil alle so hilfsbereit waren, füge ich einfach Zelazny7 und Danieles schöne Lösungen hinzu. Dies ist für Python 2.7, mit Registerkarten, um es lesbarer zu machen:

def get_code(tree, feature_names, tabdepth=0):
    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, tabdepth=0):
            if (threshold[node] != -2):
                    print '\t' * tabdepth,
                    print "if ( " + features[node] + " <= " + str(threshold[node]) + " ) {"
                    if left[node] != -1:
                            recurse (left, right, threshold, features,left[node], tabdepth+1)
                    print '\t' * tabdepth,
                    print "} else {"
                    if right[node] != -1:
                            recurse (left, right, threshold, features,right[node], tabdepth+1)
                    print '\t' * tabdepth,
                    print "}"
            else:
                    print '\t' * tabdepth,
                    print "return " + str(value[node])

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

Dies baut auf der Antwort von @paulkernfeld auf. Wenn Sie ein Dataframe X mit Ihren Features und ein Zieldatenframe y mit Ihren Antworten haben und Sie eine Vorstellung davon bekommen möchten, welcher y-Wert in welchem ​​Knoten endete (und auch entsprechend plotten) soll, können Sie Folgendes tun:

    def tree_to_code(tree, feature_names):
        codelines = []
        codelines.append('def get_cat(X_tmp):\n')
        codelines.append('   catout = []\n')
        codelines.append('   for codelines in range(0,X_tmp.shape[0]):\n')
        codelines.append('      Xin = X_tmp.iloc[codelines]\n')
        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]
                codelines.append ('{}if Xin["{}"] <= {}:\n'.format(indent, name, threshold))
                recurse(tree_.children_left[node], depth + 1)
                codelines.append( '{}else:  # if Xin["{}"] > {}\n'.format(indent, name, threshold))
                recurse(tree_.children_right[node], depth + 1)
            else:
                codelines.append( '{}mycat = {}\n'.format(indent, node))

        recurse(0, 1)
        codelines.append('      catout.append(mycat)\n')
        codelines.append('   return pd.DataFrame(catout,index=X_tmp.index,columns=["category"])\n')
        codelines.append('node_ids = get_cat(X)\n')
        return codelines
    mycode = tree_to_code(clf,X.columns.values)

    # now execute the function and obtain the dataframe with all nodes
    exec(''.join(mycode))
    node_ids = [int(x[0]) for x in node_ids.values]
    node_ids2 = pd.DataFrame(node_ids)

    print('make plot')
    import matplotlib.cm as cm
    colors = cm.Rainbow(np.linspace(0, 1, 1+max( list(set(node_ids)))))
    #plt.figure(figsize=cm2inch(24, 21))
    for i in list(set(node_ids)):
        plt.plot(y[node_ids2.values==i],'o',color=colors[i], label=str(i))  
    mytitle = ['y colored by node']
    plt.title(mytitle ,fontsize=14)
    plt.xlabel('my xlabel')
    plt.ylabel(tagname)
    plt.xticks(rotation=70)       
    plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.00), shadow=True, ncol=9)
    plt.tight_layout()
    plt.show()
    plt.close 

nicht die eleganteste Version, aber es macht den Job ... 

2
horseshoe

Scikit learn hat in Version 0.21 (Mai 2019) eine köstliche neue Methode namens export_text eingeführt, um die Regeln aus einem Baum zu extrahieren. Dokumentation hier . Es ist nicht mehr erforderlich, eine benutzerdefinierte Funktion zu erstellen.

Sobald Sie Ihr Modell angepasst haben, benötigen Sie nur zwei Codezeilen. Importieren Sie zunächst export_text:

from sklearn.tree.export import export_text

Zweitens erstellen Sie ein Objekt, das Ihre Regeln enthält. Verwenden Sie das Argument feature_names, um die Regeln besser lesbar zu machen, und übergeben Sie eine Liste Ihrer Feature-Namen. Wenn Ihr Modell beispielsweise model heißt und Ihre Features in einem Datenrahmen mit dem Namen X_train benannt sind, können Sie ein Objekt mit dem Namen tree_rules erstellen:

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

Dann drucken oder speichern Sie einfach tree_rules. Ihre Ausgabe sieht folgendermaßen aus:

|--- 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
1
yzerman

Ich habe das durchgemacht, aber ich brauchte die Regeln, um in diesem Format geschrieben zu werden 

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

Also habe ich die Antwort von @paulkernfeld (Danke) angepasst, die Sie an Ihre Bedürfnisse anpassen können

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)
1
Ala Ham

Hier ist eine Funktion, Druckregeln eines Entscheidungsbaums für Scikit-Lernen unter Python 3 und mit Offsets für bedingte Blöcke, um die Struktur lesbarer zu machen:

def print_decision_tree(tree, feature_names=None, offset_unit='    '):
    '''Plots textual representation of rules of a decision tree
    tree: scikit-learn representation of tree
    feature_names: list of feature names. They are set to f1,f2,f3,... if not specified
    offset_unit: a string of offset of the conditional block'''

    left      = tree.tree_.children_left
    right     = tree.tree_.children_right
    threshold = tree.tree_.threshold
    value = tree.tree_.value
    if feature_names is None:
        features  = ['f%d'%i for i in tree.tree_.feature]
    else:
        features  = [feature_names[i] for i in tree.tree_.feature]        

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

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

Anscheinend hat sich schon vor langer Zeit jemand entschieden, die folgenden Funktionen zu den Baumexportfunktionen des offiziellen Scikits hinzuzufügen (die grundsätzlich nur export_graphviz unterstützen).

def export_dict(tree, feature_names=None, max_depth=None) :
    """Export a decision tree in dict format.

Hier ist sein volles Engagement:

https://github.com/scikit-learn/scikit-learn/blob/79bdc8f711d0af225ed6be9fdb708cea9f98a910/sklearn/tree/export.py

Nicht genau sicher, was mit diesem Kommentar passiert ist. Sie können jedoch auch versuchen, diese Funktion zu verwenden.

Ich denke, dies erfordert eine seriöse Dokumentationsanforderung an die guten Leute von scikit-learn, um die sklearn.tree.Tree-API, die die zugrunde liegende Baumstruktur ist, die DecisionTreeClassifier als Attribut tree_ verfügbar macht, ordnungsgemäß zu dokumentieren.

0
Aris Koning

Sie können es auch informativer gestalten, indem Sie unterscheiden, zu welcher Klasse es gehört, oder indem Sie sogar den Ausgabewert angeben.

def print_decision_tree(tree, feature_names, offset_unit='    '):    
left      = tree.tree_.children_left
right     = tree.tree_.children_right
threshold = tree.tree_.threshold
value = tree.tree_.value
if feature_names is None:
    features  = ['f%d'%i for i in tree.tree_.feature]
else:
    features  = [feature_names[i] for i in tree.tree_.feature]        

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

                #To remove values from node
                temp=str(value[node])
                mid=len(temp)//2
                tempx=[]
                tempy=[]
                cnt=0
                for i in temp:
                    if cnt<=mid:
                        tempx.append(i)
                        cnt+=1
                    else:
                        tempy.append(i)
                        cnt+=1
                val_yes=[]
                val_no=[]
                res=[]
                for j in tempx:
                    if j=="[" or j=="]" or j=="." or j==" ":
                        res.append(j)
                    else:
                        val_no.append(j)
                for j in tempy:
                    if j=="[" or j=="]" or j=="." or j==" ":
                        res.append(j)
                    else:
                        val_yes.append(j)
                val_yes = int("".join(map(str, val_yes)))
                val_no = int("".join(map(str, val_no)))

                if val_yes>val_no:
                    print(offset,'\033[1m',"YES")
                    print('\033[0m')
                Elif val_no>val_yes:
                    print(offset,'\033[1m',"NO")
                    print('\033[0m')
                else:
                    print(offset,'\033[1m',"Tie")
                    print('\033[0m')

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

enter image description here

0
Amit Rautray

Code von Zelazny7 wurde geändert, um SQL aus dem Entscheidungsbaum abzurufen.

# SQL from decision tree

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]
     le='<='               
     g ='>'
     # 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)
     print 'case '
     for j,child in enumerate(idx):
        clause=' when '
        for node in recurse(left, right, child):
            if len(str(node))<3:
                continue
            i=node
            if i[1]=='l':  sign=le 
            else: sign=g
            clause=clause+i[3]+sign+str(i[2])+' and '
        clause=clause[:-4]+' then '+str(j)
        print clause
     print 'else 99 end as clusters'
0
Arslán

Hier können Sie den gesamten Baum in einen einzigen (nicht unbedingt zu lesbaren) Python-Ausdruck übersetzen, indem Sie die Bibliothek SKompiler verwenden:

from skompiler import skompile
skompile(dtree.predict).to('python/code')
0
KT.

Verwenden Sie einfach die Funktion aus sklearn.tree

from sklearn.tree import export_graphviz
    export_graphviz(tree,
                out_file = "tree.dot",
                feature_names = tree.columns) //or just ["petal length", "petal width"]

Suchen Sie dann in Ihrem Projektordner nach der Datei tree.dot, kopieren Sie den ALLEN Inhalt und fügen Sie ihn hier ein http://www.webgraphviz.com/ und erstellen Sie Ihr Diagramm :)

0
chainstair