Capítulo 4. Clasificación de textos

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Uno de los usos más novedosos de la clasificación binaria es el análisis de sentimiento, que examina una muestra de texto como la reseña de un producto, un tweet o un comentario dejado en un sitio web y lo puntúa en una escala de 0,0 a 1,0, donde 0,0 representa un sentimiento negativo y 1,0 un sentimiento positivo. Una opinión del tipo "producto estupendo a un precio estupendo" podría tener una puntuación de 0,9, mientras que "producto excesivamente caro que apenas funciona" podría tener una puntuación de 0,1. La puntuación es la probabilidad de que el texto exprese un sentimiento positivo. Los modelos de análisis de sentimientos son difíciles de construir algorítmicamente, pero son relativamente fáciles de elaborar con el aprendizaje automático. Para ver ejemplos de cómo se utiliza el análisis de sentimientos en las empresas hoy en día, consulta el artículo "8 Sentiment Analysis Real-World Use Cases" de Nicholas Bianchi.

El análisis de sentimientos es un ejemplo de tarea que implica clasificar datos textuales en lugar de datos numéricos. Dado que el aprendizaje automático funciona con números, debes convertir el texto en números antes de entrenar un modelo de análisis de sentimiento, un modelo que identifique correos spam o cualquier otro modelo que clasifique texto. Un enfoque común es construir una tabla de frecuencias de palabras llamada bolsa de palabras. Scikit-Learn proporciona clases de ayuda. También incluye soporte para normalizar el texto de modo que, por ejemplo, "impresionante" e "Impresionante" no cuenten como dos palabras diferentes.

Este capítulo comienza describiendo cómo preparar texto para utilizarlo en modelos de clasificación. Después de construir un modelo de análisis de sentimientos, aprenderás sobre otro algoritmo de aprendizaje popular llamado Naive Bayes que funciona especialmente bien con texto y lo utilizarás para construir un modelo que distinga entre correos electrónicos legítimos y correos electrónicos de spam. Por último, conocerás una técnica matemática para medir la similitud de dos muestras de texto y la utilizarás para crear una aplicación que te recomiende películas basándose en otras películas que te gusten.

Preparar el texto para la clasificación

Antes de que entrene un modelo para clasificar texto, debes convertir el texto en números, un proceso conocido como vectorización. En el Capítulo 1 se presentó la ilustración reproducida en la Figura 4-1, que muestra una técnica habitual para vectorizar texto. Cada fila representa una muestra de texto, como un tuit o una crítica de cine, y cada columna representa una palabra del texto de entrenamiento. Los números de las filas son recuentos de palabras, y el número final de cada fila es una etiqueta: 0 para negativo y 1 para positivo.

Figura 4-1. Conjunto de datos para el análisis de sentimientos

El texto suele limpiarse antes de vectorizarlo. Algunos ejemplos de limpieza son la conversión de caracteres a minúsculas (de modo que, por ejemplo, "Excelente" equivalga a "excelente"), la eliminación de los símbolos de puntuación y, opcionalmente, la eliminación de las stop words,palabras comunescomo the y and que probablemente tengan poco impacto en el resultado. Una vez limpiado, las frases se dividen en palabras individuales(tokenizadas) y las palabras se utilizan para producir conjuntos de datos como el de la Figura 4-1.

Scikit-Learn tiene tres clases que se encargan de la mayor parte del trabajo de limpieza y vectorización del texto:

CountVectorizer
Crea un diccionario(vocabulario) a partir del corpus de palabras del texto de entrenamiento y genera una matriz de recuento de palabras como la de la Figura 4-1
HashingVectorizer
Utiliza hashes de palabras de en lugar de un vocabulario en memoria para producir recuentos de palabras y, por tanto, es más eficiente en memoria
TfidfVectorizer
Crea un diccionario a partir de las palabras que se le proporcionan y genera una matriz similar a la de la Figura 4-1, pero en lugar de contener recuentos enteros de palabras, la matriz contiene valores de frecuencia de términos-frecuencia inversa de documentos (TFIDF) entre 0,0 y 1,0 que reflejan la importancia relativa de las palabras individuales

Las tres clases de son capaces de convertir el texto a minúsculas, eliminar los símbolos de puntuación, eliminar las palabras vacías, dividir las frases en palabras individuales, etc. También admiten n-gramas, que son combinaciones de dos o más palabras consecutivas (tú especificas el número n) que deben tratarse como una sola palabra. La idea es que palabras como crédito y puntuación pueden tener más significado si aparecen una junto a otra en una frase que si aparecen muy separadas. Sin los n-gramas, se ignora la proximidad relativa de las palabras. El inconveniente de utilizar n-gramas es que aumenta el consumo de memoria y el tiempo de entrenamiento. Sin embargo, utilizado juiciosamente, puede hacer que los modelos de clasificación de textos sean más precisos.

Nota

Las redes neuronales tienen otras formas más potentes de tener en cuenta el orden de las palabras que no requieren que las palabras relacionadas aparezcan una junto a otra. Un modelo convencional de aprendizaje automático no puede relacionar las palabras azul y cielo en la frase "Me gusta el azul, porque es el color del cielo", pero una red neuronal sí puede. Arrojaré más luz sobre esto en el Capítulo 13.

