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.
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
In [2]:
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 [33]:
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")
Out[33]:
In [3]:
# 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 [4]:
title0 = top100titles[0]
dir(title0)
Out[4]:
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 [5]:
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 [6]:
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 [7]:
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 [8]:
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 [9]:
vocab_frame = pd.DataFrame({'words': totalvocab_tokenized}, index = totalvocab_stemmed)
print('Existem ' + str(vocab_frame.shape[0]) + ' itens no vocab_frame')
In [10]:
vocab_frame.head()
Out[10]:
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 [11]:
YouTubeVideo("t2Nq3MFK_pg")
Out[11]:
In [12]:
YouTubeVideo("xYQb6f1SIEk")
Out[12]:
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:
In [13]:
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)
A matriz final tem dimensão (100x88), ou seja, 100 textos caracterizados por 88 termos. Observe que isso diminui bastante a dimensionalidade se comparado com todas as palavras do vocabulário (aproximadamente 5000). O vocabulário final pode ser obtido pelo código a seguir:
In [14]:
terms = tfidf_vectorizer.get_feature_names()
print(terms)
A matriz TF-IDF é o objeto de entrada para o K-Means. Cada texto é representado por um conjunto de termos e ele vai utilizar esse conjunto de termos para caracterizar cada instância e calcular a distância entre eles na execução do algoritmo.
In [15]:
num_clusters = 5
km = KMeans(n_clusters=num_clusters)
%time km.fit(tfidf_matrix)
Out[15]:
Modelo treinado, podemos retornar os clusters gerados:
In [16]:
clusters = km.labels_.tolist()
print(len(clusters))
clusters é uma lista que associa em cada posição um número que representa o cluster. Por exemplo, o documento zero foi classificado no cluster 3.
In [17]:
print(clusters[0])
In [18]:
films = { 'title': list_of_titles, 'synopsis': list_of_synopses, 'cluster': clusters}
frame = pd.DataFrame(films, index = [clusters] , columns = ['title', 'synopsis'])
In [19]:
frame.head()
Out[19]:
Uma outra coisa interessante a se fazer é identificar quais são as N palavras mais próximas do centróide de cada cluster. Essa informação pode nos dar uma idéia geral do tipo de filme que compõe cada cluster. Nesse caso, vamos utilizar N = 6.
In [23]:
N = 6
dict_class = {}
print("Top termos por cluster:")
print()
#sort cluster centers by proximity to centroid
order_centroids = km.cluster_centers_.argsort()[:, ::-1]
for i in range(num_clusters):
print("Palavras do Cluster %d :" % i, end='')
list_words = []
for ind in order_centroids[i, :N]: #replace 6 with n words per cluster
word = vocab_frame.loc[terms[ind].split(' ')].values.tolist()[0][0].encode('utf-8', 'ignore')
list_words.append(word)
print(' %s' % word, end=',')
dict_class[i] = list_words
print()
print()
print("Filmes do Cluster %d:" % i, end='')
for title in frame.loc[i]['title'].values.tolist():
print(' %s,' % title, end='')
print()
print()
In [118]:
from sklearn.cluster import DBSCAN
dbscan = DBSCAN(eps=1.0, min_samples=2)
%time dbscan.fit(tfidf_matrix)
clusters_dbscan = dbscan.labels_.tolist()
print(len(set(clusters_dbscan)))
print(clusters_dbscan)
In [119]:
films_dbscan = { 'title': list_of_titles, 'synopsis': list_of_synopses, 'cluster': clusters_dbscan}
frame_dbscan = pd.DataFrame(films_dbscan, index = [clusters_dbscan] , columns = ['title', 'synopsis'])
In [121]:
print("Top termos por cluster:")
print()
num_clusters = len(set(clusters_dbscan)) - 1 #desconsidera os clusters -1
print(num_clusters)
for i in range(num_clusters):
print("Filmes do Cluster %d:" % i, end='')
for title in frame_dbscan.loc[i]['title'].values.tolist():
#print(frame_dbscan.loc[i])
print(' %s,' % title, end='')
print()
print()
Podemos utilizar a classificação gerada para classificar novos documentos. Ou seja, utilizaríamos 5 classes e usaríamos esta classificação como nossa base de treinamento e então poderíamos utilizar este modelo para classificar novos filmes. Essa tarefa é proposta em outro exercício. Para isso, vamos gerar a nossa base de treinamento e armazenar em um arquivo que será utilizado na outra tarefa.
In [122]:
data_classified = []
count = 0
for imdbid in list_of_ids:
data_classified.append({'imdbid': imdbid, 'class': clusters[count], 'title': list_of_titles[count], 'sinopsis': list_of_synopses[count]})
count += 1
In [123]:
joblib.dump(data_classified, "moviesclassifieds.pkl")
Out[123]:
Para facilitar a identificação dos Clusters vamos salvar também um dicionário com a identificação de clada cluster. Estamos utilizando a classificação gerada pelo KMeans.
In [124]:
joblib.dump(dict_class, "moviesclass.pkl")
Out[124]: