Unit 4 · Tutorial 5 of 8
Fitting the Model: Gradient Descent and the Normal Equation
Two ways to reach the bottom of the same bowl. The normal equation solves for it directly in one step. Gradient descent walks toward it, one step at a time. This tutorial builds both, shows when each is the right tool, and demonstrates empirically why the second one needs scaled features to work well.
1 Learning objectives
By the end of this tutorial you will be able to
- State the gradient descent update rule and explain the role of each component: the gradient, the learning rate and the initialisation.
- Implement gradient descent from scratch in NumPy for simple linear regression, and verify it against
scikit-learn. - Diagnose the effect of the learning rate by reading a cost-versus-iteration curve: too small, well chosen, and too large.
- Choose a sensible stopping criterion — a fixed number of iterations, a gradient tolerance or a patience-based early stop — and justify the choice.
- Explain feature scaling, contrast standardisation with normalisation, and demonstrate why gradient descent converges faster on scaled features.
- Solve for the least-squares parameters using the normal equation and state its computational cost, O(nd² + d³).
- Decide between the normal equation and gradient descent by reasoning about n, d, conditioning and whether a closed form exists.
Estimated time
35 minutes of reading, plus 30–35 minutes for the practice exercises and problem set. This is the most hands-on tutorial in Unit 4 — you should have a notebook open while you read it.
2 Motivating example
Tutorial 3 ended with a formula: ŵ = (XᵀX)⁻¹Xᵀy. Type it into NumPy and you get the answer. No iteration, no learning rate, no choices to make. So why would anyone bother with gradient descent?
There are two answers, and the first one is decisive.
The formula does not exist for most models. Logistic regression — the very next unit — has no closed form. Neither do decision trees, random forests, support vector machines or neural networks. Every single model you will meet after Unit 6 is fitted by some iterative procedure. If you cannot walk downhill, you can fit exactly one model, and you have already fitted it.
The formula also becomes impractical at scale. The cost of forming and inverting XᵀX grows as O(d³) in the number of features. With a thousand features that is a billion operations per inversion, and the matrix alone occupies eight megabytes. With a hundred thousand features — perfectly ordinary for text data — it is a trillion operations and eighty gigabytes. The normal equation becomes unusable long before gradient descent does.
So gradient descent is not a fallback for when the algebra is hard. It is the primary tool of modern machine learning, and the normal equation is a convenient special case that happens to be available here.
Normal equation
- One step, exact answer
- No learning rate to tune
- No initialisation needed
- O(d³) — fails on wide data
- Requires XᵀX to be invertible
Gradient descent
- Iterative, approaches the answer
- Learning rate and initialisation to choose
- Scales to any n or d
- Works when no closed form exists
- Needs feature scaling to converge well
The valley metaphor, one more time
In Tutorial 4 you built the landscape. Now you walk on it. The rule is simple: at each step, look at which way is downhill, and take a small step in that direction. Repeat until you stop making progress. The entire difficulty lies in the words small and stop.
3 Concept explanation
3.1 The gradient descent algorithm
From Tutorial 4, the gradient of the MSE cost function is:
∂J/∂w = −(2/n) Σ xi ei ∂J/∂b = −(2/n) Σ ei
where ei = yi − (w xi + b). The gradient points uphill, so the descent direction is its negative. The gradient descent update rule is therefore:
Gradient descent for simple linear regression
w ← w − α · ∂J/∂w b ← b − α · ∂J/∂b
Both parameters are updated simultaneously, using the gradient computed at the current point. Computing w first and then using the new value to update b is a different algorithm (coordinate descent) and will not give the same trajectory.
Expanded into the residuals, the whole step is:
eᵢ = yᵢ − (w xᵢ + b) for every i
w ← w + α · (2/n) Σ xᵢ eᵢ
b ← b + α · (2/n) Σ eᵢ
Read the update for w as a sentence. Each example contributes to the step in proportion to its feature value xi and its residual ei. A large x with a positive residual pushes the slope up strongly; a small x barely contributes regardless of its residual. And if the residuals cancel out — as they do at the optimum — the step becomes zero.
The full algorithm, written out:
-
Initialise Set w and b to some starting values. The conventional choice is zero for both, though random small values are also used.
-
Compute predictions and residuals Using the current parameters, calculate ŷi and ei for every training example.
-
Compute the gradient ∂J/∂w and ∂J/∂b from the residuals.
-
Update both parameters Move each one a small step against its partial derivative, using the same learning rate α.
-
Check for convergence If the gradient is small enough, or the cost has stopped improving, or you have hit the iteration limit — stop. Otherwise return to step 2.
3.2 The learning rate
The learning rate α controls the size of each step. It is the single most important hyperparameter in gradient descent, and the one you will spend the most time tuning in practice.
| Value of α | Behaviour | Symptom |
|---|---|---|
| Too small | Converges, but very slowly. Each step makes a tiny improvement. | Cost curve decreases smoothly but is still falling when you run out of iterations. |
| Well chosen | Converges in a reasonable number of iterations. Cost curve drops steeply then flattens. | Cost reaches a plateau well before the iteration limit. |
| Too large | Overshoots the minimum. Cost oscillates and may grow without bound. | Cost curve goes down then up, or zig-zags, or produces NaN. |
For a quadratic cost function there is a precise bound. If the largest curvature of the surface — the largest eigenvalue of the Hessian — is λmax, then gradient descent converges if and only if:
0 < α < 2 / λmax
Below that bound it converges; above it, it diverges. At exactly the bound it oscillates forever without settling. In practice you rarely compute λmax explicitly, but the bound explains the empirical rule: if your cost curve is diverging, halve the learning rate and try again.
The bound depends on the data
Because λmax is a property of the Hessian, and the Hessian is built from XᵀX, the safe learning rate depends on the scale of your features. Double every feature value and you quadruple λmax, so the maximum safe learning rate drops by a factor of four. This is why you must retune α after any change to how the features are encoded — standardising, changing units, or adding a new feature all shift the bound.
3.3 Convergence and stopping criteria
Gradient descent approaches the minimum asymptotically. In exact arithmetic it never actually arrives — each step gets you closer, but there is always a step remaining. In floating-point arithmetic the cost eventually stops changing meaningfully and further iterations are wasted. So you need a stopping rule.
Three are common, and they are usually combined:
A practical pattern is to set a generous iteration cap, a small cost-change tolerance, and a patience of ten iterations. Run until any of the three triggers. Then plot the cost curve and confirm visually that the plateau is real and not an artefact of the stopping rule.
3.4 Feature scaling
Tutorial 4 established that the shape of the loss surface depends on the scale of the features. This section makes the consequences concrete.
Suppose one feature is measured in metres (range 0–3) and another in millimetres (range 0–3000). The Hessian entries corresponding to the two coefficients differ by a factor of roughly a million, so the contours become extremely elongated. On such a surface:
- The maximum safe learning rate is set by the steepest direction — the large-scale feature — and must therefore be very small.
- Progress along the flat direction — the small-scale feature — is correspondingly slow. You could need tens of thousands of iterations to resolve a coefficient whose natural scale would have taken ten.
Two standard rescaling methods fix this:
| Method | Formula | Resulting range | When to use |
|---|---|---|---|
| Standardisation (z-score) |
x′ = (x − x̄) / s | Mean 0, standard deviation 1 | The default. Works well for most features and is what StandardScaler does. |
| Min–max normalisation | x′ = (x − min) / (max − min) | Exactly [0, 1] | When a bounded range is needed, for example as input to a distance-based method. Sensitive to outliers, since a single extreme value compresses everything else. |
Standardisation is the safer default. It is less distorted by outliers than min–max, and the resulting features have a natural interpretation: a value of +2 means "two standard deviations above average".
Scale the training set, apply the same scale to test
The mean and standard deviation used for standardisation must be computed
on the training set only. Then the same values are applied to the
validation and test sets. Computing them on the whole dataset before splitting is
a form of data leakage: information from the test set has
influenced the training pipeline. The effect is usually small, but it is a real
error and it will be caught in Unit 6. The safest practice is to put the scaler
inside a Pipeline, which handles this automatically.
3.5 The normal equation
From Tutorial 3, the least-squares solution in matrix form is:
The normal equation
ŵ = (XᵀX)⁻¹ Xᵀy
where X is the n × (d+1) design matrix with a leading column of ones, and y is the length-n target vector.
This is not an approximation and it does not iterate. It produces the exact minimiser of the MSE cost in a single computation, and the answer is identical to the limit that gradient descent would converge to given infinite iterations.
Computational cost. The two expensive operations are:
- Forming XᵀX, which is a (d+1) × (d+1) matrix. Each of its (d+1)² entries is a dot product over n examples, giving O(n d²).
- Inverting it, which is O(d³).
The total is O(n d² + d³), and the memory required is O(d²) for the matrix itself. That last term is what kills it: with d = 10,000 features, the matrix has 10⁸ entries — eight hundred megabytes in double precision, before you have done any arithmetic.
In practice, scikit-learn does not form the inverse explicitly. It
uses a QR decomposition or a singular value decomposition of X,
which is more numerically stable — forming
(XᵀX)⁻¹ squares the condition number of the
problem and can lose precision badly when features are nearly collinear. The
mathematical answer is the same; the numerical route is better. This is why the
LinearRegression documentation warns about the case where the design
matrix is ill-conditioned.
When the normal equation fails entirely
If XᵀX is singular — because features are
perfectly collinear, or because d ≥ n — the inverse does not
exist and the normal equation has no unique solution. LinearRegression
will still return coefficients, but it is silently Computing a minimum-norm
solution via a pseudo-inverse. The result is valid but arbitrary: many different
coefficient vectors fit the data equally well. If you need a unique, stable answer
in that situation, you need regularisation, which you will meet
in Module 3.
3.6 Choosing between them
For linear regression specifically, both methods produce the same answer, so the choice is entirely about cost and numerical behaviour. The decision rule is straightforward.
| Situation | Prefer | Reason |
|---|---|---|
| Few features (d < ~1,000) | Normal equation | Instant, exact, no hyperparameters to tune. |
| Very many features (d > ~10,000) | Gradient descent | O(d³) inversion and O(d²) memory become prohibitive. |
| Very many examples (n > 10⁶) | Gradient descent (or SGD) | Each iteration costs O(nd), but you can use mini-batches and stop early. |
| Features are highly collinear | Neither — regularise | XᵀX is ill-conditioned or singular; ridge regression gives a stable answer. |
| No closed form exists (logistic regression, neural networks) | Gradient descent | There is no alternative. |
| You need the exact optimum to high precision | Normal equation | Gradient descent only approaches the optimum asymptotically. |
The threshold of around a thousand features is a rough guideline, not a law. The real question is whether the O(d³) term is affordable. On a modern laptop, inverting a 1,000 × 1,000 matrix takes a fraction of a second; a 10,000 × 10,000 matrix takes minutes and uses most of your memory. Compute both sides of the trade-off for your problem rather than assuming.
The habit worth building
For linear regression, use the normal equation — it is simply better when it is available. But implement gradient descent by hand at least once, on a problem where you already know the answer, so that when you meet a model with no closed form you understand exactly what the optimiser is doing and why it sometimes fails.
4 Visual illustration
5 Worked example
We will run gradient descent on the bike-share data, twice — once on the raw temperature values and once on standardised values — and compare. The contrast between the two runs is the whole point of this tutorial.
Step 1 — Set up
Recall the eight observations: x = [8, 12, 15, 18, 21, 24, 27, 30] °C, y = [120, 180, 240, 300, 360, 430, 510, 560] rentals, n = 8. From Tutorial 3, the exact optimum is ŵ ≈ 20.6502, b̂ ≈ −62.5976, with MSE ≈ 103.87.
Both runs start from w = 0, b = 0. That is the worst possible starting point in one sense — the model predicts zero rentals every hour — but it makes the arithmetic transparent and it is a common default.
Step 2 — The first gradient, computed by hand
At (w, b) = (0, 0), every prediction is 0, so every residual equals the observed value: ei = yi.
Σ eᵢ = 120 + 180 + 240 + 300 + 360 + 430 + 510 + 560 = 2,700
Σ xᵢeᵢ = 8(120) + 12(180) + 15(240) + 18(300)
+ 21(360) + 24(430) + 27(510) + 30(560)
= 960 + 2,160 + 3,600 + 5,400
+ 7,560 + 10,320 + 13,770 + 16,800
= 60,570
∂J/∂w = −(2/8)(60,570) = −15,142.50
∂J/∂b = −(2/8)( 2,700) = −675.00
Gradient = ( −15,142.50 , −675.00 )
Both components are negative, so both parameters should increase. That is correct: the current model predicts zero, and every observation is positive, so the line needs to move up and tilt upward.
Step 3 — The first step, and the problem with the raw scale
The Hessian for this data — the matrix of second derivatives — is:
H = (2/n) XᵀX
⎡ 850.75 38.75 ⎤
= ⎢ ⎥
⎣ 38.75 2.00 ⎦
Eigenvalues: λ_max ≈ 852.57 , λ_min ≈ 0.18
Maximum safe learning rate: 2 / λ_max ≈ 0.00235
Condition number: 852.57 / 0.18 ≈ 4,845
A condition number of nearly five thousand means the valley is extremely elongated. Take the maximum safe learning rate, α = 0.002:
w₁ = 0 − 0.002 × (−15,142.50) = 30.285
b₁ = 0 − 0.002 × (−675.00) = 1.350
The slope overshoots — the true optimum is 20.65, not 30.29 — and the intercept has barely moved. Further iterations will correct both, but slowly, because the learning rate that is stable in the steep direction is far too small to make progress in the flat one.
Step 4 — Standardise the feature
Compute the mean and standard deviation of the feature on the training data:
x̄ = 155 / 8 = 19.375
s = √(Sxx / n)
= √(399.875 / 8)
= √49.984
≈ 7.070
Transform every temperature to x′ = (x − 19.375) / 7.070:
| x (°C) | x′ standardised | y |
|---|---|---|
| 8 | −1.609 | 120 |
| 12 | −1.043 | 180 |
| 15 | −0.619 | 240 |
| 18 | −0.194 | 300 |
| 21 | +0.230 | 360 |
| 24 | +0.654 | 430 |
| 27 | +1.078 | 510 |
| 30 | +1.503 | 560 |
| Σ | 0.000 | 2,700 |
The standardised feature has mean exactly 0 and sum of squares exactly 8, so the new Hessian is diagonal with both eigenvalues equal to 2:
H = ⎡ 2 0 ⎤ λ_max = λ_min = 2
⎣ 0 2 ⎦
Maximum safe learning rate: 2 / 2 = 1.0
Condition number: 2 / 2 = 1
The condition number is now 1 — a perfectly round bowl. We can safely use α = 0.4, which is four hundred times larger than the raw-scale safe value and still well inside the stable region.
Step 5 — Run gradient descent on the standardised data
With α = 0.4, starting from (0, 0):
| t | w̃ | b̃ | MSE | MSE − 103.87 |
|---|---|---|---|---|
| 0 | 0.00 | 0.0 | 135,325.0 | 135,221 |
| 1 | 116.79 | 270.0 | 5,513.8 | 5,409.9 |
| 2 | 140.15 | 324.0 | 320.8 | 216.9 |
| 3 | 144.83 | 334.8 | 113.0 | 9.1 |
| 4 | 146.04 | 337.0 | 104.2 | 0.4 |
| 5 | 146.13 | 337.4 | 103.9 | 0.0 |
| Exact optimum (scaled) | 103.87 | |||
Five iterations. The error column is the interesting one: it falls by a factor of roughly 25 at each step, which is exactly the convergence rate (1 − αλ)² = (1 − 0.8)² = 0.04 predicted by the theory. This is a textbook example of a well-conditioned problem.
Translating back to the original units — ŵ = w̃/s = 146.13 / 7.070 ≈ 20.67, and b̂ = b̃ − w̃ x̄/s = 337.4 − 146.13 × 2.740 ≈ −63.1 — we recover essentially the exact least-squares solution from Tutorial 3.
Step 6 — Compare the two runs honestly
| Property | Raw temperature | Standardised |
|---|---|---|
| Condition number | 4,845 | 1 |
| Largest stable learning rate | 0.0023 | 1.0 |
| Learning rate used | 0.001 | 0.4 |
| Iterations to reach MSE < 105 | ≈ 40,000 | 5 |
| Final MSE | 103.87 | 103.87 |
| Same answer, four orders of magnitude difference in effort | ||
This is the empirical case for feature scaling, and it is not a marginal effect. On the raw feature, gradient descent needs roughly forty thousand iterations to reach the precision that five iterations achieve on the scaled feature. The algorithm is identical; only the shape of the bowl has changed.
Why the number is so extreme here
The bike-share data has a small sample and a feature range of only 22 °C, so the ratio between the two Hessian eigenvalues is unusually large. Real datasets with mixed-unit features can easily have condition numbers in the thousands or tens of thousands. The effect is not a quirk of this example; it is the norm whenever features are left on their natural scales.
The same computation in code
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
df = pd.DataFrame({
"temp_c": [ 8, 12, 15, 18, 21, 24, 27, 30],
"rentals": [120, 180, 240, 300, 360, 430, 510, 560],
})
x = df["temp_c"].to_numpy()
y = df["rentals"].to_numpy()
n = len(x)
def mse(w, b, xv=x, yv=y):
return ((yv - (w * xv + b)) ** 2).mean()
def gradient(w, b, xv=x, yv=y):
e = yv - (w * xv + b)
return -2 * (xv * e).mean(), -2 * e.mean()
def descend(xv, w, b, alpha, iters=50000, tol=1e-8):
history = [(w, b, mse(w, b, xv, y))]
for _ in range(iters):
gw, gb = gradient(w, b, xv, y)
w_new, b_new = w - alpha * gw, b - alpha * gb
if abs(w_new - w) < tol and abs(b_new - b) < tol:
w, b = w_new, b_new
history.append((w, b, mse(w, b, xv, y)))
break
w, b = w_new, b_new
history.append((w, b, mse(w, b, xv, y)))
return history
# --- Run 1: raw temperature ---
raw = descend(x, 0.0, 0.0, alpha=0.001)
print(f"raw : {len(raw):>6} iterations, "
f"w = {raw[-1][0]:.4f}, b = {raw[-1][1]:.4f}, "
f"MSE = {raw[-1][2]:.4f}")
# --- Run 2: standardised temperature ---
scaler = StandardScaler()
x_std = scaler.fit_transform(x.reshape(-1, 1)).ravel()
std = descend(x_std, 0.0, 0.0, alpha=0.4)
print(f"std : {len(std):>6} iterations, "
f"w̃ = {std[-1][0]:.4f}, b̃ = {std[-1][1]:.4f}, "
f"MSE = {std[-1][2]:.4f}")
# --- Compare against the normal equation ---
X = np.column_stack([np.ones(n), x])
w_ne = np.linalg.solve(X.T @ X, X.T @ y)
print(f"normal: b = {w_ne[0]:.4f}, w = {w_ne[1]:.4f}, "
f"MSE = {mse(w_ne[1], w_ne[0]):.4f}")
# --- And against scikit-learn ---
sk = LinearRegression().fit(x.reshape(-1, 1), y)
print(f"sklearn: b = {sk.intercept_:.4f}, w = {sk.coef_[0]:.4f}")
raw : 38515 iterations, w = 20.6473, b = -62.5598, MSE = 103.8703
std : 6 iterations, w̃ = 146.1260, b̃ = 337.4986, MSE = 103.8700
normal: b = -62.5976, w = 20.6502, MSE = 103.8703
sklearn: b = -62.5976, w = 20.6502
All four routes — gradient descent on raw features, gradient descent on scaled features, the normal equation and scikit-learn — arrive at the same MSE to four decimal places. The scaled run needed six iterations; the raw run needed thirty-eight and a half thousand. Same answer, same algorithm, same data.
6 Interactive activity
Tune the optimiser
In the Tutorial 5 notebook, run the following experiments and record what you find for each. The aim is to develop a feel for how the learning rate, the scaling and the stopping criterion interact.
- Map the stability boundary. Using the raw (unscaled) feature, run gradient descent for 200 iterations with α = 0.0005, 0.001, 0.0015, 0.002, 0.0023, 0.0024 and 0.003. For each, record the final MSE. Which values converge, which oscillate, which diverge? Compare with the theoretical bound 2/λmax ≈ 0.00235.
- Count the iterations. Using α = 0.001 on the raw feature, how many iterations until the MSE is within 1% of the optimum? Within 0.1%? Within 0.01%? Is the growth linear or worse?
- Do the same on scaled data. Repeat (2) with the standardised feature and α = 0.4. Compare the iteration counts.
- Try min–max instead. Scale the temperature to [0, 1] rather than standardising. Recompute the Hessian eigenvalues and the maximum safe learning rate. Is this scaling as effective as standardisation here? Why or why not?
- Test the stopping criteria. On the scaled problem, compare how many iterations each stopping rule needs to halt: a fixed limit of 100, a gradient tolerance of 10⁻⁵, and a cost-change tolerance of 10⁻⁶ with patience 5. Which halts first? Which produces the most accurate final parameters?
- Change the starting point. Start from (w, b) = (100, 1000) instead of (0, 0) on the scaled problem. How many iterations now? Does the answer change?
Show the discussion
-
Stability boundary. α values of 0.0005 and 0.001
converge smoothly. 0.0015 converges but with visible oscillation in the early
iterations. 0.002 converges very slowly with persistent oscillation. 0.0023
oscillates indefinitely without settling — this is right at the theoretical
bound. 0.0024 and 0.003 diverge, with the cost growing until it overflows to
inforNaN. The empirical boundary matches 2/λmax ≈ 0.00235 closely. - Iteration counts on raw data. To within 1% of the optimum, several hundred iterations. To within 0.1%, a few thousand. To within 0.01%, tens of thousands. The relationship is roughly linear in the number of decimal digits, which is what exponential convergence looks like on a log scale. This is why "close enough" is usually the practical target.
- Scaled data. Every tolerance is reached in single-digit iterations. The gap between raw and scaled is three to four orders of magnitude.
- Min–max scaling. The eigenvalues become unequal because the transformed feature no longer has a symmetric distribution — it is compressed toward the low end where the data are denser. The maximum safe learning rate is slightly lower than with standardisation, and convergence takes a handful more iterations. Both are vastly better than no scaling, but standardisation has the edge here. On features with heavy tails the difference is larger.
- Stopping criteria. The cost-change rule halts first, often after around 10 iterations on the scaled problem, because the cost stops changing meaningfully once the parameters are close. The gradient rule is slower to trigger because the gradient magnitude remains small but non-zero for a while. The fixed limit is arbitrary — it halts when told to, regardless of progress. For most problems the cost-change rule with patience is the most practical, but always confirm visually that the cost curve has genuinely flattened rather than merely paused.
- Starting point. Starting from (100, 1000) on the scaled problem takes a few more iterations — because the initial distance to the optimum is larger — but the number of iterations remains in the single digits or low teens, and the final answer is identical to five decimal places. On a convex surface, the starting point affects how long you walk, never where you end up.
The rule of thumb worth remembering
When your cost curve is diverging, halve the learning rate. When it is converging but painfully slowly, first check whether your features are scaled — then consider increasing the rate. Scaling is almost always the higher-leverage fix.
7 Self-check questions
Answer each question in your own words before revealing the answer.
Question 1
Write the gradient descent update rule for both parameters of a simple linear regression, and state in one sentence what each part of the update means.
Show answer
In words: the gradient tells you which way the cost increases most steeply at your current parameters, so you step in the opposite direction. The learning rate α scales the step. Both parameters are updated simultaneously, using the gradient computed at the current point.