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

Multi-head
e a máscara causal

Por que doze cabeças em vez de uma — e a única linha de código que separa o GPT do RoBERTa.

Apostila 04 de 15 Pré-requisito: apostila 03

1 O problema de uma cabeça só

A atenção da apostila 03 funciona, mas tem uma limitação séria: cada linha do mapa de atenção soma 1. Isso significa que a atenção é um orçamento fixo. Se um token gasta 70% olhando para seu antecedente sintático, sobram 30% para todo o resto.

Só que uma palavra participa de várias relações simultâneas. Pegue "mordeu" em "o cachorro grande mordeu o carteiro ontem":

Com uma cabeça só, essas quatro necessidades competem pelo mesmo orçamento de 100%. O modelo é forçado a fazer uma média entre relações que são qualitativamente diferentes — e uma média de coisas diferentes costuma ser uma coisa ruim.

Analogia

Imagine investigar um crime com um único perito que precisa dividir seu tempo entre digitais, DNA, câmeras e testemunhas. Ele vai fazer tudo pela metade. Agora imagine doze peritos especializados, cada um com 100% do seu tempo dedicado a uma pista, todos trabalhando ao mesmo tempo, e um relatório final que combina os doze laudos.

Isso é multi-head attention. Cada cabeça tem seu próprio orçamento de 100% e sua própria especialidade — aprendida sozinha, sem ninguém mandar.

2 Como funciona: dividir, não multiplicar

O detalhe que deixa a ideia elegante: multi-head não custa mais caro. Em vez de doze cabeças de 768 dimensões cada, o modelo divide as 768 dimensões existentes em doze fatias de 64.

dk = dmodelh = 76812 = 64 h = número de cabeças. Cada cabeça trabalha num subespaço de 64 dimensões. O custo total é idêntico ao de uma cabeça de 768 — só a organização interna mudou.

O procedimento completo:

cabeçai = Atenção(hWiQ, hWiK, hWiV)
MultiHead(h) = Concat(cabeça1, …, cabeçah) WO Concatena as 12 saídas de 64 dimensões, recuperando 768. Depois passa por WO (768×768, aprendida), que mistura as contribuições — é ela que decide como combinar os doze laudos.

A matriz WO é essencial e frequentemente ignorada. Sem ela, as doze cabeças ficariam em compartimentos estanques, cada uma escrevendo em suas 64 dimensões e nunca conversando. É WO (chamada c_proj no GPT-2) que permite que a informação da cabeça 3 influencie as dimensões que a cabeça 7 usa.

As dimensões, na prática

GPT-2 small, frase de 5 tokens, 12 cabecas:

h                    (5, 768)
  |
c_attn (768 -> 2304) (5, 2304)          uma matmul so
  split em 3         Q,K,V: (5, 768) cada
  reshape p/ cabecas Q,K,V: (12, 5, 64) <- 12 cabecas em paralelo
  |
atencao por cabeca   (12, 5, 64)         12 mapas 5x5 independentes
  |
concat (juntar)      (5, 768)            reagrupa as fatias
c_proj (768 -> 768)  (5, 768)            mistura as cabecas
  |
saida                (5, 768)            mesma forma da entrada

O que as cabeças de fato aprendem

Ninguém atribui papéis. As especializações emergem do treino. Estudos de interpretabilidade em GPT-2 e BERT encontraram consistentemente cabeças como:

Tipo de cabeçaO que ela faz
Posicionalolha sempre o token imediatamente anterior, independente do conteúdo. Uma memória de curtíssimo prazo.
Sintáticaliga verbo↔sujeito, substantivo↔adjetivo, preposição↔complemento.
De induçãoa mais fascinante: detecta que a sequência "A B" já apareceu antes e, ao ver "A" de novo, aponta para "B". É o mecanismo por trás do aprendizado em contexto (in-context learning).
Coreferencialliga pronome ao antecedente — a cabeça que resolve o caso do troféu.
Nula / "vala"despeja quase toda a atenção no primeiro token. Parece defeito, mas é um mecanismo de escape: quando não há nada relevante para buscar, a cabeça precisa de algum lugar onde gastar seus 100% sem contaminar o resultado.

3 A máscara causal — a linha que define tudo

Agora chegamos ao ponto mais importante do curso inteiro. Preste bastante atenção nesta seção, porque tudo na Parte III depende dela.

O problema

O GPT-2 é treinado para prever a próxima palavra. Damos "O Brasil é" e ele deve produzir "gigante". Mas, para ser eficiente, o treino não roda uma palavra de cada vez — ele processa a frase inteira de uma vez e cobra todas as previsões simultaneamente:

Entrada:  O      Brasil  é       gigante  pela
Alvo:     Brasil é       gigante pela     própria
          ^      ^       ^       ^        ^
          5 previsoes cobradas ao mesmo tempo, num unico forward pass.

E aqui está a catástrofe: a atenção da apostila 03 deixa cada token ver todos os outros — inclusive os que vêm depois. Ao tentar prever que depois de "é" vem "gigante", o token "é" está olhando diretamente para "gigante".

O modelo aprenderia a tarefa trivial de copiar a resposta. Na hora de gerar texto de verdade, onde o futuro não existe, ele não saberia fazer nada. É vazamento de rótulo — o pecado capital do aprendizado supervisionado.

A solução: proibir explicitamente

Antes do softmax, some uma matriz de máscara que coloca −∞ em toda posição futura:

Mij = 0   se j ≤ i  (passado ou presente: pode) −∞  se j > i  (futuro: proibido)
A = softmax(QK⊤√dk + M)

Por que −∞ e não zero?

Essa é a pergunta certa, e a resposta é bonita. Zerar o score não adianta — um score 0 ainda vira e0 = 1 no softmax, um peso perfeitamente respeitável. E zerar o peso depois do softmax quebraria a soma-1.

O −∞ funciona porque:

exp(−∞) = 0 O peso vira exatamente zero antes da normalização. Logo, o denominador do softmax também ignora essas posições, e as linhas continuam somando exatamente 1 — só que distribuído apenas entre o passado. Matematicamente limpo, sem remendo.

Na implementação usa-se um número muito negativo (-1e9, ou torch.finfo(dtype).min) para evitar NaN em aritmética de ponto flutuante.

O formato triangular

Mapa de atencao APOS a mascara (5 tokens). "." = proibido.

           O   Brasil  e   gigante  pela
   O     [0.99   .      .     .       .   ]  <- so ve a si mesmo
   Brasil[0.41  0.59    .     .       .   ]
   e     [0.12  0.55   0.33   .       .   ]
   gigante[0.08 0.31   0.19  0.42     .   ]
   pela  [0.05  0.22   0.11  0.38    0.24 ]  <- ve tudo ate aqui

Triangular inferior. Cada linha soma 1. O futuro nao existe.
A frase mais importante do curso

Adicionar essa máscara transforma um encoder em um decoder. É só isso. GPT-2 e RoBERTa compartilham a mesma equação de atenção, o mesmo bloco Transformer, quase o mesmo tokenizador e tamanhos quase idênticos. A diferença arquitetural entre "o modelo que gera texto" e "o modelo que compreende texto" é uma matriz triangular somada antes do softmax.

Tudo o mais — o objetivo de treino, o formato da saída, para que serve cada um — é consequência dessa escolha. Voltaremos a isso, em detalhe, na apostila 11.

A consequência prática: o cache KV

A máscara causal traz um presente de graça. Como o token 5 nunca influencia o token 3, os vetores K e V das posições anteriores nunca mudam quando um token novo é acrescentado.

Logo, ao gerar texto, não é preciso recalcular a frase inteira a cada palavra. Guarda-se K e V de tudo que já passou (o KV cache) e só se calcula o token novo. A geração cai de custo O(n²) por token para O(n).

Isso é o que torna a geração autoregressiva viável na prática — e o RoBERTa, sendo bidirecional, não pode fazer isso, porque nele acrescentar um token muda a representação de todos os outros.

