Cómo explorar un árbol de decisiones construido usando scikit learn

Estoy construyendo un árbol de decisiones usando

clf = tree.DecisionTreeClassifier() clf = clf.fit(X_train, Y_train) 

Todo esto funciona bien. Sin embargo, ¿cómo exploro el árbol de decisiones?

Por ejemplo, ¿cómo encuentro qué entradas de X_train aparecen en una hoja en particular?

Necesitas usar el método de predicción.

Después de entrenar el árbol, alimenta los valores X para predecir su salida.

 from sklearn.datasets import load_iris from sklearn.tree import DecisionTreeClassifier clf = DecisionTreeClassifier(random_state=0) iris = load_iris() tree = clf.fit(iris.data, iris.target) tree.predict(iris.data) 

salida:

 >>> tree.predict(iris.data) array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]) 

Para obtener detalles sobre la estructura del árbol, podemos usar el tree_.__getstate__()

Estructura de árbol traducida en una imagen de “arte ASCII”

  0 _____________ 1 2 ______________ 3 12 _______ _______ 4 7 13 16 ___ ______ _____ 5 6 8 9 14 15 _____ 10 11 

La estructura de un árbol como una matriz.

 In [38]: tree.tree_.__getstate__()['nodes'] Out[38]: array([(1, 2, 3, 0.800000011920929, 0.6666666666666667, 150, 150.0), (-1, -1, -2, -2.0, 0.0, 50, 50.0), (3, 12, 3, 1.75, 0.5, 100, 100.0), (4, 7, 2, 4.949999809265137, 0.16803840877914955, 54, 54.0), (5, 6, 3, 1.6500000953674316, 0.04079861111111116, 48, 48.0), (-1, -1, -2, -2.0, 0.0, 47, 47.0), (-1, -1, -2, -2.0, 0.0, 1, 1.0), (8, 9, 3, 1.5499999523162842, 0.4444444444444444, 6, 6.0), (-1, -1, -2, -2.0, 0.0, 3, 3.0), (10, 11, 2, 5.449999809265137, 0.4444444444444444, 3, 3.0), (-1, -1, -2, -2.0, 0.0, 2, 2.0), (-1, -1, -2, -2.0, 0.0, 1, 1.0), (13, 16, 2, 4.850000381469727, 0.042533081285444196, 46, 46.0), (14, 15, 1, 3.0999999046325684, 0.4444444444444444, 3, 3.0), (-1, -1, -2, -2.0, 0.0, 2, 2.0), (-1, -1, -2, -2.0, 0.0, 1, 1.0), (-1, -1, -2, -2.0, 0.0, 43, 43.0)], dtype=[('left_child', ' 

Dónde:

  • El primer nodo [0] es el nodo raíz.
  • Los nodos internos tienen left_child y right_child que hacen referencia a nodos con valores positivos, y mayores que el nodo actual.
  • las hojas tienen un valor -1 para los nodos secundarios izquierdo y derecho.
  • Los nodos 1,5,6, 8,10,11,14,15,16 son hojas.
  • la estructura del nodo se construye utilizando el algoritmo de búsqueda en profundidad.
  • el campo de característica nos dice cuál de las características de iris.data se usó en el nodo para determinar la ruta para esta muestra.
  • el umbral nos dice el valor utilizado para evaluar la dirección en función de la función.
  • la impureza alcanza 0 en las hojas ... ya que todas las muestras están en la misma clase una vez que se alcanza la hoja.
  • n_node_samples nos dice cuántas muestras llegan a cada hoja.

Usando esta información, podríamos realizar un seguimiento trivial de cada X de muestra a la hoja donde finalmente aterriza siguiendo las reglas de clasificación y los umbrales en un script. Además, los n_node_samples nos permitirían realizar pruebas unitarias asegurando que cada nodo obtenga el número correcto de muestras. Luego, utilizando la salida de tree.predict, podríamos asignar cada hoja a la clase asociada.

NOTA: Esto no es una respuesta, solo un indicio de posibles soluciones.

Me encontré con un problema similar recientemente en mi proyecto. Mi objective es extraer la correspondiente cadena de decisiones para algunas muestras particulares. Creo que su problema es un subconjunto mío, ya que solo necesita registrar el último paso en la cadena de decisiones.

Hasta ahora, parece que la única solución viable es escribir un método de predict personalizado en Python para realizar un seguimiento de las decisiones en el camino. La razón es que el método de predict provisto por scikit-learn no puede hacer esto fuera de la caja (que yo sepa). Y para empeorar las cosas, es una envoltura para la implementación de C que es bastante difícil de personalizar.

La personalización está bien para mi problema, ya que estoy tratando con un conjunto de datos desequilibrado, y las muestras que me importan (las positivas) son raras. Así que puedo filtrarlos primero usando el predict Sklearn y luego obtener la cadena de decisiones usando mi personalización.

Sin embargo, esto puede no funcionar para usted si tiene un conjunto de datos grande. Porque si analiza el árbol y predice en Python, se ejecutará lento en la velocidad de Python y no se escalará (fácilmente). Es posible que tenga que recurrir a la personalización de la implementación de C.

El siguiente código debe producir un gráfico de sus diez características principales:

 import numpy as np import matplotlib.pyplot as plt importances = clf.feature_importances_ std = np.std(clf.feature_importances_,axis=0) indices = np.argsort(importances)[::-1] # Print the feature ranking print("Feature ranking:") for f in range(10): print("%d. feature %d (%f)" % (f + 1, indices[f], importances[indices[f]])) # Plot the feature importances of the forest plt.figure() plt.title("Feature importances") plt.bar(range(10), importances[indices], color="r", yerr=std[indices], align="center") plt.xticks(range(10), indices) plt.xlim([-1, 10]) plt.show() 

Tomado de aquí y modificado ligeramente para ajustarse al DecisionTreeClassifier .

Esto no le ayuda exactamente a explorar el árbol, pero sí le informa sobre el árbol.

Este código hará exactamente lo que quieras. Aquí, n es el número de observaciones en X_train . Al final, el (n, number_of_leaves) matriz del leaf_observations mantiene en cada columna los valores booleanos para indexar en X_train para obtener las observaciones en cada hoja. Cada columna de leaf_observations corresponde a un elemento en las leaves , que tiene los ID de nodo para las hojas.

 # get the nodes which are leaves leaves = clf.tree_.children_left == -1 leaves = np.arange(0,clf.tree_.node_count)[leaves] # loop through each leaf and figure out the data in it leaf_observations = np.zeros((n,len(leaves)),dtype=bool) # build a simpler tree as a nested list: [split feature, split threshold, left node, right node] thistree = [clf.tree_.feature.tolist()] thistree.append(clf.tree_.threshold.tolist()) thistree.append(clf.tree_.children_left.tolist()) thistree.append(clf.tree_.children_right.tolist()) # get the decision rules for each leaf node & apply them for (ind,nod) in enumerate(leaves): # get the decision rules in numeric list form rules = [] RevTraverseTree(thistree, nod, rules) # convert & apply to the data by sequentially &ing the rules thisnode = np.ones(n,dtype=bool) for rule in rules: if rule[1] == 1: thisnode = np.logical_and(thisnode,X_train[:,rule[0]] > rule[2]) else: thisnode = np.logical_and(thisnode,X_train[:,rule[0]] <= rule[2]) # get the observations that obey all the rules - they are the ones in this leaf node leaf_observations[:,ind] = thisnode 

Esto necesita la función de ayuda definida aquí, que recursivamente atraviesa el árbol a partir de un nodo específico para construir las reglas de decisión.

 def RevTraverseTree(tree, node, rules): ''' Traverase an skl decision tree from a node (presumbly a leaf node) up to the top, building the decision rules. The rules should be input as an empty list, which will be modified in place. The result is a nested list of tuples: (feature, direction (left=-1), threshold). The "tree" is a nested list of simplified tree attributes: [split feature, split threshold, left node, right node] ''' # now find the node as either a left or right child of something # first try to find it as a left node try: prevnode = tree[2].index(node) leftright = -1 except ValueError: # failed, so find it as a right node - if this also causes an exception, something's really f'd up prevnode = tree[3].index(node) leftright = 1 # now let's get the rule that caused prevnode to -> node rules.append((tree[0][prevnode],leftright,tree[1][prevnode])) # if we've not yet reached the top, go up the tree one more step if prevnode != 0: RevTraverseTree(tree, prevnode, rules) 

He cambiado un poco lo que el Dr. Drew publicó.
El siguiente código, dado un dataframe y el árbol de decisión después de ser ajustado, devuelve:

  • rules_list : una lista de reglas
  • values_path : una lista de entradas (entradas para cada clase que atraviesa la ruta)

     import numpy as np import pandas as pd from sklearn.tree import DecisionTreeClassifier def get_rules(dtc, df): rules_list = [] values_path = [] values = dtc.tree_.value def RevTraverseTree(tree, node, rules, pathValues): ''' Traverase an skl decision tree from a node (presumbly a leaf node) up to the top, building the decision rules. The rules should be input as an empty list, which will be modified in place. The result is a nested list of tuples: (feature, direction (left=-1), threshold). The "tree" is a nested list of simplified tree attributes: [split feature, split threshold, left node, right node] ''' # now find the node as either a left or right child of something # first try to find it as a left node try: prevnode = tree[2].index(node) leftright = '<=' pathValues.append(values[prevnode]) except ValueError: # failed, so find it as a right node - if this also causes an exception, something's really f'd up prevnode = tree[3].index(node) leftright = '>' pathValues.append(values[prevnode]) # now let's get the rule that caused prevnode to -> node p1 = df.columns[tree[0][prevnode]] p2 = tree[1][prevnode] rules.append(str(p1) + ' ' + leftright + ' ' + str(p2)) # if we've not yet reached the top, go up the tree one more step if prevnode != 0: RevTraverseTree(tree, prevnode, rules, pathValues) # get the nodes which are leaves leaves = dtc.tree_.children_left == -1 leaves = np.arange(0,dtc.tree_.node_count)[leaves] # build a simpler tree as a nested list: [split feature, split threshold, left node, right node] thistree = [dtc.tree_.feature.tolist()] thistree.append(dtc.tree_.threshold.tolist()) thistree.append(dtc.tree_.children_left.tolist()) thistree.append(dtc.tree_.children_right.tolist()) # get the decision rules for each leaf node & apply them for (ind,nod) in enumerate(leaves): # get the decision rules rules = [] pathValues = [] RevTraverseTree(thistree, nod, rules, pathValues) pathValues.insert(0, values[nod]) pathValues = list(reversed(pathValues)) rules = list(reversed(rules)) rules_list.append(rules) values_path.append(pathValues) return (rules_list, values_path) 

Sigue un ejemplo:

 df = pd.read_csv('df.csv') X = df[df.columns[:-1]] y = df['classification'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) dtc = DecisionTreeClassifier(max_depth=2) dtc.fit(X_train, y_train) 

El Árbol de decisiones ajustado ha generado el siguiente árbol: Árbol de decisiones con ancho 2

En este punto, simplemente llamando a la función:

 get_rules(dtc, df) 

Esto es lo que devuelve la función:

 rules = [ ['first <= 63.5', 'first <= 43.5'], ['first <= 63.5', 'first > 43.5'], ['first > 63.5', 'second <= 19.700000762939453'], ['first > 63.5', 'second > 19.700000762939453'] ] values = [ [array([[ 1568., 1569.]]), array([[ 636., 241.]]), array([[ 284., 57.]])], [array([[ 1568., 1569.]]), array([[ 636., 241.]]), array([[ 352., 184.]])], [array([[ 1568., 1569.]]), array([[ 932., 1328.]]), array([[ 645., 620.]])], [array([[ 1568., 1569.]]), array([[ 932., 1328.]]), array([[ 287., 708.]])] ] 

Obviamente, en los valores, para cada ruta, también están los valores de hoja.

Creo que una opción fácil sería utilizar el método de aplicación del árbol de decisiones capacitado. Entrene el árbol, aplique el traindata y cree una tabla de búsqueda a partir de los índices devueltos:

 import numpy as np from sklearn.tree import DecisionTreeClassifier from sklearn.datasets import load_iris iris = load_iris() clf = DecisionTreeClassifier() clf = clf.fit(iris.data, iris.target) # apply training data to decision tree leaf_indices = clf.apply(iris.data) lookup = {} # build lookup table for i, leaf_index in enumerate(leaf_indices): try: lookup[leaf_index].append(iris.data[i]) except KeyError: lookup[leaf_index] = [] lookup[leaf_index].append(iris.data[i]) # test unkown_sample = [[4., 3.1, 6.1, 1.2]] index = clf.apply(unkown_sample) print(lookup[index[0]]) 

¿Ha intentado volcar su DecisionTree en un archivo .dot de graphviz ‘[1] y luego cargarlo con graph_tool [2] .:

 import numpy as np from sklearn.tree import DecisionTreeClassifier from sklearn.datasets import load_iris from graph_tool.all import * iris = load_iris() clf = DecisionTreeClassifier() clf = clf.fit(iris.data, iris.target) tree.export_graphviz(clf,out_file='tree.dot') #load graph with graph_tool and explore structure as you please g = load_graph('tree.dot') for v in g.vertices(): for e in v.out_edges(): print(e) for w in v.out_neighbours(): print(w) 

[1] http://scikit-learn.org/stable/modules/generated/sklearn.tree.export_graphviz.html

[2] https://graph-tool.skewed.de/