learn-classification-regression-with-phoebe / Session 4 of 10
Learn Classification & Regression with Phoebe · Session 4 of 10

How models learn: gradient descent

Session 3 ended on a cliffhanger: log-loss is the thing logistic regression minimizes. Today you see HOW anything gets minimized. Loss is a landscape over the weights, the gradient points downhill, and the model walks - one stride at a time, stride length set by a single knob called the learning rate. Then you build the walk yourself: a 15-line numpy loop, run on the real Lumen data, that reproduces scikit-learn's coefficients to two decimals. After today, "solver" stops being a scary word in the docs - it is just this loop wearing a suit.

🟡 Core zoo Builders · DA / DE / DS Python · numpy 45 min
0-3 · Setup 3-18 · The surface and the step 18-40 · Build: descend from scratch 40-45 · Wrap
Part 0

From "what we minimize" to "how"

In session 3 you learned that logistic regression picks the weights that minimize log-loss - and we waved a hand at how it finds them. Today the hand-waving stops. Gradient descent is the how, and it is almost embarrassingly simple: check which way is downhill, take a step, repeat. Every solver in scikit-learn, and every optimizer that trains every neural network on Earth, is a variation on this one loop. Learn it once and a whole shelf of documentation stops being intimidating.

Live - built in session Self-study - run after ★ The one big idea The case: Lumen Skincare
★ What you walk out with today A 15-line numpy loop - sigmoid, gradient, step rule - that descends the real Lumen log-loss surface and lands on w = [0.313, 0.238], b = -3.461, matching sklearn's [0.315, 0.240], -3.465 to two decimals. Plus permanent immunity to solver-fear: lbfgs, sag, saga are this loop with better shoes.
Part 1 · the landscape

The loss surface 7 min live

Pick any pair of weights and the loss function hands back one number: how wrong the model is with those weights. Do that for every possible pair and you get a surface - a landscape where altitude is wrongness. Training is finding the lowest point in that landscape. For log-loss (and for MSE in linear regression) the landscape is a convex bowl: one valley, one global minimum, no fake bottoms to get stuck in. That single geometric fact is why training a logistic model is reliable and reproducible.

The loss surface - log-loss over the weights is a bowl loss weight w start: random weights steep = long stride flat = short stride global minimum the gradient says which way is downhill, the learning rate says how big a stride
🔍 Click to zoom - one valley, one bottom: the ball cannot end up anywhere else
LiveWhat a gradient is - the slope in every direction at once4 min▶

In one dimension the slope is a single number: uphill or downhill, and how steeply. With many weights, the gradient is just the slope measured along every weight at once - a vector that points in the steepest uphill direction. Walk against it and you are heading downhill as fast as locally possible. That is the whole trick: the model never sees the landscape, it only ever feels the tilt under its feet.

How do we compute the tilt of a loss that runs through a sigmoid? The chain rule: a chain of effects multiplies. How loss changes with the prediction, times how the prediction changes with the score, times how the score changes with the weight. For log-loss through a sigmoid the chain collapses to something almost rude in its simplicity: the gradient is driven by (p - y) - prediction minus truth. Wrong by a lot, pushed by a lot.

Self-study pointer: we use the chain rule, we do not derive it. StatQuest's "The Chain Rule" episode is the gentlest 18 minutes on it anywhere - watch it before the deep-learning course, where the chain gets long.
LiveWhy convexity matters - one valley vs a bumpy range3 min▶

Session 3's callback: why log-loss instead of just squaring the errors on the sigmoid's output? Because MSE pushed through a sigmoid is not convex - the surface ripples, with local dents where a descending ball can park and honestly believe it is done. Log-loss on a linear score is provably one smooth bowl: wherever you start, walking downhill ends at the same global minimum.

This is why two runs of LogisticRegression on the same data give the same coefficients, and why the solver can tell you it "converged" with a straight face. Convexity is not a math nicety - it is the difference between "training works" and "training sometimes works".

Part 2 · the walk

The step rule and its one knob 9 min live

The entire algorithm is one line: w := w - lr * gradient. Current weights, minus the uphill direction scaled by the learning rate. Repeat until the loss stops falling. Everything that can go wrong with gradient descent goes wrong in that one multiplication - which makes the learning rate the single most consequential knob you will meet today.

lr too small ...still going... tiny hops, thousands of iterations to converge lr just right big strides early, gentle landing at the bottom lr too big - diverges each stride overshoots - loss climbs, ends in nan one knob: too small crawls, right glides, too big bounces higher each step and diverges
🔍 Click to zoom - three learning rates, three fates on the same bowl
LiveThe learning rate - the only knob that can ruin everything3 min▶

Too small: the ball crawls, and you wait thousands of iterations for what should take hundreds. Too big: each stride flies past the valley floor and lands higher on the opposite wall - the loss climbs instead of falling and eventually overflows to nan. There is no partial credit; a diverging run is garbage.

