- 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.
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.
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.
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.
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.
Doelen: 0, 0, 2, 2, 5, 5. Begin bij hun gemiddelde. Elke stap fit een nieuwe regressiestomp op de actuele residuen.
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.
03Wat 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.