Phase 05 - Lesson 03

Word Embeddings — Word2Vec do zero

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

Uma palavra e a companhia que mantem. Treine uma rede rasa com essa ideia e a geometria emerge.

Tipo: Build Linguagens: Python Pre-requisitos: Fase 5 · 02 (BoW + TF-IDF), Fase 3 · 03 (Backpropagation do Zero) Tempo: ~75 minutos

O Problema

O TF-IDF sabe que dog e puppy sao palavras diferentes. Ele nao sabe que elas significam quase a mesma coisa. Um classificador treinado em dog nao consegue generalizar para uma resenha sobre puppy. Voce pode disfarcar isso listando sinonimos, mas isso falha com termos raros, jargao de dominio e todo idioma que voce nao antecipou.

Voce quer uma representacao em que dog e puppy cheguem perto um do outro no espaco. Em que king - man + woman caia perto de queen. Em que um modelo treinado em dog transfira algum sinal de graca para puppy.

O Word2Vec nos deu esse espaco. Rede neural de duas camadas, execucoes de treino com trilhoes de tokens, publicado em 2013. A arquitetura e quase constrangedoramente simples. Os resultados remodelaram o NLP por uma decada.

O Conceito

Janela skip-gram e espaco de embeddings

Hipotese distribucional (Firth, 1957): "Voce conhecera uma palavra pela companhia que ela mantem." Se duas palavras aparecem em contextos similares, provavelmente significam coisas similares.

O Word2Vec vem em dois sabores, ambos explorando essa ideia.

  • Skip-gram. Dada uma palavra central, preve as palavras ao redor. cat -> (the, sat, on) com janela de tamanho 2.
  • CBOW (continuous bag of words). Dadas as palavras ao redor, preve a central. (the, sat, on) -> cat.

O skip-gram e mais lento para treinar, mas lida melhor com palavras raras. Tornou-se o padrao.

A rede tem uma camada oculta sem nao linearidade. A entrada e um vetor one-hot sobre o vocabulario. A saida e um softmax sobre o vocabulario. Apos o treino, voce descarta a camada de saida. Os pesos da camada oculta sao os embeddings.

one-hot(center) ── W ──▶ hidden (d-dim) ── W' ──▶ softmax(vocab)
                          ^
                          this is the embedding

O truque: um softmax sobre 100k palavras e proibitivamente caro. O Word2Vec usa amostragem negativa (negative sampling) para transforma-lo em uma tarefa de classificacao binaria. Preve "esta palavra de contexto apareceu perto desta palavra central, sim ou nao". Amostre um punhado de palavras negativas (que nao co-ocorrem) por par de treino em vez de calcular o softmax sobre o vocabulario inteiro.

Construa

Passo 1: pares de treino a partir de um corpus

def skipgram_pairs(docs, window=2):
    pairs = []
    for doc in docs:
        for i, center in enumerate(doc):
            for j in range(max(0, i - window), min(len(doc), i + window + 1)):
                if i == j:
                    continue
                pairs.append((center, doc[j]))
    return pairs
>>> skipgram_pairs([["the", "cat", "sat", "on", "mat"]], window=2)
[('the', 'cat'), ('the', 'sat'),
 ('cat', 'the'), ('cat', 'sat'), ('cat', 'on'),
 ('sat', 'the'), ('sat', 'cat'), ('sat', 'on'), ('sat', 'mat'),
 ...]

Todo par (central, contexto) dentro de uma janela e um exemplo de treino positivo.

Passo 2: tabelas de embedding

Duas matrizes. W e a tabela de embeddings da palavra central (a que voce mantem). W' e a tabela da palavra de contexto (frequentemente descartada, as vezes mediada com W).

import numpy as np


def init_embeddings(vocab_size, dim, seed=0):
    rng = np.random.default_rng(seed)
    W = rng.normal(0, 0.1, size=(vocab_size, dim))
    W_prime = rng.normal(0, 0.1, size=(vocab_size, dim))
    return W, W_prime

Inicializacao aleatoria pequena. Vocabulario de 10k e dim 100 e realista; para ensino, 50 de vocabulario x 16 de dim e suficiente para ver a geometria.

Passo 3: objetivo de amostragem negativa

Para cada par positivo (center, context), amostre k palavras aleatorias do vocabulario como negativas. Treine o modelo para que o produto escalar W[center] · W'[context] seja alto para positivos e baixo para negativos.

def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-np.clip(x, -20, 20)))


