12 Extração de palavras-chave - Key term ou keyword extraction

Dado um ou mais documentos, dá se o nome de keyword - ou key phrases, key terms, key segments - extraction (KE), ou ainda keyword detection e keyword analysis à extração automatizada de termos que descrevem estes documentos, podendo ser extraídas palavras, frases ou segmentos. Além de extração de palavras-chave (keywords), a KE pode ser utilizada, por exemplo, para fazer um resumo automático de um texto. Pode ser usado para identificar palavras chave relevantes a um determinado assunto em uma massa de dados, como jornais/revistas científicas.

“Extração de palavras-chave é definida como a tarefa que identifica automaticamente um conjunto de termos que melhor descrevem o assunto do documento”
“Keyword extraction (KE) is defined as the task that automatically identifies a set of the terms that best describe the subject of document” (BELIGA, 2014)

Há quem use o termo “keyphrase” sob o argumento de que as palavras chave são compostas de uma, duas ou mais palavras. Há duas abordagens no problema da geração automática de termos: (1) atribuição de palavras chave (keyword assignment), e (2) keyword extraction. Na atribuição de palavras chave as palavras chave são definidos previamente pelo pesquisador através de um vocabulário controlado de termos, e tais termos chave não precisam estar presentes no documento. Os documentos podem então ser classificados conforme estas categorias. Já na extração de palavras chave (KE), os termos são escolhidos pelo processo de automatização e estão previamente no texto.

12.1 Atribuição de palavra chave Keyword assignment

Busca-se com a atribuição de palavra chave selecionar frases ou termos presentes em um vocabulário pré-definido pelo pesquisador.

12.2 Key term ou keyword extraction

Como dito, extração de termos ou palavras-chave consiste na extração automatizada de termos contidos no texto, sem o uso de vocabulário controlado externo. Segundo Beliga (2014) como exemplos de key word extraction temos:

  • métodos estatísticos não supervisionados, como TF-IDF, KP-Miner, RAKE (Rapid Automatic Keyword Extraction).
  • métodos baseados em grafos, como TextRank, SingleRank, ExpandRank TopicRank, PositionRank, TopologicalPageRank e MultipartitieRank.
  • método supervisionado, como o KEA.

Além destes, pode-se destacar ainda os seguintes algoritmos de KE: Os ngrams podem ser considerados uma forma de extrair palavras chave, bem como Parts-of-Speech tagging, ou POS (por exemplo, pegando os substantivos mais frequentes), colocação e Coocorrências, PAT tree (Patricia Tree), YAKE! (Yet Another Keyword Extractor), Selectivity-Based Keyword Extraction, Yum!, GenEx (o nome é uma junção entre Genitor, um algoritmo genético e Extractor, um algoritmo de extração de palavras chave parametrizado), KeyBERT.

Os algoritmos GenEx e Kea estabeleceram a fundação dos métodos de extração de termos que vieram depois.

Dicas

O artigo que apresentou o GenEx:

  • Chamamos de métodos não supervisionados aqueles que não utilizam fontes externas, utilizam somente os textos, documentos, etc.
  • Chamamos de métodos supervisionados aqueles que utilizam alguma fonte externa - como modelos pré-treinados ou dicionários - para extração de palavras chave.

12.2.1 TF-IDF: Term-Frequency Inverse Document Frequency

A “frequência do termo–inverso da frequência nos documentos”, do inglês “Term-Frequency Inverse Document Frequency”, ou “TF-IDF” é utilizado para medir a relevância de palavras em uma série de documentos. Para funcionar, requer que existam vários documentos, ou textos, ou capítulos, etc. Neste algoritmo, as palavras que aparecem em todos ou em muitos documentos - como as stopwords - serão “penalizadas” e terão pontuação baixa. Agora, se uma palavra aparece bastante em um documento, mas não em outros, terá pontuação alta, e isto pode indicar que seja relevante, significativa para entender a peculiaridade daquele documento/texto. TF-IDF é útil num processo chamado de keyword extraction” ou “extração de palavras chave”.

