Modelagem de Tópicos em Python utilizando o Modelo de Alocação Latente de Dirichlet (LDA)

Letícia Pires
Leti Pires
Published in
10 min readJun 10, 2021

Repost: Blog da Sauter Digital

A modelagem de tópicos é um método que extrai tópicos ocultos de grandes volumes de texto. Ela utiliza as aplicações do processamento de linguagem natural para extrair os tópicos que as pessoas estão mais discutindo, dentre os volumes de texto apresentados.

E o modelo Latent Dirichlet Allocation (LDA) é um algoritmo utilizado para modelagem de tópicos que tem implementações no pacote Gensim do Python.

Esse processo é bem importante para as empresas que querem criar estratégias de monetização e melhoria de serviços, por exemplo, seja analisando avaliações de clientes, feedbacks de usuários, notícias, redes sociais, etc.

Sendo assim, o objetivo deste artigo é de criar um algoritmo automatizado que possa ler documentos e gerar os tópicos mais discutidos.

Para isso, é de extrema importância que os dados tenham qualidade no pré-processamento do texto e na melhor estratégia para encontrar o número de tópicos. Isso pode garantir uma qualidade maior, clareza e significância nos tópicos extraídos.

Esta análise foi realizada com dados extraídos de um repositório do Github nomeado “Manchetes Brasil”, de Paula Dornhofer Paro Costa (Costa, PDP), 2017. Essa base conta com dados de 500 manchetes de jornais brasileiros em datas específicas de dezembro de 2016 a agosto de 2017. Os jornais são: Valor Econômico, O Globo, Folha de S. Paulo e O Estado de S. Paulo.

O link para a base de dados pode ser acessada no link: https://github.com/pdpcosta/manchetesBrasildatabase

Como será estruturado este documento:

  1. Importação de pacotes
  2. Coleta de dados
  3. Limpeza de dados
  4. Modelagem de bigramas e trigramas
  5. Transformação de dados: corpus e dicionário
  6. Aplicação do modelo LDA
  7. Métricas: coerência e complexidade
  8. Encontrando o número ideal de tópicos
  9. Conclusão
  10. Referências

1. IMPORTAÇÃO DE BIBLIOTECAS

Para começar, foi necessário importar algumas bibliotecas importantes, dentre elas o pandas, numpy, matplotlib, nltk, re e gensim:

import re 
import numpy as np
import pandas as pd
from pprint import pprint
import unicodedata
# Importando a library Natural Language Toolkit - NLTK para tratamento de linguagem natural.
import nltk
nltk.download('wordnet')
nltk.download('punkt')
#Importando as stopwords
from nltk.corpus import stopwords
nltk.download('stopwords')
language = 'portuguese'
stopwords = stopwords.words(language)
stopwords = list(set(stopwords))
#Gensim
import gensim
import gensim.corpora as corpora
from gensim.utils import simple_preprocess
from gensim.models import CoherenceModel
#Plotagem
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from wordcloud import WordCloud, STOPWORDS
%matplotlib inline
import logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.ERROR)
import warnings warnings.filterwarnings("ignore",category=DeprecationWarning)

2. COLETA DE DADOS

O conjunto de dados utilizado, como foi mencionado anteriormente, é a base de manchetes brasileiras.

Portanto, foi importado para dentro do Google Colab o arquivo csv, através do código abaixo. O arquivo contém colunas de dia, mês, ano, jornal e as headlines(notícias).

Para visualizar, foi aplicado o método head() que traz os 5 primeiros dados do nosso dataset:

caminho = '/content/manchetesBrasildatabase.csv'
dataframe = pd.read_csv(caminho, quotechar="'", header = None, names = ["Day", "Month", "Year", "Company", "Headline"])
dataframe.head()

Para este artigo, foi realizada a modelagem de tópicos somente para o jornal Folha de São Paulo, portanto, aplicou-se o método loc() para selecionar somente este jornal, atribuindo a uma nova variável, como mostra abaixo:

dataframe_folha = dataframe.loc[dataframe['Company'] == 'Folha'] dataframe_folha

Sendo assim, o dataframe ficou com 127 colunas e 5 colunas.

3. LIMPEZA DOS DADOS

Como é possível visualizar na coluna Headline, os textos apresentam pontuações, acentuações, letras maiúsculas, stopwords… Para aplicação do modelo LDA é necessário que as palavras estejam sem essas distrações.

