Tutorial de PNL: Modelagem de Tópicos com Decomposição de Valor Singular (SVD) e SVD Truncado (bibliotecas python fbpca e Sklearn usadas)

Georgios Drakos Segue 20 de jun · 11 min ler

A modelagem de tópicos é uma tarefa interessante para alguém começar a se familiarizar com a PNL. Resumidamente, é a tarefa de usar o aprendizado não supervisionado para extrair os principais tópicos (representados como um conjunto de palavras) que ocorrem em uma coleção de documentos. Neste artigo, estou resolvendo um problema de modelagem de tópico usando uma técnica popular de decomposição de matriz denominada Singular Value Decomposition (SVD) .

Índice

  • O problema
  • Motivação
  • Começando
  • Importar Bibliotecas
  • Carregue os dados
  • Técnicas de pré-processamento de dados de texto
  • Quando usar essas técnicas?
  • Pré-processamento de dados
  • Decomposição de Valor Singular (SVD)
  • SVD Completo vs Reduzido
  • Visualização de Tópicos
  • SVD randomizado
  • Conclusão
  • Referências

O problema

Começamos com uma matriz de documentos de termo :

A matriz acima pode ser decomposta em uma matriz fina e alta vezes uma matriz grande e curta (possivelmente com uma matriz diagonal no meio).

Observe que essa representação não leva em conta a ordem das palavras ou a estrutura da sentença. É um exemplo de abordagem de um saco de palavras .

Motivação

Considere o caso mais extremo – reconstruindo a matriz usando um produto externo de dois vetores . Claramente, na maioria dos casos, não conseguiremos reconstruir a matriz exatamente. Mas se tivéssemos um vetor com a frequência relativa de cada palavra do vocabulário fora da contagem total de palavras, e um com o número médio de palavras por documento, então esse produto externo seria o mais próximo possível.

Agora considere aumentar essas matrizes para duas colunas e duas linhas. A decomposição ideal seria agora agrupar os documentos em dois grupos, cada um com uma distribuição de palavras tão diferente quanto possível, mas o mais semelhante possível entre os documentos no cluster. Vamos chamar esses dois grupos de "tópicos". E agruparíamos as palavras em dois grupos, com base naqueles que aparecem com mais frequência em cada um dos tópicos.

Começando

Coletaremos um conjunto de dados de documentos de várias categorias diferentes e encontraremos tópicos (consistindo em grupos de palavras) para eles. Conhecer as categorias reais nos ajuda a avaliar se os tópicos que encontramos fazem sentido.

Vamos tentar isso usando uma técnica de fatoração de matriz denominada Singular Value Decomposition (SVD).

Importar Bibliotecas

Primeiro, vamos importar as bibliotecas necessárias do Python:

 import numpy como np 
de sklearn.datasets import fetch_20newsgroups
da decomposição de importação sklearn
do linalg importação scipy
import matplotlib.pyplot como plt
% matplotlib inline
np.set_printoptions (suprimir = verdadeiro)

Carregue os dados

O Sklearn vem com vários conjuntos de dados internos, bem como o carregamento de utilitários para carregar vários conjuntos de dados externos padrão. Esse é um ótimo recurso , e os conjuntos de dados incluem preços de imóveis em Boston, imagens de rosto, manchas de floresta, diabetes, câncer de mama e muito mais. Nós estaremos usando o conjunto de dados dos newsgroups .

Grupos de discussão são grupos de discussão na Usenet, que foi popular nos anos 80 e 90 antes de a web realmente decolar. Esse conjunto de dados inclui 18.000 postagens de grupos de notícias com 20 tópicos. Ao executar o comando abaixo, baixamos os dados que podem levar algum tempo. Para limitar os dados, selecionamos um subconjunto e simplesmente mantemos todos os artigos pertencentes a categorias = ['alt.atheism', 'talk.religion.misc', 'comp.graphics', 'sci.space'].

 categories = ['alt.atheism', 'talk.religion.misc', 'comp.graphics', 'sci.space'] 
remove = ('cabeçalhos', 'rodapés', 'citações')
newsgroups_train = fetch_20newsgroups (subconjunto = 'train', categorias = categorias, remove = remove)
newsgroups_test = fetch_20newsgroups (subconjunto = 'teste', categorias = categorias, remover = remover)
newsgroups_train.filenames.shape, newsgroups_train.target.shape

Vamos dar uma olhada em alguns dos dados e tentar adivinhar a categoria da mensagem?

 print (" n" .join (newsgroups_train.data [: 1])) newsgroups_train.data [: 1] 

O atributo de destino desta mensagem é:

 np.array (newsgroups_train.target_names) [newsgroups_train.target [0]] 

