Imaginemos que queremos construir un servicio que recomiende artículos para leer basándose en las preferencias de los usuarios. Una forma de empezar es preguntando a los usuarios por qué tipo de noticias les interesa más (deportes, política, economía, etc.) y sugerirles noticias de aquellos temas que han elegido. Pero, si queremos enviar noticias de distintas fuentes, más o menos conocidas, primero vamos a tener que resolver un problema: ¿cómo sabemos a qué categoría pertenece una noticia? En esta entrada veremos una forma rápida y sencilla de hacer esto, con una implementación real que funciona gracias a la librería scikit-learn de Python. El resultado final será un programa capaz de clasificar un texto en función de otros textos que ha analizado previamente y que le han servido para aprender a clasificar textos. Hablamos de machine learning en estado puro.

Representación de documentos

Consideremos un texto (o documento) como un conjunto letras, que forman palabras, que, a su vez, forman párrafos, que, finalmente, componen un texto. Queremos que nuestro programa aprenda de qué trata cada documento**, para lo que vamos a necesitar representar los documentos de una forma que podamos procesar después.

Una forma muy común de representar documentos en problemas de clasificación es mediante una bolsa de palabras, que no es más que una estructura que contendrá todas las palabras que aparecen en el documento junto con el número de veces que aparece en el mismo. Veamos un ejemplo con un documento muy sencillo:

La política puede llegar a ser muy divertida. Las mejores noticias sobre política directamente en su smartphone.

Este documento podría representarse así:

a directamente en la las llegar mejores muy noticias política puede ser smartphone sobre su
1 1 1 1 1 1 1 1 1 2 1 1 1 1 1

Como podemos ver, todas las palabras aparece una sola vez excepto la palabra política, que aparece dos.

Distancia entre dos documentos: Cuánto se parecen dos artículos

Ahora que tenemos una forma de representar documentos, veamos cómo podemos medir cuánto se parecen entre sí, es decir, vamos a buscar una medida de similitud para dos documentos. Tomemos las dos bolsas de palabras que representan cada documento:

1 0 0 0 5 3 0 0 1 0 0 0 0
3 0 0 0 2 0 0 1 0 1 0 0 0

Hemos ordenado la lista con las apariciones de cada palabra de la misma forma en ambos casos, por lo que sabemos que una cierta palabra aparece una vez en el primer documento y tres veces en el segundo, otra aparce cinco veces en el primer documento y dos en el segundo; otras sólo aparecen en uno de ellos o en ninguno (Llegados a este punto, realmente no nos importa qué palabras son).

Podemos medir la similitud de dos documentos multiplicando las apariciones de cada palabra en cada uno de los documentos entre sí y sumando los resultados para todas las palabras:

1 * 3 + 0 * 0 + 0 * 0 + 0 * 0 + 5 * 2 + 3 * 0 + 0 * 0 + 0 * 1 + 1 * 0 + 0 * 1 + 0 * 0 + 0 * 0 + 0 * 0 = 13

Ahora os podréis preguntar, ¿realmente esto funciona? Pongámosle cara a los ejemplos para entender un poco mejor lo que está pasando. Supongamos que los dos primeros documentos corresponden a dos artículos sobre deportes (concretamente fútbol) y que las palabras cuya frecuencia estábamos represetando eran las que siguen:

Messi rifle conflicto paz gol portero guerra seguridad regate falta política guerrillero banda
1 0 0 0 5 3 0 0 1 0 0 0 0
3 0 0 0 2 0 0 1 0 1 0 0 0

Supongamos que ahora queremos ver cómo de similar es el primer artículo con otro sacado de la sección de internacional del periódico, que trata sobre un conflicto armado:

Messi rifle conflicto paz gol portero guerra seguridad regate falta política guerrillero banda
0 0 1 0 0 0 9 0 0 0 6 4 0

Usando la misma fórmula que en el caso anterior:

1 * 0 + 0 * 0 + 0 * 1 + 0 * 0 + 5 * 0 + 3 * 0 + 0 * 9 + 0 * 0 + 1 * 0 + 0 * 0 + 0 * 6 + 0 * 4 + 0 * 0 = 0

Tenemos que nuestra medida de similitud nos da en este caso un valor de cero. Es decir, los dos artículos de fútbol tienen una medida de similitud de 13 mientras que el primer artículo de fútbol y el que trata sobre el conflicto armado tienen una medida de similitud de cero. Este resultado concuerda con la idea intuitiva que tenemos acerca de cómo de parecidos son estos artículos.

El problema de la longitud de documentos

