Lab 01 — A From-Scratch Supervised Training Loop
Phase 32 · Lab 01 · Phase README · Warmup
The problem
Every deep-learning framework hides the same four-step loop behind model.fit() or
loss.backward(); optimizer.step(): forward → loss → backward → step. If you have only ever
called that method, you cannot answer the interview question "what does .backward() actually
compute?" or debug a model whose loss goes to NaN, sits flat, or diverges. So you build the loop
yourself, for the simplest model that still has every moving part: binary logistic regression.
Logistic regression is the honest floor of supervised learning — a single linear layer plus a
sigmoid, trained by gradient descent on a convex loss. It has a loss function (binary
cross-entropy), an analytic gradient (the famous residual p − y), a regularizer (L2), a
training loop (epochs × mini-batches with a shuffle), an overfitting control (early
stopping on a validation set), and an evaluation story (a confusion matrix and its metrics).
Get all of that right in pure Python and the same skeleton scales — unchanged in shape — to a
100-layer network; only the gradient computation gets automated (that is what autograd is, and
the sibling Senior AI Engineer track builds it from scratch).
What you build
| Piece | What it does | The lesson |
|---|---|---|
sigmoid | numerically stable 1/(1+e^-z) | why the naive form overflows and how the sign-branch fixes it |
bce_loss | mean binary cross-entropy + (l2/2)‖w‖² | the loss is what you actually minimize; L2 lives inside it |
compute_gradients | analytic ∂L/∂w = (1/n)Σ(p−y)x + l2·w, ∂L/∂b = (1/n)Σ(p−y) | the residual p−y is the whole trick; the FD check proves it |
LogisticRegression.fit | epoch × mini-batch loop, seeded shuffle, SGD step | forward → loss → backward → step, made concrete |
| early stopping | patience on validation loss, restore best weights | the cheapest, most-used regularizer in practice |
train_val_split | deterministic shuffle-and-split, no row overlap | you never measure generalization on training data |
confusion_matrix + accuracy/precision/recall/f1_score | evaluation from four counts | accuracy lies under imbalance; precision/recall/F1 don't |
File map
| File | Role |
|---|---|
lab.py | your implementation (fill the # TODOs) |
solution.py | reference implementation + worked example (python3 solution.py) |
test_lab.py | 27 tests: sigmoid stability, loss, gradient-vs-finite-difference, convergence, L2, early stopping, metrics, determinism |
requirements.txt | pytest only |
Run it
python3 -m pytest test_lab.py -q # your lab.py (red until you implement)
LAB_MODULE=solution python3 -m pytest test_lab.py -q # the reference (green)
python3 solution.py # worked example + learning curve
Success criteria
-
sigmoid(1000)andsigmoid(-1000)return1.0/0.0without overflowing. -
bce_lossis small for confident-correct weights, large for confident-wrong, and its L2 term adds exactly(l2/2)‖w‖². -
compute_gradientsmatches a central finite-difference estimate ofbce_lossto1e-5for every weight and the bias — the proof your backward pass is correct. - Training loss decreases over epochs and reaches >90% accuracy on separable data.
-
A larger
l2produces a smaller learned weight norm. - Early stopping fires before the epoch cap and restores the best-validation weights.
-
Metrics match a hand-checked confusion matrix, and precision/recall/F1 return
0.0(not aZeroDivisionError) when a denominator is empty. - Same seed → identical trained weights; different seed → a different trajectory.
-
All 27 tests pass under both
labandsolution.
How this maps to the real stack
compute_gradientsis whatloss.backward()does in PyTorch and whatGradientTaperecords in TensorFlow — for one linear layer we can write the gradient by hand because it has a closed form; a deep network has the autograd engine apply the chain rule for you, but the quantity computed is identical (the partial derivative of the loss w.r.t. every parameter). The finite-difference check here is exactlytorch.autograd.gradcheckin miniature.LogisticRegression.fitis the canonical PyTorch training loop —for epoch: for batch in loader: optimizer.zero_grad(); loss = criterion(model(x), y); loss.backward(); optimizer.step()— and Keras'smodel.fit(...)withvalidation_dataand anEarlyStoppingcallback. Our per-batch updatew -= lr * gradis plain SGD; Adam and momentum (Warmup §5) change how the step is computed from the gradient, not the loop's shape.- The seeded shuffle is
DataLoader(shuffle=True, generator=torch.Generator().manual_seed(s))/tf.data.Dataset.shuffle(..., seed=s): reproducible epoch order is a real requirement, not a test convenience. - Early stopping with
restore_best_weightsiskeras.callbacks.EarlyStoppingand the standard hand-rolled PyTorch pattern — the single most common regularizer in production, and themin_delta/patience semantics here match Keras's exactly. - The metrics are
sklearn.metrics.confusion_matrix/precision_recall_fscore_support; computing them from the four cells is what those library calls do under the hood.
Limits of the miniature. Real optimizers use momentum/Adam and learning-rate schedules; ours is fixed-LR SGD. Real tokenized/standardized features come from a preprocessing pipeline (Phase 27); ours is a clean synthetic Gaussian. Real autograd handles arbitrary computation graphs; our gradient is hand-derived for one convex model. The shape — forward, loss, backward, step, validate, early-stop, evaluate — is the production shape.
Extensions (your own machine)
- Add momentum (
v = μv − lr·g; w += v) and then Adam (Warmup §5) and compare convergence speed on the same seed. - Swap BCE for hinge loss to turn this into a linear SVM; note how the gradient changes.
- Add k-fold cross-validation (Warmup §14) around
fitand report mean ± std of val F1. - Extend to multi-class with a softmax head and categorical cross-entropy.
- Add a learning-rate schedule (step decay) and watch the loss curve smooth out near the end.
Interview / resume signal
"Implemented logistic regression end-to-end in pure Python: numerically stable sigmoid, binary cross-entropy with L2, the analytic
p−ygradient verified against a finite-difference check, a mini-batch SGD training loop with a seeded shuffle, early stopping with best-weight restoration, and confusion-matrix metrics (precision/recall/F1) — the same forward→loss→backward→step loop every framework hides behind.fit(), built so I can debug a flat, diverging, or overfitting loss instead of guessing."