Brincando com dados: Descobrindo tópicos em livros com LDA


O LDA (Latent Dirichlet Allocation), que é um modelo para descobrir tópicos latentes em documentos, será aplicado aqui em capítulos de um livro. Técnicas deste tipo são parte de uma área da computação e ciência dos dados que trata do processamento de linguagem natural (Natural Language Processing). Para tal utilizaremos python para exemplificar todo o processo que vai desde a obteção e limpeza dos dados até a aplicação do LDA e visualização dos resultados obtidos.

Em poucas palavras, o LDA é um modelo no qual cada documento é composto por uma mistura de tópicos latentes. Cada tópico tendo a sua própria distribuição de palavras associadas a ele. Uma definição mais formal pode ser encontrada no artigo: Blei, David M., Andrew Y. Ng, and Michael I. Jordan. “Latent dirichlet allocation.” the Journal of machine Learning research 3 (2003): 993-1022. Outra forma de entender o modelo é utilizar a sua representação gráfica como nesta figura extraída do post de Matt Burton. Os círculos sombreados são as partes observáveis, enquanto as em branco são latentes.

LDA Graphic Model

Alice no País das Maravilhas: preparando o texto

Para acompanhar a execução do código, basta baixar o arquivo do jupyter notebook e executá-lo em seu computador. Para instruções sobre como instalar este aplicativo, basta clicar aqui .

Neste experimento utilizamos o texto do livro ‘Alice no País das Maravilhas’ obtido utilizando uma requisição HTTP GET no site do projeto Gutenberg. Após o download do conteúdo, removemos caracteres especiais e trechos do conteúdo que não fazem parte do livro em si. Esta etapa de preparação dos dados é essencial para normalizar os dados e evitar inconsistências nos tópicos. Desta forma é possível, por exemplo, evitar que a mesma palavra com acentuações distintas (por um erro de OCR por exemplo) sejam consideradas diferentes.


%matplotlib inline

import numpy as np
import nltk
from nltk.tokenize import TweetTokenizer
import re
import lda
import urllib2
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from wordcloud import WordCloud, STOPWORDS
from sklearn.feature_extraction.text import CountVectorizer

book_raw =  urllib2.urlopen("https://www.gutenberg.org/ebooks/11.txt.utf-8").read() 
book  = book_raw.split("*** START OF THIS PROJECT GUTENBERG EBOOK")[-1].split("End of Project Gutenberg's")[0]
book = re.sub(r"\r|\n|\*|'d|'ll|'s|'ve|'m|'S |busy"," ",book)
book = re.sub(r"_i_|_I_","I",book)


print("Book extract : \n\n{}...".format(book[50:600:1]))

book_chapters = book.split("CHAPTER")[1:-1]
print("\nThe book has {} chapters.").format(len(book_chapters)) 
>>>Book extract : 
>>>ALICE ADVENTURES IN WONDERLAND    Lewis Carroll    THE MILLENNIUM FULCRUM EDITION 3.0          CHAPTER I. Down the Rabbit-Hole    
>>>Alice was beginning to get very tired of sitting by her sister on the  bank, and of having nothing to do: once or twice she had peeped into 
>>>the  book her sister was reading, but it had no pictures or conversations in  it, 'and what is the use of a book,' thought Alice 'without
>>>pictures or  conversations?'    So she was considering in her own mind (as well as she could, for the  hot day made her feel very sleep...
>>>
>>>The book has 11 chapters
Alice's Adventures in Wonderland

Selecionando somente os substantivos

Neste experimento estamos interessados em visualizar tópicos inferidos utilizando somente substantivos. Para isso iremos utilizar a biblioteca nltk que implementa métodos para classificar palavras em suas categorias sintáticas (Part-of-speech tagging), por exemplo, “NN” é o código utilizado para caracterização de substatinvos. Este processo de preparação dos dados é iterativo e encontrar o subset do conteúdo ideal para essa análise pode demandar diversos experimentos.


tknzr = TweetTokenizer()
tokens = tknzr.tokenize(book)
tagged_tokens = nltk.pos_tag(tokens)
nouns = {}
words_to_remove = ["doesn't","eat","heard","set","kept","look", "use", 
                   "chapter","tell","sort","try","was","don't","wouldn't",
                    "try","play","read","read","think","thought","didn't"]