Vamos para um exemplo:

Documento 1: “Eu quero abacaxi”

Documento 2: “Eu? Eu quero banana”

Frequência de termos, ou TF, representa a proporção que uma palavra tem no documento em questão. Esta frequência pode ser apresentada no formato de matriz.

Frequência Doc 1 Doc2
Eu 1 2
quero 1 1
abacaxi 1 0
banana 0 1
total palavras 3 4

12.2.1.1 TF: Frequência de termos (Term Frequency)

O TF de um termo que ocorre em um documento é calculado da seguinte maneira:

\(tf(t,d)\) : contagem de t(termo) em d(documento) / número de palavras no documento

O documento 1 possui 3 palavras, o documento 2 possui 4 palavras, portanto, a frequência de termos (TF) fica assim:

TF Doc 1 Doc2
Eu 1 / 3 = 0,33333 2 / 4 = 0,5
quero 1 / 3 = 0,33333 1 / 4 = 0,25
abacaxi 1 / 3 = 0,33333 0
banana 0 1 / 4 = 0,25
Total 1 1

No Documento 1, temos 3 palavras no total, cada uma, por ser única no documento, possui TF de 1/3, ou 0,33333. O Documento 2 possui 4 palavras no total. “quero” e “banana” possui um TF de 1/4 cada, ou 0,25, enquanto “eu”, que apareceu duas vezes, possui TF de 2/4 ou 0,5. TF me diz o quão frequente é uma palavra/termo em um documento. Isto pode ser feito em números absolutos bem como em termos proporcionais (bom olhar as documentações dos pacotes para entender qual o padrão utilizado)

12.2.1.2 IDF: Inverse Document Frequency

IDF, inverse document frequency, mostra o peso de um termo em relação à coleção total de documentos/textos, dando um valor baixo para termos frequentes em todos os documentos e que por isso são pouco informativos sobre as peculiaridades daquele documento, bem como privilegia termos frequentes em poucos documentos.

Assim, no nosso exemplo, as palavras “eu” e “quero” estão presentes em dois documentos de um total de dois documentos, tendo o IDF de 0. Já “abacaxi” e “banana”, termos que aparecem uma vez e somente em um documento cada, possuem IDF de 0,30102.

O cálculo é feito da seguinte forma:

Número de documentos no corpus (no caso acima, dois), dividido pelo número de documentos onde o termo aparece. Se o termo aparece uma vez somente ou 50 vezes em um documento, em ambos os casos será computado como um. O resultado disto é posto num logaritmo.

IDF
Eu Log(2/2) = 0
quero Log(2/2) = 0
abacaxi Log(2/1) = 0,30102
banana Log(2/1) = 0,30102

Com o IDF sabemos quais termos ocorrem em vários documentos e os que ocorrem em poucos. Para saber o peso de cada termo em cada documento, usamos então o TF-IDF.

12.2.1.3 Calculando TF-IDF

TF-IDF é a multiplicação dos dois termos, TF * IDF. Ao multiplicar TF por IDF, obtemos o score da palavra no documento.

TF Doc 1 TF Doc2 IDF TF-IDF Doc1 TF-IDF Doc2
Eu 1 / 3 = 0,33333 2 / 4 = 0,5 Log(2/2) = 0 0,33333 * 0 = 0 0,5 * 0 = 0
quero 1 / 3 = 0,33333 1 / 4 = 0,25 Log(2/2) = 0 0,33333 * 0 = 0 0,5 * 0 = 0
abacaxi 1 / 3 = 0,33333 0 Log(2/1) = 0,301 0,3 * 0,3 = 0,1003 0 * 0,3 = 0
banana 0 1 / 4 = 0,25 Log(2/1) = 0,301 0 * 0,3 = 0 0,25 * 0,3 = 0,0752

Matemática

