02-02-02 — Self-attention e multi-head attention (visual)

⏱ 15 min Fontes validadas em: 2026-04-29

TL;DR

Self-attention é o mecanismo pelo qual cada token "consulta" todos os outros para montar sua representação contextual. Usa três vetores: Query (o que estou procurando), Key (o que eu ofereço), Value (o que entrego se forem compatíveis). Multi-head faz isso N vezes em paralelo, cada "cabeça" capturando um aspecto diferente da linguagem.

A analogia com banco de dados

A maneira mais intuitiva de entender Q/K/V é por analogia com um sistema de busca:

  • Query (Q): A sua consulta — "o que estou procurando?"
  • Key (K): O índice de cada item — "sobre o que sou eu?"
  • Value (V): O conteúdo real de cada item — "o que você recebe se me selecionar"

No SQL, seria algo como: SELECT value FROM context WHERE similarity(query, key) > threshold

Mas em vez de binário (seleciona/não seleciona), attention é suave — cada token recebe uma mistura ponderada de todos os values, com pesos proporcionais à compatibilidade Q-K.

O cálculo de attention passo a passo

flowchart LR T[Token X] --> Q[Gera vetor Q] T --> K[Gera vetor K] T --> V[Gera vetor V] Q --> DOT[Produto Q · K de todos os tokens] DOT --> SCALE[Divide por √d_k] SCALE --> SOFT[Softmax → pesos de atenção] SOFT --> WV[Multiplica pelos Values] WV --> OUT[Representação contextual de X] style SOFT fill:#6366f1,color:#fff style OUT fill:#22c55e,color:#fff

A fórmula completa:

import torch
import torch.nn.functional as F
import math

def attention(Q, K, V):
    d_k = Q.size(-1)
    # Scores: compatibilidade entre cada Q e cada K
    scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
    # Softmax transforma scores em probabilidades (somam 1)
    weights = F.softmax(scores, dim=-1)
    # Output: soma ponderada dos Values
    return torch.matmul(weights, V)

Exemplo concreto: pronome e referente

Frase: "O banco aprovou o empréstimo porque ele tinha boa reputação."

Para o token "ele", o mecanismo de attention calcula pesos aproximadamente assim:

graph LR ELE[ele] -->|0.72| BANCO[banco] ELE -->|0.08| APROVOU[aprovou] ELE -->|0.04| O1[O] ELE -->|0.06| EMPRESTIMO[empréstimo] ELE -->|0.05| TINHA[tinha] ELE -->|0.05| BOA[boa] style BANCO fill:#6366f1,color:#fff style ELE fill:#374151,color:#fff

O modelo "aprende" que "ele" provavelmente se refere a "banco" (não a "empréstimo") porque essas co-ocorrências aparecem nos dados de treino com essa frequência. O peso 0.72 em "banco" faz a representação de "ele" absorver contexto de "banco".

Multi-head attention: perspectivas simultâneas

Uma única cabeça de attention captura um tipo de relação. Multi-head attention roda N cabeças independentes em paralelo, cada uma com seus próprios pesos Q, K, V aprendidos.

graph TB INPUT[Token embeddings] --> H1[Head 1\nrelações sintáticas] INPUT --> H2[Head 2\nco-referência] INPUT --> H3[Head 3\nrelações semânticas] INPUT --> HN[Head N...] H1 --> CONCAT[Concatena + Linear] H2 --> CONCAT H3 --> CONCAT HN --> CONCAT CONCAT --> OUT[Representação final] style CONCAT fill:#6366f1,color:#fff

O que cada cabeça aprende? Ninguém programa isso — emerge do treino. Pesquisas de interpretabilidade descobriram cabeças especializadas em:

  • Rastrear sujeito-verbo em longas sentenças
  • Identificar relações de co-referência
  • Capturar proximidade posicional
  • Detectar relações semânticas (hiperonímia, sinonímia)

Positional Encoding: como o modelo sabe a ordem

Self-attention é permutation-invariant — se você embaralhar os tokens, as operações Q/K/V não mudam. Para preservar a ordem, posição é injetada via positional encoding.

O Transformer original usa funções seno/coseno:

import numpy as np

def positional_encoding(seq_len, d_model):
    PE = np.zeros((seq_len, d_model))
    for pos in range(seq_len):
        for i in range(0, d_model, 2):
            PE[pos, i]   = np.sin(pos / 10000 ** (2*i / d_model))
            PE[pos, i+1] = np.cos(pos / 10000 ** (2*i / d_model))
    return PE

LLMs modernos usam RoPE (Rotary Position Embedding) ou ALiBi — variantes que generalizam melhor para sequências mais longas do que as vistas no treino.

🔬 Por que RoPE importa para context windows

O encoding posicional original do Transformer generaliza mal para posições não vistas no treino. RoPE (usado em Llama, Mistral, GPT-NeoX) codifica posição de forma relativa via rotação de vetores — o que permite extensão de context window com menos degradação. É um dos motivos pelos quais modelos com context window de 128k+ são viáveis hoje.

⚠️ Custo computacional do attention

Para N tokens, self-attention computa N × N pares de compatibilidade. Uma sequência de 32k tokens gera 1 bilhão de operações só na camada de attention — por layer. GPT-4 tem ~120 layers. Flash Attention (Dao et al., 2022) reescreveu esse cálculo para ser IO-aware, reduzindo uso de memória GPU em 10-20x sem alterar o resultado.

Como isso se conecta

  • 02-02-01: A arquitetura Transformer que usa self-attention como base
  • 02-02-03: Como encoder e decoder usam attention diferentemente
  • 02-05-01: Context window é limitada por custo de attention

Fontes

  1. Vaswani et al. (2017) — Attention Is All You Need — seções 3.2 e 3.3 cobrem Q/K/V e multi-head
  2. Dao et al. (2022) — FlashAttention: Fast and Memory-Efficient Exact Attention
  3. Su et al. (2021) — RoFormer: Enhanced Transformer with Rotary Position Embedding
  4. Elhage et al. (2021) — A Mathematical Framework for Transformer Circuits — Anthropic