Além disso, para ser consumido pelo LDA, é necessário fazer uma quebra de cada frase em palavras através da tokeinização.

Portando o seguinte processo foi realizado:

a) Conversão da coluna para lista, remoção de novas linhas e distrações:

# Convertendo para lista data = dataframe_folha.Headline.values.tolist()# Removendo novas linhas 
data = [re.sub('\s+', ' ', sent) for sent in data]
# Removendo distrações
data = [re.sub("\'", "", sent) for sent in data]

b) Substituição de letras maiúsculas por letras minúsculas:

#Aplicando função para deixar somente letras minúsculas.
def to_lowercase(words):
new_words = []
for word in words:
new_word = word.lower()
new_words.append(new_word)
return new_words

c) Remoção de caracteres NON-ASCII:

#Aplicando função para remover os caracteres Non ASCII
def remove_non_ascii(words):
"""Remove non-ASCII characters from list of tokenized words"""
new_words = []
for word in words:
new_word = unicodedata.normalize('NFKD', word).encode('ascii', 'ignore').decode('utf-8', 'ignore')
new_words.append(new_word)
return new_words

d) Remoção de stop words:

As stop words (ou palavras de parada) são palavras que podem ser consideradas irrelevantes para um conjunto de documentos. Ex: e, os, de, para, com, sem, foi.

def remove_stopwords(texts):
return [[word for word in simple_preprocess(str(doc)) if word not in stopwords] for doc in texts]
# Removendo Stop Words
data_words_nostops = remove_stopwords(data_words)

Somente aplicando as stopwords do NLTK não é suficiente para as palavras em português, pois não possui uma base tão boa. Por isso, aplicou-se o método append() para algumas palavras identificadas na análise, adicionando-as à biblioteca de stopwords.

#Adicionando novas stopwords em português 
stopwords = nltk.corpus.stopwords.words('portuguese') stopwords.append('ja')
stopwords.append('viu')
stopwords.append('vai')
stopwords.append('ne')
stopwords.append('ai')
stopwords.append('ta')
stopwords.append('gente')
stopwords.append('nao')
stopwords.append('aqui')
stopwords.append('tambem')
stopwords.append('vc')
stopwords.append('voce')
stopwords.append('entao')
stopwords.append('ate')
stopwords.append('agora')
stopwords.append('ser')
stopwords.append('sempre')
stopwords.append('ter')
stopwords.append('so')
stopwords.append('porque')
stopwords.append('sobre')
stopwords.append('ainda')
stopwords.append('la')
stopwords.append('tudo')
stopwords.append('ninguem')
stopwords.append('de')

e) Remoção de pontuação e tokeinização através do simple_preprocess do Geisim:

#Removendo pontuação e fazendo a tokeinização (para conseguir aplicar o modelo LDA)def sent_to_words(sentences):
for sentence in sentences:
yield(gensim.utils.simple_preprocess(str(sentence), deacc=True)) # deacc=True removes punctuations
data_words = list(sent_to_words(data))

4. MODELAGEM DE BIGRAMAS E TRIGRAMAS

Os bigramas são duas palavras que ocorrem juntas num mesmo documentos e os trigramas são 3 palavras que ocorrem frequentemente juntas. Como exemplo temos a palavra ‘São Paulo’.

O Gensim apresenta um modelo Phrases que implementa esses bigramas e trigramas. Pra isso, é preciso passar dois argumentos importantes: o min_count e o threshold.

“Quanta mais altos os valores desses parâmetros, mais difícil será para as palavras serem combinadas com os bigramas.” (*PRABHAKARAN, 2020)

def make_bigrams(texts):
return [bigram_mod[doc] for doc in texts]
def make_trigrams(texts):
return [trigram_mod[bigram_mod[doc]] for doc in texts]
# Formando Bigrams
data_words_bigrams = make_bigrams(data_words_nostops) data_words_bigrams

5. TRANSFORMAÇÃO DE DADOS: CORPUS E DICIONÁRIO

Para dar entrada no modelo LDA é necessário ter um dicionário (id2world) e o corpus. O código abaixo realiza essa passagem:

