Werkplaats / De werkplaats
In jouw eigen tempo
← Klassieke machine learning
MODULE 03 · LES 11

Laat het volgende model de resterende fout leren

Bouw squared-error gradient boosting met echte regressiestompen en een leersnelheid.

75 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je verklaart waarom residuen bij kwadratisch verlies de volgende trainingsdoelen zijn.
  • Je bouwt een ensemble dat nieuwe voorbeelden met alle geleerde stompen voorspelt.

Je voorspelt voor vier planten steeds groeisnelheid 2, maar de werkelijke waarden zijn [0,0,4,4]. Voor twee planten zit je twee te hoog en voor twee twee te laag. In plaats van opnieuw te beginnen, kun je een klein model laten leren welke correctie nog ontbreekt.

Dat is de gedachte achter gradient boosting: voeg modellen na elkaar toe. Een forest laat bomen naast elkaar leren; bij boosting hangen de trainingsdoelen voor de volgende boom af van wat de eerdere modellen al voorspellen.

1. Maak van de resterende fout een nieuw doel

Noem de huidige voorspelling F(x) en het werkelijke doel y. Voor half-gekwadrateerd verlies is de afgeleide naar F gelijk aan F−y; het negatieve daarvan is y−F. Dat residu wijst dus precies in de richting waarin de voorspelling moet veranderen.

ℓ=12(y−F)2−dℓdF=y−F=r

De factor een half heft de twee uit de afgeleide van het kwadraat op. r is het nieuwe trainingsdoel; een positief residu vraagt om ophogen, een negatief residu om verlagen. Bij andere verliezen verandert dit signaal.

xs = [0, 1, 2, 3]
y = [0, 0, 4, 4]
base = sum(y) / len(y)
pred = [base] * len(y)
r = []
for i in range(len(y)):
    r.append(y[i] - pred[i])
print(base, r)  # 2.0 [-2, -2, 2, 2]

Maak boost_residuen(y,pred) met één verschil per paar. Lege lijsten geven een lege lijst; ongelijke lengtes geven ValueError. De basisvoorspelling komt uit het trainingsgemiddelde, nooit uit de uiteindelijke testdoelen.

01
CHECKPOINTGebruik de fout als nieuwe opdracht

2. Laat één kleine boom de correctie leren

Een regressiestomp heeft één vraag en twee constante uitkomsten. Anders dan bij een classificatieboom zijn de bladeren gemiddelden van getallen, geen meerderheidslabels. Een grens 1.5 verdeelt onze residuen in [−2,−2] en [2,2], zodat bladvoorspellingen −2 en 2 alle residuen exact verklaren.

Bij grens 0.5 krijgt links gemiddelde −2 en rechts gemiddelde 2/3. Rechts blijven afwijkingen −8/3, 4/3 en 4/3 over; hun kwadratensom is 32/3, ongeveer 10.667. De perfecte grens 1.5 heeft kwadratensom nul en wint.

def beoordeel(xs, r, t):
    links, rechts = [], []
    for i in range(len(xs)):
        if xs[i] < t:
            links.append(r[i])
        else:
            rechts.append(r[i])
    laag = sum(links) / len(links)
    hoog = sum(rechts) / len(rechts)
    fout = 0
    for v in links:
        fout = fout + (v - laag) ** 2
    for v in rechts:
        fout = fout + (v - hoog) ** 2
    return fout, t, laag, hoog

print(beoordeel(xs, r, 0.5))
# ongeveer (10.667, 0.5, -2, 0.667)
print(beoordeel(xs, r, 1.5))
# (0.0, 1.5, -2.0, 2.0)

Maak fit_stomp(xs,r): vergelijk alle grenzen tussen unieke, gesorteerde x-waarden zoals bij beslisbomen. Bewaar per kandidaat (fout,t,laag,hoog) en kies de laagste fout, met dezelfde math.isclose-toleranties van 1e−12: bij gelijke fout blijft de kleinste grens staan. Gebruik ook de besproken afrondingscorrectie voor bijna gelijke floats.

Geef {"t":t,"left":laag,"right":hoog} terug. Bij gelijke x-waarden is geen split mogelijk: geef {"value":gemiddelde}. Lege of ongelijk lange invoer geeft ValueError; we werken met eindige getallen.

02
CHECKPOINTFit één eenvoudige correctie

3. Neem een deel van de correctie over

Met leersnelheid eta = 0.5 nemen we de helft van iedere correctie over: 2+0.5×(−2)=1 en 2+0.5×2=3. De nieuwe voorspellingen zijn [1,1,3,3]. De MSE daalt van 4 naar 1.

Fnieuw(x)=Foud(x)+ηh(x)

h is de nieuw gefitte stomp; eta bepaalt hoeveel ervan wordt toegevoegd. Vóór ronde twee bereken je opnieuw de residuen: [−1,−1,1,1]. Dezelfde grens past bij deze kleine dataset, nu met bladwaarden −1 en 1.

eta = 0.5
correctie = [-2, -2, 2, 2]
for i in range(len(pred)):
    pred[i] = pred[i] + eta * correctie[i]
nieuw_r = []
for i in range(len(y)):
    nieuw_r.append(y[i] - pred[i])
print(pred)     # [1.0, 1.0, 3.0, 3.0]
print(nieuw_r)  # [-1.0, -1.0, 1.0, 1.0]

Het diagram heeft drie verschillende doelgebieden; daarvoor kunnen opeenvolgende stompen andere grenzen kiezen. Vergelijk de eerste twee stappen met de beginsituatie. Een andere eta begint de proef opnieuw.

