6 Representação Vetorial: Embeddings e Positional Encoding
Em modelos de Processamento de Linguagem Natural (NLP) baseados em Deep Learning, especificamente na arquitetura Transformer, os dados brutos (texto) não podem ser processados diretamente. O primeiro passo crítico é a transformação de tokens discretos (índices inteiros de um vocabulário) em representações vetoriais contínuas e densas, enriquecidas com informações sobre a ordem da sequência.
Este capítulo detalha a arquitetura da camada de entrada, composta por dois subcomponentes principais: Input Embeddings e Positional Encoding.
6.1 1. Input Embeddings (Camada de Embeddings)
A camada de Embeddings atua como uma tabela de consulta (lookup table) aprendível. Enquanto a representação One-Hot Encoding gera vetores esparsos e de altíssima dimensionalidade (tamanho do vocabulário), os Embeddings projetam esses tokens em um espaço vetorial denso de dimensão inferior (\(d_{model}\)), onde a proximidade geométrica reflete a similaridade semântica.
6.1.1 Características Técnicas
- Mapeamento: Cada ID de token \(x\) é mapeado para um vetor \(v \in \mathbb{R}^{d_{model}}\).
- Dimensionalidade (\(d_{model}\)): Um hiperparâmetro da arquitetura (ex: 512 no Transformer original, 4096 no GPT-3).
- Escalonamento de Variância: No artigo original “Attention Is All You Need”, os pesos dos embeddings são multiplicados por \(\sqrt{d_{model}}\). Isso é feito para contrabalancear a magnitude do produto escalar na camada de atenção subsequente, auxiliando na estabilidade dos gradientes durante o treinamento.
6.2 2. Positional Encoding (Codificação Posicional)
Diferente de Redes Neurais Recorrentes (RNNs) ou LSTMs, a arquitetura Transformer não processa dados sequencialmente; ela processa a sequência inteira em paralelo. Consequentemente, o mecanismo de Self-Attention é invariante à permutação. Sem uma injeção explícita de posição, o modelo veria as frases “O cão mordeu o homem” e “O homem mordeu o cão” como idênticas em termos de composição de tokens.
Para resolver isso, injetamos um vetor de Positional Encoding (PE) somando-o ao vetor de Embedding.
6.2.1 Implementação Matemática
A codificação posicional utiliza frequências de ondas senoidais e cossenos de diferentes comprimentos de onda. Para uma posição \(pos\) na sequência e uma dimensão \(i\) dentro do vetor de embedding:
\[ PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) \]
\[ PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right) \]
Por que esta fórmula? 1. Valores Determinísticos: Não requer parâmetros aprendíveis extras (embora embeddings posicionais aprendíveis também sejam usados em modelos modernos como BERT). 2. Relatividade Linear: Para qualquer deslocamento fixo \(k\), \(PE_{pos+k}\) pode ser representado como uma função linear de \(PE_{pos}\). Isso facilita para o modelo aprender a atender posições relativas (ex: o token anterior ou o próximo). 3. Extrapolação: Permite que o modelo processe sequências mais longas do que as vistas durante o treinamento.
6.3 3. Arquitetura do Fluxo de Dados
A combinação dessas duas camadas resulta na entrada final para os blocos do Transformer. A operação é uma soma elemento a elemento (element-wise addition), seguida geralmente por uma camada de Dropout para regularização.
6.3.1 Diagrama de Fluxo
graph TD
subgraph "Pré-processamento"
RawText[Texto Bruto] --> Tokenizer[Tokenizador]
Tokenizer --> TokenIDs[IDs dos Tokens (Inteiros)]
end
subgraph "Camada de Representação Vetorial"
TokenIDs --> EmbedLayer[Embedding Lookup Table]
EmbedLayer --> Scale[Escalar por sqrt(d_model)]
PosIndex[Índices de Posição 0..N] --> PosEncCalc[Cálculo Seno/Cosseno]
PosEncCalc --> PosVector[Vetor Positional Encoding]
Scale --> Sum((Soma Element-wise))
PosVector --> Sum
Sum --> Dropout[Dropout Layer]
end
Dropout --> TransformerBlock[Bloco Transformer]
style Sum fill:#f9f,stroke:#333,stroke-width:2px
style EmbedLayer fill:#bbf,stroke:#333,stroke-width:2px
style PosEncCalc fill:#bbf,stroke:#333,stroke-width:2px
6.4 4. Implementação de Referência (PyTorch)
Abaixo apresentamos uma implementação robusta e anotada, seguindo as especificações padrão da indústria.
import torch
import torch.nn as nn
import math
class InputEmbeddings(nn.Module):
def __init__(self, d_model: int, vocab_size: int):
"""
Args:
d_model (int): Dimensão do vetor de embedding.
vocab_size (int): Tamanho do vocabulário.
"""
super().__init__()
self.d_model = d_model
self.vocab_size = vocab_size
# Camada de Embedding padrão do PyTorch
self.embedding = nn.Embedding(vocab_size, d_model)
def forward(self, x):
# Escalonamento por sqrt(d_model) conforme paper original
return self.embedding(x) * math.sqrt(self.d_model)
class PositionalEncoding(nn.Module):
def __init__(self, d_model: int, seq_len: int, dropout: float):
"""
Args:
d_model (int): Dimensão do modelo.
seq_len (int): Comprimento máximo da sequência.
dropout (float): Taxa de dropout.
"""
super().__init__()
self.dropout = nn.Dropout(dropout)
# Cria uma matriz de (seq_len, d_model) com zeros
pe = torch.zeros(seq_len, d_model)
# Cria um vetor de posições (0, 1, ... seq_len-1)
# Shape: (seq_len, 1)
position = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(1)
# Termo divisor para as frequências (10000^(2i/d_model))
# Implementado em log-space para estabilidade numérica
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
# Aplica Seno aos índices pares (2i)
pe[:, 0::2] = torch.sin(position * div_term)
# Aplica Cosseno aos índices ímpares (2i+1)
pe[:, 1::2] = torch.cos(position * div_term)
# Adiciona dimensão de batch para facilitar o broadcast na soma: (1, seq_len, d_model)
pe = pe.unsqueeze(0)
# Registra como buffer (não é um parâmetro aprendível, mas faz parte do estado do modelo)
self.register_buffer('pe', pe)
def forward(self, x):
"""
Args:
x: Embeddings de entrada. Shape: (Batch_Size, Seq_Len, d_model)
"""
# Soma o embedding com o positional encoding (até o comprimento da sequência atual)
# x.requires_grad_(False) não é necessário pois pe é buffer, mas a soma mantém o gradiente de x
x = x + self.pe[:, :x.shape[1], :]
return self.dropout(x)
# Exemplo de uso integrado
class TransformerInputLayer(nn.Module):
def __init__(self, vocab_size, d_model, max_len, dropout=0.1):
super().__init__()
self.embeddings = InputEmbeddings(d_model, vocab_size)
self.positional_encoding = PositionalEncoding(d_model, max_len, dropout)
def forward(self, x):
x = self.embeddings(x)
x = self.positional_encoding(x)
return x6.4.1 Análise do Código
- Estabilidade Numérica: No cálculo do
div_term, utilizamostorch.expemath.log. Matematicamente, \(e^{\ln(x)} = x\). Isso evita potências diretas de números muito grandes ou muito pequenos, mantendo a precisão em ponto flutuante. - Buffers: O uso de
register_buffergarante que a matriz de Positional Encoding seja salva junto com o modelo (state_dict), mas não seja atualizada pelo otimizador (Backpropagation), pois é fixa. - Broadcasting: A forma do tensor
peé(1, seq_len, d_model). Isso permite que ele seja somado automaticamente a um batch de entradas(Batch, seq_len, d_model)sem necessidade de duplicação de memória.
6.5 5. Embedding não é significado isolado
A linha da tabela de embeddings fornece apenas o ponto de partida de um token. Depois de cada bloco, sua representação se torna contextual. Assim, ocorrências de “banco” em “banco de dados” e “banco da praça” começam com o mesmo vetor, mas terminam com estados ocultos diferentes por causa dos tokens vizinhos.
Se os IDs têm formato \((B,T)\), nn.Embedding produz $(B,T,d_{model}). A codificação posicional deve ser compatível com as duas últimas dimensões; a soma não concatena posição e conteúdo, portanto o formato permanece inalterado. Esse é um bom ponto para usarassert x.shape == (B,T,d_model)` durante a implementação.
Além das senoides, modelos atuais podem usar posições aprendidas, RoPE ou vieses relativos. O critério não é apenas representar ordem: é também generalizar para comprimentos diferentes e permitir que a atenção expresse distâncias relevantes.
Troque dois tokens de uma sequência e compare a soma embedding + posição. Depois, remova a codificação posicional e explique por que a autoatenção deixa de distinguir as duas ordens.
6.6 Posições relativas com RoPE
Embeddings posicionais absolutos somam um vetor que representa o índice. A codificação posicional rotativa, conhecida como RoPE, segue outra ideia: pares de coordenadas de consultas e chaves são girados por ângulos dependentes da posição. Como o produto escalar entre vetores girados depende da diferença entre os ângulos, a atenção recebe informação de distância relativa.
Para um par \((x_{2i},x_{2i+1})\) e ângulo \(\theta\), a rotação é
\[ \begin{bmatrix} x'_{2i}\\x'_{2i+1} \end{bmatrix} = \begin{bmatrix} \cos\theta & -\sin\theta\\ \sin\theta & \cos\theta \end{bmatrix} \begin{bmatrix} x_{2i}\\x_{2i+1} \end{bmatrix}. \]
Frequências diferentes são usadas ao longo das dimensões: algumas variam rapidamente e distinguem posições próximas; outras variam lentamente e preservam relações em escalas maiores. A rotação é aplicada a \(Q\) e \(K\), não necessariamente a \(V\), porque sua finalidade é alterar o cálculo de compatibilidade.
RoPE não torna o contexto ilimitado. Comprimentos muito além dos observados no treinamento podem produzir padrões angulares pouco familiares. Técnicas de escalonamento ajustam frequências ou posições, mas precisam ser avaliadas quanto a recuperação de informação, estabilidade e perda de resolução local.
O embedding representa o token; a transformação posicional modifica como tokens em lugares diferentes se relacionam. Embora sejam combinados no cálculo, essas funções ajudam a depurar o modelo separadamente.
6.7 Visualizando embeddings com cuidado
PCA, t-SNE e UMAP projetam vetores de alta dimensão para duas ou três dimensões. Essas figuras ajudam a formular hipóteses sobre agrupamentos, mas inevitavelmente distorcem relações. t-SNE enfatiza vizinhanças locais; UMAP tenta preservar mais estrutura global; PCA é linear e mais fácil de interpretar. Nenhum gráfico bidimensional prova, sozinho, que o modelo aprendeu um conceito.