# Criando dicionário id2word = corpora.Dictionary(data_words_bigrams)  # Criando corpus 
texts = data_words_bigrams
# Frequencia do documento do termo
corpus = [id2word.doc2bow(text) for text in texts]
# visualizando
print(corpus[:1])
#RESULTADO [[(0, 1), (1, 1), (2, 1), (3, 1), (4, 1), (5, 1)]]

6. APLICAÇÃO DO MODELO LDA

Com o corpus e o dicionário, é possível criar o modelo LDA. Além disso, é necessário incluir o número de topicos para treinar o modelo.

Outros parâmetros passados são:

  • alpha e eta, que segundo a documentação do Gensim tem um padrão;
  • chunksize que representa o numero de documentos que serão usados em um bloco de treinamento;
  • update every mostra a frequência em que os parâmetros são atualizados;
  • passes representa o total de passes para treinamento.
# Construindo LDA Model
lda_model = gensim.models.ldamodel.LdaModel(corpus=corpus,
id2word=id2word,
num_topics=10,
random_state=100,
update_every=1,
chunksize=100,
passes=10,
alpha='auto',
per_word_topics=True)
# Imprindo as palavras chaves nos 10 tópicos pprint(lda_model.print_topics())
doc_lda = lda_model[corpus]

Neste modelo utilizou-se 10 tópicos diferentes, no qual cada tópico representa uma combinação de palavras-chave e cada uma possui uma contribuição com um nível de importância diferente (peso).

#RESULTADO:
[(0,
'0.052*"lava" + 0.052*"jato" + 0.026*"ameaca" + 0.026*"economia" + ' '0.026*"novo" + 0.026*"pf" + 0.022*"manteve" + 0.022*"repasse" + ' '0.022*"ilicito" + 0.022*"retomada"'),
(1,
'0.042*"lula" + 0.031*"trump" + 0.031*"diz" + 0.028*"menos" + ' '0.021*"politica" + 0.021*"mesquita" + 0.021*"ministros" + 0.021*"maior" + ' '0.018*"deflagra" + 0.018*"decreto"'),
(2,
'0.042*"pais" + 0.023*"crise" + 0.023*"presidente" + 0.020*"rio" + ' '0.020*"dias" + 0.020*"afirma" + 0.020*"cada" + 0.017*"colega" + ' '0.017*"elmar" + 0.017*"nascimento"'),
(3,
'0.027*"ataque" + 0.026*"deve" + 0.023*"deixa" + 0.023*"mortos" + ' '0.023*"helio" + 0.023*"schwartsman" + 0.023*"aposentadoria" + 0.023*"vira" ' '+ 0.023*"ato" + 0.023*"venezuela"'),
(4,
'0.058*"contra" + 0.044*"brasil" + 0.034*"coreia" + 0.034*"cria" + ' '0.034*"plano" + 0.034*"fuga" + 0.034*"embaixada" + 0.027*"maduro" + ' '0.024*"reformas" + 0.021*"temer"'),
(5,
'0.038*"stf" + 0.025*"oab" + 0.025*"pressiona" + 0.025*"convocar" + ' '0.025*"auxiliares" + 0.025*"apuracao" + 0.025*"acelerar" + 0.025*"juizes" + ' '0.010*"gera" + 0.005*"sabe"'),
(6,
'0.037*"temer" + 0.029*"admite" + 0.029*"plebiscito" + 0.029*"barbosa" + ' '0.029*"celso" + 0.025*"agradar" + 0.025*"trabalhadores" + 0.025*"tenta" + ' '0.025*"pacote" + 0.025*"empresarios"'),
(7,
'0.054*"odebrecht" + 0.020*"pt" + 0.020*"quer" + 0.020*"vitoria" + ' '0.020*"mulher" + 0.020*"corte" + 0.017*"refens" + 0.017*"psdb" + ' '0.017*"haddad" + 0.017*"atraso"'),
(8,
'0.048*"ano" + 0.032*"doria" + 0.028*"sp" + 0.027*"bi" + 0.024*"eike" + ' '0.024*"cuba" + 0.021*"prioriza" + 0.021*"zeladoria" + 0.021*"centro" + ' '0.021*"aposentado"'),
(9,
'0.029*"anos" + 0.025*"esquerda" + 0.025*"recorde" + 0.025*"deficit" + ' '0.022*"deve" + 0.022*"passado" + 0.022*"govwerno" + 0.022*"idade" + ' '0.022*"minima" + 0.022*"subir"')]

