Introducción al topic modeling con Gensim (II): asignación de tópicos

Mar 31, 2021 | Machine Learning, NLP | 2 Comentarios

En la anterior publicación aprendimos lo que es el Topic Modeling y el funcionamiento de su modelo más popular denominado Latent Dirichlet Allocation (LDA), utilizado principalmente para la extracción de tópicos en textos.

También comenzamos a realizar nuestro ejemplo práctico, en el cual estamos interesados en aplicar el modelo LDA para la asignación de tópicos en noticias periodísticas. Para ello, recopilamos 5665 noticias extraídas de distintos periódicos digitales españoles y les aplicamos el preprocesamento necesario que incluye la tokenización de los textos, la eliminación de las stopwords y el proceso de stemming. Ahora que ya tenemos todo lo necesario, ¡es hora de pasar a la acción!

En esta publicación, seguiremos justo donde lo dejamos (si te perdiste el anterior puedes echarle un vistazo aquí) y construiremos por fin nuestro modelo LDA entrenándolo con las noticias ya preprocesadas usando para ello la librería Gensim de Python. Finalmente, evaluaremos los resultados obtenidos.

Pero antes que nada, tenemos que construir un diccionario con todas las palabras y un corpus con la frecuencia de las palabras en cada documento. Así, estos dos serán las principales entradas de nuestro modelo LDA. Veamos en primer lugar cómo podemos obtenerlos.

 

Generación del diccionario y corpus, principales entradas del modelo LDA

Primeramente, importamos las librerías que vamos a necesitar para este tutorial práctico:

In [1]:
from gensim.corpora import Dictionary
from gensim.models import LdaModel
import random
import numpy as np
import matplotlib.pyplot as plt
from wordcloud import WordCloud
%matplotlib inline
Los diccionarios son objetos que asignan un identificador numérico a cada palabra única y se pueden usar para obtener el identificador a partir de la palabra y viceversa.

La generación de estos diccionarios con la librería Gensim es tan fácil como crear un objeto de tipo Dictionary pasándole como argumento las listas de palabras. En nuestro caso y como vimos en el anterior post, tenemos almacenadas las listas de palabras, pertenecientes al conjunto de noticias, en la columna Tokens de nuestro objeto DataFrame.

In [2]:
diccionario = Dictionary(df.Tokens)
print(f'Número de tokens: {len(diccionario)}')
Número de tokens: 47388
Después, reducimos el diccionario filtrando las palabras más raras o demasiado frecuentes. Para ello haremos uso de la función filter_extremes, que nos proporciona el objeto Dictionary y que nos servirá para mantener únicamente aquellos tokens que se encuentran en al menos 2 documentos (no_below) y los que están contenidos en no más del 80% de documentos (no_above). En este último caso le debemos pasar una fracción del tamaño del corpus como podéis ver en el siguiente ejemplo:
In [3]:
diccionario.filter_extremes(no_below=2, no_above = 0.8)
print(f'Número de tokens: {len(diccionario)}')
Número de tokens: 25522
De esta forma pasaremos de tener 47.377 tokens únicos en el diccionario a tener 25.522 tokens.

Luego, inicializamos el corpus en base al diccionario que acabamos de crear. Cada documento se transformará en una bolsa de palabras (BOW del inglés bag-of-words) con las frecuencias de aparición.

Tras aplicar esta técnica veremos que cada documento está representado como una lista de tuplas donde el primer elemento es el identificador numérico de la palabra y el segundo es el número de veces que esa palabra aparece en el documento.

A continuación se muestra como construimos el corpus aplicando la función doc2bow del diccionario a cada lista de palabras:

In [4]:
# Creamos el corpus 
corpus = [diccionario.doc2bow(noticia) for noticia in df.Tokens]