Esta é a fórmula do TF-IDF, e nos retorna o índice TF-IDF para cada palavra em cada documento.

\(W_{ij} = tf_{i,j} \times \log(\frac{N}{df_i})\)

Destrinchando a fórmula:

Fórmula Descrição
\(W_{ij}\) um termo \(i\) num documento \(j\), para o qual vamos calcular o TF-IDF
\(tf_{i,j}\) frequência do termo \(i\), no documento \(j\)
\(df_{ij}\) Número de documentos que contenham o termo \(i\). Pouco importa se aparece apenas uma vez ou se 500 vezes num mesmo documento, seu valor em cada documento, se presente, é 1
\(N\) Número total de documentos
\(df_i\) frequência de documentos que contenham o termo \(i\)

Se o resultado encontrado se aproximar de “0”, então a palavra se encontra presente em vários documentos. Caso contrário, quanto mais se aproxima de “1”, mais rara é esta palavra em outros documentos e mais concentrada em poucos documentos. Vale ressaltar que o cálculo TF-IDF pode ser feito tanto com a frequência absoluta ou como com a relativa.

Vimos exemplo de TF-IDF com apenas dois “documentos”. Vamos usar mais documentos para entender melhor o TF-IDF. Usamos um pacote de R nos bastidores para gerar a tabela à seguir, mas veremos o código que o gerou mais à frente.

doc1 <- "Eu quero abacaxi!"
doc2 <- "Eu quero açaí!"
doc3 <- "Eu quero manga ou açaí! Eu quero manga! Manga!"

O exemplo acima possui diferentes configurações de palavras para observarmos o TF-IDF:

  • “eu” e “quero” em todos os docs
  • “abacaxi”, “manga” e “ou” que ocorrem uma vez.
  • “açaí” que ocorre uma vez em dois documentos diferentes.
  • “manga” que aparece várias vezes em um documento somente.
## Document-feature matrix of: 3 documents, 6 features (38.89% sparse) and 0 docvars.
##        features
## docs    eu quero   abacaxi       açaí     manga         ou
##   text1  0     0 0.1590404 0          0         0         
##   text2  0     0 0         0.05869709 0         0         
##   text3  0     0 0         0.01956570 0.1590404 0.05301347
  • “eu” e “quero” ocorrem em todos os docs e possuem TF-IDF de 0 em todos os casos.
  • “abacaxi” aparece somente na primeira frase e tem TF-IDF de 0.1590404
  • “açaí” ocorre uma vez em dois documentos possui TF-IDF em um doc com menos palavras no total.
  • “ou” e “manga” só aparecem na frase 3, e manga aparece 3 vezes e tem TF-IDF maior que a palavra “ou”, que só aparece uma vez.
  • No doc3, “ou” (que só aparece uma vez em um doc) possui TF-IDF maior que “açaí”, que aparece em mais de um doc.

Podemos realizar o TF-IDF no R calculando manualmente, como neste exemplo em video ou neste tutorial, ou podemos usar alguns dos vários pacotes que tem já implementadas a função, como o Tidytext, Quanteda e TM, que veremos a seguir. Vale atentar que cálculos feitos com diferentes pacotes podem não bater entre si. Se for este o caso, atente para se usam frequência absoluta ou relativa, e qual a base do Logaritmo utilizado (se de base 2 ou 10).

Em um exemplo real de uso de TF-IDF, este tutorial usou TF-IDF entre diferentes livros do Harry Potter:

Fonte: Text Mining: Term vs. Document Frequency do AFIT Data Science Lab R Programming Guide

12.2.1.4 TF-IDF no R: Tidyverse

Podemos realizar o TF-IDF no R com o tidytext com a função tidytext::bind_tf_idf.

doc1 <- "Eu quero abacaxi"
doc2 <- "Eu? Eu quero banana"
# criando o data frame, onde cada linha é um documento.
df <- data.frame("texto" = c(doc1,doc2), 
                 # ID de "identificação"
                 "ID" = c(1,2), 
                 stringsAsFactors = F)

