Naive Bayes
1 Un problema real: reseñas de restaurantes
Imagina que tienes 12 reseñas de restaurantes, cada una etiquetada como positiva o negativa. Quieres construir un clasificador que, dada una reseña nueva, prediga si el cliente quedó satisfecho o no.
| # | Reseña (preprocesada) | Sentimiento |
|---|---|---|
| 1 | “comida excelente servicio rápido” | Pos |
| 2 | “comida mala servicio lento” | Neg |
| 3 | “lugar muy bueno excelente comida” | Pos |
| 4 | “nunca más vuelvo pésimo servicio” | Neg |
| 5 | “comida rica lugar acogedor” | Pos |
| 6 | “caro lento mala experiencia” | Neg |
| 7 | “excelente relación calidad precio” | Pos |
| 8 | “servicio terrible comida fría” | Neg |
| 9 | “volveré sin duda excelente lugar” | Pos |
| 10 | “decepcionante nada recomendable” | Neg |
| 11 | “comida buena servicio amable” | Pos |
| 12 | “lugar encantador volveré pronto” | Pos |
Nota
Preprocesamiento. Las reseñas ya están procesadas: minúsculas, sin puntuación, sin stop words, con stemming. Consulta el capítulo de preprocesamiento de texto para el pipeline completo.
2 Probabilidades previas
De las 12 reseñas, 7 son positivas y 5 negativas. En la práctica, las reseñas positivas suelen ser más frecuentes que las negativas: la gente tiende a recomendar más que a quejarse.
La razón de previos (prior odds) es la razón entre ambas probabilidades:
Esto significa que, sin saber nada del contenido de la reseña, una reseña es 1.4 veces más probable que sea positiva que negativa. Es lo que sabemos antes de leer una sola palabra.
Para la fórmula de puntuación, usamos el logaritmo de esa razón:
¿Por qué importa? Porque en la fórmula de puntuación:
el previo actúa como un punto de partida: si las positivas son más frecuentes en el corpus, el modelo “empieza” inclinándose hacia positivo (+0.336). Si las clases fueran 50/50, el logaritmo sería \(\log(1) = 0\) y la decisión dependería solo de las palabras.
3 Frecuencias por clase
Contamos cuántas veces aparece cada palabra en reseñas positivas y negativas:
corpus = [
("comida excelente servicio rápido", "pos"),
("comida mala servicio lento", "neg"),
("lugar muy bueno excelente comida", "pos"),
("nunca más vuelvo pésimo servicio", "neg"),
("comida rica lugar acogedor", "pos"),
("caro lento mala experiencia", "neg"),
("excelente relación calidad precio", "pos"),
("servicio terrible comida fría", "neg"),
("volveré sin duda excelente lugar", "pos"),
("decepcionante nada recomendable", "neg"),
("comida buena servicio amable", "pos"),
("lugar encantador volveré pronto", "pos"),
]
# Contar frecuencias
freq_pos, freq_neg = {}, {}
n_pos, n_neg = 0, 0
vocabulario = set()
for texto, clase in corpus:
palabras = texto.split()
for p in palabras:
vocabulario.add(p)
if clase == "pos":
freq_pos[p] = freq_pos.get(p, 0) + 1
n_pos += 1
else:
freq_neg[p] = freq_neg.get(p, 0) + 1
n_neg += 1
vocabulario = sorted(vocabulario)
V = len(vocabulario)
print(f"Vocabulario: {V} palabras únicas")
print(f"Total palabras en reseñas positivas: {n_pos}")
print(f"Total palabras en reseñas negativas: {n_neg}")
| Palabra | Pos | Neg | Palabra | Pos | Neg | |
|---|---|---|---|---|---|---|
| acogedor | 1 | 0 | amable | 1 | 0 | |
| bueno | 1 | 0 | buena | 1 | 0 | |
| caro | 0 | 1 | calidad | 1 | 0 | |
| comida | 4 | 2 | decepcionante | 0 | 1 | |
| duda | 1 | 0 | encantador | 1 | 0 | |
| experiencia | 0 | 1 | excelente | 4 | 0 | |
| fría | 0 | 1 | larga | 0 | 1 | |
| lento | 0 | 2 | lugar | 3 | 0 | |
| mala | 0 | 2 | más | 0 | 1 | |
| nada | 0 | 1 | nunca | 0 | 1 | |
| pésimo | 0 | 1 | pronto | 1 | 0 | |
| precio | 1 | 0 | rápido | 1 | 0 | |
| rica | 1 | 0 | recomendable | 0 | 1 | |
| relación | 1 | 0 | servicio | 2 | 2 | |
| sin | 1 | 0 | terrible | 0 | 1 | |
| vuelvo | 0 | 1 | Total | 31 | 18 |
Observa algo interesante: “comida” y “servicio” aparecen en ambas clases, pero con diferente peso. “Comida” aparece 4 veces en positivas y solo 2 en negativas, por lo que tiene una señal ligeramente positiva. “Servicio” aparece 2 veces en cada clase, por lo que es más neutral. En cambio, “excelente” solo aparece en positivas y “terrible” solo en negativas: estas son las palabras claramente discriminantes.
4 Probabilidades condicionales
La probabilidad de que una palabra aparezca dado el sentimiento:
Donde \(N_{\text{Pos}} = 31\) es el total de palabras en la clase positiva.
Veamos el cálculo concreto para dos palabras:
Ejemplo con “excelente”: Aparece 4 veces en reseñas positivas y 0 en negativas.
Ejemplo con “comida”: Aparece 4 veces en positivas y 2 en negativas.
Observa la diferencia: “excelente” tiene una señal clara (existe solo en positivas), mientras que “comida” es más ambigua (aparece en ambas clases, aunque ligeramente más en positivas).
La siguiente tabla muestra una selección de las palabras más representativas. La tabla completa, con las 29 palabras del vocabulario, se genera con el código:
# Probabilidades crudas (sin suavizar)
for w in vocabulario:
p_pos = freq_pos.get(w, 0) / n_pos
p_neg = freq_neg.get(w, 0) / n_neg
print(f"{w:18s} P(w|Pos)={p_pos:.3f} P(w|Neg)={p_neg:.3f}")
| Palabra | \(P(w \mid \text{Pos})\) | \(P(w \mid \text{Neg})\) |
|---|---|---|
| excelente | 0.129 | 0.000 |
| comida | 0.129 | 0.111 |
| lugar | 0.097 | 0.000 |
| servicio | 0.065 | 0.111 |
| buena | 0.032 | 0.000 |
| amable | 0.032 | 0.000 |
| rica | 0.032 | 0.000 |
| lento | 0.000 | 0.111 |
| mala | 0.000 | 0.111 |
| terrible | 0.000 | 0.056 |
El problema es inmediatamente visible: “excelente” tiene probabilidad cero en la clase negativa. Si una reseña negativa contiene la palabra “excelente” (por ejemplo, “no fue excelente”), el producto completo será cero, anulando toda la evidencia restante.
5 Suavización de Laplace
La suavización de Laplace resuelve el problema de las probabilidades cero añadiendo 1 al numerador y el tamaño del vocabulario \(V\) al denominador:
Veamos el cálculo concreto para las mismas dos palabras, con \(V = 29\):
Ejemplo con “excelente” (freq Pos = 4, freq Neg = 0):
Ejemplo con “comida” (freq Pos = 4, freq Neg = 2):
Observa cómo el +1 en el numerador elimina las probabilidades cero: “excelente” ya no tiene \(P(\text{excelente} \mid \text{Neg}) = 0\), sino 0.021. El +V en el denominador compensa ese aumento para que todas las probabilidades sigan sumando 1.
La siguiente tabla muestra una selección de las palabras más representativas. La tabla completa se genera con el código:
V = len(vocabulario) # 29 palabras únicas
print("Con suavización de Laplace:")
for w in vocabulario:
p_pos = (freq_pos.get(w, 0) + 1) / (n_pos + V)
p_neg = (freq_neg.get(w, 0) + 1) / (n_neg + V)
print(f"{w:18s} P(w|Pos)={p_pos:.3f} P(w|Neg)={p_neg:.3f}")
| Palabra | \(P(w \mid \text{Pos})\) | \(P(w \mid \text{Neg})\) |
|---|---|---|
| excelente | 0.098 | 0.025 |
| comida | 0.098 | 0.075 |
| lugar | 0.073 | 0.025 |
| servicio | 0.049 | 0.050 |
| buena | 0.024 | 0.025 |
| amable | 0.024 | 0.025 |
| rica | 0.024 | 0.025 |
| lento | 0.024 | 0.050 |
| mala | 0.024 | 0.050 |
| terrible | 0.024 | 0.037 |
| Suma | 1.000 | 1.000 |
Tip
Propiedad clave. Con Laplace, “excelente” ya no tiene probabilidad cero en la clase negativa (0.026 en vez de 0). Las sumas por clase siguen siendo exactamente 1.
6 Verosimilitud logarítmica
En la práctica, multiplicar muchas probabilidades pequeñas produce resultados con desbordamiento numérico. La solución: trabajar con logaritmos, convirtiendo productos en sumas.
6.1 Lambda: la señal de cada palabra
Definimos Lambda (\(\lambda\)) como el logaritmo de la razón de verosimilitudes:
| \(\lambda\) | Significado |
|---|---|
| \(> 0\) | Palabra positiva (señal a favor de Pos) |
| \(= 0\) | Palabra neutral (no discrimina) |
| \(< 0\) | Palabra negativa (señal a favor de Neg) |
La siguiente tabla muestra las palabras más representativas. La tabla completa se genera con el código:
import numpy as np
print("Lambda por palabra:")
for w in vocabulario:
p_pos = (freq_pos.get(w, 0) + 1) / (n_pos + V)
p_neg = (freq_neg.get(w, 0) + 1) / (n_neg + V)
lam = np.log(p_pos / p_neg)
print(f" {w:18s} λ={lam:+.3f}")
| Palabra | \(\lambda\) | Tipo |
|---|---|---|
| excelente | +1.366 | positiva |
| lugar | +1.072 | positiva |
| comida | +0.268 | positiva |
| rica | −0.041 | neutral |
| servicio | −0.020 | neutral |
| amable | −0.041 | neutral |
| buena | −0.041 | neutral |
| lento | −0.734 | negativa |
| mala | −0.734 | negativa |
| terrible | −0.432 | negativa |
Nota
¿Por qué “comida” no es neutral? Aunque “comida” aparece en ambas clases, aparece el doble de veces en reseñas positivas (4 de 31) que en negativas (2 de 18). Esa proporción desbalanceada le da un λ de +0.268, una señal positiva modesta. “Servicio”, en cambio, aparece 2 veces en cada clase y es mucho más neutral (λ = −0.020).
6.2 Puntuación de un tweet
La puntuación completa es:
| score | Decisión |
|---|---|
| \(> 0\) | Positivo |
| \(< 0\) | Negativo |
| \(= 0\) | Indeciso |
7 Ejemplo: clasificar una reseña nueva
Reseña: “comida excelente pero lenta”
7.1 Paso 1: Preprocesar
Tokens: [comida, excelente, pero, lenta]
“pero” no está en nuestro vocabulario (no apareció en entrenamiento), se ignora.
7.2 Paso 2: Sumar Lambdas
| Palabra | \(\lambda\) |
|---|---|
| comida | +0.268 |
| excelente | +1.366 |
| lenta | −0.734 (usando “lento”) |
| Suma | +0.900 |
7.3 Paso 3: Añadir log-previo
7.4 Paso 4: Decisión
La reseña se clasifica como positiva. Aunque “lenta” es negativa, “excelente” tiene un peso mucho mayor (λ = +1.366 vs. λ = −0.734), y el previo ya inclina la balanza hacia positivo.
Reseña 2: “comida mala servicio lento”
| Palabra | \(\lambda\) |
|---|---|
| comida | +0.268 |
| mala | −0.734 |
| servicio | −0.020 |
| lento | −0.734 |
| Suma | −1.220 |
Reseña 3: “lugar acogedor comida rica”
| Palabra | \(\lambda\) |
|---|---|
| lugar | +1.072 |
| acogedor | +1.366 |
| comida | +0.268 |
| rica | −0.041 |
| Suma | +2.665 |
Nota
Efecto del previo. Con clases equilibradas (50/50), el previo sería 0 y la decisión dependería solo de las palabras. Aquí, el previo de 0.336 da una “ventaja” de +0.336 a la clase positiva antes de mirar cualquier palabra. Esto es realista: en reseñas de restaurantes, las positivas suelen ser más frecuentes.
8 Variantes de Naive Bayes
Existen tres variantes principales, cada una adecuada a un tipo de dato diferente:
| Variante | Tipo de dato | Distribución | Ejemplo |
|---|---|---|---|
| Multinomial | Frecuencias (conteos) | Multinomial | Clasificación de texto (BOW) |
| Bernoulli | Presencia/ausencia (0/1) | Bernoulli | Spambinario, clasificación de documentos |
| Gaussiano | Variables continuas | Normal (gaussiana) | Clasificación con features numéricas |
La Multinomial es la más usada para texto y es exactamente la que hemos implementado: cuenta frecuencias y estima probabilidades condicionales.
# scikit-learn ofrece las tres variantes
from sklearn.naive_bayes import MultinomialNB, BernoulliNB, GaussianNB
# Multinomial: para frecuencias
modelo_multi = MultinomialNB()
# Bernoulli: para presencia/ausencia
modelo_bern = BernoulliNB()
# Gaussiano: para datos continuos
modelo_gauss = GaussianNB()
8.1 Naive Bayes Gaussiano en detalle
Mientras el Multinomial cuenta frecuencias (apariciones de palabras), el Gaussiano estima la densidad de probabilidad de cada feature asumiendo una distribución normal.
¿Cuándo se usa? Cuando los datos son numéricos continuos: medidas de flores (largo del pétalo, ancho del sépalo), datos médicos (presión, temperatura, edad), financieros (ingresos, gastos)…
Cómo “entrena”: Para cada clase, estima dos parámetros de cada feature:
Es decir, la media y la varianza de cada feature dentro de cada clase.
Cómo predice: La probabilidad de que un dato \(x_i\) pertenezca a la clase \(c\) se calcula con la fórmula de la gaussiana:
Y la clasificación:
8.1.1 Ejemplo paso a paso
Clasificar flores como Setosa o Versicolor según el largo del pétalo.
Lo que sabemos (entrenamiento):
| Clase | \(\mu\) (media) | \(\sigma^2\) (varianza) |
|---|---|---|
| Setosa | 1.5 cm | 0.005 |
| Versicolor | 4.6 cm | 0.008 |
Pregunta: ¿A qué clase pertenece un pétalo de 3.0 cm?
Paso 1: Calcular \(P(3.0 \mid \text{Setosa})\)
La gaussiana dibuja una campana centrada en la media. Cuanto más lejos estés de la media, más bajo es el valor. Calculemos la distancia de 3.0 a la media de Setosa:
- Distancia al cuadrado: \((3.0 - 1.5)^2 = 2.25\)
- Dos veces la varianza: \(2 \cdot 0.005 = 0.01\)
- Número de “varianzas de distancia”: \(\frac{2.25}{0.01} = 225\)
- \(e^{-225} \approx 0\)
Resultado: \(P(3.0 \mid \text{Setosa}) \approx 0\)
¿Por qué? Porque 3.0 cm está a 1.5 cm de la media de Setosa (1.5), pero la varianza es tiny (0.005). Los pétalos de Setosa siempre están muy cerca de 1.5 cm. Un valor de 3.0 es “imposible” para Setosa.
Paso 2: Calcular \(P(3.0 \mid \text{Versicolor})\)
- Distancia al cuadrado: \((3.0 - 4.6)^2 = 2.56\)
- Dos veces la varianza: \(2 \cdot 0.008 = 0.016\)
- Número de “varianzas de distancia”: \(\frac{2.56}{0.016} = 160\)
- \(e^{-160} \approx 0\)
Resultado: \(P(3.0 \mid \text{Versicolor}) > P(3.0 \mid \text{Setosa})\)
Aunque ambas probabilidades son extremadamente pequeñas, \(P(3.0 \mid \text{Versicolor})\) es muchísimo mayor que \(P(3.0 \mid \text{Setosa})\):
Ambos son números absurdamente pequeños. Pero \(10^{-70}\) es \(10^{28}\) veces mayor que \(10^{-98}\). Es como comparar el tamaño de un átomo con el del universo observable: los dos son pequeñísimos, pero uno es inmensamente mayor que el otro.
Naive Bayes no necesita probabilidades “grandes”; solo necesita que la razón entre ellas sea clara. Y aquí \(10^{28}\) es una ventaja abrumadora a favor de Versicolor.
8.1.2 Ejemplo 2: un pétalo más cercano a Setosa
Ahora clasifiquemos un pétalo de 1.7 cm, mucho más cerca de la media de Setosa (1.5 cm):
Paso 1: \(P(1.7 \mid \text{Setosa})\)
- \((1.7 - 1.5)^2 = 0.04\)
- \(\frac{0.04}{2 \cdot 0.005} = 4\)
- \(e^{-4} = 0.018\)
Paso 2: \(P(1.7 \mid \text{Versicolor})\)
- \((1.7 - 4.6)^2 = 8.41\)
- \(\frac{8.41}{2 \cdot 0.008} = 525.6\)
- \(e^{-525.6} \approx 0\)
Resultado:
Setosa gana por goleada. La razón es infinita (o al menos enorme). Un pétalo de 1.7 cm es típico de Setosa y prácticamente imposible de Versicolor.
Comparación visual:
Setosa (μ=1.5): Versicolor (μ=4.6):
│ ╱╲ │ ╱╲
│ ╱ ╲ │ ╱ ╲
│╱ ╲ │╱ ╲
┼──●───╲──────→ ┼──────╲──●──→
1.0 1.5 2.0 4.0 4.6 5.2
↑ ↑
1.7 cm 1.7 cm
(cerca del centro, (muy lejos,
P = 0.018) P ≈ 0)
Lo que nos enseñan los dos ejemplos:
| Pétalo | \(P(\cdot \mid \text{Setosa})\) | \(P(\cdot \mid \text{Versicolor})\) | Clase |
|---|---|---|---|
| 3.0 cm | \(10^{-98}\) | \(10^{-70}\) | Versicolor |
| 1.7 cm | \(0.018\) | \(\approx 0\) | Setosa |
La gaussiana es sensible a la distancia a la media: cuanto más cerca estés del centro de una clase, más alta es la probabilidad. Cuanto más lejos, más la “castiga”.
La intuición visual:
Setosa (μ=1.5): Versicolor (μ=4.6):
│ ╱╲ │ ╱╲
│ ╱ ╲ │ ╱ ╲
│╱ ╲ │╱ ╲
┼──────╲──────→ ┼──────╲──────→
1.0 1.5 2.0 4.0 4.6 5.2
↑ ↑
3.0 cm está aquí 3.0 cm está aquí
(muy lejos, P≈0) (lejos pero menos, P>bajo)
3.0 cm está “en la cola” de ambas campanas, pero más cerca del centro de Versicolor que del centro de Setosa.
from sklearn.datasets import load_iris
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import cross_val_score
iris = load_iris()
gnb = GaussianNB()
scores = cross_val_score(gnb, iris.data, iris.target, cv=5)
print(f"Accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
9 Aplicaciones más allá del sentimiento
9.1 Filtrado de spam
Dado un correo, clasificarlo como spam o ham:
Palabras como “gratis”, “oferta”, “urgente” tienen alta verosimilitud en la clase spam. Naive Bayes fue uno de los primeros filtros de spam exitosos y sigue siendo un baseline sólido.
9.2 Clasificación de temas
Dado un artículo de periódico, determinar si es de deportes, política o economía:
Se calcula la probabilidad para cada tema y se elige el máximo.
9.3 Filtrado de contenido
Dada una página web, decidir si es apropiada para menores. Las palabras del vocabulario se asocian con categorías de contenido, y Naive Bayes estima la probabilidad de cada categoría.
10 Limitaciones
10.1 La suposición de independencia
Naive Bayes asume que las palabras son independientes dado la clase. Esto es falso en el lenguaje natural:
- “comida” y “excelente” tienden a aparecer juntas en reseñas positivas
- “no” y “bueno” juntos cambian el sentido por completo
- El orden importa: “no fue bueno” ≠ “fue bueno”
A pesar de esto, Naive Bayes funciona bien porque la clasificación solo necesita que el orden de las probabilidades sea correcto, no que los valores sean exactos.
10.2 Sensibilidad al preprocesamiento
- Eliminar “no” como stop word puede cambiar el sentimiento
- El stemming puede unir palabras con sentidos diferentes
- Los signos de exclamación son señales útiles que se pierden
10.3 Desbalance de clases
Si el corpus de entrenamiento tiene 90% positivas y 10% negativas, pero la realidad es 50/50, las previas serán incorrectas y el modelo será sistemáticamente optimista.
10.4 Ataques adversarios
El sarcasmo y la ironía confunden a Naive Bayes:
“Qué lugar tan ‘excepcional’, 3 horas de espera y la comida fría”
Las comillas y el tono indican sarcasmo, pero para Naive Bayes “excepcional” es una palabra positiva.
Tip
Cuándo usar Naive Bayes. Es ideal como baseline rápido: entrenar toma segundos y da un punto de referencia sólido. Si un modelo más complejo no supera significativamente a Naive Bayes, probablemente no merece la complejidad adicional. En clasificación de texto, Naive Bayes a menudo iguala o supera a modelos mucho más sofisticados.