Laboratório GPT-2 · RoBERTa · Parte I — A fundação
02

Embeddings:
onde nasce o significado

De um inteiro sem sentido a um vetor de 768 números que sabe que "rei" e "rainha" são parentes.

Apostila 02 de 15 Pré-requisito: apostila 01

1 O problema: inteiros não têm significado

Saímos da apostila 01 com uma lista de inteiros. Digamos que gato = 4899 e cachorro = 5257. Esses números são etiquetas de armário, não medidas. O armário 5257 não é "maior" nem "mais quente" que o 4899. Se o vocabulário fosse embaralhado, gato seria 12 e cachorro seria 40 001, e nada mudaria.

Mas uma rede neural só sabe fazer aritmética. Se ela recebe 4899 e 5257, ela vai inevitavelmente calcular diferenças, médias, produtos — e vai extrair relações que não existem. Alimentar IDs crus numa rede é um erro conceitual grave.

A primeira solução que costuma aparecer, e por que não serve: one-hot

O jeito clássico de dizer "esta é a categoria 4899, e as categorias não têm ordem" é o vetor one-hot: um vetor de 50 257 posições, todas zero, exceto um 1 na posição 4899.

Isso resolve o problema da falsa ordem. Mas cria dois novos:

2 A ideia: dar coordenadas às palavras

E se, em vez de 50 257 posições com um 1 solitário, cada palavra tivesse 768 números contínuos? Não escolhidos por nós — aprendidos pelo modelo durante o treino, ajustados a cada erro cometido.

Esses 768 números são as coordenadas da palavra num espaço de 768 dimensões. E aqui está o ponto central: se o treino for bem feito, palavras que aparecem em contextos parecidos acabam com coordenadas parecidas. Elas se agrupam. O espaço ganha geografia semântica.

Analogia

Pense num mapa do Brasil. Cada cidade é um par de números (latitude, longitude). Esses dois números não são arbitrários: cidades com números parecidos são de fato próximas, e você pode fazer aritmética útil com eles ("qual cidade fica a 200 km ao norte?").

Um embedding é a mesma coisa, com 768 coordenadas em vez de 2. E as "direções" desse mapa não são norte-sul, mas coisas que o modelo descobriu sozinho: talvez uma direção seja "formalidade", outra "singular↔plural", outra "concreto↔abstrato". Ninguém programou esses eixos — eles emergiram porque ajudavam a prever a próxima palavra.

A demonstração clássica

Em espaços de embedding bem treinados, relações semânticas viram deslocamentos vetoriais constantes:

v(rei) − v(homem) + v(mulher) ≈ v(rainha) O vetor que vai de "homem" a "mulher" é aproximadamente o mesmo que vai de "rei" a "rainha". A relação gênero virou uma direção fixa no espaço. Isso não foi programado: foi consequência de prever texto.

3 Na prática: a matriz é uma tabela de consulta

Formalmente, a camada de embedding é uma matriz:

We ∈ ℝ|V| × d  =  ℝ50257 × 768 |V| = tamanho do vocabulário (uma linha por token possível). d = dmodel, a largura do modelo. No GPT-2 small, d = 768. Total: 38,6 milhões de parâmetros só nesta matriz — 31% do modelo inteiro.

Matematicamente, buscar o embedding do token i é multiplicar o one-hot pela matriz:

ei = onehot(i)⊤ We Mas multiplicar um vetor de 50 257 zeros por uma matriz é jogar 50 257 multiplicações no lixo para pegar uma linha.

Na implementação real, ninguém faz essa multiplicação. Faz-se indexação direta: W_e[4899]. É por isso que a camada se chama lookup table — tabela de consulta. O one-hot existe só na explicação matemática; no código ele nunca é construído.

Guarde este nome

No GPT-2 essa matriz se chama wte — word token embedding. Você vai vê-la nos nomes dos pesos: transformer.wte.weight. E vai reencontrá-la no fim do modelo, de um jeito surpreendente (seção 6).

4 O segundo embedding: posição

Existe um buraco no que construímos até agora, e ele é grave. A atenção, que você vai ver na apostila 03, é uma operação de conjunto: ela olha todos os tokens ao mesmo tempo, sem noção nenhuma de ordem. Para ela, "o cachorro mordeu o homem" e "o homem mordeu o cachorro" são idênticos — mesmos tokens, mesmo conjunto.

Isso é inaceitável para linguagem. A ordem é significado. Então precisamos injetar a posição de volta, explicitamente.

Como o GPT-2 resolve: aprender a posição também

O artigo original do Transformer (2017) usou senos e cossenos de frequências diferentes — uma fórmula fixa, elegante, que permite extrapolar para comprimentos nunca vistos. O GPT-2 abandonou isso e fez a escolha mais simples possível: uma segunda tabela de consulta, aprendida do zero.

Wp ∈ ℝnctx × d  =  ℝ1024 × 768 nctx = 1024 é o comprimento máximo de contexto do GPT-2. Uma linha por posição possível: a linha 0 é "o vetor de ser o primeiro token", a linha 1 é "o vetor de ser o segundo", e assim por diante.

No GPT-2 essa matriz se chama wpe — word position embedding.

A consequência dura do "aprendido"

Como wpe tem exatamente 1024 linhas e cada uma foi aprendida individualmente, o GPT-2 é fisicamente incapaz de processar mais de 1024 tokens. Não é uma configuração conservadora que dá para aumentar — a posição 1025 simplesmente não existe na matriz. Tentar usá-la é um erro de índice.

Isso contrasta com o Transformer senoidal original (que extrapola) e com técnicas modernas como RoPE (usada em Llama, Qwen, Mistral), que permitem esticar o contexto. É uma limitação de projeto de 2019 que você vai encostar na prática.

5 Juntando: a entrada real do modelo

Os dois embeddings são simplesmente somados, elemento a elemento:

h0 = We[tokens] + Wp[0, 1, …, n−1] h0 é o "estado oculto zero" — a entrada do primeiro bloco Transformer. Formato: (n tokens × 768). Cada linha é um token que já sabe o que ele é e onde ele está.

"Somar? Não deveria concatenar?"

Essa é a pergunta certa, e é comum fazê-la. Concatenar (768 de conteúdo + 768 de posição = 1536) parece mais limpo, mas dobraria a largura do modelo e o custo de tudo depois.

A soma funciona porque 768 dimensões é muito espaço. Em alta dimensão, dois vetores aleatórios são quase sempre praticamente ortogonais — eles ocupam "direções" diferentes e não se atrapalham. O modelo aprende a usar certas direções para conteúdo e outras para posição, e as camadas seguintes conseguem separar de volta o que foi somado. É contraintuitivo, mas é uma propriedade real de espaços de alta dimensão.

Um exemplo concreto de tamanhos

Entrada: "O Brasil é gigante"   ->  5 tokens

wte[ids]        ->  (5, 768)     o QUE cada token é
wpe[0..4]       ->  (5, 768)     ONDE cada token está
                    ---------
h_0 = soma      ->  (5, 768)     entrada do bloco 1

Esse (5, 768) atravessa os 12 blocos SEM MUDAR DE FORMA.
Sai (5, 768) do bloco 12. A forma e um invariante do Transformer.

6 A amarração final: weight tying

Aqui está um detalhe de engenharia bonito, e que muita explicação de GPT-2 esquece.

No fim do modelo, você precisa converter o vetor de 768 dimensões da última posição de volta em uma pontuação para cada um dos 50 257 tokens possíveis. Isso pede uma matriz de saída Wsaída ∈ ℝ768 × 50257 — mais 38,6 milhões de parâmetros.

O GPT-2 não cria essa matriz. Ele reusa a matriz de embedding, transposta:

logits = hfinal We⊤ A mesma We que converteu token → vetor na entrada agora converte vetor → pontuações na saída. Zero parâmetros novos.

Isso se chama weight tying (amarração de pesos), e tem duas justificativas:

7 O código

# o que existe dentro de wte e wpe
import torch
from transformers import AutoModel, AutoTokenizer

tok = AutoTokenizer.from_pretrained("openai-community/gpt2")
m   = AutoModel.from_pretrained("openai-community/gpt2")

wte = m.wte.weight   # (50257, 768)  conteúdo
wpe = m.wpe.weight   # (1024,  768)  posição

# 1) o embedding de um token e só uma linha da tabela
id_gato = tok.encode(" gato")[0]
v = wte[id_gato]                    # vetor de 768 números

# 2) similaridade de cosseno = "quão parecidas são duas palavras?"
def prox(a, b):
    va, vb = wte[tok.encode(a)[0]], wte[tok.encode(b)[0]]
    return torch.cosine_similarity(va, vb, dim=0).item()