for tupla in tagged_tokens:
    if ((tupla[1] == "NN" or tupla[1] == "NNP") and tupla[0].lower() not in words_to_remove):
        nouns[tupla[0]] = True
chapter_nouns = []
for chapter in book_chapters:
    nns = ""
    for word in chapter.split(" "):
        if word in nouns.keys():
            nns+= word + " "
    chapter_nouns.append(nns)
print(chapter_nouns[0])

>>>"Down Alice beginning sister nothing book sister Alice So mind well day sleepy
>>>pleasure making daisy-chain trouble White Rabbit nothing VERY Alice VERY way 
>>>Rabbit Oh time quite Rabbit A WATCH OUT OF ITS Alice mind rabbit watch field
>>>time moment Alice world tunnel Alice moment about herself herself well plenty 
>>>ime about wonder dark jar one disappointment jar fear put one Alice fall 
>>>nothing How anything about top fall NEVER wonder centre Let thousand Alice 
>>>VERY opportunity one practice about right distance--but wonder Latitude 
>>>Longitude idea Latitude Longitude wonder fall right How walk one sound
>>>right name country New Zealand spoke--fancy CURTSEYING you're Do manage
>>>girl nothing Alice hope saucer milk Dinah wish mice catch Alice Do 
>>>answer matter way put dream walking hand hand heap fall Alice bit dark 
>>>White Rabbit moment Alice time Rabbit herself row round Alice way one 
>>>side nothing Alice one key rate time curtain door about key delight ...

Criando uma representação do livro

Para utilizar esta implementação do LDA, é necessário converter a entrada para uma lista de termos e suas frequências. Este formato, que é conhecido como Bag of Words, assume que a posição das palavras não é relevante para o modelo.

Neste experimento, modelamos cada capítulo do livro como um documento diferente. Outra possibilidade seria considerar vários livros do autor, sendo que cada um seria um documento distinto.


tf = CountVectorizer(stop_words='english')
doc_term_freq = tf.fit_transform(chapter_nouns).toarray()
vocab = tf.vocabulary_
inverted_vocab = {v: k for k, v in vocab.items()}

print("Vocabulary (up to 10 pos): \n\n{}".format([(k,v) for k,v in vocab.items()][:10:1]))
print("\n\nDoc 0 term_freq (up to 10 pos): \n\n{}".format(doc_term_freq[2][:10:1]))

>>>Vocabulary (up to 10 pos): 
>>>
>>>[(u'whiting', 595), (u'chain', 74), (u'dance', 120), (u'month', 335), (u'sleep', 488), (u'dish', 136), (u'chair', 75), (u'milk', 329), (u'row', 440), (u'hurry', 259)]

print("\n\nDoc 0 term_freq (up to 10 pos): \n\n{}".format(doc_term_freq[0][:10:1]))

>>>Doc 0 term_freq (up to 10 pos): 
>>>
>>>[1 0 0 0 1 0 0 0 0 0]

Fazendo o fit do LDA nos documentos (capítulos do livro)

Um dos hiperparâmetros do LDA é o número de tópicos que devem ser inferidos. Uma maneira de definir o número de tópicos é utilizar o método do cotovelo de acordo com a máxima verossimilhança do modelo. Entretando, mesmo altos valores desta métrica não são garantia de que os tópicos são coerentes para uma pessoa.


model = lda.LDA(n_iter=500, n_topics=6, random_state=1)
model.fit(doc_term_freq)

Visualizando os resultados

As nuvens de palavra mostram maior as palavras que tem probabilidade maior em seu respectivo tópico. Cada imagem gerada é a visualização de um tópico diferente.


topics_word_dist =  model.topic_word_
for i, word_dist in enumerate(topics_word_dist):
#     print len(word_dist)
    topic_dist_ordered = sorted(enumerate(word_dist), key=lambda x: -x[1])
    top_10_words = ""
    word_cloud_words = ""
    for k in range(0,10):
        top_10_words+= inverted_vocab[topic_dist_ordered[k][0]] + " "
        word_cloud_words+= inverted_vocab[topic_dist_ordered[k][0]]+ " " * int(topic_dist_ordered[k][1]* 1000)
    print ("Topic {}, top 10 words: {}".format(i,top_10_words))    
    wc = WordCloud(background_color="white",stopwords=STOPWORDS.add("said"))
    # generate word cloud
    wc.generate(word_cloud_words)            
    plt.subplot(3,2,(i+1))
    plt.imshow(wc)
    plt.axis("off")
