Werkplaats / De werkplaats
In jouw eigen tempo
← Neurale netwerken
MODULE 05 · LES 17

Stuur het leertempo en bewaar de beste stap

Bouw warmup met cosine decay, begrens grote gradiënten en stop op validatieverlies met herstel van de beste parameters.

105 min3 checkpoints
DIT GA JE ONTDEKKEN
  • 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?

EERST OPBOUWEN, DAN VERFIJNENBekijk je leersnelheid vóór het trainen

100 intervallen, stapindices 0 tot en met 100. Warmup start bij nul; zonder warmup begint het schema direct bij de piek.

00.050.10.150.20255075100
Stap0Leersnelheid0.0000Eindwaarde0.010

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.

t=step−warmuptotal−warmupη=F+(P−F)1+cos(πt)2

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.

01
CHECKPOINTLeg de grenzen van warmup en cosine decay vast

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.

n=∑igi2als n > c: gnieuw=cng

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.

02
CHECKPOINTBegrens de gezamenlijke norm

3. 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.

03
CHECKPOINTStop na geduld en herstel de beste versie

Wat 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.

DE VOLGENDE STAPBouw en train een MLP op MNIST
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
Leg de grenzen van warmup en cosine decay vast

Schrijf leersnelheid(step,total,warmup,peak,floor=0), met 0≤warmup<total en 0≤step≤total.

Een hint

Splits de warmup-tak en de cosine-tak; behandel warmup=0 zonder deling door nul.

02
Begrens de gezamenlijke norm

Schrijf clip_globaal(gradients,max_norm) als nieuwe dictionary NumPy-arrays met behoud van invoer en gezamenlijke richting, ook bij grote of gemengde eindige waarden.

Een hint

Bereken de norm na max-abs-schaling. Splits max_norm, schaal en componenten met frexp; combineer exponenten met ldexp. Kleine normen blijven intact.

03
Stop na geduld en herstel de beste versie

Schrijf kies_stop(scores,states,patience,min_delta=0) als dictionary met best,best_epoch,stop_epoch voor even lange niet-lege reeksen.

Een hint

Loop in volgorde, houd bestscore en wachtteller bij en gebruik copy.deepcopy bij een verbetering. Als je niet vroeg stopt, is stop_epoch de laatste index.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np
import math
import copy

def leersnelheid(step,total,warmup,peak,floor=0):
    if not (0<=warmup<total and 0<=step<=total and 0<=floor<=peak):
        raise ValueError("Ongeldig schema")
    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

def clip_globaal(gradients,max_norm):
    arrays={name:np.asarray(g,dtype=float) for name,g in gradients.items()}
    scale=max((float(np.max(np.abs(a))) if a.size else 0.0 for a in arrays.values()),default=0.0)
    if scale==0:
        return {name:a.copy() for name,a in arrays.items()}
    length=math.sqrt(sum(float(np.sum((a/scale)**2)) for a in arrays.values()))
    limit=max_norm/length
    if scale<=limit:
        return {name:a.copy() for name,a in arrays.items()}
    cm,ce=math.frexp(max_norm)
    sm,se=math.frexp(scale)
    result={}
    for name,a in arrays.items():
        am,ae=np.frexp(a)
        result[name]=np.array(np.ldexp(am*(cm/sm/length),ae+ce-se),copy=True)
    return result

def kies_stop(scores,states,patience,min_delta=0):
    bestscore=float("inf"); best=None; best_epoch=-1; waiting=0
    stop_epoch=len(scores)-1
    for epoch,(score,state) in enumerate(zip(scores,states)):
        if best_epoch<0 or score<bestscore-min_delta:
            bestscore=score; best=copy.deepcopy(state); best_epoch=epoch; waiting=0
        else:
            waiting+=1
        if waiting>=patience:
            stop_epoch=epoch
            break
    return {"best":best,"best_epoch":best_epoch,"stop_epoch":stop_epoch}

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