Hoje vamos discutir agrupamento (cluster) de textos e Topic modeling com o pacote textmineR. Parte desse texto é uma tradução deste documento

PARTE 1

Agrupamento/Cluster de textos

Uma tarefa comum na mineração de texto é o agrupamento/cluster de documentos. Existem várias maneiras de agrupar documentos. O exemplo abaixo mostra o método usando o TF-IDF e a distância de cosseno.

Vamos carregar um banco dados e fazer uma matriz de termos de documento (document term matrix - DTM) e carregar o pacote para começar.

Tenho que admitir uma derrota aqui: o processamento acabou com a memória do meu computador. Por causa disso, vamos fazer a análise com apenas 150 textos do twitter sobre a Lei Aldir Blanc - LAB. Uma observação importante é que o cluster não funciona com NA nem com NaN. Por causa disso, vamos excluir os tweets sem texto e os pequenos demais (que agrega pouco valor aos modelos).

# semente para reproduzir as simulacoes
set.seed(12345)
# carregar a biblioteca
library(textmineR)
# carregar o banco de dados
banco_lab <- readRDS("C:/Users/Hp/Documents/GitHub/Lei_Aldir_Blanc/tweet/dados/banco_completo_lemma_27_02_2021.RDS")

#----------------------------------------------------------------
# criando uma identificacao unica (id) para cada tweet
#----------------------------------------------------------------
banco_lab$id <-1:dim(banco_lab)[1]

#----------------------------------------------------------------
# selecionando aleatoriamente 150 tweets do banco de dados
#----------------------------------------------------------------
library(dplyr)
banco_lab <- banco_lab %>% select(text,id)%>% sample_n(150)

#----------------------------------------------------------------
# eliminando os tweet pequenos demais
#----------------------------------------------------------------
banco_lab<- banco_lab %>% filter(nchar(text)>10)
dim(banco_lab)
[1] 146   2
#----------------------------------------------------------------
# criando o document term matrix 
#----------------------------------------------------------------
dtm <- CreateDtm(doc_vec = banco_lab$text, # character vector of documents
                 doc_names = banco_lab$status_id, # document names
                 ngram_window = c(1, 2), # minimum and maximum n-gram length
                 stopword_vec = c(stopwords::stopwords("pt"), # stopwords from tm
                                  stopwords::stopwords(source = "smart")), # this is the default value
                 lower = TRUE, # lowercase - this is the default value
                 remove_punctuation = TRUE, # punctuation - this is the default
                 remove_numbers = TRUE, # numbers - this is the default
                 verbose = FALSE, # Turn off status bar for this demo
                 cpus = 2) # default is all available cpus on the system

# construct the matrix of term counts to get the IDF vector
tf_lab <- TermDocFreq(dtm)


kable(head(tf_lab))
term term_freq doc_freq idf
aberta_lei 1 1 4.983607
aberta_passagens 1 1 4.983607
aberta_publico 1 1 4.983607
abertura 1 1 4.983607
abertura_xi 1 1 4.983607
abra 1 1 4.983607

TF-IDF

Primeiro, devemos pensar na contagem de palavras na matriz de termos do documento (document term matrix - DTM). Fazemos isso multiplicando a frequência do termo/palavra por um vetor de frequência inversa do documento (IDF). O resultado é um índice chamado TF-IDF. A fórmula para calcular IDF (segundo componente do TF-IDF) para o i-ésima palavra como

\[\begin{align} IDF_i = ln\big(\frac{N}{\sum_{j = 1}^N C(word_i, doc_j)}\big) \end{align}\]

onde N

é o número de documentos no corpus/twetter.

O TF-IDF (term frequency–inverse document frequency) é uma medida estatística que tem o intuito de indicar a importância de uma palavra de um documento/tweet em relação a uma coleção de documentos. Escrito de outra forma, temos?

\[\begin{equation*} TF(t) = \frac{nº\ de\ vezes\ que\ t\ aparece\ no\ texto}{total\ de\ termos\ no\ texto} \end{equation*}\] \[\begin{equation*} IDF(i) = log_e(\frac{quantidade\ total\ de\ textos}{numero\ de\ textos\ em\ que\ i\ aparece}) \end{equation*}\]