O atributo target é o índice inteiro da categoria e todo mapa de índice para um tópico:

 print (newsgroups_train.target [: 5]) np.array (newsgroups_train.target_names) [newsgroups_train.target [: 5]] 

Este conjunto de dados tem, na verdade, as notícias já agrupadas em tópicos principais. Que você pode ter um olhar mais atento executando o seguinte comando: (lembre-se de que filtramos o conjunto de dados para obter qualquer texto associado a 4 das 20 categorias de tópico que esse conjunto de dados tem para acelerar o processo)

 print (list (newsgroups_train.target_names)) 

Técnicas de pré-processamento de dados de texto

Por favor, veja também meu outro post no blog que apresenta em detalhes as técnicas de pré-processamento de texto.

Soltando termos comuns: pare palavras

Às vezes, algumas palavras extremamente comuns podem ter pouco ou nenhum valor em ajudar a selecionar o tópico do documento. Estes são chamados de palavras de parada e podem ser excluídos do vocabulário inteiramente. A estratégia geral para determinar uma lista de parada é classificar os termos por frequência de coleta (o número total de vezes que cada termo aparece na coleção de documentos) e, em seguida, usar os termos mais frequentes, geralmente filtrados manualmente para seu conteúdo semântico o domínio dos documentos sendo indexados, como uma lista de parada , cujos membros são descartados durante a indexação. Um exemplo de uma lista de parada é mostrado na figura acima.

O uso de uma lista de parada reduz significativamente o número de postagens que um sistema precisa armazenar e, em grande parte, o tempo não indexando as palavras de parada não prejudica: as pesquisas de palavras-chave com termos como e por não parecem muito úteis. No entanto, isso não é verdade para pesquisas de frase. A frase consulta "Presidente dos Estados Unidos", que contém duas palavras de parada, é mais precisa do que o presidente E "Estados Unidos". O significado de “voos para Londres” provavelmente será perdido se a palavra “para” for interrompida. Alguns títulos de músicas e versos bem conhecidos consistem inteiramente de palavras que são comumente colocadas em stop lists (ser ou não ser, Let It Be, não quero ser,…). Por esse motivo, os mecanismos de pesquisa da Web geralmente não usam listas de parada.

Não há uma lista universal única de palavras de parada. Cada biblioteca tem suas próprias palavras de parada.

Sklearn

 from sklearn.feature_extraction import stop_words classificado (list (stop_words.ENGLISH_STOP_WORDS)) [: 20] 

Spacy

Spacy é uma biblioteca nlp muito moderna e rápida. Spacy é opinativo, na medida em que normalmente oferece uma maneira altamente otimizada de fazer algo (enquanto que o nltk oferece uma enorme variedade de maneiras, embora elas geralmente não sejam tão otimizadas).

Você precisará instalá-lo.

se você usa conda:

 conda instalar -c conda-forge spacy 

se você usar pip:

 pip instale -U spacy 

Você precisará baixar o modelo em inglês:

 python -m spacy download pt_core_web_sm import spacy nlp = spacy.load ("en_core_web_sm") classificado (lista (nlp.Defaults.stop_words)) [: 20] 

NLTK

 importar nltk 
das palavras-chave de importação do nltk.corpus
nltk.download ('wordnet')
classificado (list (stopwords.words ("english"))) [: 20]

Exercício: O que as palavras de parada aparecem no spacy, mas não no sklearn?

 de sklearn.feature_extraction import stop_words 
importação spacy
nlp = spacy.load ("en_core_web_sm")
sk_stopwords = list (stop_words.ENGLISH_STOP_WORDS)
spacy_stopwords = list (nlp.Defaults.stop_words)
list (set (spacy_stopwords) -set (sk_stopwords))

Exercício: E quais palavras de parada estão no sklearn, mas não no spacy?

 list (set (sk_stopwords) -set (spacy_stopwords)) 

Caule e Lematização

As palavras abaixo são as mesmas?

  • organizar, organizar e organizar
  • democracia, democracia e democratização

O desencadeamento e a lematização geram a raiz das palavras.

  • A lematização usa as regras sobre uma linguagem . Os tokens resultantes são todos palavras reais
  • “A destruição é a lematização do pobre homem.” (Noah Smith, 2011) A Stemming é uma heurística grosseira que corta as extremidades das palavras . Os tokens resultantes podem não ser palavras reais. Stemming é mais rápido.

NLTK

Vamos ver um exemplo usando a biblioteca NLTK :

 importar nltk 
de haste de importação nltk
wnl = stem.WordNetLemmatizer ()
Porter = stem.porter.PorterStemmer ()
word_list = ['feet', 'foot', 'foots', 'footing', 'voar', 'voar', 'voar',
'organizar', 'organizar', 'organizar', 'universo', 'universidade']
print ([wnl.lemmatize (palavra) para word em word_list])
print ([porter.stem (palavra) para word em word_list])

