Clasificador de árbol de decisión, explicado: una guía visual con ejemplos de código para principiantes | de Samy Baladram | agosto, 2024

Clasificador de árbol de decisión, explicado: una guía visual con ejemplos de código para principiantes | de Samy Baladram | agosto, 2024

ALGORITMO DE CLASIFICACIÓN

Una nueva mirada a nuestro árbol invertido favorito

Los árboles de decisión son omnipresentes en el aprendizaje automático y se valoran por su resultado intuitivo. ¿A quién no le encanta un sencillo diagrama de flujo del tipo «si-entonces»? A pesar de su popularidad, sorprende lo difícil que es encontrar una explicación clara y paso a paso de cómo funcionan los árboles de decisión. (De hecho, me avergüenza el tiempo que me llevó comprender cómo funciona el algoritmo).

En este análisis, me centraré en los elementos esenciales para construir un árbol. Explicaremos EXACTAMENTE qué sucede en cada nodo y por qué, desde la raíz hasta las hojas finales (con imágenes, por supuesto).

Todas las imágenes: creadas por el autor usando Canva Pro. Optimizados para dispositivos móviles, pueden parecer de gran tamaño en una computadora de escritorio.

Un clasificador de árbol de decisión crea un árbol invertido para hacer predicciones, comenzando en la parte superior con una pregunta sobre una característica importante de sus datos y luego diversificándose según las respuestas. A medida que sigues estas ramas hacia abajo, cada parada plantea otra pregunta, lo que reduce las posibilidades. Este juego de preguntas y respuestas continúa hasta llegar al final (un nodo de hoja) donde obtienes tu predicción o clasificación final.

El árbol de decisión es uno de los algoritmos de aprendizaje automático más importantes: es una serie de preguntas de sí o no.

A lo largo de este artículo utilizaremos este conjunto de datos de golf artificial (inspirado en [1]) como ejemplo. Este conjunto de datos predice si una persona jugará golf en función de las condiciones climáticas.

Columnas: «Perspectiva» (ya codificada como soleado, nublado, lluvioso), «Temperatura» (en grados Fahrenheit), «Humedad» (en %), «Viento» (sí/no) y «Lectura» (en %) funcionalidad objetivo)
# Import libraries
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import pandas as pd
import numpy as np

# Load data
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rainy', 'rainy', 'rainy', 'overcast', 'sunny', 'sunny', 'rainy', 'sunny', 'overcast', 'overcast', 'rainy', 'sunny', 'overcast', 'rainy', 'sunny', 'sunny', 'rainy', 'overcast', 'rainy', 'sunny', 'overcast', 'sunny', 'overcast', 'rainy', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)

# Preprocess data
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
df['Wind'] = df['Wind'].astype(int)
df['Play'] = (df['Play'] == 'Yes').astype(int)

# Reorder the columns
df = df[['sunny', 'overcast', 'rainy', 'Temperature', 'Humidity', 'Wind', 'Play']]

# Prepare features and target
X, y = df.drop(columns='Play'), df['Play']

# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)

# Display results
print(pd.concat([X_train, y_train], axis=1), '\n')
print(pd.concat([X_test, y_test], axis=1))

El clasificador de árbol de decisión funciona dividiendo recursivamente los datos en función de las características más informativas. Así es como funciona:

  1. Comience con el conjunto de datos completo en el nodo raíz.
  2. Seleccione la mejor característica para dividir los datos (según métricas como la impureza de Gini).
  3. Cree nodos secundarios para cada valor posible de la característica seleccionada.
  4. Repita los pasos 2 y 3 para cada nodo secundario hasta que se cumpla un criterio de parada (por ejemplo, profundidad máxima alcanzada, número mínimo de muestras por hoja o nodos de hoja puros).
  5. Asigne la clase mayoritaria a cada nodo hoja.

En scikit-learn, el algoritmo del árbol de decisión se llama CART (árboles de clasificación y regresión). Construye árboles binarios y generalmente sigue los siguientes pasos:

  1. Comience con todas las muestras de entrenamiento en el nodo raíz.
Comenzando con el nodo raíz que contiene las 14 muestras de entrenamiento, determinaremos la mejor característica y apuntaremos a dividir los datos para comenzar a construir el árbol.

2. Para cada característica:
tiene. Ordenar valores de características.
b. Considere todos los umbrales posibles entre valores adyacentes como posibles puntos de división.

En este nodo raíz, hay 23 puntos de división para verificar. Las columnas binarias tienen sólo un punto de división.
def potential_split_points(attr_name, attr_values):
sorted_attr = np.sort(attr_values)
unique_values = np.unique(sorted_attr)
split_points = [(unique_values[i] + unique_values[i+1]) / 2 for i in range(len(unique_values) - 1)]
return {attr_name: split_points}

# Calculate and display potential split points for all columns
for column in X_train.columns:
splits = potential_split_points(column, X_train[column])
for attr, points in splits.items():
print(f"{attr:11}: {points}")

