1. Methodiek van backpropagatie
Backpropagatie is het fundamentele algoritme voor het trainen van neurale netwerken. Het werd voor het eerst geformaliseerd in de jaren tachtig en gepopulariseerd door Rumelhart, Hinton en Williams 1. Deze elegante toepassing van de kettingregel maakt het mogelijk om gradiënten efficiënt te berekenen in meerlaagse netwerken, waardoor deep learning praktisch uitvoerbaar wordt.
In de kern lost backpropagatie een fundamenteel probleem op: hoe berekenen we efficiënt de gradiënt van een verliesfunctie ten opzichte van elk gewicht in een netwerk dat mogelijk miljoenen parameters heeft? Het antwoord ligt in het systematisch terugvoeren van foutsignalen door het netwerk, waarbij tussenresultaten worden hergebruikt om overbodige berekeningen te vermijden.
1.1 Het leerprobleem
Beschouw een neuraal netwerk als een geparametriseerde functie \(f(x; \theta)\) die invoer \(x\) afbeeldt op uitvoer \(\hat{y}\). De parameters \(\theta\) omvatten alle gewichten en biases in alle lagen. Ons doel is parameters te vinden die een verliesfunctie minimaliseren:
waarbij:
- \(N\) het aantal trainingsvoorbeelden is
- \(L(y, \hat{y})\) het verlies voor één voorspelling is (bijvoorbeeld de gemiddelde kwadratische fout of kruisentropie)
- \(y_i\) het werkelijke label voor invoer \(x_i\) is
Om dit verlies met gradiëntafdaling te minimaliseren, moeten we het volgende berekenen:
Backpropagatie biedt een efficiënt algoritme om deze gradiënten te berekenen.
1.2 Netwerkarchitectuur
We bouwen ons inzicht op aan de hand van een meerlaags feedforwardnetwerk. Voor laag \(l\) (waarbij \(l = 1, 2, \ldots, L\)):
Preactivatie (gewogen som):
Activatie (niet-lineariteit):
waarbij:
- \(W^{(l)} \in \mathbb{R}^{n_l \times n_{l-1}}\) de gewichtenmatrix voor laag \(l\) is
- \(b^{(l)} \in \mathbb{R}^{n_l}\) de biasvector is
- \(a^{(l-1)}\) de activatie van de vorige laag is (met \(a^{(0)} = x\) als invoer)
- \(g^{(l)}(\cdot)\) de activatiefunctie is (bijvoorbeeld ReLU, sigmoïde of tanh)
- \(n_l\) het aantal neuronen in laag \(l\) is
De uiteindelijke uitvoer is \(\hat{y} = a^{(L)}\) en het verlies voor één voorbeeld is:
1.3 De voorwaartse doorgang
Voordat we fouten achterwaarts kunnen propageren, moeten we eerst de voorwaartse doorgang uitvoeren: de voorspellingen van het netwerk berekenen. Dit gaat als volgt:
- Initialiseren: Stel \(a^{(0)} = x\) in (de invoer)
- Voor elke laag \(l = 1, 2, \ldots, L\):
- Bereken \(z^{(l)} = W^{(l)} a^{(l-1)} + b^{(l)}\)
- Bereken \(a^{(l)} = g^{(l)}(z^{(l)})\)
- Uitvoer: \(\hat{y} = a^{(L)}\)
- Bereken het verlies: \(L = L(y, \hat{y})\)
Tijdens deze doorgang slaan we alle tussenwaarden op, zowel \(z^{(l)}\) als \(a^{(l)}\). Ze zijn essentieel voor de achterwaartse doorgang.
1.4 De achterwaartse doorgang: backpropagatie afleiden
Nu volgt het elegante gedeelte. We willen \(\frac{\partial L}{\partial W^{(l)}}\) en \(\frac{\partial L}{\partial b^{(l)}}\) voor elke laag \(l\) berekenen. Het belangrijkste inzicht is om vanuit de uitvoerlaag achterwaarts te werken en daarbij tussentijdse gradiënten te berekenen en op te slaan.
1.4.1 Fout van de uitvoerlaag
Voor de laatste laag \(L\) beginnen we met het berekenen van de verandering van het verlies ten opzichte van de preactivatie:
Volgens de kettingregel:
Hierbij staat \(\odot\) voor elementsgewijze vermenigvuldiging (het Hadamardproduct) en is \(g'^{(L)}\) de afgeleide van de activatiefunctie.
Voor veelgebruikte combinaties van verliesfunctie en activatiefunctie vereenvoudigt dit tot:
Gemiddelde kwadratische fout met lineaire uitvoer:
Kruisentropie met softmax-uitvoer:
(waarbij \(y\) een one-hot-gecodeerde vector is)
1.4.2 Fouten van verborgen lagen
Voor verborgen lagen (\(l < L\)) propageren we de fout achterwaarts met behulp van de kettingregel. De fout in laag \(l\) hangt af van de fout in laag \(l+1\):
Omdat \(z^{(l)}\) het verlies beïnvloedt via \(a^{(l)}\), dat op zijn beurt \(z^{(l+1)}\) beïnvloedt:
Uit vergelijking \(\ref{eq:pre_activation}\) weten we dat \(z^{(l+1)} = W^{(l+1)} a^{(l)} + b^{(l+1)}\), dus:
En uit vergelijking \(\ref{eq:activation}\):
Door deze uitdrukkingen te combineren, verkrijgen we de backpropagatievergelijking voor verborgen lagen:
Dit is de kern van backpropagatie: fouten uit laag \(l+1\) worden via de gewichten achterwaarts gepropageerd en gemoduleerd door de afgeleide van de activatiefunctie.
1.4.3 Gradiënten ten opzichte van parameters
Zodra we \(\delta^{(l)}\) voor elke laag kennen, is het berekenen van de gradiënten eenvoudig:
Gradiënt ten opzichte van de gewichten:
Gradiënt ten opzichte van de biases:
Deze formules volgen rechtstreeks uit de kettingregel, toegepast op vergelijkingen \(\ref{eq:pre_activation}\) en \(\ref{eq:activation}\).
1.5 Het volledige backpropagatiealgoritme
Als we alles samenvoegen, krijgen we het volledige algoritme voor één trainingsvoorbeeld:
1.5.1 Algoritme: backpropagatie
Invoer: Trainingsvoorbeeld \((x, y)\), netwerkparameters \(\{W^{(l)}, b^{(l)}\}_{l=1}^L\)
Uitvoer: Gradiënten \(\{\frac{\partial L}{\partial W^{(l)}}, \frac{\partial L}{\partial b^{(l)}}\}_{l=1}^L\)
Voorwaartse doorgang:
- Stel \(a^{(0)} = x\) in
- Voor \(l = 1\) tot \(L\):
- \(z^{(l)} = W^{(l)} a^{(l-1)} + b^{(l)}\)
- \(a^{(l)} = g^{(l)}(z^{(l)})\)
- Bereken het verlies: \(L = L(y, a^{(L)})\)
Achterwaartse doorgang:
- Bereken de uitvoerfout: \(\delta^{(L)} = \frac{\partial L}{\partial a^{(L)}} \odot g'^{(L)}(z^{(L)})\)
- Voor \(l = L-1\) aflopend tot \(1\):
- \(\delta^{(l)} = (W^{(l+1)})^T \delta^{(l+1)} \odot g'^{(l)}(z^{(l)})\)
- Voor \(l = 1\) tot \(L\):
- \(\frac{\partial L}{\partial W^{(l)}} = \delta^{(l)} (a^{(l-1)})^T\)
- \(\frac{\partial L}{\partial b^{(l)}} = \delta^{(l)}\)
1.6 Rekenefficiëntie
Backpropagatie is opmerkelijk efficiënt. Zonder dit algoritme zou een naïeve berekening van de gradiënten \(O(W^2)\) bewerkingen vereisen, waarbij \(W\) het totale aantal gewichten is. Backpropagatie verlaagt dit tot \(O(W)\) door tussenresultaten te hergebruiken.
Belangrijk inzicht: Elke tussentijdse gradiënt wordt precies één keer berekend en in alle daaropvolgende berekeningen hergebruikt. Dit is dynamisch programmeren toegepast op differentiëren.
Voor een netwerk met \(L\) lagen en \(n\) neuronen per laag:
- Voorwaartse doorgang: \(O(Ln^2)\) bewerkingen
- Achterwaartse doorgang: \(O(Ln^2)\) bewerkingen
- Totaal: \(O(Ln^2)\) — dezelfde orde als de voorwaartse doorgang!
1.7 Een gedetailleerd voorbeeld: een netwerk met twee lagen
We werken een concreet voorbeeld uit met een netwerk met twee lagen (één verborgen laag).
Architectuur:
- Invoerlaag: 2 neuronen (\(x \in \mathbb{R}^2\))
- Verborgen laag: 3 neuronen met sigmoïdeactivatie
- Uitvoerlaag: 1 neuron met sigmoïdeactivatie
- Verlies: gemiddelde kwadratische fout
Trainingsvoorbeeld: \(x = [0.5, 1.0]^T\), \(y = 0.8\)
Initiële gewichten en biases (willekeurig gekozen ter illustratie):
1.7.1 Voorwaartse doorgang
Laag 1 (verborgen):
(waarbij \(\sigma(z) = \frac{1}{1 + e^{-z}}\) de sigmoïdefunctie is)
Laag 2 (uitvoer):
Verlies:
1.7.2 Achterwaartse doorgang
Fout van de uitvoerlaag (\(\delta^{(2)}\)):
Voor MSE-verlies en sigmoïdeactivatie:
Fout van de verborgen laag (\(\delta^{(1)}\)):
Gradiënten:
Voor \(W^{(2)}\):
Voor \(b^{(2)}\):
Voor \(W^{(1)}\):
Voor \(b^{(1)}\):
1.8 Implementatie in Python
Hieronder staat een overzichtelijke, gevectoriseerde implementatie van backpropagatie:
import numpy as np
class NeuralNetwork:
"""
A simple feedforward neural network with backpropagation.
"""
def __init__(self, layer_sizes, activation='relu'):
"""
Initialize network with random weights.
Parameters:
- layer_sizes: list of layer sizes [input_size, hidden1, hidden2, ..., output_size]
- activation: 'relu', 'sigmoid', or 'tanh'
"""
self.layer_sizes = layer_sizes
self.num_layers = len(layer_sizes)
self.activation_name = activation
# Initialize weights and biases with He initialization for ReLU
self.weights = []
self.biases = []
for i in range(1, self.num_layers):
# He initialization: good for ReLU
w = np.random.randn(layer_sizes[i], layer_sizes[i-1]) * np.sqrt(2.0 / layer_sizes[i-1])
b = np.zeros((layer_sizes[i], 1))
self.weights.append(w)
self.biases.append(b)
def activation(self, z):
"""Apply activation function."""
if self.activation_name == 'relu':
return np.maximum(0, z)
elif self.activation_name == 'sigmoid':
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
elif self.activation_name == 'tanh':
return np.tanh(z)
def activation_derivative(self, z):
"""Compute derivative of activation function."""
if self.activation_name == 'relu':
return (z > 0).astype(float)
elif self.activation_name == 'sigmoid':
s = self.activation(z)
return s * (1 - s)
elif self.activation_name == 'tanh':
return 1 - np.tanh(z)**2
def forward(self, x):
"""
Forward pass through the network.
Parameters:
- x: input vector of shape (input_size, 1)
Returns:
- activations: list of activations for each layer
- z_values: list of pre-activation values for each layer
"""
activations = [x]
z_values = []
a = x
for i in range(self.num_layers - 1):
z = self.weights[i] @ a + self.biases[i]
z_values.append(z)
# Use linear activation for output layer, activation function for hidden layers
if i == self.num_layers - 2: # Output layer
a = z # Linear output for regression
else:
a = self.activation(z)
activations.append(a)
return activations, z_values
def backward(self, x, y, activations, z_values):
"""
Backward pass: compute gradients using backpropagation.
Parameters:
- x: input vector
- y: true output
- activations: list from forward pass
- z_values: list from forward pass
Returns:
- weight_gradients: list of gradients for weights
- bias_gradients: list of gradients for biases
"""
weight_gradients = [None] * (self.num_layers - 1)
bias_gradients = [None] * (self.num_layers - 1)
# Output layer error (assuming MSE loss and linear output)
delta = activations[-1] - y
# Backpropagate through layers
for l in range(self.num_layers - 2, -1, -1):
# Compute gradients for this layer
weight_gradients[l] = delta @ activations[l].T
bias_gradients[l] = delta
# Propagate error to previous layer (if not input layer)
if l > 0:
delta = (self.weights[l].T @ delta) * self.activation_derivative(z_values[l-1])
return weight_gradients, bias_gradients
def train_step(self, x, y, learning_rate=0.01):
"""
Perform one training step: forward pass, backward pass, update weights.
Parameters:
- x: input vector (column vector)
- y: true output (scalar or column vector)
- learning_rate: step size for gradient descent
Returns:
- loss: mean squared error for this example
"""
# Ensure inputs are column vectors
x = x.reshape(-1, 1)
y = np.array(y).reshape(-1, 1)
# Forward pass
activations, z_values = self.forward(x)
# Compute loss (MSE)
loss = 0.5 * np.sum((activations[-1] - y)**2)
# Backward pass
weight_grads, bias_grads = self.backward(x, y, activations, z_values)
# Update weights and biases
for i in range(self.num_layers - 1):
self.weights[i] -= learning_rate * weight_grads[i]
self.biases[i] -= learning_rate * bias_grads[i]
return loss
def predict(self, x):
"""Make a prediction for input x."""
x = x.reshape(-1, 1)
activations, _ = self.forward(x)
return activations[-1]
# Example: Train on XOR problem
if __name__ == "__main__":
# XOR dataset
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]).T # Shape: (2, 4)
y = np.array([[0], [1], [1], [0]]).T # Shape: (1, 4)
# Create network: 2 inputs, 4 hidden neurons, 1 output
nn = NeuralNetwork([2, 4, 1], activation='sigmoid')
# Training loop
epochs = 5000
print("Training on XOR problem...")
for epoch in range(epochs):
total_loss = 0
for i in range(4):
x_i = X[:, i:i+1]
y_i = y[:, i:i+1]
loss = nn.train_step(x_i, y_i, learning_rate=0.5)
total_loss += loss
if epoch % 1000 == 0:
print(f"Epoch {epoch:4d}, Loss: {total_loss:.6f}")
# Test predictions
print("\nPredictions after training:")
for i in range(4):
x_i = X[:, i]
pred = nn.predict(x_i)
print(f"Input: {x_i}, True: {y[0, i]:.1f}, Predicted: {pred[0, 0]:.4f}")
Uitvoer:
Training on XOR problem...
Epoch 0, Loss: 0.847213
Epoch 1000, Loss: 0.006891
Epoch 2000, Loss: 0.002456
Epoch 3000, Loss: 0.001339
Epoch 4000, Loss: 0.000889
Predictions after training:
Input: [0 0], True: 0.0, Predicted: 0.0187
Input: [0 1], True: 1.0, Predicted: 0.9795
Input: [1 0], True: 1.0, Predicted: 0.9801
Input: [1 1], True: 0.0, Predicted: 0.0213
1.9 Veelgebruikte activatiefuncties en hun afgeleiden
Backpropagatie vereist dat we \(g'(z)\) voor elke activatiefunctie berekenen. Hieronder staan de meest gebruikte functies:
| Activatiefunctie | Definitie | Afgeleide |
|---|---|---|
| ReLU | \(g(z) = \max(0, z)\) | \(g'(z) = \begin{cases} 1 & z > 0 \\ 0 & z \leq 0 \end{cases}\) |
| Sigmoïde | \(g(z) = \frac{1}{1 + e^{-z}}\) | \(g'(z) = g(z)(1 - g(z))\) |
| Tanh | \(g(z) = \tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}}\) | \(g'(z) = 1 - \tanh^2(z)\) |
| Leaky ReLU | \(g(z) = \max(\alpha z, z)\), waarbij \(\alpha \approx 0.01\) | \(g'(z) = \begin{cases} 1 & z > 0 \\ \alpha & z \leq 0 \end{cases}\) |
| Softmax | \(g(z)_i = \frac{e^{z_i}}{\sum_{j} e^{z_j}}\) | \(\frac{\partial g(z)_i}{\partial z_j} = g(z)_i(\delta_{ij} - g(z)_j)\) |
Veelgebruikte toepassingen:
- ReLU / Leaky ReLU: Standaardkeuze voor verborgen lagen; snelle berekening en vermindering van het probleem van verdwijnende gradiënten
- Sigmoïde / Tanh: Klassieke activatiefuncties; nuttig voor begrensde uitvoer of poorten in LSTM-netwerken
- Softmax: Uitvoer voor classificatie met meerdere klassen; produceert kansverdelingen
1.10 Verdwijnende en exploderende gradiënten
Een uitdaging bij het trainen van diepe netwerken is dat gradiënten kunnen verdwijnen of exploderen wanneer ze achterwaarts door veel lagen worden gepropageerd.
Verdwijnende gradiënten: Bij gebruik van sigmoïde- of tanh-activaties zijn de afgeleiden begrensd door 1. Wanneer we veel van deze termen in vergelijking \(\ref{eq:backprop_recursion}\) vermenigvuldigen, kan de gradiënt exponentieel afnemen:
Als \(\|g'^{(l)}\| < 1\) en \(\|W^{(l)}\| < 1\), ontvangen de eerste lagen zeer kleine gradiënten en leren ze erg langzaam.
Oplossingen: - ReLU-activaties gebruiken (de afgeleide is 0 of 1) - De gewichten op de juiste manier initialiseren (He, Xavier) - Batchnormalisatie - Residuele verbindingen (skip connections) - LSTM/GRU voor recurrente netwerken
Exploderende gradiënten: Het tegenovergestelde probleem treedt op wanneer gradiënten exponentieel groeien. Dit komt vaker voor bij recurrente netwerken.
Oplossingen: - Gradiënten afkappen (gradient clipping) - De gewichten zorgvuldig initialiseren - Lagere leersnelheden
1.11 Een perspectief vanuit de matrixrekening
Backpropagatie kan elegant worden uitgedrukt met behulp van matrixrekening. De sleutel is om bij te houden hoe het verlies verandert wanneer we elke parameter verstoren.
Voor een scalair verlies \(L\) en een matrixparameter \(W\) is de gradiënt:
Door de kettingregel systematisch toe te passen:
Omdat \(z^{(l)} = W^{(l)} a^{(l-1)} + b^{(l)}\):
Dit perspectief kan op natuurlijke wijze worden uitgebreid naar convolutielagen, recurrente lagen en andere architecturen.
1.12 Het perspectief van de rekengraaf
Moderne frameworks voor deep learning (PyTorch, TensorFlow) stellen berekeningen voor als gerichte acyclische grafen (DAG's). Elke knoop is een bewerking en de verbindingen geven de gegevensstroom weer.
Voorwaartse doorgang: Doorloop de graaf van invoer naar uitvoer en bereken de waarden Achterwaartse doorgang: Doorloop de graaf in omgekeerde richting en pas bij elke knoop de kettingregel toe
Deze abstractie maakt het volgende mogelijk:
- Automatisch differentiëren (autograd)
- Dynamische rekengrafen
- Eenvoudige implementatie van complexe architecturen
- Efficiënte parallellisatie op GPU's
Het beschreven backpropagatiealgoritme is precies wat deze frameworks automatisch uitvoeren wanneer je .backward() of tape.gradient() aanroept.
1.13 Praktische overwegingen
Gradiëntafdaling met minibatches: In plaats van gradiënten voor één voorbeeld te berekenen, nemen we doorgaans het gemiddelde over een minibatch:
Dit biedt: - Betere schattingen van de gradiënt (lagere variantie) - Efficiënte gevectoriseerde berekeningen - Een regulariserend effect door de ruis
Een leersnelheidsschema: Begin met hogere leersnelheden en verlaag ze na verloop van tijd: - Stapsgewijze afname: verlaag de leersnelheid met een factor om de \(N\) epochs - Exponentiële afname: \(\eta_t = \eta_0 e^{-kt}\) - Cosinusannealing: een geleidelijke afname volgens een cosinuscurve
Momentum en adaptieve methoden: Moderne optimalisatiealgoritmen breiden gewone gradiëntafdaling uit: - Momentum: Accumuleer gradiënten over de tijd om updates vloeiender te maken - Adam: Adaptieve leersnelheden per parameter, gebaseerd op momenten van de gradiënt - RMSprop: Schaal leersnelheden met een voortschrijdend gemiddelde van de grootte van de gradiënten
Regularisatie: Voorkom overfitting door straftermen toe te voegen: - L2-regularisatie (gewichtsverval): Voeg \(\frac{\lambda}{2}\|\theta\|^2\) toe aan het verlies - Dropout: Zet tijdens het trainen willekeurig neuronen op nul - Batchnormalisatie: Normaliseer activaties binnen elke minibatch
1.14 Historische context en impact
Backpropagatie bracht een revolutie teweeg in machine learning toen het in de jaren tachtig werd gepopulariseerd 1. Hoewel het kernidee (het toepassen van de kettingregel) ouder is, toonde het baanbrekende werk van Rumelhart, Hinton en Williams de praktische effectiviteit ervan aan voor het trainen van meerlaagse netwerken 23.
Belangrijke mijlpalen: - Jaren zestig en zeventig: Vroeg onderzoek naar automatisch differentiëren en de achterwaartse modus daarvan - 1986: Rumelhart, Hinton en Williams 1 populariseren backpropagatie voor neurale netwerken - Jaren negentig: Uitdagingen met diepe netwerken en verdwijnende gradiënten - 2006: Hintons deep belief networks wekken opnieuw belangstelling - 2012: AlexNet gebruikt backpropagatie en GPU's om ImageNet te winnen en luidt daarmee het tijdperk van deep learning in 4 - Vandaag: Backpropagatie wordt gebruikt om netwerken met miljarden parameters te trainen (GPT, BERT enz.)
De elegantie van het algoritme ligt in zijn eenvoud: het is niets meer dan de kettingregel, systematisch toegepast. Toch maakt dit eenvoudige idee het mogelijk om de ongelooflijk complexe modellen te trainen waarop moderne AI berust 4.
1.15 Voorbij feedforwardnetwerken
Hoewel we ons hebben gericht op feedforwardnetwerken, kan backpropagatie ook op andere architecturen worden toegepast:
Convolutionele neurale netwerken (CNN's): Backpropagatie door convolutiebewerkingen met behulp van getransponeerde convoluties
Recurrente neurale netwerken (RNN's): Backpropagatie door de tijd (BPTT), waarbij het netwerk over tijdstappen wordt uitgerold
Transformers: Backpropagatie door zelfaandachtsmechanismen, laagnormalisatie en residuele verbindingen
Graafneurale netwerken: Het doorgeven en aggregeren van berichten met backpropagatie op graafstructuren
Het fundamentele principe blijft hetzelfde: voer de voorwaartse doorgang uit en propageer vervolgens de gradiënten achterwaarts met behulp van de kettingregel.
1.16 Samenvatting
Backpropagatie is het werkpaard van deep learning en maakt efficiënte gradiëntberekening mogelijk via:
- Voorwaartse doorgang: Bereken de uitvoer en sla tussenwaarden op
- Achterwaartse doorgang: Bereken fouten recursief met \(\delta^{(l)} = (W^{(l+1)})^T \delta^{(l+1)} \odot g'^{(l)}(z^{(l)})\)
- Gradiëntberekening: Gebruik \(\frac{\partial L}{\partial W^{(l)}} = \delta^{(l)} (a^{(l-1)})^T\)
- Parameterupdate: Pas gradiëntafdaling of varianten daarvan toe
De complexiteit van het algoritme, \(O(W)\) (lineair in het aantal gewichten), maakt het trainen van diepe netwerken uitvoerbaar. In combinatie met krachtige hardware (GPU's, TPU's) en moderne optimalisatiealgoritmen heeft backpropagatie de revolutie van deep learning mogelijk gemaakt 23.
-
David E. Rumelhart, Geoffrey E. Hinton, and Ronald J. Williams. Learning representations by back-propagating errors. Nature, 323(6088):533–536, 1986. doi:10.1038/323533a0. ↩↩↩
-
Ian Goodfellow, Yoshua Bengio, and Aaron Courville. Deep Learning. MIT Press, 2016. URL: https://www.deeplearningbook.org/. ↩↩
-
Michael A. Nielsen. Neural Networks and Deep Learning. Determination Press, 2015. URL: http://neuralnetworksanddeeplearning.com/. ↩↩
-
Yann LeCun, Yoshua Bengio, and Geoffrey Hinton. Deep learning. Nature, 521(7553):436–444, 2015. doi:10.1038/nature14539. ↩↩