Introducción
Los árboles de decisión son algoritmos de aprendizaje automático ampliamente utilizados para tareas de clasificación y regresión. Su estructura ramificada permite tomar decisiones secuenciales basadas en características de los datos, llevando a conclusiones en nodos hoja. Este artículo explora los fundamentos, implementaciones y mejoras de los árboles de decisión, con ejemplos en Python.
Fundamentos de los árboles de decisión
Definición y estrucutra
Un árbol de decisión es un modelo predictivo que utiliza una jerarquía de nodos de decisión y hojas. Cada nodo interno representa una prueba sobre una característica, cada rama un resultado de la prueba, y cada nodo hoja una etiqueta de clase o valor de predicción. La construcción implica seleccionar características que maximicen la pureza de los nodos, reduciendo la incertidumbre en las predicciones.
Flujo de construcción
El proceso general para construir un árbol de decisión incluye:
- Recopilar y preparar datos, discretizando si es necesario.
- Analizar datos para identificar características relevantes.
- Entrenar el árbol mediante división recursiva basada en métricas como la ganancia de información.
- Evaluar el modelo con datos de prueba.
- Desplegar el árbol para predicciones interpretables.
Entropía y ganancia de información
La entropía mide la impureza o desorden en un conjunto de datos. Se calcula como:
H(S) = - Σ p_i log₂(p_i)
donde p_i es la proporción de muestras de la clase i. La ganancia de información evalúa cuánto reduce la entropía al dividir los datos por una característica. Se prefiere la característica con mayor ganancia.
Ejemplo de cálculo de entropía en Python
import math
def calcular_entropia(datos):
n = len(datos)
conteo_etiquetas = {}
for punto in datos:
etiqueta = punto[-1]
conteo_etiquetas[etiqueta] = conteo_etiquetas.get(etiqueta, 0) + 1
entropia = 0.0
for etiqueta, frecuencia in conteo_etiquetas.items():
prob = frecuencia / n
entropia -= prob * math.log2(prob)
return entropia
División de datos y selección de características
Para dividir datos, se evalúan subconjuntos basados en valores de características. La función de división extrae muestras donde la característica coincide con un valor específico, eliminando esa característica del conjunto resultante.
def dividir_datos(datos, indice_caracteristica, valor):
subconjunto = []
for punto in datos:
if punto[indice_caracteristica] == valor:
punto_reducido = punto[:indice_caracteristica] + punto[indice_caracteristica+1:]
subconjunto.append(punto_reducido)
return subconjunto
Construcción recursiva del árbol
El árbol se construye recursivamente seleccionando la mejor característica en cada paso. Si todas las muestras en un nodo pertenecen a la misma clase, se crea una hoja. Si se agotan las características, se usa votación mayoritaria para la etiqueta.
def votar_mayoria(etiquetas):
conteo = {}
for etiqueta in etiquetas:
conteo[etiqueta] = conteo.get(etiqueta, 0) + 1
return max(conteo, key=conteo.get)
def construir_arbol(datos, nombres_caracteristicas):
etiquetas = [punto[-1] for punto in datos]
if len(set(etiquetas)) == 1:
return etiquetas[0]
if not nombres_caracteristicas:
return votar_mayoria(etiquetas)
mejor_caract = seleccionar_mejor_caracteristica(datos)
nombre_caract = nombres_caracteristicas[mejor_caract]
arbol = {nombre_caract: {}}
nombres_rest = nombres_caracteristicas.copy()
del nombres_rest[mejor_caract]
valores_unicos = set([punto[mejor_caract] for punto in datos])
for valor in valores_unicos:
sub_datos = dividir_datos(datos, mejor_caract, valor)
arbol[nombre_caract][valor] = construir_arbol(sub_datos, nombres_rest)
return arbol
Visualización del árbol
Se puede usar Matplotlib para dibujar el árbol, calculando el número de hojas y la profundidad para posicionar nodos. Anotaciones y estilos diefrencian nodos de decisión de hojas.
import matplotlib.pyplot as plt
def dibujar_nodo(texto, centro, padre, estilo):
plt.annotate(texto, xy=padre, xytext=centro, va="center", ha="center", bbox=estilo, arrowprops=dict(arrowstyle="<-"))
def visualizar_arbol(arbol):
# Implementación simplificada para propósitos ilustrativos
# Se omite código detallado por brevedad
pass
Mejoras y algoritmos avanzados
Algoritmo ID3 y sus limitaciones
ID3 utiliza ganancia de información para dividir, pero puede sesgarse hacia características con muchos valores. Por ejemplo, un identificador único tendría ganancia máxima pero no es útil predictivamente.
C4.5: Tasa de ganancia de información
C4.5 aborda el sesgo de ID3 usando la tasa de ganancia de información, que normaliza la ganancia por la entropía intrínseca de la característica. Esto penaliza características con valores numerosos.
CART: Coeficiente Gini
CART usa el índice de Gini como métrica de impureza, definido como Gini(S) = 1 - Σ p_i². Es más rápido de calcular y produce árboles binarios.
Técnicas de poda para evitar sobreajuste
Poda previa
La poda previa detiene el crecimiento del árbol durante el entrenamiento si la división no mejora la precisión en un conjunto de validación. Reduce complejidad y tiempo de entrenamiento, pero puede causar subajuste.
Poda posterior
La poda posterior recorta ramas después de construir el árbol completo, evaluando la precisión en datos de validación. Suele producir modelos más precisos pero es más costosa computacionalmente.
Implementación de poda previa
def construir_arbol_con_poda_previa(datos_entrenamiento, etiquetas_entrenamiento, datos_validacion, etiquetas_validacion, nombres):
# Lógica para construir el árbol, verificando en cada paso si la división mejora la precisión en validación
# Si no mejora, se devuelve la etiqueta mayoritaria como hoja
# Implementación omitida por brevedad
pass
Implementación de poda posterior
def podar_arbol(arbol, datos_prueba, etiquetas_prueba, nombres):
# Recorre el árbol recursivamente, reemplazando subárboles por hojas si reducen el error
# Utiliza una métrica de pérdida que combina impureza y número de hojas
# Implementación omitida por brevedad
pass