Atenção Mais MLP, Repetida
Duas Subcamadas
Um bloco transformer contém exatamente duas subcamadas, cada uma operando em uma sequência de tokens com forma [batch, seq_len, d_model]:
1. Camada de atenção multi-head. Os tokens olham uns para os outros. A Atividade 68 cobriu isso em detalhes. O formato de saída corresponde ao formato de entrada.
2. Camada MLP feed-forward. Cada token é transformado independentemente por meio de um perceptron de duas camadas. Não há fluxo de informação entre tokens. O formato de saída corresponde ao formato de entrada.
Ambas as sublayers preservam o formato [batch, seq_len, d_model]. Essa preservação permite empilhar blocos: a saída da camada N alimenta a entrada da camada N+1 sem acrobacias de formato.
O Que Cada Sublayer Contribui
A atenção move informação entre posições: um token na posição 17 pode puxar informação das posições 1 a 16. A MLP transforma informação dentro de cada posição: a representação de um token é remodelada por funções não lineares aprendidas, mas nunca vê seus vizinhos.
Empilhar blocos alterna essas duas operações. A atenção da Camada 1 mistura posições. O MLP da Camada 1 remodela por posição. A atenção da Camada 2 mistura novamente, agora sobre as representações remodeladas. Essa alternância aumenta o poder expressivo com a profundidade.
A Pilha da ANDREA
| Variante | n_layer | n_head | d_model | mlp_dim |
|---|---|---|---|---|
| ANDREA-12M | 6 | 12 | 384 | 1536 |
| ANDREA-120M | 12 | 12 | 768 | 3072 |
| ANDREA-480M | 16 | 24 | 1536 | 6144 |
Note que mlp_dim = 4 × d_model em toda a família. Essa proporção se mantém em quase todos os transformers modernos. A Seção 3 explica o porquê.
Nomeando as Sublayers
Por que as Conexões de Pulo São Importantes
O Padrão Residual
Cada subcamada envolve seu cálculo em uma conexão residual. A saída adiciona de volta a entrada:
x = x + Attention(LayerNorm(x)) # subcamada de atenção
x = x + MLP(LayerNorm(x)) # subcamada MLP
Dentro de cada subcamada, a função Attention(...) ou MLP(...) produz um delta. O bloco não substitui a entrada; ele adiciona uma correção aprendida.
Por Que Isso Importa
Três razões pelas quais as conexões residuais dominam arquiteturas profundas:
1. Fluxo de gradiente. Durante a retropropagação, a adição dá aos gradientes um caminho direto da saída de volta para a entrada, contornando a subcamada. Uma pilha de 12 camadas sem resíduos perderia o sinal de gradiente muito antes de alcançar os embeddings; com resíduos, a magnitude do gradiente permanece utilizável através de centenas de camadas.
2. Inicialização de identidade. Na inicialização, os pesos da subcamada produzem saídas pequenas. A conexão residual significa que a camada N inicialmente passa quase inalterada. O treinamento aprende deltas progressivamente a partir de um ponto de partida funcional.
3. Aprendizado composicional. Cada bloco aprende uma refinamento, não uma substituição. A Camada 1 pode adicionar informações posicionais. A Camada 2 pode adicionar estrutura sintática. A Camada 7 pode adicionar relações semânticas. O fluxo residual acumula.
Normalização de Camada
Antes de cada subcamada, LayerNorm reescala a representação de cada token para média zero e variância unitária, depois aplica ganho e viés aprendidos por feature:
y = gamma * (x - mean(x)) / sqrt(var(x) + epsilon) + beta
A média & variância são calculadas ao longo da dimensão d_model, separadamente para cada token. Dois vetores aprendidos (gamma, beta, cada um com formato [d_model]) restauram a escala expressiva. A normalização mantém as ativações em uma faixa numericamente estável; sem ela, pequenas instabilidades no treinamento se acumulam em gradientes NaN.
Pré-Norm vs Pós-Norm
Uma Escolha Sutil Mas Crítica
Duas maneiras de conectar a normalização de camada em uma subcamada residual:
Pós-norm (artigo original de 2017):
x = LayerNorm(x + Attention(x))
A normalização de camada fica após a adição residual. O próprio fluxo residual é normalizado em cada camada.
Pré-norm (padrão moderno, usado no ANDREA):
x = x + Attention(LayerNorm(x))
A normalização de camada fica antes da subcamada, dentro do ramo residual. O fluxo residual permanece sem normalização; apenas a entrada da subcamada é reescalada.
Por que o Pré-Norm Venceu
O pós-norm treina mal sem aquecimento de LR e ajuste cuidadoso de hiperparâmetros. Os gradientes explodem nas camadas iniciais porque cada normalização de camada embaralha o estado acumulado do fluxo residual. O artigo original de 2017 usou pós-norm com ajuste extensivo; trabalhos subsequentes (GPT-2, LLaMA, ANDREA) padronizaram no pré-norm.
O treinamento pré-norm é estável. O fluxo residual acumula de forma limpa em todas as camadas; apenas as entradas das subcamadas são normalizadas para estabilidade numérica. Transformers modernos usam pré-norm por padrão, e o ANDREA herda essa escolha.
Equação Final do Bloco
Combinando resíduos, layer norm na posição pré-norm e ambas as sublayers, obtemos o bloco completo do ANDREA:
```python
def block_forward(x):
```
x = x + Attention(LayerNorm(x)) # subcamada de atenção
x = x + MLP(LayerNorm(x)) # subcamada MLP
return x
Duas subcamadas, duas adições residuais, duas normalizações de camada (nota: cada subcamada tem sua própria normalização de camada; ANDREA-120M tem 24 normalizações de camada em 12 blocos mais uma final na saída, totalizando 25). Repita 12 vezes. Essa é o tronco do ANDREA-120M.
Por que o Pre-Norm Estabiliza o Treinamento
Duas Camadas Lineares, Uma Ativação
Três Operações
A sublayer MLP é um perceptron de duas camadas com uma ativação não linear entre as camadas:
def mlp_forward(x):
h = x · W_1 + b_1 # expandir: d_model → mlp_dim
h = GELU(h) # ativação não linear
y = h · W_2 + b_2 # contrair: mlp_dim → d_model
return y
Três operações. Duas lineares, uma não linear. A primeira linear expande a largura; a segunda contrai de volta.
A Razão de Expansão 4×
Transformers modernos definem mlp_dim = 4 × d_model. ANDREA-120M:
d_model = 768
mlp_dim = 4 × 768 = 3072
Formato de W_1 = [768, 3072] # ~2.36M params
Formato de W_2 = [3072, 768] # ~2.36M params
Parâmetros MLP por bloco = 4.72M (ignorando biases)
Duas MLPs ficam entre cada par de subcamadas de atenção (uma por bloco). Doze blocos × 4.72M ≈ 56.6M parâmetros MLP no total em ANDREA-120M, aproximadamente metade de todos os parâmetros.
Por que 4×
A proporção 4× surgiu empiricamente. Proporções menores reduzem a capacidade do modelo. Proporções maiores produzem retornos decrescentes por parâmetro gasto. Ao longo de décadas de busca por arquitetura, 4× se manteve; ela aparece em GPT, BERT, T5, LLaMA e ANDREA.
Trabalhos recentes (PaLM, Chinchilla) descobriram que mecanismos de gating (SwiGLU) podem usar expansão 8/3× com capacidade comparável a um custo menor; ANDREA mantém o clássico GELU + 4× por simplicidade.
GELU: Uma Ativação Suave
O que o GELU Calcula
GELU (Gaussian Error Linear Unit) é a ativação padrão entre as camadas MLP em transformadores modernos. Sua fórmula:
GELU(x) = x · Φ(x)
Φ(x) é a função de distribuição acumulada do normal padrão: a probabilidade de que uma variável aleatória gaussiana padrão caia em ou abaixo de x. Calculada numericamente:
Φ(x) ≈ 0.5 × (1 + tanh(sqrt(2/π) × (x + 0.044715 × x³)))
Comportamento Por Região
- Para x positivo grande: Φ(x) ≈ 1, logo GELU(x) ≈ x. Como ReLU.
- Para x negativo grande: Φ(x) ≈ 0, logo GELU(x) ≈ 0. Como ReLU.
- Perto de x = 0: Φ(x) ≈ 0.5, logo GELU(0) = 0 exatamente. Transição suave através da origem.
Diferente do ReLU, o GELU permite que alguns valores negativos passem, ponderados por Φ(x). Um pequeno valor negativo ainda contribui com uma pequena saída negativa, apenas menor que a entrada completa seria.
Por que o GELU Superou o ReLU
Empiricamente, transformers treinados com GELU alcançam perda menor do que transformers treinados com ReLU com o mesmo número de parâmetros. A suavidade ao redor de zero importa: o corte abrupto do ReLU em zero produz descontinuidades no gradiente; a curva suave do GELU fornece gradientes mais limpos para a retropropagação.
O motor de treinamento da ANDREA microgpt_cuda.cu inclui um kernel CUDA GELU escrito à mão. O kernel usa a aproximação tanh acima; GPUs modernas incluem tanh como uma operação de instrução única.
Calculando Parâmetros do MLP
Doze Blocos Compõem o ANDREA-120M
Do Bloco ao Modelo
Passada forward completa do ANDREA-120M:
def model_forward(token_ids):
x = token_embed(token_ids) + position_embed(positions)
for block_idx in range(n_layer): # 12 blocos
x = block_forward(x) # attention + MLP com resíduos
x = LayerNorm(x) # normalização final
logits = x · token_embed.T # pesos compartilhados para projeção de saída
return logits
Seis linhas. O grosso vive dentro de block_forward, chamado doze vezes. Embeddings iniciam o pipeline; unembedding amarrado (a mesma matriz usada para lookup de entrada, transposta para projeção de saída) o finaliza.
Profundidade Como Composição
Cada bloco lê o residual stream, computa um delta e o adiciona de volta. Após doze passadas, o stream contém contribuições acumuladas de cada bloco. Internamente, as camadas tendem a se especializar:
- Camadas iniciais (1-3): padrões sintáticos, estrutura posicional
- Camadas médias (4-8): relações entre palavras, limites de frases
- Camadas finais (9-12): conteúdo semântico, recall factual
Essa especialização emerge da pressão de treinamento, não de escolhas arquiteturais. O mesmo design de bloco uniforme produz camadas especializadas quando treinado em linguagem.
Parâmetros Totais do Bloco
| Component | Por bloco | Em 12 blocos |
|---|---|---|
| Projeções de Atenção (4×W) | 2.36M | 28.3M |
| Pesos MLP (W_1 + W_2) | 4.72M | 56.6M |
| Normalizações de camada (gamma, beta) | ~3K (negligível) | ~37K |
| Total por bloco | ~7.1M | ~85M |
85M parâmetros no tronco. Adicione ~13M em embeddings de tokens (8449 vocabulário × 768 d_model × 2 para entrada/saída atada) mais uma normalização de camada final, & o número de parâmetros do ANDREA-120M chega a aproximadamente 120M. O design do bloco representa dois terços; embeddings o resto.