Aquí tienes un ejemplo que demuestra lo que hace CountVectorizer y cómo se utiliza:

import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer

lines = [
    'Four score and 7 years ago our fathers brought forth,',
    '... a new NATION, conceived in liberty $$$,',
    'and dedicated to the PrOpOsItIoN that all men are created equal',
    'One nation\'s freedom equals #freedom for another $nation!'
]

# Vectorize the lines
vectorizer = CountVectorizer(stop_words='english')
word_matrix = vectorizer.fit_transform(lines)

# Show the resulting word matrix
feature_names = vectorizer.get_feature_names_out()
line_names = [f'Line {(i + 1):d}' for i, _ in enumerate(word_matrix)]

df = pd.DataFrame(data=word_matrix.toarray(), index=line_names,
                  columns=feature_names)

df.head()

Este es el resultado:

El corpus de texto en este caso son cuatro cadenas en una lista de Python. CountVectorizer descompuso las cadenas en palabras, eliminó las palabras de parada y los símbolos, y convirtió todas las palabras restantes a minúsculas. Esas palabras forman las columnas del conjunto de datos, y los números de las filas muestran cuántas veces aparece una palabra determinada en cada cadena. El parámetro stop_words='english' indica a CountVectorizer que elimine las palabras de parada utilizando un diccionario incorporado de más de 300 palabras de parada en inglés. Si lo prefieres, puedes proporcionar tu propia lista de palabras reservadas en una lista de Python. (O puedes dejar las palabras de parada ahí; a menudo no importa.) Y si estás entrenando con texto escrito en otro idioma, puedes obtener listas de palabras de parada multilingües de otras bibliotecas de Python, como el Conjunto de herramientas de lenguaje natural (NLTK) y Stop-words.

Observa en que equal y equals cuentan como palabras separadas, aunque tengan un significado similar. A veces, los científicos de datos van un paso más allá cuando preparan el texto para el aprendizaje automático, al separar o lematizar las palabras. Si el texto anterior estuviera acortado, todas las apariciones de equals se convertirían en equal. Scikit carece de soporte para el acortamiento y la lematización, pero puedes obtenerlo de otras bibliotecas como NLTK.

CountVectorizer elimina los símbolos de puntuación, pero no los números. Ignoró el 7 de la línea 1 porque ignora los caracteres simples. Pero si cambiara 7 por 777, el término 777 aparecería en el vocabulario. Una forma de solucionarlo es definir una función que elimine los números y pasársela a CountVectorizer a través del parámetro preprocessor:

import re

def preprocess_text(text):
    return re.sub(r'\d+', '', text).lower()

vectorizer = CountVectorizer(stop_words='english', preprocessor=preprocess_text)
word_matrix = vectorizer.fit_transform(lines)

Observa la llamada a lower para convertir el texto a minúsculas. CountVectorizer no convierte el texto a minúsculas si proporcionas una función de preprocesamiento, por lo que la función de preprocesamiento debe convertirlo por sí misma. Sin embargo, sigue eliminando los caracteres de puntuación.

Otro parámetro de útil para CountVectorizer es min_df, que ignora las palabras que aparecen menos del número de veces especificado. Puede ser un número entero que especifique un recuento mínimo (por ejemplo, ignorar las palabras que aparezcan menos de cinco veces en el texto de entrenamiento, o min_df=5), o puede ser un valor en coma flotante de 0,0 a 1,0 que especifique el porcentaje mínimo de muestras en las que debe aparecer una palabra; por ejemplo, ignorar las palabras que aparezcan en menos del 10% de las muestras (min_df=0.1). Es estupendo para filtrar palabras que probablemente no tengan sentido de todos modos, y reduce el consumo de memoria y el tiempo de entrenamiento al disminuir el tamaño del vocabulario. Count​Vec⁠tor⁠izer también admite un parámetro max_df para eliminar palabras que aparecen con demasiada frecuencia.

Los ejemplos anteriores de utilizan CountVectorizer, lo que probablemente te haga preguntarte cuándo (y por qué) utilizarías HashingVectorizer o TfidfVectorizer en su lugar. HashingVectorizer es útil cuando se trabaja con grandes conjuntos de datos. En lugar de almacenar palabras en memoria, aplica un hash a cada palabra y utiliza el hash como índice en una matriz de recuentos de palabras. Por tanto, puede hacer más con menos memoria y es muy útil para reducir el tamaño de los vectorizadores al serializarlos, de modo que puedas restaurarlos más tarde -un tema del que hablaré más en el Capítulo 7-. El inconveniente de HashingVectorizer es que no te permite trabajar hacia atrás desde el texto vectorizado hasta el texto original. Count​Vec⁠tor⁠izer sí lo hace, y para ello proporciona un métodoinverse_transform .

TfidfVectorizer es utilizado frecuentemente para realizar la extracción de palabras clave: examinar un documento o conjunto de documentos y extraer las palabras clave que caracterizan su contenido. Asigna a las palabras ponderaciones numéricas que reflejan su importancia, y utiliza dos factores para determinar las ponderaciones: la frecuencia con que aparece una palabra en documentos individuales y la frecuencia con que aparece en el conjunto global de documentos. Las palabras que aparecen con más frecuencia en documentos individuales, pero en menos documentos, reciben una ponderación mayor. No profundizaré en ello aquí, pero si tienes curiosidad por saber más, el repositorio GitHub de este libro contiene un cuaderno que utiliza Tfidf​Vec⁠tor⁠izer para extraer palabras clave del manuscrito del Capítulo 1.

