Werkplaats / De werkplaats
In jouw eigen tempo
← Taalmodellen
MODULE 05 · LES 11

Kies het volgende token met controle over toeval

Implementeer temperature, top-k, nucleus sampling en een reproduceerbare trekking met expliciete grensgevallen.

60 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je verandert een logitverdeling zonder modelgewichten aan te passen.
  • Je onderscheidt filteren, renormaliseren en daadwerkelijk een token trekken.

Je mini-GPT kan steeds dezelfde zin herhalen. Kun je andere vervolgen krijgen zonder opnieuw te trainen? Een decodeerstrategie (decoding strategy) verandert hoe je uit de voorspelde logits een volgende token kiest; de modelgewichten blijven gelijk.

Denk aan een schaal met kaartjes. Temperatuur verandert de verhouding tussen de stapels, filters halen kandidaten weg en pas de trekking kiest één kaartje. Meer toeval maakt de uitvoer gevarieerder, maar controleert geen feiten.

1. Maak scherpe of vlakke kansen

De logits zijn scores, nog geen kansen. Bij positieve temperatuur deel je de verschillen tussen scores door τ, voordat je softmax toepast. Een lage τ versterkt verschillen; een hoge τ verkleint ze, terwijl de rangorde gelijk blijft.

pi=e(zi−m)/τ∑je(zj−m)/τ

Hier is z de logitvector, i de gekozen positie, j iedere mogelijke token en m de hoogste logit. Dezelfde m aftrekken verandert de kansen niet en voorkomt grote positieve exponenten. De formule geldt voor τ>0.

Neem logits [0,log(2),log(4)]. Bij τ=1 worden de exponenten evenredig met [1,2,4], dus de laatste token krijgt 4/7 kans. Bij τ=2 worden die verhoudingen [1,√2,2] en daalt zijn kans naar ongeveer .4531.

z = np.log([1.,2.,4.])
for temp in [1.,2.,0.]:
    p = temperature_probs(z,temp)
    print(np.round(p,4))
# [.1429 .2857 .5714]
# [.2265 .3204 .4531]
# [0. 0. 1.]

Maak temperature_probs voor eindige, niet-lege 1D-logits en een eindige scalaire temperatuur vanaf nul. Bij τ=0 spreken we greedy af: de eerste hoogste logit krijgt kans één. Dat is een afzonderlijke tak; delen door nul is geen implementatie van greedy.

01
CHECKPOINTTemperatuur verandert de scherpte van de verdeling

2. Filter kandidaten en normaliseer opnieuw

Top-k houdt maximaal k kandidaten met de hoogste kansen over. Nucleus sampling (top-p) houdt de kleinste gesorteerde prefix met samen minstens p kansmassa. De kandidaat die de grens passeert moet blijven, anders haal je de gewenste massa niet.

Neem [.5,.3,.2]. Top-k met k=2 bewaart .5 en .3; delen door .8 geeft [.625,.375,0]. Top-p met p=.6 heeft op de oorspronkelijke verdeling eveneens twee kandidaten nodig, want .5 alleen is te weinig.

p = np.array([.5,.3,.2])
k = top_k(p,2)
print(k)  # [.625 .375 0.]
print(top_p(p,.6))
# [.625 .375 0.]
print(top_p(k,.6))  # [1. 0. 0.]

Het laatste resultaat laat zien waarom de volgorde telt: na top-k is de eerste kans al .625 en haalt één kandidaat de grens .6. Onze les gebruikt temperatuur → top-k → top-p. Bij gelijke kansen wint de laagste token-ID de eerdere sorteerplek.

VAN LOGITS NAAR ÉÉN KEUZEWelke token mag nog meedoen?

Zes verzonnen logits. Volgorde: temperatuur → top-k → top-p → trekking.

TokenNa temperatuurNa filters
delogit 241.4%41.4%
eenlogit 1.525.1%25.1%
hetlogit 115.2%15.2%
mijnlogit 0.59.2%9.2%
jouwlogit 05.6%5.6%
onslogit -0.53.4%3.4%
Gekozen bij u = 0.500eenTokens met positieve kans6

Verander eerst alleen temperatuur en daarna één filter in de visualisatie. Bekijk zowel de percentages als het aantal kandidaten; een grotere kans is nog geen daadwerkelijke keuze. Maak top_k en top_p met het opgegeven kanscontract en renormaliseer na ieder filter.

02
CHECKPOINTTop-k telt kandidaten, top-p telt kansmassa

3. Trek uit intervallen

Voor [0,.25,0,.75] krijgt ID 1 interval [0,.25) en ID 3 interval [.25,1). Een trekking precies op .25 valt dus in het tweede interval. De nulkansen hebben geen interval en mogen nooit winnen.

p = [0.,.25,0.,.75]
for u in [0.,.249,.25,.999]:
    print(inverse_sample(p,u))
# 1  (u=0)
# 1  (u=.249)
# 3  (u=.25)
# 3  (u=.999)

Maak inverse_sample met u in [0,1). Bescherm de laatste cumulatieve grens tegen afronding en retourneer een Python-int. Een vaste seed kan buiten deze functie reproduceerbare u-waarden leveren; één herhaalbare uitkomst bewijst nog geen goede taalvervolging.

