Werkplaats / De werkplaats
In jouw eigen tempo
← Klassieke machine learning
MODULE 04 · LES 13

Laat ieder voorbeeld een keer onbekend zijn

Bouw k-fold cross-validatie met een verse fit per fold en kies hyperparameters zonder de eindtest te gebruiken.

60 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je verdeelt iedere rij precies één keer over validatiefolds.
  • Je fit opnieuw per fold en kiest een parameter uitsluitend op validatiescores.

Je vergelijkt twee modellen, maar één andere train/validatiesplit draait de winnaar om. Was het model beter, of kreeg het toevallig eenvoudigere validatievoorbeelden? K-fold crossvalidatie (cross-validation) herhaalt de vergelijking met verschillende achtergehouden delen van dezelfde ontwikkeldata.

Ieder voorbeeld krijgt één validatiebeurt. De uiteindelijke testset blijft apart: je gebruikt de folds om een aanpak te kiezen, fit die daarna opnieuw op alle ontwikkeldata en beoordeelt haar ten slotte op die eindtest.

1. Geef iedere rij één beurt buiten de training

Neem vijf ontwikkelrijen met indices [0,1,2,3,4] en k = 2. Je kunt vijf niet eerlijk in twee gelijke gehele groepen verdelen. Geef daarom de eerste fold drie validatierijen en de tweede twee.

RondeTrainingValidatie
13,40,1,2
20,1,23,4

Iedere rij staat precies eenmaal in een validatielijst. Binnen iedere ronde zijn training en validatie disjunct en vormen ze samen alle ontwikkelrijen. Een rij kan dus in een andere ronde wél training zijn.

n, k = 5, 2
start = 0
for i in range(k):
    grootte = n // k
    if i < n % k:
        grootte = grootte + 1
    einde = start + grootte
    val = list(range(start, einde))
    train = []
    for j in range(n):
        if j not in val:
            train.append(j)
    print(train, val)
    start = einde
# [3, 4] [0, 1, 2]
# [0, 1, 2] [3, 4]

// geeft het gehele quotiënt en % de rest. Maak folds(n,k) als lijst van (train,val)-paren, met gehele aantallen en 2 ≤ k ≤ n; ongeldige aantallen geven ValueError. Er wordt hier niet geschud: de invoer bevat onafhankelijke voorbeelden in een passende volgorde.

VIER KEER EEN NIEUWE FITElke rij krijgt één validatiebeurt

Twaalf rijen, vier folds. Groen is training; oranje is validatie. Kies een ronde en volg de drie onbekende rijen.

Trainingsrijen9Validatierijen1, 2, 3Nieuwe fit1 / 4

Ook de scaler wordt elke ronde opnieuw op alleen de negen trainingsrijen gefit. De uiteindelijke testset zit niet in dit schema.

Het diagram heeft twaalf rijen en vier folds. Het toont rijlabels 1–12 voor de leesbaarheid; jouw code gebruikt indices vanaf nul. Zoek in ronde 3 welke drie rijen onbekend zijn en controleer dat training hun complement is.

Gewone folds bewaken geen plantgroepen of tijdsvolgorde. Gebruik bij afhankelijke gegevens de groeps- en tijdsplitsingen uit de datatrack.

01
CHECKPOINTVerdeel iedere rij precies één keer

2. Laat een functie per fold opnieuw leren

De evaluatiecode hoeft niet te weten welk model je kiest. Zij krijgt een functie fit_predict(train_x,train_y,val_x) mee: de functie zelf is het argument, zonder haar meteen aan te roepen. Pas binnen iedere ronde roep je haar met de drie juiste gegevenslijsten aan.

Begin met een eenvoudige baseline die alleen het trainingsgemiddelde leert. Bij doelen [0,1,2,3,4] is in ronde 1 het trainingsgemiddelde (3+4)/2 = 3.5. De voorspellingen voor de drie validatierijen zijn dus [3.5,3.5,3.5].

def fit_gemiddelde(train_x, train_y, val_x):
    gemiddelde = sum(train_y) / len(train_y)
    return [gemiddelde] * len(val_x)

xs = [10, 20, 30, 40, 50]
ys = [0, 1, 2, 3, 4]
p = fit_gemiddelde(
    [40, 50], [3, 4], [10, 20, 30])
print(p)  # [3.5, 3.5, 3.5]

De callback gebruikt train_x hier niet, omdat een constante baseline geen features nodig heeft. Een echte modelcallback bouwt bij iedere aanroep een nieuwe pipeline, fit haar op train_x/train_y en voorspelt val_x. Ook een scaler mag alleen uit die trainingsrijen leren.

3. Meet alleen de achtergehouden doelen

De kwadratische fouten in ronde 1 zijn 12.25, 6.25 en 2.25: MSE = 20.75/3, ongeveer 6.916667. In ronde 2 leert de baseline gemiddelde 1 en voorspelt dat voor doelen 3 en 4: MSE = (4+9)/2 = 6.5.

