Attenzione Più MLP, Ripetuto
Due Sottolivelli
Un blocco transformer contiene esattamente due sottolivelli, ciascuno che opera su una sequenza di token di forma [batch, seq_len, d_model]:
1. Sottostrato di attenzione multi-head. I token si guardano a vicenda. L'Attività 68 ha trattato questo in dettaglio. La forma dell'output corrisponde alla forma dell'input.
2. Sottostrato MLP feed-forward. Ogni token si trasforma indipendentemente attraverso un percettrone a due strati. Nessun flusso di informazioni tra token. La forma dell'output corrisponde alla forma dell'input.
Entrambi i sottostrati preservano la forma [batch, seq_len, d_model]. Questa preservazione permette di impilare i blocchi: l'output del layer N alimenta l'input del layer N+1 senza acrobazie di forma.
Cosa Contribuisce Ogni Sottostrato
L'attenzione sposta le informazioni tra le posizioni: un token in posizione 17 può estrarre informazioni dalle posizioni 1 attraverso 16. L'MLP trasforma le informazioni all'interno di ogni posizione: la rappresentazione di un token viene rimodellata attraverso funzioni non lineari apprese, ma non vede mai i suoi vicini.
Lo stacking dei blocchi alterna queste due operazioni. L'attenzione del Layer 1 mescola le posizioni. L'MLP del Layer 1 rimodella per-posizione. L'attenzione del Layer 2 mescola di nuovo, ora sulle rappresentazioni rimodellate. Questa alternanza aumenta il potere espressivo con la profondità.
Lo Stack di ANDREA
| Variant | 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 |
Nota che mlp_dim = 4 × d_model in tutta la famiglia. Questo rapporto vale in quasi ogni transformer moderno. La Sezione 3 spiega il perché.
Nominare i Sottostrati
Perché le Connessioni di Skip Sono Importanti
Il Pattern Residuo
Ogni sublayer avvolge il suo calcolo in una connessione residua. L'output aggiunge indietro l'input:
x = x + Attention(LayerNorm(x)) # sublayer attention
x = x + MLP(LayerNorm(x)) # Sottolivello MLP
All'interno di ciascun sottolivello, la funzione Attention(...) o MLP(...) produce un delta. Il blocco non sostituisce l'input; aggiunge una correzione appresa.
Perché è Importante
Tre ragioni per cui le connessioni residue dominano le architetture profonde:
1. Flusso del gradiente. Durante la backpropagation, l'addizione fornisce ai gradienti un percorso diretto dall'output all'input, bypassando il sottomodulo. Una pila di 12 layer senza residui perderebbe il segnale del gradiente molto prima di raggiungere gli embedding; con i residui, la magnitudine del gradiente rimane utilizzabile attraverso centinaia di layer.
2. Inizializzazione identity. All'inizializzazione, i pesi del sottomodulo producono output piccoli. La connessione residua significa che il layer N inizialmente passa quasi invariato. L'addestramento impara i delta progressivamente da un punto di partenza funzionante.
3. Apprendimento composizionale. Ogni blocco impara un raffinamento, non una sostituzione. Il Layer 1 potrebbe aggiungere informazioni posizionali. Il Layer 2 potrebbe aggiungere struttura sintattica. Il Layer 7 potrebbe aggiungere relazioni semantiche. Il flusso residuo accumula.
Normalizzazione del Layer
Prima di ogni sottomodulo, LayerNorm ridimensiona la rappresentazione di ogni token a media zero e varianza unitaria, poi applica gain e bias appresi per feature:
[BLOCK_TYPE CONTENT residuals_and_norm/residual_motivation]
y = gamma * (x - mean(x)) / sqrt(var(x) + epsilon) + beta
La media e la varianza vengono calcolate lungo la dimensione d_model, separatamente per ogni token. Due vettori appresi (gamma, beta, ciascuno di forma [d_model]) ripristinano la scala espressiva. La normalizzazione mantiene le attivazioni in un intervallo numericamente stabile; senza di essa, piccole instabilità durante l'addestramento si accumulano in gradienti NaN.
Pre-Norm vs Post-Norm
Una Scelta Sottile Ma Critica
Due modalità per integrare la layer norm in un sottolivello residuo:
Post-norm (paper originale del 2017):
x = LayerNorm(x + Attention(x))
La layer norm si trova dopo l'addizione residua. Il flusso residuo stesso viene normalizzato in ogni layer.
Pre-norm (standard moderno, usato in ANDREA):
x = x + Attention(LayerNorm(x))
La layer norm si trova prima del sublayer, all'interno del ramo residuo. Il residual stream rimane non normalizzato; solo l'input al sublayer viene ridimensionato.
Perché Pre-Norm Ha Vinto
Post-norm si allena male senza LR warmup e un tuning attento degli iperparametri. I gradienti esplodono nei layer iniziali perché ogni layer norm rimescola lo stato accumulato del residual stream. L'articolo originale del 2017 usava post-norm con un tuning estensivo; lavori successivi (GPT-2, LLaMA, ANDREA) hanno standardizzato su pre-norm.
L'addestramento pre-norm è stabile. Lo stream residuo si accumula in modo pulito attraverso tutti i layer; solo gli input dei sublayer vengono normalizzati per stabilità numerica. I transformer moderni usano di default pre-norm, & ANDREA eredita quella scelta.
Equazione del Blocco Finale
Combinando i residuali, la layer norm in posizione pre-norm, & entrambi i sublayer si ottiene il blocco completo di ANDREA:
```python
def block_forward(x):
```
x = x + Attention(LayerNorm(x)) # sottoclasse attention
x = x + MLP(LayerNorm(x)) # sottoclasse MLP
return x
Due sottoclassi, due addizioni residue, due normalizzazioni di layer (nota: ogni sottoclasse ha la propria normalizzazione di layer; ANDREA-120M ha 24 normalizzazioni di layer attraverso 12 blocchi più una finale in output, quindi 25 totali). Ripeti 12 volte. Questo è il tronco di ANDREA-120M.
Perché Pre-Norm Stabilizza l'Addestramento
Due Layer Lineari, Un'Attivazione
Tre Operazioni
Il sottolayer MLP è un perceptron a due layer con un'attivazione non lineare tra i layer:
def mlp_forward(x):
h = x · W_1 + b_1 # espansione: d_model → mlp_dim
h = GELU(h) # attivazione non lineare
y = h · W_2 + b_2 # contrazione: mlp_dim → d_model
return y
Tre operazioni. Due lineari, una non lineare. La prima lineare espande la larghezza; la seconda la contrae di nuovo.
Il Rapporto di Espansione 4×
I transformer moderni impostano mlp_dim = 4 × d_model. ANDREA-120M:
d_model = 768
mlp_dim = 4 × 768 = 3072
Forma di W_1 = [768, 3072] # ~2.36M parametri
Forma di W_2 = [3072, 768] # ~2.36M parametri
Parametri MLP per blocco = 4.72M (ignorando i bias)
Due MLP si trovano tra ogni coppia di sottocameri di attenzione (una per blocco). Dodici blocchi × 4.72M ≈ 56.6M parametri MLP totali in ANDREA-120M, circa la metà di tutti i parametri.
Perché 4×
Il rapporto 4× è emerso empiricamente. Rapporti più piccoli riducono la capacità del modello. Rapporti più grandi producono rendimenti decrescenti per parametro speso. Attraverso decenni di ricerca architetturale, il 4× ha resistito; appare in GPT, BERT, T5, LLaMA e ANDREA.
Lavori recenti (PaLM, Chinchilla) hanno scoperto che i meccanismi di gating (SwiGLU) possono usare un'espansione 8/3× con capacità comparabile a minor costo; ANDREA rimane con il classico GELU + 4× per semplicità.
GELU: Un'Attivazione Fluida
Cosa Calcola GELU
GELU (Gaussian Error Linear Unit) è l'attivazione standard tra i layer MLP nei transformer moderni. La sua formula:
GELU(x) = x · Φ(x)
Φ(x) è la funzione di distribuzione cumulativa della normale standard: la probabilità che una variabile casuale gaussiana standard cada a o sotto x. Calcolata numericamente:
Φ(x) ≈ 0.5 × (1 + tanh(sqrt(2/π) × (x + 0.044715 × x³)))
Comportamento Per Regione
- Per x grandi positivi: Φ(x) ≈ 1, quindi GELU(x) ≈ x. Come ReLU.
- Per x grandi negativi: Φ(x) ≈ 0, quindi GELU(x) ≈ 0. Come ReLU.
- Vicino a x = 0: Φ(x) ≈ 0.5, quindi GELU(0) = 0 esattamente. Transizione fluida attraverso l'origine.
A differenza di ReLU, GELU lascia passare alcuni input negativi, pesati da Φ(x). Un piccolo input negativo contribuisce ancora a un piccolo output negativo, solo meno dell'input completo.
Perché GELU ha superato ReLU
Sperimentalmente, i transformer addestrati con GELU raggiungono una perdita inferiore rispetto ai transformer addestrati con ReLU con lo stesso numero di parametri. La regolarità intorno allo zero è importante: il taglio netto di ReLU a zero produce discontinuità nei gradienti; la curva regolare di GELU fornisce gradienti più puliti per la backpropagation.
Il motore di addestramento di ANDREA microgpt_cuda.cu include un kernel CUDA GELU scritto a mano. Il kernel usa l'approssimazione tanh sopra; le GPU moderne includono tanh come operazione a istruzione singola.
Calcolo dei Parametri MLP
Dodici Blocchi Compongono ANDREA-120M
Dal Blocco al Modello
Il forward pass completo di ANDREA-120M:
def model_forward(token_ids):
x = token_embed(token_ids) + position_embed(positions)
for block_idx in range(n_layer): # 12 blocchi
x = block_forward(x) # attention + MLP w/ residuals
x = LayerNorm(x) # normalizzazione finale
logits = x · token_embed.T # pesi condivisi per la proiezione di output
return logits
Sei linee. La parte principale vive dentro block_forward, chiamata dodici volte. Gli embedding avviano il pipeline; l'unembedding legato (la stessa matrice usata per la ricerca input, trasposta per la proiezione output) lo conclude.
Profondità Come Composizione
Ogni blocco legge lo residual stream, calcola una delta, e la aggiunge indietro. Dopo dodici passaggi, lo stream contiene contributi accumulati da ogni blocco. Internamente, i layer tendono a specializzarsi:
- Layer iniziali (1-3): pattern sintattici, struttura posizionale
- Layer medi (4-8): relazioni tra parole, confini di frasi
- Layer finali (9-12): contenuto semantico, richiamo fattuale
Questa specializzazione emerge dalla pressione dell'addestramento, non dalle scelte architettoniche. Lo stesso design di blocco uniforme produce strati specializzati quando addestrato sul linguaggio.
Parametri Totali del Blocco
| Componente | Per blocco | Su 12 blocchi |
|---|---|---|
| Proiezioni di Attention (4×W) | 2.36M | 28.3M |
| Pesi MLP (W_1 + W_2) | 4.72M | 56.6M |
| Layer norms (gamma, beta) | ~3K (negligibile) | ~37K |
| Totale per blocco | ~7.1M | ~85M |
85M parametri nel tronco. Aggiungi ~13M negli embedding dei token (8449 vocabolario × 768 d_model × 2 per input/output legati) più una layer norm finale, e il conteggio parametri di ANDREA-120M arriva a circa 120M. Il design del blocco rappresenta due terzi; gli embedding il resto.