Pular para o conteúdo principal

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:

SelfAttention(π(X))=π(SelfAttention(X))\text{SelfAttention}(\pi(X)) = \pi(\text{SelfAttention}(X))

Onde π\pi é 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:

E=Embedding(token_ids)E = \text{Embedding}(\text{token\_ids})

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:

PE(pos,2i)=sin⁡(pos100002i/dmodel)PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i+1)=cos⁡(pos100002i/dmodel)PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right)

Onde:

  • pospos = posição na sequência
  • ii = índice da dimensão
  • dmodeld_{model} = 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​

  1. Única: Cada posição tem uma codificação única
  2. Limitada: Valores sempre em [-1, 1]
  3. Posições relativas: Relação linear permite aprender posições relativas
  4. 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​

AspectoSenoidalAprendido
ParâmetrosNenhummax_len × d_model
ExtrapolaçãoMelhorLimitado ao max_len
DesempenhoComparávelComparável
FlexibilidadeFixoAdapta-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:

fq(xm,m)=xmeimθf_q(x_m, m) = x_m e^{im\theta}

Principais Benefícios​

  1. Posições relativas: Captura naturalmente informações de posição relativa
  2. Extrapolação: Melhor tratamento de sequências longas
  3. 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:

softmax(qi⋅kj−m⋅∣i−j∣)\text{softmax}(q_i \cdot k_j - m \cdot |i - j|)

Onde mm é uma inclinação específica de cada head.

Benefícios​

  1. Sem parâmetros adicionais
  2. Excelente extrapolação de comprimento
  3. 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​

Summary
  1. Informação de posição é essencial—Transformers são invariantes à permutação sem ela
  2. Embeddings de token convertem tokens discretos em vetores contínuos
  3. Codificação senoidal usa padrões de frequência fixa (Transformer original)
  4. Embeddings aprendidos se adaptam aos dados, mas têm comprimento máximo fixo
  5. RoPE codifica posições relativas por rotação (LLMs modernos)
  6. 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.