Clasificación de Texto con Naive Bayes en Python

En este post vas a aprender qué es Naive Bayes, cómo funciona y cómo usarlo en Python. Al fin y al cabo, aunque Naive Bayes es uno de los modelos más simples dentro del mundo del Machine Learning, es también muy utilizado en proyectos de procesamiento de lenguaje natural. Por eso, Naive Bayes es un modelo que, sí o sí, toda persona en el mundo del Machine Learning debe dominar.

Así pues, en este post aprenderás:

  1. Qué es y cómo funciona Naive Bayes.
  2. Cómo puedes aplicar Naive Bayes en Python. Además, incluye un ejemplo práctico de clasificación de texto.
  3. Cuáles son las ventajas y desventajas de usar Naive Bayes.

¿Te suena interesante? ¡Vamos con ello!

Cómo funciona Naive Bayes

Naive Bayes es un modelo bayesiano, es decir, probabilístico. Esto quiere decir que, las predicciones se basan en calcular probabilidades.

Además, tal como su nombre indica, es un modelo Naive, es decir, ingénuo. Esto es porque Naive Bayes asume que el efecto de una variable es independiente al resto de variables. De esta forma, el cálculo de las probabilidades es mucho más sencillo.

En definitiva, si en un mensaje aparece la palabra «buenos» esto será independiente de que aparezce la palabra «días». Como puedes esperar, en la realidad esto no es así. Sin embargo, esta asunción facilita mucho los cálculos, tal como veremos más adelantes.

Y es que, en términos matemáticos, el cálculo matemático de Naive Bayes se traduce en la siguiente fórmula, la cual se conoce como regla de Bayes:

\(P(A|B) =\frac {P(B|A)P(A)}{P(B)}\)

  • P(A|B): probabilidad de que ocurra A sabiendo que B ya ha ocurrido.
  • P(B|A): probabilidad de que ocurra B sabiendo que se ha dado A.
  • P(A): probabilidad de que ocurra A.
  • P(B): probabilidad de que ocurra B.

Teniendo esto en cuenta, depende del tipo de distribución que usemos para calcular las probabilidades, tendremos un tipo u otro de implementación de Naive Bayes.

En general hay dos principales implementaciones de Naive Bayes: Naive Bayes Gaussiana y Naive Bayes Multinominal.

Naive Bayes Gaussiano

Como su nombre indica, Naive Bayes Gaussiano asume que los datos siguen una distribución Gaussiana. En este caso, si desarrollamos la fórmula que hemos visto anteriormente obtendremos la siguiente función:

\(P(x_i|y)= \frac{1}{\sqrt{2\pi\sigma^{2}{y}}}e^{\frac{-(x-\mu{y})^2}{2\sigma^{2}_{y}}}\)

Donde:

En el fondo, lo que Naive Bayes Gaussiano hace es suponer una distribución normal para cada variable y clase dado el promedio y desviación típica de los datos obtenidos. De esta forma, si vamos a hacer una clasificación binaria, para cada variable tendremos dos distribuciones (una por clase), tal como se muestra en la siguiente imagen:

Probabilidades en Naive Bayes Gaussiano

Así pues, cuando nos llegue una observación calcularemos la probabilidad de que ese valor provenga de cada una de las distribuciones, tal como se muestra en la siguiente imagen:

Calcular probabilidad en Naive Bayes Gaussiano

Repetiremos este proceso para cada una de las variables y obtendremos la probabilidad final de que esa observación pertenezca a cada grupo. Por último, aquella clase que obtenga mayor probabilidad será la predicción del algoritmo.

A la hora de obtener las probabilidades se suelen tomar logaritmos. De esta forma, se evitan problemas en el cálculo de la probabilidad total cuando la probabilidad en una de las variables es muy baja.

Ahora que entiendes cómo funciona Naive Bayes Gaussiano, veamos cómo funciona Naive Bayes Multinomial.

Naive Bayes Multinomial

Naive Bayes Multinomial, como ya supondrás, se basa en asumir una distribución Multinomial. La distribución Multinomial es una extensión de la distribución Binomial, de tal forma que la probabilidad de cada resultado es independiente y su suma siempre será la unidad (enlace).

Así pues, en el caso de Naive Bayes Multinomial deberemos:

  1. Calcular la probabilidad de que se de cada una de las clases.
  2. Probabilidad de que cada valor se de dentro de una misma clase.
  3. Para cada clase posible, computar la probabilidad final de que, dados los dados de entrada, esos datos pertenezcan a esa clase.

Dicho así, seguramente sea dificil de entender. Así pues, como este post va de entender Naive Bayes y, sobre todo, saber cómo hacer clasificación de texto en Python con Naive Bayes, veamos un ejemplo teórico.