def train_pair(W, W_prime, center_idx, context_idx, negative_indices, lr):
    v_c = W[center_idx]
    u_pos = W_prime[context_idx]
    u_negs = W_prime[negative_indices]

    pos_score = sigmoid(v_c @ u_pos)
    neg_scores = sigmoid(u_negs @ v_c)

    grad_center = (pos_score - 1) * u_pos
    for i, u in enumerate(u_negs):
        grad_center += neg_scores[i] * u

    W[context_idx] = W[context_idx]
    W_prime[context_idx] -= lr * (pos_score - 1) * v_c
    for i, neg_idx in enumerate(negative_indices):
        W_prime[neg_idx] -= lr * neg_scores[i] * v_c
    W[center_idx] -= lr * grad_center

A formula magica: perda logistica no par positivo (queremos sigmoide perto de 1) mais perda logistica nos pares negativos (queremos sigmoide perto de 0). Os gradientes fluem para ambas as tabelas. A derivacao completa esta no artigo original; percorra-a uma vez com lapis e papel se quiser que ela grude.

Passo 4: treine em um corpus de brinquedo

def train(docs, dim=16, window=2, k_neg=5, epochs=100, lr=0.05, seed=0):
    vocab = build_vocab(docs)
    vocab_size = len(vocab)
    rng = np.random.default_rng(seed)
    W, W_prime = init_embeddings(vocab_size, dim, seed=seed)
    pairs = skipgram_pairs(docs, window=window)

    for epoch in range(epochs):
        rng.shuffle(pairs)
        for center, context in pairs:
            c_idx = vocab[center]
            ctx_idx = vocab[context]
            negs = rng.integers(0, vocab_size, size=k_neg)
            negs = [n for n in negs if n != ctx_idx and n != c_idx]
            train_pair(W, W_prime, c_idx, ctx_idx, negs, lr)
    return vocab, W

Apos epocas suficientes em um corpus grande, palavras que compartilham contextos tem embeddings centrais similares. Em um corpus de brinquedo, voce ve o efeito de forma tenue. Em bilhoes de tokens, voce o ve de forma dramatica.

Passo 5: o truque da analogia

def nearest(vocab, W, target_vec, topk=5, exclude=None):
    exclude = exclude or set()
    inv_vocab = {i: w for w, i in vocab.items()}
    norms = np.linalg.norm(W, axis=1, keepdims=True) + 1e-9
    W_norm = W / norms
    target = target_vec / (np.linalg.norm(target_vec) + 1e-9)
    sims = W_norm @ target
    order = np.argsort(-sims)
    out = []
    for i in order:
        if i in exclude:
            continue
        out.append((inv_vocab[i], float(sims[i])))
        if len(out) == topk:
            break
    return out


def analogy(vocab, W, a, b, c, topk=5):
    v = W[vocab[b]] - W[vocab[a]] + W[vocab[c]]
    return nearest(vocab, W, v, topk=topk, exclude={vocab[a], vocab[b], vocab[c]})

Sobre os vetores pre-treinados Google News de 300d:

>>> analogy(vocab, W, "man", "king", "woman")
[('queen', 0.71), ('monarch', 0.62), ('princess', 0.59), ...]

king - man + woman = queen. Nao porque o modelo sabe o que e realeza. Porque o vetor (king - man) captura algo como "real", e adiciona-lo a woman cai perto da regiao real-feminina.

Use

Escrever o Word2Vec do zero e didatico. O NLP de producao usa gensim.

from gensim.models import Word2Vec

sentences = [
    ["the", "cat", "sat", "on", "the", "mat"],
    ["the", "dog", "ran", "across", "the", "room"],
]

model = Word2Vec(
    sentences,
    vector_size=100,
    window=5,
    min_count=1,
    sg=1,
    negative=5,
    workers=4,
    epochs=30,
)

print(model.wv["cat"])
print(model.wv.most_similar("cat", topn=3))

Para trabalho real, voce quase nunca treina o Word2Vec por conta propria. Voce baixa vetores pre-treinados.

  • GloVe — abordagem de fatoracao de matriz de co-ocorrencia da Stanford. Checkpoints de 50d, 100d, 200d, 300d. Boa cobertura geral. A licao 04 cobre o GloVe especificamente.
  • fastText — extensao do Word2Vec do Facebook que embute n-gramas de caracteres. Lida com palavras fora do vocabulario compondo subpalavras. Licao 04.
  • Word2Vec pre-treinado no Google News — 300d, vocabulario de 3M de palavras, publicado em 2013. Ainda baixado diariamente.