# Mostramos el BOW de una noticia
print(corpus[6])
[(3, 1), (25, 1), (26, 6), (29, 1), (40, 1), (41, 3), (44, 1), (48, 7), (52, 2), (67, 1), (68, 1), (77, 1), (86, 1), (94, 1), (96, 1), (108, 4), (116, 1), (118, 1), (121, 1), (131, 2), (146, 2), (149, 2), (164, 1), (172, 1), (176, 2), (178, 1), (193, 1), (204, 1), (210, 1), (222, 1), (235, 3), (236, 4), (238, 1), (245, 1), (268, 1), (276, 1), (283, 1), (295, 1), (299, 1), (311, 3), (312, 2), (339, 1), (349, 1), (367, 11), (372, 1), (394, 12), (407, 1), (413, 1), (431, 1), (436, 2), (439, 1), (440, 1), (450, 1), (454, 2), (462, 2), (475, 3), (478, 2), (492, 1), (498, 1), (502, 2), (513, 2), (525, 1), (531, 3), (549, 1), (561, 1), (574, 1), (587, 2), (615, 1), (631, 1), (640, 1), (650, 3), (653, 2), (656, 1), (660, 1), (677, 1), (680, 1), (683, 1), (684, 1), (686, 2), (694, 1), (732, 1), (784, 2), (793, 2), (794, 1), (804, 2), (817, 3), (830, 1), (839, 1), (840, 1), (842, 1), (852, 1), (866, 2), (913, 1), (915, 1), (917, 2), (921, 2), (933, 1), (958, 1), (1055, 1), (1057, 1), (1060, 1), (1073, 1), (1122, 1), (1140, 1), (1141, 1), (1142, 1), (1143, 1), (1144, 1), (1145, 1), (1146, 1), (1147, 1), (1148, 1), (1149, 1), (1150, 6), (1151, 1), (1152, 1), (1153, 1), (1154, 13), (1155, 1), (1156, 1), (1157, 1), (1158, 3), (1159, 1), (1160, 1), (1161, 1), (1162, 1), (1163, 1), (1164, 1), (1165, 1), (1166, 2), (1167, 3), (1168, 1), (1169, 1), (1170, 1), (1171, 1), (1172, 1), (1173, 1), (1174, 1), (1175, 1), (1176, 2), (1177, 2), (1178, 1), (1179, 1), (1180, 1), (1181, 1), (1182, 1), (1183, 1), (1184, 1), (1185, 1), (1186, 1), (1187, 1), (1188, 1), (1189, 1), (1190, 1), (1191, 1), (1192, 1), (1193, 1), (1194, 1), (1195, 1), (1196, 1), (1197, 2), (1198, 1), (1199, 1), (1200, 1), (1201, 1), (1202, 1), (1203, 2), (1204, 3), (1205, 1), (1206, 1), (1207, 1), (1208, 1), (1209, 1), (1210, 2), (1211, 1), (1212, 1), (1213, 1), (1214, 1), (1215, 10), (1216, 1), (1217, 1), (1218, 1), (1219, 1), (1220, 1), (1221, 1), (1222, 1), (1223, 1), (1224, 1), (1225, 1), (1226, 3), (1227, 1), (1228, 2), (1229, 1), (1230, 3)]
En el resultado se muestra la representación BOW de una noticia cualquiera (print(corpus[6])) en la que los primeros 5 elementos son: [(3, 1), (25, 1), (26, 6), (29, 1), (40, 1) …].

Así, la primera tupla (3, 1) indica que, para el primer documento, la palabra con el identificador 3 aparece una vez. Mediante el comando (diccionario[3]) podéis ver que el identificador 3 corresponde al token afirm.

 

Construyendo el modelo LDA

Es hora de construir nuestro modelo LDA. Para ello simplemente creamos un objeto LdaModel de la librería Gensim pasándole como argumento el corpus y el diccionario y le indicamos adicionalmente los siguientes parámetros:

  • num_topics: número de tópicos. Para este tutorial extraeremos 50 tópicos.
  • random_state: parámetro para controlar la aleatoriedad del proceso de entrenamiento y que nos devuelva siempre los mismos resultados.
  • chunksize: número de documentos que será utilizado en cada pasada de entrenamiento.
  • passes: número de pasadas por el corpus durante el entrenamiento.
  • alpha: representa la densidad de tópicos por documento. Un mayor valor de este parámetro implica que los documentos estén compuestos de más tópicos. En este caso, fijamos el valor en auto.

