The network of 05-02 has 497 random parameters and says "0.55" to every order. This lesson explains how those numbers go from random to useful, that is, how a network learns. We announced the idea when closing 03-04: learning is optimising, and when the loss function is smooth there is no need to probe neighbours at random as in hill climbing, because we can compute the slope and walk down it. We will see the loss as a landscape, the derivative as a slope, gradient descent step by step with a one-variable example worked by hand, the role of the learning rate, the chain rule that lets us distribute the error backwards through the layers (backpropagation), the batch and mini-batch variants, the optimisers (SGD with momentum, Adam) and, at last, the network-specific regularisation techniques we left pending in 04-06: early stopping, dropout, data augmentation and batch normalisation. In code: a gradient descent from scratch in numpy for one neuron and the complete PyTorch training loop for the NovaMarket MLP, evaluated with AUC against the logistic regression of 04-05. It matters because this algorithm is the one that trains everything, from our 497-parameter network to the large language models of 05-05.
Contents
- The loss as a landscape: from hill climbing to a compass
- The derivative as a slope and the intuition of the gradient
- Gradient descent step by step: a one-variable example by hand
- The learning rate: too large, too small
- Chain rule and backpropagation
- Batches, mini-batches, epochs and optimisers (SGD, momentum, Adam)
- Network-specific regularisation: early stopping, dropout, data augmentation and batch normalisation
- Code (a): gradient descent from scratch in numpy
- Code (b): full training of the NovaMarket MLP in PyTorch
- Common Mistakes and Tips
- Exercises
- Conclusion
- The loss as a landscape: from hill climbing to a compass
Remember 04-01: the loss function measures, with a single number, how wrong the model is on the training data with its current parameters. For the sigmoid neuron with binary cross-entropy (05-02), the loss depends on the weights and the bias: change the weights and the loss changes. Picture a landscape in which every point on the ground is a combination of parameters and the height is the loss: training is walking down to the lowest valley. With 2 parameters the landscape can be drawn; with 497 it cannot, but the geometry is the same.
In 03-04 we went downhill "blind": hill climbing tried random neighbours and moved if they improved, and simulated annealing sometimes accepted getting worse. That worked for delivery routes, where there is no defined slope (swapping two stops is not "a little to the left"). With continuous parameters and a smooth loss we have something better: a compass that points downhill, the gradient. No need to try neighbours: you compute the direction of steepest descent and take a step. Repeated thousands of times, that step is gradient descent.
- The derivative as a slope and the intuition of the gradient
The derivative of a function at a point is its slope there: how much the function rises per unit you move forward. If f(w) = (w − 3)² + 1 (a parabola with its minimum at w = 3), its derivative is f'(w) = 2·(w − 3). At w = 0 it is −6 (the function drops steeply towards the right); at w = 2.5 it is −1 (a gentle drop); at w = 3 it is 0 (flat: the minimum); at w = 5 it is +4 (rising). You do not need to know how to compute derivatives for this course: the idea of "slope at the point" is enough, together with knowing that PyTorch computes them for you (section 5).
With several parameters, the gradient is the vector of partial derivatives, one per parameter: the slope of the loss "if I move only w₁", "if I move only w₂", and so on. It points in the direction in which the loss rises fastest; its opposite, downhill. The gradient descent rule for each parameter θ is:
θ ← θ − η · ∂L/∂θ
that is, "subtract from each parameter its slope multiplied by the learning rate η". If the slope is negative (the loss decreases as θ grows), the subtraction increases it; if it is positive, it decreases it. Compare it with the perceptron rule of 05-01, w ← w + η·e·x: it is the same shape, and in fact we will see that the perceptron rule is a special case.
- Gradient descent step by step: a one-variable example by hand
Let us minimise f(w) = (w − 3)² + 1 from w = 0 with η = 0.1. In each iteration: we compute the slope 2(w − 3), and update w ← w − 0.1 · slope.
| Iteration | Current w |
f(w) |
Slope 2(w − 3) |
Step −0.1 · slope |
New w |
|---|---|---|---|---|---|
| 1 | 0 | 10 | −6 | +0.6 | 0.6 |
| 2 | 0.6 | 6.76 | −4.8 | +0.48 | 1.08 |
| 3 | 1.08 | 4.686 | −3.84 | +0.384 | 1.464 |
| 4 | 1.464 | 3.359 | −3.072 | +0.307 | 1.771 |
| 5 | 1.771 | 2.510 | −2.458 | +0.246 | 2.017 |
| 6 | 2.017 | 1.966 | −1.966 | +0.197 | 2.214 |
Three things visible in the table that hold for any network: (1) the loss decreases at every step (10 → 6.76 → 4.69 → ...); (2) the steps are large far from the minimum and small near it, because the slope softens as we approach the valley, without anyone changing η; (3) it never reaches the minimum (w = 3) exactly but keeps getting closer indefinitely: in practice you stop when the improvement is negligible or, better, when validation says so (section 7). In a network the only difference is that instead of one slope there are 497 (one per parameter) and that the function is not a parabola but a landscape with many valleys; the algorithm walks down into the valley it falls into, which may not be the deepest one, exactly the local optima problem of 03-04.
- The learning rate: too large, too small
η is the most important hyperparameter of training. With the same parabola and the same starting point:
η |
w after 1, 2, 3, 4, 5 iterations |
Behaviour |
|---|---|---|
| 0.01 | 0.06; 0.119; 0.176; 0.233; 0.288 | Too small: it goes down, but after 5 steps it has barely covered 10 % of the way; it would take hundreds |
| 0.1 | 0.6; 1.08; 1.46; 1.77; 2.02 | Adequate: converges smoothly |
| 0.5 | 3.0; 3.0; 3.0; 3.0; 3.0 | Perfect for this parabola (it arrives in one step); a coincidence of the quadratic shape, not generalisable |
| 1.1 | 6.6; −1.32; 8.18; −3.22; 10.47 | Too large: it jumps from one side of the valley to the other, further each time; the loss rises (10 → 14 → 20 → 28 → 40) and it diverges |
If you plotted the loss per iteration: with a small η, a curve that drops slowly and flattens before arriving; with an adequate η, a fast drop that stabilises; with a large η, oscillations or a curve that rises and blows up (in PyTorch you get nan). There is no universal value: it depends on the scale of the data (that is why we standardise) and on the architecture. Typical values with Adam (section 6): between 0.0001 and 0.01. In practice it is tuned with validation (04-06), and it is often reduced during training (a learning rate schedule): large steps at the beginning, fine ones at the end, the same intuition as the temperature in simulated annealing.
- Chain rule and backpropagation
To apply θ ← θ − η·∂L/∂θ you need the slope of the loss with respect to each weight, and in a network the weight of the first layer affects the loss through a long chain of operations. The tool is the chain rule: the slope of a composition of functions is the product of the slopes of each link. If L depends on p, p on z and z on w, then ∂L/∂w = (∂L/∂p) · (∂p/∂z) · (∂z/∂w). Intuition: if z rises 2 units per unit of w, p rises 0.25 per unit of z and L drops 3 per unit of p, then L drops 3 × 0.25 × 2 = 1.5 per unit of w.
Let us see it in the computation graph of a sigmoid neuron with BCE loss (our logistic regression):
flowchart LR
x["x (inputs)"] --> Z["z = w·x + b"]
w["w, b"] --> Z
Z --> P["p = σ(z)"]
P --> L["L = −[y·log p + (1−y)·log(1−p)]"]
y["y (label)"] --> L
L -. "∂L/∂p" .-> P
P -. "∂p/∂z = p(1−p)" .-> Z
Z -. "∂z/∂w = x, ∂z/∂b = 1" .-> w
The solid arrows are the forward pass (computing the prediction and the loss); the dashed ones, the backward pass: starting from the loss, each node receives the accumulated slope, multiplies it by its local slope and passes it on to its inputs. For the sigmoid + BCE pair the first two links simplify beautifully: ∂L/∂z = p − y (the predicted probability minus the label). Therefore ∂L/∂w = (p − y)·x and ∂L/∂b = p − y. Compare with the perceptron rule: e·x with e = y − ŷ. It is the same rule, with the probability in place of the step, and now we know where it comes from: it is the gradient of the loss.
Numerical example: order x = (0.5, 1.0, 1) (standardised amount, days and new customer), label y = 1 (it was returned), weights w = (0.4, 0.3, 0.9), b = −2.
- Forward:
z = 0.2 + 0.3 + 0.9 − 2 = −0.6;p = σ(−0.6) = 0.354; lossL = −log(0.354) = 1.037. The network gives 35 % to something that happened: bad. - Backward:
∂L/∂z = p − y = −0.646;∂L/∂w = −0.646 · x = (−0.323, −0.646, −0.646);∂L/∂b = −0.646. - Step with
η = 0.5:w ← (0.4, 0.3, 0.9) − 0.5·(−0.323, −0.646, −0.646) = (0.561, 0.623, 1.223);b ← −2 + 0.323 = −1.677. - Check: now
z = 0.449,p = 0.61,L = 0.494. The loss has dropped from 1.037 to 0.494 for this order. With all the orders, the gradient used is the average of the individual gradients.
In a network with hidden layers the chain is longer but identical in spirit: the slope of the loss with respect to the output of the last layer is propagated to the second-to-last one by multiplying by the weights and by the slope of the activation (1 or 0 for ReLU; ≤ 0.25 for the sigmoid, hence the vanishing of 05-02), and so on down to the first. That algorithm, which reuses the backward computations layer by layer instead of redoing them for each weight, is backpropagation, popularised in 1986 by Rumelhart, Hinton and Williams (01-01), and it is what made it possible to train the hidden layers that solved XOR. In PyTorch you will never write it: every tensor remembers the operations that produced it (the computation graph), and L.backward() walks that graph backwards and leaves in p.grad the slope of each parameter. This is automatic differentiation, and it is the reason deep learning frameworks exist (07-03).
- Batches, mini-batches, epochs and optimisers (SGD, momentum, Adam)
Over how many examples do we compute the gradient before taking a step?
| Variant | Examples per step | Steps per epoch (1,799 training orders) | Advantages | Drawbacks |
|---|---|---|---|---|
| Batch | All | 1 | Exact gradient, smooth descent | Slow per step; does not fit in memory with millions of images |
| Stochastic (pure SGD) | 1 | 1,799 | Cheap per step; the noise helps escape poor local optima | Very noisy; does not exploit GPU parallelism |
| Mini-batches | 32-512 (here 64) | 29 | The balance: reasonable gradient, GPU-efficient, some useful noise | One more hyperparameter (the batch size) |
An epoch is a full pass over the training data (as in the perceptron). With mini-batches of 64 and 1,799 orders, each epoch is 29 gradient steps; 100 epochs, 2,900 steps. The data are shuffled at the start of each epoch so that the batches change.
The optimiser is the rule that turns gradient into update. The basic one is that of section 2 (SGD). Two almost universal improvements:
- Momentum: instead of moving only according to the current gradient, a "velocity" is accumulated (a moving average of the recent gradients). Like a rolling ball: it crosses small bumps, accelerates on long slopes and damps oscillations in narrow valleys.
torch.optim.SGD(..., momentum=0.9). - Adam (adaptive moment estimation): momentum plus a learning rate adapted to each parameter (those receiving large gradients are slowed down, those receiving small gradients are sped up). It is robust to the choice of
ηand is the default optimiser to start with.torch.optim.Adam(..., lr=0.001).
In our MLP, the validation loss after 1, 5, 10, 20 and 30 epochs was: SGD (η = 0.01) 0.72 → 0.60 → 0.53 → 0.47 → 0.45; SGD with momentum 0.55 → 0.45 → 0.42 → 0.34 → 0.33; Adam (η = 0.001) 0.73 → 0.48 → 0.39 → 0.34 → 0.33. All three go to the same place; momentum and Adam get there much sooner.
- Network-specific regularisation: early stopping, dropout, data augmentation and batch normalisation
A network with hundreds of parameters for every few examples overfits easily (04-06). Besides L2 regularisation (here called weight decay, an argument of the optimiser), deep learning adds four techniques of its own:
| Technique | What it does | Why it works | When |
|---|---|---|---|
| Early stopping | Evaluate the loss on a validation set at the end of each epoch and stop when it has gone k epochs without improving (patience), restoring the weights of the best epoch |
At first the network learns what is general; with more epochs it starts memorising the training noise and validation gets worse | Always. It is free and replaces "choosing the number of epochs" |
| Dropout | During training, at each step, randomly switch off a fraction p (0.1-0.5) of the neurons of a layer; at prediction time all are used |
No neuron can rely on a specific other one being there; the network learns redundant, robust features, like an ensemble of many sub-networks | Large dense layers; in our small MLP it helps little |
| Data augmentation | Create new examples by transforming existing ones: rotate, crop or change the brightness of photos; substitute synonyms in text | More "free" examples that teach the right invariances (a damaged package is still damaged even if the photo is rotated) | Images and audio above all (05-04) |
| Batch normalisation (batch norm) | Standardise the output of each layer with the mean and standard deviation of the mini-batch (plus two learned parameters) | Keeps activations in a healthy range layer by layer; allows larger learning rates and acts as a mild regulariser | Deep networks, especially convolutional ones |
This closes what 04-06 left pending. In the code of section 9 we will use early stopping and dropout; data augmentation and batch normalisation appear in 05-04.
- Code (a): gradient descent from scratch in numpy
We train a sigmoid neuron (logistic regression) on 3,000 NovaMarket orders with three columns, writing the gradient by hand as in section 5:
import numpy as np
from novamarket_ml import generate_orders_ml
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def bce_loss(p, y):
eps = 1e-9 # avoids log(0)
return -np.mean(y * np.log(p + eps) + (1 - y) * np.log(1 - p + eps))
orders = generate_orders_ml(3000, 42)
cols = ["amount", "delivery_days", "new_customer"]
X = orders[cols].values.astype(float)
y = orders["returned"].values.astype(float)
X = (X - X.mean(axis=0)) / X.std(axis=0) # standardise (essential)
w = np.zeros(3); b = 0.0 # starting point
rate = 0.5
print(f"{'epoch':>5} {'loss':>8} {'w_amount':>10} {'w_days':>8} {'w_new':>8} {'b':>7}")
for epoch in range(1, 201):
z = X @ w + b # 1. forward: weighted sum of the 3,000 orders
p = sigmoid(z) # probabilities
grad_z = p - y # 2. backward: dL/dz = p - y for each order
grad_w = X.T @ grad_z / len(y) # dL/dw = average of (p - y) * x
grad_b = grad_z.mean() # dL/db = average of (p - y)
w -= rate * grad_w # 3. step downhill
b -= rate * grad_b
if epoch in (1, 2, 3, 5, 10, 20, 50, 100, 200):
L = bce_loss(sigmoid(X @ w + b), y)
print(f"{epoch:5d} {L:8.4f} {w[0]:10.3f} {w[1]:8.3f} {w[2]:8.3f} {b:7.3f}")Output:
epoch loss w_amount w_days w_new b
1 0.6250 0.064 0.031 0.056 -0.168
2 0.5727 0.120 0.058 0.104 -0.315
3 0.5322 0.170 0.082 0.147 -0.444
5 0.4753 0.252 0.124 0.220 -0.659
10 0.4040 0.399 0.202 0.351 -1.031
20 0.3568 0.577 0.311 0.516 -1.436
50 0.3310 0.811 0.490 0.751 -1.900
100 0.3266 0.934 0.603 0.880 -2.132
200 0.3261 0.987 0.653 0.936 -2.233Reading: with w = 0 the initial loss is ln 2 = 0.693 (the network says 0.5 to everything); in 10 epochs it has dropped to 0.40 and then flattens at 0.326: we have reached the bottom of the valley (this loss is convex, so it is the global minimum). The weights converge to (0.99, 0.65, 0.94) and b = −2.23; LogisticRegression without regularisation on the same data gives (0.996, 0.661, 0.945) and −2.248: we have reproduced fit in twenty lines. This is batch descent (all 3,000 orders at every step); with rate = 0.01 after 200 epochs the loss was still at 0.51 (too slow), and with rate = 20 it oscillates between 0.47 and 0.66 without converging (too large), just as section 4 predicts.
- Code (b): full training of the NovaMarket MLP in PyTorch
Now the network 21 → 16 → 8 → 1 of 05-02, with everything we have learned: mini-batches, Adam, dropout, early stopping with a validation set and final evaluation with AUC on the same test set as 04-05.
9.1 Data: training, validation and test
import numpy as np, torch, torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from novamarket_ml import generate_orders_ml, dirty_orders, prepare_orders, build_preprocessing
X, y = prepare_orders(dirty_orders(generate_orders_ml(3000, 42), 42))
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y) # test: as in 04-05
Xtr2, Xval, ytr2, yval = train_test_split(Xtr, ytr, test_size=0.2, random_state=42, stratify=ytr) # validation
prep = build_preprocessing().fit(Xtr2) # the preprocessing is fitted ONLY on training data
def to_tensor(Xd, yd):
return (torch.tensor(prep.transform(Xd), dtype=torch.float32),
torch.tensor(yd.values, dtype=torch.float32).unsqueeze(1)) # y as a column (n x 1)
Xtr_t, ytr_t = to_tensor(Xtr2, ytr2)
Xval_t, yval_t = to_tensor(Xval, yval)
Xte_t, yte_t = to_tensor(Xte, yte)
print(Xtr_t.shape, Xval_t.shape, Xte_t.shape) # torch.Size([1799, 21]) torch.Size([450, 21]) torch.Size([750, 21])The test set is the same 750 orders as in 04-05 (same seed), so the AUC will be comparable with the 0.844 of logistic regression. The validation set (450 orders) serves for early stopping; the test set is looked at only once, at the end (04-06).
9.2 Network, loss, optimiser and loop
def build_net(dropout=0.2):
return nn.Sequential(
nn.Linear(21, 16), nn.ReLU(), nn.Dropout(dropout),
nn.Linear(16, 8), nn.ReLU(), nn.Dropout(dropout),
nn.Linear(8, 1)) # NO sigmoid: BCEWithLogitsLoss provides it
def evaluate(net, X_t, y_t, loss_fn):
net.eval() # evaluation mode: disables dropout
with torch.no_grad(): # no computation graph: faster
logits = net(X_t)
L = loss_fn(logits, y_t).item()
prob = torch.sigmoid(logits).numpy().ravel() # now yes, sigmoid to read probabilities
return L, roc_auc_score(y_t.numpy().ravel(), prob)
def train(net, epochs=300, rate=0.001, batch=64, patience=15, seed=0):
torch.manual_seed(seed)
loader = DataLoader(TensorDataset(Xtr_t, ytr_t), batch_size=batch, shuffle=True) # shuffled mini-batches
loss_fn = nn.BCEWithLogitsLoss() # sigmoid + binary cross-entropy, stable
opt = torch.optim.Adam(net.parameters(), lr=rate)
best_val, best_state, no_improvement, history = float("inf"), None, 0, []
for ep in range(1, epochs + 1):
net.train() # training mode: dropout active
total = 0.0
for xb, yb in loader: # 29 batches per epoch
opt.zero_grad() # 1. clear the gradients of the previous step
L = loss_fn(net(xb), yb) # 2. forward: prediction and loss of the batch
L.backward() # 3. backward: backpropagation -> p.grad of each parameter
opt.step() # 4. Adam updates the 497 parameters
total += L.item() * len(xb)
L_train = total / len(Xtr_t)
L_val, auc_val = evaluate(net, Xval_t, yval_t, loss_fn)
history.append((ep, L_train, L_val, auc_val))
if L_val < best_val - 1e-4: # does validation improve?
best_val, no_improvement = L_val, 0
best_state = {k: v.clone() for k, v in net.state_dict().items()} # save the best weights
else:
no_improvement += 1
if no_improvement >= patience: # early stopping
print(f"Early stopping at epoch {ep}; best epoch: {ep - patience}")
break
net.load_state_dict(best_state) # restore the weights of the best epoch
return history
torch.manual_seed(42)
net = build_net(dropout=0.2)
history = train(net)
for ep, ltr, lval, auc in history:
if ep in (1, 2, 3, 5, 10, 20, 30, 40, 50, 60, 70, 75, 80, 90):
print(f"epoch {ep:3d} train loss {ltr:.4f} val loss {lval:.4f} val AUC {auc:.3f}")
L_test, auc_test = evaluate(net, Xte_t, yte_t, nn.BCEWithLogitsLoss())
print("Test loss:", round(L_test, 4), " test AUC:", round(auc_test, 3))Output (varies slightly with the seed and the PyTorch version):
| Epoch | Train loss | Val loss | Val AUC | Comment |
|---|---|---|---|---|
| 1 | 0.7508 | 0.7316 | 0.544 | Starting point: almost chance |
| 2 | 0.7220 | 0.7087 | 0.474 | |
| 3 | 0.6953 | 0.6765 | 0.397 | Still "knows" less than a coin toss |
| 5 | 0.5529 | 0.5067 | 0.553 | The fast descent begins |
| 10 | 0.3909 | 0.3962 | 0.740 | |
| 20 | 0.3556 | 0.3581 | 0.801 | |
| 30 | 0.3431 | 0.3367 | 0.825 | |
| 40 | 0.3385 | 0.3291 | 0.833 | |
| 50 | 0.3335 | 0.3286 | 0.831 | Flattening out |
| 60 | 0.3282 | 0.3257 | 0.834 | |
| 70 | 0.3185 | 0.3270 | 0.833 | |
| 75 | 0.3226 | 0.3241 | 0.835 | Best epoch (validation minimum) |
| 80 | 0.3190 | 0.3248 | 0.836 | Validation no longer improves |
| 90 | 0.3206 | 0.3248 | 0.835 | 15 epochs without improvement: stop |
Explanation of the key pieces of the loop:
DataLoader(..., batch_size=64, shuffle=True)delivers the 1,799 orders in 29 mini-batches that differ in every epoch.- The trio
zero_grad()→backward()→step()is the heart: withoutzero_grad()the gradients accumulate from one batch to the next (a classic mistake);backward()is the backpropagation of section 5, automatic;step()applies Adam's rule. net.train()/net.eval()toggle the dropout: random switching off while training, all neurons when evaluating. That is why the training loss in the table is somewhat pessimistic (it is computed with neurons switched off) and may sit above the validation loss in the first epochs.- Early stopping has chosen epoch 75 looking only at validation; without it, what would have happened? You will see it in exercise 2: continuing for 300 epochs without dropout, the training loss drops to 0.22-0.23 but the validation loss rises to 0.39-0.40 and the test AUC falls below 0.78. Textbook overfitting.
9.3 Result and reading for NovaMarket
The test AUC is 0.834; repeating with five different seeds gives between 0.834 and 0.842 (mean 0.838). The logistic regression of 04-05 gave 0.844 and the tuned forest of 04-06, 0.838. The network does not win: it ties within the noise, with more complexity, less interpretability and more decisions to make. Marta explains it to Diego bluntly: for the returns predictor, tabular data with 2,249 examples and a fairly linear "truth", logistic regression is still the choice (04-06: for equal performance, the simpler model). This is to be expected and worth knowing: on small tabular data, networks rarely beat well-tuned classical models. The network earns its place where they cannot reach, and that is where we are heading in 05-04 and 05-05: the incident photos and the reviews.
Common Mistakes and Tips
- Forgetting
opt.zero_grad(). The gradients add up to the previous step's and training runs wild. It is the number one mistake when writing the loop by hand. - Sigmoid in the network and
BCEWithLogitsLossat the same time. Double sigmoid: the network learns poorly and the "probabilities" end up squashed. Either sigmoid +BCELoss, or nothing +BCEWithLogitsLoss(recommended). - Not toggling
train()/eval(). Evaluating with dropout active gives worse, random results; training inevalmode disables the dropout. - Learning rate by eye. If the loss does not go down or gives
nan, that is the first thing to touch (lower it). If it goes down very slowly, raise it or use Adam. Tune it with validation like any hyperparameter. - Choosing the number of epochs by looking at the test set. It contaminates the single final look (04-06). That is what validation and early stopping are for.
- Comparing with logistic regression on a different data split. Our test set is exactly that of 04-05 (same seed): only then can the 0.834 be compared with the 0.844.
- Concluding "networks are bad" from this result. It is a small, almost linear tabular problem; the verdict changes with text and images (05-04, 05-05).
Exercises
Exercise 1. Compute by hand one gradient descent step for the neuron of section 5 with x = (1, 0, 2), y = 0 (not returned), w = (0.5, −1, 0.8), b = −1 and η = 0.5: obtain z, p, the loss, ∂L/∂z, ∂L/∂w, ∂L/∂b, the new parameters and the new loss. Check that it has gone down.
Exercise 2. Train the network of section 9 without dropout (build_net(0.0)) and without early stopping (patience=10**6, epochs=300), evaluating in eval mode the training loss, the validation loss and the test AUC at epochs 10, 30, 50, 100, 200 and 300 (with evaluate, and without restoring the "best state"). Describe the curve and explain at which epoch you should have stopped.
Exercise 3. In the code of section 8, change rate to 0.01, to 5 and to 20, and note the loss at epochs 1, 5, 20, 50 and 200. Classify each rate according to the table in section 4 and explain why the loss with rate = 20 does not go down even though it does not blow up either.
Solutions
Solution 1. z = 0.5·1 + (−1)·0 + 0.8·2 − 1 = 1.1; p = σ(1.1) = 0.750; loss L = −log(1 − 0.75) = 1.386 (it gives 75 % to a return that did not happen). ∂L/∂z = p − y = 0.75; ∂L/∂w = 0.75·x = (0.75, 0, 1.5); ∂L/∂b = 0.75. New parameters: w = (0.5 − 0.375, −1 − 0, 0.8 − 0.75) = (0.125, −1, 0.05), b = −1 − 0.375 = −1.375. New pass: z = 0.125 + 0.1 − 1.375 = −1.15, p = 0.240, L = −log(0.76) = 0.275. The loss has dropped from 1.386 to 0.275; the weight of the second column does not change because its input was 0 (∂L/∂w₂ = 0.75·0).
Solution 2. In our run (exact values vary with the seed): epoch 10, train 0.36 / val 0.39 / test AUC 0.78; epoch 30, 0.30 / 0.33 / 0.83; epoch 50, 0.29 / 0.32 / 0.83; epoch 100, 0.27 / 0.33 / 0.82; epoch 200, 0.24-0.25 / 0.35 / 0.78-0.80; epoch 300, 0.22-0.23 / 0.39-0.40 / 0.73-0.78. The training loss keeps dropping (the network memorises), the validation loss bottoms out around epoch 50 and then rises, and the test AUC follows it: from 0.83 it falls below 0.78. You should have stopped around epoch 50, which is exactly what early stopping with patience does on its own. It is the overfitting curve of 04-06 seen along the "epochs" axis instead of "complexity".
Solution 3. rate = 0.01: losses 0.692; 0.686; 0.666; 0.629; 0.507. It always goes down but after 200 epochs it is far from 0.326: too small. rate = 5: 0.345; 0.328; 0.326; 0.326; 0.326: converges in 20 epochs, adequate (even better than 0.5 for such a smooth problem). rate = 20: 0.587; 0.470; 0.600; 0.660; 0.609: too large, it jumps from one side of the valley to the other without settling. It does not blow up because the log loss with a sigmoid grows slowly (large jumps in w saturate the sigmoid and the slope becomes small, which brakes the next step), unlike the parabola of section 4, whose slope grows with distance and does diverge.
Conclusion
We have seen how a network learns. The loss is a landscape and, unlike the hill climbing of 03-04, we now know the slope: the derivative, and with several parameters the gradient. Gradient descent takes steps θ ← θ − η·∂L/∂θ (we followed it by hand on a parabola: 10 → 6.76 → 4.69 → ...), the learning rate decides whether the descent is slow, adequate or divergent, and the chain rule lets us compute the slope of every weight by multiplying local slopes backwards through the computation graph: that is backpropagation, which PyTorch does on its own with backward(). We understood mini-batches and epochs, what momentum and Adam improve, and closed the pending list of 04-06 with early stopping, dropout, data augmentation and batch normalisation. In code, we reproduced fit of logistic regression with twenty lines of numpy (loss 0.693 → 0.326) and trained the NovaMarket MLP in PyTorch with the loop zero_grad → backward → step: test AUC 0.834, tied with logistic regression (0.844) and the forest (0.838). For the returns predictor, Marta sticks with logistic regression.
That tie is the best introduction to the next lesson. If networks do not win on order tables, where do they win, and why? In Deep Learning and Its Applications we will see what makes deep learning "deep" and why it took off in 2012, and the architectures that build in the structure of the data: convolutional networks, which we will try on NovaMarket's incident photos (damaged or intact package), recurrent networks for sequences, autoencoders for anomalies and transfer learning, the idea of not training from scratch.
Fundamentals of Artificial Intelligence (AI)
Module 1: Introduction to Artificial Intelligence
Module 2: Basic Principles of AI
- Fundamental Concepts: Agents, Environments and Rationality
- Types of Artificial Intelligence
- Data as the Raw Material of AI
- Ethics and Considerations in AI
Module 3: Algorithms in AI
- Introduction to Algorithms
- Search Algorithms
- Adversarial Search: Games and Minimax
- Optimization Algorithms
Module 4: Machine Learning
- Basic Concepts of Machine Learning
- Types of Machine Learning
- Data Preparation and Feature Engineering
- Machine Learning Algorithms
- Model Evaluation and Validation
- Overfitting, Regularization and Hyperparameter Tuning
Module 5: Neural Networks and Deep Learning
- Introduction to Neural Networks
- Neural Network Architecture
- How a Network Learns: Gradient Descent and Backpropagation
- Deep Learning and Its Applications
- Transformers, Large Language Models and Generative AI
Module 6: Logic and Expert Systems
- Logic in AI
- Expert Systems
- Reasoning under Uncertainty: Probability and Bayesian Networks
- Applications of Expert Systems
Module 7: Tools and Programming Languages in AI
- Programming Languages for AI
- Scientific Python: NumPy, pandas and Matplotlib
- Popular Tools and Libraries
- Development Environments
Module 8: Projects and Case Studies
Module 9: Exercises and Practice
- Algorithm Exercises
- Machine Learning Practice
- Neural Network Projects
- Capstone Project: from Idea to Prototype