Quando o Word2Vec ainda vence em 2026

  • Recuperacao leve e especifica de dominio. Treine em resumos medicos em uma hora num laptop e obtenha vetores especializados que nenhum modelo geral captura.
  • Engenharia de features estilo analogia. gender_vector = mean(man - woman pairs). Subtraia-o de outras palavras para obter um eixo de genero neutro. Ainda usado em pesquisa de equidade (fairness).
  • Interpretabilidade. 100d e pequeno o suficiente para plotar via PCA ou t-SNE e realmente ver clusters se formando.
  • Em qualquer lugar onde a inferencia precise rodar no dispositivo sem GPU. A busca do Word2Vec e a leitura de uma unica linha.

Onde o Word2Vec falha

O muro da polissemia. bank tem um unico vetor. river bank e financial bank o compartilham. table (planilha vs. movel) o compartilha. Um classificador a jusante nao consegue distinguir os sentidos a partir do vetor.

Embeddings contextuais (ELMo, BERT, todo transformer desde entao) resolveram isso produzindo um vetor diferente para cada ocorrencia da palavra com base no contexto ao redor. Esse e o salto do Word2Vec para o BERT: do estatico ao contextual. A Fase 7 cobre a metade do transformer.

O problema de fora do vocabulario (out-of-vocabulary) e a outra falha. O Word2Vec nunca viu Zoomer-approved se isso nao estava nos dados de treino. Nenhum fallback. O fastText resolve isso com composicao de subpalavras (licao 04).

Entregue

Salve como outputs/skill-embedding-probe.md:

---
name: embedding-probe
description: Inspect a word2vec model. Run analogies, find neighbors, diagnose quality.
version: 1.0.0
phase: 5
lesson: 03
tags: [nlp, embeddings, debugging]
---

You probe trained word embeddings to verify they are working. Given a `gensim.models.KeyedVectors` object and a vocabulary, you run:

1. Three canonical analogy tests. `king : man :: queen : woman`. `paris : france :: tokyo : japan`. `walking : walked :: swimming : ?`. Report the top-1 result and its cosine.
2. Five nearest-neighbor tests on domain-specific words the user supplies. Print top-5 neighbors with cosines.
3. One symmetry check. `similarity(a, b) == similarity(b, a)` to within float precision.
4. One degenerate check. If any embedding has a norm below 0.01 or above 100, the model has a training bug. Flag it.

Refuse to declare a model good on analogy accuracy alone. Analogy benchmarks are gameable and do not transfer to downstream tasks. Recommend intrinsic + downstream evaluation together.

Exercicios

  1. Facil. Rode o loop de treino em um corpus minusculo (20 frases sobre gatos e cachorros). Apos 200 epocas, verifique se nearest(vocab, W, W[vocab["cat"]]) retorna dog em seu top 3. Se nao, aumente as epocas ou o vocabulario.
  2. Medio. Adicione subamostragem de palavras frequentes. Palavras com frequencia acima de 10^-5 sao descartadas dos pares de treino com probabilidade proporcional a sua frequencia. Meca o efeito na similaridade de palavras raras.
  3. Dificil. Treine um modelo no corpus 20 Newsgroups. Calcule dois eixos de vies: he - she e doctor - nurse. Projete palavras de ocupacao em ambos os eixos. Relate quais ocupacoes tem a maior diferenca de vies. Esse e o tipo de sondagem que pesquisadores de equidade usam.

Termos-Chave

Termo O que as pessoas dizem O que realmente significa
Word embedding Palavra como vetor Uma representacao densa, de baixa dimensao (tipicamente 100-300) aprendida a partir do contexto.
Skip-gram Truque do Word2Vec Prever palavras de contexto a partir da palavra central. Mais lento que o CBOW, melhor para palavras raras.
Amostragem negativa Atalho de treino Substituir o softmax sobre o vocabulario completo por classificacao binaria contra k palavras aleatorias.
Embedding estatico Um vetor por palavra Mesmo vetor independente do contexto. Falha na polissemia.
Embedding contextual Vetor sensivel ao contexto Vetor diferente para cada ocorrencia com base nas palavras ao redor. O que os transformers produzem.
OOV Fora do vocabulario Palavra nao vista no treino. O Word2Vec nao consegue produzir um vetor para elas.

Leitura Complementar

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