Análisis de Sentimiento

Para entrenar un modelo de análisis de sentimientos, necesitas un conjunto de datos etiquetados. Existen varios conjuntos de datos de este tipo de dominio público. Uno de ellos,, es el conjunto de datos de críticas de películas IMDB, que contiene 25.000 muestras de críticas negativas y 25.000 muestras de críticas positivas publicadas en el sitio web Internet Movie Database. Cada crítica está meticulosamente etiquetada con un 0 para el sentimiento negativo o un 1 para el sentimiento positivo. Para demostrar cómo funciona el análisis de sentimientos, vamos a construir un modelo de clasificación binario y a entrenarlo con este conjunto de datos. Utilizaremos la regresión logística como algoritmo de aprendizaje. LogisticRegres⁠sionLa puntuación del análisis de sentimientos obtenida con este modelo es simplemente la probabilidad de que la entrada exprese un sentimiento positivo, que se obtiene fácilmente llamando al método predict_proba método.

Empieza descargando el conjunto de datos y copiándolo en el subdirectorio Datos del directorio que aloja tus cuadernos Jupyter. A continuación, ejecuta el siguiente código en un cuaderno para cargar el conjunto de datos y mostrar las cinco primeras filas:

import pandas as pd
 
df = pd.read_csv('Data/reviews.csv', encoding='ISO-8859-1')
df.head()

El atributo encoding es necesario porque el archivo CSV utiliza la codificación de caracteres ISO-8859-1 en lugar de UTF-8. La salida es la siguiente:

Averigua cuántas filas contiene el conjunto de datos y confirma que no faltan valores:

df.info()

Utiliza el enunciado siguiente para ver cuántas instancias hay de cada clase (0 para negativo y 1 para positivo):

df.groupby('Sentiment').describe()

Este es el resultado:

Hay un número par de muestras positivas y negativas, pero en cada caso, el número de muestras únicas es menor que el número de muestras para esa clase. Eso significa que el conjunto de datos tiene filas duplicadas, y las filas duplicadas podrían sesgar un modelo de aprendizaje automático. Utiliza las siguientes sentencias para eliminar las filas duplicadas y comprobar de nuevo el equilibrio:

df = df.drop_duplicates()
df.groupby('Sentiment').describe()

Ahora no hay filas duplicadas, y el número de muestras positivas y negativas es aproximadamente igual.

A continuación, utiliza CountVectorizer para preparar y vectorizar el texto en la columna Text. Ajusta min_df a 20 para ignorar las palabras que aparecen con poca frecuencia en el texto de entrenamiento. Esto reduce la probabilidad de errores fuera de memoria y probablemente también hará que el modelo sea más preciso. Utiliza también el parámetro ngram_range para permitir que Count​Vec⁠tor⁠izer incluya pares de palabras además de palabras individuales:

from sklearn.feature_extraction.text import CountVectorizer
 
vectorizer = CountVectorizer(ngram_range=(1, 2), stop_words='english',
                             min_df=20)

x = vectorizer.fit_transform(df['Text'])
y = df['Sentiment']

Ahora divide el conjunto de datos para entrenamiento y prueba. Utilizaremos una división 50/50, ya que hay casi 50.000 muestras en total:

from sklearn.model_selection import train_test_split
 
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.5,
                                                    random_state=0)

El siguiente paso es entrenar un clasificador. Utilizaremos la clase LogisticRegression de Scikit, que utiliza la regresión logística para ajustar un modelo a los datos:

from sklearn.linear_model import LogisticRegression
 
model = LogisticRegression(max_iter=1000, random_state=0)
model.fit(x_train, y_train)

Valida el modelo entrenado con el 50% del conjunto de datos reservado para las pruebas y muestra los resultados en una matriz de confusión:

%matplotlib inline
from sklearn.metrics import ConfusionMatrixDisplay as cmd

cmd.from_estimator(model, x_test, y_test,
                   display_labels=['Negative', 'Positive'],
                   cmap='Blues', xticks_rotation='vertical')

La matriz de confusión revela que el modelo identificó correctamente 10.795 reseñas negativas, mientras que se equivocó en 1.574 de ellas. Identificó correctamente 10.966 reseñas positivas y se equivocó 1.456 veces:

Ahora viene la parte divertida: analizar el texto en función del sentimiento. Utiliza los siguientes enunciados para obtener una puntuación de sentimiento de la frase "Las largas colas y el mal servicio de atención al cliente me desanimaron":

text = 'The long lines and poor customer service really turned me off'
model.predict_proba(vectorizer.transform([text]))[0][1]

Este es el resultado:

0.09183447847778639

Ahora haz lo mismo con "¡La comida estaba buenísima y el servicio era excelente!":

text = 'The food was great and the service was excellent!'
model.predict_proba(vectorizer.transform([text]))[0][1]

Si esperabas una puntuación más alta para éste, no te decepcionará:

0.8536277207125618

No dudes en probar tus propias frases y ver si estás de acuerdo con las puntuaciones de sentimiento que predice el modelo. No es perfecto, pero es lo suficientemente bueno como para que, si pasas cientos de opiniones o comentarios por él, obtengas una indicación fiable del sentimiento expresado en el texto.