Caule e lematização são dependentes da linguagem. Idiomas com morfologias mais complexas podem apresentar maiores benefícios.

Spacy

Vamos ver o mesmo exemplo acima usando a biblioteca Spacy :

Derivação e lematização são dependentes da implementação. Spacy é uma biblioteca nlp muito moderna e rápida. Spacy é opinativo, na medida em que normalmente oferece uma maneira altamente otimizada de fazer algo (enquanto que o nltk oferece uma enorme variedade de maneiras, embora elas geralmente não sejam tão otimizadas).

Spacy não oferece um stemmer (já que a lematização é considerada melhor – este é um exemplo de ser opinativo!)

 importação spacy 
de lemmatizador de importação spacy.lemmatizer
Lematizador = Lematizador ()
[lemmatizer.lookup (palavra) por palavra em word_list]

Quando usar essas técnicas?

Essas foram consideradas técnicas padrão há muito tempo, mas podem prejudicar seu desempenho se usarem aprendizado profundo . Derivando, lematizando, e removendo palavras de parada todos envolvem jogando fora a informação.

No entanto, eles ainda podem ser úteis ao trabalhar com modelos mais simples e conjuntos de dados menores.

Pré-processamento de dados

Isso envolve o seguinte:

  • Tokenização : Divida o texto em frases e as frases em palavras. Diminua as palavras e remova a pontuação.
  • Palavras com menos de 3 caracteres são removidas.
  • Todas as palavras de parada são removidas.
  • Palavras são lematizadas

Para isso usamos o método Sklearn CountVectorizer para pré-processar o texto, vamos verificar seus hiperparâmetros:

  • tokenizer = LemmaTokenizer (): um tokenizador embutido é passado para pré-processar o texto, lematizando e descartando qualquer palavra com menos de 3 caracteres e palavras irrelevantes. O NLTK nos ajuda nessa tarefa.
  • max_df = 0.9: qualquer palavra com frequência acima de 90% é jogada fora
  • min_df = 5: da mesma forma, qualquer encontro de palavras com menos de 5 vezes é jogado fora
 de sklearn.feature_extraction.text import CountVectorizer 
de nltk import word_tokenize, stem
import re
classe LemmaTokenizer (objeto):
def __init __ (self):
self.wnl = stem.WordNetLemmatizer ()
def __call __ (self, doc):
SYMBOLS_TO_KEEP = re.compile ('[^ A-Za-z0-9] +')
doc = re.sub (SYMBOLS_TO_KEEP, "", doc)
return [self.wnl.lemmatize (t) para t em word_tokenize (doc) if (len (t)> 3) & (t não em stop_words.ENGLISH_STOP_WORDS)]
vectorizer = CountVectorizer (tokenizer = LemmaTokenizer (),
minúscula = Verdadeiro
max_df = 0,9
min_df = 5)
# Poderíamos ter usado o TfidfVectorizer () como vetores = vectorizer.fit_transform (newsgroups_train.data) .todense ()
vectors.shape

Além disso, podemos ter uma visão mais próxima das palavras que são consideradas executando o seguinte código:

 vocab = np.array (vectorizer.get_feature_names ()) 
print (vocab.shape)
vocabulário [3000: 3050]

Decomposição de Valor Singular (SVD)

Como Gilbert Strang mencionou, “SVD não é tão famoso como deveria ser”. Na verdade, é uma ferramenta realmente útil.

Esperaríamos claramente que as palavras que aparecem com mais frequência em um tópico aparecessem com menos frequência no outro – caso contrário, essa palavra não seria uma boa escolha para separar os dois tópicos. Portanto, esperamos que os tópicos sejam ortogonais .

O algoritmo SVD fatoriza uma matriz em uma matriz com colunas ortogonais e uma com linhas ortogonais (juntamente com uma matriz diagonal, que contém a importância relativa de cada fator).

O SVD é uma decomposição exata, pois as matrizes criadas são grandes o suficiente para cobrir totalmente a matriz original. O SVD é extremamente usado na álgebra linear e, especificamente, na ciência de dados.

Primeiro, vamos executar o SVD em nosso vetor vetorial de contagem da seguinte forma:

 % de tempo U, s, Vh = linalg.svd (vetores, full_matrices = Falso) print (U.shape, s.shape, Vh.shape, vocab.shape) 

Vale a pena observar o fato de que especificamos full_matrices=False , se tivéssemos especificado como True, isso nos daria como saída:

 % time U, s, Vh = linalg.svd (vetores, full_matrices = True) print (U.shape, s.shape, Vh.shape, vocab.shape) 

SVD Completo vs Reduzido