The convergence check is beautifully low-tech: plot the loss every few iterations. Falling steadily? Converging. Flat? Done - stop. Rising or jagged? Cut the learning rate and restart. When you tune, try values several times apart (0.005, 0.05, 0.5) rather than inching - the failure modes are orders-of-magnitude apart, so the search should be too.

LiveFeature scaling makes the bowl round3 min▶

On Lumen, prior_30d_spend runs 0-400 while pages_viewed runs 1-15. Feed those in raw and the bowl is not round - it is a stretched canyon, thousands of times longer in one direction than the other. The steepest-downhill direction in a canyon points at the near wall, not along the valley - so descent zigzags wall to wall, making tiny real progress per step. One learning rate cannot fit both axes: small enough for the steep wall means glacial along the floor.

Standardize the features and the canyon becomes a bowl: every direction has a comparable slope, downhill points at the minimum, and one learning rate works everywhere. This is also the payoff of session 3's solver card: this is WHY sag and saga demand scaled inputs - they are descent methods, and unscaled features hand them a canyon.

Rule of thumb: anything that learns by descending - logistic via sag/saga, SGD, neural nets - gets standardized features. Fit the scaler on train only, forever.
Self-studyBatch vs stochastic vs mini-batch4 min▶

Batch gradient descent (today's lab) computes the gradient over ALL training rows before each step: smooth, exact, but one step costs a full pass over the data. Stochastic (SGD) computes it from ONE row: each step is noisy and often slightly wrong, but costs almost nothing - so you take thousands of cheap wobbly steps and still reach the valley, even when the data no longer fits in memory. Mini-batch - a few dozen rows per step - is the compromise that deep learning standardized on.

In scikit-learn this lives in SGDClassifier(loss="log_loss"): same logistic model, descent done one row at a time. When Lumen outgrows lbfgs - say 400 million sessions instead of 40,000 - this is the escape hatch. The loss curve wobbles instead of gliding; squint and it still goes down.

Part 3 · the reveal

What sklearn's solvers actually are 5 min live

Now re-read session 3's solver table with today's eyes. lbfgs, newton-cholesky, sag, saga - every one is an industrial-strength variant of the loop you are about to write. lbfgs approximates the bowl's curvature to pick smarter strides; newton methods use the curvature exactly; sag and saga are stochastic descent with variance tricks so they converge like batch. Different shoes, same walk. You now know the family, so the docs read like a menu instead of a spellbook.

Self-studyWhen descent fails - a postcard from deep learning3 min▶

Everything today leaned on one luxury: convexity. Neural-network loss surfaces are NOT convex - they are mountain ranges with saddle points (flat in one direction, curved in another) and countless local dips. Descent still works there, but it needs help: momentum to coast through flats, Adam to adapt the stride per weight, and backpropagation to compute gradients through many layers - which is just the chain rule, chained longer.

None of that is this course's job. It belongs to the deep-learning course - and when you get there, the optimizer pages will read as "gradient descent plus tricks", because that is what they are.

Real worldA data scientist pinged her team channel in a mild panic: "ConvergenceWarning: lbfgs failed to converge - is the model broken?" Nobody who fears solvers can answer that; anyone who knows the loop can. The descent simply ran out of steps before the loss flattened - on a canyon-shaped surface, because two features were unscaled. Scale the features (or raise max_iter) and the warning evaporates. Ten minutes, no mystery, no rebuilt model.
Build-along

Descend from scratch - and reproduce sklearn ★ 18 min · everyone builds

The lab this whole session exists for: batch gradient descent on the real Lumen data, two scaled features, learning rate 0.5, 400 iterations, about 15 lines of numpy. Then we fit LogisticRegression on the exact same features and compare coefficients. If the two match, you did not learn an analogy of how models train - you learned the thing itself.

Build the pieces. Standardize prior_30d_spend and pages_viewed on train only, then write the sigmoid and the log-loss function - the surface we are descending, straight from session 3.

Take ONE step by hand. Compute (p - y), the gradient, and one update of w and b. Print the loss before and after - it fell. That falling number IS learning.

Loop it 400 times. lr = 0.5. Watch the printed loss fall fast, then level off - the flattening is your convergence check.

Compare with sklearn. LogisticRegression(C=1e6) (near-unregularized, so the bowls match) on the same two features. Read the two sets of numbers side by side, slowly.

descend.py - the 15-line loop that is every solver's grandparentimport numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression df = pd.read_parquet("lumen_sessions.parquet") X = df[["prior_30d_spend", "pages_viewed"]].to_numpy(dtype=float) y = df["converted"].to_numpy(dtype=float) X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y) mu, sd = X_tr.mean(axis=0), X_tr.std(axis=0) # scale on TRAIN only - X_tr = (X_tr - mu) / sd # a round bowl descends straight def loss(p, y): # log-loss: the c3 surface return -(y * np.log(p) + (1 - y) * np.log(1 - p)).mean() w, b, lr = np.zeros(2), 0.0, 0.5 for i in range(400): p = 1 / (1 + np.exp(-(X_tr @ w + b))) # sigmoid grad_w = X_tr.T @ (p - y_tr) / len(y_tr) # which way is uphill grad_b = (p - y_tr).mean() w, b = w - lr * grad_w, b - lr * grad_b # the step rule, verbatim if i % 100 == 0: print(f"iter {i:3d} loss {loss(p, y_tr):.4f}") # falling = learning print(w.round(3), round(b, 3)) # [0.313 0.238] -3.461 sk = LogisticRegression(C=1e6).fit(X_tr, y_tr) print(sk.coef_[0], sk.intercept_[0]) # ~[0.315 0.240] -3.465
★ The payoff line: your loop just reproduced scikit-learn. w = [0.313, 0.238], b = -3.461 from 15 lines of numpy vs [0.315, 0.240], -3.465 from the library - a match to two decimals. The remaining crumbs are stopping rules and floating-point, not concept. Solvers are officially demystified.
the cautionary run - same loop, learning rate cranked to 50w, b, lr = np.zeros(2), 0.0, 50 # one knob, turned way too far for i in range(400): p = 1 / (1 + np.exp(-(X_tr @ w + b))) w -= lr * (X_tr.T @ (p - y_tr) / len(y_tr)) b -= lr * (p - y_tr).mean() # loss explodes to nan within a few steps: every stride flies over the # valley and lands higher on the far wall. Divergence is not "slower # training" - the run produces nothing usable at all.
If your numbers differ in the 2nd decimal: fine - library-version jitter. Differ in the 1st? Check the seed (42), that you standardized on train statistics only, and that C=1e6 - regular C=1 regularizes, which moves the bowl's bottom on purpose (that story was session 3's).
Homework

