Attention Plus MLP, Herhaald
Twee Sublayers
Een transformerblok bevat precies twee sublayers, elk opererend op een tokenvolgorde van vorm [batch, seq_len, d_model]:
1. Multi-head attention sublaag. Tokens kijken naar elkaar. Activiteit 68 behandelde dit in detail. Output-vorm komt overeen met invoervorm.
2. Feed-forward MLP sublaag. Elk token transformeert onafhankelijk via een tweelaags perceptron. Geen informatiestroom tussen tokens. Output-vorm komt overeen met invoervorm.
Beide sublagen behouden de [batch, seq_len, d_model]-vorm. Die behoud laat blokken stapelen: output van laag N voedt invoer van laag N+1 zonder vormacrobatiek.
Wat Elke Sublag Bijdraagt
Attention verplaatst informatie over posities heen: een token op positie 17 kan informatie ophalen van posities 1 tot en met 16. De MLP transformeert informatie binnen elke positie: de representatie van een token wordt gevormd door geleerde niet-lineaire functies, maar ziet nooit zijn buren.
Het stapelen van blokken wisselt deze twee operaties af. Attention van laag 1 mixt posities. MLP van laag 1 herschaapt per positie. Attention van laag 2 mixt opnieuw, nu over de herschaapte representaties. Deze afwisseling vergroot de expressieve kracht met de diepte.
ANDREA's Stapel
| 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 |
Let op dat mlp_dim = 4 × d_model in de hele familie geldt. Die verhouding geldt in bijna elke moderne transformer. Sectie 3 legt uit waarom.
De Sublayers Benennen
Waarom Skip Connections Belangrijk Zijn
Het Residual Patroon
Elke sublayer wikkelt zijn berekening in een residual connection. De output voegt de input weer toe:
x = x + Attention(LayerNorm(x)) # attention sublayer
x = x + MLP(LayerNorm(x)) # MLP sublayer
Binnen elke sublaag produceert de functie Attention(...) of MLP(...) een delta. Het blok vervangt de invoer niet; het voegt een geleerde correctie toe.
Waarom Dit Belangrijk Is
Drie redenen waarom residual connecties domineren in diepe architecturen:
1. Gradientenstroom. Tijdens backpropagation geeft de optelling gradiënten een direct pad van uitvoer terug naar invoer, waarbij de sublaag wordt omzeild. Een stack van 12 lagen zonder residuals zou het gradiëntsignaal verliezen lang voordat het de embeddings bereikt; met residuals blijft de gradiëntmagnitude bruikbaar door honderden lagen heen.
2. Identiteitsinitialisatie. Bij initialisatie produceren sublaaggewichten kleine uitvoerwaarden. De residual connectie betekent dat laag N aanvankelijk bijna onveranderd doorgeeft. Training leert deltas progressief vanaf een werkend startpunt.
3. Compositief leren. Elke block leert een verfijning, geen vervanging. Laag 1 voegt mogelijk positionele informatie toe. Laag 2 voegt mogelijk syntactische structuur toe. Laag 7 voegt mogelijk semantische relaties toe. De residual stream accumuleert.
Laag Normalisatie
Voor elke sublaag rescaleert LayerNorm de representatie van elk token naar nul gemiddelde & unit variantie, en past vervolgens geleerde per-feature gain & bias toe:
y = gamma * (x - mean(x)) / sqrt(var(x) + epsilon) + beta
Gemiddelde & variantie worden berekend over de d_model dimensie, apart voor elk token. Twee geleerde vectoren (gamma, beta, elk vorm [d_model]) herstellen de expressieve schaal. Normalisatie houdt activaties in een numeriek stabiel bereik; zonder dit rollen kleine trainingsinstabiliteiten uit tot NaN-gradienten.
Pre-Norm vs Post-Norm
Een subtiele maar cruciale keuze
Twee manieren om layer norm in een residual sublayer te integreren:
Post-norm (origineel 2017 paper):
x = LayerNorm(x + Attention(x))
Layer norm zit na de residual optelling. De residual stream zelf wordt genormaliseerd in elke laag.
Pre-norm (moderne standaard, gebruikt in ANDREA):
x = x + Attention(LayerNorm(x))
Layer norm zit vóór de sublaag, binnen de residual branch. De residual stream blijft niet-genormaliseerd; alleen de input naar de sublaag wordt geschaald.
Waarom Pre-Norm Won
Post-norm traint slecht zonder LR warmup & zorgvuldige hyperparameter-afstemming. Gradienten exploderen in vroege lagen omdat elke layer norm de geaccumuleerde toestand van de residual stream door elkaar schudt. Het originele 2017 paper gebruikte post-norm met uitgebreide afstemming; vervolgwerk (GPT-2, LLaMA, ANDREA) standaardiseerde op pre-norm.
Pre-norm traint stabiel. De residual stream hoopt schoon op over alle lagen; alleen sublaag-ingangen worden genormaliseerd voor numerieke stabiliteit. Moderne transformers gebruiken standaard pre-norm, & ANDREA erft die keuze.
Eindblokvergelijking
Het combineren van residuals, layer norm in pre-norm positie, & beide sublagen geeft ANDREA's volledige blok:
def block_forward(x):
x = x + Attention(LayerNorm(x)) # attention sublaag
x = x + MLP(LayerNorm(x)) # MLP sublaag
return x
Twee sublayers, twee residual toevoegingen, twee layer norms (opmerking: elke sublayer heeft zijn eigen layer norm; ANDREA-120M heeft 24 layer norms over 12 blokken plus een finale aan de output, dus 25 totaal). Herhaal 12 keer. Dat is de stam van ANDREA-120M.
Waarom Pre-Norm Training Stabiliseert
Twee Lineaire Lagen, Eén Activatiefunctie
Drie Operaties
De MLP sublayer is een tweelaags perceptron met een niet-lineaire activatie tussen de lagen:
def mlp_forward(x):
h = x · W_1 + b_1 # uitbreiden: d_model → mlp_dim
h = GELU(h) # niet-lineaire activatie
y = h · W_2 + b_2 # inkrimpen: mlp_dim → d_model
return y
Drie operaties. Twee lineair, één niet-lineair. De eerste lineaire vergroot de breedte; de tweede trekt deze weer samen.
De 4× Uitbreidingsverhouding
Moderne transformers stellen mlp_dim = 4 × d_model. ANDREA-120M:
d_model = 768
mlp_dim = 4 × 768 = 3072
W_1 vorm = [768, 3072] # ~2.36M params
W_2 vorm = [3072, 768] # ~2.36M params
MLP params per blok = 4.72M (bias negerend)
Twee MLPs zitten tussen elk paar attention-sublagen (één per blok). Twaalf blokken × 4.72M ≈ 56.6M MLP-parameters totaal in ANDREA-120M, ruwweg de helft van alle parameters.
Waarom 4×
De 4× ratio ontstond empirisch. Kleinere ratios verminderen de modelcapaciteit. Grotere ratios leveren afnemende rendementen per bestede parameter. Over decennia van architectuuronderzoek heeft 4× standgehouden; het verschijnt in GPT, BERT, T5, LLaMA, & ANDREA.
Recent werk (PaLM, Chinchilla) ontdekte dat gating-mechanismen (SwiGLU) een 8/3× expansie kunnen gebruiken met vergelijkbare capaciteit tegen lagere kosten; ANDREA blijft bij klassieke GELU + 4× voor eenvoud.
GELU: Een Vloeiende Activering
Wat GELU Berekent
GELU (Gaussian Error Linear Unit) is de standaard activering tussen MLP-lagen in moderne transformers. De formule:
GELU(x) = x · Φ(x)
Φ(x) is de cumulatieve verdelingsfunctie van de standaard normale verdeling: de kans dat een standaard Gaussische willekeurige variabele op of onder x valt. Numeriek berekend:
Φ(x) ≈ 0.5 × (1 + tanh(sqrt(2/π) × (x + 0.044715 × x³)))
Gedrag Per Regio
- Voor grote positieve x: Φ(x) ≈ 1, dus GELU(x) ≈ x. Net als ReLU.
- Voor grote negatieve x: Φ(x) ≈ 0, dus GELU(x) ≈ 0. Net als ReLU.
- Rond x = 0: Φ(x) ≈ 0.5, dus GELU(0) = 0 precies. Vloeiende overgang door de oorsprong.
In tegenstelling tot ReLU laat GELU sommige negatieve inputs door, gewogen door Φ(x). Een kleine negatieve input draagt nog steeds een kleine negatieve output bij, alleen minder dan de volledige input zou doen.
Waarom GELU ReLU overtrof
Empirisch bereiken transformers getraind met GELU een lagere loss dan transformers getraind met ReLU bij hetzelfde aantal parameters. De vloeiendheid rond nul doet ertoe: ReLU's harde afkap punt bij nul produceert gradiëntdiscontinuïteiten; GELU's vloeiende kromme biedt schonere gradiënten voor backpropagation.
ANDREA's training engine microgpt_cuda.cu bevat een handgeschreven GELU CUDA-kernel. De kernel gebruikt de tanh-benadering hierboven; moderne GPU's bevatten tanh als een single-instruction-op.
Berekenen van MLP-parameters
Twaalf Blokken Samenstellen ANDREA-120M
Van Blok naar Model
De volledige forward pass van ANDREA-120M:
def model_forward(token_ids):
x = token_embed(token_ids) + position_embed(positions)
for block_idx in range(n_layer): # 12 blocks
x = block_forward(x) # attention + MLP w/ residuals
x = LayerNorm(x) # final norm
logits = x · token_embed.T # tied weights for output projection
return logits
Zes lijnen. De kern zit in block_forward, twaalf keer aangeroepen. Embeddings starten de pijplijn; gekoppelde unembedding (dezelfde matrix gebruikt voor input-opzoeking, getransponeerd voor output-projectie) sluit hem af.
Diepte Als Compositie
Elk blok leest de residual stream, berekent een delta, & voegt het terug toe. Na twaalf passes bevat de stream geaccumuleerde bijdragen van elk blok. Intern specialiseren lagen zich:
- Vroege lagen (1-3): syntactische patronen, positionele structuur
- Middenlagen (4-8): woordrelaties, frasegrenzen
- Late lagen (9-12): semantische inhoud, feitelijke herinnering
Deze specialisatie ontstaat door trainingsdruk, niet door architectonische keuzes. Hetzelfde uniforme blokontwerp produceert gespecialiseerde lagen wanneer het op taal wordt getraind.
Totale Blokparameters
| Component | Per blok | Over 12 blokken |
|---|---|---|
| Attention-projecties (4×W) | 2.36M | 28.3M |
| MLP-weights (W_1 + W_2) | 4,72M | 56,6M |
| Laagnormalisaties (gamma, beta) | ~3K (verwaarloosbaar) | ~37K |
| Totaal per blok | ~7,1M | ~85M |
85M parameters in de trunk. Voeg ~13M toe in token-embeddings (8449 vocab × 768 d_model × 2 voor gebonden input/output) plus een finale laagnormalisatie, & ANDREA-120M's parameterenaantal komt op ruwweg 120M. Het blokontwerp telt voor twee-derde; embeddings de rest.