Aprendizado Não-Supervisionado

Exercício 01 - Clusterização de Documentos

Nesse exercício vamos utilizar os métodos de clusterização estudados para agrupar documentos. Neste exemplo, documentos serão compostos pelas sinópses de filmes. Vamos seguir a proposto deste tuturial. Para construir a base de dados, coletamos as sinopses dos 100 filmes mais bem votados do IMDb. Para isso, utilizamos a API do imdbpie que permite coletar informações diretamente da base do IMDb. Para o pré-processamento dos dados textuais utilizamos o NLTK.

Para funcionar é preciso instalar o pacode que permite carregar os dados do IMDb e o NLKT (caso não tenha instalado). Para isso, siga os seguintes passos:

Para quem usa a máquina do C9.io

  • Acesse a pasta inteligenciaartificial
  • Acesse o espaço viritual para instalação dos pacotes pelo comando: source ia_env/bin/activate
  • Instale o pacote do IMDb pelo comando: pip install imdbpie
  • Instale o pacote do NLTK pelo comando: pip install nltk

Pronto. Agora é só executar o comando a seguir.


In [1]:
# Imports necessários para este exercício
from __future__ import print_function
import nltk
import re
import pandas as pd
from sklearn.cluster import KMeans
from imdbpie import Imdb
from nltk.stem.snowball import SnowballStemmer
from sklearn.externals import joblib
from IPython.display import YouTubeVideo, Image

Dataset

O dataset é composto por uma lista de 100 filmes e suas respectivas sinopses. Como leva um certo tempo para acessar a base do IMDb e retornar as informações dos filmes, coletamos os dados e persistimos em um arquivo top100titles.pkl.


In [29]:
imdb = Imdb()
imdb = Imdb(anonymize=True) # to proxy requests

Caso seja a primeira vez que esteja executando o tutorial é necessário rodar o código a seguir para gerar a base. Caso contrário, pode pular e carregar as informações diretamente do arquivo.


In [30]:
top100 = imdb.top_250()[:100]
title_object = []
    
for movie in top100:
    print("Collecting %s" % movie['title'])
    title = imdb.get_title_by_id(movie['tconst'])
    title_object.append(title)
    
joblib.dump(title_object, "top100titles.pkl")


Collecting The Shawshank Redemption
Collecting The Godfather
Collecting The Godfather: Part II
Collecting The Dark Knight
Collecting 12 Angry Men
Collecting Schindler's List
Collecting Pulp Fiction
Collecting The Lord of the Rings: The Return of the King
Collecting The Good, the Bad and the Ugly
Collecting Fight Club
Collecting The Lord of the Rings: The Fellowship of the Ring
Collecting Forrest Gump
Collecting Star Wars: Episode V - The Empire Strikes Back
Collecting Inception
Collecting The Lord of the Rings: The Two Towers
Collecting One Flew Over the Cuckoo's Nest
Collecting Goodfellas
Collecting The Matrix
Collecting Seven Samurai
Collecting Star Wars: Episode IV - A New Hope
Collecting City of God
Collecting Se7en
Collecting The Silence of the Lambs
Collecting It's a Wonderful Life
Collecting Life Is Beautiful
Collecting The Usual Suspects
Collecting Léon: The Professional
Collecting Saving Private Ryan
Collecting Spirited Away
Collecting American History X
Collecting Once Upon a Time in the West
Collecting Interstellar
Collecting Psycho
Collecting The Green Mile
Collecting Casablanca
Collecting City Lights
Collecting The Intouchables
Collecting Modern Times
Collecting Raiders of the Lost Ark
Collecting The Pianist
Collecting The Departed
Collecting Rear Window
Collecting Terminator 2
Collecting Back to the Future
Collecting Whiplash
Collecting Gladiator
Collecting The Prestige
Collecting The Lion King
Collecting Memento
Collecting Apocalypse Now
Collecting Alien
Collecting The Great Dictator
Collecting Sunset Blvd.
Collecting Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb
Collecting Cinema Paradiso
Collecting The Lives of Others
Collecting Grave of the Fireflies
Collecting Paths of Glory
Collecting Blade Runner 2049
Collecting Django Unchained
Collecting The Shining
Collecting WALL·E
Collecting American Beauty
Collecting The Dark Knight Rises
Collecting Princess Mononoke
Collecting Oldboy
Collecting Aliens
Collecting Witness for the Prosecution
Collecting Once Upon a Time in America
Collecting Das Boot
Collecting Citizen Kane
Collecting Dangal
Collecting Vertigo
Collecting North by Northwest
Collecting Star Wars: Episode VI - Return of the Jedi
Collecting Braveheart
Collecting Reservoir Dogs
Collecting M
Collecting Requiem for a Dream
Collecting Amélie
Collecting Like Stars on Earth
Collecting A Clockwork Orange
Collecting Your Name
Collecting Lawrence of Arabia
Collecting Double Indemnity
Collecting Amadeus
Collecting Eternal Sunshine of the Spotless Mind
Collecting Taxi Driver
Collecting To Kill a Mockingbird
Collecting Full Metal Jacket
Collecting Dunkirk
Collecting 2001: A Space Odyssey
Collecting Singin' in the Rain
Collecting Toy Story 3
Collecting Toy Story
Collecting The Sting
Collecting 3 Idiots
Collecting Bicycle Thieves
Collecting Inglourious Basterds
Collecting The Kid
Out[30]:
['top100titles.pkl']

