Train een mini-GPT dat zelf tekst vervolgt
Bouw een echt decoder-only taalmodel met eigen NumPy-backprop, train 6.629 parameters en genereer tekst uit een klein Nederlands oefencorpus.
De opdracht, leerdoelen en voorkennis
- Je verbindt token- en positie-embeddings, een transformerblok en een volgende-tokenhead.
- Je traint alle parameters, kiest op validatie en vergelijkt teacher forcing met vrij genereren.
Lees eerst: Pas de stapgrootte per parameter aan · Geef dezelfde token op een andere plek een positie mee · Maak van tekst een reeks volgende-tokenopdrachten · Bouw een volledig pre-norm transformerblok.
Hoe wordt “de kat ” een voorspelling voor het volgende teken? In dit project verbind je je bouwstenen tot één echt taalmodel. De route is teken-ID → token- en positievector → transformerblok → laatste normalisatie → logits → nieuw teken.
Het standaardmodel heeft één pre-norm blok, vier heads, breedte 24 en context 16. Het leert met eigen NumPy-backprop op 64 zelf samengestelde zinnen: 50 training, 7 validatie en 7 test. We gebruiken 29 vaste tekentokens; een andere tokenizer vraagt passende nieuwe embedding- en headvormen.
De voorbeelden gebruiken gegeven helpers en jouw zes notebookfuncties. De laatste rapportproef werkt nadat je het notebook hebt uitgevoerd. Je eigen init_gpt staat onder de gegeven bouwstenen in de eerste cel.
1. Welke getallen kunnen leren?
Het blok heeft twaalf parametergroepen. Je voegt token- en positie-embeddings, gf/bf voor de laatste LayerNorm en head/bias voor de uitvoer toe: samen achttien groepen. Het blok telt 4.776 getallen, de zes extra arrays 1.853; samen zijn dat 6.629 trainbare getallen.
params = init_gpt(29)
total = sum(a.size for a in
params.values())
print(total,len(params)) # 6629 18
Maak init_gpt volgens de gegeven vormen en seeds. Die afspraken houden de standaardproef vergelijkbaar. De kleine rekenvoorbeelden hieronder staan los van deze vaste architectuur.
012. Van een laatste vector naar kansen
Neem voor één plek N=[1,2] en een kleine head W met drie mogelijke uitvoertokens. De kolommen [1,0], [0,1] en [−1,1] geven logits [1,2,1]. Met nulbias en doel-ID 1 wordt de kans ongeveer .5761 en de natuurlijke log-loss .5514.
Hier is z de logitrij, N de uitvoer van de laatste normalisatie, W de head en b de bias. In het echte model herhaal je dit voor iedere batch- en tijdpositie. Het causale masker blijft in het blok gelden.
n = np.array([1.,2.])
w = np.array([[1.,0.,-1.],
[0.,1.,1.]])
z = n@w
p = np.exp(z-z.max());p/=p.sum()
print(z) # [1. 2. 1.]
print(round(-np.log(p[1]),4))
# .5514
Maak gpt_forward met de opgegeven cache. Token-ID’s moeten geheel en geldig zijn; een leeg of te lang venster geeft ValueError. Posities beginnen in ieder afgeknipt venster opnieuw bij nul, zoals tijdens deze training; dit project bewaart geen KV-cache.
023. Verzamel gedeelde invloeden
Voor één doel is dLogits=p−onehot. De head ontvangt het outer-product van N en die rij; bias ontvangt de rij zelf. Bij meerdere voorspellingen tel je zulke bijdragen over batch en tijd op, zonder de verliesmiddeling opnieuw toe te passen.
dz = p-np.array([0.,1.,0.])
dw = n[:,None]*dz[None,:]
print(np.round(dw,4))
# [[ .2119 -.4239 .2119]
# [ .4239 -.8478 .4239]]
print(np.round(dz@w.T,4))
# [0. -.2119]
De terugweg loopt via de laatste LayerNorm en het blok naar de embeddings. Stel dat token-ID 1 tweemaal voorkomt en bijdragen [1,2] en [3,4] ontvangt. Zijn gedeelde rij krijgt [4,6]; de twee positierijen bewaren hun afzonderlijke tijdplekken.
ids = np.array([[1,1]])
dx = np.array([[[1.,2.],[3.,4.]]])
tokens = np.zeros((3,2))
np.add.at(tokens,ids.ravel(),
dx.reshape(-1,2))
print(tokens[1]) # [4. 6.]
print(dx.sum(axis=0))
# [[1. 2.]
# [3. 4.]]
Maak gpt_backward met alle achttien sleutels. Herhaalde tokenrijen tellen op; positiegradiënten worden over de batchas verzameld. De kleine numerieke proef controleert ook de routes door het hele model.
034. Train, begrens en selecteer
Train de standaardproef 250 stappen met minibatches van 16 vensters en de opgegeven Adaminstellingen. Globale clipping gebruikt één norm over alle gradiëntarrays. Voor [3,4] is die norm 5: begrenzen op 1 maakt [.6,.8], met dezelfde richting.
g = np.array([3.,4.])
scale = min(1.,1/np.linalg.norm(g))
print(g*scale) # [.6 .8]
Meet vóór training en iedere 25 stappen de train- en validatie-NLL. Een validatiereeks .8→.6→.7 kiest de middelste parameterkopie, ook als training verder verbetert. Een verwijzing naar veranderende arrays is geen opgeslagen model.
best = np.inf;chosen = None
param = np.zeros(1)
for step,val in [(0,.8),(25,.6),
(50,.7)]:
param[:] = step
if val<best:
best=val;chosen=step
kept=param.copy()
print(chosen,kept[0]) # 25 25.0
Maak train_gpt met afzonderlijke initial- en beste parameterkopieën. Het krijgt geen testdata. steps is een exacte integer van 0 tot 500; de standaardproef gebruikt de afgesproken seeds en traint alle groepen.
045. Voer je eigen keuze terug
Elke generatiestap berekent logits opnieuw voor het laatste contextvenster en gebruikt de laatste logitrij. Onderstaand speelgoedspoor kiest 0→1→2→0. De lijst groeit echt: de volgende keuze hangt van het nieuw toegevoegde token af.
toy = np.array([[0.,2.,0.],
[0.,0.,2.],
[2.,0.,0.]])
ids = [0]
for _ in range(3):
rows = toy[ids[-2:]]
next_id = rows[-1].argmax()
ids.append(int(next_id))
print(ids)
# [0,1]
# [0,1,2]
# [0,1,2,0]
Bij temperature nul kies je argmax; anders sample je uit softmax(logits/temperature). Voor [0,2,0] geeft temperature 1 ongeveer [.1065,.7870,.1065], terwijl 2 ongeveer [.2119,.5761,.2119] geeft. Sampling is dus geen uniforme keuze uit tekens.
for temp in [1.,2.]:
scores = toy[0]/temp
shifted = scores-scores.max()
prob = np.exp(shifted)
prob /= prob.sum()
print(np.round(prob,4))
Maak generate zonder parameters te wijzigen. Prompt is niet-lege tekst uit het vaste alfabet; count is een exacte integer 0..512 en temperature een eindig niet-negatief getal. Ongeldige invoer geeft ValueError; dezelfde seed maakt dezelfde eigen samplingprocedure herhaalbaar.
056. Kijk naar metingen én tekst
Maak make_report uit het meegegeven model en de meegegeven datasplits. De standaardreferentie kiest stap 250 en meet ongeveer .3527 validatie-NLL en .3606 test-NLL, tegenover log(29)≈3.3673 voor uniform voorspellen. Deze meting gebruikt twaalf vaste vensters; overlap kan tekstposities meerdere keren laten meetellen.
r = make_report(trained,data)
print(r['parameters']) # 6629
print(r['changed_groups']) # 18
print(round(r['test_nll'],4))
# .3606
Behoud de echte verliescurves en geleerde attentionfiguur. Lage teacherforcing-NLL sluit herhaling en fouten zoals “de kind” in vrij gegenereerde tekst niet uit. Dit simpele taalspel is geen algemene assistent of representatieve taalbenchmark; een attentionkaart verklaart evenmin het hele antwoord.
06Wat neem je mee?
- Alle modelonderdelen delen één voorwaartse en teruggaande berekening.
- Selecteer op validatie en rapporteer de aparte testmeting.
- Generatie voert eigen keuzes terug en gebruikt steeds de laatste logitrij.
Hierna onderzoek je sampling, contextvensters en efficiënter genereren afzonderlijk.
Bronnen: Vaswani et al. — attention en transformerbouwstenen ↗ Xiong et al. — pre-norm en post-norm ↗ Karpathy — nanoGPT als historische architectuurreferentie ↗ · Gecontroleerd 2026-10-28.
Maak een model met vaste, controleerbare vormen
Dit project verbindt de bouwstenen tot een echt decoder-only taalmodel. Open bovenaan de opdracht voor de kleine head-, gradiënt-, selectie- en generatievoorbeelden. Je eigen init_gpt staat onder de gegeven helpers en het corpus; die bouwstenen hoef je niet opnieuw te implementeren.
Begin met init_block(width,seed) en voeg zes arrays toe: tokens(V,C), positions(context,C), gf(C), bf(C), head(C,V) en bias(V). Maak een nieuwe NumPy-generator met dezelfde seed voor tokens, positions en head, telkens normale waarden met standaardafwijking 0.1; gf begint op één en bf/bias op nul. Zo krijg je achttien groepen en in de standaardproef 6.629 getallen.
Behoud de standaardarchitectuur, seeds en corpusverdeling. Het alfabet heeft 29 vaste tekens; onbekende tekens geven ValueError. Het kleine corpus heeft 50 trainingszinnen, 7 validatiezinnen en 7 testzinnen met verwante patronen: dit is een leerproef, geen algemene taalbenchmark.
Bekijk voorbeeld
import json
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}
def split_heads(x,heads):
if x.ndim!=3 or type(heads) is not int or heads<=0 or x.shape[-1]%heads:
raise ValueError('Aantal heads moet de modelbreedte delen')
b,t,c=x.shape
return x.reshape(b,t,heads,c//heads).transpose(0,2,1,3)
def merge_heads(x):
if x.ndim!=4:
raise ValueError('Verwacht (batch,heads,tijd,headbreedte)')
b,h,t,d=x.shape
return x.transpose(0,2,1,3).reshape(b,t,h*d)
def multihead(x,params,heads,allowed):
x=np.asarray(x,float)
params={name:np.asarray(value,float) for name,value in params.items()}
q,k,v=(split_heads(x@params[name],heads) for name in ['wq','wk','wv'])
out,acache=attention(q,k,v,allowed)
joined=merge_heads(out)
return joined@params['wo'],{'x':x,'heads':heads,'attention':acache,'joined':joined}
def multihead_backward(cache,params,dy):
x=np.asarray(cache['x'],float)
dy=np.asarray(dy,float)
params={name:np.asarray(value,float) for name,value in params.items()}
gradients={'wo':np.einsum('btc,btd->cd',cache['joined'],dy)}
raw=attention_backward(cache['attention'],split_heads(dy@params['wo'].T,cache['heads']))
dx=np.zeros_like(x)
for name,part in [('wq','q'),('wk','k'),('wv','v')]:
dpart=merge_heads(raw[part])
gradients[name]=np.einsum('btc,btd->cd',x,dpart)
dx+=dpart@params[name].T
return dx,gradients
def layernorm_forward(x,gamma,beta,epsilon=1e-5):
x=np.asarray(x,float);gamma=np.asarray(gamma,float);beta=np.asarray(beta,float)
inv=1/np.sqrt(x.var(axis=-1,keepdims=True)+epsilon)
norm=(x-x.mean(axis=-1,keepdims=True))*inv
return norm*gamma+beta,{'norm':norm,'inv':inv,'gamma':gamma}
def layernorm_backward(cache,dy):
dy=np.asarray(dy,float)
norm,inv,gamma=(cache[k] for k in ['norm','inv','gamma'])
dn=dy*gamma
dx=inv*(dn-dn.mean(axis=-1,keepdims=True)-norm*(dn*norm).mean(axis=-1,keepdims=True))
axes=tuple(range(dy.ndim-1))
return dx,(dy*norm).sum(axis=axes),dy.sum(axis=axes)
def feedforward(x,params):
x=np.asarray(x,float)
params={name:np.asarray(value,float) for name,value in params.items()}
z=x@params['w1']+params['b1']
hidden=np.maximum(z,0)
return hidden@params['w2']+params['b2'],{'x':x,'z':z,'hidden':hidden}
def feedforward_backward(cache,params,dy):
dy=np.asarray(dy,float)
params={name:np.asarray(value,float) for name,value in params.items()}
dz=(dy@params['w2'].T)*(cache['z']>0)
gradients={'w2':np.einsum('btc,btd->cd',cache['hidden'],dy),'b2':dy.sum(axis=(0,1)),
'w1':np.einsum('btc,btd->cd',cache['x'],dz),'b1':dz.sum(axis=(0,1))}
return dz@params['w1'].T,gradients
def transformer_block(x,params,heads,allowed):
x=np.asarray(x,float)
n1,c1=layernorm_forward(x,params['g1'],params['b1n'])
a,ca=multihead(n1,params,heads,allowed)
residual=x+a
n2,c2=layernorm_forward(residual,params['g2'],params['b2n'])
f,cf=feedforward(n2,params)
return residual+f,{'norm1':c1,'attention':ca,'residual':residual,'norm2':c2,'ff':cf}
def block_backward(cache,params,dy):
dy=np.asarray(dy,float)
dn2,g=feedforward_backward(cache['ff'],params,dy)
dr,g['g2'],g['b2n']=layernorm_backward(cache['norm2'],dn2)
dr+=dy
dn1,ag=multihead_backward(cache['attention'],params,dr)
g.update(ag)
dx,g['g1'],g['b1n']=layernorm_backward(cache['norm1'],dn1)
return dx+dr,g
def init_block(width=4,seed=7):
rng=np.random.default_rng(seed)
params={name:rng.normal(0,.2,(width,width)) for name in ['wq','wk','wv','wo']}
params.update({'g1':np.ones(width),'b1n':np.zeros(width),'g2':np.ones(width),'b2n':np.zeros(width),
'w1':rng.normal(0,.2,(width,2*width)),'b1':rng.normal(0,.2,2*width),
'w2':rng.normal(0,.2,(2*width,width)),'b2':np.zeros(width)})
return params
import matplotlib.pyplot as plt
alphabet=' \nabcdefghijklmnopqrstuvwxyz.'
stoi={char:i for i,char in enumerate(alphabet)}
def encode_text(text):
if not isinstance(text,str):
raise ValueError('Geef tekst als string')
if any(char not in stoi for char in text):
raise ValueError('Dit oefenmodel gebruikt alleen kleine letters, spatie, punt en nieuwe regel')
return np.array([stoi[char] for char in text],dtype=int)
def make_corpus():
nouns=['de kat','de hond','de robot','het kind']
verbs=['loopt door','kijkt naar','zit in','speelt in']
places=['het park','de tuin','het huis','het bos']
sentences=[f'{n} {v} {p}.\n' for n in nouns for v in verbs for p in places]
order=np.random.default_rng(21).permutation(len(sentences))
return {'train':''.join(sentences[i] for i in order[14:]),
'val':''.join(sentences[i] for i in order[:7]),
'test':''.join(sentences[i] for i in order[7:14])}
corpus=make_corpus()
data={name:encode_text(text) for name,text in corpus.items()}
print('Train / validatie / test:',*(len(data[k]) for k in ['train','val','test']))
print('Vaste tekentokens:',len(alphabet))
def init_gpt(vocab_size,context=16,width=24,seed=9):
rng=np.random.default_rng(seed)
params=init_block(width,seed)
params.update({'tokens':rng.normal(0,.1,(vocab_size,width)),
'positions':rng.normal(0,.1,(context,width)),
'gf':np.ones(width),'bf':np.zeros(width),
'head':rng.normal(0,.1,(width,vocab_size)),'bias':np.zeros(vocab_size)})
return params
Voorspel op alle posities tegelijk met een causaal masker
Een invoer-ID kiest een tokenrij; de tijdpositie voegt een positierij toe. Het blok en de laatste LayerNorm leveren N, waarna logits=N@head+bias iedere mogelijke volgende letter scoort. In het handvoorbeeld N=[1,2] en de kleine drie-kolomshead worden de logits [1,2,1].
Maak gpt_forward met cache ids,block,finalnorm,n. Gebruik het causale driehoekmasker inclusief diagonaal en de laatste LayerNorm met gf/bf. De uitvoer heeft vorm (B,T,V); wijs niet-gehele, lege, ongeldige of te lange ID-vensters af met ValueError.
Token- en positievectoren hebben dezelfde breedte C. Posities beginnen in ieder afgeknipt venster opnieuw bij nul, zoals in deze training. De gegeven token_loss middelt al over voorspellingen; dit project bewaart geen KV-cache.
Bekijk voorbeeld
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 gpt_forward(ids,params,heads=4):
params={name:np.asarray(value,float) for name,value in params.items()}
ids=np.asarray(ids)
if ids.ndim!=2 or ids.dtype.kind not in 'iu' or ids.size==0 or ids.shape[1]>len(params['positions']) or ids.min()<0 or ids.max()>=len(params['tokens']):
raise ValueError('Ongeldige tokenreeks of contextlengte')
x=params['tokens'][ids]+params['positions'][:ids.shape[1]]
allowed=np.tril(np.ones((ids.shape[1],ids.shape[1]),bool))
h,bc=transformer_block(x,params,heads,allowed)
n,nc=layernorm_forward(h,params['gf'],params['bf'])
return n@params['head']+params['bias'],{'ids':ids,'block':bc,'finalnorm':nc,'n':n}
Verbind de terugweg aan embeddings en alle modelparameters
Het headvoorbeeld geeft voor doel 1 de rij p−onehot. Het outer-product met N verzamelt de headbijdrage; bias krijgt de som van logitgradiënten. Daarna loopt de gradiënt via de laatste LayerNorm en het blok terug naar token- en positievectoren.
Maak gpt_backward met alle achttien sleutels. Head- en biasbijdragen sommeren over batch en tijd; herhaalde token-ID’s verzamelen hun dx bij dezelfde rij. Positiegradiënten sommeren over de batchas en blijven op hun eigen tijdpositie staan.
Twee voorkomens van hetzelfde token met bijdragen [1,2] en [3,4] geven samen [4,6], niet alleen de laatste bijdrage. Gebruik bijvoorbeeld np.add.at voor die gedeelde rijen. Deel niet nogmaals door batch of tijd: de verliesfunctie heeft al gemiddeld.
Bekijk voorbeeld
def gpt_backward(cache,params,dlogits):
params={name:np.asarray(value,float) for name,value in params.items()}
dlogits=np.asarray(dlogits,float)
g={'head':np.einsum('btc,btv->cv',cache['n'],dlogits),'bias':dlogits.sum(axis=(0,1))}
dh,g['gf'],g['bf']=layernorm_backward(cache['finalnorm'],dlogits@params['head'].T)
dx,bg=block_backward(cache['block'],params,dh)
g.update(bg)
g['tokens']=np.zeros_like(params['tokens'])
np.add.at(g['tokens'],cache['ids'].ravel(),dx.reshape(-1,dx.shape[-1]))
g['positions']=np.zeros_like(params['positions'])
g['positions'][:dx.shape[1]]=dx.sum(axis=0)
return g
Train met Adam, selecteer uitsluitend op validatie
Een leerstap voorspelt echte volgende tokens, berekent alle gradiënten en past alle parametergroepen aan. De globale clipnorm wordt over alle arrays samen bepaald; [3,4] wordt bij grens 1 bijvoorbeeld [.6,.8]. Gebruik voor de standaardrun 250 minibatches van 16 vensters, seed 14 en Adam met η=0.005, β₁=0.9, β₂=0.999 en ε=1e−8.
Meet vóór training en iedere 25 stappen met de gegeven evaluate-helper. Bij validatieverliezen .8→.6→.7 blijft de middelste parameterkopie gekozen; initial en beste params zijn echte afzonderlijke kopieën. train_gpt krijgt geen testdata en accepteert een exact integeraantal stappen 0..500.
Retourneer params,history,chosen,initial en behoud de presentatie onder de functie. De standaardrun leert alle achttien groepen. Een lage trainingsloss alleen kiest geen goed checkpoint: kijk naar de apart gehouden validatie.
Bekijk voorbeeld
def windows(data,starts,context):
x=np.stack([data[s:s+context] for s in starts])
y=np.stack([data[s+1:s+context+1] for s in starts])
return x,y
def evaluate(params,data,heads=4):
context=len(params['positions'])
starts=np.linspace(0,len(data)-context-1,12,dtype=int)
x,y=windows(data,starts,context)
logits,_=gpt_forward(x,params,heads)
return token_loss(logits,y)[0]
def train_gpt(train_data,val_data,vocab_size,steps=250):
if type(steps) is not int or not 0<=steps<=500:
raise ValueError('Gebruik 0 tot 500 trainingsstappen')
params=init_gpt(vocab_size)
rng=np.random.default_rng(14)
first={k:v.copy() for k,v in params.items()}
m={k:np.zeros_like(v) for k,v in params.items()};v={k:np.zeros_like(a) for k,a in params.items()}
history=[(0,evaluate(params,train_data),evaluate(params,val_data))]
best=history[0][2];chosen=0;best_params={k:a.copy() for k,a in params.items()}
context=len(params['positions'])
for step in range(1,steps+1):
starts=rng.integers(0,len(train_data)-context,16)
x,y=windows(train_data,starts,context)
logits,cache=gpt_forward(x,params)
loss,dlogits=token_loss(logits,y)
grads=gpt_backward(cache,params,dlogits)
norm=np.sqrt(sum(float(np.sum(g*g)) for g in grads.values()))
scale=min(1.,1./max(norm,1e-12))
for name in params:
g=grads[name]*scale
m[name]=.9*m[name]+.1*g
v[name]=.999*v[name]+.001*g*g
params[name]-=.005*(m[name]/(1-.9**step))/(np.sqrt(v[name]/(1-.999**step))+1e-8)
if step%25==0 or step==steps:
tr=evaluate(params,train_data);va=evaluate(params,val_data)
history.append((step,tr,va))
if va<best:
best=va;chosen=step;best_params={k:a.copy() for k,a in params.items()}
return {'params':best_params,'history':history,'chosen':chosen,'initial':first}
trained=train_gpt(data['train'],data['val'],len(alphabet),250)
if trained is not None:
print('Gekozen stap:',trained['chosen'])
print('Validatie-NLL:',round(evaluate(trained['params'],data['val']),4))Voer eigen gekozen tekens terug in het model
Genereren voert de eigen keuze terug: in het speelgoedspoor groeit [0] via [0,1] en [0,1,2] naar [0,1,2,0]. Bereken iedere stap opnieuw het laatste contextvenster en gebruik de laatste logitrij. De eerste rij hoort bij een eerdere voorspelling.
Maak generate zonder parameters te veranderen. Bij temperature 0 kies je argmax; anders sample je uit de genormaliseerde kansen na temperatuurdeling, met een lokale generator voor de seed. Ongelijke logits blijven dus ongelijk gewogen; een uniforme sampler berekent een andere verdeling.
Prompt is niet-lege tekst uit het vaste alfabet, count een exacte integer 0..512 en temperature een eindig niet-negatief getal; ongeldige invoer geeft ValueError. Dezelfde seed maakt jouw samplingprocedure reproduceerbaar. Posities worden bij ieder afgeknipt venster opnieuw 0..15; dit is geen KV-cache.
Bekijk voorbeeld
def generate(params,prompt,count=100,temperature=.8,seed=41):
ids=list(encode_text(prompt))
try:
value=np.asarray(temperature)
temperature=float(value) if value.ndim==0 and value.dtype.kind in 'biuf' else float('nan')
except (TypeError,ValueError,OverflowError):
raise ValueError('Temperature moet een eindig niet-negatief getal zijn') from None
if not ids or type(count) is not int or not 0<=count<=512 or not np.isfinite(temperature) or temperature<0:
raise ValueError('Gebruik een prompt, maximaal 512 nieuwe tekens en temperature >= 0')
rng=np.random.default_rng(seed)
for _ in range(count):
context=ids[-len(params['positions']):]
logits,_=gpt_forward(np.array([context]),params)
scores=logits[0,-1]
if temperature==0:
token=int(np.argmax(scores))
else:
scores=(scores-scores.max())/temperature
p=np.exp(scores);p/=p.sum()
token=int(rng.choice(len(p),p=p))
ids.append(token)
return ''.join(alphabet[i] for i in ids)
if trained is not None:
print('Greedy vervolg:')
print(generate(trained['params'],'de ',100,0))
print('Sampling, temperature 0.8:')
print(generate(trained['params'],'de ',100,.8))Bekijk echte leercurves, testverlies en aangeleerde attention
Maak het rapport uit het daadwerkelijk meegegeven model en de meegegeven datasplits. Rapporteer parameters,chosen,validation_nll,test_nll,uniform_nll en changed_groups. Uniform voorspellen geeft log(29), terwijl de standaardreferentie ongeveer .3527 validatie-NLL en .3606 test-NLL haalt.
Behoud de echte train-/validatiecurves, gekozen stap en geleerde head 1 voor prompt “de kat ”. De testmeting gebruikt twaalf vaste vensters; overlap kan dezelfde tekstpositie meermaals laten meetellen en stuurt de selectie niet. Controleer dat alle achttien parametergroepen werkelijk veranderden.
Bekijk daarnaast vrije tekst: de referentie kan bijvoorbeeld “de kind” schrijven en zinnen herhalen ondanks lage teacherforcing-NLL. Een attentionkaart is een rekenresultaat en verklaart niet het hele taalantwoord. Benoem in je eigen conclusie wat deze kleine corpusproef wel aantoont en waar je meer data of een andere evaluatie nodig hebt.
Bekijk voorbeeld
def make_report(trained,data):
params=trained['params']
return {'parameters':sum(a.size for a in params.values()),'chosen':trained['chosen'],
'validation_nll':evaluate(params,data['val']),'test_nll':evaluate(params,data['test']),
'uniform_nll':float(np.log(len(alphabet))),
'changed_groups':sum(not np.array_equal(params[k],v) for k,v in trained['initial'].items())}
report=make_report(trained,data)
if report is not None:
print('Trainbare getallen:',report['parameters'])
print('Gewijzigde parametergroepen:',report['changed_groups'])
print('Uniforme basis-NLL:',round(report['uniform_nll'],4))
print('Test-NLL op 12 vaste vensters:',round(report['test_nll'],4))
history=np.asarray(trained['history'])
fig,ax=plt.subplots(figsize=(5,3.2))
ax.plot(history[:,0],history[:,1],label='Training',color='#278b74')
ax.plot(history[:,0],history[:,2],label='Validatie',color='#c27440')
ax.axvline(report['chosen'],color='#627068',linestyle='--',linewidth=1)
ax.set(xlabel='Trainingsstap',ylabel='Gemiddelde token-NLL',title='Echt mini-GPT trainen')
ax.legend();ax.tick_params(labelsize=11);fig.tight_layout()
prompt='de kat '
logits,cache=gpt_forward(encode_text(prompt)[None,:],trained['params'])
attention_map=cache['block']['attention']['attention']['weights'][0,0]
fig2,ax2=plt.subplots(figsize=(5,4))
im=ax2.imshow(attention_map,vmin=0,vmax=1,cmap='Greens')
ticks=list(range(len(prompt)));labels=[c if c!=' ' else 'sp' for c in prompt]
ax2.set_xticks(ticks,labels);ax2.set_yticks(ticks,labels)
ax2.set(xlabel='Keypositie',ylabel='Querypositie',title='Geleerde head 1: de kat [spatie]')
fig2.colorbar(im,ax=ax2,label='Attentiongewicht')
fig2.tight_layout()