Nós definimos full_matrices=False para calcular o SVD reduzido. Para o SVD completo, U e V são matrizes quadradas , onde as colunas extras em U formam uma base ortonormal (mas zeram quando multiplicadas por linhas extras de zeros em S).

Vamos agora plotar os valores singulares s que, como vemos, são classificados em ordem decrescente e todos positivos, já que executamos o SVD reduzido:

 plt.plot (s); soma (s <0) 

Os tópicos mais populares serão aqueles que correspondem aos maiores valores singulares s.

Agora podemos obter os 10 tópicos mais populares simplesmente executando os seguintes comandos e restringindo-os a conter apenas as 8 palavras mais populares:

 num_top_words = 8 def show_topics (a): 
top_words = lambda t: [vocab [i] para i em np.argsort (t) [: - num_top_words-1: -1]]
topic_words = ([top_words (t) para t em a])
return ['' .join (t) para t em topic_words]
show_topics (Vh [: 10]) # Tópicos atuais
np.array (newsgroups_train.target_names)

Recebemos tópicos que correspondem aos tipos de clusters que esperamos! Isso ocorre apesar do fato de que este é um algoritmo não supervisionado – o que significa que nunca contamos ao algoritmo como nossos documentos são agrupados.

Visualização de Tópicos

Para descobrir como nossos tópicos são distintos, podemos visualizá-los. É claro que não podemos visualizar mais de 3 dimensões, por isso usaremos uma técnica relativamente nova chamada UMAP (Uniform Manifold Approximation and Projection), que pode nos ajudar a visualizar dados de alta dimensão em dimensões mais baixas.

 importar umap.umap_ asp 
incorporação = umap.UMAP (n_neighbors = 150, min_dist = 0.5, random_state = 12) .fit_transform (U [:,: 10])
plt.figure (figsize = (7,5))
plt.scatter (incorporação [:, 0], incorporação [:, 1],
c = newsgroups_train.target,
s = tamanho 10 #
)
print (newsgroups_train.target.shape)
plt.show ()

Por favor, note que assumimos que existem tópicos distintos no conjunto de dados. Portanto, se o conjunto de dados for um monte de tweets aleatórios, os resultados do modelo podem não ser tão interpretáveis.

SVD randomizado

Como mostrado acima, estávamos interessados em encontrar apenas os 10 principais tópicos, observando as primeiras 10 linhas de Vh. Então, há alguma maneira de acelerar esse processo?

Sim, é chamado de SVD Randomizado, que leva em consideração o fato de que estamos apenas interessados nos vetores correspondentes aos maiores valores singulares. Vamos fazer uma comparação de tempo:

 da decomposição de importação sklearn 
import fbpca #Randomized SVD da biblioteca do Facebook fbpca - muito rápido
% time u_svd, s_svd, v_svd = np.linalg.svd (vetores, full_matrices = False)
print (u_svd.shape, s_svd.shape, v_svd.shape)
print ()
% time u_rand, s_rand, v_rand = decomposição.randomized_svd (vetores, 10)
print (u_rand.shape, s_rand.shape, v_rand.shape)
print ()
% time u_fbpca, s_fbpca, v_fbpca = fbpca.pca (vetores, 10)
print (u_fbpca.shape, s_fbpca.shape, v_fbpca.shape)
impressão()

Similarmente para Visualização de Tópicos

Randomized_svd

 importar umap.umap_ asp 
incorporação = umap.UMAP (n_neighbors = 150, min_dist = 0.5, random_state = 12) .fit_transform (u_rand)
plt.figure (figsize = (7,5))
plt.scatter (incorporação [:, 0], incorporação [:, 1],
c = newsgroups_train.target,
s = tamanho 10 #
)
print (newsgroups_train.target.shape)
plt.show ()

fbpca

 importar umap.umap_ asp 
incorporação = umap.UMAP (n_neighbors = 150, min_dist = 0.5, random_state = 12) .fit_transform (u_fbpca)
plt.figure (figsize = (7,5))
plt.scatter (incorporação [:, 0], incorporação [:, 1],
c = newsgroups_train.target,
s = tamanho 10 #
)
print (newsgroups_train.target.shape)
plt.show ()

Dito isso, o fbpca é muito mais rápido do que as outras bibliotecas, tornando-o um candidato ideal quando se trabalha com uma enorme quantidade de documentos.

Para mais informações sobre SVD randomizado, confira esta palestra do PyBay 2017 .

Conclusão

Isso nos leva ao final deste artigo. Espero que você tenha uma compreensão básica de como o SVD pode ser usado na modelagem de tópico. Por favor, lembre-se de usá-lo, pois é um poderoso algoritmo não supervisionado . Sinta-se à vontade para usar o snippet de código Python deste artigo.

Obrigado pela leitura e estou ansioso para ouvir suas perguntas 🙂

Referências