Trabajar con datos de texto

El objetivo de esta guía es explorar algunas de las principales herramientas de scikit-learn en una única tarea práctica: analizar una colección de documentos de texto (posts de grupos de noticias) sobre veinte temas diferentes.

En esta sección veremos cómo:

  • cargar el contenido del archivo y las categorías

  • extraer vectores de características adecuados para el aprendizaje automático

  • entrenar un modelo lineal para realizar la categorización

  • utiliza una estrategia de búsqueda en cuadrícula para encontrar una buena configuración tanto de los componentes de extracción de características como del clasificador

Configuración del tutorial

Para empezar con este tutorial, primero debes instalar scikit-learn y todas sus dependencias necesarias.

Consulta la página instrucciones de instalación para obtener más información y conocer las instrucciones específicas del sistema.

La fuente de este tutorial se puede encontrar dentro de su carpeta scikit-learn:

scikit-learn/doc/tutorial/text_analytics/

El código fuente también se puede encontrar en Github.

La carpeta del tutorial debe contener las siguientes subcarpetas:

  • *.rst files - la fuente del documento tutorial escrito con sphinx

  • data - carpeta para poner los conjuntos de datos utilizados durante el tutorial

  • skeletons -muestra de script incompletos para los ejercicios

  • solutions - soluciones de los ejercicios

Ya puedes copiar los esqueletos en una nueva carpeta en algún lugar de tu disco duro llamada sklearn_tut_workspace donde editarás tus propios archivos para los ejercicios manteniendo los esqueletos (skeletons) originales intactos:

cp -r skeletons work_directory/sklearn_tut_workspace

Los algoritmos de aprendizaje automático necesitan datos. Ve a cada subcarpeta $TUTORIAL_HOME/data y ejecuta el script fetch_data.py desde allí (después de haberlos leído primero).

Por ejemplo:

cd $TUTORIAL_HOME/data/languages
less fetch_data.py
python fetch_data.py

Carga del conjunto de datos de 20 grupos de noticias

El conjunto de datos se llama «Twenty Newsgroups». Aquí está la descripción oficial, citada del sitio web:

El conjunto de datos 20 Newsgroups es una colección de aproximadamente 20.000 documentos de grupos de noticias, repartidos (casi) uniformemente entre 20 grupos de noticias diferentes. Por lo que sabemos, fue recopilado originalmente por Ken Lang, probablemente para su artículo «Newsweeder: Learning to filter netnews», aunque no menciona explícitamente esta colección. La colección de 20 grupos de noticias se ha convertido en un popular conjunto de datos para experimentos en aplicaciones de texto de técnicas de aprendizaje automático, como la clasificación y la agrupación de textos.

A continuación utilizaremos el cargador de conjuntos de datos incorporado para 20 grupos de noticias de scikit-learn. Alternativamente, es posible descargar el conjunto de datos manualmente desde el sitio web y utilizar la función sklearn.datasets.load_files apuntando a la subcarpeta 20news-bydate-train de la carpeta de archivos sin comprimir.

Para conseguir tiempos de ejecución más rápidos para este primer ejemplo trabajaremos sobre un conjunto de datos parcial con sólo 4 categorías de las 20 disponibles en el conjunto de datos:

>>> categories = ['alt.atheism', 'soc.religion.christian',
...               'comp.graphics', 'sci.med']

Ahora podemos cargar la lista de archivos de correspondencia con esas categorías de la siguiente manera:

>>> from sklearn.datasets import fetch_20newsgroups
>>> twenty_train = fetch_20newsgroups(subset='train',
...     categories=categories, shuffle=True, random_state=42)

El conjunto de datos devuelto es un «bunch» de scikit-learn: un simple objeto titular con campos a los que se puede acceder como claves dict de python o como atributos object para mayor comodidad, por ejemplo el target_names contiene la lista de los nombres de las categorías solicitadas:

>>> twenty_train.target_names
['alt.atheism', 'comp.graphics', 'sci.med', 'soc.religion.christian']

