Decifrando Emoções, O Poder da Análise de Sentimentos com NLP (Uma Abordagem Prática)

Ciência de Dados
NLP
Projeto Prático
Author

SergioJr

Published

January 11, 2024

Psicologia? Não, Ciência de dados!
Você sabe a importância e o poder que a análise de sentimentos com Processamento de Linguagem Natural tem nos dias de hoje? Ainda não? Escrevi um artigo para que possamos entender melhor como funciona esse mundo fascinante e como podemos ajudar pessoas e até salvar vidas com com PLN! Agora de uma maneira prática.

Decifrando Emoções, O Poder da Análise de Sentimentos com NLP (Uma Abordagem Prática)

Bem, no final de 2023 eu acabei dando uma pequena pausa nos artigos para criar esse blog onde eu pudesse ter uma liberdade melhor e poder entregar um conteúdo de melhor qualidade para vocês, porém chegou a hora de voltar aos trabalhos e começar a escrever os artigos que deixei apenas no “blueprint”. Um deles era esse, eu tinha começado uma série de 2 artigos sobre análise de sentimentos com NLP, e na época, eu prometi que faria um artigo mostrando na prática passo a passo como fazer uma analise de sentimentos, e aqui estamos!

No outro artigo (se você ainda não o leu, clique aqui) nós tivemos uma breve introdução ao mundo do processamento de linguagem natural, análise de sentimentos e a sua importância no mundo e sociedade atuais e num futuro próximo. Agora veremos uma abordagem prática e a aplicação da análise de dados de uma forma fácil e simples.

Antes de tudo, gostaria de deixar claro que meu objetivo com esse artigo em específico é apenas demonstrar algo, e não ensinar. Num futuro próximo, com mais conhecimento, farei artigos que possam ensinar algo.

Os vetores de palavras

Vetores de palavras, também chamados de Word Embeddings são uma maneira de representar palavras como números. Cada palavra é transformada em um vetor numérico, o que ajuda os computadores a entenderem e trabalharem com o significado das palavras (lembra do meu artigo sobre Machine Learning quando eu disse que eles só entendem números?). Esses vetores capturam relações semânticas entre as palavras, permitindo que os algoritmos compreendam melhor o contexto e as associações linguísticas. Essa abordagem facilita tarefas como análise de sentimentos, tradução automática e outras aplicações relacionadas ao processamento de texto.

Como exemplo, podemos considerar 3 palavras simples: “gato”, “cachorro” e “peixe”. Em um modelo de vetor de palavras, cada uma dessas palavras seria representada por um vetor numérico, onde as posições no vetor capturam características semânticas. Por exemplo:

  • “gato” pode ser representado como [0.8, 0.2, -0.5],
  • “cachorro” como [-0.6, 0.9, 0.3],
  • “peixe” como [0.1, -0.7, 0.9].

Esses vetores foram escolhidos de forma a capturar relações semânticas entre os animais. Nota-se que palavras semelhantes tendem a ter vetores mais próximos no espaço numérico. Se aplicarmos operações matemáticas nesses vetores, como subtração ou adição, podemos explorar relações semânticas, como “cachorro - gato” resultando em algo próximo de “animal de estimação”.

Continuando com os exemplos encima dos vetores de palavras, veremos um pouco da próximidade semântica de vetores. A distância (ou proximidade) semântica de vetores pode ser calculada usando uma métrica como a distância euclidiana. Por exemplos a distância entre “gato” e “cachorro”, suponha que seja de 1.5, indicando que os vetores estão relativamente próximos, ou então, a distância entre “gato” e “peixe”: Digamos que seja 1.8, indicando uma proximidade menor. Esses números representam a proximidade relativa no espaço vetorial. Em um modelo bem treinado, palavras semanticamente relacionadas terão vetores mais próximos, refletindo a proximidade de significado. Essa proximidade é fundamental para tarefas como encontrar sinônimos, identificar relações entre palavras e melhorar a compreensão semântica em geral.

Antes de fazermos a análise de sentimentos, teremos que entender o básico de semântica e vetorização de palavras:

Um pouco de Semântica

semântica é o estudo do significado, ou seja, como as palavras, frases e expressões carregam seu sentido. Existem duas principais categorias de semântica: semântica lexical, que trabalha com o significado de palavras individuais, e semântica composicional, que lida como o significado é construído através da combinação de palavras em frases e textos.

Semântica Lexical

Na semãntica lexical temos a denotação, a qual se refere ao significado literal ou objetivo de uma palavra. Por exemplo, “sol” denota a estrela central do sistema solar. E também temos a conotação, que envolve associações subjetivas e sentimentos que uma palavra pode evocar. Por exemplo, “laranja” pode conotar vitalidade e energia.

Semântica Composicional

Já na semântica composicional temos 3 itens principais, a sinonímia e antonímia, polissemia e a ambiguidade.

Sinônimos são palavras com significados semelhantes, enquanto antônimos têm significados opostos. Por exemplo, “rápido” e “veloz” são sinônimos; “grande” e “pequeno” são antônimos.

A polissemia se trata da situação em que uma palavra tem múltiplos significados relacionados. Por exemplo, “braço” pode significar a parte do corpo humano ou um apoio de cadeira.

Por último temos a ambiguidade que ocorre quando uma palavra ou expressão pode ter interpretações diferentes. Por exemplo, a frase “Vi um homem na colina com um telescópio” pode ser ambígua, pois não está claro se o homem ou o telescópio está na colina.

Entender a semântica é crucial para que as máquinas compreendam o significado das palavras e frases. Modelos de linguagem, como os vetores de palavras mencionados anteriormente, são desenvolvidos para capturar relações semânticas entre palavras.

Vetores de palavras em Python

Para trabalharmos com vetores de palavras (word embeddings) em python, usarei uma biblioteca chamada SpaCy, que serve para processamento avançado de linguagem natural. O SpaCy disponibiliza alguns bons modelos de linguagem em vários idiomas, os quais nos oferecem vocabulário, sintaxe, entidades e vetores (apenas nos modelos maiores).

Os modelos (em inglês) oferecidos pelo SpaCy são:

  • en_core_web_sm, é o menor de todos, oferece apenas vocabulário, sintaxe, entidades.
  • en_core_web_md é um modelo de tamanho médio, contendo 685 mil keys e 20 mil vetores únicos (300 dimensões).
  • en_core_web_lg é o modelo de tamanho grande, contém 685 mil chaves e 685 mil vetores únicos (300 dimensões).

O Spacy ainda tem um modelo maior ainda (en_vectors_web_lg) o qual disponibiliza cerca de 1.1 milhão de chaves e 1.1 milhão de vetores únicos. Porém, para nosso objetivo de análise de vetores, o modelo en_core_web_md será mais que suficiente.

Olá vetor!

Antes de vermos como se parece uma palavra em formato de vetor, temos que importar o SpaCy e carregar o nosso modelo escolhido.

import spacy
nlp = spacy.load('en_core_web_md')

dica: Não se esqueça do “python -m spacy download en_core_web_md” 😉

Agora, vamos ver como se parece a palavra “lion” de forma vetorizada:

nlp(u'lion').vector
array([  1.2746  ,   0.46242 ,  -1.1829  ,  -5.2661  ,  -2.7128  ,
         1.8521  ,  -0.94273 ,   2.1865  ,   6.503   ,   0.6704  ,
         1.5361  ,   2.5992  ,  -0.36233 ,   4.3965  ,  -6.5644  ,
         1.6141  ,  -1.2897  ,   2.1184  ,  -0.63654 ,  -3.4572  ,
        -4.3771  ,   4.2074  ,  -3.6411  ,  -0.97214 ,   1.3253  ,
        -2.3125  ,  -3.6531  ,  -2.8398  ,   2.7913  ,  -1.53    ,
        -2.9984  ,  -2.6357  ,   0.50615 ,  -2.6925  ,   4.3401  ,
        -5.6017  ,   0.045691,   4.3832  ,  -0.19535 ,  -1.0751  ,
         0.32172 ,   2.4395  ,   4.6638  ,   3.4471  ,  -3.3847  ,
        -1.8238  ,   0.70212 ,   0.58557 ,   5.0032  ,  -3.1072  ,
         1.2364  ,   7.4595  ,   0.057368,   1.0111  ,  -1.0827  ,
         0.69113 ,   2.8009  ,  -3.4383  ,  -1.0599  ,  -2.2627  ,
        -5.149   ,  -5.0636  ,   3.1405  ,   1.0793  ,  -0.72892 ,
        -3.9939  ,  -0.69551 ,  -0.55767 ,   3.2555  ,  -2.9449  ,
         4.7114  ,   1.6388  ,   1.3828  ,   1.4255  ,  -3.2334  ,
        -2.274   ,  -1.8136  ,   2.2966  ,   2.5462  ,   1.0722  ,
        -0.73447 ,   1.2148  ,  -0.9196  ,  -0.065012,   2.088   ,
         0.57002 ,   3.5746  ,   1.7192  ,  -8.335   ,   0.71079 ,
         0.91314 ,  -5.0107  ,   1.899   ,  -4.4658  ,   4.7993  ,
        -0.39899 ,  -2.673   ,  -2.9354  ,   4.304   ,   1.4336  ,
         3.7121  ,   0.34882 ,   4.6512  ,  -4.5731  ,  -4.5665  ,
         1.5988  ,  -0.50383 ,   0.95857 ,   0.68728 ,  -0.39976 ,
        -3.1922  ,   4.4363  ,  -0.69479 ,  -1.9528  ,   4.9376  ,
         2.7259  ,   2.2485  ,   5.5734  ,   2.5842  ,   4.7836  ,
        -1.0274  ,   2.2703  ,  -2.0696  ,  -1.0642  ,  -4.932   ,
        -2.274   ,   4.1409  ,   0.73313 ,   2.1889  ,  -0.098888,
         1.6472  ,  -2.3985  ,   2.5911  ,   3.6026  ,   1.885   ,
         5.7822  ,  -1.4481  ,   1.8914  , -10.044   ,  -5.7452  ,
        -4.3224  ,  -3.854   ,   2.3084  ,  -0.84018 ,  -0.40526 ,
         4.7741  ,  -2.3271  ,   7.064   ,   0.95753 ,  -2.356   ,
         0.83953 ,   0.40004 ,   0.33743 ,   0.8376  ,   3.9285  ,
         0.05955 ,   2.4422  ,   4.3492  ,   3.9861  ,   2.1043  ,
        -1.0197  ,  -0.61752 ,  -0.42999 ,  -0.1014  ,  -5.9571  ,
        -0.53818 ,  -1.7797  ,   1.7446  ,   2.3934  ,  -0.50263 ,
        -1.6222  ,  -0.37372 ,  -6.8938  ,   0.55018 ,  -2.267   ,
         0.64912 ,   3.1525  ,  -2.2541  ,  -4.0384  ,   3.206   ,
         0.14962 ,  -2.6662  ,   0.18167 ,   5.0028  ,   2.1521  ,
         0.92419 ,   5.4163  ,  -2.2408  ,   1.6585  ,  -5.1625  ,
         5.029   ,   0.1026  ,  -0.44542 ,   2.0557  ,   3.7778  ,
         3.8679  ,  -2.7135  ,   5.3242  ,  -3.2916  ,   5.6421  ,
         5.0466  ,   1.6072  ,  -1.3206  ,   4.2044  ,  -0.33793 ,
        -3.1139  ,   2.8841  ,  -3.1565  ,  -2.9832  ,  -0.23235 ,
         2.3259  ,   3.5477  ,  -2.1299  ,  -1.8344  ,   2.7271  ,
         1.5568  ,   5.6865  ,   0.9412  ,  -2.6412  ,  -5.3254  ,
         1.3494  ,  -0.47159 ,   2.4979  ,  -1.5568  ,  -1.6911  ,
        -2.1842  ,   6.0319  ,   0.022573,   2.3824  ,  -1.1002  ,
         0.90216 ,  -1.9113  ,   1.5527  ,   5.7413  ,  -3.1956  ,
         0.68655 ,  -1.6068  ,   1.7404  ,  -3.2142  ,   6.4783  ,
         1.7548  ,  -2.9795  ,   0.97631 ,  -0.018354,  -0.6379  ,
         0.80559 ,   3.1923  ,   3.3335  ,   4.3068  ,  -1.0819  ,
        -1.3839  ,  -4.7626  ,  -4.6637  ,  -1.2201  ,  -3.2741  ,
         1.5204  ,   0.78119 ,   8.7339  ,   1.6009  ,  -0.79332 ,
         5.8416  ,  -1.485   ,   1.5978  ,   2.9746  ,  -0.30759 ,
        -1.8023  ,  -4.8344  ,   1.2817  ,  -2.5469  ,   2.6517  ,
         1.4881  ,   2.1952  ,  -0.12652 ,   1.2223  ,   0.44763 ,
        -3.1445  ,  -2.2051  ,  -4.1785  ,  -3.6539  ,   5.1929  ,
         0.78457 ,  -1.2312  ,   5.5624  ,  -1.8462  ,   6.1262  ,
        -1.6653  ,  -2.7557  ,  -0.066465,  -3.6362  ,   5.2005  ,
        -1.2865  ,   2.8855  ,   6.1219  ,   1.7824  ,   1.4264  ,
        10.628   ,  -0.36028 ,   1.9268  ,  -7.835   ,   0.57865 ],
      dtype=float32)

Sim, é um vetor bem grande para 1 palavra apenas. Porém cada valor representa a posição da palavra “lion” em um espaço vetorial, cada dimensão do vetor pode capturar diferentes características ou contextos associados à palavra “lion”, e nesse caso, temos 300 dimensões. Interessante não é mesmo?

Vejamos essa mesma aplicação porém para uma frase:

doc = nlp(u'The quick brown fox jumped over the lazy dogs.')

doc.vector
array([-1.7769655 ,  0.39714497, -1.695121  , -0.1089559 ,  3.861494  ,
       -0.10778303, -0.02750097,  3.191314  ,  1.0857747 , -0.2615487 ,
        4.0720797 ,  1.5932049 , -2.7569218 ,  0.70982707,  2.0976841 ,
        0.08150103,  0.8847861 , -0.505237  ,  0.767067  , -2.88911   ,
       -0.28514975, -0.331664  ,  0.306348  , -2.25347   ,  0.96798134,
       -0.030282  , -3.765162  , -2.168157  ,  1.3985709 ,  2.175709  ,
       -0.81103534, -0.55156004, -1.033463  , -2.3130198 , -2.892054  ,
       -2.843568  , -0.33247897,  1.620013  ,  3.03307   , -0.42730814,
        1.298548  ,  0.18969259,  1.234282  , -0.14263602, -1.427765  ,
       -0.05807757,  0.33836406, -1.6987331 , -2.13661   ,  0.10412004,
        0.62479395,  3.9712129 , -0.31110606, -1.9676571 , -0.11860895,
        0.55582994, -0.660888  ,  1.947435  ,  1.6391805 ,  0.6569032 ,
        0.054408  , -2.08993   ,  1.0370519 ,  0.5363236 ,  0.00807395,
       -0.91060096, -3.3870788 , -1.4823462 ,  1.4170542 ,  0.32670596,
        0.602952  , -1.047483  , -0.3633799 , -0.09132097,  0.722465  ,
       -0.74786097, -1.8563267 ,  0.827946  , -0.99609107, -0.3298212 ,
       -3.472247  , -0.94788504, -0.46503416,  0.6626488 ,  2.5601602 ,
       -1.2395241 ,  0.61391175, -0.61511296, -1.459838  ,  0.14399411,
       -1.1767578 , -0.10416207,  1.6930513 , -1.6688293 ,  0.26341552,
       -0.84874   ,  1.5149789 ,  0.03238799,  1.768853  ,  0.42900094,
        1.43253   , -0.53126115,  2.916109  ,  1.7515122 ,  0.62033385,
        1.7113819 , -1.5602219 ,  1.0098228 , -1.277114  , -0.816329  ,
       -0.28804332,  0.0696778 , -1.0609343 ,  0.960142  ,  1.765322  ,
       -0.18756203,  0.75897396, -0.64144975,  1.7536061 ,  0.10283799,
       -0.31856102, -1.091177  , -1.6496038 ,  1.657584  , -1.5179831 ,
       -1.886636  ,  1.6479073 , -1.196546  ,  3.3722177 , -0.22696403,
       -3.203148  , -1.708502  ,  2.9725442 , -0.320748  ,  0.466757  ,
        1.6034908 , -1.6832211 , -2.80418   ,  1.4318919 , -1.0059    ,
       -2.9437232 , -1.430186  , -1.3512889 , -0.09892895, -0.38347203,
        1.071111  , -1.02584   ,  0.30393195,  1.5452302 ,  0.28571904,
        0.10674398,  2.7844248 ,  0.43838865, -0.21123195, -0.69085443,
        0.04283998,  2.3449368 , -0.8889019 , -0.2773927 ,  0.3125515 ,
        0.264121  , -2.649219  ,  0.33148596,  0.39187098, -2.2444918 ,
       -1.1317899 , -1.935579  ,  0.52987385,  0.303826  ,  1.681892  ,
        1.0111669 ,  1.5380409 ,  1.7308229 , -0.080899  , -0.892347  ,
       -0.23734598, -0.21603405, -0.69041294, -1.6095406 , -0.8730756 ,
       -1.5468609 , -0.17627892, -0.1907803 ,  0.17952934, -0.803991  ,
       -0.03223496, -1.9790027 , -1.6414549 ,  0.20527199, -1.104895  ,
        0.24875899,  1.186315  , -0.8186741 , -2.410284  ,  0.5833171 ,
       -0.784715  , -2.627035  , -0.15004005, -1.4183891 ,  0.442288  ,
       -0.62965304, -1.113676  , -0.46552286, -1.651365  ,  1.8519121 ,
        1.1279902 , -3.2509658 ,  0.04463506,  0.11902802,  0.68590844,
        0.77328   ,  1.3373908 , -1.7096472 , -0.5530244 , -0.02548897,
        1.19019   ,  0.10446696, -1.351313  , -1.7751659 , -0.44060117,
       -1.4011772 ,  0.9672308 , -0.97586805,  1.2529299 , -1.9867659 ,
       -0.7944795 ,  0.6362045 ,  2.010871  ,  3.6185539 ,  0.62658596,
        0.9231445 , -3.4679253 ,  0.42413408,  1.219738  , -0.0507701 ,
        2.0426223 , -1.5670619 ,  1.5156981 , -0.09616538,  1.6220791 ,
        0.775658  , -1.8741112 ,  2.0503078 , -0.7602806 , -1.442644  ,
        1.69713   , -1.6083733 , -0.278282  , -1.431804  ,  0.60193396,
       -0.51194596, -2.1616828 , -4.4544115 , -0.3637256 , -0.98130643,
       -2.1879544 ,  1.471617  ,  3.2054775 , -0.562816  , -1.099867  ,
        2.353531  ,  1.6450421 ,  2.8005474 ,  1.1693535 ,  0.086842  ,
       -2.5300715 , -1.5670309 ,  0.31365603, -2.450636  ,  1.4588621 ,
        1.24279   ,  0.08502197,  1.06439   , -0.86917496,  0.32117197,
       -0.909351  ,  0.52400905, -1.35512   , -0.72316396,  1.9528949 ,
        0.49172202,  0.4663611 , -0.43013096, -0.09305   ,  1.265938  ,
       -1.251461  , -0.85874796, -0.2679918 , -0.173774  ,  0.56781405,
       -0.5377843 ,  0.871497  ,  2.4677212 ,  0.6728541 , -0.97043693,
        2.501867  , -0.572828  ,  0.504657  , -3.4507267 ,  0.45634192],
      dtype=float32)

A ideia por trás das incorporações de palavras (ou word embedding) é mapear palavras para vetores contínuos em um espaço n-dimensional (nesse caso 300), onde palavras semelhantes têm representações vetoriais mais próximas. Isso nos possibilita omparar semelhanças entre palavras, frases e até mesmo documentos inteiros.

Identificando vetores similares

A melhor maneira de expor a relação entre vetores é usando o método dos tokens .similarity(). Para isso criaremos um objeto DOC de 3 tokens:

tokens = nlp(u'lion cat pet')

Agora iremos por meio de combinações de tokens.

for token1 in tokens:
    for token2 in tokens:
        similarity_percentage = token1.similarity(token2) * 100
        print(f"{token1.text} -> {token2.text} = {similarity_percentage:.2f}%")
lion -> lion = 100.00%
lion -> cat = 38.55%
lion -> pet = 20.03%
cat -> lion = 38.55%
cat -> cat = 100.00%
cat -> pet = 73.30%
pet -> lion = 20.03%
pet -> cat = 73.30%
pet -> pet = 100.00%

Note que a ordem não importa. token1.similarity(token2) teria o mesmo resultado que token2.similarity(token1).

Voltando para a nossa análise de similaridade, podemos notar que o token lion em relação a ele mesmo (lion -> lion) tem uma similaridade de 100%, afinal é ela mesma. Agora observe a relação entre “lion” e “cat”, eles tem uma similaridade de 38.55%! A alta similaridade entre as palavras “lion” e “cat” pode ser explicada pela natureza semântica e taxonômica das palavras no contexto do treinamento do modelo que utilizamos. Em nosso caso, “Lion” e “Cat” ambos são animais e compartilham características semelhantes. Eles pertencem à mesma categoria taxonômica (felídeos), têm algumas características físicas em comum e podem aparecer em contextos semelhantes em textos relacionados à fauna, biologia ou em descrições cotidianas.

Outras palavras que tiveram uma boa relação foram as “Cat” e “Pet”, acho que eu nem preciso explicar o motivo, correto?

A similaridade entre palavras é calculada com base na proximidade dos vetores de palavra no espaço vetorial. Se as palavras têm contextos semelhantes, seus vetores no espaço tendem a ser mais próximos, resultando em uma alta pontuação de similaridade.

Opostos, mas não diferentes

As palavras que tem significados opostos não são necessariamente diferentes por aqui. Isso acontece pois apesar de serem opostas, elas aparecem no mesmo contexto. Vejamos:

tokens = nlp(u'like love hate')

for token1 in tokens:
    for token2 in tokens:
        similarity_percentage = token1.similarity(token2) * 100
        print(f"{token1.text} -> {token2.text} = {similarity_percentage:.2f}%")
like -> like = 100.00%
like -> love = 52.13%
like -> hate = 50.65%
love -> like = 52.13%
love -> love = 100.00%
love -> hate = 57.08%
hate -> like = 50.65%
hate -> love = 57.08%
hate -> hate = 100.00%

As 3 palavras podem aparecer no mesmo contexto correto? Por isso todas tiveram uma pontuação acima de 50%!

Aritmética vetorial

Para fechar esse assunto de vetores, veremos algo muito interessante que podemos fazer com nossos vetores, acredite se quiser, podemos calcular novos vetores apenas decrescendo ou acrescendo vetores que tem alguma relação. Temos o exemplo da rainha que é bem famoso:

“king” - “man” + “woman” = “queen”

Rei - Homem + Mulher = Rainha. Ou seja, se tirarmos o vetor “Homem” do vetor “Rei” e adicionarmos o vetor “Mulher”, teremos de volta o vetor “Rainha”. Ainda não acredita em mim? Veja:

from scipy import spatial

cosine_similarity = lambda x, y: 1 - spatial.distance.cosine(x, y)

king = nlp.vocab['king'].vector
man = nlp.vocab['man'].vector
woman = nlp.vocab['woman'].vector


new_vector = king - man + woman
computed_similarities = []

for word in nlp.vocab:
    if word.has_vector:
        if word.is_lower:
            if word.is_alpha:
                similarity = cosine_similarity(new_vector, word.vector)
                computed_similarities.append((word, similarity))

computed_similarities = sorted(computed_similarities, key=lambda item: -item[1])

print([w[0].text for w in computed_similarities[:10]])
['king', 'queen', 'the', 'and', 'that', 'havin', 'where', 'she', 'they', 'woman']

É um código grande e eu posso explicar. Primeiro calculamos a similaridade do cosseno entre dois vetores ‘x’ e ‘y’ e guardamos essa função em cosine_similarity. Depois guardamos os vetores das palavras “King”, “Man” e “Woman”. Feito isso, podemos encontrar o vetor mais perto no vocabulário para o resultado de “‘king’ - ‘man’ + ‘woman’”, que no caso é “Queen”! No bloco for temos 3 verificações que fazem um simples filtro, o primeiro deles verifica se a palavra tem um vetor, o segundo verifica se tem letras maiúsculas (queremos apenas minúsculas) e por último, verificamos se é alfadecimal.

E como podem ver, a palavra mais próxima de “King” agora é “Queen”, sendo “King” a nossa palavra inicial e “Queen” a palavra que conseguimos por meio da aritmética vetorial!

Análise Sentimental

Agora que conhecemos os vetores (mesmo de uma forma básica), podemos fazer uma simples análise sentimental. O objetivo é encontrar pontos em comum entre documentos, com o entendimento de que vetores combinados de forma semelhante devem corresponder a sentimentos semelhantes.

VADER

Para isso usaremos um módulo chamado VADER da biblioteca NLTK (outra famosa biblioteca para processamento de linguagem natural avançada). Esse módulo fornece pontuações de sentimento com base nas palavras usada (“completamente” aumenta a pontuação, enquanto “ligeiramente” a reduz), em letras maiúsculas e pontuação (“ÓTIMO!!!” é mais forte que “ótimo”).

Primeiro, temos que importar o NLTK e fazer o download do módulo VADER:

import nltk
nltk.download('vader_lexicon')

Feito isso, importaremos o VADER e o instanciamos:

from nltk.sentiment.vader import SentimentIntensityAnalyzer

sid = SentimentIntensityAnalyzer()

O SentimentIntensityAnalyzer() recebe uma string e retorna um dicionário de pontuações em cada uma das quatro categorias:

  • negativo
  • neutro
  • positivo
  • composto (calculado normalizando as pontuações acima)

Veja:

a = 'This was a good movie.'
sid.polarity_scores(a)
{'neg': 0.0, 'neu': 0.508, 'pos': 0.492, 'compound': 0.4404}

Como dito, fornecemos uma String, e nos é retornado um dicionário com as pontuações de cada categoria, para sabermos o resultado (em qual categoria o VADER encaixou nossa frase), temos que olhar para o ‘compound’ (composto), pois ele é o cálculo normalizado de todas as pontuações. Sendo assim, quanto mais positivo for nosso ‘compound’, mais positiva é nossa frase, quanto mais negativo é o ‘compound’, mais negativa é nossa frase. Fácil não é?

Agora veja essa frase:

a = 'This was the best, most awesome movie EVER MADE!!!'
sid.polarity_scores(a)
{'neg': 0.0, 'neu': 0.425, 'pos': 0.575, 'compound': 0.8877}

Nosso compound chega quase a 100%! Isso se dá pois usamos de pontuações, letras maiúsculas, etc. Expressamos de forma mais clara nossos sentimentos e emoções na frase, e assim o VADER conseguiu identificar melhor nosso sentimento de alegria.

Olha como fica com uma frase negativa:

a = 'This was the worst film to ever disgrace the screen.'
sid.polarity_scores(a)
{'neg': 0.477, 'neu': 0.523, 'pos': 0.0, 'compound': -0.8074}

Nosso ‘compound’ fica com o valor negativado. E isso nos diz que essa é uma frase com um sentimento ou emoção negativa. Interessante não é mesmo?

Aplicando VADER em base de dados

Agora que já entendemos e vimos como funciona o VADER de uma forma básica com alguns textos, vamos ver como ele lida com base de dados. Para isso eu tenho uma base de dados de reviews de filmes.

import numpy as np
import pandas as pd
df = pd.read_csv('moviereviews.tsv', sep='\t')
df.head()
    label                                               review
0   neg     how do films like mouse hunt get into theatres...
1   neg     some talented actresses are blessed with a dem...
2   pos     this has been an extraordinary year for austra...
3   pos     according to hollywood movies made in last few...
4   neg     my first press screening of 1998 and already i...

Acima temos nosso dataset, note que temos a label a qual indica se a review é positiva ou negativa, e temos a review em si. Irei fazer uma simples limpeza nesse dataset, começando pelos valores nulos e em seguida, verificarei se existem reviews em branco.

df.dropna(inplace=True)
blanks = []

for i,lb,rv in df.itertuples():
  if type(rv) == str:
    if rv.isspace():
      blanks.append(i)
      
blanks
[57,
 71,
 147,
 151,
 283,
 307,
 313,
 323,
 343,
 351,
 427,
 501,
 633,
 675,
 815,
 851,
 977,
 1079,
 1299,
 1455,
 1493,
 1525,
 1531,
 1763,
 1851,
 1905,
 1993]

Podemos ver que temos muitas reviews em branco… Deletarei todas:

df.drop(blanks, inplace=True)
df['label'].value_counts()
neg    969
pos    969
Name: label, dtype: int64

Agora temos as labels equilibradas com 969 reviews negativas e positivas, e com um dataset limpo. Isso nos permite começar nossa análise sentimental nessas reviews:

import nltk
from nltk.sentiment.vader import SentimentIntensityAnalyzer
nltk.download('vader_lexicon')
sid = SentimentIntensityAnalyzer()

Agora, só nos resta passar as reviews para o VADER, primeiro calcularei as pontuações gerais (de todas as categorias):

df['scores'] = df['review'].apply(lambda review:sid.polarity_scores(review))

Em seguida temos que calcular o composto, pois ele que decidirá qual label será aplicada em nossa review:

df['compound'] = df['scores'].apply(lambda d:d['compound'])

Por último, mas muito importante, definiremos a label, lembra que o VADER nos fornece 3 categorias (tirando o composto)? Eram elas: positiva, negativa e neutra. Aqui usaremos apenas a positiva e negativa de acordo com o composto, se composto for maior que 0 a frase é positiva, se for menor é negativa:

df['comp_score'] = df['compound'].apply(lambda score: 'pos' if score >= 0 else 'neg')

E nosso dataset no final ficará mais ou menos assim:

df.head()

    label                                             review                                              scores    compound    comp_score
0   neg     how do films like mouse hunt get into theatres...   {'neg': 0.121, 'neu': 0.778, 'pos': 0.101, 'co...   -0.9125     neg
1   neg     some talented actresses are blessed with a dem...   {'neg': 0.12, 'neu': 0.775, 'pos': 0.105, 'com...   -0.8618     neg
2   pos     this has been an extraordinary year for austra...   {'neg': 0.068, 'neu': 0.781, 'pos': 0.15, 'com...    0.9951     pos
3   pos     according to hollywood movies made in last few...   {'neg': 0.071, 'neu': 0.782, 'pos': 0.147, 'co...    0.9972     pos
4   neg     my first press screening of 1998 and already i...   {'neg': 0.091, 'neu': 0.817, 'pos': 0.093, 'co...   -0.2484     neg

Dando uma rápida olhada em nosso dataset, podemos ver a coluna ‘label’ com nossos sentimentos reais das reviews, e a coluna ‘comp_score’ que é o sentimento que o VADER nos disse que é o verdadeiro. Vamos avaliar essa classificação feita pelo modelo VADER e provar se ele é eficaz ou não.

from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
accuracy = round(accuracy_score(df['label'], df['comp_score'])*100)

print(f'Acurácia de: {accuracy}%')
Acurácia de: 64%

Nosso modelo obteve uma acurácia geral de 64%, ou seja, de 100% dos sentimentos, ele classificou 64% corretamente. Vamos ver mais métricas para entendermos melhor como foi a performance de nosso modelo:

print(classification_report(df['label'], df['comp_score']))
              precision    recall  f1-score   support

         neg       0.72      0.44      0.55       969
         pos       0.60      0.83      0.70       969

    accuracy                           0.64      1938
   macro avg       0.66      0.64      0.62      1938
weighted avg       0.66      0.64      0.62      1938

O modelo parece ter um desempenho razoável em identificar instâncias da classe positiva (pos), com uma precisão de 60% e recall de 83%. Porém, para a classe negativa (neg), a precisão é mais alta (72%), mas o recall é relativamente baixo (44%). E como foi visto a acurácia global é de 64%, o que significa que o modelo está correto em 64% das predições.

Conclusão

Chegamos ao fim de mais uma aventura! Como foi dito ao começo deste artigo, o mesmo não tem o objetivo de ensinar, mas sim de mostrar algo, mostrar um pouco de como a análise sentimental funciona por de baixo dos panos e uma breve introdução a vetores de palavras.

Espero que tenha sido uma viagem divertida e útil para você! Qualquer dúvida, feedback ou ideias para artigos sinta-se livre para deixar nos comentários!


Obrigado por me acompanhar nesta viagem!
Que tal começarmos outra?
🚀