Werkplaats / De werkplaats
In jouw eigen tempo
← Taalmodellen
MODULE 03 · LES 08

Maak van tekst een reeks volgende-tokenopdrachten

Bouw verschoven trainingsvensters en stabiele tokenverliezen, met expliciete behandeling van padding en lege loss-maskers.

75 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je koppelt iedere inputpositie aan het volgende token zonder splitgrenzen te overschrijden.
  • Je middelt cross-entropy over de bedoelde voorspellingen en maskeert padding correct.

Hoe maak je van “de kat slaapt” een leeropdracht? Geef een model steeds een begin van de tekst en vraag welk token daarna komt. Eén zin levert zo meerdere voorspellingen op; het model moet de antwoorden nog steeds uitsluitend uit het toegestane verleden afleiden.

Bij training geven we de echte eerdere tokens mee: teacher forcing. Tijdens vrij genereren kiest het model zelf een token en gebruikt dat als nieuwe invoer. Een lage trainingsloss bewijst daarom niet dat een lange eigen tekst vrij blijft van herhaling of fouten.

De voorbeelden gebruiken NumPy en de drie functies die je in deze editor bouwt. Het losse speelgoedvocabulaire van deze proef is de=1, kat=2, slaapt=3 en eindtoken=0. Een echt taalmodel moet zo'n eindteken expliciet in zijn eigen tokenizercontract opnemen.

1. Koppel ieder venster aan zijn vervolg

Neem data [1,2,3,0] en contextlengte T=2. Vanaf start 0 is x=[1,2] en y=[2,3]; vanaf start 1 is x=[2,3] en y=[3,0]. Het laatste doeltoken moet nog bestaan, dus de laatste geldige start is lengte−T−1: hier 4−2−1=1.

import numpy as np
data = np.array([1,2,3,0])
starts = np.array([0,1])
x,y = windows(data,starts,2)
print(x.tolist())  # [[1,2],[2,3]]
print(y.tolist())  # [[2,3],[3,0]]

Binnen het eerste venster ziet tijdpositie t=0 alleen “de” en voorspelt “kat”. Tijdpositie t=1 ziet “de kat” en voorspelt “slaapt”. Beide berekeningen kunnen tegelijk plaatsvinden, omdat het causale masker voorkomt dat positie 0 de al aanwezige invoer op positie 1 leest.

tToegestaan beginDoel
0dekat
1de katslaapt

Maak windows met niet-lege 1D-gehele data en starts en een positieve exacte integercontext. Negatieve ID’s, negatieve starts en vensters buiten de reeks geven ValueError. Bewaar de aangevraagde volgorde en herhalingen: starts zijn opdrachten, geen verzameling unieke indices.

x,_ = windows(data,np.array([1,0,1]),2)
print(x.tolist())
# [[2,3],[1,2],[2,3]]

Splits train, validatie en test vóór het maken van vensters. De functie ontvangt steeds slechts één split; zij kan uit losse ID’s niet zelf herkennen waar een document- of evaluatiegrens ligt. Een verschoven doeltabel is geen vervanging voor die datascheiding.

01
CHECKPOINTVerschuif het doel precies één token

2. Geef de juiste voorspelling zijn verlies

Logits bevat B vensters, T tijdposities en V mogelijke token-ID’s. Targets bevat voor iedere batch- en tijdpositie het juiste ID. Zonder loss-masker tellen N=B×T voorspellingen mee; het vocabulaire V bepaalt de softmaxbreedte, niet de verliesnoemer.

Hier is L het gemiddelde verlies en indexeert n één voorspelling. Het symbool yₙ is het juiste token-ID; pₙ,ᵧₙ is de voorspelde kans op dat doel.

L=−1N∑n=1Nlogpn,yn

Gebruik voor beide posities scores log([1,1,4,2]). Softmax maakt daarvan [1/8,1/8,1/2,1/4]. De doelen 2 en 3 krijgen kansen 1/2 en 1/4: hun verliezen zijn log(2) en log(4), gemiddeld ongeveer 1.0397.

logits = np.log(np.array(
    [[[1.,1.,4.,2.],[1.,1.,4.,2.]]]))
targets = np.array([[2,3]])
loss,grad = token_loss(logits,targets)
print(round(loss,4))  # 1.0397
print(grad)
# [[[.0625 .0625 -.25  .125]
#   [.0625 .0625  .25 -.375]]]

De gradiënt per rij is softmax minus de one-hotdoelvector, gedeeld door N. Bij de eerste rij wordt de doelkolom dus (1/2−1)/2=−1/4. Gebruik natuurlijke logaritmen en trek het rijmaximum af vóór exponentiëren; reken eerst met floats zodat een integeraftrekking niet al overloopt.

Maak token_loss zonder logits te wijzigen. Dezelfde berekening werkt voor (N,V) met targets (N,); invoer is eindig en niet leeg en targets zijn geldige gehele ID’s. Flatten alleen voorbeeld- en tijdassen, en herstel de volledige gradiëntvorm.

