Aula 2, FastText

Esta aula trata do FastText, que melhora o Word2Vec ao olhar para dentro das palavras. Em vez de tratar cada palavra como um bloco indivisível, ele a quebra em pedaços de caracteres, o que dá a superpotência de representar palavras nunca vistas, como erros de digitação e flexões raras.

O Word2Vec tem um ponto cego. Ele aprende um vetor para cada palavra do vocabulário, e só para essas. Se aparecer uma palavra que não estava no treino, por exemplo um erro de digitação como corredor escrito corredr, ou uma flexão pouco comum, o modelo não tem o que oferecer, ela é simplesmente desconhecida. Em português, com a sua morfologia rica e tantas conjugações, esse problema é frequente.

O FastText, criado por Bojanowski e colegas na linha do Word2Vec, ataca isso representando cada palavra pela soma dos seus pedaços, os n-gramas de caracteres. Assim, mesmo uma palavra inédita ganha um vetor razoável, montado a partir das suas partes conhecidas. Nesta aula você vai entender essa ideia e ver, na prática, uma palavra fora do vocabulário receber um vetor próximo ao da sua forma correta.


Objetivos

Ao final desta aula, você deve ser capaz de:

  • Explicar a limitação do Word2Vec com palavras fora do vocabulário.
  • Entender como o FastText representa palavras por n-gramas de caracteres.
  • Calcular a similaridade entre palavras a partir dos seus subpedaços.
  • Reconhecer por que isso ajuda especialmente idiomas de morfologia rica.

Teoria

O FastText herda tudo do Word2Vec, as arquiteturas CBOW e skip-gram e o aprendizado pelo contexto, mas muda a unidade básica. Em vez de associar um vetor a cada palavra inteira, ele associa um vetor a cada n-grama de caracteres, e o vetor de uma palavra passa a ser a soma dos vetores dos seus n-gramas.

Tomemos a palavra gato com n-gramas de tamanho 3. Adicionando marcadores de início e fim, escrevemos <gato>, e os trigramas são <ga, gat, ato, to>. A palavra é representada pela combinação desses pedaços. A consequência é poderosa. Palavras que compartilham raízes e terminações compartilham n-gramas, então correr, corredor e correndo ficam naturalmente próximas, porque dividem o pedaço corr. E uma palavra nunca vista pode ser representada, desde que os seus n-gramas tenham aparecido em outras palavras.

flowchart LR
    P["palavra: corredor"] --> N["n-gramas: cor, orr, rre, red, edo, dor"]
    N --> S["vetor = soma dos n-gramas"]
    S --> R["próximo de correr, correndo"]

Esse desenho resolve o problema das palavras fora do vocabulário e melhora a representação de palavras raras, que aproveitam os n-gramas de palavras comuns parecidas. Por isso o FastText costuma brilhar em idiomas com muita flexão, como o português.

Explicação Intuitiva

Pense na diferença entre reconhecer uma pessoa só pelo nome completo ou também pelos traços do rosto. O Word2Vec é como quem só conhece nomes, se aparece um nome novo, fica perdido. O FastText também olha os traços, os pedaços da palavra, então mesmo diante de um nome desconhecido ele percebe parecenças, este se parece com aquele que eu já conheço.

É essa atenção aos pedaços que dá ao FastText a sua robustez. Um aluno que escreve funçao sem o til, ou conjuga um verbo de um jeito incomum, ainda é compreendido, porque os pedaços da palavra apontam para o lugar certo do mapa. Para um assistente educacional, que recebe texto real, cheio de variações e erros, isso é precioso.

Explicação Matemática

A mudança em relação ao Word2Vec é onde mora o vetor. No FastText, cada n-grama de caracteres $g$ tem um vetor $z_g$. O vetor de uma palavra $w$ é a soma dos vetores dos seus n-gramas:

\[v_w = \sum_{g \in G_w} z_g,\]

em que $G_w$ é o conjunto de n-gramas de $w$, incluindo a própria palavra com marcadores de fronteira. O treino é igual ao do skip-gram, com o contexto sendo previsto a partir de $v_w$, mas agora o gradiente atualiza os vetores dos n-gramas, e não o de uma palavra inteira.

A grande vantagem aparece na inferência. Para uma palavra fora do vocabulário, calculamos $v_w$ somando os vetores dos seus n-gramas conhecidos, e assim obtemos uma representação útil sem nunca ter visto a palavra. Nesta aula, para enxergar o princípio sem treinar um modelo inteiro, vamos medir similaridade direto pelos n-gramas compartilhados entre palavras, o que já revela o comportamento essencial.

Exemplo Prático

Vamos representar palavras pelos seus n-gramas de caracteres e medir a similaridade do cosseno entre elas. A expectativa, que os números vão confirmar, é dupla. Primeiro, palavras da mesma família, como correr, corredor e correndo, terão alta similaridade, por compartilharem pedaços. Segundo, e mais importante, uma palavra fora do vocabulário, como corredores, ficará muito próxima de corredor, mostrando o tratamento de palavras novas que o Word2Vec não tinha.

Esse exemplo usa apenas as contagens de n-gramas, sem treinar embeddings, o que basta para ver a ideia funcionando. Na prática, usaríamos o gensim com FastText. O código está no notebook notebooks/modulo-04/02-fasttext.ipynb, então abra-o ao lado para acompanhar.

Código Comentado

from collections import Counter
import math


def ngramas(palavra, n=3):
    """N-gramas de caracteres da palavra, com marcadores de início e fim."""
    p = "<" + palavra + ">"
    return [p[i:i + n] for i in range(len(p) - n + 1)]


def vetor_ngram(palavra, n=3):
    """Representa a palavra como a contagem dos seus n-gramas."""
    return Counter(ngramas(palavra, n))


def cosseno(a, b):
    chaves = set(a) | set(b)
    produto = sum(a.get(k, 0) * b.get(k, 0) for k in chaves)
    na = math.sqrt(sum(v * v for v in a.values()))
    nb = math.sqrt(sum(v * v for v in b.values()))
    return produto / (na * nb) if na and nb else 0.0


def sim(p1, p2):
    return round(cosseno(vetor_ngram(p1), vetor_ngram(p2)), 3)


print("correr ~ corredor :", sim("correr", "corredor"))
print("correr ~ correndo :", sim("correr", "correndo"))
print("gato ~ gatinho    :", sim("gato", "gatinho"))
print("correr ~ gato     :", sim("correr", "gato"))
print("FORA DO VOCAB corredores ~ corredor:", sim("corredores", "corredor"))

Ao rodar, correr e corredor aparecem com similaridade alta, em torno de 0,58, porque compartilham vários n-gramas. Palavras sem relação, como correr e gato, dão zero, pois não dividem pedaços. E o ponto alto é a palavra corredores, que não estava no nosso conjunto, ficar a cerca de 0,78 de corredor, a sua forma base. É essa capacidade de representar o que nunca foi visto, compondo a partir dos pedaços, que torna o FastText tão útil no mundo real.

Exercícios

1) Conceitual: Por que o Word2Vec não consegue representar uma palavra fora do vocabulário, e como o FastText resolve isso? 2) Conceitual: Por que o FastText tende a ajudar mais em idiomas de morfologia rica, como o português, do que em idiomas com pouca flexão? 3) Prático: Mude o tamanho n dos n-gramas, por exemplo para 2 ou 4, e observe como as similaridades mudam. 4) Prático: Teste pares com erros de digitação, como função e funcao, e veja se eles ficam próximos. 5) Extensão: Pesquise como o FastText combina os n-gramas durante o treino real e compare com a soma simples que usamos aqui.

Projeto da Aula

Construa um corretor de vocabulário por similaridade de n-gramas. A entrega é um programa que, dada uma palavra possivelmente errada ou inédita, encontra, em uma lista de palavras conhecidas, aquela de maior similaridade de n-gramas, sugerindo-a como a forma mais provável.

Considere o projeto pronto quando o sistema sugerir corretamente a forma base para algumas palavras com erros de digitação ou flexões inéditas, e quando você comentar um caso em que a sugestão por n-gramas falha, por exemplo confundindo palavras que se parecem na escrita mas diferem no sentido. Esse comportamento, de aproximar pela forma, é a marca do FastText.

Leituras Recomendadas

  • O artigo de Bojanowski e colegas que introduziu o FastText, com a formulação por n-gramas de caracteres.
  • Documentação do gensim sobre FastText, para treinar e usar embeddings de subpalavra em corpora reais.
  • Comparações entre Word2Vec e FastText em tarefas de NLP, para ver quando cada um se sai melhor.

Referências Científicas

As referências abaixo são reais e estão registradas em references/referencias.bib. As chaves entre parênteses são as do BibTeX.

  • Bojanowski, P., Grave, E., Joulin, A., e Mikolov, T. (2017). Enriching Word Vectors with Subword Information. TACL, 5, 135-146. (bojanowski2017enriching)
  • Mikolov, T., Chen, K., Corrado, G., e Dean, J. (2013). Efficient Estimation of Word Representations in Vector Space. (mikolov2013efficient)
  • Sennrich, R., Haddow, B., e Birch, A. (2016). Neural Machine Translation of Rare Words with Subword Units. ACL. (sennrich2016bpe)