Werkplaats / De werkplaats
In jouw eigen tempo
← Taalmodellen
MODULE 02 · LES 06

Laat iedere query informatie uit andere posities mengen

Bouw stabiele gemaskeerde softmax, scaled dot-product attention en de volledige gradiënten voor Q, K en V.

75 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je rekent attention uit met een masker vóór de softmax.
  • Je berekent de terugweg door de gewogen som, softmax en inproducten.

In “de kat slaapt omdat hij moe is” moet een positie informatie uit andere plekken kunnen gebruiken. Attention maakt daarvoor een gewogen mengsel. Denk aan een query die verschillende kaartjes vergelijkt: keys bepalen de vergelijkingsscore en values leveren de inhoud die je uiteindelijk mengt.

De namen query, key en value beschrijven rekenrollen, geen menselijk begrip. Een hoog attentiongewicht is geen volledige verklaring van een modelantwoord. We kiezen hieronder kleine getallen, zodat de gewichten én de terugweg met de hand te controleren zijn.

De voorbeelden gebruiken NumPy en jouw functies uit deze editor. Q bevat T queries met elk d kenmerken, K bevat N keys met dezelfde breedte en V bevat N valuevectoren. Eventuele voorloopassen, zoals batch en heads, moeten in dit lescontract gelijk zijn.

1. Een masker vóór de verdeling

Stel dat twee toegestane scores log(2) en 0 zijn. Na exponentiëren krijg je 2 en 1; delen door de som geeft gewichten 2/3 en 1/3. Een verboden key krijgt vóór deze normalisatie score min oneindig, zodat zijn exponent en gewicht nul zijn.

scores = np.array([[np.log(2),0.]])
allowed = np.array([[True,True]])
print(masked_softmax(scores,allowed))
# [[0.66666667 0.33333333]]
allowed[0,1] = False
print(masked_softmax(scores,allowed))
# [[1. 0.]]

Trek het hoogste toegestane getal per rij af vóór exponentiëren; de gewichtsverhouding blijft gelijk, terwijl enorme exponenten worden voorkomen. Nul zetten ná softmax is een misvatting: de overgebleven gewichten tellen dan doorgaans niet meer op tot één. Hier staat True voor toegestaan.

Maak masked_softmax voor eindige scores met minstens twee assen en een niet-lege laatste as, plus een booleaans broadcastbaar masker. Een volledig verboden rij wordt volgens ons expliciete lescontract een nulrij, zonder NaN; die rij vormt geen kansverdeling. Bij gewone causale attention blijft de diagonaal toegestaan.

01
CHECKPOINTZet verboden scores vóór softmax op min oneindig

2. Van overeenkomsten naar een mengsel

Het matrixproduct QKᵀ berekent iedere query-keyovereenkomst. Deel door de wortel van d, normaliseer over keys tot P en bereken Y=PV. De schaling tempert grote scores bij bredere heads; het is geen harde bovengrens of deling door het aantal tokens.

S=QKTd,Y=PV

Neem d=1, query 1, keys log(2) en 0 en values 2 en 8. De gewichten zijn opnieuw 2/3 en 1/3; de uitvoer wordt 2×2/3+8×1/3=4. Andere valuebreedtes zijn toegestaan: ieder valuekenmerk gebruikt dezelfde gewichten.

q = np.array([[1.]])
k = np.array([[np.log(2)],[0.]])
v = np.array([[2.],[8.]])
allowed = np.ones((1,2),dtype=bool)
y,cache = attention(q,k,v,allowed)
print(y)  # [[4.]]

Maak attention met uitvoer en cache voor q,k,v,weights. Controleer passende vormen en d>0. Reken met floats vóór dot-products, zodat bijvoorbeeld int16-invoer niet eerst overloopt.

QUERY ↓ · KEY →Bekijk welke waarden een query mengt

Handgemaakte Q, K en V voor drie woorden; geen aangeleerde betekenis. Verander de query van één rij. De score is q·k/√2, daarna volgt softmax.

Attentiongewichten per queryrij
Querydekatslaapt
de1.000——
kat0.3620.638—
slaapt0.4720.3810.147

Keys én values in dit rekenvoorbeeld: de=(1,0), kat=(0,1), slaapt=(−1,0.5). De streepjes zijn exact nul door het masker.

Uitvoer x0.3622Uitvoer y0.6378Som gekozen rij1.0000

Deze uitvoer is de gewogen som van V. Een hoog attentiongewicht is op zichzelf geen verklaring van het hele model.

02
CHECKPOINTSchaal QKᵀ en gebruik de gewichten op V

3. Softmax koppelt de keys aan elkaar

Uit Y=PV volgen dV=PᵀdY en dP=dYVᵀ. De speciale p−y-terugweg uit cross-entropy kun je hier niet gebruiken: attention krijgt een algemene gradiënt dP. Het verhogen van één score verandert immers ook de andere genormaliseerde gewichten.

Schrijf per rij g=dP en c als de som van p×g over alle keys. Dan is de scoregradiënt p×(g−c); c wordt naar iedere kolom van dezelfde rij gebroadcast. Dit volgt uit de softmax-afgeleide hieronder, waarbij δ alleen bij gelijke indices één is.