Ejemplo del funcionamiento de Naive Bayes Multinomial

Pongamos que queremos clasificar mensajes de texto entre spam o no spam. Para ello contamos con 20 mensajes diferentes.

Así pues, lo primero de todo es crear una tabla que nos indique cuántas veces ha aparecido cada palabra en los casos en los que el mensaje era Spam y cuántas veces han aparecido en los mensajes no Spam. Supongamos que la tabla es la siguiente:

| Tipo de Documento | Estimado | Amigo | Comida | Dinero |
|-------------------|----------|-------|--------|--------|
| No Spam           | 8        | 5     | 3      | 1      |
| Spam              | 2        | 1     | 0      | 4      |

Partiendo de esta tabla podemos calcular cómo de probable es que aparezca la palabra «Estimado» dentro de un mensaje «No Spam». Esto no es más que la proporción de veces que la palabra «Estimado» ha salido en los mensajes «No Spam»:

\(P(Estimado|No Spam) = \frac {8}{8+5+3+1} = 0.47\)

Si hiciésemos este mismo proceso para cada una de las palabras y cada una de las clases, terminaríamos obteniendo la siguiente tabla:

| Tipo de Documento | Estimado | Amigo | Comida | Dinero |
|-------------------|----------|-------|--------|--------|
| No Spam           | 0.47     | 0.29  | 0.18   | 0.06   |
| Spam              | 0.29     | 0.14  | 0      | 0.57   |

Por otro lado, necesitaríamos también conocer la probabilidad de que una palabra sea Spam o no sea Spam, esto es, la proporción de palabras Spam y no Spam.

\(Prob(No Spam) = \frac{8 + 5 + 3 + 1}{8 + 5 + 3 + 1 + 2 + 1 + 0 + 4} = \frac{17}{17+7} = 0.71\)

\(Prob(Spam) = \frac{2 + 1 + 0 + 4}{8 + 5 + 3 + 1 + 2 + 1 + 0 + 4} = \frac{7}{17+7} = 0.29\)

Con esta información, supongamos que nos llegase un mensaje con las palabras «Estimado Amigo». Ahora sí, podríamos aplicar la fórmula anterior para poder clasificar dicho mensaje. Veámoslo:

\(P(No Spam) \times P(Estimado | No Spam) \times P(Amigo | No Spam) = 0.71 \times 0.47 \times 0.29 = 0.10\)

\(P(Spam) \times P(Estimado | Spam) \times P(Amigo | Spam) = 0.29 \times 0.29 \times 0.14 = 0.01\)

Como vemos, es más probable que ese mensaje sea No Spam que Spam. Por tanto, la predicción realizada por Naive Bayes es que ese mensaje no es Spam.

Problemas con probabilidades de cero

Seguramente el funcionamiento de Naive Bayes te sea intuitivo y tenga sentido. Sin embargo, ¿qué hubiese pasado si el mensaje que nos llega tiene las palabras «Dinero», «Comida» y «Dinero»? Veamoslo:

\(P(No Spam) = 0.71 \times 0.06 \times 0.18 \times 0.06 = 0.0004\)

\(P(Spam) = 0.29 \times 0.57 \times 0.0 \times 0.57 = 0\)

Como vemos, por mucho que intuitivamente el mensaje sea Spam, puesto que la palabra «Dinero» aparece mucho en mensajes Spam, el mensaje será clasificado como «No Spam». Esto es debido a que la palabra «Comida» nunca ha aparecido en un mensaje Spam, por lo que su probabilidad es de cero, haciendo que la probabilidad del mensaje sea de cero.

Para solucionar este problema se aplica Laplace. Laplace consiste, básicamente, en sumar 1 a todas las observaciones. De esta forma, sus probabilidades dejan de ser cero y nos evitamos el problema anterior. Veámoslo:

Tabla de Frecuencias sin Aplicar Laplace

| Tipo de Documento | Estimado | Amigo | Comida | Dinero |
|-------------------|----------|-------|--------|--------|
| No Spam           | 8        | 5     | 3      | 1      |
| Spam              | 2        | 1     | 0      | 4      |

Tabla de Frecuencias Aplicando Laplace:

| Tipo de Documento | Estimado | Amigo | Comida | Dinero |
|-------------------|----------|-------|--------|--------|
| No Spam           | 9        | 6     | 4      | 2      |
| Spam              | 3        | 2     | 1      | 5      |

Como ves, ahora todas las variables han aparecido al menos una vez, de tal forma que si calculamos las probabilidades no existe ninguna variable con probabilidad de cero:

