El algoritmo Naive Bayes es un clasificador probabilístico basado en el teorema de Bayes con supuestos de independencia entre las características. Es especialmente adecuado para la clasificación de texto. Antes, recordemos algunos conceptos básicos:
Teorema de Bayes: El fundamento detrás del algoritmo Naive Bayes es el teorema de Bayes, que se formula como:
P(C|D)=P(D|C)⋅P(C)P(D)Donde:
Independencia: Dentro del Teorema de Bayes, el concepto de independencia se refiere a la suposición de que ciertas variables o eventos no afectan la probabilidad de otros eventos o variables.
Cuando hablamos del clasificador Naive Bayes, nos referimos específicamente a la independencia condicional de las características dado un resultado o clase particular. Esta es la suposición "ingenua" (naive) que le da nombre al método.
En términos matemáticos, la independencia condicional en Naive Bayes se expresa así:
P(X1,X2,…,Xn|Y)=P(X1|Y)⋅P(X2|Y)…P(Xn|Y)
Donde:
Lo que esto significa es que, dado un valor particular de Y, la probabilidad conjunta de todas las características es simplemente el producto de sus probabilidades individuales. En otras palabras, estamos asumiendo que la presencia (o ausencia) de una característica no afecta la presencia (o ausencia) de cualquier otra característica, siempre que conozcamos la clase Y.
Este supuesto simplifica enormemente los cálculos y, aunque rara vez es cierto en la práctica (especialmente en el procesamiento del lenguaje natural donde las palabras están frecuentemente relacionadas entre sí), el clasificador Naive Bayes puede ser sorprendentemente eficaz en muchas situaciones a pesar de su suposición de independencia.
Veamos un ejemplo. Supongamos que queremos clasificar frases entre las categorías "Cine" y "Literatura". Las frases de entrenamiento son:
Frases de entrenamiento:
Algunas palabras, como "libro", "película", y "autor", aparecen en ambas categorías.
La probabilidad a priori de cada categoría es:
P(Cine)=59Esto viene a significar que una frase a clasificar tiene, a priori, una probabilidad de 59 de ser de la categoría "Cine" y una probabilidad de 49 de ser de la categoría "Literatura".
Ahora, calculamos las probabilidades condicionales para cada palabra en cada categoría (eliminamos previamente las stop-words). Fíjate que hay palabras que aparecen en ambas categorías. Por ejemplo, la palabra "libro" aparece en dos frases de "Literatura" y en una frase de "Cine". La palabra "autor" aparece en una frase de "Literatura" y en otra frase de "Cine". Y así sucesivamente. Por tanto, tenemos que:
P(libro|Literatura)=218...y así sucesivamente para las demás palabras.
Nos podemos dar cuenta de que puede haber palabras que no aparezcan en una categoría. Por ejemplo, la palabra "paisajes" no aparece en ninguna frase de "Cine". En este caso, la probabilidad condicional es cero:
P(paisajes|Cine)=0Esto es un problema, porque si multiplicamos muchas probabilidades condicionales, el resultado será cero. Para evitar esto, podemos usar un suavizado (smoothing) para evitar que las probabilidades condicionales sean cero. Por ejemplo, podemos usar el suavizado de Laplace, que consiste en sumar un valor α (normalmente, 1) al numerador y el número de palabras únicas (vocabulario) en el denominador.
P(wi|Ck)=Número de veces que wi aparece en Ck+αTotal de palabras en Ck+α×Tamaño del vocabulario...y así sucesivamente para las demás palabras.
El motivo de sumar el tamaño del vocabulario en el denominador es para que la suma de todas las probabilidades condicionales sea 1.
Vayamos haciendo un script en Python para calcular las probabilidades condicionales de cada palabra en cada categoría:
import nltk
# nltk.download()
documents = [
"La película fue emocionante y llena de acción.",
"Ese libro tiene una trama intrigante.",
"Los actores hicieron un trabajo excelente.",
"El autor describe paisajes con gran detalle.",
"El cine de autor siempre me ha fascinado.",
"La novela estaba llena de giros inesperados.",
"El guion de esa película fue escrito por un famoso novelista.",
"Los personajes del libro eran muy realistas.",
"Esa película está basada en un libro aclamado."
]
labels = [0, 1, 0, 1, 0, 1, 0, 1, 0]
label_names = {0: "Cine", 1: "Literatura"}
# Preprocesamiento
def preprocess(docs):
txts = [doc.lower().replace(".", "") for doc in docs]
txts = [doc.split() for doc in txts]
# Eliminación de stopwords
stopwords = nltk.corpus.stopwords.words("spanish")
txts = [[word for word in doc if word not in stopwords] for doc in txts]
return txts
documents = preprocess(documents)
# Clasificador Naive Bayes
# Creación del vocabulario
def get_vocab(docs):
vocab = set()
for doc in docs:
for word in doc:
vocab.add(word)
vocab = list(vocab)
return vocab
vocab = get_vocab(documents)
print(len(vocab), "palabras en el vocabulario")
# Creación de la matriz de características
import numpy as np
X = np.zeros((len(documents), len(vocab)))
for i, doc in enumerate(documents):
for word in doc:
j = vocab.index(word)
X[i][j] += 1
# Probabilidades a priori
def get_prior(labels):
prior = np.zeros(2)
for label in labels:
prior[label] += 1
prior = prior / len(labels)
return prior
prior = get_prior(labels)
# Probabilidades condicionales
def get_conditional(X, labels, alpha=1):
conditional = np.ones((2, X.shape[1])) * alpha
for i, label in enumerate(labels):
conditional[label] += X[i]
print("Número de palabras en cine:", int(conditional[0].sum()))
print("Número de palabras en literatura:", int(conditional[1].sum()))
conditional = conditional / (conditional.sum(axis=1).reshape(-1, 1) + alpha * len(vocab))
return conditional
conditional = get_conditional(X, labels, 0.1)
print("Probabilidades a priori:")
print(label_names[0], round(prior[0], 3))
print(label_names[1], round(prior[1], 3))
print()
from tabulate import tabulate
table = []
for i, w in enumerate(vocab):
table.append([i, w, round(conditional[0][i],4), round(conditional[1][i],4)])
print(tabulate(table, headers=["idx", "Palabra", "Pr. Cine", "Pr. Literatura"]))
30 palabras en el vocabulario Número de palabras en cine: 24 Número de palabras en literatura: 18 Probabilidades a priori: Cine 0.556 Literatura 0.444 idx Palabra Pr. Cine Pr. Literatura ----- ----------- ---------- ---------------- 0 excelente 0.0407 0.0048 1 libro 0.0407 0.1 2 inesperados 0.0037 0.0524 3 fascinado 0.0407 0.0048 4 novela 0.0037 0.0524 5 detalle 0.0037 0.0524 6 describe 0.0037 0.0524 7 novelista 0.0407 0.0048 8 basada 0.0407 0.0048 9 famoso 0.0407 0.0048 10 cine 0.0407 0.0048 11 acción 0.0407 0.0048 12 autor 0.0407 0.0524 13 aclamado 0.0407 0.0048 14 siempre 0.0407 0.0048 15 paisajes 0.0037 0.0524 16 gran 0.0037 0.0524 17 película 0.1148 0.0048 18 realistas 0.0037 0.0524 19 llena 0.0407 0.0524 20 actores 0.0407 0.0048 21 intrigante 0.0037 0.0524 22 escrito 0.0407 0.0048 23 personajes 0.0037 0.0524 24 emocionante 0.0407 0.0048 25 guion 0.0407 0.0048 26 trama 0.0037 0.0524 27 trabajo 0.0407 0.0048 28 giros 0.0037 0.0524 29 hicieron 0.0407 0.0048
# Clasificación
def predict(doc, vocab, prior, conditional):
x = np.zeros(len(vocab))
for word in doc:
j = vocab.index(word)
x[j] += 1
p = prior.copy()
for i in range(len(p)):
for j in range(len(x)):
if vocab[j] in doc:
print(vocab[j], round(conditional[i][j],3))
p[i] *= conditional[i][j] ** x[j]
print("------ Probabilidad de ser de la clase " + label_names[i] + ":", round(p[i],7), "\n")
return np.argmax(p)
docs = ["el libro es de un autor."]
# docs = ["el libro es de un autor de trama."]
# docs = ["el libro es de un autor aclamado."]
docs = preprocess(docs)
for doc in docs:
print("\nPredicción: " + label_names[predict(doc, vocab, prior, conditional)])
autor 0.041 libro 0.041 ------ Probabilidad de ser de la clase Cine: 0.0009221 autor 0.052 libro 0.1 ------ Probabilidad de ser de la clase Literatura: 0.002328 Predicción: Literatura
Fíjate en las probabilidades que nos resultan para cada clase, son muy pequeñas. Si el tamaño de nuestro vocabulario fuera mucho mayor (lo que sería muy normal) las probabilidades serían aún mucho más pequeñas y podríamos tener problemas de precisión numérica para calcularlas. Es una práctica común y recomendada transformar las probabilidades con logaritmos cuando se trabaja con Naive Bayes, precisamente para evitar problemas de precisión numérica. Los productos de probabilidades pequeñas pueden acercarse a cero en la aritmética de punto flotante, lo que puede dar lugar a errores o inestabilidades.
Dada la propiedad del logaritmo: log(a⋅b)=log(a)+log(b)
Puedes transformar las multiplicaciones de probabilidades en sumas de logaritmos.
Si estás calculando: P(Ck|documento)=P(Ck)⋅∏iP(wi|Ck)P(documento)
Puedes tomar el logaritmo en ambos lados: log(P(Ck|documento))=log(P(Ck))+∑ilog(P(wi|Ck))−log(P(documento))
Al clasificar un documento, calculas el valor anterior para cada clase y eliges la clase con el valor más alto. No es necesario convertir estos valores de nuevo usando la función exponencial porque el logaritmo es una función monótona creciente. Por lo tanto, si log(a)>log(b), entonces a>b.
Al trabajar con sumas en lugar de productos, evitas los problemas de precisión numérica y, además, el cálculo se vuelve computacionalmente más eficiente.
import math
def predict(doc, vocab, prior, conditional):
x = np.zeros(len(vocab))
for word in doc:
j = vocab.index(word)
x[j] += 1
# Tomar el logaritmo de los priors
p = [math.log(prior_val) for prior_val in prior]
for i in range(len(p)):
for j in range(len(x)):
if vocab[j] in doc:
print(vocab[j], round(math.log(conditional[i][j]), 3))
# Sumar el logaritmo de las probabilidades
p[i] += x[j] * math.log(conditional[i][j])
print("------ Probabilidad de ser de la clase " + label_names[i] + ":", p[i], "\n")
return np.argmax(p)
docs = ["el libro es de un autor."]
docs = preprocess(docs)
for doc in docs:
print("\n")
print("\nPredicción: " + label_names[predict(doc, vocab, prior, conditional)])
autor -3.201 libro -3.201 ------ Probabilidad de ser de la clase Cine: -6.988840037302129 autor -2.949 libro -2.303 ------ Probabilidad de ser de la clase Literatura: -6.062727567129474 Predicción: Literatura
Modifica el código anterior para poder hacer la clasificación entre tres categorías: "Cine", "Literatura" y "Música".
documents = [
"La película fue emocionante y llena de acción.",
"Ese libro tiene una trama intrigante.",
"Los actores hicieron un trabajo excelente.",
"El autor describe paisajes con gran detalle.",
"El cine de autor siempre me ha fascinado.",
"La novela estaba llena de giros inesperados.",
"El guion de esa película fue escrito por un famoso novelista.",
"Los personajes del libro eran muy realistas.",
"Esa película está basada en un libro aclamado.",
"El nuevo álbum de la banda es increíble.",
"El concierto en el estadio estuvo lleno.",
"La guitarra eléctrica tiene un sonido potente y claro.",
"Los festivales de música al aire libre son mis favoritos.",
"El pianista interpretó una pieza clásica maravillosamente.",
"La lista de reproducción incluye varios géneros, desde jazz hasta rock."
]
labels = [0, 1, 0, 1, 0, 1, 0, 1, 0, 2, 2, 2, 2, 2, 2]
label_names = {0: "Cine", 1: "Literatura", 2: "Música"}
documents_test = [
"El libro estaba basado en emocionantes paisajes.",
"El guion fue aclamado por su trama intrigante.",
"La banda tocó jazz y rock en el concierto.",
"El cine muestra películas emocionantes de acción.",
"El pianista tocó una pieza de música clásica."
]
labels_test = [1, 0, 2, 0, 2]
¿Qué ocurre cuando tenemos palabras en un conjunto de test que no están en el vocabulario del conjunto de entrenamiento? ¿Cómo podríamos solucionarlo?
https://www.nltk.org/index.html
NLTK, que significa "Natural Language Toolkit", es una biblioteca líder en Python para trabajar con datos de lenguaje humano. Fue creado con el objetivo de facilitar la investigación y el desarrollo en el procesamiento del lenguaje natural (NLP) y la lingüística computacional.
Algunas características y capacidades clave de NLTK incluyen:
NLTK es ampliamente utilizado en la academia y la industria debido a su versatilidad, amplia gama de funcionalidades y comunidad activa. Es especialmente útil para prototipos y enseñanza debido a su diseño claro y documentación extensa. Sin embargo, para ciertas aplicaciones de producción o tareas que requieren alta eficiencia, otros frameworks o bibliotecas, como spaCy o scikit-learn, podrían ser más adecuados.
import nltk
# nltk.download()
from nltk.tokenize import word_tokenize
# Datos de entrenamiento
texts = [
"La película fue emocionante y llena de acción.",
"Ese libro tiene una trama intrigante.",
"Los actores hicieron un trabajo excelente.",
"El autor describe paisajes con gran detalle.",
"El cine de autor siempre me ha fascinado.",
"La novela estaba llena de giros inesperados.",
"El guion de esa película fue escrito por un famoso novelista.",
"Los personajes del libro eran muy realistas.",
"Esa película está basada en un libro aclamado.",
"El nuevo álbum de la banda es increíble.",
"El concierto en el estadio estuvo lleno.",
"La guitarra eléctrica tiene un sonido potente y claro.",
"Los festivales de música al aire libre son mis favoritos.",
"El pianista interpretó una pieza clásica maravillosamente.",
"La lista de reproducción incluye varios géneros, desde jazz hasta rock."
]
labels = [0, 1, 0, 1, 0, 1, 0, 1, 0, 2, 2, 2, 2, 2, 2]
label_names = {0: "Cine", 1: "Literatura", 2: "Música"}
# Convertir texto a formato NLTK
documents = [(word_tokenize(text), label) for text, label in zip(texts, labels)]
all_words = nltk.FreqDist(w.lower() for w in word_tokenize(' '.join(texts)))
word_features = list(all_words)[:2000] # Usamos las 2000 palabras más comunes como características, aunque en este ejemplo no es necesario ya que son pocas palabras.
def document_features(document):
document_words = set(document)
features = {}
for word in word_features:
features['contains({})'.format(word)] = (word in document_words)
return features
featuresets = [(document_features(d), c) for (d,c) in documents]
train_set = featuresets
classifier = nltk.NaiveBayesClassifier.train(train_set)
test_texts = [
"El libro estaba basado en emocionantes paisajes.",
"El guion fue aclamado por su trama intrigante.",
"La banda tocó jazz y rock en el concierto.",
"El cine muestra películas emocionantes de acción.",
"El pianista tocó una pieza de música clásica."
]
labels_test = [1, 0, 2, 0, 2]
for text in test_texts:
print(classifier.classify(document_features(word_tokenize(text))))
1 0 2 0 2
https://scikit-learn.org/stable/index.html
scikit-learn
(comúnmente conocido como sklearn
) es una biblioteca de código abierto para Python que proporciona herramientas simples y eficientes para el análisis y modelado de datos. Está construida sobre NumPy
, SciPy
y matplotlib
. Aunque no está diseñada específicamente para el procesamiento del lenguaje natural (NLP), ofrece herramientas y utilidades que son ampliamente utilizadas en tareas de NLP.
Aquí hay un resumen de sus características principales:
Modelos de Aprendizaje Supervisado: Incluye algoritmos como máquinas de soporte vectorial, regresión logística, árboles de decisión, random forests, gradient boosting, k-vecinos más cercanos y muchos otros.
Modelos de Aprendizaje No Supervisado: Incluye algoritmos como clustering (k-means, clustering jerárquico, DBSCAN), reducción de dimensionalidad (PCA, t-SNE, NMF), y detección de outliers.
Herramientas de Selección y Evaluación de Modelos: Proporciona métodos para la validación cruzada, ajuste de hiperparámetros (como búsqueda en grilla y búsqueda aleatoria), métricas de rendimiento, y más.
Transformadores y Pipelines: sklearn
incluye herramientas para preprocesar datos, como normalización, estandarización, codificación one-hot, y otras transformaciones. La funcionalidad de "pipelines" permite combinar transformadores y estimadores en una secuencia coherente de pasos de procesamiento.
Interoperabilidad: scikit-learn
es compatible con estructuras de datos de Python estándar como listas y arrays de NumPy
, y también con estructuras de datos de pandas
.
Documentación y Comunidad: Una de las grandes ventajas de scikit-learn
es su extensa documentación que incluye muchos ejemplos prácticos. Además, tiene una comunidad activa que contribuye regularmente a su desarrollo y mejora.
Diseño Consistente: Una de las características distintivas de sklearn
es su API coherente. Una vez que te familiarizas con los fundamentos (como fit()
, transform()
, predict()
), es fácil de usar con muchos algoritmos diferentes.
Dado que es una herramienta versátil y ampliamente adoptada, scikit-learn
es una elección popular para muchos profesionales del campo de la ciencia de datos y el aprendizaje automático.
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import make_pipeline
# Datos de entrenamiento
texts = [
"La película fue emocionante y llena de acción.",
"Ese libro tiene una trama intrigante.",
"Los actores hicieron un trabajo excelente.",
"El autor describe paisajes con gran detalle.",
"El cine de autor siempre me ha fascinado.",
"La novela estaba llena de giros inesperados.",
"El guion de esa película fue escrito por un famoso novelista.",
"Los personajes del libro eran muy realistas.",
"Esa película está basada en un libro aclamado.",
"El nuevo álbum de la banda es increíble.",
"El concierto en el estadio estuvo lleno.",
"La guitarra eléctrica tiene un sonido potente y claro.",
"Los festivales de música al aire libre son mis favoritos.",
"El pianista interpretó una pieza clásica maravillosamente.",
"La lista de reproducción incluye varios géneros, desde jazz hasta rock."
]
labels = [0, 1, 0, 1, 0, 1, 0, 1, 0, 2, 2, 2, 2, 2, 2]
En scikit-learn
, un Pipeline es una forma de automatizar y simplificar un flujo de trabajo que involucra múltiples pasos de procesamiento y modelado. A menudo, en el aprendizaje automático, los datos pasan por una serie de etapas de preprocesamiento antes de ser utilizados por un algoritmo de aprendizaje. Un Pipeline ayuda a definir y automatizar estos pasos secuenciales.
CountVectorizer
implementa el modelo "Bag-of-Words" (BoW). El enfoque BoW se refiere al proceso de convertir texto en una representación numérica basada en la frecuencia de las palabras en el texto, sin tener en cuenta el orden o la estructura de las frases.
Cuando utilizas CountVectorizer
, básicamente estás aplicando el proceso de BoW:
Tokenización: Divide el texto en palabras individuales (o tokens).
Construcción del vocabulario: Se crea un vocabulario con todas las palabras únicas que aparecen en el conjunto de datos.
Vectorización: Se codifica cada documento en función de la frecuencia de las palabras del vocabulario en ese documento.
El resultado es una matriz en la que cada fila representa un documento y cada columna representa una palabra del vocabulario. El valor en una posición específica de la matriz indica la frecuencia con la que la palabra (columna) aparece en el documento (fila).
MultinomialNB
implementa el algoritmo de Naive Bayes multinomial diseñado especialmente para características discretas (como cuentas de palabras en textos).
model = make_pipeline(CountVectorizer(), MultinomialNB())
model.fit(texts, labels) # Entrenamiento del modelo
Pipeline(steps=[('countvectorizer', CountVectorizer()), ('multinomialnb', MultinomialNB())])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
Pipeline(steps=[('countvectorizer', CountVectorizer()), ('multinomialnb', MultinomialNB())])
CountVectorizer()
MultinomialNB()
# Frases de test
test_texts = [
"El libro estaba basado en emocionantes paisajes.",
"El guion fue aclamado por su trama intrigante.",
"La banda tocó jazz y rock en el concierto.",
"El cine muestra películas emocionantes de acción.",
"El pianista tocó una pieza de música clásica."
]
labels_test = [1, 0, 2, 0, 2]
# Predicciones
predicted_labels = model.predict(test_texts)
print(predicted_labels)
[1 0 2 0 2]