02
CHECKPOINTMiddel over voorspellingen, niet over vocabulaire

3. Kies welke doelen meetellen

Een attentionmasker begrenst informatieoverdracht tussen posities. Een loss-masker kiest voorspellingen voor het trainingsdoel, bijvoorbeeld wanneer een positie alleen padding bevat. Een nulgewicht in de loss geeft een token dus niet automatisch toestemming om andere posities te beïnvloeden.

Laat in ons voorbeeld alleen de eerste voorspelling meetellen. Nu is N=1, loss=log(2) en verdubbelt de eerste gradiëntrij ten opzichte van het gemiddelde over twee doelen. De tweede rij wordt exact nul, ook al stond daar een geldige voorspelling.

mask = np.array([[True,False]])
loss,grad = masked_token_loss(
    logits,targets,mask)
print(round(loss,4))  # .6931
print(grad)
# [[[.125 .125 -.5 .25]
#   [0.   0.   0.  0.]]]

Maak masked_token_loss met een booleaans masker van exact dezelfde vorm als targets; anders volgt ValueError. Middel alleen over gekozen posities en schrijf hun gradiënten terug in een nularray. Alle target-ID’s blijven geldig: er is geen verborgen −100-conventie.

Als niets meetelt, spreken we loss nul en een volledige nulgradiënt af. Dat voorkomt delen door nul; het bewijst niet dat het model de tekst goed voorspelde. Er was eenvoudig geen trainingsdoel geselecteerd.

loss,grad = masked_token_loss(
    logits,targets,np.zeros_like(mask))
print(loss,np.count_nonzero(grad))
# 0.0 0
03
CHECKPOINTEen paddingpositie hoort niet in de verliesnoemer

Wat neem je mee?

  • Het doeltoken staat precies één plek na zijn invoerpositie.
  • Attentionmasker en loss-masker hebben verschillende taken.
  • De noemer telt voorspellingen die meedoen, niet vocabulairekolommen.

Hierna bouw je het transformerblok dat de context voor zulke voorspellingen berekent.

DE VOLGENDE STAPBouw een volledig pre-norm transformerblok
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
Verschuif het doel precies één token

Schrijf windows(data,starts,context) en retourneer (x,y), beide (B,T). data en starts zijn niet-lege 1D-gehele arrays; context is een positieve exacte integer. Ongeldige vormen, negatieve ID’s of starts, en te lange vensters geven ValueError. Behoud de volgorde en herhalingen van starts.

Een hint

Controleer s+context<len(data). np.stack verzamelt de vensters nadat alle grenzen gecontroleerd zijn.

02
Middel over voorspellingen, niet over vocabulaire

Schrijf token_loss(logits,targets), retourneer (gemiddelde_loss,grad_logits). Gebruik natuurlijke logaritmen. Wijzig logits niet.

Een hint

Flatten uitsluitend de voorbeeld-/tijdassen om de targetkolommen te selecteren; herstel daarna de oorspronkelijke logitvorm.

03
Een paddingpositie hoort niet in de verliesnoemer

Schrijf masked_token_loss(logits,targets,mask). Controleer een booleaans masker met dezelfde vorm als targets, anders ValueError. Gebruik token_loss voor de geselecteerde posities.

Een hint

logits[mask] heeft vorm (N,V). Schrijf de berekende subgradiënten terug in een nularray van de volledige vorm.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

def windows(data,starts,context):
    data=np.asarray(data);starts=np.asarray(starts)
    if data.ndim!=1 or starts.ndim!=1 or data.size==0 or starts.size==0 or data.dtype.kind not in 'iu' or starts.dtype.kind not in 'iu':
        raise ValueError('Gebruik niet-lege gehele reeksen')
    if type(context) is not int or context<=0 or context>=len(data) or data.min()<0 or starts.min()<0 or int(starts.max())>len(data)-context-1:
        raise ValueError('Ongeldig venster')
    return np.stack([data[s:s+context] for s in starts]),np.stack([data[s+1:s+context+1] for s in starts])

def token_loss(logits,targets):
    logits=np.asarray(logits,float);targets=np.asarray(targets)
    shifted=logits-logits.max(axis=-1,keepdims=True)
    exp=np.exp(shifted);total=exp.sum(axis=-1,keepdims=True)
    logp=shifted-np.log(total)
    rows=np.arange(targets.size)
    loss=-float(logp.reshape(-1,logits.shape[-1])[rows,targets.ravel()].mean())
    grad=(exp/total).reshape(-1,logits.shape[-1])
    grad[rows,targets.ravel()]-=1
    return loss,grad.reshape(logits.shape)/targets.size

def masked_token_loss(logits,targets,mask):
    mask=np.asarray(mask)
    if mask.dtype.kind!='b' or mask.shape!=targets.shape:
        raise ValueError('Ongeldig loss-masker')
    grad=np.zeros_like(logits,dtype=float)
    if not mask.any():
        return 0.,grad
    value,selected_grad=token_loss(logits[mask],targets[mask])
    grad[mask]=selected_grad
    return value,grad

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