Before session 5 ◐ 25 min

Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · What does the learning rate control, and what happens when it is too big?

w := w - lr * gradient. The gradient gives direction, lr gives stride length. Too small merely crawls; too big lands higher on the opposite wall every step - divergence, the one failure with no partial credit.

2 · Why do we scale features before gradient descent?

A stretched valley means no single learning rate suits both axes: safe for the steep wall = glacial along the floor. Standardizing rounds the bowl - which is why descent-based solvers (sag, saga, SGD) demand scaled inputs.

3 · Stochastic gradient descent vs batch - what is the trade, and who wins on huge data?

One row per step means each step is slightly wrong but costs almost nothing - thousands of wobbly steps still find the valley. That is SGDClassifier(loss="log_loss"): the same logistic model, built for data that outgrows lbfgs.

Source material

What this session covers

Session 4 covers the gradient-descent core of the anchor sources - StatQuest's GD episodes and Andrew Ng's C1 optimization arc, with the classic numpy GD lab pattern mirrored on Lumen data. Videos, graded labs, and certificates stay with the official providers.

✓StatQuest - Gradient Descent (step-by-step)Parts 1-2 + the lab
✓StatQuest - Stochastic Gradient DescentPart 2 batch/SGD/mini-batch card
◐StatQuest - The Chain Rulenamed + intuition only; pointed as self-study
✓Ng C1 W1 - gradient descent, implementing GD, learning rateParts 1-2 + demo
✓Ng C1 W2 - feature scaling, convergence check, choosing the learning ratethe numpy-lab pattern, mirrored on Lumen
◐sklearn - lbfgs/newton/sag/saga as descent variants; SGDClassifier as scale-outPart 3; ties to c3's solver card
✗Adam, momentum, backpropagationnot covered by design - the deep-learning course

Session 4 cheat sheet · pin this

The step rulew := w - lr * gradient. Gradient = steepest uphill (built from (p - y) via the chain rule); walk against it, repeat until the loss flattens.
Learning-rate fatesToo small: crawls for thousands of iterations. Right: glides to the bottom. Too big: overshoots every step, loss climbs, nan. Tune in multiples, watch the loss curve.
Convex bowlLog-loss and MSE give ONE valley, one global minimum - any start, same answer. MSE-through-sigmoid ripples; that is why c3 chose log-loss.
Scale firstUnscaled features = stretched canyon = zigzag descent. Standardize (train stats only) and the bowl is round - the reason sag/saga/SGD demand scaling.
Batch / SGD / mini-batchAll rows per step (smooth, dear) / one row (noisy, ~free, scales forever) / a few dozen (deep learning's default). SGDClassifier(loss="log_loss") when data outgrows lbfgs.
The 15-line receiptBatch GD, 2 scaled features, lr 0.5, 400 iters: w [0.313, 0.238], b -3.461 vs sklearn [0.315, 0.240], -3.465. Solvers = this loop with better shoes.