ALGORITMO DE CLASIFICACIÓN
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).
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.
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.
# 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:
- Comience con el conjunto de datos completo en el nodo raíz.
- Seleccione la mejor característica para dividir los datos (según métricas como la impureza de Gini).
- Cree nodos secundarios para cada valor posible de la característica seleccionada.
- 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).
- 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:
- Comience con todas las muestras de entrenamiento en el nodo raíz.
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.
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.
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.
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
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.
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()
Así es como funciona el proceso de predicción una vez que se forma el árbol de decisión:
- Comience en el nodo raíz del árbol de decisión entrenado.
- Evalúe la funcionalidad y la condición de división en el nodo actual.
- Repita el paso 2 en cada nodo subsiguiente hasta llegar a un nodo de hoja.
- La etiqueta de clase del nodo hoja se convierte en la predicción de la nueva instancia.
# Make predictions
y_pred = dt_clf.predict(X_test)
print(y_pred)
# 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.
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.
Como cualquier algoritmo de aprendizaje automático, los árboles de decisión tienen sus fortalezas y limitaciones.
Beneficios :
- Interpretabilidad:Fácil de entender y visualizar el proceso de toma de decisiones.
- Sin escalamiento de funciones:Puede manejar datos numéricos y categóricos sin normalización.
- Maneja relaciones no lineales:Puede capturar patrones complejos en los datos.
- Importancia de las características:Proporciona una indicación clara de qué características son más importantes para la predicción.
Desventajas:
- Sobreajuste: Tendencia a crear árboles demasiado complejos que no se generalizan bien, especialmente con conjuntos de datos pequeños.
- Inestabilidad:Pequeños cambios en los datos pueden dar lugar a que se genere un árbol completamente diferente.
- Sesgo con conjuntos de datos desequilibrados:Puede estar sesgado a favor de las clases dominantes.
- 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()