df %>% 
  # quebrando o texto em tokens
     tidytext::unnest_tokens(output = 'word', token = 'words', 
                             # input =  nome da coluna do dataframe
                             input = texto) %>%
              # contando os termos
              dplyr::count(ID, word, sort = TRUE) %>%
              # TF-IDF
            tidytext::bind_tf_idf(word, ID, n)
##   ID    word n        tf       idf    tf_idf
## 1  2      eu 2 0.5000000 0.0000000 0.0000000
## 2  1 abacaxi 1 0.3333333 0.6931472 0.2310491
## 3  1      eu 1 0.3333333 0.0000000 0.0000000
## 4  1   quero 1 0.3333333 0.0000000 0.0000000
## 5  2  banana 1 0.2500000 0.6931472 0.1732868
## 6  2   quero 1 0.2500000 0.0000000 0.0000000

Em bind_tf_idf() sendo: word a coluna contendo termos, ID a coluna contendo os IDs dos docs, n a contagem de palavras produzido por count().

Dicas

Exemplo/tutorial de TF-IDF com o tidyverse de Julia Silge e David Robinson.

12.2.1.5 TF-IDF no R: Quanteda

TF-IDF com o pacote Quanteda é usado com a função quanteda::dfm_tfidf

doc1 <- "Eu quero Elias"
doc2 <- "Eu? Eu quero Durkheim"

# criando um vetor com documentos para transformar em um corpus no quanteda
meuvetor <- c(doc1,doc2)
# criando um objeto tipo corpus a partir do vetor
meucorpus <- quanteda::corpus(meuvetor)

# criando uma matriz de frequência
meudfm <- meucorpus %>% 
    # quebrando em tokens
    quanteda::tokens(
    # removendo a pontuação
     remove_punct = TRUE) %>%
    # transformando em um document frame matrix
    quanteda::dfm()

meudfm
## Document-feature matrix of: 2 documents, 4 features (25.00% sparse) and 0 docvars.
##        features
## docs    eu quero elias durkheim
##   text1  1     1     1        0
##   text2  2     1     0        1

# gerando o tf-idf (frequencia absoluta)
quanteda::dfm_tfidf(meudfm)
## Document-feature matrix of: 2 documents, 4 features (25.00% sparse) and 0 docvars.
##        features
## docs    eu quero   elias durkheim
##   text1  0     0 0.30103  0      
##   text2  0     0 0        0.30103

# TF-IDF usando frequência relativa, proporcional
quanteda::dfm_tfidf(meudfm, scheme_tf = "prop")
## Document-feature matrix of: 2 documents, 4 features (25.00% sparse) and 0 docvars.
##        features
## docs    eu quero     elias  durkheim
##   text1  0     0 0.1003433 0        
##   text2  0     0 0         0.0752575

quanteda::dfm_tfidf(x, scheme_tf = “count”, scheme_df = “inverse”, base = 2)

Parâmetro Descrição
x objeto de entrada, devendo ser um document-feature matrix
scheme_tf esquema para dfm_weight(); sendo o padrão “count”. Para usar a frequência relativa, usa-se o “prop”
scheme_df esquema para docfreq(); sendo o padrão “inverse”.
base A base para logaritmo no dfm_weight() e docfreq(), sendo 10 o valor padrão. Outro valor comum é 2

12.2.1.6 TF-IDF no R: TM

Vamos usar agora o pacote TM.

doc1 <- "Eu quero Durkheim"
doc2 <- "Elias! Eu quero Elias"

vetor_vetores <- c(doc1,doc2)

# criando o objeto tipo corpus para o TM
meu_corpus <- tm::Corpus(tm::VectorSource(vetor_vetores))
# observando a estrutura do objeto criado, que é uma lista
str(meu_corpus)
## Classes 'SimpleCorpus', 'Corpus'  hidden list of 3
##  $ content: chr [1:2] "Eu quero Durkheim" "Elias! Eu quero Elias"
##  $ meta   :List of 1
##   ..$ language: chr "en"
##   ..- attr(*, "class")= chr "CorpusMeta"
##  $ dmeta  :'data.frame': 2 obs. of  0 variables

Vamos a um pré-processamento do pacote tm com a função tm_map() e tm::removePunctuation.

meu_corpus2 <- 
  # passando tudo para minúsculo
  tm::tm_map(meu_corpus, tolower) %>%
  # removendo pontuações
  tm::tm_map(., tm::removePunctuation) 
## Warning in tm_map.SimpleCorpus(meu_corpus, tolower): transformation drops
## documents
## Warning in tm_map.SimpleCorpus(., tm::removePunctuation): transformation drops
## documents
meu_corpus2
## <<SimpleCorpus>>
## Metadata:  corpus specific: 1, document level (indexed): 0
## Content:  documents: 2

A matriz de termo por documento (document-term-matrix) computa quantas vezes um termo aparece por documento, que no nosso caso foi uma frase simples. “Durkheim” aparece uma vez no documento 1, “quero” aparece uma vez em cada documento e “Elias” aparece duas vezes no documento 2.

Opção 1, mais simples

dtm.tfidf <- tm::DocumentTermMatrix(meu_corpus2,
           control = list(weighting = tm::weightTfIdf))
## Warning in TermDocumentMatrix.SimpleCorpus(x, control): custom functions are
## ignored
# vendo a estrutura
dtm.tfidf
## <<DocumentTermMatrix (documents: 2, terms: 3)>>
## Non-/sparse entries: 2/4
## Sparsity           : 67%
## Maximal term length: 8
## Weighting          : term frequency - inverse document frequency (normalized) (tf-idf)
# Para visualizar, transformamos nosso objeto em matriz
as.matrix(dtm.tfidf)
##     Terms
## Docs durkheim quero     elias
##    1      0.5     0 0.0000000
##    2      0.0     0 0.6666667

Opção 2: com normalização e retirada de stopwords

doc1 <- "Eu quero Durkheim"
doc2 <- "Elias! Eu quero Elias"

vetor.docs <- c(doc1,doc2)

# criando o objeto tipo corpus para o TM
# como nossa fonte são vetores, usamos VectorSource
meu_corpus <- tm::Corpus(tm::VectorSource(vetor.docs))

tm::DocumentTermMatrix(meu_corpus,
# para concatenar várias transformacoes, vamos usar function
           control = list(weighting = function(x) 
             tm::weightTfIdf(x, normalize = T),
             removePunctuation = TRUE,
             stopwords = TRUE)) %>% as.matrix
## Warning in TermDocumentMatrix.SimpleCorpus(x, control): custom functions are
## ignored
##     Terms
## Docs durkheim quero     elias
##    1      0.5     0 0.0000000
##    2      0.0     0 0.6666667

Dicas TF-IDF

Tutorial

Vídeos:

12.3 Colocação e Coocorrência