Podemos interpretar os tópicos da seguinte forma: o tópico 0 apresenta o seguinte:

'0.052*"lava" + 0.052*"jato" + 0.026*"ameaca" + 0.026*"economia" + '   '0.026*"novo" + 0.026*"pf" + 0.022*"manteve" + 0.022*"repasse" + '   '0.022*"ilicito" + 0.022*"retomada'

Ou seja, as 10 principais palavras chave que contribuem para o tópico são: lava, jato, ameaca, economia… e o peso de ‘lava’ é 0.052.

Dessa forma, olhando para essas palavras, é possível identificar qual o tópico macro seria? Entendendo mais do contexto, podemos dizer que o tópico seria ‘política’.

Da mesma forma podemos analisar as outras palavras chave e inferir o tópico sobre elas.

7. MÉTRICAS: COERÊNCIA E PERPLEXIDADE

A coerência e perplexidade fornecem uma medida para julgar se o modelo de tópico pode ser bom ou não. A coerência do modelo segundo referências externas, é o que melhor tem fornecido resultados úteis.

# Calculando a perplexidade print('\nPerplexity: ', lda_model.log_perplexity(corpus))  # a measure of how good the model is. lower the better.# Calculando o score de coerência 
coherence_model_lda = CoherenceModel(model=lda_model, texts=data_words_bigrams, dictionary=id2word, coherence='c_v')
coherence_lda = coherence_model_lda.get_coherence() print('\nCoherence Score: ', coherence_lda)

8. ENCONTRANDO O NÚMERO IDEAL DE TÓPICOS

A forma utilizada para entender qual a quantidade de tópicos pode ser ideal para o modelo é escolhendo o maior valor de coerência fornecido.

“A escolha de um ‘k’ que marca o fim de um rápido crescimento da coerência do tópico geralmente oferece tópicos significativos e interpretáveis.” (*PRABHAKARAN, 2020)

A função abaixo treina vários modelos LDA e fornece as pontuações de coerência.

# Função para determinar a melhor quantidade de tópicos para a modelagem
def compute_coherence_values(dictionary, corpus, texts, limit, start=3, step=3):
""" Compute c_v coherence para vários números de tópicos
Parâmetros passados:
----------
dicionário : Gensim dicionário
corpus : Gensim corpus
texto : Lista com os textos de entrada
limite : número máximo de tópicos
Retorno:
-------
model_list : Lista de modelos de tópicos de LDA
coherence_values : Valor de coerência correspondente ao modelo LDA com o respectivo número de tópicos.
"""
coherence_values = []
model_list = []
for num_topics in range(start, limit, step):
model =gensim.models.ldamodel.LdaModel(corpus=corpus, num_topics=num_topics, id2word=id2word)
model_list.append(model)
coherencemodel = CoherenceModel(model=model, texts=texts, dictionary=dictionary, coherence='c_v')
coherence_values.append(coherencemodel.get_coherence())

return model_list, coherence_values
#Pode demorar pra executar
model_list, coherence_values = compute_coherence_values(dictionary=id2word, corpus=corpus, texts=data_words_bigrams, start=3, limit=40, step=6)
# Mostrando gráfico
limit=40; start=3; step=6;
x = range(start, limit, step)
plt.plot(x, coherence_values)
plt.xlabel("Num Topics")
plt.ylabel("Coherence score")
plt.legend(("coherence_values"), loc='best')
plt.show()
# Lista dos valores de coerência, para melhor identificar o ponto de inflexão do gráfico 
for m, cv in zip(x, coherence_values):
print("A quantidade de tópicos =", m, " tem um valor de coerência de ", round(cv, 4))
#RESULTADO
A quantidade de tópicos = 3 tem um valor de coerência de 0.683
A quantidade de tópicos = 9 tem um valor de coerência de 0.6061
A quantidade de tópicos = 15 tem um valor de coerência de 0.4835
A quantidade de tópicos = 21 tem um valor de coerência de 0.4506
A quantidade de tópicos = 27 tem um valor de coerência de 0.4127
A quantidade de tópicos = 33 tem um valor de coerência de 0.4037
A quantidade de tópicos = 39 tem um valor de coerência de 0.4201

Pelo gráfico podemos entender que quanto menor o número de tópicos, maior é o CV.

Portanto, para as próximas etapas vou utilizar num_topics igual a 3, ou a posição 0 da lista acima.