Nota

A veces, la lista incorporada de palabras de parada de CountVectorizerdisminuye la precisión de un modelo porque la lista es muy amplia. Como experimento, elimina stop_words='english' de CountVectorizer y vuelve a ejecutar el código. Comprueba la matriz de confusión. ¿Aumenta o disminuye la precisión? Siéntete libre de variar también otros parámetros como min_df y ngram_range. En el mundo real, los científicos de datos suelen probar muchas combinaciones de parámetros diferentes para determinar cuál produce los mejores resultados .

Bayes ingenuos

La regresión logística es un algoritmo habitual para los modelos de clasificación y suele ser muy eficaz para clasificar texto. Pero en escenarios que implican la clasificación de texto, los científicos de datos suelen recurrir a otro algoritmo de aprendizaje llamado Naive Bayes. Es un algoritmo de clasificación basado en el teorema de Bayes, que proporciona un medio para calcular probabilidades condicionales. Matemáticamente, el teorema de Bayes se enuncia así:

P ( A | B ) = P(B|A)-P(A) P(B)

Esto significa que la probabilidad de que A sea cierta dado que B es cierta es igual a la probabilidad de que B sea cierta dado que A es cierta multiplicada por la probabilidad de que A sea cierta dividida por la probabilidad de que B sea cierta. Eso es un trabalenguas y, aunque exacto, no explica por qué Naive Bayes es tan útil para clasificar texto, ni cómo se aplica, por ejemplo, a una colección de correos electrónicos para determinar cuáles son spam.

Empecemos en con un ejemplo sencillo. Supón que el 10% de todos los correos electrónicos que recibes son spam. Eso es P(A). El análisis revela que el 5% de los correos spam que recibes contienen la palabra felicidades, pero sólo el 1% de todos tus correos contienen la misma palabra. Por tanto, P(B|A) es 0,05 y P(B) es 0,01. La probabilidad de que un correo electrónico sea spam si contiene la palabra felicidades es P(A|B), que es (0,05 x 0,10) / 0,01, o 0,50.

Por supuesto, un filtro de spam debe tener en cuenta todas las palabras de un correo electrónico, no sólo una. Resulta que si haces algunas suposiciones simples (ingenuas) -que el orden de las palabras en un correo electrónico no importa, y que cada palabra tiene el mismo peso- puedes escribir la ecuación de Bayes de esta manera para un clasificador de spam:

P ( S | m e s s a g e ) = P ( S ) - P ( w o r d 1 | S ) - P ( w o r d 2 | S ) ... P ( w o r d n | S )

En pocas palabras, la probabilidad de que un mensaje sea spam es proporcional al producto de:

  • La probabilidad de que cualquier mensaje del conjunto de datos sea spam, o P(S)

  • La probabilidad de que cada palabra del mensaje aparezca en un mensaje de spam, o P(palabra|S)

La P(S) se puede calcular con bastante facilidad: es simplemente la fracción de los mensajes del conjunto de datos que son mensajes de spam. Si entrenas un modelo de aprendizaje automático con 1.000 mensajes y 500 de ellos son spam, entonces P(S) = 0,5. Para una palabra determinada, P(palabra|S) es simplemente el número de veces que la palabra aparece en los mensajes de spam dividido por el número de palabras de todos los mensajes de spam. Todo el problema se reduce al recuento de palabras. Puedes hacer un cálculo similar para calcular la probabilidad de que el mensaje no sea spam, y luego utilizar la mayor de las dos probabilidades para hacer una predicción.

Aquí tienes un ejemplo con cuatro correos electrónicos de muestra. Los correos electrónicos son:

Texto Spam
Aumenta tu puntuación crediticia en minutos 1
Aquí están las actas de la reunión de ayer 0
Reunión mañana para revisar los resultados de ayer 0
Consigue los medicamentos de mañana a los precios de ayer 1

Si eliminas las palabras intermedias, conviertes los caracteres a minúsculas y separas las palabras de modo que mañana se convierta en mañana, te queda esto:

Texto Spam
aumentar la puntuación de crédito minuto 1
acta de la reunión de ayer 0
reunión mañana revisión puntuación ayer 0
puntuación mañana med ayer precio 1

Como dos de los cuatro mensajes son spam y dos no, la probabilidad de que cualquier mensaje sea spam(P(S)) es 0,5. Lo mismo ocurre con la probabilidad de que cualquier mensaje no sea spam(P(N) = 0,5). Además, los mensajes spam contienen nueve palabras únicas, mientras que los mensajes no spam contienen un total de ocho.

El siguiente paso es construir la siguiente tabla de frecuencias de palabras. Tomemos como ejemplo la palabra ayer. Aparece una vez en un mensaje etiquetado como spam, por lo que P(ayer|S) es 1/9, o 0,111. Aparece dos veces en mensajes no spam, por lo que P(ayer|N) es 2/8, o 0,250:

Palabra P(palabra|S) P(palabra|N)
sube 1/9 = 0.111 0/8 = 0.000
crédito 1/9 = 0.111 0/8 = 0.000
puntuación 2/9 = 0.222 1/8 = 0.125
minuto 1/9 = 0.111 1/8 = 0.125
ayer 1/9 = 0.111 2/8 = 0.250
reunión 0/9 = 0.000 2/8 = 0.250
mañana 1/9 = 0.111 1/8 = 0.125
revisa 0/9 = 0.000 1/8 = 0.125
med 1/9 = 0.111 0/8 = 0.000
precio 1/9 = 0.111 0/8 = 0.000

Esto funciona hasta cierto punto, pero los ceros de la tabla son un problema. Digamos que quieres determinar si "Las puntuaciones deben revisarse antes de mañana" es spam. Si eliminas las palabras clave, te queda "revisión de puntuaciones mañana". Puedes calcular la probabilidad de que el mensaje sea spam de esta forma:

P ( S | s c o r e r e v i e w t o m o r r o w ) = P ( S ) - P ( s c o r e | S ) - P ( r e v i e w | S ) - P ( t o m o r r o w | S )
P ( S | s c o r e r e v i e w t o m o r r o w ) = 0 . 5 - 0 . 222 - 0 . 0 - 0 . 111 = 0
P ( S | s c o r e r e v i e w t o m o r r o w ) = 0

El resultado es 0 porque la revisión no aparece en un mensaje de spam, y 0 veces cualquier cosa es 0. El algoritmo simplemente no puede asignar una probabilidad de spam a "Las puntuaciones deben ser revisadas antes de mañana".

Una forma habitual de resolver esto en es aplicar el suavizado de Laplace, también conocido como suavizado aditivo. Normalmente, esto implica añadir 1 a cada numerador y el número de palabras únicas del conjunto de datos (en este caso, 10) a cada denominador. Ahora, P(reseña|S) se evalúa como (0 + 1) / (9 + 10), lo que equivale a 0,053. No es mucho, pero es mejor que nada (literalmente). Aquí tienes de nuevo las frecuencias de las palabras, esta vez revisadas con el suavizado de Laplace:

Palabra P(palabra|S) P(palabra|N)
sube (1 + 1) / (9 + 10) = 0.105 (0 + 1) / (8 + 10) = 0.056
crédito (1 + 1) / (9 + 10) = 0.105 (0 + 1) / (8 + 10) = 0.056
puntuación (2 + 1) / (9 + 10) = 0.158 (1 + 1) / (8 + 10) = 0.111
minuto (1 + 1) / (9 + 10) = 0.105 (1 + 1) / (8 + 10) = 0.111
ayer (1 + 1) / (9 + 10) = 0.105 (2 + 1) / (8 + 10) = 0.167
reunión (0 + 1) / (9 + 10) = 0.053 (2 + 1) / (8 + 10) = 0.167
mañana (1 + 1) / (9 + 10) = 0.105 (1 + 1) / (8 + 10) = 0.111
revisa (0 + 1) / (9 + 10) = 0.053 (1 + 1) / (8 + 10) = 0.111
med (1 + 1) / (9 + 10) = 0.105 (0 + 1) / (8 + 10) = 0.056
precio (1 + 1) / (9 + 10) = 0.105 (0 + 1) / (8 + 10) = 0.056

Ahora puedes determinar si "Las puntuaciones deben revisarse antes de mañana" es spam realizando dos sencillos cálculos:

P ( S | s c o r e r e v i e w t o m o r r o w ) = 0 . 5 - 0 . 158 - 0 . 053 - 0 . 105 = 0 . 000440
P ( N | s c o r e r e v i e w t o m o r r o w ) = 0 . 5 - 0 . 111 - 0 . 111 - 0 . 111 = 0 . 000684

Según esta medida, es probable que "Las puntuaciones deben revisarse antes de mañana" no sea spam. Las probabilidades son relativas, pero podrías normalizarlas y concluir que hay un 40% de posibilidades de que el mensaje sea spam y un 60% de que no lo sea, basándote en los correos electrónicos con los que se entrenó el modelo.

Afortunadamente, no tienes que hacer estos cálculos a mano. Scikit-Learn proporciona varias clases para ayudarte, incluida la claseMultinomialNB , que funciona muy bien con las tablas de recuento de palabras producidas por CountVectorizer.

Filtrado de spam

No es casualidad que los filtros de spam modernos sean extraordinariamente hábiles a la hora de identificar el spam. Prácticamente todos ellos se basan en el aprendizaje automático. Estos modelos son difíciles de implementar algorítmicamente, porque un algoritmo que utiliza palabras clave como crédito y puntuación para determinar si un correo electrónico es spam se engaña fácilmente. El aprendizaje automático, por el contrario, examina un conjunto de correos electrónicos y utiliza lo que aprende para clasificar el siguiente correo electrónico. Estos modelos suelen alcanzar una precisión superior al 99%. Y se hacen más inteligentes con el tiempo, a medida que se entrenan con más y más correos electrónicos.

El ejemplo anterior utilizó la regresión logística para predecir si el texto introducido expresaba un sentimiento positivo o negativo. Utilizó la probabilidad de que el texto exprese un sentimiento positivo como puntuación del sentimiento, y viste que expresiones como "Las largas colas y el mal servicio de atención al cliente me desanimaron" tienen una puntuación cercana a 0,0, mientras que expresiones como "La comida era estupenda y el servicio excelente" tienen una puntuación cercana a 1,0. Ahora vamos a construir un modelo de clasificación binaria que clasifique los correos electrónicos como spam o no spam y a utilizar Naive Bayes para ajustar el modelo a los datos de entrenamiento.