4 O código completo

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

class AtencaoMultiCabeca(nn.Module):
    def __init__(self, d_model=768, n_cabecas=12, causal=True):
        super().__init__()
        self.h = n_cabecas
        self.d_k = d_model // n_cabecas      # 64
        self.causal = causal                  # <<< GPT=True, RoBERTa=False
        self.c_attn = nn.Linear(d_model, 3 * d_model)  # Q,K,V fundidos
        self.c_proj = nn.Linear(d_model, d_model)      # W^O

    def forward(self, x):
        B, n, d = x.shape

        # 1) uma matmul produz Q, K e V de uma vez
        q, k, v = self.c_attn(x).split(d, dim=2)

        # 2) (B, n, 768) -> (B, 12, n, 64): separa as cabecas
        q = q.view(B, n, self.h, self.d_k).transpose(1, 2)
        k = k.view(B, n, self.h, self.d_k).transpose(1, 2)
        v = v.view(B, n, self.h, self.d_k).transpose(1, 2)

        # 3) scores escalados -> (B, 12, n, n)
        att = (q @ k.transpose(-2, -1)) / math.sqrt(self.d_k)

        # 4) ===== A LINHA QUE DEFINE A ARQUITETURA =====
        if self.causal:
            futuro = torch.triu(torch.ones(n, n, dtype=torch.bool), diagonal=1)
            att = att.masked_fill(futuro, float("-inf"))
        # ==============================================

        att = F.softmax(att, dim=-1)
        y = att @ v                                    # (B, 12, n, 64)

        # 5) reagrupa as cabecas e mistura
        y = y.transpose(1, 2).contiguous().view(B, n, d)
        return self.c_proj(y)

Leia de novo o passo 4. causal=True → GPT-2. causal=False → RoBERTa. Todo o resto é idêntico.

5 No laboratório

.venv\Scripts\python.exe lab\04_multihead_mask.py
  1. Imprime a máscara triangular em ASCII, para você ver a forma.
  2. Roda a mesma entrada com causal=True e False, e mostra as duas matrizes de atenção lado a lado. A diferença visual é imediata.
  3. Prova o vazamento: muda um token do fim da frase e mede o quanto a representação do início mudou. No modo causal: zero. No bidirecional: bastante. Este é o teste que demonstra causalidade em vez de afirmar.
  4. Retrato das 12 cabeças da camada 0 do GPT-2 real: para cada cabeça, calcula a distância média a que ela olha e a entropia da sua distribuição. Você vai identificar as cabeças posicionais (distância ≈ 1) e as de "vala" (entropia baixíssima, tudo no token 0).
  5. Mede o ganho do KV cache gerando 50 tokens com e sem ele.

Exercício 4 — preveja antes de rodar

  1. Numa máscara causal de n=1024, que fração das n² posições é desperdiçada com −∞? (Aproximadamente metade — e nenhuma GPU comum aproveita isso. Por que será que ninguém otimiza?)
  2. O primeiro token de uma sequência causal só pode olhar para si mesmo. Sua linha de atenção é sempre [1.0, 0, 0, …]. Qual é a implicação disso para a utilidade do primeiro token? Isso explica a cabeça "vala"?
  3. Se você aplicasse a máscara depois do softmax (zerando os pesos futuros), o que quebraria?
  4. Difícil: o GPT-2 XL tem 25 cabeças e dmodel=1600. Logo dk = 64. Todos os GPT-2 mantêm dk = 64, variando só o número de cabeças. Por que essa constante? (Pense no √dk e na expressividade de um subespaço.)
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. Vaswani, A. et al. (2017). Attention Is All You Need. NeurIPS 2017. arXiv:1706.03762
  2. Pope, R. et al. (2023). Efficiently Scaling Transformer Inference. MLSys 2023. arXiv:2211.05102

A atenção move informação entre posições. Mas ela é essencialmente uma média ponderada — uma operação linear.
A apostila 05 mostra as peças que faltam: onde entra o pensamento não-linear, e como se empilham 48 camadas sem o treino explodir.