∂pi∂sj=pi(δij−pj)
dSj=pj·(gj−c)

Voor ons voorbeeld is dY=1, dus g=[2,8] en c=4. De scoregradiënten zijn [−4/3,4/3]: meer gewicht naar de tweede value verhoogt de uitvoer. De som van deze twee gradiënten is nul, passend bij de onveranderde softmax als alle scores evenveel verschuiven.

p = cache['weights']
dp = np.array([[2.,8.]])
ds = p*(dp-(p*dp).sum(
    axis=-1,keepdims=True))
print(np.round(ds,4))
# [[-1.3333  1.3333]]

Daarna zijn dQ=dSK/√d en dK=dSᵀQ/√d. Dezelfde schaalfactor hoort in beide routes; dV krijgt die factor niet opnieuw. Verboden keys hebben P=0 en daardoor dS=0, ook voor een volledig gemaskeerde rij.

g = attention_backward(
    cache,np.ones_like(y))
print(np.round(g['v'],4))
# [[0.6667]
#  [0.3333]]
eps = 1e-5
plus = attention(q+eps,k,v,allowed)[0]
minus = attention(q-eps,k,v,allowed)[0]
numeric = (plus-minus)/(2*eps)
print(np.allclose(g['q'],numeric))
# True

Maak attention_backward zonder een extra batch- of tijdmiddeling. Deze kleine differentieproef controleert één querygetal; de checkpoints onderzoeken ook meerdere batches, breedtes en maskers.

03
CHECKPOINTLaat Q, K en V allemaal leren

Wat neem je mee?

  • Masker vóór softmax en normaliseer over de toegestane keys.
  • Queries en keys bepalen gewichten; values leveren het mengsel.
  • De softmax-terugweg koppelt alle keys en gebruikt de gecachete gewichten.

Hierna verdeel je de modelbreedte over meerdere heads en leer je de projecties eromheen.

DE VOLGENDE STAPVerdeel de representatie over meerdere attention-heads
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 3
01
Zet verboden scores vóór softmax op min oneindig

Schrijf masked_softmax(scores,allowed), met minstens twee scoreassen en een niet-lege laatste as. Valideer eindige scores en booleaans masker. Retourneer een nulrij als niets is toegestaan.

Een hint

Na np.where(allowed,scores,-np.inf) kan het rijmaximum −inf zijn. Gebruik dan maximum=0 en deel met np.divide(...,out=zeros,where=total>0).

02
Schaal QKᵀ en gebruik de gewichten op V

Schrijf attention(q,k,v,allowed): retourneer (y,cache) met q,k,v,weights. Gebruik de gegeven vormafspraken, controleer passende voorloopassen, headbreedte en gelijk aantal keys/values en eis d>0.

Een hint

np.swapaxes(k,-2,-1) verwisselt alleen de laatste twee assen, ook bij meerdere heads.

03
Laat Q, K en V allemaal leren

Schrijf attention_backward(cache,dy), met dictionary q,k,v. Ondersteun de voorloopassen en gebruik de gecachete weights.

Een hint

Gebruik swapaxes voor transponeren en keepdims=True voor de rijgewijze softmaxsom.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

def masked_softmax(scores,allowed):
    scores=np.asarray(scores,float)
    allowed=np.asarray(allowed)
    if scores.ndim<2 or scores.shape[-1]==0 or allowed.dtype.kind!='b' or not np.isfinite(scores).all():
        raise ValueError('Gebruik eindige scores en een booleaans masker')
    allowed=np.broadcast_to(allowed,scores.shape)
    masked=np.where(allowed,scores,-np.inf)
    maximum=np.max(masked,axis=-1,keepdims=True)
    maximum=np.where(np.isfinite(maximum),maximum,0.)
    weights=np.exp(masked-maximum)
    total=weights.sum(axis=-1,keepdims=True)
    return np.divide(weights,total,out=np.zeros_like(weights),where=total>0)

def attention(q,k,v,allowed):
    q,k,v=(np.asarray(value,float) for value in (q,k,v))
    if min(q.ndim,k.ndim,v.ndim)<2 or q.shape[:-2]!=k.shape[:-2] or q.shape[:-2]!=v.shape[:-2] or q.shape[-1]!=k.shape[-1] or k.shape[-2]!=v.shape[-2] or q.shape[-1]==0:
        raise ValueError('Vormen van Q, K en V passen niet')
    scores=q@np.swapaxes(k,-2,-1)/np.sqrt(q.shape[-1])
    weights=masked_softmax(scores,allowed)
    return weights@v,{'q':q,'k':k,'v':v,'weights':weights}

def attention_backward(cache,dy):
    dy=np.asarray(dy,float)
    q,k,v,p=(cache[name] for name in ['q','k','v','weights'])
    dv=np.swapaxes(p,-2,-1)@dy
    dp=dy@np.swapaxes(v,-2,-1)
    ds=p*(dp-(dp*p).sum(axis=-1,keepdims=True))
    scale=np.sqrt(q.shape[-1])
    return {'q':ds@k/scale,'k':np.swapaxes(ds,-2,-1)@q/scale,'v':dv}

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.