Por supuesto, hay más parámetros para tener en cuenta en el entrenamiento de un modelo LDA y que podéis consultar en la documentación de Gensim, pero en este tutorial solo utilizaremos los anteriores descritos.

En el siguiente código mostramos como construimos nuestro modelo:

In [5]:
lda = LdaModel(corpus=corpus, id2word=diccionario, 
               num_topics=50, random_state=42, 
               chunksize=1000, passes=10, alpha='auto')
Para visualizar los tópicos extraídos podemos utilizar la función print_topics indicándole el número de tópicos y el número de palabras por tópico que queremos que se muestre:
In [6]:
topicos = lda.print_topics(num_words=5, num_topics=20)
for topico in topicos:
    print(topico)
(32, '0.065*"azul" + 0.055*"eolic" + 0.041*"sueñ" + 0.039*"arroy" + 0.037*"luz"')
(10, '0.103*"dolar" + 0.084*"uu" + 0.084*"ee" + 0.025*"jon" + 0.023*"street"')
(38, '0.173*"encuest" + 0.047*"votant" + 0.042*"dat" + 0.041*"ciudadan" + 0.040*"cis"')
(20, '0.081*"israel" + 0.042*"palestin" + 0.019*"netanyahu" + 0.018*"trump" + 0.014*"territori"')
(29, '0.133*"dependent" + 0.042*"punic" + 0.038*"dependient" + 0.038*"ramirez" + 0.029*"list"')
(34, '0.121*"cantabri" + 0.087*"santand" + 0.048*"cantabr" + 0.040*"miguel" + 0.031*"mar"')
(24, '0.028*"efect" + 0.024*"estudi" + 0.015*"tratamient" + 0.014*"pued" + 0.013*"salud"')
(9, '0.039*"miguel" + 0.023*"cas" + 0.015*"conden" + 0.013*"pnv" + 0.012*"corrupcion"')
(37, '0.305*"castill" + 0.184*"leon" + 0.085*"junt" + 0.064*"valladol" + 0.038*"provinci"')
(4, '0.035*"cas" + 0.031*"univers" + 0.027*"mast" + 0.020*"cifuent" + 0.015*"curs"')
(26, '0.032*"millon" + 0.024*"eur" + 0.019*"econom" + 0.014*"año" + 0.009*"banc"')
(35, '0.009*"proyect" + 0.008*"agu" + 0.007*"desarroll" + 0.006*"cambi" + 0.006*"medi"')
(17, '0.021*"inform" + 0.016*"public" + 0.011*"empres" + 0.008*"eur" + 0.007*"segun"')
(48, '0.015*"barri" + 0.013*"social" + 0.012*"viv" + 0.012*"call" + 0.012*"person"')
(43, '0.020*"pp" + 0.020*"part" + 0.018*"gobiern" + 0.013*"polit" + 0.010*"sanchez"')
(1, '0.017*"gobiern" + 0.014*"comun" + 0.013*"sanitari" + 0.010*"salud" + 0.008*"sanid"')
(46, '0.008*"histori" + 0.007*"libr" + 0.006*"pelicul" + 0.005*"cin" + 0.005*"años"')
(49, '0.014*"tribunal" + 0.008*"fiscal" + 0.008*"judicial" + 0.008*"justici" + 0.007*"derech"')
(11, '0.012*"años" + 0.010*"pas" + 0.009*"cas" + 0.009*"hac" + 0.008*"dos"')
(19, '0.022*"hac" + 0.017*"si" + 0.014*"pued" + 0.011*"cre" + 0.011*"ser"')
Por cada tópico podemos ver las cinco palabras que más contribuyen a ese tópico y sus pesos en el mismo. El peso nos indica como de importante es la palabra en ese tópico. Esto significa que, por ejemplo, las cinco principales palabras en el tópico 32 son: azul, eolic, sueñ, arroy y luz. Por lo tanto, esta claro que este tópico se centra en el medio ambiente y la energía renovable.

Es fácil también interpretar que el tópico 26 tiene relación con la economía al contener palabras como millón, eur y banc y el tópico 38 se centra en elecciones, ya que sus palabras más significativas son encuest, votant y dat.