| Tipo de Documento | Estimado | Amigo | Comida | Dinero |
|-------------------|----------|-------|--------|--------|
| No Spam           | 0.43     | 0.29  | 0.19   | 0.10   |
| Spam              | 0.27     | 0.18  | 0.09   | 0.45   |

Ahora, si volvemos a clasificar el mensaje con las palabras «Dinero», «Comida» y «Dinero» obtendremos la siguiente predicción:

\(P(No Spam) = 0.66 \times 0.10 \times 0.19 \times 0.10 = 0.0012\)

\(P(Spam) = 0.34 \times 0.45 \times 0.09 \times 0.45 = 0.0062\)

Como puedes ver, gracias a aplicar Laplace el mensaje ha pasado de ser clasificado (incorrectamente) como «No Spam» a ser clasificado (correctamente) como «Spam».

Hasta aquí la introducción teórica de Naive Bayes. Como ves, es un modelo muy simple pero que suele funcionar muy bien. Ahora bien, ¿cómo puedes aplicar Naive Bayes en Python? ¡Veamoslo!

Clásificación de texto en Python con Naive Bayes

La forma más sencilla de usar Naive Bayes en Python es, cómo no, usando Scikit Learn, la principal librería para uso de modelos de Machine Learning en Python.

Si no conoces Scikit Learn en profundidad, te recomiendo que leas este post.

Para poder usar el modelo Naive Bayes en Python, lo podemos encontrar dentro del módulo naive_bayes de Sklearn. Más concretamente, este módulo cuenta con seis modelos de Naive Bayes diferentes:  Gaussian Naive Bayes, Multinomial Naive Bayes, Complement Naive Bayes, etc.

Aunque haya varios modelos, en mi opinión los más usados son Gaussian Naive Bayes, que es el modelo Naive Bayes tradicional y Multinomial Naive Bayes que es el modelo de Naive Bayes que se suele aplicar en los proyectos de clasificación de texto.

Sabiendo esto, veamos cómo usar el modelo Naive Bayes en Python para clasificar texto.  

Caso práctico: detección de Spam en SMS

Como una de las principales fortalezas de Naive Bayes es, precisamente, su capacidad de usar muchas variables, vamos a usar un caso de este estilo: la detección de spam en SMSs.

Para ello, usaremos el dataset Spam Collection que se puede encontrar de forma grauita en Kaggle (enlace). En mi caso, leeré el fichero desde este repositorio.

Así pues, lo primero de todo tendremos que leer los datos:

# pip install requests
import requests 
import zipfile
import pandas as pd

url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00228/smsspamcollection.zip'
data_file = 'SMSSpamCollection'

# Make request
resp = requests.get(url)

# Get filename
filename = url.split('/')[-1]

# Download zipfile
with open(filename, 'wb') as f:
  f.write(resp.content)

# Extract Zip
with zipfile.ZipFile(filename, 'r') as zip:
  zip.extractall('')

# Read Dataset
data = pd.read_table(data_file, 
                     header = 0,
                     names = ['type', 'message']
                     )

# Show dataset
data.head()
	type	message
0	ham	Ok lar... Joking wif u oni...
1	spam	Free entry in 2 a wkly comp to win FA Cup fina...
2	ham	U dun say so early hor... U c already then say...
3	ham	Nah I don't think he goes to usf, he lives aro...
4	spam	FreeMsg Hey there darling it's been 3 week's n...

Ahora que tenemos los datos, veaoms cómo usar Naive Bayes en Python. ¡Vamos con ello!

Transformación de datos

Una vez tenemos los datos, lo primero de todo tenemos que procesarlos. Si bien el procesamiento de datos es un paso fundamental en todo proyecto de machine learning, en los proyectos de NLP (como este) procesar los datos resulta aún mucho más importante.

Así pues, lo primero de todo vamos seguir los siguientes procesos:

  1. Tokenización: consiste en separar los mensajes en palabras para así poder tratar cada una de las palabras. Esto lo podemos hacer gracias a la función word_tokenize.
  2. Eliminación de stop words: eliminación de palabras que no aportan valor (preposiciones, conjunciones, etc.). Esto se realiza puesto que aumentaría mucho el tamaño de nuestro dataset (ya de por sí grande) y no aportaría ningún valor, solo ruido. Existen dos grandes fuentes de stpwords en Python, las de Sklearn y las de la librería NLTK.
# Sklearn
from sklearn.feature_extraction import text
text.text.ENGLISH_STOP_WORDS
# NLTK
import nltk
nltk.download('stopwords')
from nltk.corpus import stopwords
stopwords.words('english')