Es fácil darse cuenta de que esta forma de calcular el parecido entre dos documentos ponderará de forma distinta documentos con distinto tamaño. Imaginemos que los dos artículos de deportes son el doble de largos y que la aparición de las palabras se mantiene en la misma proporción (irreal pero ilustrativo). Las bolsas de palabras se parecerán a las siguientes:

2 0 0 0 10 0 6 0 0 2 0 0 0 0
6 0 0 0 4 0 0 2 0 2 0 0 0 0

Si calculamos la similitud de ambos ahora tendremos:

2 * 6 + 0 * 0 + 0 * 0 + 0 * 0 + 10 * 4 + 6 * 0 + 0 * 0 + 0 * 2 + 2 * 0 + 0 * 2 + 0 * 0 + 0 * 0 + 0 * 0 = 52

Como vemos, el número es sensiblemente superior al original, 13. Este problema podemos resolverlo normalizando el vector con las apariciones de cada palabra. Para ello, sólo tenemo que dividirlo por la raíz cuadrada de la suma de los cuadrados de cada elemento (en el caso del primero documento, es 6):

1/6 0 0 0 5/6 3/6 0 0 1/6 2 0 0 0

Priorizando las palabras distintivas

Mediante la normalización podemos librarnos del problema con documentos con distintos tamaños pero aún podemos mejorar nuestra forma de comparar documentos basándonos en las palabras que aparecen en ellos.

Hay palabras que se repetirán mucho en todos los documentos (verbos y sustantivos muy comunes, preposiciones, etc.) mientras que otras se repetirán mucho únicamente para aquellos documentos relacionados. Las primeras serán palabras comunes en el conjunto de todos los documentos que harán que la medida de similitud entre dos documentos cualesquiera sea más alta aún no estando relacionados. Las segundas son las que realmente nos interesa ponderar, pues son la que realmente diferencian documentos parecidos de documentos sin relación. Es decir, queremos ponderar aquellas palabras que:

  • Aparecen mucho en un documento
  • Aparecen poco en el conjunto de todos los documentos

Decimos que las palabras importantes son aquellas localmente comunes y globalmente raras. Así llegamos al vector de frecuencias de términos y frecuencia inversa en documentos (term frequency - inverse document frequency, tf-idf). El cálculo de dicho vector para un documento es sencillo. Partiendo del vector original donde ya teníamos la frecuencia de cada palabra en un documento. Consideramos también un vector para las mismas palabras donde cada valor se calcula como:

Fórmula palabras tf-idf

Si miramos con detalle esta fórmula para cada palabra, veremos que para aquellas palabras que aparecen en casi todos los documentos el resultado será el logaritmo de un número cercano a 1, el cual se aproxima a cero, mientras que, para palabras que aparecen en pocos documentos, será el logaritmo de un número cada vez mayor conforme la palabra aparece en menos documentos (más rara es). El cálculo del vector tf-idf para un documento no es más que el resultado de multiplicar el primer verctor (el de frecuencias de palabras en un documento) por éste último.

Gracias a la ponderación que se hace de las palabras realmente importantes, nuestra medida de similitud entre dos documentos será mucho más precisa si utilizamos el vector tf-idf que si sencillamente usamos el vector de frecuencias de palabras.

Encontrando documentos parecidods

Supongamos que tenemos un conjunto de documentos y queremos saber cuál es el documento que más se parece a un documento cualquiera. Calculando el vector tf-idf para el primer documento así como para el conjunto de documentos que tenemos, y calculando el parecido entre documentos usando dicho vector tf-idf, el documento con un valor más alto según nuestra medida de similitud será el candidato que más se parece dentro de nuestro cuerpo de documentos, es decir, será el vecino más cercano.

Agrupando documentos parecidos

Hemos visto que podemos definir el documento más parecido a otro como el documento más cercano. Es decir, podemos usar nuestra medida de similitud como una medida de distancia entre documentos. Esto nos permite utilizar uno de los algoritmos más famosos en machine learning para agrupar documentos que se parecen: K-means clustering. Sin entrar en muchos más detalles, nos basta saber que mediante este algoritmo podemos agrupar un conjunto de elementos por cercanía entre los mismos. Como ya tenemos una medida de la distancia entre documentos, podemos utilizarlo para obtener grupos de elementos que minizan la distancia respecto del centro de la agrupación. Así, formaremos categorías de documentos que se parecen entre sí.

Una vez que tengamos los grupos formados para un conjunto de documentos (ejemplos que utilizaremos para el aprendizaje) podemos predecir a qué categoría pertenece un nuevo documento calculando su distancia al centro de cada una de las categorías que tenemos. La categoría predicha será aquella cuyo centro está más cercano al documento.