ELKE STOMP LEERT DE RESTERENDE FOUTTel echte correcties bij elkaar op

Doelen: 0, 0, 2, 2, 5, 5. Begin bij hun gemiddelde. Elke stap fit een nieuwe regressiestomp op de actuele residuen.

012345012345
Stompen0Trainings-MSE4.222

Dit is trainingsfout. Meer stompen kunnen op onbekende data alsnog slechter presteren.

4. Bewaar de hele som voor nieuwe voorbeelden

Na twee rondes bewaart het model de basis, eta en beide stompen. Een query 0.5 krijgt 2+0.5×(−2−1)=0.5; query 2.5 krijgt 2+0.5×(2+1)=3.5.

model = {"base": 2., "eta": .5,
         "trees": [
             {"t": 1.5, "left": -2,
              "right": 2},
             {"t": 1.5, "left": -1,
              "right": 1}]}
for x in [0.5, 2.5]:
    p = model["base"]
    for boom in model["trees"]:
        kant = "right"
        if x < boom["t"]:
            kant = "left"
        p = p + model["eta"] * boom[kant]
    print(p)  # eerst 0.5, daarna 3.5

Maak voorspel_boost(model,query) met ook ondersteuning voor constante bladeren. Maak vervolgens fit_boost(xs,y,eta,rondes): begin met het gemiddelde en een lege bomenlijst, voorspel op training, bereken residuen, fit een stomp en voeg hem toe. Herhaal dit het gekozen aantal rondes.

Weiger ongeldige paren, eta buiten (0,1] en een niet-geheel of negatief aantal rondes. Nul rondes laat alleen de basis over. Veelgemaakte fout: de laatst toegevoegde stomp als volledig model gebruiken; daarmee verdwijnen de eerdere correcties.

03
CHECKPOINTBewaar de hele som van correcties

Wat neem je mee?

  • Bij half-gekwadrateerd verlies zijn residuen de negatieve gradiënten naar de voorspellingen.
  • Iedere stomp leert de actuele fout; eta bepaalt zijn bijdrage.
  • Het ensemble voorspelt met de volledige som, en validatie bepaalt passende rondes en leersnelheid.

Hierna onderzoek je een andere manier om klassen te scheiden: ruimte laten rond de grens.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-28.
DE VOLGENDE STAPZoek ruimte tussen de klassen
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
Gebruik de fout als nieuwe opdracht

Schrijf boost_residuen(y,pred), met ValueError bij ongelijke lengtes.

Een hint

Trek de huidige voorspelling van ieder doel af.

02
Fit één eenvoudige correctie

Schrijf fit_stomp(xs,r) als {t,left,right}, of {value} bij constante x.

Een hint

Bereken voor iedere grens beide gemiddelden en de totale kwadratensom.

03
Bewaar de hele som van correcties

Schrijf fit_boost(xs,y,eta,rondes) en voorspel_boost(model,query). Weiger ongeldige paren, eta buiten (0,1] en niet-gehele of negatieve rondes.

Een hint

Maak een voorspeller die base + eta*som(stompvoorspellingen) berekent. Gebruik die na elke nieuwe stomp opnieuw.

Vastgelopen? Bekijk de voorbeeldoplossing
def boost_residuen(y, pred):
    if len(y) != len(pred):
        raise ValueError("Ongelijke lengtes")
    r = []
    for i in range(len(y)):
        r.append(y[i] - pred[i])
    return r


def fit_stomp(xs, r):
    import math
    if not xs or len(xs) != len(r):
        raise ValueError("Niet-lege gelijke paren vereist")
    waarden = sorted(set(xs))
    if len(waarden) == 1:
        return {"value": sum(r) / len(r)}
    kandidaten = []
    for j in range(len(waarden) - 1):
        a, b = waarden[j], waarden[j + 1]
        t = a / 2 + b / 2
        if t <= a:
            t = b
        links, rechts = [], []
        for i in range(len(xs)):
            if xs[i] < t:
                links.append(r[i])
            else:
                rechts.append(r[i])
        laag = sum(links) / len(links)
        hoog = sum(rechts) / len(rechts)
        fout = 0
        for v in links:
            fout = fout + (v - laag) ** 2
        for v in rechts:
            fout = fout + (v - hoog) ** 2
        kandidaten.append((fout, t, laag, hoog))
    beste = kandidaten[0]
    for kandidaat in kandidaten[1:]:
        fout = kandidaat[0]
        gelijk = math.isclose(fout, beste[0], rel_tol=1e-12, abs_tol=1e-12)
        if fout < beste[0] and not gelijk:
            beste = kandidaat
    fout, t, laag, hoog = beste
    return {"t": t, "left": laag, "right": hoog}


def voorspel_boost(model, query):
    result = []
    for x in query:
        p = model["base"]
        for boom in model["trees"]:
            if "value" in boom:
                correctie = boom["value"]
            elif x < boom["t"]:
                correctie = boom["left"]
            else:
                correctie = boom["right"]
            p = p + model["eta"] * correctie
        result.append(p)
    return result


def fit_boost(xs, y, eta, rondes):
    if not xs or len(xs) != len(y) or not 0 < eta <= 1:
        raise ValueError("Ongeldige trainingsparen of leersnelheid")
    if type(rondes) is not int or rondes < 0:
        raise ValueError("Niet-negatief geheel aantal rondes vereist")
    model = {"base": sum(y) / len(y), "eta": eta, "trees": []}
    for i in range(rondes):
        pred = voorspel_boost(model, xs)
        r = boost_residuen(y, pred)
        model["trees"].append(fit_stomp(xs, r))
    return model

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