Existen varios conjuntos de datos de clasificación de spam de dominio público. Cada uno contiene una colección de correos electrónicos con muestras etiquetadas con 1s para spam y 0s para no spam. Utilizaremos un conjunto de datos relativamente pequeño que contiene 1.000 muestras. Empieza descargando el conjunto de datos y copiándolo en el subdirectorio Datos de tus cuadernos. A continuación, carga los datos y visualiza las cinco primeras filas:

import pandas as pd
 
df = pd.read_csv('Data/ham-spam.csv')
df.head()

Ahora comprueba si hay filas duplicadas en el conjunto de datos:

df.groupby('IsSpam').describe()

El conjunto de datos contiene una fila duplicada. Eliminémosla y comprobemos el equilibrio:

df = df.drop_duplicates()
df.groupby('IsSpam').describe()

El conjunto de datos contiene ahora 499 muestras que no son spam, y 500 que sí lo son. El siguiente paso es utilizar CountVectorizer para vectorizar los correos electrónicos. Una vez más, permitiremos que CountVectorizer tenga en cuenta los pares de palabras, además de las palabras individuales, y eliminaremos las palabras vacías utilizando el diccionario incorporado de Scikit de palabras vacías en inglés:

from sklearn.feature_extraction.text import CountVectorizer
 
vectorizer = CountVectorizer(ngram_range=(1, 2), stop_words='english')
x = vectorizer.fit_transform(df['Text'])
y = df['IsSpam']

Divide el conjunto de datos de modo que el 80% pueda utilizarse para el entrenamiento y el 20% para la prueba:

from sklearn.model_selection import train_test_split
 
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2,
                                                    random_state=0)

El siguiente paso es entrenar un clasificador Naive Bayes utilizando la claseMultinomialNB de Scikit:

from sklearn.naive_bayes import MultinomialNB
 
model = MultinomialNB()
model.fit(x_train, y_train)

Validar el modelo entrenado con el 20% del conjunto de datos reservado para las pruebas mediante una matriz de confusión:

%matplotlib inline
from sklearn.metrics import ConfusionMatrixDisplay as cmd

cmd.from_estimator(model, x_test, y_test,
                   display_labels=['Not Spam', 'Spam'],
                   cmap='Blues', xticks_rotation='vertical')

El modelo identificó correctamente 101 de 102 correos legítimos como no spam, y 95 de 98 correos spam como spam:

Utiliza el método score para obtener una medida aproximada de la precisión del modelo:

model.score(x_test, y_test)

Ahora utiliza la clase RocCurveDisplay de Scikit para visualizar la curva ROC:

from sklearn.metrics import RocCurveDisplay as rcd
import seaborn as sns
sns.set()

rcd.from_estimator(model, x_test, y_test)

Los resultados son alentadores. Entrenado con sólo 999 muestras, el área bajo la curva ROC (AUC) indica que el modelo tiene una precisión superior al 99,9% a la hora de clasificar los correos electrónicos como spam o no spam:

Veamos cómo clasifica el modelo unos cuantos correos electrónicos que no ha visto antes, empezando por uno que no es spam. El método predict del modelo predice una clase-0 para no spam, o 1 para spam:

msg = 'Can you attend a code review on Tuesday to make sure the logic is solid?'
input = vectorizer.transform([msg])
model.predict(input)[0]

El modelo dice que este mensaje no es spam, pero ¿cuál es la probabilidad de que no lo sea? Puedes saberlo en predict_proba, que devuelve una matriz con dos valores: la probabilidad de que la clase predicha sea 0 y la probabilidad de que la clase predicha sea 1, en ese orden:

model.predict_proba(input)[0][0]

La modelo parece muy segura de que este correo electrónico es legítimo:

0.9999497111473539

Ahora prueba el modelo con un mensaje de spam:

msg = 'Why pay more for expensive meds when you can order them online ' \
      'and save $$$?'

input = vectorizer.transform([msg])
model.predict(input)[0]

¿Cuál es la probabilidad de que el mensaje no sea spam?

model.predict_proba(input)[0][0]

La respuesta es:

0.00021423891260677753

¿Cuál es la probabilidad de que el mensaje sea spam?

model.predict_proba(input)[0][1]

Y la respuesta es:

0.9997857610873945

Observa en que predict y predict_proba aceptan una lista de entradas. Basándote en eso, ¿podrías clasificar un lote entero de correos electrónicos con una sola llamada a cualquiera de los dos métodos? ¿Cómo obtendrías los resultados de cada correo electrónico?

Sistemas de recomendación

Otra rama del aprendizaje automático de que ha demostrado su valía en los últimos años son los sistemas de recomendación: sistemasque recomiendan productos o servicios a los clientes. Se dice que el sistema de recomendación de Amazon genera el 35% de sus ventas. La buena noticia es que no tienes que ser Amazon para beneficiarte de un sistema de recomendación, ni tienes que tener los recursos de Amazon para crear uno. Son relativamente sencillos de crear una vez que aprendes algunos principios básicos.