plt.show()

>>> Topic 0, top 10 words: march hatter dormouse hare thing tea course piece clock court 
>>> Topic 1, top 10 words: mock turtle alice gryphon won moral repeat voice course lobster 
>>> Topic 2, top 10 words: alice time round head thing wish half rabbit till wonder 
>>> Topic 3, top 10 words: alice quite way moment right duchess door end day deal 
>>> Topic 4, top 10 words: mouse sure caterpillar let dodo house queer question pigeon pool 
>>> Topic 5, top 10 words: queen king cat white hand rabbit cook quite game stood 

Word Cloud

Para os documentos podemos ver qual tópico está mais relacionado com ele, analisando os scores de cada tópico em cada um dos documentos (capítulos).


from ipy_table import *
len(topics_word_dist)
doc_topic = model.doc_topic_
table = [["Doc","Topico 0","Topico 1","Topico 2","Topico 3","Topico 4","Topico 5"]]
i=0
for doc in doc_topic:
    table.append(["Documento "+str(i)] +doc.tolist())
    i+=1
table
make_table(table)
apply_theme('basic')
doc_topics

Os tópicos fazem sentido?

so confusing

Um dos desafios de gerar tópicos com um modelo de tópicos como o LDA é que não possuimos uma maneira direta e simples de avaliar a coesão dos tópicos automaticamente. Alguns trabalhos recentes tentam resolver este problema, por exemplo utilizando uma coleção externa como a Wikipedia, medindo por exemplo a quantidade de vezes que as palavars de cada tópico aparecem em conjunto. Para os mais curiosos seguem algumas sugestões de leitura:

Uma forma simples de avaliar a qualidade dos tópicos é realizando uma inspeção manual. Como podemos ver, o tópico 2 representa o desejo constante da duquesa de cortar cabeças, incluíndo a da Alice. Além disso os animais do livro aparecem em seus próprios tópicos como o grifo e a tartaruga.

Tags: python, imdb, data science, dados, LDA, book, topic model, latent topics, NLP, documents, and Ipython Notebook

Related Posts

Brincando com dados: Prevendo ganhadores do Oscar de 2017

No último post post analisamos as features do Rotten Tomatoes. Agora, vamos juntar todas as features relativas as principais premiações com as geradas utilizando os valores do Rotten Tomatoes e tentar prever quais serão os ganhadores dos Oscar de “Melhor Filme”, “Melhor Atriz” e “Melhor Ator”.

Brincando com dados: Análise do Oscar - Rotten Tomatoes

Finalmente chegamos àquela época do ano. Podemos chamá-la de “carnaval dos cinéfilos”, ou simplesmente de “aquela época em sabemos que a Meryl Streep será indicada”. A época das premiações mais importantes do mundo do cinema. Então vamos aproveitar essa época e fazer uma análise sobre as notas no Rotten Tomatoes dos indicados ao Oscar de melhor filme.

Prevendo evasão (churning) usando scikit-learn

Prevendo evasão de clientes em python utilizando scikit-learn.

Análise de sobrevivência (Survival Analysis)

Pequeno estudo sobre Análise de sobreviência que é um ramo da estatística que analisa tempos até ao acontecimento de determinado evento.

Brincando com dados: Vencedores do Oscar Parte 2

Na parte 2 vamos analisar os dados dos indicados ao prêmio de melhor filme dos últimos 15 anos e tentar prever o ganhador do ano de 2016.

Brincando com dados: Vencedores do Oscar Parte 1

O objetivo desses posts intitulados "Brincando com os dados" é dar exemplos de como importar, limpar e analisar dados. Nesse primeiro exemplo iremos importar no MongoDB os dados do IMDb dos filmes indicados a categoria de melhor filme do Oscar.

Projetando um ambiente inovador

Como criar um ambiente que estimule inovação

Gladiator Features

The manual task that became a script. The script that became a feature. The feature that defined a product.