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

Por: Letícia Pires

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, Data Analyst, na Sauter

Leia também

Melhores Práticas de Segurança em Nuvem

Melhores Práticas de Segurança em Nuvem

por Beatriz Ribeiro, analista de marketing na Sauter Hoje, para os negócios, a computação em nuvem é mais do que apenas mais uma alternativa. Ela se tornou um meio eficaz de reduzir custos, garantir disponibilidade constante e diminuir o tempo de inatividade. No...

read more

Vamos nos conectar

Nos envie um e-mail e nós retornaremos nas próximas 24 horas.

15 + 5 =