val_y = [0, 1, 2]
totaal = 0
for i in range(len(p)):
    totaal = totaal + (val_y[i] - p[i]) ** 2
print(totaal / len(p))  # ongeveer 6.916667

Maak cv_scores(xs,ys,k,fit_predict) met een verse callbackaanroep per fold en één MSE per ronde. Controleer gelijke invoerlengtes en precies één voorspelling per validatierij; een verkeerde voorspellinglengte geeft ValueError. Gebruik de validatie-indices om doelen en voorspellingen correct te koppelen.

Veelgemaakte fout: de trainings-x-waarden als doelen doorgeven. Dat kan onzichtbaar blijven als een oefening toevallig x = y gebruikt; onze hoogtes en doelen zijn bewust verschillend.

02
CHECKPOINTEen verse fit, inclusief voorbereiding

4. Kies eerst het criterium, dan de winnaar

Een hyperparameter stel je vóór het fitten in, zoals een boomdiepte. Vergelijk per kandidaat het gemiddelde van zijn foldscores. Hier telt iedere fold even zwaar, ook wanneer de aantallen validatierijen verschillen.

S=1k∑Lj

j benoemt een fold en L haar gemiddelde verlies. Voor onze baseline geeft dit ongeveer 6.708333; alle vijf afzonderlijke fouten samen middelen zou 6.75 geven. Beide criteria zijn mogelijk, maar je moet vooraf weten welk je gebruikt.

resultaten = {1: [6, 8],
              2: [4, 6],
              3: [3, 7]}
kandidaten = []
for parameter, scores in resultaten.items():
    gemiddelde = sum(scores) / len(scores)
    paar = gemiddelde, parameter
    kandidaten.append(paar)
print(min(kandidaten)[1])  # 2

Parameters 2 en 3 hebben beide gemiddelde 5; onze vaste tie-regel kiest de kleinste integerparameter. Maak kies_parameter(resultaten) en weiger een lege dictionary of lege scorelijsten. Gebruik eindige scores.

De winnende CV-score is al door de selectie beïnvloed. Beoordeel de uiteindelijk gekozen aanpak daarom op een aparte testset, of gebruik een buitenste crossvalidatieronde wanneer je het hele selectieproces wilt onderzoeken.

03
CHECKPOINTMaak je selectiecriterium expliciet

Wat neem je mee?

  • Iedere rij is eenmaal validatie; training is per ronde het complement.
  • De callback leert iedere ronde opnieuw, inclusief voorbereiding.
  • Het selectiecriterium en de aparte eindtest horen bij de modelkeuze.

Hierna onderzoek je waarom meer modelvrijheid soms helpt en soms vooral trainingsruis volgt.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-28.
DE VOLGENDE STAPMeer kunnen passen is niet altijd beter
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
Verdeel iedere rij precies één keer

Schrijf folds(n,k) als lijst van (trainindices,valindices). Vereis gehele aantallen met 2≤k≤n, anders ValueError.

Een hint

Gebruik n//k als basisgrootte en verdeel de rest over de eerste blokken.

02
Een verse fit, inclusief voorbereiding

Schrijf cv_scores(xs,ys,k,fit_predict) als lijst MSE-scores.

Een hint

Gebruik folds en bouw de drie argumenten met de juiste indices; controleer evenveel voorspellingen als validatiedoelen.

03
Maak je selectiecriterium expliciet

Schrijf kies_parameter(resultaten), met een dictionary parameter→niet-lege lijst verliezen.

Een hint

Minimaliseer (gemiddelde_loss,parameter); weiger lege resultaten of scorelijsten.

Vastgelopen? Bekijk de voorbeeldoplossing
def folds(n,k):
    if type(n) is not int or type(k) is not int or not 2<=k<=n:
        raise ValueError("Vereist 2<=k<=n")
    result=[]
    start=0
    for i in range(k):
        size=n//k+int(i<n%k)
        val=list(range(start,start+size))
        train=[j for j in range(n) if not start<=j<start+size]
        result.append((train,val))
        start+=size
    return result

def cv_scores(xs,ys,k,fit_predict):
    if len(xs)!=len(ys):
        raise ValueError("Ongelijke lengtes")
    scores=[]
    for train,val in folds(len(xs),k):
        pred=fit_predict([xs[i] for i in train],[ys[i] for i in train],[xs[i] for i in val])
        if len(pred)!=len(val):
            raise ValueError("Verkeerd aantal voorspellingen")
        scores.append(sum((ys[i]-p)**2 for i,p in zip(val,pred))/len(val))
    return scores

def kies_parameter(resultaten):
    if not resultaten or any(not scores for scores in resultaten.values()):
        raise ValueError("Scores ontbreken")
    return min(resultaten,key=lambda p:(sum(resultaten[p])/len(resultaten[p]),p))

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