Recomendador hay sistemas de muchas formas. Los sistemas basados en la popularidad presentan opciones a los clientes en función de los productos y servicios que son populares en ese momento; por ejemplo, "Aquí están los más vendidos de esta semana". Los sistemas colaborativos hacen recomendaciones basadas en lo que otros han seleccionado, como en "La gente que compró este libro también compró estos libros". Ninguno de estos sistemas requiere aprendizaje automático.

Los sistemas basados en el contenido, por el contrario, se benefician enormemente del aprendizaje automático. Un ejemplo de sistema basado en el contenido es el que dice "si compraste este libro, puede que también te gusten estos otros". Estos sistemas requieren un medio para cuantificar la similitud entre elementos. Si te gustó la película Jungla de Cristal, puede que te guste o no Monty Python y el Santo Grial. Si te gustó Toy Story, probablemente también te guste A Bug's Life. ¿Pero cómo se hace esa determinación algorítmicamente?

Los recomendadores basados en el contenido de requieren dos ingredientes: una forma de vectorizar -convertiren números- los atributos que caracterizan a un servicio o producto, y un medio para calcular la similitud entre los vectores resultantes. El primero es fácil. Count​Vec⁠tor⁠izer convierte el texto en tablas de recuento de palabras. Todo lo que necesitas es una forma de medir la similitud entre filas de recuentos de palabras y podrás construir un sistema de recomendación. Y una de las formas más sencillas y eficaces de hacerlo es una técnica llamada similitud del coseno.

Similitud del coseno

La similitud del coseno es un medio matemático para calcular la similitud entre pares de vectores (o filas de números tratados como vectores). La idea básica es tomar cada valor de una muestra -por ejemplo, el recuento de palabras de una fila de texto vectorizado- y utilizarlos como coordenadas del punto final de un vector, con el otro punto final en el origen del sistema de coordenadas. Hazlo para dos muestras y, a continuación, calcula el coseno entre vectores en un espacio m-dimensional, donde m es el número de valores de cada muestra. Como el coseno de 0 es 1, dos vectores idénticos tienen una similitud de 1. Cuanto más disímiles sean los vectores, más se acercará el coseno a 0.

He aquí un ejemplo en el espacio bidimensional para ilustrarlo. Supongamos que tienes tres filas que contienen dos valores cada una:

1 2
2 3
3 1

Quieres determinar si la fila 2 es más parecida a la fila 1 o a la fila 3. Es difícil saberlo simplemente mirando los números, y en la vida real, hay muchos más números. Si simplemente sumaras los números de cada fila y compararas las sumas, llegarías a la conclusión de que la fila 2 es más parecida a la fila 3. Pero, ¿y si trataras cada fila como un vector, como se muestra en la Figura 4-2?

  • Fila 1: (0, 0) → (1, 2)

  • Fila 2: (0, 0) → (2, 3)

  • Fila 3: (0, 0) → (3, 1)

Figura 4-2. Semejanza del coseno

Ahora puedes trazar cada fila como un vector, calcular los cosenos de los ángulos formados por 1 y 2 y 2 y 3, y determinar que la fila 2 se parece más a la fila 1 que a la fila 3. Eso es la similitud del coseno en pocas palabras.

La similitud del coseno no se limita a dos dimensiones; también funciona en un espacio de dimensiones superiores. Para ayudar a calcular las similitudes del coseno independientemente del número de dimensiones, Scikit ofrece la funcióncosine_similarity . El siguiente código calcula las similitudes del coseno de las tres muestras del ejemplo anterior:

data = [[1, 2], [2, 3], [3, 1]]
cosine_similarity(data)

El valor devuelto es una matriz de similitud que contiene los cosenos de cada par de vectores. La anchura y la altura de la matriz son iguales al número de muestras:

array([[1.        , 0.99227788, 0.70710678],
       [0.99227788, 1.        , 0.78935222],
       [0.70710678, 0.78935222, 1.        ]])

A partir de aquí, puedes ver que la similitud de las filas 1 y 2 es de 0,992, mientras que la similitud de las filas 2 y 3 es de 0,789. En otras palabras, la fila 2 es más similar a la fila 1 que a la fila 3. También hay más similitud entre las filas 2 y 3 (0,789) que entre las filas 1 y 3 (0,707).

Crear un sistema de recomendación de películas

Pongamos a trabajar la similitud coseno para construir un sistema de recomendación de películas basado en el contenido. Empieza descargando el conjunto de datos, que es uno de los varios conjuntos de datos de películas disponibles en Kaggle.com. Éste contiene información de unas 4.800 películas, incluyendo título, presupuesto, géneros, palabras clave, reparto, etc. Coloca el archivo CSV en el subdirectorio Datos de tus cuadernos Jupyter. A continuación, carga el conjunto de datos y examina su contenido:

import pandas as pd
 
df = pd.read_csv('Data/movies.csv')
df.head()

El conjunto de datos contiene 24 columnas, de las que sólo unas pocas son necesarias para describir una película. Utiliza las siguientes sentencias para extraer columnas clave como title y genres y rellenar los valores que faltan con cadenas vacías:

df = df[['title', 'genres', 'keywords', 'cast', 'director']]
df = df.fillna('') # Fill missing values with empty strings
df.head()

A continuación, añade una columna llamada features que combine todas las palabras de las otras columnas:

df['features'] = df['title'] + ' ' + df['genres'] + ' ' + \
            df['keywords'] + ' ' + df['cast'] + ' ' + \
            df['director']