En este ejemplo usamos las palabras de stopwords por defecto. Sin embargo, suele ser muy relevante ver el listado de palabras y añadir palabras al mismo, o incluso eliminar ciertas palabras.

# Sklearn
from sklearn.feature_extraction import text
text.text.ENGLISH_STOP_WORDS
# NLTK
import nltk
nltk.download('stopwords')
from nltk.corpus import stopwords
stopwords.words('english')

En este ejemplo usamos las palabras de stopwords por defecto. Sin embargo, suele ser muy relevante ver el listado de palabras y añadir palabras al mismo, o incluso eliminar ciertas palabras.

3. Stemming o lemmatization: el objetivo de este paso es que dos palabras que signifiquen lo mismo, pero no estén igual escritas, pasen a estar igual escritas. Al fin y al cabo, para el modelo la palabra «bueno» y la palabra «buena» son diferentes. Para hacer esto hay dos grandes técnicas:

  •  Stemming: el stemming consiste en la eliminación de las terminaciones de las palabras para quedarnos únicamnete con la raíz. Siguiendo el caso anterior, en ambos casos (bueno, buena) nos quedaríamos con «buen».
  • Lemmatization: es más utilizado en proyectos de NLP en inglés, ya que en este idioma bueno (good), mejor (better) y el mejor (best) son palabras completamente diferentes. En estos casos el stemming no funcionaría. Así pues, la lematización convertiría todas esas palabras a su basa (good), de tal forma que pasen a significar lo mismo.

Para realiazar todos estos procesos usaremos el paquete nltk, es decir, el Natural Language Toolkit (enlace), el cual incluye muchas funcionalidades sobre NLP para Python y que, sin duda, te serán muy utiles para usar Naive Bayes en Python.

import nltk

# Install everything necessary
nltk.download('punkt')
nltk.download('stopwords')


from nltk.stem.porter import *
from nltk.corpus import stopwords
stop = stopwords.words('english')

# Tokenize
data['tokens'] = data.apply(lambda x: nltk.word_tokenize(x['message']), axis = 1)

# Remove stop words
data['tokens'] = data['tokens'].apply(lambda x: [item for item in x if item not in stop])

# Apply Porter stemming
stemmer = PorterStemmer()
data['tokens'] = data['tokens'].apply(lambda x: [stemmer.stem(item) for item in x])
[nltk_data] Downloading package punkt to
[nltk_data]     C:\Users\Ander\AppData\Roaming\nltk_data...
[nltk_data]   Unzipping tokenizers\punkt.zip.
[nltk_data] Downloading package stopwords to
[nltk_data]     C:\Users\Ander\AppData\Roaming\nltk_data...
[nltk_data]   Unzipping corpora\stopwords.zip.

Perfecto, ya hemos hecho la limpieza de nuestros datos. Sin embargo, esto no acaba aquí. Y es que, actualmente únicamente disponemos de una columna que cuenta con un vector de palabras.

Tal como he explicado en el primer apartado teórico, Naive Bayes admite dos cuestiones:

  1. Una matriz TF, es decir, una matriz en la que aparece, para cada documento, cuántas veces ha aparecido cada una de las palabras que hay en en todos los documentos.
  2. Una matriz de apariciones. Es similar a una matriz TF, pero en este caso, en vez de indicar el número de apariciones, simplemente indica si esa palabra aparecía o no.

El uso de cada uno de ellas dependerá mucho del contextos. En el caso de SMSs, al ser mensajes muy cortos es poco probable que las palabras se repitan, por lo que seguramente ambos enfoques devuelvan el mismo resultado.

Sin embargo, en textos más largos seguramente sea más interesante aplicar una matriz TF que una matriz de apariciones.

Dicho esto, para poder llegar a una matriz TF vamos a hacer lo siguiente:

  1. Destokenizar los valores, de tal forma que la columna «tokens» no contenga listas, sino texto. Esto es necesario para que el tercer paso funcione correctamente.
  2. Hacer un split entre train y test. Es muy importante realizar el proceso de train y test antes de llegar a la matriz TF. Sino, tendremos problemas de data leakage y puede afectar a nuestro resultaod (incluso es nos permite comprobar que nuestro pipeline de datos es correcto).
  3. Aplicar la función CountVectorizer del módulo feature_extraction.text de Sklearn a nuestros datos de train y test. Esta función nos permite crear la matriz TF o, si indicamos el parámetro binary = True, creará una matriz de apariciones.

Dicho esto, veamos cómo poder aplicar nuestra matriz TF para poder entrenar nuestro modelo de NLP con Naive Bayes en Python:

from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer

# Unify the strings once again
data['tokens'] = data['tokens'].apply(lambda x: ' '.join(x))

# Make split
x_train, x_test, y_train, y_test = train_test_split(
    data['tokens'], 
    data['type'], 
    test_size= 0.2
    )

# Create vectorizer
vectorizer = CountVectorizer(
    strip_accents = 'ascii', 
    lowercase = True
    )

# Fit vectorizer & transform it
vectorizer_fit = vectorizer.fit(x_train)
x_train_transformed = vectorizer_fit.transform(x_train)
x_test_transformed = vectorizer_fit.transform(x_test)

Entrenamiento de Naive Bayes

Perfecto, ya tenemos nuestros datos transformados. Por último solo queda entrenar el modelo. Tal como hemos visto anteriormente, para proyectos de texto, la función que mejor se adapta es la función MultinomialNB de Sklearn.

Así pues, simplemente hacemos el fit y el predict tanto en train como en test. Además, para visualizar la capacidad predictiva del modelo vamos a ver tanto el balanced accuracy como la matriz de confusión:

# Build the model
from sklearn.naive_bayes import MultinomialNB

# Train the model
naive_bayes = MultinomialNB()
naive_bayes_fit = naive_bayes.fit(x_train_transformed, y_train)

from sklearn.metrics import confusion_matrix, balanced_accuracy_score

# Make predictions
train_predict = naive_bayes_fit.predict(x_train_transformed)
test_predict = naive_bayes_fit.predict(x_test_transformed)

def get_scores(y_real, predict):
  ba_train = balanced_accuracy_score(y_real, predict)
  cm_train = confusion_matrix(y_real, predict)

  return ba_train, cm_train 

def print_scores(scores):
  return f"Balanced Accuracy: {scores[0]}\nConfussion Matrix:\n {scores[1]}"

train_scores = get_scores(y_train, train_predict)
test_scores = get_scores(y_test, test_predict)


print("## Train Scores")
print(print_scores(train_scores))
print("\n\n## Test Scores")
print(print_scores(test_scores))
## Train Scores
Balanced Accuracy: 0.9888480691883254
Confussion Matrix:
 [[3837   10]
 [  12  597]]


## Test Scores
Balanced Accuracy: 0.9404936733271031
Confussion Matrix:
 [[974   3]
 [ 16 122]]

¡Genial! Como podemos ver, sea un modelo muy básico, tiene una muy buena capacidad predictiva en la clasificación de texto. Así pues, ¡ya sabes cómo entrenar un modelo de Naive Bayes en Python para proyectos de NLP!

Así pues, vamos a ver los pros y contras de este modelo para que sepas, en mi opinión, cuando es bueno que lo uses o que no lo uses.

Pros de Naive Bayes

  • Es un modelo muy fácilmente interpretable, lo cual es un punto muy positivo sobre todo de cara a NLP, donde existen pocas opciones alternativas que también sean interpretables.
  • Es un modelo muy sencillo de entrenar y de ajustar sus hiperparámetros, y aún así puede dar muy buenos resultados, tal como hemos visto en el ejemplo anterior.
  • Trabaja muy bien para datasets con muchas variables, lo cual es poco común dentro de los modelos de Machine Learning. Una vez más, esto lo hace muy interesante de cara a NLP.
  • Sirva tanto para probelmas de clasificación binaria, como clasificación multiclase.

Contras de Naive Bayes

  • La asunción de que las variables son independientes. En la gran mayoria de casos esta asunción no se cumple.
  • Aparición de nuevas palabras o clases. En el caso de proyectos de NLP es muy normal que a la hora de realizar predicciones aparezcan nuevas palabras que el modelo no ha visto a la hora de entrenar. Como resultado, el modelo no tendra en cuenta dichas palabras de cara a hacer las predicciones, por lo que habrá que reentrenarlo de forma frecuente.

Conclusión

Sin duda alguna Naive Bayes es un modelo muy sencillo de entender, interpretar y aplicar. Por normal general, no suele ser un modelo que funcione muy bien en proyectos de clasificación. Sin embargo, cuando se trata de clasificar texto, Naive Bayes es, en mi opinión, el primero modelo que se debería probar.

En cualquier caso, cuando se trata de un proyecto de clasificación de texto, siempre es interesante probar otro tipo de modelos (como los Support Vector Classifiers) y prestar mucha atención al proceso de limpieza y calidad de datos.

Espero que este post te haya servido para entender mejor cómo funciona este modelo. Si es así y te gustaría estar al tanto de los posts que voy subiendo, te animo a que te suscribas para que te pueda avisar. ¡Nos vemos en el siguiente!

Blog patrocinado por: