Phase 05 - Lesson 04

GloVe, FastText e Embeddings de Subpalavras

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

O Word2Vec treinou um embedding por palavra. O GloVe fatorou a matriz de coocorrência. O FastText embutiu os pedaços. O BPE fez a ponte para os transformers.

Tipo: Build Linguagens: Python Pré-requisitos: Fase 5 · 03 (Word2Vec do Zero) Tempo: ~45 minutos

O Problema

O Word2Vec deixou duas questões em aberto.

Primeiro, havia uma linha paralela de pesquisa que fatorava a matriz de coocorrência diretamente (LSA, HAL) em vez de fazer atualizações skip-gram online. A abordagem iterativa do Word2Vec era fundamentalmente melhor, ou a diferença era um artefato de como os dois métodos tratavam as contagens? O GloVe respondeu isso: fatoração de matriz com uma função de perda cuidadosamente escolhida iguala ou supera o Word2Vec, e custa menos para treinar.

Segundo, nenhum dos métodos tinha uma solução para palavras que nunca havia visto. Zoomer-approved, dogecoin, qualquer nome próprio cunhado na semana passada, toda forma flexionada de uma raiz rara. O FastText resolveu isso embutindo n-gramas de caracteres: uma palavra é a soma de suas partes, incluindo morfemas, então mesmo palavras fora do vocabulário recebem um vetor sensato.

Terceiro, quando os transformers chegaram, a questão mudou novamente. Vocabulários em nível de palavra atingem o limite em torno de um milhão de entradas; a linguagem real é mais aberta que isso. A codificação por pares de bytes (BPE) e suas variantes resolveram isso aprendendo um vocabulário de unidades de subpalavras frequentes que cobre tudo. Todo tokenizador moderno de todo LLM moderno é um tokenizador de subpalavras.

Esta lição percorre os três e depois explica qual usar e quando.

O Conceito

Três abordagens de embedding: coocorrência GloVe, subpalavras FastText, merges BPE

GloVe (Global Vectors). Construa a matriz de coocorrência palavra-palavra X onde X[i][j] é a frequência com que a palavra j aparece no contexto da palavra i. Treine vetores de modo que v_i · v_j + b_i + b_j ≈ log(X[i][j]). Pondere a perda para que pares frequentes não dominem. Pronto.

FastText. Uma palavra é a soma de seus n-gramas de caracteres mais a própria palavra. where se torna <wh, whe, her, ere, re>, <where>. O vetor da palavra é a soma desses vetores componentes. Treine como Word2Vec. Benefício: palavras não vistas (whereupon) se compõem a partir de n-gramas conhecidos.

BPE (Byte-Pair Encoding). Comece com um vocabulário de bytes (ou caracteres) individuais. Conte todo par adjacente no corpus. Faça merge do par mais frequente em um novo token. Repita por k iterações. Resultado: um vocabulário de k + 256 tokens onde sequências frequentes (ing, tion, the) são tokens únicos e palavras raras são quebradas em pedaços familiares. Toda frase se tokeniza em algo.

Construa

GloVe: fatore a matriz de coocorrência

import numpy as np
from collections import Counter


def build_cooccurrence(docs, window=5):
    pair_counts = Counter()
    vocab = {}
    for doc in docs:
        for token in doc:
            if token not in vocab:
                vocab[token] = len(vocab)
    for doc in docs:
        indexed = [vocab[t] for t in doc]
        for i, center in enumerate(indexed):
            for j in range(max(0, i - window), min(len(indexed), i + window + 1)):
                if i != j:
                    distance = abs(i - j)
                    pair_counts[(center, indexed[j])] += 1.0 / distance
    return vocab, pair_counts


def glove_train(vocab, pair_counts, dim=16, epochs=100, lr=0.05, x_max=100, alpha=0.75, seed=0):
    n = len(vocab)
    rng = np.random.default_rng(seed)
    W = rng.normal(0, 0.1, size=(n, dim))
    W_tilde = rng.normal(0, 0.1, size=(n, dim))
    b = np.zeros(n)
    b_tilde = np.zeros(n)

    for epoch in range(epochs):
        for (i, j), x_ij in pair_counts.items():
            weight = (x_ij / x_max) ** alpha if x_ij < x_max else 1.0
            diff = W[i] @ W_tilde[j] + b[i] + b_tilde[j] - np.log(x_ij)
            coef = weight * diff

            grad_W_i = coef * W_tilde[j]
            grad_W_tilde_j = coef * W[i]
            W[i] -= lr * grad_W_i
            W_tilde[j] -= lr * grad_W_tilde_j
            b[i] -= lr * coef
            b_tilde[j] -= lr * coef

    return W + W_tilde

Duas engrenagens em movimento que vale nomear. A função de ponderação f(x) = (x/x_max)^alpha reduz o peso de pares muito frequentes (como (the, and)) para que não dominem a perda. O embedding final é a soma das tabelas W (centro) e W_tilde (contexto). Somar ambas é um truque publicado que tende a superar o uso de apenas uma.

FastText: embeddings cientes de subpalavras

def char_ngrams(word, n_min=3, n_max=6):
    wrapped = f"<{word}>"
    grams = {wrapped}
    for n in range(n_min, n_max + 1):
        for i in range(len(wrapped) - n + 1):
            grams.add(wrapped[i:i + n])
    return grams
>>> char_ngrams("where")
{'<where>', '<wh', 'whe', 'her', 'ere', 're>', '<whe', 'wher', 'here', 'ere>', '<wher', 'where', 'here>'}

Cada palavra é representada por seu conjunto de n-gramas (tipicamente de 3 a 6 caracteres). O embedding da palavra é a soma de seus embeddings de n-gramas. Para o treinamento skip-gram, encaixe isto onde o Word2Vec usava um único vetor.

def fasttext_vector(word, ngram_table):
    grams = char_ngrams(word)
    vecs = [ngram_table[g] for g in grams if g in ngram_table]
    if not vecs:
        return None
    return np.sum(vecs, axis=0)

Para uma palavra não vista, você ainda obtém um vetor desde que alguns de seus n-gramas sejam conhecidos. whereupon compartilha <wh, her, ere e <where com where, então as duas ficam próximas uma da outra.

BPE: vocabulário de subpalavras aprendido

def learn_bpe(corpus, k_merges):
    vocab = Counter()
    for word, freq in corpus.items():
        tokens = tuple(word) + ("</w>",)
        vocab[tokens] = freq

    merges = []
    for _ in range(k_merges):
        pair_freq = Counter()
        for tokens, freq in vocab.items():
            for a, b in zip(tokens, tokens[1:]):
                pair_freq[(a, b)] += freq
        if not pair_freq:
            break
        best = pair_freq.most_common(1)[0][0]
        merges.append(best)

        new_vocab = Counter()
        for tokens, freq in vocab.items():
            new_tokens = []
            i = 0
            while i < len(tokens):
                if i + 1 < len(tokens) and (tokens[i], tokens[i + 1]) == best:
                    new_tokens.append(tokens[i] + tokens[i + 1])
                    i += 2
                else:
                    new_tokens.append(tokens[i])
                    i += 1
            new_vocab[tuple(new_tokens)] = freq
        vocab = new_vocab
    return merges


def apply_bpe(word, merges):
    tokens = list(word) + ["</w>"]
    for a, b in merges:
        new_tokens = []
        i = 0
        while i < len(tokens):
            if i + 1 < len(tokens) and tokens[i] == a and tokens[i + 1] == b:
                new_tokens.append(a + b)
                i += 2
            else:
                new_tokens.append(tokens[i])
                i += 1
        tokens = new_tokens
    return tokens
>>> corpus = Counter({"low": 5, "lower": 2, "newest": 6, "widest": 3})
>>> merges = learn_bpe(corpus, k_merges=10)
>>> apply_bpe("lowest", merges)
['low', 'est</w>']

A primeira iteração faz merge do par adjacente mais comum. Depois de iterações suficientes, substrings frequentes (low, est, tion) tornam-se tokens únicos e palavras raras quebram de forma limpa.

Os tokenizadores reais de GPT / BERT / T5 aprendem de 30 mil a 100 mil merges. Resultado: qualquer texto se tokeniza em uma sequência de comprimento limitado de IDs conhecidos, nunca há OOV.

Use

Na prática, você raramente treina qualquer um destes por conta própria. Você carrega checkpoints pré-treinados.

import fasttext.util
fasttext.util.download_model("en", if_exists="ignore")
ft = fasttext.load_model("cc.en.300.bin")
print(ft.get_word_vector("whereupon").shape)
print(ft.get_word_vector("zoomerapproved").shape)

Para tokenização de subpalavras no estilo BPE na era dos transformers:

from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("gpt2")
print(tok.tokenize("unbelievably tokenized"))
['un', 'bel', 'iev', 'ably', 'Ġtoken', 'ized']

O prefixo Ġ marca os limites de palavra (uma convenção do GPT-2). Todo tokenizador moderno é uma variante de BPE, WordPiece (BERT) ou SentencePiece (T5, LLaMA).

Quando escolher qual

Situação Escolha
Vetores de palavra pré-treinados de propósito geral, sem necessidade de tolerância a OOV GloVe 300d
Vetores de palavra pré-treinados de propósito geral, precisa lidar com erros de digitação / neologismos / línguas morfologicamente ricas FastText
Qualquer coisa que entre em um transformer (treinamento ou inferência) Qualquer tokenizador que veio com o modelo. Nunca troque.
Treinar seu próprio modelo de linguagem do zero Treine primeiro um tokenizador BPE ou SentencePiece no seu corpus
Classificação de texto em produção com um modelo linear Ainda TF-IDF. Lição 02.

Entregue

Salve como outputs/skill-tokenizer-picker.md:

---
name: tokenizer-picker
description: Pick a tokenization approach for a new language model or text pipeline.
version: 1.0.0
phase: 5
lesson: 04
tags: [nlp, tokenization, embeddings]
---

Given a task and dataset description, you output:

1. Tokenization strategy (word-level, BPE, WordPiece, SentencePiece, byte-level). One-sentence reason.
2. Vocabulary size target (e.g., 32k for an English-only LM, 64k-100k for multilingual).
3. Library call with the exact training command. Name the library. Quote the arguments.
4. One reproducibility pitfall. Tokenizer-model mismatch is the single most common silent production bug; call out which pair must be used together.

Refuse to recommend training a custom tokenizer when the user is fine-tuning a pretrained LLM. Refuse to recommend word-level tokenization for any model targeting production inference. Flag non-English / multi-script corpora as needing SentencePiece with byte fallback.

Exercícios

  1. Fácil. Execute char_ngrams("playing") e char_ngrams("played"). Calcule a sobreposição de Jaccard dos dois conjuntos de n-gramas. Você deve ver pedaços compartilhados substanciais (pla, lay, play), que é o motivo pelo qual o FastText transfere bem entre variantes morfológicas.
  2. Médio. Estenda learn_bpe para acompanhar o crescimento do vocabulário. Plote os tokens-por-caractere-de-corpus em função do número de merges. Você deve ver compressão rápida no início, com assíntota perto de ~2-3 caracteres por token.
  3. Difícil. Treine um BPE de mil merges sobre as obras completas de Shakespeare. Compare a tokenização de palavras comuns com a de nomes próprios raros. Meça a média de tokens por palavra antes e depois. Escreva o que te surpreendeu.

Termos-Chave

Termo O que as pessoas dizem O que realmente significa
Matriz de coocorrência Tabela de frequência palavra-palavra X[i][j] = a frequência com que a palavra j aparece em uma janela ao redor da palavra i.
Subpalavra Pedaço de uma palavra Um n-grama de caracteres (FastText) ou token aprendido (BPE/WordPiece/SentencePiece).
BPE Byte-pair encoding Merge iterativo dos pares adjacentes mais frequentes até o vocabulário atingir o tamanho-alvo.
OOV Fora do vocabulário Palavra que o modelo nunca viu. Word2Vec/GloVe falham. FastText e BPE lidam com isso.
BPE em nível de byte BPE sobre bytes brutos O esquema do GPT-2. O vocabulário começa com 256 bytes, então nada nunca é OOV.

Leitura Adicional

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).