Hoofdstuk 11 · Het trainen verfijnen
Mini-batches
We weten hoe een netwerk leert: bereken de fout, bereken de gradient, zet een stapje bergaf. Tot nu toe deden we dat telkens over álle voorbeelden tegelijk. In dit hoofdstuk verfijnen we precies dat ene punt — en het blijkt een verrassend groot verschil te maken voor hoe snel een netwerk leert.
Eén stap per ronde is traag
In onze Optelnet en RekenNet werkte het trainen zo: per epoch liepen we alle voorbeelden langs, telden alle gradiënten op, en zetten daarna één stap bergaf. Dat heet volledige-batch gradient descent. Het is netjes — je stap wijst exact de gemiddelde richting van álle data — maar het is ook traag: voor elke stap moet je eerst de hele dataset doorrekenen.
Bij 25 sommen valt dat niet op. Maar stel je een dataset voor van een miljoen foto's. Dan zou je een miljoen beelden moeten verwerken om één enkel stapje te kunnen zetten. Zo kom je nooit vooruit. Er moet een snellere manier zijn.
Van alles tegelijk naar één tegelijk
De truc: je hoeft niet álle data te zien voordat je een stap zet. Een deel ervan geeft al een redelijke schatting van de juiste richting. Dat geeft drie varianten, van traag-en-precies tot snel-en-ruig:
- Volledige-batch — alle voorbeelden, dan één stap. De richting klopt perfect, maar je zet weinig stappen.
- Stochastisch (SGD) — één voorbeeld, meteen een stap. Je zet razendveel stappen, maar elke stap is een ruwe gok op basis van dat ene voorbeeld — het pad slingert.
- Mini-batch — een klein groepje voorbeelden (bijvoorbeeld 32), dan een stap. De gulden middenweg: stappen die nauwkeurig genoeg zijn, maar waarvan je er veel kunt zetten. Dit is wat in de praktijk vrijwel altijd gebruikt wordt.
Hier hoort een nieuw woord bij. Een epoch is nog altijd één keer de hele dataset doorlopen. Maar binnen één epoch zet je nu meerdere stappen — één per batch. Zo'n losse stap heet een iteratie. Met 25 voorbeelden en batches van 5 doe je dus 5 iteraties per epoch, waar volledige-batch er maar 1 doet.
Sneller bij hetzelfde aantal epochs
Onderstaande figuur traint exact ons Optelnet drie keer met dezelfde startgewichten, alleen met een andere batchgrootte. Uitgezet staat de fout tegen het aantal epochs.
Het verschil is dramatisch. Na 300 epochs zit volledige-batch nog rond een fout van 1,4 en haalt het amper twee derde van de sommen juist, terwijl mini-batch en stochastisch allang foutloos zijn. De reden is eenvoudig: in dezelfde 300 epochs heeft mini-batch 5× zoveel stappen gezet, en stochastisch zelfs 25× zoveel. Veel kleine stappen brengen je sneller naar beneden dan weinig grote.
De keerzijde zie je aan de blauwe curve: elke stochastische stap berust op één enkel voorbeeld en is dus een ruwe schatting. Het pad daalt razendsnel maar zou van dichtbij grillig blijken — het stuitert wat rond het minimum in plaats van er rustig in te zakken. Mini-batch (oranje) vangt het beste van twee werelden: snel én redelijk stabiel.
De trainingslus ombouwen
Het mooie is dat de wiskunde — voorwaarts, verlies, achterwaarts — geen letter verandert. Alleen de trainingslus eromheen wordt anders. In plaats van één keer per epoch over alles, schudden we de data, knippen ze in batches, en sturen we na elke batch bij. Hier is die nieuwe lus, eerst in Java:
int[] order = new int[n];
for (int i = 0; i < n; i++) order[i] = i;
for (int epoch = 1; epoch <= EPOCHS; epoch++) {
shuffle(order); // 1) elke epoch opnieuw schudden
for (int start = 0; start < n; start += BATCH_SIZE) { // 2) in batches doorlopen
int end = Math.min(start + BATCH_SIZE, n);
int[] batch = Arrays.copyOfRange(order, start, end);
trainOnBatch(batch, X, T); // 3) na elke batch bijsturen
}
}En precies dezelfde structuur in Python:
order = list(range(n))
for epoch in range(1, EPOCHS + 1):
rng.shuffle(order) # 1) elke epoch opnieuw schudden
for start in range(0, n, BATCH_SIZE): # 2) in batches doorlopen
batch = order[start:start + BATCH_SIZE]
train_on_batch(batch) # 3) na elke batch bijsturenHet schudden in stap 1 is belangrijk: zou je de voorbeelden altijd in dezelfde volgorde aanbieden, dan zou het netwerk de toevallige rangschikking kunnen meeleren. Door elke epoch te herschudden blijven de batches telkens anders samengesteld.
De functie trainOnBatch bevat exact de voorwaartse en achterwaartse doorgang die je al kent, maar dan over alleen de voorbeelden in die ene batch. Eén klein maar cruciaal detail bij het bijsturen: we delen door de grootte van de batch, niet meer door het totale aantal voorbeelden — scale = LEARNING_RATE / len(batch). Zo blijft de stapgrootte vergelijkbaar, ongeacht hoe groot je batches kiest.
Als we dit draaien, leert hetzelfde netje nu in een paar honderd epochs wat eerder duizenden epochs kostte:
De uitvoer hieronder is die van de Python-versie, ingekort tot de sleutelmomenten; de Java-versie geeft door zijn eigen toevalsgenerator nét andere tussenstanden, met dezelfde eindstand.
epoch 1 verlies 2.1912 juistheid 16% epoch 50 verlies 1.5537 juistheid 52% epoch 100 verlies 0.6796 juistheid 100% epoch 200 verlies 0.0961 juistheid 100% epoch 400 verlies 0.0212 juistheid 100% 5 voorbeelden per batch -> 5 bijstuurstappen per epoch (volledige-batch zou er 1 doen) Resultaat op alle 25 combinaties: 25/25 juist
De volledige programma's AddNetMiniBatch.java en AddNetMiniBatch.py (hieronder te downloaden) zijn regel voor regel hetzelfde als de Optelnet uit hoofdstuk 1-4, op de trainingslus na — vergelijk ze gerust naast elkaar.
Hoe groot maak je een batch?
De batchgrootte is een knop die je zelf kiest, met een herkenbare afweging:
- Kleine batches (bv. 8 of 16) — veel, snelle stappen en wat ruis, die in de praktijk zelfs licht tegen overfitting blijkt te helpen. Maar de schattingen zijn grilliger.
- Grote batches (bv. 256 of meer) — stabielere, nauwkeurigere stappen, en op een GPU efficiënter te berekenen. Maar je zet minder stappen en hebt meer geheugen nodig.
In de praktijk liggen waarden tussen 32 en 256 het meest voor de hand, vaak afgestemd op wat er in het geheugen van de grafische kaart past. Er is dus geen one-size-fits-all — het is, net als de leersnelheid, een instelling om mee te spelen.
Hiermee is de gereedschapskist voor onze kleine netwerken compleet. Tijd voor de grote sprong: in Deel II maken we een écht taalmodel open — geen 329 parameters, maar 494 miljoen — en ontdekken we dat daarbinnen precies dezelfde bouwstenen zitten.