3. Para cada punto de división potencial:
tiene. Calcule la impureza (por ejemplo, impureza de Gini) del nodo actual.
b. Calcule el promedio ponderado de las impurezas.

Por ejemplo, para la función «soleada» con un punto de división de 0,5, la impureza (como la «impureza de Gini») se calcula para ambas partes del conjunto de datos.
Como otro ejemplo, el mismo proceso también se puede aplicar a funciones continuas como «Temperatura».
def gini_impurity(y):
p = np.bincount(y) / len(y)
return 1 - np.sum(p**2)

def weighted_average_impurity(y, split_index):
n = len(y)
left_impurity = gini_impurity(y[:split_index])
right_impurity = gini_impurity(y[split_index:])
return (split_index * left_impurity + (n - split_index) * right_impurity) / n

# Sort 'sunny' feature and corresponding labels
sunny = X_train['sunny']
sorted_indices = np.argsort(sunny)
sorted_sunny = sunny.iloc[sorted_indices]
sorted_labels = y_train.iloc[sorted_indices]

# Find split index for 0.5
split_index = np.searchsorted(sorted_sunny, 0.5, side='right')

# Calculate impurity
impurity = weighted_average_impurity(sorted_labels, split_index)

print(f"Weighted average impurity for 'sunny' at split point 0.5: {impurity:.3f}")

4. Después de calcular todas las impurezas para todas las funciones y puntos de división, elija la más baja.

La función «nublado» con un punto de división de 0,5 da la impureza más baja. ¡Esto significa que el punto de división será el más puro de todos los demás puntos de división!
def calculate_split_impurities(X, y):
split_data = []

for feature in X.columns:
sorted_indices = np.argsort(X[feature])
sorted_feature = X[feature].iloc[sorted_indices]
sorted_y = y.iloc[sorted_indices]

unique_values = sorted_feature.unique()
split_points = (unique_values[1:] + unique_values[:-1]) / 2

for split in split_points:
split_index = np.searchsorted(sorted_feature, split, side='right')
impurity = weighted_average_impurity(sorted_y, split_index)
split_data.append({
'feature': feature,
'split_point': split,
'weighted_avg_impurity': impurity
})

return pd.DataFrame(split_data)

# Calculate split impurities for all features
calculate_split_impurities(X_train, y_train).round(3)

5. Cree dos nodos secundarios según la función elegida y el punto de división:
– Hijo izquierdo: muestras con valor característico <= punto de división
– Hijo derecho: muestras con valor característico > punto de división

El punto de división seleccionado divide los datos en dos partes. Como una parte ya es pura (¡el lado derecho! ¡Por eso su impureza es baja!), todo lo que tenemos que hacer es continuar el árbol en el nodo izquierdo.

6. Repita recursivamente los pasos del 2 al 5 para cada nodo secundario. También puede detenerse hasta que se cumpla un criterio de parada (por ejemplo, alcanzar la profundidad máxima, número mínimo de muestras por nodo de hoja o disminución mínima de impurezas).

# Calculate split impurities forselected index
selected_index = [4,8,3,13,7,9,10] # Change it depending on which indices you want to check
calculate_split_impurities(X_train.iloc[selected_index], y_train.iloc[selected_index]).round(3)
from sklearn.tree import DecisionTreeClassifier

# The whole Training Phase above is done inside sklearn like this
dt_clf = DecisionTreeClassifier()
dt_clf.fit(X_train, y_train)

Árbol final completo

La etiqueta de clase de un nodo hoja corresponde a la clase mayoritaria de muestras de entrenamiento que llegaron a ese nodo.

El árbol de la derecha es el árbol final que se utilizará para la clasificación. Ya no necesitamos las muestras en este momento.
import matplotlib.pyplot as plt
from sklearn.tree import plot_tree
# Plot the decision tree
plt.figure(figsize=(20, 10))
plot_tree(dt_clf, filled=True, feature_names=X.columns, class_names=['Not Play', 'Play'])
plt.show()
En esta salida de scikit-learn, también se almacena la información del nodo que no es hoja, como el número de muestras y el número de cada clase en el nodo (valor).

Así es como funciona el proceso de predicción una vez que se forma el árbol de decisión:

  1. Comience en el nodo raíz del árbol de decisión entrenado.
  2. Evalúe la funcionalidad y la condición de división en el nodo actual.
  3. Repita el paso 2 en cada nodo subsiguiente hasta llegar a un nodo de hoja.
  4. La etiqueta de clase del nodo hoja se convierte en la predicción de la nueva instancia.
Solo necesitamos las columnas que solicita el árbol. Aparte de “nublado” y “temperatura”, los demás valores no son importantes para realizar la predicción.
# Make predictions
y_pred = dt_clf.predict(X_test)
print(y_pred)
El árbol de decisión proporciona una precisión adecuada. Como nuestro árbol solo verifica dos características, es posible que no capture bien las características del conjunto de prueba.
# Evaluate the classifier
print(f"Accuracy: {accuracy_score(y_test, y_pred)}")

