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

Leer van antwoorden zonder de vraag als antwoorddoel te tellen

Serialiseer een klein chatformaat, verschuif labels correct en bereken assistant-only cross-entropy met numeriek gecontroleerde gradiënten.

60 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je maakt expliciet welke antwoordtokens een SFT-voorbeeld laten meetellen.
  • Je verschuift een loss-masker samen met de doelen en voorkomt een fout van één positie.

Een leerling moet een vraag beantwoorden, niet de vraag overschrijven. Zo kan supervised fine-tuning (SFT) een taalmodel van gewenste antwoorden laten leren, terwijl de vraag wel als context beschikbaar blijft. Dat vraagt een precies tokenformaat én een masker voor de juiste doelen.

Bij instructietuning veranderen de voorbeelden en het gewenste gedrag. De volgende-tokenloss kan dezelfde blijven. In deze les kiezen we assistant-only loss: antwoordinhoud en het einde van een assistantbericht tellen, andere tokens niet.

1. Maak rollen expliciet

Ons eigen speelgoedformaat gebruikt BOS=1, eindmarker=2 en rolmarkers system=3, user=4, assistant=5. Inhoudstokens beginnen bij ID 10. Een userbericht [10] gevolgd door assistantantwoord [11] wordt [1,4,10,2,5,11,2].

messages = [
    {'role':'user','tokens':[10]},
    {'role':'assistant','tokens':[11]}]
ids,learn = serialize_chat(messages)
print(ids)  # [1 4 10 2 5 11 2]
print(learn.astype(int))
# [0 0 0 0 0 1 1]

De laatste twee vlaggen markeren antwoord 11 en zijn eindmarker. BOS, rolmarkers en userinhoud zijn geen antwoorddoelen. De rol komt uit gestructureerde metadata; tekst die toevallig “assistant” zegt verandert die metadata niet.

Maak serialize_chat voor de opgegeven berichtenlijst en int64-inhoudstokens. System mag alleen vooraan staan en de hele reeks heeft maximaal 512 tokens. Onbekende rollen of ongeldige inhoud geven ValueError.

Dit formaat is een eigen oefencontract. Een echt geladen model gebruikt zijn eigen tokenizer en chattemplate. Een template bepaalt de serialisatie, maar vormt geen beveiligingsgrens tegen promptinjectie.

01
CHECKPOINTEen chattemplate is een exact tokencontract

2. Verschuif doelen en vlaggen samen

De modelrij voor inputtoken t voorspelt de volgende token. Inputs worden daarom ids[:-1], targets ids[1:] en het doelmasker learn[1:]. De vlag hoort bij wat je probeert te voorspellen, niet bij de token die je invoert.

x,y,mask = sft_pairs(ids,learn)
print(x)  # [1 4 10 2 5 11]
print(y)  # [4 10 2 5 11 2]
print(mask.astype(int))
# [0 0 0 0 1 1]
print(x[mask])  # [5 11]

De rolmarker 5 is zelf geen antwoordinhoud, maar zijn uitvoer voorspelt het eerste antwoordtoken 11 en telt dus wel mee. Een onverschoven masker mist dat eerste doel. Dat is de typische fout van één positie.

Maak sft_pairs met onafhankelijke kopieën. In een gesprek met meerdere assistantbeurten tellen hier alle antwoorden. Alleen de laatste beurt trainen vraagt een bewust ander masker.

02
CHECKPOINTHet masker hoort bij de doeltoken, niet bij de inputtoken

3. Deel door het aantal antwoorddoelen

Neem drie logitrijen met twee mogelijke tokens. De eerste rij is prompt en telt niet; beide antwoordrijen geven hun juiste token kans .75. Hun gemiddelde negatieve logkans is −log(.75), ongeveer .2877.

L=−1M∑tmtlogpt(yt)

m is het doelmasker, M het aantal True-posities, y de target en p zijn modelkans. De som loopt over tijdposities t. Een volledig leeg doelmasker weigeren we, zodat een lege trainingsbatch opvalt.

z = np.array([[10.,-10.],
              [0.,np.log(3)],
              [np.log(3),0.]])
target = np.array([0,1,0])
mask = np.array([0,1,1],bool)
loss,grad = sft_loss(z,target,mask)
print(round(loss,4))  # .2877
print(np.round(grad,3))
# [[ 0.     0.   ]
#  [ .125 -.125]
#  [-.125  .125]]

Maak sft_loss met stabiele logkansen en nulgradiënten op niet-geselecteerde logitrijen. De noemer is twee antwoorddoelen, niet drie inputrijen. Promptcontext kan nog steeds de latere antwoorden en verborgen gradiënten beïnvloeden; nul promptlogitgradiënten zeggen niet dat de vraag wordt genegeerd.

Lange antwoorden leveren binnen zo’n samengevoegde batch meer doelen dan korte. Gemiddelde loss per antwoord is een ander leerdoel. Kies de reductie expliciet en controleer de kwaliteit op apart gehouden taken; lage trainingsloss garandeert geen waarheid of veiligheid.

03
CHECKPOINTMiddel uitsluitend over de geleerde antwoordposities

Wat neem je mee?

  • Een chattemplate maakt rollen en tokenvolgorde precies.
  • Het leermasker verschuift samen met de targets.
  • Assistant-only loss middelt over geselecteerde antwoordtokens.

Hierna vergelijk je twee antwoorden om een voorkeursscore te leren.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-28.
DE VOLGENDE STAPLeer een voorkeursscore en begrens de beleidsverandering
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
Een chattemplate is een exact tokencontract

Schrijf serialize_chat(messages) met (ids,learn), twee 1D-arrays (int64 en bool). messages is een niet-lege lijst met exact role en tokens; tokens is een niet-lege lijst van exacte ints vanaf 10 tot int64-max. Onbekende rollen, system buiten positie nul en meer dan 512 tokens geven ValueError.

Een hint

Begin met ids=[1] en learn=[False]. Voeg per bericht een ongeleerde rolmarker toe, daarna inhoud plus eindmarker met de juiste rolvlag.

02
Het masker hoort bij de doeltoken, niet bij de inputtoken

Schrijf sft_pairs(ids,learn) met gekopieerde (x,y,target_mask). Valideer een 1D-gehele niet-negatieve reeks van minstens twee tokens en een booleaanse evenlange learn-array; anders ValueError.

Een hint

Verschuif learn precies zoals y. Controleer ook dat de teruggegeven arrays geen schrijfbare views op de input zijn.

03
Middel uitsluitend over de geleerde antwoordposities

Schrijf sft_loss(logits,targets,mask) met (loss,grad). logits is een eindige niet-lege matrix (T,V), targets een geldige gehele vector (T,), mask een boolvector met minstens één True. Ongeldige invoer geeft ValueError.

Een hint

Bereken eerst de stabiele logkansen, selecteer de gekozen targetposities en nul vervolgens alle niet-geselecteerde gradiëntrijen.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

def serialize_chat(messages):
    roles={'system':3,'user':4,'assistant':5};ids=[1];learn=[False]
    if not isinstance(messages,list) or not messages:
        raise ValueError('Geen berichten')
    for index,message in enumerate(messages):
        if not isinstance(message,dict) or set(message)!={'role','tokens'} or not isinstance(message['role'],str) or message['role'] not in roles:
            raise ValueError('Ongeldig bericht')
        role=message['role'];tokens=message['tokens']
        if role=='system' and index!=0:
            raise ValueError('System hoort vooraan')
        if not isinstance(tokens,list) or not tokens or any(type(t) is not int or not 10<=t<=np.iinfo(np.int64).max for t in tokens):
            raise ValueError('Content-ID moet vanaf 10 zijn')
        ids.extend([roles[role],*tokens,2]);learn.extend([False]+[role=='assistant']*(len(tokens)+1))
    if len(ids)>512:
        raise ValueError('Maximaal 512 tokens')
    return np.array(ids,dtype=np.int64),np.array(learn,bool)

def sft_pairs(ids,learn):
    ids=np.asarray(ids);learn=np.asarray(learn)
    if ids.ndim!=1 or ids.size<2 or ids.dtype.kind not in 'iu' or np.any(ids<0) or learn.shape!=ids.shape or learn.dtype.kind!='b':
        raise ValueError('Ongeldige SFT-reeks')
    return ids[:-1].copy(),ids[1:].copy(),learn[1:].copy()

def sft_loss(logits,targets,mask):
    logits=np.asarray(logits,float);targets=np.asarray(targets);mask=np.asarray(mask)
    if logits.ndim!=2 or not logits.size or targets.shape!=logits.shape[:1] or targets.dtype.kind not in 'iu' or mask.shape!=targets.shape or mask.dtype.kind!='b' or not mask.any() or not np.isfinite(logits).all() or targets.min()<0 or targets.max()>=logits.shape[-1]:
        raise ValueError('Ongeldige SFT-logits of leeg doelmasker')
    z=logits-logits.max(axis=-1,keepdims=True);e=np.exp(z);p=e/e.sum(axis=-1,keepdims=True)
    logp=z-np.log(e.sum(axis=-1,keepdims=True));rows=np.arange(len(targets))
    loss=-float(logp[rows[mask],targets[mask]].mean());grad=p.copy();grad[rows,targets]-=1
    grad*=mask[:,None]/mask.sum()
    return loss,grad

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