También podemos visualizar las palabras más importantes de cada tópico mediante nube de palabras, donde el tamaño de cada palabra corresponde con su contribución en el tópico. En el siguiente código construimos un gráfico de nube de palabras gracias a la librería wordcloud:

In [7]:
for i in range(1, 5):
    plt.figure()
    plt.imshow(WordCloud(background_color='white', prefer_horizontal=1.0)
               .fit_words(dict(lda.show_topic(i, 20))))
    plt.axis("off")
    plt.title("Tópico " + str(i))
    plt.show()
 

 

Evaluación del modelo

Para evaluar el rendimiento del modelo, estimaremos los tópicos en dos documentos diferentes. En el primer caso, escogeremos un documento de entre las noticias utilizadas en el corpus para entrenar el modelo y para la segunda prueba utilizaremos una nueva noticia.

Así, empezamos escogiendo una noticia del corpus al azar y mostramos su contenido y titular:

In [8]:
indice_noticia = random.randint(0,len(df))
noticia = df.iloc[indice_noticia]
print("Titular: " + noticia.Titular)
print(noticia.Noticia)
Titular: Cerradas otras seis aulas de Infantil y Primaria en Astillero, Reinosa y Torrelavega
Seis aulas de cinco colegios de Cantabria han sido cerradas por los positivos de cinco alumnos y de un docente que han obligado a poner en cuarentena preventiva a toda la clase, con lo que se elevan a 22 en total en la comunidad desde el inicio del curso…
Como podemos ver, esta noticia trata sobre el cierre de aulas en Cantabria debido a la pandemia del COVID-19. En un primer momento podemos pensar que los tópicos asignados por el modelo deberían tener relación con el COVID-19, la salud o incluso Cantabria. ¡Veamos el resultado!

Primero debemos obtener la representación BOW del documento y la distribución de los tópicos:

In [9]:
bow_noticia = corpus[indice_noticia]
distribucion_noticia = lda[bow_noticia]
Después, sacamos los índices y la contribución (proporción) de los tópicos más significativos para nuestra noticia y los mostramos en un gráfico de barras para un mejor entendimiento.
In [10]:
# Indices de los topicos mas significativos
dist_indices = [topico[0] for topico in lda[bow_noticia]]
# Contribución de los topicos mas significativos
dist_contrib = [topico[1] for topico in lda[bow_noticia]]
In [11]:
distribucion_topicos = pd.DataFrame({'Topico':dist_indices,
                                     'Contribucion':dist_contrib })
distribucion_topicos.sort_values('Contribucion', 
                                 ascending=False, inplace=True)
ax = distribucion_topicos.plot.bar(y='Contribucion',x='Topico', 
                                   rot=0, color="orange",
                                   title = 'Tópicos mas importantes'
                                   'de noticia ' + str(indice_noticia))
grafico1

El tópico más predominante en la noticia es el 34, seguido de los tópicos 0 y 5. Seguidamente, imprimimos las palabras más significativas de estos.

In [12]:
for ind, topico in distribucion_topicos.iterrows():
    print("*** Tópico: " + str(int(topico.Topico)) + " ***")
    palabras = [palabra[0] for palabra in lda.show_topic(
        topicid=int(topico.Topico))]
    palabras = ', '.join(palabras)
    print(palabras, "\n")
*** Tópico: 34 ***
cantabri, santand, cantabr, miguel, mar, angel, salmeron, menendez, torrelaveg, ceip 

*** Tópico: 0 ***
cas, resident, posit, person, dat, fallec, ultim, centr, nuev, cov 

*** Tópico: 5 ***
educ, centr, alumn, niñ, curs, colegi, profesor, clas, aul, famili 

*** Tópico: 36 ***
guerr, vasc, avion, ejercit, segund, dos, histori, part, mundial, navarr 

*** Tópico: 30 ***
vot, eleccion, electoral, part, alcald, ayunt, candidat, pod, municipal, concejal 