In [31]:
# Carregando as informações diretamente do arquivo gerado
top100titles = joblib.load('top100titles.pkl')

A variável top100titles é uma lista de objetos do tipo Title. Esse objeto encapsula um filme do IMDb. Para ter acesso a todas as informações disponíveis, acesse a documentação da API. A seguir usamos o comando a função dir do python para listar todos os métodos que possuem na classe que implementa o Objeto Title.


In [32]:
title0 = top100titles[0]
dir(title0)


Out[32]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__slotnames__',
 '__str__',
 '__subclasshook__',
 '__unicode__',
 '__weakref__',
 '_extract_cast_summary',
 '_extract_cover_url',
 '_extract_creators',
 '_extract_credits',
 '_extract_directors_summary',
 '_extract_trailer_image_urls',
 '_extract_trailers',
 '_extract_writers_summary',
 '_extract_year',
 'cast_summary',
 'certification',
 'cover_url',
 'creators',
 'credits',
 'directors_summary',
 'genres',
 'imdb_id',
 'plot_outline',
 'plots',
 'poster_url',
 'rating',
 'release_date',
 'runtime',
 'tagline',
 'title',
 'trailer_image_urls',
 'trailers',
 'type',
 'votes',
 'writers_summary',
 'year']

Nossa base de dados consiste em uma lista de Título dos Filmes e uma lista correspondente das Sinopses destes filmes. O código a seguir gera essas duas listas a partir da lista com os Top100 filmes do IMDb. Uma filme pode ter mais de uma sinopse no IMDb. Desta forma, concatemos todas estes textos em um único que representa o conteúdo do documento.


In [33]:
list_of_titles = []
list_of_synopses = []
list_of_ids = []

for title in top100titles:
    plot_str = ""
    list_of_titles.append(title.title)
    for plot in title.plots:
        plot_str += plot + " "
        
    list_of_synopses.append(plot_str)
    list_of_ids.append(title.imdb_id)

Por fim, para completar a base vamos gerar o vocabulário. O vocabulário será formado de duas formas: uma lista das palavras e uma lista do stemmer. O stemmer é o radical da palavra. Para classificação isso se mostrar interessante porque não diferencia palavras como run, running, runner. Todas, independente da conjugação, remetem a ação de correr. Sendo assim, considerando uma palavra como uma feature, teríamos somente uma run que seria referencianda em qualquer ocorrência de seus derivados.

A biblioteca do NLTK já vem com métodos que realiza essa tarefa. Primeiro precisamos instanciar o objeto stemmer.


In [34]:
stemmer = SnowballStemmer("english")

Vão ser criadas duas funções: tokenize_and_stem que tokeniza o texto utilizando o stemmer. E a função tokenize_only que faz a tokenização padrão do texto.


In [35]:
def tokenize_and_stem(text):
    # first tokenize by sentence, then by word to ensure that punctuation is caught as it's own token
    tokens = [word for sent in nltk.sent_tokenize(text) for word in nltk.word_tokenize(sent)]
    filtered_tokens = []
    # filter out any tokens not containing letters (e.g., numeric tokens, raw punctuation)
    for token in tokens:
        if re.search('[a-zA-Z]', token):
            filtered_tokens.append(token)
    stems = [stemmer.stem(t) for t in filtered_tokens]
    return stems


def tokenize_only(text):
    # first tokenize by sentence, then by word to ensure that punctuation is caught as it's own token
    tokens = [word.lower() for sent in nltk.sent_tokenize(text) for word in nltk.word_tokenize(sent)]
    filtered_tokens = []
    # filter out any tokens not containing letters (e.g., numeric tokens, raw punctuation)
    for token in tokens:
        if re.search('[a-zA-Z]', token):
            filtered_tokens.append(token)
    return filtered_tokens

Por fim é gerado os dois vocabulários e um DataFrame com um índice do stemmer para a palavra original.


