- Je berekent een leersnelheidsschema met expliciete begin- en eindpunten.
- Je begrenst een gezamenlijke gradiëntnorm en kiest een stopmoment zonder de testset te gebruiken.
Een goede update heeft een richting én een geschikte grootte. Je stuurt het leertempo vooraf, begrenst uitzonderlijk grote gradiënten en bewaart de versie die op validatiegegevens het beste werkt. Deze drie ingrepen hebben elk een eigen taak.
Een schema kan de start voorzichtiger maken en later kleinere aanpassingen toestaan. Clipping kan een uitschieter temperen. Vroeg stoppen voorkomt dat je automatisch de laatste fit gebruikt. Geen van drieën garandeert goede voorspellingen of herstelt een slechte datasplit.
1. Bouw eerst op, daal daarna rustig
Bekijk in de visualisatie stap 0, de warmupgrens en het einde. Zet daarna warmup op nul. Waarom begint het schema dan direct bij de piek?
100 intervallen, stapindices 0 tot en met 100. Warmup start bij nul; zonder warmup begint het schema direct bij de piek.
Warmup verhoogt de leersnelheid lineair van nul naar peak. Daarna gebruiken we cosine decay, een geleidelijke daling naar floor. Stapindices lopen van 0 tot en met total; de warmupgrens is al de piek, niet nog een kleinere tussenwaarde.
Noem de voortgang na warmup t; P en F staan in de formule voor peak en floor. Cosinus beschrijft hier een gladde boog: cos(0)=1, cos(π/2)=0 en cos(π)=−1. π is de cirkelconstante; math.cos rekent met radialen. Daardoor gaat de factor (1+cos(πt))/2 van één via een half naar nul.
Neem total=10, warmup=2, peak=1 en floor=0.1. Op stap 1 is eta=1×1/2=0.5. Op stap 6 is t=4/8=0.5, dus eta=0.1+0.9/2=0.55. Op stap 4 krijg je ongeveer 0.868198, geen rechte lijn tussen piek en eindwaarde.
import math
def leersnelheid(step,total,warmup,
peak,floor=0):
if warmup and step < warmup:
return peak*(step/warmup)
t = (step-warmup)/(total-warmup)
c = (1+math.cos(math.pi*t))/2
return floor+(peak-floor)*c
for step in [0,1,2,4,6,10]:
eta = leersnelheid(step,10,2,1,.1)
print(step,round(eta,6))
# 0 0.0; 1 0.5; 2 1.0
# 4 0.868198; 6 0.55; 10 0.1
Maak de functie voor gehele stapindices met 0≤warmup<total, 0≤step≤total en eindige 0≤floor≤peak. Floor is de eindwaarde van de daling: tijdens warmup mag eta daaronder liggen. De eerste update bij warmup heeft bewust leersnelheid nul.
2. Verklein samen, behoud de richting
Gradient clipping begrenst de gezamenlijke L2-norm. Denk alle arrays achter elkaar. Noem die vector g, zijn norm n en de positieve grens c. Alleen wanneer n groter is dan c schaal je alle componenten met dezelfde factor.
Bij arrays [3,4] en [12] is n=√(9+16+144)=13. Met grens 6.5 is de factor 0.5, dus krijg je [1.5,2] en [6]. Afzonderlijke componenten afkappen zou meestal de richting veranderen. Een kleine norm schaal je niet omhoog; nul blijft nul.
Rechtstreeks kwadrateren kan integeroverloop of oneindig opleveren. Zet daarom eerst om naar floats en bereken de norm r na delen door de grootste absolute component s. Vergelijk s met c/r, zonder het mogelijk te grote product s×r te vormen.
Ook terugschalen vraagt zorg bij gemengde groottes. frexp schrijft een getal als mantisse maal twee tot een gehele exponent: 12=0.75×2⁴. ldexp combineert die delen weer. Splits de schaalfactor en iedere component op deze manier; zo voorkom je dat een bruikbare kleine component al in een tussenstap nul wordt.
import numpy as np
def clip_globaal(gradients,max_norm):
arrays = {k:np.asarray(v,dtype=float)
for k,v in gradients.items()}
scale = max((float(np.max(np.abs(a)))
if a.size else 0.
for a in arrays.values()),
default=0.)
if scale == 0:
return {k:a.copy()
for k,a in arrays.items()}
length = math.sqrt(sum(
float(((a/scale)**2).sum())
for a in arrays.values()))
limit = max_norm/length
if scale <= limit:
return {k:a.copy()
for k,a in arrays.items()}
cm,ce = math.frexp(max_norm)
sm,se = math.frexp(scale)
result = {}
for k,a in arrays.items():
am,ae = np.frexp(a)
result[k] = np.array(np.ldexp(
am*(cm/sm/length),ae+ce-se))
return result
g = dict(a=np.array([3,4]),
b=np.array([12]))
r = clip_globaal(g,6.5)
print(r['a'].tolist(),r['b'].tolist())
# [1.5, 2.0] [6.0]
Reken de exponentstap na voor component 3. Daar zijn am=0.75 en ae=2. Bij grens 6.5 en schaal 12 wordt de nieuwe mantisse 0.75 en de nieuwe exponent 1: 0.75×2¹=1.5, zoals in het handvoorbeeld.
Maak een nieuwe dictionary met nieuwe NumPy-arrays, dezelfde namen/vormen en intacte invoer. Waarden en max_norm zijn eindig; max_norm is positief. Lege arrays en een lege dictionary mogen voorkomen. Clipping staat vóór de optimizerupdate; bij Adam is een begrensde gradiëntnorm geen rechtstreekse bovengrens voor de parameterstap.
023. Bewaar een verbetering en tel je geduld
Early stopping kiest de parameters op validatieverlies. Een score verbetert alleen als score < bestscore−min_delta. Min_delta is een absoluut verliesverschil. Bij verbetering bewaar je een diepe kopie en zet je de wachtteller op nul; anders tel je één erbij.
Neem scores [1,0.9,0.91,0.8,0.81,0.82] en patience=2. De verbetering op index 3 wist het eerdere wachten. Daarna komen twee tegenvallers: stop op index 5 en herstel de parameters van index 3.
import copy
def kies_stop(scores,states,patience,
min_delta=0):
bestscore = float('inf')
best,best_epoch,waiting = None,-1,0
stop_epoch = len(scores)-1
for epoch,(score,state) in enumerate(
zip(scores,states)):
improved=score<bestscore-min_delta
if best_epoch < 0 or improved:
bestscore = score
best = copy.deepcopy(state)
best_epoch,waiting = epoch,0
else:
waiting += 1
if waiting >= patience:
stop_epoch = epoch
break
return dict(best=best,
best_epoch=best_epoch,
stop_epoch=stop_epoch)
scores = [1,.9,.91,.8,.81,.82]
states = [{'w':[i]} for i in range(6)]
result = kies_stop(scores,states,2)
print(result)
# best={'w':[3]}, best_epoch=3,
# stop_epoch=5
Gebruik even lange, niet-lege reeksen, eindige scores en min_delta, gehele patience≥1 en min_delta≥0. De eerste score is altijd de startreferentie. Indices zijn nulgebaseerd. Zonder vroeg stoppen is stop_epoch de laatste index. Een score gelijk aan de verbetergrens telt als wachten. Verander scores en states niet; latere mutaties mogen de bewaarde beste kopie niet raken.
Kies patience en min_delta op validatiegegevens. Gebruik de eindtest pas ná deze keuzes. Een laatste epoch met slechter validatieverlies is geen reden om de eerder bewaarde betere versie weg te gooien.
03Wat neem je mee?
- Warmup start bij nul; cosine decay verbindt de piek met de eindwaarde.
- Globale clipping verkleint alle gradiëntarrays samen en laat kleine normen intact.
- Vroeg stoppen telt opeenvolgende tegenvallers en herstelt een diepe kopie van de beste validatieversie.
Hierna combineer je de bouwstenen in een echt cijferherkenningsproject.