Este método se resume a contar a frequência de uso de palavras e realizar um cálculo que gere uma estimativa de uso/importância da palavra no texto.

Todavia, quando você multiplica uma matriz por um vetor, devemos multiplica o vetor para cada coluna da matriz. Por esse motivo, precisamos da matriz transposta do DTM antes de multiplicar o vetor IDF. Em seguida, fazemos a transposta de volta para a orientação original.

# TF-IDF 
tfidf <- t(dtm[ , tf_lab$term ]) * tf_lab$idf
tfidf <- t(tfidf)

O próximo passo é calcular a similaridade por cosseno e alterá-la para uma distância. A similaridade por cosseno é uma medida da similaridade entre dois vetores num espaço vetorial que avalia o valor do cosseno do ângulo compreendido entre eles.

Similaridade por cosseno

O algoritmo de similaridade por cosseno foi o que utilizei para comparar duas frases. Ele compara as palavras de 2 textos, ignorando a ordem, e cria um vetor com as palavras mais repetidas. Depois devemos aplicar a fórmula. Por exemplo:

  • O projeto foi aprovado na Lei Aldir_Blanc (estou tratando Aldir_Blanc como uma palavra)
  • O projeto foi aprovado na Lei Rouanet

Quebramos em palavras, então teremos o seguinte vetor:

palavras <- c("o", "projeto,", "foi", "aprovado", "na", "lei", "Aldir_Blanc", "Rouanet")

Agora vamos contar quantas palavras do vetor tem em cada frase

exemplo <- data.frame(palavras =c("o", "projeto,", "foi", "aprovado", "na", "lei", "Aldir_Blanc", "Rouanet"),
                           vetor1 = c(1,1,1,1,1,1,1,0),
                           vetor2 = c(1,1,1,1,1,1,0,1))
kable(exemplo)
palavras vetor1 vetor2
o 1 1
projeto, 1 1
foi 1 1
aprovado 1 1
na 1 1
lei 1 1
Aldir_Blanc 1 0
Rouanet 0 1

Agora é só aplicar a fórmula nesses dois vetores binários:

\[\begin{equation*} similaridade\_cosseno = \frac{\sqrt{\sum{vetor1 * vetor2}}}{\sqrt{\sum{vetor1^2}} * \sqrt{\sum{vetor2^2}}} \end{equation*}\]

Nesse exemplo, temos 6/7 = 0,85 de similaridade por cosseno entre os dois textos.

A ideia de similaridade de cossenos é parecida com a distância euclidiana. O motivo de preferirmos usar a similaridade de cossenos no lugar da distância euclidiana é que ele funciona melhor para textos (em textos, os ângulos ficam mais bem preservados que as distâncias).

Como estamos trabalhando com um banco de dados de texto, vamos usar a álgebra linear para fazer isso. O produto escalar de dois vetores de comprimento unitário de valor positivo é a similaridade do cosseno entre os dois vetores.

# similaridade por cosseno
csim <- tfidf / sqrt(rowSums(tfidf * tfidf))

# %*% is matrix multiplication
csim <- csim %*% t(csim)

Para fazer cluster, precisamos de distâncias, não de semelhanças. Por esse motivo, convertemos a similaridade do cosseno em distância do cosseno subtraindo o valor de 1. Isso funciona porque a similaridade do cosseno é limitada entre 0 e 1. assim, converteremos a matriz em um objeto dist para fazer o cluster.

cdist <- as.dist(1 - csim)

A última etapa é o agrupamento/cluster. Existem muitos algoritmos de agrupamento. Se quiser, dá uma olhada no meu texto sobre os tipos de clusters

Aqui vamos utilizar o agrupamento hierárquico aglomerativo usando o método de Ward como regra. Comparado com os outros métodos, o agrupamento hierárquico não exige muito computacionalmente.

No exemplo abaixo, escolho cortar a árvore em 10 clusters. Esta é uma escolha um tanto arbitrária. Muitas vezes prefiro usar o coeficiente de silhueta. Você pode ler sobre este método aqui.

hc <- hclust(cdist, method="ward.D")

clustering <- cutree(hc, 8)

plot(hc, main = "agrupamento hierárquico dos 150 tweers da LAB",
     ylab = "", xlab = "", yaxt = "n")

rect.hclust(hc, 8, border = "red")