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:
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).
Para cada documento
#i
, se cuenta el número de apariciones de cada palabraw
y se almacena enX[i, j]
como valor de la característica#j
dondej
es el índice de la palabraw
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.
Ajuste de parámetros mediante la búsqueda en cuadrícula¶
Ya hemos encontrado algunos parámetros como use_idf
en el TfidfTransformer
. Los clasificadores también suelen tener muchos parámetros; por ejemplo, MultinomialNB
incluye un parámetro de suavizado alpha
y SGDClassifier
tiene un parámetro de penalización alpha
y términos de pérdida y penalización configurables en la función objetivo (ver la documentación del módulo, o utiliza la función help
de Python para obtener una descripción de los mismos).
En lugar de ajustar los parámetros de los distintos componentes de la cadena, es posible realizar una búsqueda exhaustiva de los mejores parámetros en una cuadrícula de valores posibles. Probamos todos los clasificadores con palabras o bigramas, con o sin idf, y con un parámetro de penalización de 0.01 o 0.001 para la SVM lineal:
>>> from sklearn.model_selection import GridSearchCV
>>> parameters = {
... 'vect__ngram_range': [(1, 1), (1, 2)],
... 'tfidf__use_idf': (True, False),
... 'clf__alpha': (1e-2, 1e-3),
... }
Obviamente, una búsqueda tan exhaustiva puede resultar cara. Si tenemos varios núcleos de CPU a nuestra disposición, podemos decirle al buscador de la red que pruebe estas ocho combinaciones de parámetros en paralelo con el parámetro n_jobs
. Si damos a este parámetro un valor de -1
, la búsqueda en cuadrícula detectará cuántos núcleos hay instalados y los utilizará todos:
>>> gs_clf = GridSearchCV(text_clf, parameters, cv=5, n_jobs=-1)
La instancia de búsqueda en cuadrícula se comporta como un modelo scikit-learn
normal. Vamos a realizar la búsqueda en un subconjunto más pequeño de los datos de entrenamiento para acelerar el cálculo:
>>> gs_clf = gs_clf.fit(twenty_train.data[:400], twenty_train.target[:400])
El resultado de llamar a fit
en un objeto GridSearchCV
es un clasificador que podemos utilizar para predict
:
>>> twenty_train.target_names[gs_clf.predict(['God is love'])[0]]
'soc.religion.christian'
Los atributos best_score_
y best_params_
del objeto almacenan la mejor puntuación media y la configuración de los parámetros correspondientes a esa puntuación:
>>> gs_clf.best_score_
0.9...
>>> for param_name in sorted(parameters.keys()):
... print("%s: %r" % (param_name, gs_clf.best_params_[param_name]))
...
clf__alpha: 0.001
tfidf__use_idf: True
vect__ngram_range: (1, 1)
Un resumen más detallado de la búsqueda está disponible en gs_clf.cv_results_
.
El parámetro cv_results_
puede ser fácilmente importado a pandas como un DataFrame
para su posterior inspección.
Ejercicios¶
Para hacer los ejercicios, copia el contenido de la carpeta “skeletons” como una nueva carpeta llamada “workspace”:
cp -r skeletons workspace
A continuación, puedes editar el contenido del espacio de trabajo sin temor a perder las instrucciones originales del ejercicio.
A continuación, inicia un terminal ipython y ejecuta el script de trabajo en curso con:
[1] %run workspace/exercise_XX_script.py arg1 arg2 arg3
Si se produce una excepción, utilice %debug
para iniciar una sesión ipdb post mortem.
Perfecciona la aplicación e itera hasta resolver el ejercicio.
Para cada ejercicio, el archivo de esqueleto proporciona todas las sentencias de importación necesarias, el código de plantilla para cargar los datos y el código de muestra para evaluar la precisión predictiva del modelo.
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:
Intenta jugar con el
analyzer
y latoken normalisation
enCountVectorizer
.Si no tienes etiquetas, intenta utilizar Clustering en tu problema.
Si tienes varias etiquetas por documento, por ejemplo, categorías, echa un vistazo a la sección Multiclase y multietiqueta.
Prueba utilizar Truncated SVD para el análisis semántico latente.
Echa un vistazo al uso de Out-of-core Classification para aprender de los datos que no caben en la memoria principal de la computadora.
Echa un vistazo a la Hashing Vectorizer como una alternativa de memoria eficiente a
CountVectorizer
.