Text Analytics (NLP) utilizando Python
Olá pessoal! Tudo bem com vocês?
Dando continuidade na série de textos mostrando um projeto de dados do início ao fim, hoje vou escrever sobre a terceira etapa do projeto: Text Analytics.
Se você caiu nesse texto de paraquedas, fique tranquilo! Nas etapas anteriores foi montado um crawler utilizando o Selenium para coletar comentários do Youtube e depois foi feito o pré-processamento desses dados coletados, utilizando python. Lembrando que todos os links colocados e citados ao longo do texto estão disponíveis na parte de referências.
Série de textos:
01. Como capturar comentários do Youtube usando Python
02. Guia inicial de pré-processamento de texto utilizando Python
03. Text Analytics (NLP) utilizando Python
04. Como carregar um dataframe Pandas no Google BigQuery
05. 4 dicas de visualização de dados
Introdução
Existem diversas análises que podem ser feitas com textos, como por exemplo, análise de sentimento, nuvem de palavras e classificação de motivo de contato — que vou mostrar hoje. Meu objetivo é entender o principal tema que as pessoas comentam no Youtube e para isso precisamos de alguns dados rotulados, ou seja, dados nos quais já sabemos qual foi o motivo de contato. Para isso a estratégia será a seguinte:
Etapa 01 - Rotulagem dos comentários
Imports
Já dando spoiler do que foi feito, segue a lista de libs que precisaremos para criar o nosso modelo:
import pandas as pdimport numpy as npfrom matplotlib import pyplot as pltfrom sklearn.cluster import MiniBatchKMeansfrom sklearn.metrics.pairwise import cosine_similarityfrom sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizerfrom sklearn.metrics import classification_reportfrom sklearn.model_selection import train_test_splitfrom sklearn.linear_model import LogisticRegressionfrom sklearn.feature_extraction import stop_wordsfrom wordcloud import WordCloudfrom operator import itemgetter
TFIDF e Kmeans
De maneira geral, a função pooler executa dois processos importantes: TF-IDF e Kmeans. O TF-IDF é uma das maneiras de transformar os comentários do youtube (documentos) em uma matriz, de acordo com a frequência com que cada termo das frases aparece, entretanto, existem outras formas de fazer isso, como por exemplo, utilizando Word2Vec ou Embedding. Por sua vez, o K-means é responsável por classificar esses textos de acordo com um número de clusters.
def pooler(documents, iters, min_df=5, max_df=0.7, ngram=(1, 3), max_features=1000):# documents vectorizervectorizer = TfidfVectorizer(min_df=min_df,max_df=max_df,ngram_range=ngram,max_features=max_features).fit(documents)vectorized = vectorizer.transform(documents)# pooler modelsse = []models = []for k in iters:model = MiniBatchKMeans(n_clusters=k, init_size=256, batch_size=512, random_state=42).fit(vectorized)models.append(model)sse.append(model.inertia_)model.predict(vectorized)return models, sse, vectorized, vectorizer.get_feature_names()def get_model(models, k):c = ((k — 2) // 2)return models[c]
Definição número de clusters.
Para identificar o número de cluster corretos para o K-means, foi utilizado o método de Elbow, ou cotovelo, que consiste em representar graficamente a variação explicada em função do número de clusters e escolher o cotovelo da curva como o número de clusters para usar. (DATANOVIA, 2018).
iters = range(2, 30 + 1, 2)models, sse, vectorized, vocabulary = pooler(df.comment_stemming.values, iters, min_df=5, max_df=0.6, max_features=1500)plt.plot(iters, sse)plt.show()
Predição do modelo
Nesse exemplo usaremos um total de 18 clusters.
k = 18model = get_model(models, k)model.predict(vectorized)df[‘cluster’] = model.predict(vectorized)
Melhores exemplos
Para selecionar os melhores exemplos em cada cluster foi utilizado a similaridade de cossenos. Uma outra abordagem comum é a utilização da distância Euclidiana. De maneira geral, a distância euclidiana corresponde à norma L2 de uma diferença entre vetores, já a similaridade do cosseno é proporcional ao produto escalar de dois vetores e inversamente proporcional ao produto de suas magnitudes. Portanto, vetores com uma pequena distância euclidiana um do outro estão localizados na mesma região de um espaço vetorial e os vetores com alta similaridade de cosseno estão localizados na mesma direção geral da origem. (BAELDUN, 2020).
def sort_best_binds(key):# calculate similaritiessimilarities = []centroid = model.cluster_centers_[key]for v in vectorized:similarities.append(cosine_similarity([centroid], v))# best bindsindexes = np.array([s[0][0] for s in similarities])indexes = np.argsort(indexes)[::-1]return indexes
Top N exemplos
Para tentar entender a classificação feita pelo modelo, foram selecionados os 15 exemplos principais de cada cluster:
topn = 15for i in range(len(model.cluster_centers_)):indexes = sort_best_binds(i)values = df.iloc[indexes[:topn]].comment_stop_word.valuesprint(f’\nCluster: {i}’)for v in range(topn):print(values[v][:200])
Definição das categorias
Analisando os dados presentes em cada cluster, gerou-se um dicionário com os motivos de contato. Com os clusters que não foram possíveis identificar o motivo de contato, foi feita uma rotulagem chamada “desconhecida”, onde posteriormente foi realizada a rotulagem manual.
maps = {0: ‘água na boca’,1: ‘bordão’,2: ‘elogio’,3: ‘desconhecido’,4: ‘duvida’,5: ‘elogio’,6: ‘desconhecido’,7: ‘José Almiro’,8: ‘elogio’,9: ‘desconhecido’,10: ‘desconhecido’,11: ‘bordão’,12: ‘crítica’,13: ‘elogio’,14: ‘água na boca’,15: ‘elogio’,16: ‘elogio’,17: ‘vou fazer’}top_binds = 50for key in maps:if maps[key]:# best bindsindexes = sort_best_binds(key)# labeling recordsdf[‘category’].iloc[indexes[:top_binds]] = maps[key]df.groupby([‘category’])[‘author’].count()
Por fim, esse dataset foi salvo e utilizado para rotulagem manual.
df[~df[‘category’].isin([‘Outro’])].to_csv(‘croresid_labeled.csv’, sep=’,’, encoding=’utf-8', index=False)
Se você chegou até aqui, parabéns! Agora nós temos o dataset rotulado e podemos utilizar a Regressão Logística para predizer as categorias dos demais comentários dos vídeos.
Etapa 02 - Classificação dos comentários
Leitura dos dados rotulados:
Para fazer a classificação dos comentários, leu-se o dataset com os dados rotulados:
data = pandas.read_csv(train_data)data = data[~data[‘category’].isin([‘ignorar’])]
TFIDF:
Assim como na etapa de rotulagem dos dados, se fez necessário transformar o texto em uma matriz numérica. Novamente utilizando o TFIDF.
vectorizer = TfidfVectorizer(ngram_range=(1, 3),max_features=1500,min_df=5, max_df=0.8).fit(documents)vectorizer
Regressão logística:
Os dados rotulados foram divididos da seguinte forma:
X = vectorizer.transform(documents)y = data.category.valuesoversample = RandomOverSampler(random_state=42)X_over, y_over = oversample.fit_resample(X, y)X_train, X_test, y_train, y_test = train_test_split(X_over, y_over, test_size=0.3, random_state=42)classifier = LogisticRegression(multi_class=’ovr’,solver=’lbfgs’,class_weight=’balanced’,random_state=42).fit(X_train, y_train)classifiery_pred = classifier.predict(X_test)print(classification_report(y_test, y_pred))
Para analisar o resultado do modelo, utilizou-se a métrica de precisão, recall e f1-score. Antes de explicar as métricas, é necessário explicar o que é matriz de confusão, pois as métricas são tiradas a partir dela.
Cada uma das classificações representa o erro ou acerto do modelo de acordo com o valor real, dessa forma, o positivo e negativo verdadeiros é quando o modelo acerta o valor. Já o positivo e negativo falso é quando o modelo inverte o resultado, ou seja, era positivo e o resultado foi negativo (e vice-versa).
Com a matriz de confusão é possível tirar algumas métricas:
- Precision: Entre a classe positivo, quantos o modelo acertou.
- Recall: Entre a classe positiva com valor esperado, quantas estão certas.
- F1-score: Média harmônica entre Precision e Recall.
Dessa forma, no nosso modelo temos o seguinte resultado:
Resultado com a base real
Tendo treinado o modelo com a base de dados rotulados, foi possível aplicá-lo em todo o restante dos dados e assim saber o motivo de contato.
real_data = pandas.read_csv(file_full)real_data = real_data[~real_data[‘comment_stemming’].isna()]real_data = real_data[[‘video_title’,‘author’,‘comment’,‘comment_cleaned’,‘comment_stop_word’,‘comment_tokenized’,‘comment_stemming’,‘comment_lematized’]]real_data[‘label’] = classifier.predict(vectorizer.transform(real_data.comment_stemming.values))real_data.groupby([‘label’])[‘author’].count().sort_values(ascending=False).plot.barh()
Conclusão
Classificar o motivo de contato é algo muito importante quando existe interação com um número alto de pessoas, como por exemplo, em canais de atendimento de empresas e redes sociais. Dessa maneira é possível identificar os principais temas e tomar decisões importantes com base em dados, nesse exemplo é possível criar vídeos baseados em dúvidas dos inscritos do canal e entender quais são as sugestões feitas pelos inscritos, assim como entender qual a razão dos elogios.
Se você chegou até aqui, muito obrigado! Espero que tenha gostado do conteúdo e que ele tenha te ajudado a entender um pouco mais sobre text analytics. Fico aberto a feedbacks e não deixe de acompanhar outros textos que tenho publicado aqui no Medium. Forte abraço e boas festas !
Série de textos:
01. Como capturar comentários do Youtube usando Python
02. Guia inicial de pré-processamento de texto utilizando Python
03. Text Analytics (NLP) utilizando Python
04. Como carregar um dataframe Pandas no Google BigQuery
05. 4 dicas de visualização de dados
Referências:
DATANOVIA, 2018. Determining the optimal number of clusters 3 must know methods. Disponível em: <https://www.datanovia.com/en/lessons/determining-the-optimal-number-of-clusters-3-must-know-methods/>DATANOVIA, 2018. Determing the optimal number of clusters 3 must know methods. Disponível em: <https://www.datanovia.com/en/lessons/determining-the-optimal-number-of-clusters-3-must-know-methods/>
BAELDUN, 2020. Euclidean distance vs cosine similarity. Disponível em: <https://www.baeldung.com/cs/euclidean-distance-vs-cosine-similarity#:~:text=The%20Euclidean%20distance%20corresponds%20to,the%20product%20of%20their%20magnitudes.>
<https://scikit-learn.org/stable/modules/clustering.html#k-means>