Análise de Emoções em tweets relacionados à pandemia de Covid-19¶

Este notebook é um experimento feito a partir do que foi desenvolvido durante minha iniciação científica (IC) durante os anos de 2020 e 2021. Os passos conduzidos durante a IC são descritos nas seções seguintes, de Entendimento do negócio à Preparação dos dados.

O experimento conduido neste notebook irá se diferenciar do trabalho original a partir da seção de Modelagem, na qual será utilizada uma diferente abordagem a fim de explorar conceitos de tratamento de dados, treinamento e teste de performance de algoritmos de machine learning.

Entendimento do negócio¶

Com a crescente pervasividade da internet e das redes sociais, também cresce sua relevância para análise do debate público, sobretudo no que tange a saúde mental. O estudo de (Wolohan, 2009), por exemplo, faz uso de postagens do Reddit para mostrar o desenvolvimento ou agravamento de casos de depressão durante a pandemia de Covid-19, demonstrando a influência de uma saúde física no desenvolvimento de doenças mentais.

Sendo assim, selecionamos o Twitter como fonte de dados textuais para compreensão do fenômenos da variação de emoções dos usuários do Twitter durante a pandemia de Covid-19, buscando assim ter um panorama de como a saúde mental dos brasileiros foi afetada pela pandemia.

As emoções do tweets foram classificadas de acordo com as 5 emoções de Ekman (Ekman 2004), a saber: felicidade, tristeza, raiva, nojo e medo. Além dessas foi acrescida também a emoção "neutro", com a qual se espera classificar postagens de noticiários.

Entendimento dos dados¶

O Twitter foi selecionado como fonte, tanto dos dados textuais de treinamento, quanto dos dados para análise final. A rede foi escolhida devido à facilidade de obtenção de dados, que pode ser feita através da API disponibilizada pela própria rede social, ou através de web crawlers disponíveis para a plataforma.

Conjunto de emoções de Ekman¶

Devido à escassez de conjuntos de dados (datasets) com textos rotulados com as emoçoes de Ekman, foi necessário fazer a coleta desses dados. Dessa forma, o conjunto de treinamento foi criado coletando tweets que contivessem hashtags relacionadas às emoções de Ekman, como proposto por (Go et al. 2009), (Nodarakis et al. 2016) e (Kouloumpis et al. 2011), que mostram como, na maioria das vezes, as hashtags utilizadas refletem o sentimento predominante na respectiva postagem.

Conjunto de tweets referentes à pandemia de Covid-19¶

Para o conjunto de dados referente à pandemia de Covid-19, o qual se pretende analisar, foram selecionados tweets que contivessem os termos "isolamento", "quarentena", "covid", "corona", "coronavirus", "corona virus", "covid-19", "covid19" e/ou "covid 19".

Preparação dos dados¶

Nesta fase, é removido o "ruído" do texto. Assim, foram retirados dos textos de ambos os datasets, os emojis, URLS, citações a outros perfis, hashtags e stopwords.

Para o conjunto de treinamento, foram ainda removidas as palavras que coincidiam com o rótulo, para evitar que o modelo criasse overfitting devido à citação da própria classificação dentro do texto. Por exemplo, um tweet cujo conteúdo fosse "estou muito feliz hoje" seria obviamente classificado como feliz, enviesando o modelo.

Modelagem¶

Durante a execução do PIBIC, o conjunto de treinamento foi balanceado para que o modelo não desse mais peso a uma classe que às demais. Neste notebook, o experimento consiste em conseguir treinar um modelo de performance igual ou superior ao modelo desenvolvido durante a IC, mas sem balancear o conjunto de treinamento.

O primeiro passo é a importação das bibliotecas necessárias:

In [1]:
import pandas as pd
import numpy as np
import spacy
import os
import pickle
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import LinearSVC
from sklearn.metrics import classification_report
from spacy.cli.download import download

Como os modelos de machine learning aceitam apenas entradas numéricas, é necessário o uso de alguma técnica que codifique o texto em algum tipo de representação numérica. Para isso o uso de sentence embeddings desempenha um papel essencial. Em termos gerais, as frases são convertidas em representações vetoriais.

Para realizar essa codificação, foi feito uso do modelo "pt_core_news_lg" da biblioteca SpaCy.

In [1]:
def criar_embeddings(df_tweets):
    try:
        nlp = spacy.load('pt_core_news_lg')
    except (IOError, OSError):
        download('pt_core_news_lg')
        nlp = spacy.load('pt_core_news_lg')
    # desativamos todos os outros pipes que vem com o modelo nlp porque não preicsaremos deles
    with nlp.disable_pipes():
        # transformamos cada tweet em um vetor e colocamos em uma array
        print('Fazendo os word embeddings')
        vetores = np.array([nlp(texto).vector for texto in df_tweets.Texto])

    return vetores


def ler_modelo(path: str):
    return pickle.load(open(path, 'rb'))


def salvar_modelo(path: str, modelo):
    return pickle.dump(modelo, open(path, 'wb'))    


def fazer_amostragem(train_dataset: pd.DataFrame):
    """

    :param train_dataset:
    :return:
    """
    sentimentos = train_dataset['Sentimento'].unique()
    df = pd.DataFrame([])

    for sentimento in sentimentos:
        df_filtrado = train_dataset.loc[train_dataset['Sentimento'] == sentimento][:600]
        df = pd.concat([df, df_filtrado])

    return df

O conjunto de dados textuais rotulados com as emoções de Ekman é carregado e dividido em subconjuntos de treinamento e de teste.

In [3]:
path_datasets = '../resources/datasets'

# importação dos dados
df_treinamento = pd.read_csv(
    f'{path_datasets}/tweets_ekman.csv',
    usecols=['Texto', 'Sentimento']
).dropna()

x_train, x_test, y_train, y_test = train_test_split(
    df_treinamento,
    df_treinamento['Sentimento'],
    test_size=0.2,
    random_state=42
)

Os embeddings são treinados e salvos, ou lidos, caso já exista um arquivo salvo com os embeddings.

In [4]:
# processamento dos dados para word embeddings
path_embeddings_treinamento = '../resources/modelos/embeddings_treinamento.pkl'
path_embeddings_teste = '../resources/modelos/embeddings_teste.pkl'

if os.path.exists(path_embeddings_treinamento):
    embeddings = ler_modelo(path_embeddings_treinamento)
else:
    embeddings = criar_embeddings(x_train)
    salvar_modelo(path_embeddings_treinamento, embeddings)

if os.path.exists(path_embeddings_teste):
    embeddings_teste = ler_modelo(path_embeddings_teste)
else:
    embeddings_teste = criar_embeddings(x_test)
    salvar_modelo(path_embeddings_teste, embeddings_teste)

Os modelos são treinados e salvos, ou lidos, caso já exista um arquivo salvo.

São treinados 3 algoritmos de machine learning: LinearSVC, Logistic Regression e Random Forest. Os três são bem utilizados na literatura para a tarefa de classificação de emoções.

Os embeddings criados anteriormente serão utilizados como entrada de cada um dos algoritmos. Ao fim serão comparados seus respectivos desempenhos.

In [5]:
path_svc = '../resources/modelos/svc.pkl'
path_lgr = '../resources/modelos/logistic_regression.pkl'
path_forest = '../resources/modelos/forest.pkl'

if os.path.exists(path_svc):
    svc = ler_modelo(path_svc)
else:
    svc = LinearSVC(C=100, random_state=0, dual=True, max_iter=10000)
    svc.fit(embeddings, y_train)
    salvar_modelo(path_svc, svc)
    
if os.path.exists(path_lgr):
    lgr = ler_modelo(path_lgr)
else:
    lgr = LogisticRegression(random_state=0)
    lgr.fit(embeddings, y_train)
    salvar_modelo(path_lgr, lgr)

if os.path.exists(path_forest):
    forest = ler_modelo(path_forest)
else:
    forest = RandomForestClassifier(random_state=0, n_jobs=-1)
    forest.fit(embeddings, y_train)
    salvar_modelo(path_forest, forest)
In [6]:
# previsões
previsoes_svc = svc.predict(embeddings_teste)
previsoes_lgr = lgr.predict(embeddings_teste)
previsoes_forest = forest.predict(embeddings_teste)

Avaliação dos modelos¶

Para avaliar os modelos, utilizamos as métricas de precision, recall e f1-score. Segue uma breve explicação sobre cada uma das medidas:

Recall é a proporção de instâncias positivas corretamente classificadas como positivas em relação ao total de instâncias verdadeiras. É uma medida útil quando o custo de uma falso negativo é alto. Ou seja, quando é importante detectar todas as instâncias positivas.

Precision é a proporação de instâncias positivas corretamente indentificadas em relação ao total de instâncias classificadas como positivas (verdadeiras e falsas positivas). É uma medida importante quando o custo de um falso positivo é alto. Ou seja, quando queremos ter certeza que uma instância positiva é realmente positiva.

F1-score é a média harmônica entre recall e precision, é uma medida que equilibra as duas anteriores. É uma medida útil quando há desbalanceamento no conjunto de dados.

In [7]:
# testar performance
print('Relatório Linear SVC')
print(classification_report(y_test, previsoes_svc))
Relatório Linear SVC
              precision    recall  f1-score   support

      Neutro       0.96      0.94      0.95       495
       feliz       0.74      0.45      0.56      1931
        medo       0.35      0.83      0.50      2320
        nojo       0.50      0.24      0.32      1648
       raiva       0.13      0.18      0.16       562
      triste       0.45      0.08      0.14      2097

    accuracy                           0.43      9053
   macro avg       0.52      0.45      0.44      9053
weighted avg       0.50      0.43      0.40      9053
In [8]:
print('Relatório Logistic Regression')
print(classification_report(y_test, previsoes_lgr))
Relatório Logistic Regression
              precision    recall  f1-score   support

      Neutro       0.97      0.94      0.96       495
       feliz       0.71      0.73      0.72      1931
        medo       0.54      0.61      0.57      2320
        nojo       0.61      0.63      0.62      1648
       raiva       0.45      0.12      0.19       562
      triste       0.52      0.53      0.53      2097

    accuracy                           0.61      9053
   macro avg       0.63      0.59      0.60      9053
weighted avg       0.60      0.61      0.60      9053
In [9]:
print('Relatório Random Forest')
print(classification_report(y_test, previsoes_forest))
Relatório Random Forest
              precision    recall  f1-score   support

      Neutro       0.95      0.87      0.91       495
       feliz       0.63      0.66      0.64      1931
        medo       0.45      0.56      0.50      2320
        nojo       0.48      0.43      0.46      1648
       raiva       0.96      0.05      0.09       562
      triste       0.45      0.47      0.46      2097

    accuracy                           0.52      9053
   macro avg       0.66      0.51      0.51      9053
weighted avg       0.55      0.52      0.51      9053

Podemos ver que o modelo de Logisti Regression obteve uma melhor métrica geral em comparação com os demais modelos. Atingindo 61% da média geral. Notamos, porém, que os três modelos tiveram baixa performance na previsão da classe "raiva".

Também podemos usar cross-validation para verificar o desempenho dos modelos. O cross-validation vai dividir o conjunto de dados em 5 partes, na qual 4 partes serão utilizadas para treinamento, enquanto que a restante será usada para teste. O processo é repetido continuamente, sempre utilizando diferentes partes para treinamento e teste. Ao fim, calculamos a média para as 5 interações.

In [10]:
pontuacoes_logreg = cross_val_score(lgr, embeddings_teste, y_test, cv=5, n_jobs=-1, scoring='f1_weighted')
pontuacoes_svc = cross_val_score(svc, embeddings_teste, y_test, cv=5, n_jobs=-1, scoring='f1_weighted')
pontuacoes_forest = cross_val_score(forest, embeddings_teste, y_test, cv=5, n_jobs=-1, scoring='f1_weighted');
In [11]:
def exibir_pontuacoes(pontuacoes):
    soma_ponuacoes = 0

    for valor in pontuacoes:
        soma_ponuacoes += valor
        
    media = soma_ponuacoes / len(pontuacoes)
    
    print(f'Lista de pontuações: {pontuacoes}\nMédia: {media}' )
    
exibir_pontuacoes(pontuacoes_logreg)
exibir_pontuacoes(pontuacoes_svc)
exibir_pontuacoes(pontuacoes_forest)
Lista de pontuações: [0.56582297 0.58788242 0.574865   0.5794129  0.56741404]
Média: 0.5750794670048374
Lista de pontuações: [0.44221795 0.51446641 0.35366093 0.47581766 0.47062506]
Média: 0.45135760052701085
Lista de pontuações: [0.47940858 0.49138109 0.46536405 0.46713247 0.48691761]
Média: 0.4780407583044698

O modelo Logistic Regression obteve o melhor desempenho dentre os modelos, enquanto que o LinearSVC e o Random Forest tiveram desempenhos semelhantes. No entanto, nenhum deles se mostrou satisfatório, atingindo menos de 60% de f1-score em cross-validation. De acordo com os relatórios, os três modelos tiveram baixo desempenho na classe "raiva". Outras medidas devem ser tomadas para tentar melhorar o desempenho.

Próximas etapas¶

Experimentar ajustar os parâmetros dos modelos para verificar se há uma melhora no desempenho de previsão da classe "raiva".