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

Optimaliseer voorkeuren direct vanuit responslogkansen

Bereken complete responslogkansen, de DPO-loss met referentiemodel en een controle op dubbele of conflicterende voorkeurparen.

60 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je implementeert de DPO-loss zonder een apart rewardmodel te trainen.
  • Je voorkomt fouten door prompttokens, verkeerde reductie of inconsistente voorkeurparen.

Je hebt voor dezelfde vraag een gekozen en een afgewezen antwoord. Is het gekozen antwoord relatief sterker geworden dan in het vaste referentiemodel? Direct Preference Optimization (DPO) gebruikt precies zo’n vergelijking van responslogkansen.

De oorspronkelijke DPO-doelfunctie heeft geen apart getraind rewardmodel of PPO-lus nodig. Het model blijft wel afhankelijk van de kwaliteit en verdeling van de voorkeurdata. Een fout label wordt niet vanzelf een juiste norm doordat er een loss omheen staat.

1. Tel alle antwoordlogkansen op

De kans van een autoregressief antwoord is het product van conditionele tokenkansen. Logaritmeren maakt daarvan een som. We tellen alleen de antwoordposities, inclusief de afgesproken eindtoken; prompt en padding blijven buiten die som.

Neem twee geselecteerde doelen die ieder kans .75 krijgen. De responskans is .75²=.5625 en de logkans 2log(.75)=−.575364. Een gemiddelde zou −.287682 geven en daarmee een andere doelfunctie maken.

z = np.array([[[np.log(3),0.],
               [0.,np.log(3)]]])
targets = np.array([[0,1]])
mask = np.ones((1,2),bool)
total = response_log_probs(
    z,targets,mask)
print(np.round(total,4))  # [-.5754]

Maak response_log_probs met één som per batchrij. Targets zijn hier al verschoven: logitrij t voorspelt targets[t]. Behoud niet-uniforme kansen; alleen antwoordlengtes tellen is geen berekening van een modelrespons.

Langere antwoorden hebben meestal negatievere totale logkansen. Begrijp en evalueer dat lengte-effect, maar vervang de reductie niet stilzwijgend door een gemiddelde. Bij een lege antwoordrij weigeren we de batch.

01
CHECKPOINTDe kans van een antwoord bevat alle antwoordtokens

2. Vergelijk met dezelfde vaste referentie

Noem a het verschil tussen gekozen en afgewezen policylogkans. Noem b hetzelfde verschil bij de vaste referentie. Dan meet a−b hoe de relatieve voorkeur verschoven is.

z=β(a−b)L=log(1+e−z)

β is een positieve schaal in dit trainingsdoel, geen samplingtemperatuur. De pairloss L wordt klein als z positief groeit. Bereken hem met logaddexp om overloop door rechtstreeks exponentiëren te voorkomen.

Neem policylogkansen chosen=−1 en rejected=−2: a=1. De referentie heeft −2 en −1: b=−1. Bij β=.5 wordt z=.5×(1−(−1))=1 en loss ongeveer .313262.

c = np.array([-1.]);r = np.array([-2.])
rc = np.array([-2.])
rr = np.array([-1.])
loss,gc,gr = dpo_loss(c,r,rc,rr,.5)
print(round(loss,4))  # .3133
print(np.round(gc,4),np.round(gr,4))
# [-.1345] [.1345]
print(round(dpo_loss(
    rc,rr,rc,rr,.5)[0],4))  # .6931

Maak dpo_loss met het gemiddelde over de paren. De afgeleide bevat β én de batchnoemer. Wanneer policy en referentie gelijk zijn, is z nul en begint ieder paar bij log(2), ongeacht hun oorspronkelijke kansverhouding.

De gekozen logkans heeft een negatieve lossgradiënt en de afgewezen een positieve. In een gedeeld taalmodel garandeert dat niet dat de absolute kans van ieder gekozen antwoord stijgt. De parameters beïnvloeden immers meer antwoorden tegelijk.

02
CHECKPOINTVergelijk verbetering ten opzichte van dezelfde referentie

3. Maak herhaling en tegenspraak zichtbaar

Een dubbele voorkeurstriple krijgt bij uniforme training extra gewicht. Twee tegengestelde labels kunnen een fout zijn, maar ook een echt beoordelaarsverschil. Onze audit rapporteert beide en verwijdert niets automatisch.

a = {'prompt':'p','chosen':'a',
     'rejected':'b'}
b = {'prompt':'p','chosen':'b',
     'rejected':'a'}
c = {**a,'prompt':'q'}
r = audit_preferences([a,a,b,c])
print(r['unique'],r['duplicates'],
      r['conflicts'])  # 3 1 1

Maak audit_preferences met exacte strings. Een conflict telt één keer per ongeordend antwoordpaar binnen dezelfde prompt; een andere prompt is een andere context. Witruimte en spelling worden hier niet genormaliseerd.

Bewaar in echte data ook beoordelingscriteria en annotatorinformatie. Splits evaluatie op prompts of verwante taakfamilies, zodat bijna dezelfde voorbeelden niet aan beide kanten belanden. Een nette deduplicatie alleen voorkomt geen datalek of voorkeurbias.

03
CHECKPOINTMaak dubbele en tegengestelde labels zichtbaar

Wat neem je mee?

  • Responslogkansen sommeren alleen de afgesproken antwoordtokens.
  • DPO vergelijkt een relatieve verschuiving met een vaste referentie.
  • β, reductie en data-audit zijn inhoudelijke keuzes in het leerdoel.

Hierna verdeel je een beperkt rekenbudget tussen modelomvang en trainingsdata.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-28.
DE VOLGENDE STAPVerdeel een rekenbudget tussen model en trainingsdata
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
De kans van een antwoord bevat alle antwoordtokens

Schrijf response_log_probs(logits,targets,mask), retourneer één som per batchrij. Vormen zijn (B,T,V), (B,T), (B,T); targets zijn geldige gehele ID’s, mask is bool met minstens één True per rij en logits zijn eindig en niet leeg. Anders ValueError.

Een hint

Gebruik stabiele log-softmax, take_along_axis voor de targets en np.where(mask,selected,0) vóór sommeren over tijd.

02
Vergelijk verbetering ten opzichte van dezelfde referentie

Schrijf dpo_loss(chosen,rejected,ref_chosen,ref_rejected,beta=0.1) met (mean_loss,grad_chosen,grad_rejected). Alle invoervectoren zijn evenlang, 1D, niet-leeg, eindig en ≤0; beta is positief en eindig. Anders ValueError.

Een hint

Gebruik logaddexp voor de loss en exp(−logaddexp(0,z)) voor sigmoid(−z). De afgeleide bevat beta en de batchnoemer.

03
Maak dubbele en tegengestelde labels zichtbaar

Schrijf audit_preferences(records) met dictionary unique,duplicates,conflicts. records is een lijst met exact niet-lege strings prompt,chosen,rejected; chosen en rejected mogen niet gelijk zijn. Anders ValueError. Tel herhaalde geordende triples en unieke conflicterende ongeordende paren.

Een hint

Een set van (prompt,chosen,rejected) detecteert dubbele records. Kijk ook naar de omgekeerde triple, maar dedupliceer conflicten met gesorteerde antwoorden.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

def response_log_probs(logits,targets,mask):
    logits=np.asarray(logits,float);targets=np.asarray(targets);mask=np.asarray(mask)
    if logits.ndim!=3 or not logits.size or targets.shape!=logits.shape[:2] or targets.dtype.kind not in 'iu' or mask.shape!=targets.shape or mask.dtype.kind!='b' or np.any(mask.sum(axis=1)==0) or not np.isfinite(logits).all() or targets.min()<0 or targets.max()>=logits.shape[-1]:
        raise ValueError('Ongeldige responsbatch')
    z=logits-logits.max(axis=-1,keepdims=True);logp=z-np.log(np.exp(z).sum(axis=-1,keepdims=True))
    selected=np.take_along_axis(logp,targets[...,None],axis=-1)[...,0]
    return np.sum(np.where(mask,selected,0.),axis=1)

def dpo_loss(chosen,rejected,ref_chosen,ref_rejected,beta=.1):
    scalars=[np.asarray(v) for v in [beta]]
    if any(v.ndim!=0 or v.dtype.kind not in 'biuf' for v in scalars):
        raise ValueError('Gebruik eindige scalaire getallen')
    beta,=map(float,scalars)
    arrays=[np.asarray(a,float) for a in [chosen,rejected,ref_chosen,ref_rejected]]
    if arrays[0].ndim!=1 or not arrays[0].size or any(a.shape!=arrays[0].shape or not np.isfinite(a).all() or np.any(a>0) for a in arrays) or not np.isfinite(beta) or beta<=0:
        raise ValueError('Gebruik eindige logkansen en positieve beta')
    c,r,rc,rr=arrays;z=beta*((c-r)-(rc-rr))
    grad=-beta*np.exp(-np.logaddexp(0.,z))/len(z)
    return float(np.logaddexp(0.,-z).mean()),grad,-grad

def audit_preferences(records):
    if not isinstance(records,list):raise ValueError('Gebruik een lijst')
    seen=set();duplicates=0;conflicts=set()
    for row in records:
        if not isinstance(row,dict) or set(row)!={'prompt','chosen','rejected'} or any(not isinstance(v,str) or not v for v in row.values()) or row['chosen']==row['rejected']:
            raise ValueError('Ongeldig voorkeurspaar')
        p,c,r=row['prompt'],row['chosen'],row['rejected'];key=(p,c,r)
        if key in seen:duplicates+=1
        if (p,r,c) in seen:conflicts.add((p,*sorted([c,r])))
        seen.add(key)
    return {'unique':len(seen),'duplicates':duplicates,'conflicts':len(conflicts)}

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