No íbamos mal desencaminados. El tópico 34 se centra en la provincia de Cantabria, mientras que el tópico 0 tiene relación con el COVID-19 al contener palabras como cas, posit y fallec. Por último, el tópico 5 tiene que ver con las escuelas y la educación.

¡El modelo no pinta nada mal! Para la segunda prueba, asignaremos los tópicos a un nuevo documento no utilizado en el entrenamiento del modelo LDA. La noticia escogida trata sobre la violencia de género en Cantabria.

Antes de nada abrimos la noticia que esta en formato texto:

In [13]:
texto_articulo = open("noticia1.txt")
articulo_nuevo = texto_articulo.read().replace("\n", " ")
texto_articulo.close()
Posteriormente, realizamos dos pasos: en primer lugar, debemos preprocesar el documento al igual que hicimos con el corpus y después realizamos las estimaciones.
In [14]:
articulo_nuevo = limpiar_texto(articulo_nuevo)
articulo_nuevo = tokenizer.tokenize(articulo_nuevo)
articulo_nuevo = filtrar_stopword_digitos(articulo_nuevo)
articulo_nuevo = stem_palabras(articulo_nuevo)
articulo_nuevo
Out[1]:
['cantabri',
 'viv',
 'año',
 'negr',
 'refier',
 'violenci',
 'gener',
 'doc',
 'mes',
 'comun',
 'acumul',
 'total',
 'denunci',
 'mujer',
 'victim',
 'violenci',
 'machist',
 ...]
In [15]:
bow_articulo_nuevo = diccionario.doc2bow(articulo_nuevo)
Una vez tenemos la representación BOW, solo nos queda mostrar los resultados:
In [16]:
# Indices de los topicos mas significativos
dist_indices = [topico[0] for topico in lda[bow_articulo_nuevo]]
# Contribucion de los topicos mas significativos
dist_contrib = [topico[1] for topico in lda[bow_articulo_nuevo]]
In [17]:
distribucion_topicos = pd.DataFrame({'Topico':dist_indices,
                                     'Contribucion':dist_contrib })
distribucion_topicos.sort_values('Contribucion', 
                                 ascending=False, inplace=True)
ax = distribucion_topicos.plot.bar(y='Contribucion',x='Topico', 
                                   rot=0, color="green",
                                   title = 'Tópicos más importantes' 
                                   'para documento nuevo')
grafico2

El tópico 7 es el más significante con diferencia en la noticia. Imprimimos de nuevo las palabras más significativas:

In [18]:
for ind, topico in distribucion_topicos.iterrows():
    print("*** Tópico: " + str(int(topico.Topico)) + " ***")
    palabras = [palabra[0] for palabra in lda.show_topic(
        topicid=int(topico.Topico))]
    palabras = ', '.join(palabras)
    print(palabras, "\n")
*** Tópico: 7 ***
mujer, sexual, violenci, gener, hombr, person, muj, femin, victim, iguald 

*** Tópico: 1 ***
gobiern, comun, sanitari, salud, sanid, situacion, med, canari, public, autonom 

*** Tópico: 49 ***
tribunal, fiscal, judicial, justici, derech, delit, suprem, cas, ley, sentenci 

*** Tópico: 0 ***
cas, resident, posit, person, dat, fallec, ultim, nuev, centr, cov 

*** Tópico: 11 ***
años, pas, cas, hac, dos, sal, lleg, dia, trabaj, llev 

*** Tópico: 34 ***
cantabri, santand, cantabr, miguel, mar, angel, salmeron, menendez, torrelaveg, ceip 

*** Tópico: 41 ***
franc, victim, español, españ, memori, civil, muert, polit, derech, iglesi 

*** Tópico: 35 ***
proyect, agu, desarroll, cambi, medi, climat, form, nuev, zon, ciud 

*** Tópico: 21 ***
person, migrant, lleg, inmigr, acog, refugi, fronter, españ, pais, rescat 

*** Tópico: 23 ***
navarr, cataluny, barcelon, catalan, generalitat, torr, govern, foral, independent, cataluñ 

*** Tópico: 46 ***
histori, libr, pelicul, cin, años, vid, mund, nuev, public, personaj 

Observando las principales palabras del tópico 7 (mujer, sexual, violencia, gener y hombr) podemos decir que coincide claramente con la temática principal de la noticia, es decir, la violencia de género.

Finalmente guardamos el modelo y el diccionario para utilizarlo más adelante. Para ello usamos la función save() en ambos casos:

In [19]:
lda.save("articulos.model")
diccionario.save("articulos.dictionary")
 

Eso ha sido todo por hoy. En este tutorial hemos visto que el modelo LDA es una buena herramienta para clasificar noticias periodísticas, pero convendría utilizar muchas más noticias para entrenar el modelo y de esta forma obtener un mejor rendimiento.

En la siguiente publicación de la serie aprenderemos como calcular la similitud de documentos a partir de los tópicos obtenidos. Mientras tanto, el cuaderno Jupyter con el código utilizado hasta ahora lo podéis descargar aquí. ¡Hasta la próxima!

También te puede interesar:

Diferencias entre inferencia y predicción

Entiende las diferencias entre inferencia y predicción, dos conceptos de la estadística y el machine learning que pueden resultar confusos.

Libros recomendados para adentrarse en el machine learning

Lista de cinco libros recomendables para principiantes que quieran aprender machine learning y ciencia de datos.

Introducción al topic modeling con Gensim (I): fundamentos y preprocesamiento de textos

En esta publicación entenderéis los fundamentos del topic modeling (modelo LDA) y se mostrará como realizar el preprocesamento necesario a los textos: tokenización, eliminación de stopwords, etc.

Introducción al topic modeling con Gensim (III): similitud de textos

En este post mostramos como utilizar la técnica de topic modeling para obtener la similitud entre textos teniendo en cuenta la semántica

Extracción de datos de Twitter con Python (sin consumir la API)

En esta publicación os enseñaremos como poder extraer datos de Twitter en Python mediante la librería Twint. De esta forma, podremos obtener facilmente los últimos tweets que contengan cierta palabra o que pertenezcan a un determinado usuario y aplicar varios filtros.

Sentiment analysis en críticas de películas mediante regresión logística

El sentimiento de análisis es una técnica que nos permite identificar la opinión emocional que hay detrás de un textol. En este artículo mostramos como construir un modelo de predicción capaz de distinguir entre críticas positivas y negativas. Estas críticas han sido descargadas previamente de la web de filmaffinity.

AutoML: creación de un modelo de análisis de sentimiento con Google Cloud AutoML

Descubre en que consiste el AutoML, que nos permite automatizar varias partes del proceso de Machine Learning y aprende a utilizar Google Cloud AutoML para realizar una tarea de sentiment analysis y construir un clasificador capaz de identificar si una crítica de película es positiva o negativa.

Introducción al clustering (I): algoritmo k-means

En este artículo explicamos el algoritmo de clustering k-means, el cual busca instancias centradas en un punto determinado, llamado centroide. Después de explicar su funcionamiento, lo aplicaremos en Python a un conjunto de datos y visualizaremos los resultados obtenidos.

Machine Learning vs. Deep Learning: Comprendiendo las diferencias en la inteligencia artificial

Explora las diferencias entre Machine Learning y Deep Learning en este artículo y comprende cuándo utilizar cada enfoque en la inteligencia artificial

Dominando Apache Spark (I): Introducción y ventajas en el procesamiento de grandes volúmenes de datos

En el artículo, exploramos la historia y ventajas de Apache Spark como un marco de procesamiento de datos de código abierto. Destacamos su evolución y las razones para su popularidad en el procesamiento de datos a gran escala, incluyendo su velocidad y capacidad de procesamiento en memoria.

Ads Blocker Image Powered by Code Help Pro

Por favor, permite que se muestren anuncios en nuestro sitio web

Querido lector,

Esperamos que estés disfrutando de nuestro contenido. Entendemos la importancia de la experiencia sin interrupciones, pero también queremos asegurarnos de que podamos seguir brindándote contenido de alta calidad de forma gratuita. Desactivar tu bloqueador de anuncios en nuestro sitio nos ayuda enormemente a lograrlo.

¡Gracias por tu comprensión y apoyo!