Consulta, Chave, Valor
Três Mapas Lineares a Partir da Mesma Entrada
Após o embedding (atividade 4), cada posição carrega um vetor x_t de 768 dimensões. A atenção começa produzindo três projeções distintas de x:
Q (consulta): o que esta posição quer saber?
K (chave): o que esta posição oferece a outras posições?
V (valor): qual conteúdo esta posição entrega se for atendida?
Cada projeção vem de uma matriz de pesos aprendida:
Q = x · W_Q # Forma de W_Q: (d_model, d_k)
K = x · W_K # Forma de W_K: (d_model, d_k)
V = x · W_V # Forma de W_V: (d_model, d_k)
Três matrizes, todas treinadas via retropropagação. Um modelo aprende: nesta posição, qual consulta melhor recupera contexto passado útil? Qual chave anuncia bem o conteúdo desta posição? Qual valor é entregue se selecionado?
Analogia com uma Biblioteca
Imagine um catálogo de fichas de biblioteca. Você entra com um tópico em mente (sua query). Cada ficha lista palavras-chave (uma key). Quando seu tópico corresponde às palavras-chave de uma ficha, você pega o conteúdo do livro (um value). A atenção faz isso para cada token em paralelo: cada posição consulta todas as outras posições, classifica o alinhamento e recupera uma combinação ponderada de vetores de valor.
Dimensões do ANDREA-120M
| Quantidade | Valor | Notas |
|---|---|---|
| d_model | 768 | Tamanho do vetor em cada posição |
| n_head | 12 | Cabeças de atenção paralelas |
| d_k | 64 | Dimensão por cabeça (= d_model / n_head) |
| T | 1024 | Comprimento do contexto |
d_k = d_model / n_head = 768 / 12 = 64. Cada cabeça vê uma fatia de 64 dimensões de um espaço completo de 768 dimensões. A Atividade 6 (grow_a_language_model_multi_head) cobre a divisão por cabeça em detalhes.
Calcule d_k
Por que Dividir por sqrt(d_k)
Uma Matriz de Pontuação
Uma vez que Q & K existem (cada um com shape (T, d_k)), a atenção computa uma matriz de pontuação:
scores = Q · K^T # shape: (T, T)
scores[i, j] = quão fortemente a query da posição i se alinha com a key da posição j. Cada par (i, j) recebe uma pontuação: 1024 × 1024 = 1.048.576 pontuações por cabeça de atenção por passagem forward.
Por Que Dividir
Produtos escalares de dois vetores unitários aleatórios de dimensão d têm magnitude na ordem de sqrt(d). Sem escalonamento, as pontuações crescem com d_k:
- d_k = 64: produtos escalares típicos na ordem de 8.
- d_k = 256: produtos escalares típicos na ordem de 16.
- d_k = 4096: produtos escalares típicos na ordem de 64.
Pontuações altas produzem um softmax pontiagudo (uma posição domina, gradientes desaparecem em outros lugares). O treinamento para. A escalabilidade corrige uma magnitude:
scaled_scores = (Q · K^T) / sqrt(d_k)
Para ANDREA-120M, sqrt(d_k) = sqrt(64) = 8. Cada pontuação é dividida por 8. As magnitudes permanecem aproximadamente em escala unitária independentemente de d_k. O softmax permanece bem comportado. Os gradientes fluem.
Justificativa Original de Vaswani
Do Attention Is All You Need (2017): 'Para grandes valores de d_k, os produtos escalares crescem em magnitude, empurrando a função softmax para regiões onde ela tem gradientes extremamente pequenos.' Um divisor sqrt(d_k) contrabalança esse crescimento.
Uma Visão do Código
Dentro de microgpt_cuda.cu, este scaling aparece como uma divisão literal:
scores[i][j] = dot(Q[i], K[j]) * (1.0f / sqrtf(d_k));
Uma multiplicação de float por score. Barato. Crítico.
Escala em d_model = 4096
Por Que a Posição i Não Pode Ver a Posição j > i
Uma Restrição Nascida da Geração
A ANDREA gera um token por vez. Durante a inferência, a posição 0 produz o primeiro token, depois a posição 1 vê a saída da posição 0 & produz um segundo token, & assim por diante. Um modelo nunca tem acesso a tokens futuros durante a geração.
O treinamento deve espelhar isso. Se durante o treinamento a posição 5 pudesse atender à posição 6, o modelo aprenderia um atalho: 'prever o token 6 lendo o token 6'. Na inferência, esse atalho desaparece (o token 6 ainda não existe). O comportamento de treinamento-versus-inferência do modelo divergiria catastroficamente.
Uma Máscara
Uma máscara causal bloqueia a atenção de qualquer posição i para qualquer posição j > i. Implementação: defina scaled_scores[i][j] = -infinito onde quer que j > i. Após o softmax, essas entradas se tornam exp(-inf) = 0. A máscara zera a atenção para posições futuras de forma limpa.
for i in range(T):
for j in range(T):
if j > i:
scaled_scores[i][j] = -1e9 # efetivamente -inf
Após o softmax (por linha), cada linha soma 1, mas apenas as entradas [0, i] carregam massa de probabilidade. A posição i mistura informação apenas de posições passadas.
Visualizando uma Máscara
Uma matriz de pontuações com formato (T, T) com máscara aplicada parece uma estrutura triangular inferior:
scaled_scores após máscara, softmax linha por linha:
linha 0: [1.0, 0, 0, 0, ...] # vê apenas a si mesma
linha 1: [0.4, 0.6, 0, 0, ...] # vê as posições 0, 1
linha 2: [0.2, 0.3, 0.5, 0, ...] # vê 0, 1, 2
linha 3: [0.1, 0.2, 0.3, 0.4, ...] # vê 0, 1, 2, 3
...
Distribuição de probabilidade estritamente triangular inferior por linha. O futuro permanece invisível.
Por Que um Transformer Apenas Decoder Precisa Disso
Modelos decoder-only como ANDREA, GPT e LLaMA compartilham um objetivo: prever o próximo token a partir do passado. Uma máscara causal torna esse objetivo treinável em paralelo: toda posição computa sua própria previsão de próximo token de uma vez, e nenhuma posição trapaceia espiando adiante.
Máscara & Sabor
De Pontuações para Saída
Softmax: De Pontuações a Probabilidades
Pontuações mascaradas e escaladas ainda variam sobre números reais. O Softmax converte cada linha em uma distribuição de probabilidade:
A[i][j] = exp(scaled_scores[i][j]) / sum_k exp(scaled_scores[i][k])
Três propriedades resultam:
- A[i][j] >= 0 para todos (i, j).
- sum_j A[i][j] = 1 para toda linha i.
- Maiores pontuações brutas produzem maiores probabilidades (monótona).
O vetor de probabilidades da linha i diz ao modelo: quanto a posição i deve prestar atenção a cada posição anterior ao computar sua saída?
Soma Ponderada de V
Uma saída final de atenção para a posição i:
output[i] = sum_j A[i][j] · V[j]
Cada vetor de valor V[j] é ponderado pela probabilidade de atenção A[i][j], depois somado. A saída da posição i combina vetores de valor de todas as posições anteriores, ponderados por relevância.
Na forma de matriz, todas as posições de uma vez:
Attention(Q, K, V) = softmax(mask(Q · K^T / sqrt(d_k))) · V
Uma linha. Todo um mecanismo de atenção. Vaswani et al. escreveram essa linha em 2017; os transformers não mudaram fundamentalmente desde então.
Formato de Saída por Cabeça
Saída de uma cabeça de atenção: formato (T, d_k). Para ANDREA-120M: (1024, 64). Todas as 12 cabeças computam em paralelo; suas saídas são concatenadas para (1024, 768) & alimentam uma projeção linear final (W_O), depois para o MLP de um bloco transformer.
A Atividade 6 (grow_a_language_model_multi_head) cobre a divisão multi-cabeça. A Atividade 7 (grow_a_language_model_transformer_block) cobre tudo que envolve a atenção: conexões residuais, normalização de camada, MLP.