Capítulo 3: Codificação Posicional e Embeddings
Introdução
Diferente das RNNs, que processam sequências token por token, os Transformers processam todas as posições simultaneamente. Isso cria um desafio fundamental: como o modelo sabe a ordem dos tokens? Este capítulo explica a codificação posicional e o processo mais amplo de embedding de tokens.
O Problema da Posição
Por Que a Posição Importa
Considere estas frases:
- "O cachorro mordeu o homem"
- "O homem mordeu o cachorro"
Mesmos tokens, significados completamente diferentes. A posição é crucial para entender a linguagem.
Transformers e Invariância à Permutação
Sem informação posicional, a self-attention é invariante à permutação:
Onde é qualquer permutação. O modelo trata "cachorro mordeu homem" e "homem mordeu cachorro" de forma idêntica!
# Demonstração: scores de atenção são independentes da posição
import torch
def attention_scores(Q, K):
return torch.softmax(Q @ K.T, dim=-1)
# Sequência original
x1 = torch.tensor([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
# Sequência permutada (troca posições 0 e 1)
x2 = torch.tensor([[0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
# Mesmo padrão de atenção (apenas permutado)!
print(attention_scores(x1, x1))
print(attention_scores(x2, x2))
Embeddings de Tokens
De Tokens para Vetores
Antes de adicionar informação posicional, primeiro convertemos tokens em vetores densos:
Implementação
import torch
import torch.nn as nn
class TokenEmbedding(nn.Module):
"""
Converte IDs de tokens em vetores densos.
"""
def __init__(self, vocab_size, d_model):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
self.d_model = d_model
def forward(self, token_ids):
"""
token_ids: (batch, seq_len) de IDs inteiros de tokens
Retorna: embeddings (batch, seq_len, d_model)
"""
# Escala os embeddings (do paper original do Transformer)
return self.embedding(token_ids) * (self.d_model ** 0.5)
# Exemplo
vocab_size = 50000
d_model = 512
embedding_layer = TokenEmbedding(vocab_size, d_model)
tokens = torch.tensor([[101, 7592, 2088, 102]]) # [CLS] Hello World [SEP]
embeddings = embedding_layer(tokens)
print(f"Shape: {embeddings.shape}") # (1, 4, 512)
Codificação Posicional Sinusoidal
A Abordagem Original
O artigo original do Transformer utilizou funções senoidais fixas:
Onde:
- = posição na sequência
- = índice da dimensão
- = dimensão do modelo
Intuição
Cada dimensão oscila em uma frequência diferente:
- Dimensões baixas: oscilação lenta (captura posições de longo alcance)
- Dimensões altas: oscilação rápida (captura posições detalhadas)
Implementação
import numpy as np
def get_sinusoidal_encoding(max_len, d_model):
"""
Gera codificações posicionais senoidais.
Args:
max_len: Comprimento máximo da sequência
d_model: Dimensão do modelo
Retorna:
Matriz de codificação posicional (max_len, d_model)
"""
position = np.arange(max_len)[:, np.newaxis]
div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
pe = np.zeros((max_len, d_model))
pe[:, 0::2] = np.sin(position * div_term) # Índices pares
pe[:, 1::2] = np.cos(position * div_term) # Índices ímpares
return pe
# Visualização
import matplotlib.pyplot as plt
pe = get_sinusoidal_encoding(100, 512)
plt.figure(figsize=(12, 4))
plt.imshow(pe.T, aspect='auto', cmap='RdBu')
plt.xlabel('Posição')
plt.ylabel('Dimensão')
plt.colorbar(label='Valor')
plt.title('Codificação Posicional Senoidal')
plt.show()
Propriedades da Codificação Senoidal
- Única: Cada posição tem uma codificação única
- Limitada: Valores sempre em [-1, 1]
- Posições relativas: Relação linear permite aprender posições relativas
- Extrapolação: Pode generalizar para sequências mais longas do que as vistas no treinamento
Implementação em PyTorch
class SinusoidalPositionalEncoding(nn.Module):
"""
Codificação posicional senoidal de 'Attention Is All You Need'.
"""
def __init__(self, d_model, max_len=5000, dropout=0.1):
super().__init__()
self.dropout = nn.Dropout(p=dropout)
# Cria a matriz de codificação posicional
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2).float() * (-np.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0) # Adiciona dimensão de batch
# Registra como buffer (não é parâmetro, mas salvo com o modelo)
self.register_buffer('pe', pe)
def forward(self, x):
"""
x: (batch, seq_len, d_model)
"""
x = x + self.pe[:, :x.size(1)]
return self.dropout(x)
Embeddings Posicionais Aprendidos
Uma Abordagem Alternativa
Muitos LLMs modernos usam embeddings posicionais aprendidos:
class LearnedPositionalEmbedding(nn.Module):
"""
Embeddings posicionais aprendidos (ex: usado no GPT-2).
"""
def __init__(self, max_len, d_model):
super().__init__()
self.position_embedding = nn.Embedding(max_len, d_model)
def forward(self, x):
"""
x: (batch, seq_len, d_model)
"""
positions = torch.arange(x.size(1), device=x.device)
return x + self.position_embedding(positions)
Comparação
| Aspecto | Senoidal | Aprendido |
|---|---|---|
| Parâmetros | Nenhum | max_len × d_model |
| Extrapolação | Melhor | Limitado ao max_len |
| Desempenho | Comparável | Comparável |
| Flexibilidade | Fixo | Adapta-se aos dados |
Rotary Position Embedding (RoPE)
Inovação Moderna
RoPE, usado em LLaMA, Mistral e muitos modelos modernos, codifica a posição por meio de rotação:
Principais Benefícios
- Posições relativas: Captura naturalmente informações de posição relativa
- Extrapolação: Melhor tratamento de sequências longas
- Eficiência: Aplicado diretamente em Q, K sem parâmetros adicionais
Implementação Simplificada
def rotary_embedding(x, seq_len, d_model):
"""
Implementação simplificada de RoPE.
x: (batch, seq_len, d_model)
"""
# Gera ângulos de rotação
position = torch.arange(seq_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
angles = position * div_term
# Aplica rotação aos pares de dimensões
x_rotated = x.clone()
x_rotated[..., 0::2] = x[..., 0::2] * torch.cos(angles) - x[..., 1::2] * torch.sin(angles)
x_rotated[..., 1::2] = x[..., 0::2] * torch.sin(angles) + x[..., 1::2] * torch.cos(angles)
return x_rotated
ALiBi: Attention with Linear Biases
Outra Abordagem
ALiBi (Attention with Linear Biases) adiciona uma penalidade linear baseada na distância:
Onde é uma inclinação específica de cada head.
Benefícios
- Sem parâmetros adicionais
- Excelente extrapolação de comprimento
- Implementação simples
Camada Completa de Embedding
Unindo Tudo
class TransformerEmbedding(nn.Module):
"""
Camada completa de embedding: token + posição.
"""
def __init__(self, vocab_size, d_model, max_len=5000,
pos_encoding='sinusoidal', dropout=0.1):
super().__init__()
self.token_embedding = nn.Embedding(vocab_size, d_model)
self.d_model = d_model
self.dropout = nn.Dropout(dropout)
if pos_encoding == 'sinusoidal':
self.pos_encoding = SinusoidalPositionalEncoding(d_model, max_len)
elif pos_encoding == 'learned':
self.pos_encoding = LearnedPositionalEmbedding(max_len, d_model)
else:
raise ValueError(f"Unknown pos_encoding: {pos_encoding}")
def forward(self, token_ids):
"""
token_ids: (batch, seq_len)
Retorna: (batch, seq_len, d_model)
"""
# Embeddings de token (escalados)
x = self.token_embedding(token_ids) * (self.d_model ** 0.5)
# Adiciona codificação posicional
x = self.pos_encoding(x)
return self.dropout(x)
# Uso
embedding = TransformerEmbedding(
vocab_size=50000,
d_model=512,
max_len=2048,
pos_encoding='sinusoidal'
)
tokens = torch.randint(0, 50000, (32, 100)) # Batch de 32, seq_len 100
embedded = embedding(tokens)
print(f"Output shape: {embedded.shape}") # (32, 100, 512)
O Quadro Completo
Veja como os tokens fluem pelos embeddings até a atenção:
IDs dos Tokens: [101, 7592, 2088, 102]
↓
Embedding de Token: Busca na tabela de embeddings
↓
[0.23, -0.45, ...] [0.89, 0.12, ...] [0.45, -0.23, ...] [0.67, 0.34, ...]
↓
Adiciona Codificação Posicional
↓
[0.23+PE_0, -0.45+PE_0, ...] [0.89+PE_1, 0.12+PE_1, ...] ...
↓
Multi-Head Attention
↓
Saída com contexto sensível à posição
Principais Pontos
- Informação de posição é essencial—Transformers são invariantes à permutação sem ela
- Embeddings de token convertem tokens discretos em vetores contínuos
- Codificação senoidal usa padrões de frequência fixa (Transformer original)
- Embeddings aprendidos se adaptam aos dados, mas têm comprimento máximo fixo
- RoPE codifica posições relativas por rotação (LLMs modernos)
- ALiBi usa vieses de atenção baseados em distância
Próximos Passos
Agora que entendemos a arquitetura completa do Transformer—atenção, multi-head attention e codificação posicional—estamos prontos para avançar para o Módulo 3, onde exploraremos como esses modelos massivos são treinados.