1. Tensors, Autograd & the Training Loop
A PyTorch tensor is NumPy's ndarray with one crucial
addition: if you set requires_grad=True, PyTorch records every operation
performed on it, building a graph it can later walk backward through to compute
gradients automatically — exactly the backpropagation you derived by hand in Week 11,
generalized to networks of any size.
import torch
import torch.nn as nn
model = nn.Sequential(
nn.Linear(20, 64), nn.ReLU(),
nn.Linear(64, 32), nn.ReLU(),
nn.Linear(32, 1), nn.Sigmoid(),
)
loss_fn = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(50):
optimizer.zero_grad() # clear gradients from the last step
y_pred = model(X_train_tensor) # forward pass
loss = loss_fn(y_pred, y_train_tensor)
loss.backward() # autograd computes every gradient
optimizer.step() # apply the update (Week 11's "W -= lr * dW", generalized)
optimizer.zero_grad() exists because PyTorch accumulates gradients
by default rather than overwriting them — forgetting this call is one of the most common
bugs in a first PyTorch training loop, silently summing gradients across steps instead of
computing each step's fresh.
2. Optimizers: SGD, Momentum & Adam
Week 11's gradient descent update — subtract the gradient, scaled by a learning rate — is plain SGD. In practice, three refinements almost always train faster and more reliably:
- SGD with momentum — accumulates a running average of past gradients, so updates build speed in a consistent direction and dampen oscillation across noisy ones.
- Adam — maintains a per-parameter adaptive learning rate based on both the gradient's recent average and its recent variance, making it far more forgiving of a poorly-chosen global learning rate.
import torch.optim as optim
sgd = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
adam = optim.Adam(model.parameters(), lr=1e-3, betas=(0.9, 0.999))
Adam is the practical default for most projects — it converges quickly with little tuning. Plain SGD with momentum still shows up in settings (some vision and large language model training runs) where its slightly different generalization behavior is worth the extra tuning effort, but as a rule: reach for Adam first, and only revisit that choice if you have a specific reason to.
3. Regularization: Dropout, Weight Decay & Early Stopping
These are Week 5's overfitting fixes, now made concrete for neural networks specifically.
model = nn.Sequential(
nn.Linear(20, 64), nn.ReLU(), nn.Dropout(p=0.3),
nn.Linear(64, 32), nn.ReLU(), nn.Dropout(p=0.3),
nn.Linear(32, 1), nn.Sigmoid(),
)
# Weight decay: L2 penalty added directly by the optimizer
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
Dropout randomly zeroes out a fraction of neurons on every training
step, forcing the network to not rely too heavily on any single neuron — a form of
built-in ensembling, conceptually related to the bagging from Week 9. Weight
decay penalizes large weight values directly in the loss, discouraging the
network from fitting noise with extreme weights. Early stopping —
stopping training the moment validation loss stops improving, exactly as with
early_stopping_rounds in Week 9's gradient boosting — prevents a network
from continuing to memorize the training set after it has already found the useful
pattern.
model.eval()
Dropout should be active during training but disabled during evaluation, so predictions are deterministic and use the full network. Calling model.eval() before validation/inference (and model.train() before resuming training) switches this automatically — a very easy step to forget.
4. Batch Normalization & Learning Rate Schedules
Batch normalization normalizes each layer's activations (subtracting the batch mean, dividing by the batch standard deviation) before the next layer sees them — the same standardization idea from Week 4, applied between layers rather than only at the input. This keeps activations in a consistent, well-behaved range throughout training, which generally allows higher learning rates and faster convergence.
model = nn.Sequential(
nn.Linear(20, 64), nn.BatchNorm1d(64), nn.ReLU(),
nn.Linear(64, 32), nn.BatchNorm1d(32), nn.ReLU(),
nn.Linear(32, 1), nn.Sigmoid(),
)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
for epoch in range(50):
# ... training loop ...
scheduler.step() # halve the learning rate every 10 epochs
A learning rate schedule lowers the learning rate over the course of training — large steps early to make fast progress, smaller steps later to settle precisely into a good minimum instead of oscillating around it. This is a direct answer to Week 11's tension between "too large" and "too small" learning rates: rather than picking one fixed value, start large and shrink it over time.
5. Reading Training/Validation Curves to Diagnose Problems
This is Week 5's learning curve, extended over training epochs rather than dataset size, and it's the single most useful debugging tool for a neural network.
- Both losses stay high and flat — underfitting; the model is too small, or the learning rate is badly chosen. Try a larger network or a different learning rate before anything else.
- Training loss keeps dropping, validation loss stops or rises — classic overfitting; apply Section 3's tools (dropout, weight decay, early stopping).
- Both losses drop together, smoothly — healthy training; keep going, or consider whether you've plateaued and could benefit from Section 4's schedule.
- Loss is
NaNor explodes — almost always a learning-rate-too-high or unstabilized-activation problem; lower the learning rate and check for batch normalization.
6. Hands-on Exercise
Train, overfit on purpose, then fix it with regularization
You're given a small, noisy synthetic classification dataset — deliberately small enough that an unregularized network will overfit visibly.
from sklearn.datasets import make_classification
import torch
X, y = make_classification(
n_samples=300, n_features=20, n_informative=6,
n_redundant=8, flip_y=0.1, random_state=42,
)
Requirements:
- Split into train/validation/test (Week 5), convert to tensors, and train an unregularized 3-layer network for 200 epochs with Adam.
- Plot training and validation loss per epoch, and confirm a visible overfitting gap (Section 5).
- Add dropout and weight decay, retrain from scratch, and confirm the gap shrinks.
- Add early stopping (track the best validation loss and stop if it hasn't improved in 15 epochs), and report at which epoch it stopped.
- Compare final test-set performance between the unregularized and regularized models.
Remember model.eval() before computing validation loss each epoch, and model.train() before the next training step — forgetting this with dropout in the model will make your validation loss look artificially noisy or wrong.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why must you call optimizer.zero_grad() at the start of every training step?
Why must you call optimizer.zero_grad() at the start of every training step?
PyTorch accumulates gradients into .grad by default rather than overwriting them on each .backward() call. Without clearing them first, each step's gradients would be summed with every previous step's, corrupting the update — zero_grad() ensures each step computes a fresh gradient from that step's data alone.
Q2
Why is Adam generally more forgiving of a poorly-chosen learning rate than plain SGD?
Why is Adam generally more forgiving of a poorly-chosen learning rate than plain SGD?
Adam maintains a per-parameter adaptive learning rate, scaled by that parameter's recent gradient magnitude and variance — parameters with large, noisy gradients automatically get smaller effective steps, and vice versa. Plain SGD applies one fixed learning rate uniformly to every parameter, so a global rate that's poorly chosen affects everything equally.
Q3
Why should dropout be disabled during evaluation, and how do you disable it in PyTorch?
Why should dropout be disabled during evaluation, and how do you disable it in PyTorch?
Dropout randomly zeroes neurons to prevent overfitting during training, but this randomness would make evaluation/inference non-deterministic and use only part of the network — you want the full, stable network making predictions at evaluation time. Calling model.eval() switches dropout (and batch norm) into evaluation mode automatically.
Q4
Training loss keeps falling but validation loss starts rising after epoch 20. What should you do?
Training loss keeps falling but validation loss starts rising after epoch 20. What should you do?
This is the classic overfitting signature from Section 5 — apply regularization (dropout, weight decay) or use early stopping to stop training around epoch 20, before validation performance degrades further. Training for more epochs at this point only makes the network memorize the training set more, not generalize better.