In [9]:
totalvocab_stemmed = []
totalvocab_tokenized = []
for i in list_of_synopses:
    allwords_stemmed = tokenize_and_stem(i) #for each item in 'synopses', tokenize/stem
    totalvocab_stemmed.extend(allwords_stemmed) #extend the 'totalvocab_stemmed' list
    
    allwords_tokenized = tokenize_only(i)
    totalvocab_tokenized.extend(allwords_tokenized)

In [36]:
vocab_frame = pd.DataFrame({'words': totalvocab_tokenized}, index = totalvocab_stemmed)
print('Existem ' + str(vocab_frame.shape[0]) + ' itens no vocab_frame')


Existem 47495 itens no vocab_frame

In [37]:
vocab_frame.head()


Out[37]:
words
chronicl chronicles
the the
experi experiences
of of
a a

Relizando a Clusterização

Montado nosso dataset, precisamos construir o objeto que de fato será passado para os métodos de clusterização. Cada texto será representado pelas palavras que o copoõe já que é isso que diferencia os textos entre si. No entanto, não vamos utilizar somente uma grande matriz com todas as palavras e sua frequência. Para isso, vamos utilizar a métrica tf-idf para caracterizar cada palavra no texto. Para entender um pouco mais sobre essa métrica acesse este link ou os dois vídeos que seguem.


In [12]:
YouTubeVideo("t2Nq3MFK_pg")


Out[12]:

In [13]:
YouTubeVideo("xYQb6f1SIEk")


Out[13]:

Para construir essa representação dos textos, vamos utilizar o método TfidfVectorizer do Scikit-Learn. São passados os seguintes parâmetros. Detalhes podem ser encontrados na documentação do método. Explicando alguns:

  • max_df: filtra todas as palavras que possuem frequência maior que 80%. Palavras muito frequentes podem não trazer informação, já que é comum em todo o texto.
  • min_df: filtra todas as palavras que possuem frequência menor que 20%. Palavras pouco frequentes podem não trazer siginificado nenhum também.
  • max_features: máximo de features que a matriz deve possuir.
  • stop_words: elimina stopwords em inglês
  • tokenizer: função de tokenizer utilizada. Neste caso, utilizaremos a função implementada com stemming;

In [38]:
from sklearn.feature_extraction.text import TfidfVectorizer

#define vectorizer parameters
tfidf_vectorizer = TfidfVectorizer(max_df=0.8, max_features=200000,
                                 min_df=0.2, stop_words='english',
                                 use_idf=True, tokenizer=tokenize_and_stem, ngram_range=(1,3))

%time tfidf_matrix = tfidf_vectorizer.fit_transform(list_of_synopses) #fit the vectorizer to synopses

print(tfidf_matrix.shape)


CPU times: user 2.02 s, sys: 22.6 ms, total: 2.04 s
Wall time: 2.08 s
(100, 92)

A matriz final tem dimensão (100x92), ou seja, 100 textos caracterizados por 92 termos. Observe que isso diminui bastante a dimensionalidade se comparado com todas as palavras do vocabulário. O vocabulário final pode ser obtido pelo código a seguir:


In [39]:
terms = tfidf_vectorizer.get_feature_names()
print(terms)


['becom', 'befor', 'begin', 'believ', 'boy', 'come', 'complet', 'continu', 'crime', 'day', 'death', 'decid', 'discov', 'doe', 'dure', 'end', 'escap', 'eventu', 'fall', 'famili', 'father', 'film', 'follow', 'forc', 'friend', 'goe', 'good', 'head', 'help', 'home', 'howev', 'includ', 'job', 'kill', 'know', 'known', 'lead', 'learn', 'leav', 'life', 'like', 'littl', 'live', 'look', 'love', 'make', 'man', 'mani', 'meet', 'men', 'mother', 'murder', "n't", 'need', 'new', 'offic', 'old', 'onc', 'onli', 'order', 'peopl', 'person', 'place', 'plan', 'play', 'power', 'realiz', 'return', 'run', 'save', 'sent', 'set', 'small', 'soon', 'start', 'state', 'stori', 'success', 'thing', 'time', 'togeth', 'tri', 'turn', 'use', 'want', 'war', 'way', 'woman', 'work', 'world', 'year', 'young']

Exercício

01) Sabendo que tfidf_matrix é sua matriz onde as linhas são os filmes e as colunas as palavras, aplique os algoritmos do KMeans e do DBScan para classificar os filmes.

02) Pesquisa na documentação dos métodos não supervisionados como exibir os itens de cada cluster. Mostre os filmes que fazem parte de cada cluster.

03) Crie uma base de dados para ser utilizada na tarefa de classificação supervisionada com os 100 textos categorizados pelos algoritmos não supervisionado.