Los árboles de decisión tienen varios parámetros importantes que controlan su crecimiento y complejidad:

1. Profundidad máxima:Esto establece la profundidad máxima del eje, lo que puede ser una herramienta valiosa para evitar el sobreajuste.

👍 Consejo útil: Considere comenzar con un árbol poco profundo (quizás de 3 a 5 niveles de profundidad) y aumentar gradualmente la profundidad.

Comience con un árbol poco profundo (por ejemplo, profundidad 3-5) y aumente gradualmente hasta encontrar el equilibrio óptimo entre la complejidad del modelo y el rendimiento de los datos de validación.

2. muestras mínimas dividido:Este parámetro determina el número mínimo de muestras necesarias para dividir un nodo interno.

👍 Consejo útil:Establecer un valor más alto (alrededor del 5 al 10 % de los datos de entrenamiento) puede ayudar a evitar que el árbol cree demasiadas divisiones pequeñas y específicas que podrían no generalizarse bien a datos nuevos.

3. Mini hoja de muestra:Esto especifica el número mínimo de muestras requeridas en un nodo hoja.

👍 Consejo útil: elija un valor que garantice que cada hoja represente un subconjunto significativo de sus datos (alrededor del 1 al 5 % de sus datos de entrenamiento). Esto puede ayudar a evitar predicciones demasiado específicas.

4. Criterios:La función utilizada para medir la calidad de una división (normalmente «gini» para la impureza de Gini o «entropía» para la ganancia de información).

👍 Consejo útil:Aunque el coeficiente de Gini es generalmente más sencillo y rápido de calcular, la entropía suele funcionar mejor en problemas de varias clases. Dicho esto, suelen dar resultados similares.

Ejemplo de cálculo de entropía para “soleado” con un punto de división de 0,5.

Como cualquier algoritmo de aprendizaje automático, los árboles de decisión tienen sus fortalezas y limitaciones.

Beneficios :

  1. Interpretabilidad:Fácil de entender y visualizar el proceso de toma de decisiones.
  2. Sin escalamiento de funciones:Puede manejar datos numéricos y categóricos sin normalización.
  3. Maneja relaciones no lineales:Puede capturar patrones complejos en los datos.
  4. Importancia de las características:Proporciona una indicación clara de qué características son más importantes para la predicción.

Desventajas:

  1. Sobreajuste: Tendencia a crear árboles demasiado complejos que no se generalizan bien, especialmente con conjuntos de datos pequeños.
  2. Inestabilidad:Pequeños cambios en los datos pueden dar lugar a que se genere un árbol completamente diferente.
  3. Sesgo con conjuntos de datos desequilibrados:Puede estar sesgado a favor de las clases dominantes.
  4. Incapacidad de extrapolar:No se pueden hacer predicciones más allá del rango de datos de entrenamiento.

En nuestro ejemplo de golf, un árbol de decisiones puede crear reglas muy precisas e interpretables para decidir si se juega golf en función de las condiciones climáticas. Sin embargo, puede sobreadaptarse a combinaciones específicas de condiciones si no se poda adecuadamente o si el conjunto de datos es pequeño.

Los clasificadores de árboles de decisión son una gran herramienta para resolver muchos tipos de problemas en el aprendizaje automático. Son fáciles de entender, pueden manejar datos complejos y nos muestran cómo toman decisiones. Esto los hace útiles en muchos campos, desde los negocios hasta la medicina. Aunque los árboles de decisión son potentes e interpretables, a menudo se utilizan como componentes básicos para métodos de conjunto más avanzados, como bosques aleatorios o máquinas de aumento de gradiente.

# Import libraries
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from sklearn.tree import plot_tree, DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# Load data
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rainy', 'rainy', 'rainy', 'overcast', 'sunny', 'sunny', 'rainy', 'sunny', 'overcast', 'overcast', 'rainy', 'sunny', 'overcast', 'rainy', 'sunny', 'sunny', 'rainy', 'overcast', 'rainy', 'sunny', 'overcast', 'sunny', 'overcast', 'rainy', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)

# Prepare data
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
df['Wind'] = df['Wind'].astype(int)
df['Play'] = (df['Play'] == 'Yes').astype(int)

# Split data
X, y = df.drop(columns='Play'), df['Play']
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)

# Train model
dt_clf = DecisionTreeClassifier(
max_depth=None, # Maximum depth of the tree
min_samples_split=2, # Minimum number of samples required to split an internal node
min_samples_leaf=1, # Minimum number of samples required to be at a leaf node
criterion='gini' # Function to measure the quality of a split
)
dt_clf.fit(X_train, y_train)

# Make predictions
y_pred = dt_clf.predict(X_test)

# Evaluate model
print(f"Accuracy: {accuracy_score(y_test, y_pred)}")

# Visualize tree
plt.figure(figsize=(20, 10))
plot_tree(dt_clf, filled=True, feature_names=X.columns,
class_names=['Not Play', 'Play'], impurity=False)
plt.show()