Recuperar líneas de límite de decisión (formato de coordenadas x, y) del árbol de decisión de SKlearn

Estoy intentando crear un gráfico de superficie en una plataforma de visualización externa. Estoy trabajando con el conjunto de datos de iris que se muestra en la página de documentación del árbol de decisiones de sklearn . También estoy usando el mismo enfoque para crear mi gráfico de superficie de decisión. Sin embargo, mi objective final no es matplot lib visual, por lo que desde aquí ingrese los datos a mi software de visualización. Para hacer esto, simplemente llamé a flatten() y tolist() en xx , yy y Z y escribí un archivo JSON que contiene estas listas.

El problema es que cuando bash trazarlo, mi progtwig de visualización falla. Resulta que los datos son demasiado grandes. Cuando se aplana, la longitud de la lista es> 86,000. Esto se debe al hecho de que el tamaño de paso / ttwig es muy pequeño .02 . Así que, esencialmente, se trata de dar pequeños pasos a través del dominio de los datos mínimo y máximo de los datos y trazar / rellenar según sea de acuerdo con las predicciones del modelo. Es como una cuadrícula de píxeles; Reduje el tamaño a una serie de solo 2000 y noté que las coordenadas eran solo líneas que iban y venían (hasta el final abarcaban todo el plano de coordenadas).

Pregunta: ¿Puedo recuperar las coordenadas x, y de las propias líneas de límite de decisión (en lugar de iterar en todo el plano)? Lo ideal es una lista que contenga sólo los puntos de inflexión de cada línea. O, alternativamente, ¿puede haber alguna otra forma completamente diferente de recrear esta ttwig, para que sea más eficiente computacionalmente?

Esto se puede visualizar de alguna manera reemplazando la llamada contourf() con countour() :

introduzca la descripción de la imagen aquí

Simplemente no estoy seguro de cómo recuperar los datos que rigen esas líneas (¿a través de xx , yy y Z o posiblemente por otros medios?).

Nota: No soy exigente con el formato exacto de la lista o la estructura de datos que contiene el formato de las líneas, siempre y cuando sea computacionalmente eficiente. Por ejemplo, para el primer gráfico anterior, algunas áreas rojas son en realidad islas en el espacio de predicción, por lo que podría significar que tendríamos que manejarlo como si fuera su propia línea. Supongo que siempre que la clase esté acoplada con las coordenadas x, y, no debería importar cuántos arreglos (que contienen coordenadas) se usen para capturar los límites de decisión.

Los árboles de decisión no tienen límites muy agradables. Tienen múltiples límites que dividen jerárquicamente el espacio de la característica en regiones rectangulares.

En mi implementación de Node Harvest escribí funciones que analizan los árboles de decisión de scikit y extraen las regiones de decisión. Para esta respuesta, modifiqué partes de ese código para devolver una lista de rectangularjs que corresponden a regiones de decisión de árboles. Debería ser fácil dibujar estos rectangularjs con cualquier biblioteca de trazado. Aquí hay un ejemplo usando 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) 

introduzca la descripción de la imagen aquí

Dondequiera que se encuentren regiones de diferente color hay un límite de decisión. Me imagino que sería posible con un esfuerzo moderado extraer solo estos límites, pero se lo dejaré a cualquiera que esté interesado.

rectangles es una matriz numpy. Cada fila corresponde a un rectángulo y las columnas son [left, right, top, bottom, class] .


Actualización: Aplicación al conjunto de datos Iris.

El conjunto de datos Iris contiene tres clases en lugar de 2, como en el ejemplo. Así que tenemos que agregar otro color a la función plot_areas : color = ['b', 'r', 'g'][int(rect[4])] . Además, el conjunto de datos es de 4 dimensiones (contiene cuatro características) pero solo podemos trazar dos características en 2D. Necesitamos elegir qué características trazar y decir la función decision_area . La función toma dos argumentos x e y : estas son las características que van en los ejes x e y, respectivamente. El valor predeterminado es x=0, y=1 que funciona con cualquier conjunto de datos que tenga más de una función. Sin embargo, en el conjunto de datos Iris, la primera dimensión no es muy interesante, por lo que usaremos un ajuste diferente.

La función decision_areas tampoco conoce la extensión del conjunto de datos. A menudo, el árbol de decisión tiene rangos de decisión abiertos que se extienden hacia el infinito (por ejemplo, cuando la longitud del sépalo es menor que xyz, es la clase B). En este caso, necesitamos reducir artificialmente el rango para el trazado. Elegí -3..3 para el conjunto de datos de ejemplo, pero para el conjunto de datos de iris, otros rangos son apropiados (nunca hay valores negativos, algunas características se extienden más allá de 3).

Aquí trazamos las regiones de decisión sobre las dos últimas características en un rango de 0..7 y 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) 

introduzca la descripción de la imagen aquí

Note cómo hay una superposición extraña de las áreas rojas y verdes en la parte superior izquierda. Esto sucede porque el árbol toma decisiones en cuatro dimensiones, pero solo podemos mostrar dos. Realmente no hay una manera limpia de evitar esto. Un clasificador de alta dimensión a menudo no tiene límites de decisión agradables en el espacio de baja dimensión.

Entonces, si estás más interesado en el clasificador, eso es lo que obtienes. Puede generar diferentes vistas a lo largo de varias combinaciones de dimensiones, pero hay límites a la utilidad de la representación.

Sin embargo, si está más interesado en los datos que en el clasificador, puede restringir la dimensionalidad antes de ajustar. En ese caso, el clasificador solo toma decisiones en el espacio bidimensional y podemos trazar buenas regiones de decisión:

 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) 

introduzca la descripción de la imagen aquí


Finalmente, aquí está la implementación:

 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) 

El enfoque de @kazemakase es el “correcto”. Para completar, aquí hay una forma sencilla de obtener cada “píxel” en Z que es un límite de decisión:

 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 de is_boundary (dilatado para que se puedan ver todas las entradas que no sean cero):

introduzca la descripción de la imagen aquí

Para aquellos interesados, recientemente tuve que implementar esto para datos de dimensiones superiores, el código era el siguiente:

 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 

Esencialmente, construyo un tensor * d * 2 donde n es el número de hojas del árbol, d es la dimensionalidad del espacio y la tercera dimensión contiene los valores mínimo y máximo. Las hojas se almacenan en tree.tree_.children_left / tree.tree_.children_right como -1, luego hago un bucle hacia atrás para encontrar la twig que causó la división en la hoja y agrega los criterios de división a los límites de decisión.