- Je combineert normalisatie, attention, feedforward en twee residualverbindingen in één blok.
- Je berekent alle parametergradiënten en beide directe residualroutes.
Attention mengt informatie tussen posities; een feedforwardnetwerk bewerkt daarna iedere positie. Een transformerblok verbindt die bewerkingen met een doorgaande residualstroom. Je kunt een nieuwe bijdrage toevoegen zonder eerst alle bestaande informatie door de sublaag te persen.
Voorspel in de visualisatie eerst wat er gebeurt als beide factoren nul zijn: Y blijft gelijk aan X. Zet daarna alleen attention aan en vervolgens alleen feedforward. Deze factoren zijn onderzoeksschakelaars; het programmeerblok gebruikt beide bijdragen zonder zulke extra factoren.
Drie tokens, vier kanalen en één causale head. Q=K=V=LN1(X), Wₒ=0.25I; FF(z)=0.3ReLU(z). Alle tussenwaarden worden opnieuw berekend.
| Stap | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| X · invoer | 1.00 | -1.00 | 0.20 | 1.00 |
| LN1(X) | 0.86 | -1.59 | -0.12 | 0.86 |
| Attention · vóór factor | 0.23 | -0.30 | -0.12 | 0.18 |
| H = X + αA | 1.23 | -1.30 | 0.08 | 1.18 |
| LN2(H) | 0.90 | -1.55 | -0.21 | 0.86 |
| Feedforward · vóór factor | 0.27 | 0.00 | 0.00 | 0.26 |
| Y = H + βF | 1.50 | -1.30 | 0.08 | 1.44 |
Uitvoer token 3: 1.5007, -1.2952, 0.0824, 1.4406
α en β zijn alleen schakelaars voor deze proef. Bij α=β=0 blijft Y=X. Verander token 3 terwijl je token 1 inspecteert: het causale masker houdt zijn uitvoer gelijk.
Wij bouwen pre-norm: normaliseren vóór elke sublaag, terwijl de directe stroom ongenormaliseerd blijft. Het oorspronkelijke transformerpaper gebruikte post-norm. Deze onderwijsvariant gebruikt ReLU en geen dropout; hij kopieert geen specifiek productiemodel.
De attentionhelpers en init_block zijn gegeven. De voorbeelden hieronder gebruiken daarnaast jouw normalisatie-, feedforward- en blokfuncties. Lees iedere terugweg als het verdelen en weer optellen van invloed.
1. Normaliseer één tokenrij
X heeft vorm (B,T,C): batch, tijd en kanalen. LayerNorm berekent per token gemiddelde μ en populatievariantie σ² over C, dus met ddof=0. De inverse schaal r is 1/√(σ²+ε); de genormaliseerde rij N wordt daarna per kanaal geschaald met γ en verschoven met β.
Neem X=[1,3], γ=[2,1] en β=[1,−1]. Dan zijn μ=2 en σ²=1. Voor deze handproef kiezen we ε=1: r=1/√2 en N=[−.7071,.7071], zodat Y≈[−.4142,−.2929]; het echte blok gebruikt standaard ε=1e−5.
import numpy as np
x = np.array([[[1.,3.]]])
gamma = np.array([2.,1.])
beta = np.array([1.,-1.])
y,lc = layernorm_forward(
x,gamma,beta,epsilon=1.)
print(np.round(y,4))
# [[[-.4142 -.2929]]]
Voor backward schrijf je D=dY×γ. Alleen rD gebruiken vergeet dat gemiddelde en schaal zelf van X afhangen. Trek eerst de kanaalgemiddelde bijdrage af en corrigeer daarna voor de invloed op de schaal; alle gemiddelden hieronder lopen over de laatste as.
Bij dY=[1,0] is D=[2,0], U=[1,−1] en mean(DN)=−.7071. De schaalcorrectie maakt daarvan [.5,−.5]; vermenigvuldigen met r geeft dX≈[.3536,−.3536]. Verzamel daarnaast dγ uit dY×N en dβ uit dY, gesommeerd over batch en tijd.
dy = np.array([[[1.,0.]]])
dx,dg,db = layernorm_backward(lc,dy)
print(np.round(dx,4))
# [[[.3536 -.3536]]]
print(np.round(dg,4),db)
# [-.7071 0.] [1. 0.]
Maak beide LayerNormfuncties met cache norm,inv,gamma. Gebruik floats vóór producten en gradiëntsommen, ook bij integerinvoer.
012. Dezelfde kleine MLP op iedere plek
De feedforwardlaag berekent Z=XW1+b1, vervolgens hidden=ReLU(Z) en F=hiddenW2+b2. W1 verbreedt van C naar M en W2 gaat terug naar C. Parameters zijn gedeeld over posities; de berekening mengt hun invoer niet.
Neem X=[1,2], de matrices hieronder en b1=[−2,0]. Dan is Z=[1,−1], hidden=[1,0] en F=[2,1]. De negatieve route verdwijnt bij ReLU.
x = np.array([[[1.,2.]]])
p = {'w1':np.array([[1.,-1.],[1.,0.]]),
'b1':np.array([-2.,0.]),
'w2':np.array([[2.,0.],[0.,1.]]),
'b2':np.array([0.,1.])}
y,fc = feedforward(x,p)
print(y) # [[[2. 1.]]]
Bij dY=[1,3] komt vóór ReLU [2,3] terug; het tekenmasker laat dZ=[2,0] over. Daardoor is dX=[2,2] en de bijdrage aan W1 het outer-product [[2,0],[4,0]]. Meerdere batch- of tijdposities tellen zulke bijdragen op; biases verzamelen de kanaalbijdragen.
dx,g = feedforward_backward(
fc,p,np.array([[[1.,3.]]]))
print(dx) # [[[2. 2.]]]
print(g['w1'])
# [[2. 0.]
# [4. 0.]]
Maak feedforward en zijn backward met cache x,z,hidden en gradiënten w1,b1,w2,b2. Bij preactivatie exact nul kiezen we ReLU-subgradiënt nul. Een zuiver lineaire vervanger kan een consistente backward hebben, maar berekent een ander model.
023. Verbind de sublagen met twee directe routes
Bereken H=X+MHA(LN1(X)) en daarna Y=H+FF(LN2(H)). De directe X- en H-routes worden niet genormaliseerd. LN1 gebruikt g1,b1n en LN2 g2,b2n; die namen onderscheiden normalisatiebiases van de FF-biases b1,b2.
p = init_block(4)
x = np.array([[[0.,1.,2.,3.],
[1.,0.,-1.,2.]]])
allowed = np.tril(np.ones((2,2),bool))
y,bc = transformer_block(x,p,2,allowed)
print(np.round(y[0,0],4))
# [-.1256 .8200 2.2928 3.0412]
Maak transformer_block met cache norm1,attention,residual,norm2,ff. Het causale masker moet binnen attention blijven gelden. Controleer toekomstinvloed door één toekomstkanaal te veranderen; een gelijke verschuiving van alle kanalen verdwijnt al door LayerNorm en bewijst dus niets over causaliteit.
034. Gebruik de opgetelde upstreamgradiënt
Uit Y=H+FF(LN2(H)) ontvangt H dY rechtstreeks plus de FF/LN2-bijdrage. Noem de som dH; attention ontvangt deze dH, niet alleen dY. Naar X tel je daarna dH op bij de attention/LN1-bijdrage.
In dezelfde numerieke proef is de eerste dY hieronder 1. De FF-route levert −.1319, dus dH=.8681; de attentionroute levert vervolgens .0159 en samen wordt dX=.8840. Dit zijn bijdragen uit de echte caches, geen verzonnen vervangingsgradiënten.
dy = np.arange(1.,9.).reshape(1,2,4)
ff_back = feedforward_backward
ln_back = layernorm_backward
mh_back = multihead_backward
dn2,_ = ff_back(bc['ff'],p,dy)
ff,_,_ = ln_back(bc['norm2'],dn2)
dh = dy+ff
dn1,_ = mh_back(bc['attention'],p,dh)
att,_,_ = ln_back(bc['norm1'],dn1)
print(round(ff[0,0,0],4)) # -.1319
print(round(dh[0,0,0],4)) # .8681
print(round(att[0,0,0],4)) # .0159
print(round((dh+att)[0,0,0],4)) # .8840
Maak block_backward met dx en exact de twaalf parametergradiënten uit init_block. Hergebruik beide residualoptellingen en alle eerdere backwards. Voeg geen tweede verliesmiddeling toe.
04Wat neem je mee?
- Normalisatie staat vóór de sublaag; de directe stroom blijft bestaan.
- Iedere gedeelde parameter verzamelt alle toepasselijke bijdragen.
- De tweede residual verandert welke gradiënt attention ontvangt.
Hierna gebruik je dit blok in een eigen mini-GPT met echte training en generatie.