Utiliza CountVectorizer para vectorizar el texto de la columna features:

from sklearn.feature_extraction.text import CountVectorizer
 
vectorizer = CountVectorizer(stop_words='english', min_df=20)
word_matrix = vectorizer.fit_transform(df['features'])
word_matrix.shape

La tabla de recuento de palabras contiene 4.803 filas -una por cada película- y 918 columnas. La siguiente tarea es calcular las similitudes del coseno para cada par de filas:

from sklearn.metrics.pairwise import cosine_similarity
 
sim = cosine_similarity(word_matrix)

En última instancia, el objetivo de este sistema es introducir un título de película e identificar las n películas más parecidas a esa película. Para ello, define una función llamada get​_recom⁠mendations que acepte un título de película, un DataFrame que contenga información sobre todas las películas, una matriz de similitudes y el número de títulos de película a devolver:

def get_recommendations(title, df, sim, count=10):
    # Get the row index of the specified title in the DataFrame
    index = df.index[df['title'].str.lower() == title.lower()]
     
    # Return an empty list if there is no entry for the specified title
    if (len(index) == 0):
        return []
 
    # Get the corresponding row in the similarity matrix
    similarities = list(enumerate(sim[index[0]]))
     
    # Sort the similarity scores in that row in descending order
    recommendations = sorted(similarities, key=lambda x: x[1], reverse=True)
     
    # Get the top n recommendations, ignoring the first entry in the list since
    # it corresponds to the title itself (and thus has a similarity of 1.0)
    top_recs = recommendations[1:count + 1]
 
    # Generate a list of titles from the indexes in top_recs
    titles = []
 
    for i in range(len(top_recs)):
        title = df.iloc[top_recs[i][0]]['title']
        titles.append(title)
 
    return titles

Esta función ordena las similitudes del coseno en orden descendente para identificar las películas de count más parecidas a la identificada por el parámetro title. A continuación, devuelve los títulos de esas películas.

Ahora utiliza get_recommendations para buscar películas similares en la base de datos. Primero pregunta por las 10 películas más parecidas al thriller de James Bond Skyfall:

get_recommendations('Skyfall', df, sim)

Este es el resultado:

['Spectre',
 'Quantum of Solace',
 'Johnny English Reborn',
 'Clash of the Titans',
 'Die Another Day',
 'Diamonds Are Forever',
 'Wrath of the Titans',
 'I Spy',
 'Sanctum',
 'Blackthorn']

Vuelve a llamar a get_recommendations para que te haga una lista de películas parecidas a Mulan:

get_recommendations('Mulan', df, sim)

También puedes probar con otras películas. Ten en cuenta que sólo puedes introducir títulos de películas que estén en el conjunto de datos. Utiliza las siguientes sentencias para imprimir una lista completa de títulos:

pd.set_option('display.max_rows', None)
print(df['title'])

Creo que estarás de acuerdo en que el sistema hace un trabajo bastante creíble a la hora de elegir películas similares. ¡No está mal para unas 20 líneas de código !

Resumen

Los modelos de aprendizaje automático que clasifican texto son habituales y tienen diversos usos en la industria y en la vida cotidiana. ¿Qué ser humano racional no desea una varita mágica que erradique todos los correos basura, por ejemplo?

El texto utilizado para entrenar un modelo de clasificación de texto debe prepararse y vectorizarse antes del entrenamiento. La preparación incluye la conversión de los caracteres a minúsculas y la eliminación de los caracteres de puntuación, y puede incluir la eliminación de las palabras vacías, la eliminación de los números, y la separación por raíces o lematización. Una vez preparado, el texto se vectoriza convirtiéndolo en una tabla de frecuencias de palabras. La clase CountVectorizer de Scikit facilita el proceso de vectorización y se encarga también de algunas de las tareas de preparación.

La regresión logística y otros algoritmos de clasificación populares pueden utilizarse para clasificar texto una vez convertido a forma numérica. Sin embargo, para las tareas de clasificación de texto, el algoritmo de aprendizaje Naive Bayes suele superar a otros algoritmos. Al hacer algunas suposiciones "ingenuas", como que el orden en que aparecen las palabras en una muestra de texto no importa, Naive Bayes se reduce a un proceso de recuento de palabras. La clase MultinomialNB de Scikit proporciona una práctica implementación de Naive Bayes.

La similitud del coseno es un medio matemático para calcular la similitud entre dos filas de números. Uno de sus usos es crear sistemas que recomienden productos o servicios basándose en otros productos o servicios que haya comprado un cliente. Las tablas de frecuencia de palabras elaboradas a partir de descripciones textuales por CountVectorizer pueden combinarse con la similitud de coseno para crear sistemas inteligentes de recomendación destinados a complementar los resultados de una empresa.

No dudes en utilizar los ejemplos de este capítulo como punto de partida para tus propios experimentos. Por ejemplo, comprueba si puedes ajustar los parámetros pasados a CountVectorizer en alguno de los ejemplos y aumentar la precisión del modelo resultante. Los científicos de datos llaman hiperajuste de parámetros a la búsqueda de la combinación óptima de parámetros, y es un tema que aprenderás en el próximo capítulo.

Get Aprendizaje Automático Aplicado e IA para Ingenieros now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.