Clasificando documentos con SCIKIT-LEARN

Partamos de un conjunto de artículos de ejemplo extraídos de algunos periódicos online. Dichos artículos pertenecen a las secciones de deportes, ciencia y economía. Cada artículo está almacenado en un fichero de texto dentro de un directorio llamado igual que la categoría a la que pertenece el artículo.

Vamos a empezar leyendo todos los artículos y creando un diccionario de artículos y etiquetas (cada etiqueta será el nombre de la categoría a la que pertenece).

def read_all_documents(root):
    labels = []
    docs = []
    for r, dirs, files in os.walk(root):
        for file in files:
            with open(os.path.join(r, file), "r") as f:
                docs.append(f.read())     
            labels.append(r.replace(root, ''))
    return dict([('docs', docs), ('labels', labels)])

Y ejecutamos al función anterior proporcionando el directorio raíz donde se encuentra cada uno de los directorios con los artículos:

data = read_all_documents('examples')
documents = data['docs']
labels = data['labels']

Creamos nuestra matriz tf-idf con el conjunto de artículos que tenemos. Para esto, vamos a utilizar la librería de Python scikit-learn:

from sklearn.feature_extraction.text import TfidfVectorizer

X_train = tfid.fit_transform(documents)
y_train = labels

Ahora que tenemos una representación adecuada del cuerpo de documentos de entrenamiento (tf-idf), ejectuamos nuestro algoritmo de aprendizaje (K-means clustering), para lo que también utilizaremos scikit-learn:

from sklearn.neighbors import KNeighborsClassifier

clf = KNeighborsClassifier(n_neighbors=3)
clf.fit(X_train, y_train)

Ahora tenemos un clasificador (clf) que puede predecir la categoría de un artículo. Probamos con un conjunto nuevo de datos y medimos cómo de buena es la predicción:

test = read_all_documents('examples2')
X_test = tfid.transform(test['docs'])
y_test = test['labels']
pred = clf.predict(X_test)

print('accuracy score %0.3f' % clf.score(X_test, y_test))

Con los conjuntos de entrenamiento y prueba proporcionados, tenemos predicciones razonablemente buenas, teniendo en cuenta lo reducido de nuestro conjunto de artículos de ejemplos (no teníamos más de 50 artículos para ninguna de las categorías).

Podemos mejorar nuestro clasificador de una forma muy sencilla. Como vimos antes, tf-idf intenta obviar aquellas palabras que se repiten en muchos documentos y que, por lo tanto, no sirven para distinguirlos. Sin embargo, podemos hacerle la vida más fácil y obtener mejores resultados si sencillamente nosotros proporcionamos una lista de palabras que no queremos que se tengan en cuenta en absoluto. Por ejemplo, para artículos en español podríamos hacer lo siguiente:

prepositions =['a','ante','bajo','cabe','con','contra','de','desde','en','entre','hacia','hasta','para','por','según','sin','so','sobre','tras']
prep_alike = ['durante','mediante','excepto','salvo','incluso','más','menos']
adverbs = ['no','si','']
articles = ['el','la','los','las','un','una','unos','unas','este','esta','estos','estas','aquel','aquella','aquellos','aquellas']
aux_verbs = ['he','has','ha','hemos','habéis','han','había','habías','habíamos','habíais','habían']

tfid = TfidfVectorizer(stop_words=prepositions+prep_alike+adverbs+articles+aux_verbs)

Con este sencillo ajuste, nuestro clasificador obtiene ahora una fiabilidad de más del 92%. ¿A que no ha sido difícil?

Conclusión

Hemos visto una técnica para clasificar documentos y una implementación que, pese a no llegar a 50 líneas de código, obtiene buenos resultados con menos de 50 artículos de ejemplo por categoría. Como vemos, cada vez utilizar técnicas de machine learning requiere menos esfuerzo. Con unas cuantas líneas de código en Python, podemos crear un prototipo bastante rápido y luego, si creemos que los resultados son prometedores, meternos más de lleno. ¿Queréis saber alguna aplicación que pueda tener un clasificador de artículos como el que hemos visto? Por ejemplo, para determinar automáticamente en qué idioma está escrito un artículo.

Código fuente y artículos de ejemplo

Puedes encontrar todo el código fuente utilizado en este artículo así como un conjunto de artículos para entrenar vuestro clasificador y comprobar su precisión en el siguiente repositorio en Gthub: https://github.com/joragupra/text-classifier.

** Realmente nos basta con que aprenda a reconocer documentos que tratan sobre el mismo tema.