Naive Bayes y Árboles de Decisión: Explicación Exhaustiva

Naive Bayes

Fundamentos del Algoritmo

Naive Bayes es un algoritmo de clasificación probabilística basado en el Teorema de Bayes, que establece la relación entre probabilidades condicionales. El teorema se expresa matemáticamente como:

P(yX)=P(Xy)P(y)P(X)P(y\mid X) = \frac{P(X\mid y) \cdot P(y)}{P(X)}

Donde:

  • P(y|X): Probabilidad posterior - probabilidad de la clase y dado las características X
  • P(X|y): Verosimilitud - probabilidad de las características X dada la clase y
  • P(y): Probabilidad prior de la clase y
  • P(X): Evidencia marginal

Asunción “Naive” de Independencia

La característica distintiva del algoritmo es la asunción “naive” (ingenua) de que todas las características son condicionalmente independientes entre sí, dada la clase. Esto significa:

P(x1,x2,...,xny)=P(x1y)P(x2y)P(xny)P(x_1, x_2, ..., x_n | y) = P(x_1 | y) \cdot P(x_2 | y) \cdots P(x_n | y)

Esta asunción simplifica enormemente los cálculos, convirtiendo el teorema de Bayes en:

P(yx1,...,xn)=P(y)i=1nP(xiy)P(x1,x2,...,xn)P(y|x_1, ..., x_n) = \frac{P(y) \cdot \prod_{i=1}^{n} P(x_i | y)}{P(x_1, x_2, ..., x_n)}

Excelencia en Clasificación de Texto

Naive Bayes demuestra una efectividad excepcional en tareas de clasificación de texto como filtrado de spam, análisis de sentimientos y categorización de documentos. Su eficacia se debe a:

  • Manejo eficiente de alta dimensionalidad: Los textos generan espacios de características muy grandes
  • Robustez con datos dispersos: Los documentos de texto típicamente contienen solo una fracción del vocabulario total
  • Velocidad de entrenamiento y predicción: Especialmente importante con grandes volúmenes de texto

Ejemplo de Código para Clasificación de Texto

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.model_selection import train_test_split
import pandas as pd

# Datos de ejemplo
datos = {
    'texto': [
        'Este es un excelente producto',
        'Muy malo, no lo recomiendo',
        'Fantástico, superó mis expectativas',
        'Terrible calidad, muy decepcionante'
    ],
    'sentimiento': ['positivo', 'negativo', 'positivo', 'negativo']
}

df = pd.DataFrame(datos)

# Vectorización del texto
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(df['texto'])
y = df['sentimiento']

# División de datos
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25)

# Entrenamiento del modelo
modelo = MultinomialNB()
modelo.fit(X_train, y_train)

# Predicción
predicciones = modelo.predict(X_test)

Problema de Laplace y Smoothing

El problema de Laplace o de probabilidad cero surge cuando una característica no aparece en el conjunto de entrenamiento para una clase específica. Esto resulta en P(x_i|y) = 0, lo que hace que toda la probabilidad posterior se vuelva cero.

Laplace Smoothing (Suavizado de Laplace)

El suavizado de Laplace resuelve este problema añadiendo un valor α (típicamente α = 1) a cada conteo:

P(xiy)=conteo(xi,y)+αconteo(y)+αVP(x_i|y) = \frac{conteo(x_i, y) + \alpha}{conteo(y) + \alpha \cdot |V|}

Donde |V| es el tamaño del vocabulario.

Ejemplo de Código con Laplace Smoothing

from sklearn.naive_bayes import MultinomialNB

# Aplicar suavizado de Laplace con alpha=1
modelo_suavizado = MultinomialNB(alpha=1.0)
modelo_suavizado.fit(X_train, y_train)

# El parámetro alpha controla el suavizado
# alpha=0: sin suavizado (puede causar problemas de probabilidad cero)
# alpha=1: suavizado de Laplace estándar
# alpha>1: suavizado más agresivo

Árboles de Decisión

Algoritmo Interpretable Basado en Reglas

Los árboles de decisión son algoritmos de aprendizaje supervisado que crean un modelo predictivo mediante una estructura jerárquica de reglas de decisión. Su principal fortaleza radica en su interpretabilidad, permitiendo visualizar y entender el proceso de toma de decisiones.

Componentes del Árbol

Los árboles de decisión están compuestos por varios elementos estructurales:

  1. Nodo Raíz: Representa todo el conjunto de datos inicial
  2. Nodos Internos: Puntos de decisión donde se aplican reglas de división
  3. Hojas: Nodos terminales que contienen la predicción final
  4. Ramas: Conexiones entre nodos que representan las diferentes decisiones

Ejemplo de Estructura Básica

from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
from sklearn import tree

# Cargar datos
iris = load_iris()
X, y = iris.data, iris.target

# Crear el árbol de decisión
clf = DecisionTreeClassifier(max_depth=3, random_state=42)
clf.fit(X, y)

# Visualizar el árbol
plt.figure(figsize=(12,8))
tree.plot_tree(clf, feature_names=iris.feature_names, 
               class_names=iris.target_names, filled=True)
plt.show()

Métricas de Pureza

Para construir un árbol eficaz, se utilizan métricas que evalúan la “pureza” de los nodos, determinando las mejores divisiones.

Índice de Gini

El Índice de Gini mide la probabilidad de clasificar incorrectamente un elemento seleccionado aleatoriamente:

Gini(t)=1i=1cpi2Gini(t) = 1 - \sum_{i=1}^{c} p_i^2

Donde p_i es la proporción de muestras que pertenecen a la clase i en el nodo t.

Entropía

La Entropía cuantifica el desorden o incertidumbre en un conjunto de datos:

Entropıˊa(S)=i=1cpilog2(pi)Entropía(S) = -\sum_{i=1}^{c} p_i \log_2(p_i)

Ganancia de Información

La Ganancia de Información mide la reducción en entropía después de una división:

IG(S,A)=Entropıˊa(S)vSvSEntropıˊa(Sv)IG(S,A) = Entropía(S) - \sum_{v} \frac{|S_v|}{|S|} Entropía(S_v)

Implementación de Métricas de Pureza

import numpy as np
from collections import Counter

def calcular_gini(y):
    """Calcula el índice de Gini para un conjunto de etiquetas"""
    if len(y) == 0:
        return 0
    
    conteos = Counter(y)
    probabilidades = [conteos[clase]/len(y) for clase in conteos]
    gini = 1 - sum([p**2 for p in probabilidades])
    return gini

def calcular_entropia(y):
    """Calcula la entropía para un conjunto de etiquetas"""
    if len(y) == 0:
        return 0
    
    conteos = Counter(y)
    probabilidades = [conteos[clase]/len(y) for clase in conteos]
    entropia = -sum([p * np.log2(p) for p in probabilidades if p > 0])
    return entropia

def ganancia_informacion(y_padre, y_izq, y_der):
    """Calcula la ganancia de información de una división"""
    n = len(y_padre)
    n_izq, n_der = len(y_izq), len(y_der)
    
    entropia_padre = calcular_entropia(y_padre)
    entropia_ponderada = (n_izq/n) * calcular_entropia(y_izq) + \
                        (n_der/n) * calcular_entropia(y_der)
    
    return entropia_padre - entropia_ponderada

# Ejemplo de uso
etiquetas = ['A', 'A', 'B', 'B', 'C']
print(f"Gini: {calcular_gini(etiquetas):.3f}")
print(f"Entropía: {calcular_entropia(etiquetas):.3f}")

Construcción: División Recursiva Minimizando Impureza

El proceso de construcción del árbol sigue un enfoque top-down y divide y vencerás:

  1. Selección del Mejor Atributo: Se evalúan todos los atributos disponibles usando métricas de pureza
  2. División Recursiva: El nodo se divide basándose en el mejor atributo
  3. Criterios de Parada: El proceso se detiene cuando se cumple alguna condición

Algoritmo de Construcción

def construir_arbol(X, y, profundidad=0, max_profundidad=5):
    """Construcción recursiva de un árbol de decisión"""
    
    # Criterios de parada
    if profundidad >= max_profundidad or len(set(y)) == 1:
        # Crear nodo hoja con la clase más común
        return {'prediccion': Counter(y).most_common(1)}
    
    mejor_ganancia = 0
    mejor_atributo = None
    mejor_umbral = None
    
    # Buscar la mejor división
    for atributo in range(X.shape):
        valores_unicos = np.unique(X[:, atributo])
        
        for umbral in valores_unicos:
            # Dividir los datos
            izq_mask = X[:, atributo] <= umbral
            der_mask = X[:, atributo] > umbral
            
            if sum(izq_mask) == 0 or sum(der_mask) == 0:
                continue
            
            # Calcular ganancia de información
            ganancia = ganancia_informacion(y, y[izq_mask], y[der_mask])
            
            if ganancia > mejor_ganancia:
                mejor_ganancia = ganancia
                mejor_atributo = atributo
                mejor_umbral = umbral
    
    # Si no hay ganancia, crear nodo hoja
    if mejor_ganancia == 0:
        return {'prediccion': Counter(y).most_common(1)}
    
    # Crear división
    izq_mask = X[:, mejor_atributo] <= mejor_umbral
    der_mask = X[:, mejor_atributo] > mejor_umbral
    
    return {
        'atributo': mejor_atributo,
        'umbral': mejor_umbral,
        'izquierda': construir_arbol(X[izq_mask], y[izq_mask], 
                                   profundidad + 1, max_profundidad),
        'derecha': construir_arbol(X[der_mask], y[der_mask], 
                                 profundidad + 1, max_profundidad)
    }

Ventajas de los Árboles de Decisión

Interpretabilidad

La principal ventaja es su transparencia y facilidad de comprensión. Los árboles proporcionan reglas claras que pueden ser seguidas por humanos.

Manejo de Valores Faltantes

Los árboles de decisión pueden manejar naturalmente valores faltantes mediante divisiones sustitutos:

from sklearn.tree import DecisionTreeClassifier
import numpy as np

# Datos con valores faltantes (NaN)
X = np.array([[1, 2], [np.nan, 3], [4, np.nan], [5, 6]])
y = np.array([0, 1, 0, 1])

# El árbol maneja automáticamente los valores faltantes
clf = DecisionTreeClassifier()
clf.fit(X, y)

# Predicción con valores faltantes
nuevo_dato = [[np.nan, 5]]
prediccion = clf.predict(nuevo_dato)

Desventajas: Tendencia al Overfitting

Los árboles de decisión son propensos al sobreajuste, especialmente cuando crecen demasiado profundos. Esto puede llevar a una pobre generalización en datos no vistos.

Técnicas de Poda (Pruning)

Para mitigar el overfitting, se aplican técnicas de poda:

Pre-poda (Early Stopping)
from sklearn.tree import DecisionTreeClassifier

# Parámetros para controlar el crecimiento del árbol
clf = DecisionTreeClassifier(
    max_depth=5,           # Profundidad máxima
    min_samples_split=10,  # Mínimo de muestras para dividir
    min_samples_leaf=5,    # Mínimo de muestras en hojas
    max_features='sqrt'    # Número máximo de características
)
Post-poda (Cost Complexity Pruning)
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import validation_curve
import matplotlib.pyplot as plt

# Entrenar árbol completo
clf = DecisionTreeClassifier(random_state=42)
clf.fit(X_train, y_train)

# Obtener valores de alpha para poda
path = clf.cost_complexity_pruning_path(X_train, y_train)
ccp_alphas = path.ccp_alphas

# Evaluar diferentes valores de alpha
train_scores, val_scores = validation_curve(
    DecisionTreeClassifier(random_state=42), X_train, y_train,
    param_name='ccp_alpha', param_range=ccp_alphas,
    cv=5, scoring='accuracy'
)

# Seleccionar el mejor alpha
mejor_alpha = ccp_alphas[np.argmax(val_scores.mean(axis=1))]

# Crear árbol podado
clf_podado = DecisionTreeClassifier(ccp_alpha=mejor_alpha, random_state=42)
clf_podado.fit(X_train, y_train)

Comparación Práctica: Naive Bayes vs Árboles de Decisión

from sklearn.datasets import make_classification
from sklearn.model_selection import cross_val_score
from sklearn.naive_bayes import GaussianNB
from sklearn.tree import DecisionTreeClassifier
import numpy as np

# Generar datos sintéticos
X, y = make_classification(n_samples=1000, n_features=10, 
                          n_informative=5, n_redundant=2, 
                          random_state=42)

# Modelos a comparar
modelos = {
    'Naive Bayes': GaussianNB(),
    'Árbol de Decisión': DecisionTreeClassifier(max_depth=10, random_state=42)
}

# Evaluación cruzada
for nombre, modelo in modelos.items():
    scores = cross_val_score(modelo, X, y, cv=5, scoring='accuracy')
    print(f"{nombre}:")
    print(f"  Precisión media: {scores.mean():.3f} (+/- {scores.std() * 2:.3f})")
    print(f"  Precisiones individuales: {scores}")
    print()

Aplicaciones Recomendadas

Naive Bayes es ideal para:

  • Clasificación de texto (spam, sentimientos, categorización)
  • Datos con alta dimensionalidad
  • Cuando se requiere rapidez en entrenamiento y predicción
  • Datasets pequeños a medianos

Árboles de Decisión son ideales para:

  • Cuando se necesita interpretabilidad del modelo
  • Datos con relaciones no lineales complejas
  • Manejo de valores faltantes sin preprocesamiento
  • Problemas donde las reglas de decisión deben ser explícitas

Ambos algoritmos representan enfoques fundamentalmente diferentes pero complementarios en el arsenal del aprendizaje automático, cada uno con sus fortalezas específicas según el contexto del problema y los requisitos del proyecto.

Naive Bayes y Árboles de Decisión - Algoritmos de Machine Learning

Author

Juan Fuentes

Publish Date

07 - 06 - 2023