Los propios archivos se cargan en la memoria en el atributo data. Como referencia, los nombres de los archivos también están disponibles:

>>> len(twenty_train.data)
2257
>>> len(twenty_train.filenames)
2257

Imprimamos las primeras líneas del primer archivo cargado:

>>> print("\n".join(twenty_train.data[0].split("\n")[:3]))
From: sd345@city.ac.uk (Michael Collier)
Subject: Converting images to HP LaserJet III?
Nntp-Posting-Host: hampton

>>> print(twenty_train.target_names[twenty_train.target[0]])
comp.graphics

Los algoritmos de aprendizaje supervisado requieren una etiqueta de categoría para cada documento del conjunto de entrenamiento. En este caso, la categoría es el nombre del grupo de noticias, que también es el nombre de la carpeta que contiene los documentos individuales.

Por razones de velocidad y eficiencia de espacio scikit-learn carga el atributo target como un arreglo de enteros que corresponde al índice del nombre de la categoría en la lista target_names. El identificador de categoría de cada muestra se almacena en el atributo target:

>>> twenty_train.target[:10]
array([1, 1, 3, 3, 3, 3, 3, 2, 2, 2])

Es posible recuperar los nombres de las categorías de la siguiente manera:

>>> for t in twenty_train.target[:10]:
...     print(twenty_train.target_names[t])
...
comp.graphics
comp.graphics
soc.religion.christian
soc.religion.christian
soc.religion.christian
soc.religion.christian
soc.religion.christian
sci.med
sci.med
sci.med

Habrás notado que las muestras se barajan aleatoriamente cuando llamamos a fetch_20newsgroups(..., shuffle=True, random_state=42): esto es útil si quieres seleccionar sólo un subconjunto de muestras para entrenar rápidamente un modelo y tener una primera idea de los resultados antes de volver a entrenar en el conjunto de datos completo más tarde.

Extracción de características de archivos de texto

Para llevar a cabo el aprendizaje automático de documentos de texto, primero tenemos que convertir el contenido del texto en vectores de características numéricas.

Bolsa de palabras

La forma más intuitiva de hacerlo es utilizar una representación de bolsa de palabras:

  1. Asigna un identificador entero fijo a cada palabra que aparezca en cualquier documento del conjunto de entrenamiento (por ejemplo, construyendo un diccionario de palabras con índices enteros).

  2. Para cada documento #i, se cuenta el número de apariciones de cada palabra w y se almacena en X[i, j] como valor de la característica #j donde j es el índice de la palabra w en el diccionario.

La representación de bolsa de palabras implica que n_features es el número de palabras distintas en el corpus: este número suele ser superior a 100.000.

Si n_samples == 10000, almacenar X como un arreglo de NumPy de tipo float32 requeriría 10000 x 100000 x 4 bytes = 4GB en RAM que es apenas manejable en las computadoras actuales.

Afortunadamente, la mayoría de los valores de X serán ceros, ya que para un documento determinado se utilizarán menos de unos pocos miles de palabras distintas. Por eso decimos que las bolsas de palabras son típicamente conjuntos de datos dispersos de alta dimensión. Podemos ahorrar mucha memoria almacenando sólo las partes distintas de cero de los vectores de características en la memoria.

Las matrices scipy.sparse son estructuras de datos que hacen exactamente esto, y scikit-learn tiene soporte incorporado para estas estructuras.

Tokenización de texto con scikit-learn

El preprocesamiento de texto, la tokenización y el filtrado de palabras de parada están incluidos en CountVectorizer, que construye un diccionario de características y transforma los documentos en vectores de características:

>>> from sklearn.feature_extraction.text import CountVectorizer
>>> count_vect = CountVectorizer()
>>> X_train_counts = count_vect.fit_transform(twenty_train.data)
>>> X_train_counts.shape
(2257, 35788)

CountVectorizer soporta recuentos de N-gramas de palabras o caracteres consecutivos. Una vez ajustado, el vectorizador ha construido un diccionario de índices de características:

>>> count_vect.vocabulary_.get(u'algorithm')
4690

El valor del índice de una palabra en el vocabulario está vinculado a su frecuencia en todo el corpus de entrenamiento.

De ocurrencias a frecuencias

El recuento de ocurrencias es un buen comienzo, pero hay un problema: los documentos más largos tendrán valores de recuento medio más altos que los documentos más cortos, aunque hablen de los mismos temas.

Para evitar estas posibles discrepancias, basta con dividir el número de apariciones de cada palabra en un documento entre el número total de palabras del mismo: estas nuevas características se denominan tf para las frecuencias de términos.

Otro refinamiento sobre tf consiste en reducir las ponderaciones de las palabras que aparecen en muchos documentos del corpus y que, por tanto, son menos informativas que las que sólo aparecen en una parte más pequeña del corpus.

Esta reducción de escala se denomina tf-idf por «Term Frequency times Inverse Document Frequency».

Tanto tf como tf-idf pueden calcularse de la siguiente manera utilizando TfidfTransformer:

>>> from sklearn.feature_extraction.text import TfidfTransformer
>>> tf_transformer = TfidfTransformer(use_idf=False).fit(X_train_counts)
>>> X_train_tf = tf_transformer.transform(X_train_counts)
>>> X_train_tf.shape
(2257, 35788)

En el código de ejemplo anterior, primero utilizamos el método fit(..) para ajustar nuestro estimador a los datos y, en segundo lugar, el método transform(..) para transformar nuestra matriz de recuento a una representación tf-idf. Estos dos pasos pueden combinarse para lograr el mismo resultado final más rápidamente, omitiendo el procesamiento redundante. Esto se hace utilizando el método fit_transform(..) como se muestra a continuación, y como se menciona en la nota de la sección anterior:

>>> tfidf_transformer = TfidfTransformer()
>>> X_train_tfidf = tfidf_transformer.fit_transform(X_train_counts)
>>> X_train_tfidf.shape
(2257, 35788)

Entrenar un clasificador

Ahora que tenemos nuestras características, podemos entrenar un clasificador para intentar predecir la categoría de un post. Empecemos con un clasificador naive Bayes, que proporciona una buena línea de base para esta tarea. scikit-learn incluye varias variantes de este clasificador; la más adecuada para el recuento de palabras es la variante multinomial:

>>> from sklearn.naive_bayes import MultinomialNB
>>> clf = MultinomialNB().fit(X_train_tfidf, twenty_train.target)

Para intentar predecir el resultado en un nuevo documento necesitamos extraer las características utilizando casi la misma cadena de extracción de características que antes. La diferencia es que llamamos a transform en lugar de fit_transform a los transformadores, ya que estos ya han sido ajustados al conjunto de entrenamiento:

>>> docs_new = ['God is love', 'OpenGL on the GPU is fast']
>>> X_new_counts = count_vect.transform(docs_new)
>>> X_new_tfidf = tfidf_transformer.transform(X_new_counts)

>>> predicted = clf.predict(X_new_tfidf)

>>> for doc, category in zip(docs_new, predicted):
...     print('%r => %s' % (doc, twenty_train.target_names[category]))
...
'God is love' => soc.religion.christian
'OpenGL on the GPU is fast' => comp.graphics

Construir un pipeline

Para facilitar el trabajo del vectorizador => transformador => clasificador, scikit-learn proporciona una clase Pipeline que se comporta como un clasificador compuesto:

>>> from sklearn.pipeline import Pipeline
>>> text_clf = Pipeline([
...     ('vect', CountVectorizer()),
...     ('tfidf', TfidfTransformer()),
...     ('clf', MultinomialNB()),
... ])

Los nombres vect, tfidf y clf (clasificador) son arbitrarios. Los utilizaremos para realizar la búsqueda en cuadrícula de hiperparámetros adecuados más adelante. Ahora podemos entrenar el modelo con un solo comando:

>>> text_clf.fit(twenty_train.data, twenty_train.target)
Pipeline(...)

Evaluación del rendimiento en el conjunto de pruebas

Evaluar la precisión predictiva del modelo es igualmente fácil:

>>> import numpy as np
>>> twenty_test = fetch_20newsgroups(subset='test',
...     categories=categories, shuffle=True, random_state=42)
>>> docs_test = twenty_test.data
>>> predicted = text_clf.predict(docs_test)
>>> np.mean(predicted == twenty_test.target)
0.8348...

Hemos conseguido un 83,5% de precisión. Vamos a ver si podemos hacerlo mejor con una máquina de vectores de soporte (SVM), que es ampliamente considerado como uno de los mejores algoritmos de clasificación de texto (aunque también es un poco más lento que el Bayes ingenuo). Podemos cambiar el algoritmos de aprendizaje simplemente introduciendo un objeto clasificador diferente en nuestro pipeline:

>>> from sklearn.linear_model import SGDClassifier
>>> text_clf = Pipeline([
...     ('vect', CountVectorizer()),
...     ('tfidf', TfidfTransformer()),
...     ('clf', SGDClassifier(loss='hinge', penalty='l2',
...                           alpha=1e-3, random_state=42,
...                           max_iter=5, tol=None)),
... ])

>>> text_clf.fit(twenty_train.data, twenty_train.target)
Pipeline(...)
>>> predicted = text_clf.predict(docs_test)
>>> np.mean(predicted == twenty_test.target)
0.9101...

Hemos conseguido un 91,3% de precisión con la SVM. scikit-learn proporciona más utilidades para un análisis más detallado del rendimiento de los resultados:

>>> from sklearn import metrics
>>> print(metrics.classification_report(twenty_test.target, predicted,
...     target_names=twenty_test.target_names))
                        precision    recall  f1-score   support

           alt.atheism       0.95      0.80      0.87       319
         comp.graphics       0.87      0.98      0.92       389
               sci.med       0.94      0.89      0.91       396
soc.religion.christian       0.90      0.95      0.93       398

              accuracy                           0.91      1502
             macro avg       0.91      0.91      0.91      1502
          weighted avg       0.91      0.91      0.91      1502


>>> metrics.confusion_matrix(twenty_test.target, predicted)
array([[256,  11,  16,  36],
       [  4, 380,   3,   2],
       [  5,  35, 353,   3],
       [  5,  11,   4, 378]])

Como era de esperar, la matriz de confusión muestra que los mensajes de los grupos de noticias sobre ateísmo y cristianismo se confunden más a menudo entre sí que con la infografía.

Ejercicio 1: Identificación de la lengua

  • Escribir una pipeline de clasificación de texto utilizando un preprocesador personalizado y CharNGramAnalyzer utilizando datos de artículos de Wikipedia como conjunto de entrenamiento.

  • Evalúa el rendimiento en un conjunto de pruebas que se haya mantenido.

línea de comandos ipython:

%run workspace/exercise_01_language_train_model.py data/languages/paragraphs/

Ejercicio 2: Análisis del sentimiento en las críticas de películas

  • Escribe un pipeline de clasificación de textos para clasificar las críticas de películas como positivas o negativas.

  • Encuentra un buen conjunto de parámetros utilizando la búsqueda en cuadrícula.

  • Evalúa el rendimiento en un conjunto de pruebas retenidas.

línea de comandos ipython:

%run workspace/exercise_02_sentiment.py data/movie_reviews/txt_sentoken/

Ejercicio 3: Utilidad de clasificación de texto CLI

Utilizando los resultados de los ejercicios anteriores y el módulo cPickle de la biblioteca estándar, escribe una utilidad de línea de comandos que detecte el idioma de algún texto proporcionado en stdin y estime la polaridad (positiva o negativa) si el texto está escrito en inglés.

Punto extra si la empresa es capaz de dar un nivel de confianza para sus predicciones.

A partir de aquí, a dónde ir

Aquí hay algunas sugerencias para ayudar a mejorar su intuición de scikit-learn después de la finalización de este tutorial: