Word Embedding: fazendo o computador entender o significado das palavras
Uma introdução a conceitos muito importantes em NLP: embeddings e word2vec.
Agradecimentos especiais a Julia Pocciotti e Luísa Mendes Heise que ajudaram a preparar os materiais desse texto.
Olá, caro leitor, seja bem-vindo a mais um Turing Talks! Se você vem acompanhado nossos textos sobre Processamento de Linguagem Natural (PLN/NLP), mal deve esperar pelo de hoje, pois abordaremos um conceito importantíssimo na área: Word Embedding! Além disso, esse será o primeiro texto de uma série sobre Embeddings e Word2vec.
Se você não sabe o que é ou não está muito familiarizado com NLP, recomendamos nosso Turing Talks de Introdução a NLP. Uma lida nele deve ser o suficiente para prosseguir com a leitura de hoje.
Sem mais delongas, vamos para o que nos interessa…
O que é Word Embedding?
No nosso texto de Introdução a Bag of Words e TF-IDF, explicamos que para usar um modelo estatístico ou de deep learning em NLP, precisamos transformar o texto em uma informação numérica, mais especificamente um vetor. Se você não sabe o que é um vetor, para o escopo desse texto, é como se fosse uma lista de valores numéricos ou um array/matriz 1xN.
Lá, apresentamos duas formas de fazer isso: Bag of Words (one-hot vectors ou frequência dos termos) e TF-IDF. No entanto, esses métodos têm algumas limitações. A primeira é que os vetores obtidos terão o mesmo tamanho do vocabulário (conjunto de todas as palavras que ocorrem em um texto), o que se torna um problema quando temos vocabulários muito grandes, e é comum lidarmos com vocabulários de milhares de palavras ou mais. A segunda é que eles não conseguem captar significado entre palavras, isto é, extraem pouca informação semântica e sintática dos textos.
Bem… e de que outra forma poderíamos representar palavras? Uma ideia seria defini-las através de escalas representando alguma informação sobre elas. Por exemplo, poderíamos definir a palavra “rainha” com uma escala “Gênero” que vai de -1 a 1: quanto mais perto de -1, mais feminina a palavra, e quanto mais perto de 1, mais masculina.
Todavia, apenas com informação sobre gênero não conseguimos representar bem uma palavra, então poderíamos adicionar mais uma dimensão com uma escala “Realeza”, em que a palavra tem mais a ver com monarca quanto mais perto de 1 e mais a ver com plebeu quanto mais perto de -1. Assim adicionamos cada vez mais dimensões para definir melhor a palavra, construindo um vetor em que cada dimensão serve como uma forma de capturar um tipo de informação sobre o seu significado, e é nisso que consiste Word Embedding! Essa representação vetorial que acabamos de descrever é chamada de embedding, veja exemplos para as palavras “rei” e “rainha”:
Mas sair atribuindo valores para essas escalas manualmente para TODAS as palavras em um corpus (conjunto de textos) seria muito complicado, certo? Então, como obtemos esses embeddings de fato? Fazemos o computador ‘aprendê-los’, isto é, usamos algum algoritmo de machine learning para gerá-los, a partir de seu contexto. Meio doido né, mas a intuição por trás disso é que o significado de uma palavra está intimamente relacionado às palavras que em geral aparecem junto a ela. Perceba que, assim, Word Embedding supera as duas dificuldades que tínhamos citado anteriormente (tamanho dos embeddings e pouca informação semântica), pois nós que escolhemos quantas dimensões o embedding terá e cada dimensão terá informação semântica. Quanto ao tamanho ideal do vetor, escolhemos de acordo com o corpus de treinamento: quanto menor, menos dimensões; mas geralmente é um valor entre 100 e 1000.
Há vários modos de ‘aprender’ esses vetores, como aplicação de algoritmos de redução de dimensionalidade (PCA, LDA, etc) na matriz de co-ocorrência das palavras, modelos probabilísticos, entre outros. Mas o modo mais popular é treinando redes neurais, como é o caso do famoso Word2vec.
Word2vec
Word2vec é um algoritmo para obter word embeddings treinando uma rede neural rasa (com apenas uma hidden layer) com duas arquiteturas possíveis: CBOW ou Skip-Gram. Como o objetivo desse texto é introduzir o contexto de embeddings, se você não entende muito de redes neurais, não precisa se preocupar com isso agora. Falaremos mais em detalhes sobre Word2vec no próximo texto dessa série. De qualquer maneira, se quiser se aprofundar em RNs, temos textos sobre: #1, #2 e #3.
Suponhamos que a frase “Fui ao supermercado e comprei abacate” esteja no nosso corpus, para exemplificar. Se escolhermos o modelo CBOW, treinaremos uma rede com a tarefa de prever uma palavra dado o contexto, ou seja, completar “fui ao ___ e comprei abacate” corretamente com “supermercado”. Se escolhermos o Skip-Gram, a tarefa da rede será dada uma palavra, prever o contexto, ou seja, a partir de “supermercado”, prever “fui”, “ao”, “e”, “comprei”, “abacate”.
Se você estiver se perguntando: essas tarefas são supervisionadas ou não supervisionadas? Na verdade, dizemos que são ‘self-supervised’, ou auto-supervisionadas, já que a rede aprende por labels, porém não precisamos fazer o labelling, pois elas estão contidas no corpus base.
Com a rede enfim treinada, extraímos os embeddings da matriz de pesos da hidden layer (a dimensão dessa matriz é um hiperparâmetro, nós que definimos, então é aqui que escolhemos o tamanho dos nossos embeddings, diminuindo assim a dimensão em relação ao vocabulário). Por levar em conta o contexto, esse algoritmo geralmente é capaz de gerar vetores com valor semântico, de modo que podemos então usá-los para estabelecer relações entre as palavras: o quão semelhantes elas são, por exemplo. Podemos inclusive plotar esses vetores e visualizar essas relações na prática, como na imagem abaixo:
Similaridade entre vetores
Distância euclidiana
Uma das maneiras mais conhecidas de calcular a semelhança entre vetores é pela distância euclidiana, que é a forma padrão de calcular a distância entre eles. Para entender como fazer o cálculo dessa distância, vamos analisar primeiro o caso 2D:
Com ajuda da imagem, vemos que a distância d(p,q) corresponde ao menor ‘trajeto’ possível entre p e q: o segmento de reta entre os dois pontos; daí, sua fórmula é nada mais nada menos que a aplicação do teorema de Pitágoras.
Para calcular a distância entre vetores de tamanho arbitrário n, generalizamos então essa fórmula:
Obs: p1, p2, …, pn e q1, q2, …, qn são as componentes dos vetores p e q, respectivamente. Por exemplo, p2 é o valor do vetor na linha 2.
Entretanto, para dimensões maiores ou igual a 4, já não é mais possível fazer a visualização da fórmula como no caso 2. Vale lembrar que, no nosso contexto, estaremos sempre comparando vetores de mesmo tamanho (o valor escolhido para a dimensão do embedding).
Dizemos, no entanto, que a distância euclidiana entre embeddings é uma medida de dissimilaridade, ou seja, quanto maior, mais dissimilar, pois esperamos que quanto mais distantes os vetores, menos semelhantes eles sejam.
Para exemplificar, usaremos aqui embeddings treinados e disponibilizados pelo NILC, o Núcleo Interinstitucional de Linguística Computacional, que reúne pesquisadores de NLP de várias universidades brasileiras. Você pode baixá-los aqui. Nesse texto, estamos usando os embeddings gerados via CBOW (word2vec) de 100 dimensões.
Com o arquivo baixado, usamos os embeddings por meio da biblioteca gensim. Para baixá-la, se você já tiver o pip instalado (e se não tiver, recomendo), basta rodar “pip install gensim” no terminal ou uma célula com “!pip install gensim” se estiver usando um jupyter notebook. Depois, rodamos:
Vamos conferir um dos nossos vetores:
Para calcular a distância euclidiana, fazemos:
Obs: a função do numpy linalg.norm( ) com o argumento ord = None (default) calcula a raiz da soma dos quadrados de cada componente do vetor.
Vamos comparar as distâncias d1, entre “mulher” e “pessoa”; d2, entre “mulher” e “homem”; e d3, entre “mulher” e “abacate”:
Vemos que d1 é menor que d2 que é menor que d3, o que faz sentido, pois “mulher” é mais similar a “pessoa” do que a “homem”, mas é mais similar a “homem” do que a “abacate”.
No entanto, se tratando de vetores de palavras, a direção deles é um componente importante para a aquisição do significado. Por isso, é mais interessante saber o ângulo entre dois vetores do que a distância entre eles. Dessa forma, a medida de similaridade mais usada de fato é a similaridade de cossenos.
Similaridade de cossenos
A similaridade de cossenos é um produto interno normalizado, para quem já estudou álgebra linear. Para quem não sabe o que é isso, basta compreender que ela permite calcular o cosseno do ângulo entre dois vetores, valor que varia entre -1 e 1, sendo 0 quando o ângulo for de 90º. A fórmula que usamos é:
Quanto maior o ângulo entre dois vetores (valor entre 0º e 180º), menor o cosseno, ou seja, maior a similaridade.
Usamos o seguinte código para calcular a similaridade de cossenos:
Vamos calcular as mesmas relações que fizemos com a distância euclidiana: s1, a similaridade entre “mulher” e “pessoa”; s2, a similaridade entre “homem” e “mulher”; e s3, a similaridade entre “mulher” e “abacate”.
Agora, s1 é maior que s2 que é maior que s3, o que faz sentido, pois a similaridade de cossenos deve ser maior à medida que as palavras são mais semelhantes.
Analogias
Outra relação interessante que podemos fazer é uma analogia, ou seja, ver que palavra está para palavra x, assim como a palavra y está para a w. O exemplo clássico de uma analogia é: que palavra está para “rei”, assim como “mulher” está para “homem”? Você deve ter prontamente pensado em “rainha”, mas como fazer o computador responder isso? Denominando f(x) como a função de embedding, ou seja, que leva as palavras a sua representação vetorial, uma forma de pensar em uma analogia matematicamente é:
f(“mulher”) - f(“homem”) = f(p) - f(“rei”)
sendo p a palavra que queremos encontrar. Daí,
f(p) = f(“mulher”) - f(“homem”) + f(“rei”)
Ou seja, fazemos a diferença entre os embeddings de “mulher” e “homem” e depois somamos ao de “rei”. Então, procuramos o embedding mais semelhante ao resultado (já que dificilmente o resultado vai dar algo exatamente igual a um dos embeddings). Traduzindo em código:
.most_similar( ) é um método da gensim que retorna os vetores mais similares de acordo com a similaridade de cossenos. Os argumentos positive e negative recebem uma lista com vetores para considerar positivamente e negativamente respectivamente, refletindo a conta que vimos acima.
Vamos conferir alguns exemplos:
Conseguimos identificar corretamente que “filha” está para “filho” assim como “homem” está para “mulher”. Repare também que as demais palavras retornadas são todas femininas e tem relação com família, mostrando como o modelo conseguiu absorver significado nesse caso.
Nesse exemplo, “nadando” (palavra esperada) ficou em segundo lugar, mas ainda é um resultado bom. Além disso, todas as demais palavras são verbos no infinitivo ou gerúndio.
Avaliação
Mas… como saber se nossos embeddings são bons mesmo? Quando fazemos um modelo de machine learning, geralmente usamos métricas como acurácia e F1-score para avaliar seu desempenho, mas com embeddings isso é mais complicado. Nesse caso, temos dois tipos de avaliação: intrínseca e extrínseca.
Em uma avaliação intrínseca, analisamos analogias, similaridades e plots para ver se os embeddings capturaram informações semânticas do jeito esperado, o que já começamos a fazer nas duas últimas seções. Mas, vamos dar uma olhada em mais uma analogia um pouco mais complexa:
Esse resultado já não é tão bom, “rainha” nem chega a aparecer, o que mostra que ainda precisamos melhorar nossos embeddings. Para isso, poderíamos testar aumentar a dimensão dos embeddings ou outro modelo, como o Skipgram.
Para visualizar os embeddings, como estaremos sempre lidando com dimensões bem maiores que 3, é preciso aplicar algoritmos de redução de dimensionalidade para plotá-los, como t-SNE. No entanto, não vamos entrar em detalhes sobre isso nesse texto.
Já a avaliação extrínseca consiste em verificar se os embeddings melhoram a performance na tarefa de interesse. Por exemplo, se quiséssemos fazer uma classificador de sentimentos de reviews de um produto, poderíamos fazer um modelo sem e outro com o uso de embeddings e ver se usá-los de fato melhora as métricas do classificador. Poderíamos fazer o mesmo para comparar modelos de embedding diferentes.
Conclusão
Ufa! Isso é uma introdução, mas foi bastante coisa né? Vimos a ideia por trás dos word embeddings e seu funcionamento básico, bem como seu potencial de agregar informação semântica a tarefas de NLP! Agora é sua vez: para absorver todos esses conceitos, recomendo baixar embeddings e brincar com similaridades, analogias e etc. No repositório do NILC tem vários modelos diferentes que você pode explorar.
No próximo texto dessa série explicaremos mais a fundo o funcionamento do Word2vec, acompanhe nossas redes sociais para não perder! Siga-nos no Facebook, Linkedin, Instagram, Medium e participe do nosso servidor no Discord! E se você curte NLP, dá uma olhadinha nos nossos outros textos sobre que tem muita coisa legal!