# Selecionando o modelo  e imprimindo os tópicos. 
optimal_model = model_list[0]
model_topics = optimal_model.show_topics(formatted=False)
pprint(optimal_model.print_topics(num_words=10))
#RESULTADO
[(0,
'0.011*"presidente" + 0.007*"ataque" + 0.007*"deixa" +
0.007*"temer" + ' '0.007*"diz" + 0.006*"pais" + 0.005*"ano" +
0.005*"reformas" + 0.005*"apoio" ' '+ 0.005*"esquerda"'),
(1,
'0.010*"deve" + 0.010*"governo" + 0.008*"trump" + 0.007*"apos" + ' '0.006*"maduro" + 0.006*"pais" + 0.006*"contra" + 0.006*"camara" + ' '0.005*"stf" + 0.005*"brasil"'),
(2,
'0.013*"diz" + 0.011*"temer" + 0.009*"crise" + 0.009*"menos" + 0.008*"doria" ' '+ 0.006*"preciso" + 0.005*"odebrecht" + 0.005*"lula" + 0.005*"sociedade" + ' '0.005*"poder"')]

9. ENCONTRANDO PALAVRAS CHAVE EM CADA DOCUMENTO

A modelagem de tópicos também permite determinar de qual tópico o documento trata.

A função abaixo encontra esses tópicos e mostra em um dataframe apresentável:

def format_topics_sentences(ldamodel=lda_model, corpus=corpus, texts=data):
# Saída inicial
sent_topics_df = pd.DataFrame()
# Obtém o tópico principal em cada documento
for i, row in enumerate(ldamodel[corpus]):
row = sorted(row, key=lambda x: (x[1]), reverse=True)
# Obtém o tópico dominante, contribuição em percentual e palavras-chave para cada documento
for j, (topic_num, prop_topic) in enumerate(row):
if j == 0: # => tópico dominante
wp = ldamodel.show_topic(topic_num)
topic_keywords = ", ".join([word for word, prop in wp])
sent_topics_df = sent_topics_df.append(pd.Series([int(topic_num), round(prop_topic,4), topic_keywords]), ignore_index=True)
else:
break
sent_topics_df.columns = ['Tópico dominante', 'Percentual de Contribuição', 'Palavras Chave']
# Adiciona o texto original no final da saída
contents = pd.Series(texts)
sent_topics_df = pd.concat([sent_topics_df, contents], axis=1)
return(sent_topics_df)
df_topic_sents_keywords = format_topics_sentences(ldamodel=optimal_model, corpus=corpus, texts=data) # Formatando
df_dominant_topic = df_topic_sents_keywords.reset_index()
df_dominant_topic.columns = ['Número do documento', 'Tópico dominante', 'Perc. de Contribuição do Tópico', 'Palavras Chave', 'Transcription']
# Mostre
df_dominant_topic.head(10)

10. CONCLUSÃO

Por fim, gerou-se uma Wordcloud pra visualizar melhor os tópicos encontrados, através do modelo ótimo. A partir disso, é possível visualizar a relevância de cada palavra dentro de cada tópico.

# Criando wordclouds
cols = [color for name, color in mcolors.XKCD_COLORS.items()]
cloud = WordCloud(stopwords=stopwords,
background_color='white',
width=2500,
height=1800,
max_words=20,
colormap='tab10',
color_func=lambda *args,**kwargs: cols[i],
prefer_horizontal=1.0)
topics = optimal_model.show_topics(formatted=False)
fig, axes = plt.subplots(1, 3, figsize=(10,10), sharex=True, sharey=True)
for i, ax in enumerate(axes.flatten()):
fig.add_subplot(ax)
topic_words = dict(topics[i][1])
cloud.generate_from_frequencies(topic_words, max_font_size=600)
plt.gca().imshow(cloud)
plt.gca().set_title('Tópico ' + str(i), fontdict=dict(size=16))
plt.gca().axis('off')
plt.subplots_adjust(wspace=0, hspace=0)
plt.axis('off')
plt.margins(x=0, y=0)
plt.tight_layout()
plt.show()

11. REFERÊNCIAS

--

--

Letícia Pires
Leti Pires

Jr. Data Scientist at Sauter 💜 & Civil Engineer . Content creator in @letispires. Fond of Python, Machine Learning and AI.