print(f"king  x queen  {prox(' king',' queen'):+.3f}")   # alto
print(f"king  x banana {prox(' king',' banana'):+.3f}")  # baixo

# 3) a prova do weight tying: as duas matrizes são o MESMO objeto
lm = AutoModelForCausalLM.from_pretrained("openai-community/gpt2")
print(lm.lm_head.weight.data_ptr() == lm.transformer.wte.weight.data_ptr())
#    True  -> mesmo endereço de memória. Não é cópia, é o mesmo tensor.

8 No laboratório

.venv\Scripts\python.exe lab\02_embeddings.py
  1. Mostra o vetor cru de um token (os primeiros 12 dos 768 números). Serve para você ver que não há nada de místico: são floats.
  2. Ranking de vizinhos semânticos: dada uma palavra, quais são os 10 tokens mais próximos por cosseno. Repare que muitos vizinhos são variações da mesma palavra (maiúscula, plural, com espaço) — o espaço é dominado por forma tanto quanto por sentido.
  3. A analogia rei−homem+mulher rodando de verdade. E ela funciona: " queen" sai em +0,709 e " princess" logo atrás. Mas o script também mostra a ressalva que as demonstrações bonitas costumam esconder — " king" aparece em primeiro lugar, porque partimos do vetor dele e mexemos pouco. Quase toda demo famosa dessa analogia remove as palavras de entrada do ranking antes de mostrar o resultado.
  4. Compara os tamanhos das matrizes nos quatro GPT-2, mostrando que wte é a maior peça isolada em todos eles.
  5. Verifica o weight tying comparando endereços de memória.
O limite real desta camada: ela é estática

O wte dá uma linha fixa por token, antes de qualquer contexto. Ou seja: "banco" de praça e "banco" de dinheiro recebem exatamente o mesmo vetor. Nesta camada, a ambiguidade é irresolúvel por construção.

E o GPT-2 não precisa que ela resolva. Ele tem 12 blocos de atenção depois dela justamente para construir o significado contextual — o "banco" vai se especializar conforme os vizinhos. O wte é um ponto de partida bruto; o mapa semântico de verdade mora nas camadas do meio. Guarde isso: é a diferença entre embeddings estáticos (Word2Vec, GloVe, wte) e contextuais (a saída de qualquer camada do Transformer), e é a razão pela qual o BERT foi uma revolução em 2018.

Exercício 2 — preveja antes de rodar

  1. Calcule o cosseno entre " cat" e " cats", e entre " cat" e " dog". Qual par você espera que seja mais próximo — o de forma ou o de sentido? Rode e veja se sua intuição está certa.
  2. Quanto ocupam wte + wpe em MB no GPT-2 XL (vocabulário 50 257, d=1600, contexto 1024), em fp32? Calcule à mão antes: cada float ocupa 4 bytes.
  3. O wpe tem 1024 linhas. Pegue as linhas 0, 1, 2 e 500, e calcule o cosseno entre elas. Posições vizinhas deveriam ter vetores parecidos? Você acha que o modelo aprendeu isso sozinho? (Rode: a resposta é sim, e é bonita.)
  4. Difícil: se o weight tying faz a mesma matriz servir para entrada e saída, o gradiente que chega nela vem de dois caminhos. Isso ajuda ou atrapalha o treino? Argumente os dois lados.
Referências

As fontes primárias dos conceitos deste capítulo. Os números do texto foram medidos no laboratório; as ideias vêm daqui.

Os termos técnicos têm aqui o sentido de aprendizado de máquina. O glossário do curso mostra esse sentido ao lado dos outros que as mesmas palavras têm na TI.

  1. Mikolov, T., Chen, K., Corrado, G. e Dean, J. (2013). Efficient Estimation of Word Representations in Vector Space. ICLR 2013, workshop. arXiv:1301.3781
  2. Vaswani, A. et al. (2017). Attention Is All You Need. NeurIPS 2017. arXiv:1706.03762
  3. Press, O. e Wolf, L. (2017). Using the Output Embedding to Improve Language Models. EACL 2017. arXiv:1608.05859

Agora cada token é um vetor que sabe o que é e onde está — mas ainda está sozinho, sem falar com os vizinhos.
Na apostila 03 eles finalmente conversam. É o coração do Transformer.