03
CHECKPOINTEen reproduceerbare trekking uit cumulatieve kansen

Wat neem je mee?

  • Temperatuur verandert kansen, filters veranderen de kandidatenverzameling.
  • Renormaliseren en de filtervolgorde bepalen de uiteindelijke verdeling.
  • Pas een trekking maakt van kansen één gekozen token.

Hierna versnel je het opnieuw voorspellen met een cache voor keys en values.

DE VOLGENDE STAPHergebruik keys en values zonder voorspellingen te veranderen
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
Temperatuur verandert de scherpte van de verdeling

Schrijf temperature_probs(logits,temperature=1.). logits is een eindige, niet-lege 1D-vector; temperatuur is een eindig scalair getal vanaf nul. Retourneer kansen met som 1. Weiger ongeldige invoer met ValueError.

Een hint

Voor τ>0: exp((logits−max)/τ), daarna delen door de som. Voor τ=0 vul je één argmax-positie.

02
Top-k telt kandidaten, top-p telt kansmassa

Schrijf top_k(probs,k) en top_p(probs,p). Accepteer een niet-lege eindige 1D-vector met niet-negatieve waarden en een eindige positieve som. k is een exacte integer van 1 tot V; p is een eindig getal met 0<p≤1. Anders ValueError.

Een hint

np.lexsort((token_ids,-probs)) geeft een vaste rangorde. Gebruik searchsorted op de cumulatieve som met side="left" en neem die positie mee.

03
Een reproduceerbare trekking uit cumulatieve kansen

Schrijf inverse_sample(probs,u), retourneer een Python-int token-ID. Gebruik hetzelfde kanscontract als de filters; u is eindig met 0≤u<1. Weiger ongeldige invoer. Bescherm de laatste cumulatieve grens tegen afronding.

Een hint

Werk alleen met positieve posities. Zet de laatste cumulatieve waarde expliciet op 1 en gebruik searchsorted(...,side="right").

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

def temperature_probs(logits,temperature=1.):
    scalar=np.asarray(temperature)
    if scalar.ndim!=0 or scalar.dtype.kind not in 'biuf':
        raise ValueError('Gebruik een eindig scalair getal')
    temperature=float(scalar)
    logits=np.asarray(logits,float)
    if logits.ndim!=1 or logits.size==0 or not np.isfinite(logits).all() or not np.isfinite(temperature) or temperature<0:
        raise ValueError('Ongeldige logits of temperatuur')
    if temperature==0:
        p=np.zeros_like(logits);p[np.argmax(logits)]=1.
        return p
    with np.errstate(over='ignore',under='ignore'):
        scaled=logits/temperature if temperature>=1 else logits-logits.max()
        scaled=scaled-scaled.max() if temperature>=1 else scaled/temperature
        p=np.exp(scaled)
    return p/p.sum()

def top_k(probs,k):
    probs=np.asarray(probs,float)
    if probs.ndim!=1 or probs.size==0 or not np.isfinite(probs).all() or np.any(probs<0) or not np.isfinite(probs.sum()) or probs.sum()<=0 or type(k) is not int or not 1<=k<=len(probs):
        raise ValueError('Ongeldige kansen of k')
    order=np.lexsort((np.arange(len(probs)),-probs))
    result=np.zeros_like(probs);result[order[:k]]=probs[order[:k]]
    return result/result.sum()

def top_p(probs,p):
    scalar=np.asarray(p)
    if scalar.ndim!=0 or scalar.dtype.kind not in 'biuf':
        raise ValueError('Gebruik een eindig scalair getal')
    p=float(scalar)
    probs=np.asarray(probs,float)
    if probs.ndim!=1 or probs.size==0 or not np.isfinite(probs).all() or np.any(probs<0) or not np.isfinite(probs.sum()) or probs.sum()<=0 or not np.isfinite(p) or not 0<p<=1:
        raise ValueError('Ongeldige kansen of p')
    normalized=probs/probs.sum()
    order=np.lexsort((np.arange(len(probs)),-normalized))
    count=min(len(probs),int(np.searchsorted(np.cumsum(normalized[order]),p,side='left'))+1)
    result=np.zeros_like(probs);result[order[:count]]=normalized[order[:count]]
    return result/result.sum()

def inverse_sample(probs,u):
    scalar=np.asarray(u)
    if scalar.ndim!=0 or scalar.dtype.kind not in 'biuf':
        raise ValueError('Gebruik een eindig scalair getal')
    u=float(scalar)
    probs=np.asarray(probs,float)
    if probs.ndim!=1 or probs.size==0 or not np.isfinite(probs).all() or np.any(probs<0) or not np.isfinite(probs.sum()) or probs.sum()<=0 or not np.isfinite(u) or not 0<=u<1:
        raise ValueError('Ongeldige kansen of trekking')
    positive=np.flatnonzero(probs>0)
    cumulative=np.cumsum(probs[positive]/probs.sum());cumulative[-1]=1.
    return int(positive[np.searchsorted(cumulative,u,side='right')])

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