Uma das vantagens de tokenizers, em relação à grande maioria dos tokenizadores - exceção ao Quanteda - é a possibilidade de criar ngrams de diferentes tamanhos, ao mesmo tempo, alterando apenas um parâmetro, sem demandar novas linhas de código. Esta facilidade se encontra também no pacote Quanteda (ambos pacotes possuem criadores em comum). Isto é chamado de “multi-word units” {#multiword_units}, ou “colocação” (collocation”) {#colocation} na linguística. São tokens que aparecem juntos muito frequentemente, como em nomes próprios ou junções frequentes entre substantivo e Por exemplo, “Minas Gerais”, “Machine Learning”, “ódio mortal”, “cena do crime”. É útil considerá-los como uma palavra só; O Quanteda, além de também realizar a tokenização “multi words”, oferece também as funções textstat_collocations() que nos retorna uma tabela com cáculo estatístico identificando colocações e a função tokens_compound() que adiciona um underscore entre as palavras da colocação, de modo que formem um único vocábulo, por exemplo, “machine_learning”, “New_York”.

txt.raizesBr <- 'A democracia no Brasil foi sempre um lamentável mal-entendido. Uma aristocracia rural e semifeudal importou-a e tratou de acomodá-la, onde fosse possível, aos seus direitos ou privilégios, os mesmos privilégios que tinham sido, no Velho Mundo, o alvo da luta da burguesia contra os aristocratas. E assim puderam incorporar à situação tradicional, ao menos como fachada ou decoração externa, alguns lemas que pareciam os mais acertados para a época e eram exaltados nos livros e discursos. É curioso notar-se que os movimentos aparentemente reformadores, no Brasil, partiram quase sempre de cima para baixo: foram de inspiração intelectual, se assim se pode dizer, tanto quanto sentimental. Nossa independência, as conquistas liberais que fizemos durante o decurso de nossa evolução política vieram quase de surpresa; a grande massa do povo recebeu-as com displicência, ou hostilidade. Não emanavam de uma predisposição espiritual e emotiva particular, de uma concepção da vida bem definida e específica, que tivesse chegado à maturidade plena. Os campeões das novas idéias esqueceram-se, com freqüência, de que as formas de vida nem sempre são expressões do arbítrio pessoal, não se "fazem" ou "desfazem" por decreto. A célebre carta de Aristides Lobo sobre o 15 de Novembro é documento flagrante do imprevisto que representou para nós, a despeito de toda a propaganda, de toda a popularidade entre os moços das academias, a realização da idéia republicana. "Por ora", dizia o célebre paredro do novo regime, "por ora a cor do governo é puramente militar e deverá ser assim. O fato foi deles, deles só, porque a colaboração de elemento civil foi quase nula. O povo assistiu àquilo bestializado, atônito, surpreso, sem conhecer o que significava.'

12.3.1 Colocação com o Quanteda

Vejamos um exemplo:

corp <- data_corpus_inaugural[1:2]
head(cols <- quanteda.textstats::textstat_collocations(corp, size = 2, min_count = 2), 10)
##    collocation count count_nested length   lambda        z
## 1    have been     5            0      2 5.704259 7.354588
## 2     has been     3            0      2 5.565217 6.409333
## 3       of the    24            0      2 1.673501 6.382475
## 4       i have     5            0      2 3.743580 6.268303
## 5      which i     6            0      2 3.172217 6.135144
## 6      will be     4            0      2 3.868500 5.930143
## 7    less than     2            0      2 6.279494 5.529680
## 8  public good     2            0      2 6.279494 5.529680
## 9     you will     2            0      2 4.917893 5.431752
## 10      may be     3            0      2 4.190711 5.328038

head(cols <- quanteda.textstats::textstat_collocations(txt.raizesBr, size = 2, min_count = 2), 10)
##   collocation count count_nested length   lambda        z
## 1   no brasil     2            0      2 6.626718 3.781401
## 2     por ora     2            0      2 6.626718 3.781401
## 3      toda a     2            0      2 5.770551 3.518257
## 4     de toda     2            0      2 4.456670 2.827344
## 5      de uma     2            0      2 4.456670 2.827344
quanteda.textstats::textstat_collocations(txt.raizesBr, size = 2, min_count = 2)
##   collocation count count_nested length   lambda        z
## 1   no brasil     2            0      2 6.626718 3.781401
## 2     por ora     2            0      2 6.626718 3.781401
## 3      toda a     2            0      2 5.770551 3.518257
## 4     de toda     2            0      2 4.456670 2.827344
## 5      de uma     2            0      2 4.456670 2.827344

EM EXPANSÃO Este trecho será expandido

12.3.2 Colocação com Udpipe

EM EXPANSÃO Este trecho será expandido

Manning, C. D., Raghavan, P., & Schütze, H. Introduction to Information Retrieval. Cambridge: Cambridge University Press. 2008.

Dicas