๐ Linear Regression
Linear regression is the one model whose optimum can be written down in closed form and verified by hand. Everything harder — logistic regression, SVMs, neural networks — replaces one piece of it and loses that closed form, so it is worth seeing exactly where the solution comes from.
The model is a weighted sum:
$$\hat{y}_i = w_0 + w_1 x_{i1} + \dots + w_p x_{ip} = \mathbf{x}_i^\top \mathbf{w}$$
and the loss is squared error:
$$\mathcal{L}(\mathbf{w}) = \sum_{i=1}^{n}(y_i - \mathbf{x}_i^\top\mathbf{w})^2 = \lVert \mathbf{y} - X\mathbf{w} \rVert^2$$
Below: the design matrix, $X^\top X$ and its inverse printed entry by entry, the normal equation solved by hand, the same answer reached again by gradient descent, then $R^2$, the residuals, and what ridge and lasso change. Every result is checked against scikit-learn.
Display helpers ready: banner, step, check, expand, show_matrix, show_matmul, show_elementwise.
1. The Data
12 EEG recordings. The target is y_reg — seizure burden in minutes per hour — and
the three features are the same ones used in the tree walkthroughs.
12 recordings x 3 features
id region line_length spike_rate age y_class y_reg
-------------------------------------------------------------------------------
rec00 frontal 12.0 0.5 24.0 0 0.2
rec01 temporal 15.0 1.2 31.0 0 0.5
rec02 temporal 42.0 6.8 58.0 1 6.4
rec03 occipital 38.0 5.1 47.0 1 4.8
rec04 frontal 11.0 0.3 19.0 0 0.1
rec05 temporal 55.0 8.2 63.0 1 9.1
rec06 occipital 40.0 1.9 35.0 1 3.5
rec07 temporal 47.0 7.4 52.0 1 7.2
rec08 frontal 14.0 0.8 28.0 0 0.3
rec09 occipital 51.0 6.1 60.0 1 6.9
rec10 frontal 45.0 2.4 41.0 0 1.6
rec11 temporal 35.0 4.7 30.0 1 4.1
-------------------------------------------------------------------------------
class balance: 5 negative, 7 positive
y_reg mean = 3.7250, std = 3.0518
2. The Design Matrix
The intercept $w_0$ is handled by adding a column of ones. After that the intercept is just another weight and the whole model is a single matrix product:
$$\hat{\mathbf{y}} = X\mathbf{w}, \qquad X \in \mathbb{R}^{12 \times 4},\;\; \mathbf{w} \in \mathbb{R}^{4}$$
design matrix X: (12, 4) (12 rows, 1 bias + 3 features)
id 1 (bias) line_length spike_rate age | y
-------------------------------------------------------------------------
rec00 1.00 12.00 0.50 24.00 | 0.20
rec01 1.00 15.00 1.20 31.00 | 0.50
rec02 1.00 42.00 6.80 58.00 | 6.40
rec03 1.00 38.00 5.10 47.00 | 4.80
rec04 1.00 11.00 0.30 19.00 | 0.10
rec05 1.00 55.00 8.20 63.00 | 9.10
rec06 1.00 40.00 1.90 35.00 | 3.50
rec07 1.00 47.00 7.40 52.00 | 7.20
rec08 1.00 14.00 0.80 28.00 | 0.30
rec09 1.00 51.00 6.10 60.00 | 6.90
rec10 1.00 45.00 2.40 41.00 | 1.60
rec11 1.00 35.00 4.70 30.00 | 4.10
-------------------------------------------------------------------------
the model has 4 unknowns and 12 equations - overdetermined,
so in general no w makes every residual zero. Least squares finds the w that
makes them as small as possible in the squared sense.
3. The Normal Equation
Write the loss in matrix form and differentiate:
$$\mathcal{L}(\mathbf{w}) = (\mathbf{y} - X\mathbf{w})^\top(\mathbf{y} - X\mathbf{w}) = \mathbf{y}^\top\mathbf{y} - 2\mathbf{w}^\top X^\top \mathbf{y} + \mathbf{w}^\top X^\top X \mathbf{w}$$
$$\frac{\partial \mathcal{L}}{\partial \mathbf{w}} = -2X^\top\mathbf{y} + 2X^\top X\mathbf{w} = 0$$
$$\boxed{\;\mathbf{w}^{*} = (X^\top X)^{-1} X^\top \mathbf{y}\;}$$
The loss is a convex quadratic, so this stationary point is the global minimum — there are no local minima to worry about and no learning rate to tune.
The geometry is worth holding onto: $X\mathbf{w}$ can only ever land in the column space of $X$. Least squares picks the point in that subspace closest to $\mathbf{y}$, which makes the residual orthogonal to every column of $X$. That orthogonality is checked numerically below.
STEP 1 : X^T X
==============================================================================
==============================================================================
X^T (4, 12) @ X (12, 4) = X^T X (4, 4)
==============================================================================
shape rule: (4 x 12) @ (12 x 4) -> (4 x 4) inner dim 12 cancels
(4,12) @ (12,4) -> (4,4): a small square matrix, whatever n is
==============================================================================
X^T (4, 12) =
0 | +1.000 +1.000 +1.000 +1.000 +1.000 +1.000 +1.000 +1.000 +1.000 +1.000 +1.000 +1.000
1 | +12.000 +15.000 +42.000 +38.000 +11.000 +55.000 +40.000 +47.000 +14.000 +51.000 +45.000 +35.000
2 | +0.500 +1.200 +6.800 +5.100 +0.300 +8.200 +1.900 +7.400 +0.800 +6.100 +2.400 +4.700
3 | +24.000 +31.000 +58.000 +47.000 +19.000 +63.000 +35.000 +52.000 +28.000 +60.000 +41.000 +30.000
X (12, 4) =
0 | +1.000 +12.000 +0.500 +24.000
1 | +1.000 +15.000 +1.200 +31.000
2 | +1.000 +42.000 +6.800 +58.000
3 | +1.000 +38.000 +5.100 +47.000
4 | +1.000 +11.000 +0.300 +19.000
5 | +1.000 +55.000 +8.200 +63.000
6 | +1.000 +40.000 +1.900 +35.000
7 | +1.000 +47.000 +7.400 +52.000
8 | +1.000 +14.000 +0.800 +28.000
9 | +1.000 +51.000 +6.100 +60.000
10 | +1.000 +45.000 +2.400 +41.000
11 | +1.000 +35.000 +4.700 +30.000
X^T X (4, 4) =
0 | +12.000 +405.000 +45.400 +488.000
1 | +405.000 +16579.000 +1976.300 +18840.000
2 | +45.400 +1976.300 +265.340 +2284.700
3 | +488.000 +18840.000 +2284.700 +22334.000
worked element X^T X[0,0] = row 0 of X^T . column 0 of X
row 0 of X^T = [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
col 0 of X = [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
(+1.0000)*(+1.0000) + (+1.0000)*(+1.0000) + (+1.0000)*(+1.0000) + (+1.0000)*(+1.0000) +
(+1.0000)*(+1.0000) + (+1.0000)*(+1.0000) + (+1.0000)*(+1.0000) + (+1.0000)*(+1.0000) +
(+1.0000)*(+1.0000) + (+1.0000)*(+1.0000) + (+1.0000)*(+1.0000) + (+1.0000)*(+1.0000)
= +12.000000 | from array: +12.000000 | MATCH: True
STEP 2 : X^T y
==============================================================================
X^T y (1, 4) =
1 (bias)line_lengthspike_rate age
0 | +44.700 +2012.700 +268.720 +2300.200
element 0 = sum of y = 44.7000
element 1 = sum of x1 * y = 2012.7000
check against the array : True
STEP 3 : invert X^T X
==============================================================================
(X^T X)^-1 (4, 4) =
1 (bias)line_lengthspike_rate age
0 | +1.447993 -0.000838 +0.208314 -0.052242
1 | -0.000838 +0.001639 -0.002661 -0.001092
2 | +0.208314 -0.002661 +0.065530 -0.009011
3 | -0.052242 -0.001092 -0.009011 +0.003029
X^T X @ (X^T X)^-1 (should be the identity) (4, 4) =
0 | +1.00000 +0.00000 +0.00000 -0.00000
1 | +0.00000 +1.00000 +0.00000 -0.00000
2 | -0.00000 +0.00000 +1.00000 -0.00000
3 | +0.00000 -0.00000 -0.00000 +1.00000
is it the identity? True
condition number of X^T X = 57,391.9
A large condition number means the inverse amplifies small changes in the
data into large changes in w - that is multicollinearity, and it is exactly
what ridge regression is built to control.
STEP 4 : w* = (X^T X)^-1 X^T y
==============================================================================
==============================================================================
(X^T X)^-1 (4, 4) @ X^T y (4, 1) = w* (4, 1)
==============================================================================
shape rule: (4 x 4) @ (4 x 1) -> (4 x 1) inner dim 4 cancels
==============================================================================
(X^T X)^-1 (4, 4) =
0 | +1.448 -0.001 +0.208 -0.052
1 | -0.001 +0.002 -0.003 -0.001
2 | +0.208 -0.003 +0.066 -0.009
3 | -0.052 -0.001 -0.009 +0.003
X^T y (4, 1) =
0 | +44.700
1 | +2012.700
2 | +268.720
3 | +2300.200
w* (4, 1) =
0 | -1.150
1 | +0.034
2 | +0.839
3 | +0.013
worked element w*[0,0] = row 0 of (X^T X)^-1 . column 0 of X^T y
row 0 of (X^T X)^-1 = [ 1.448 -0.0008 0.2083 -0.0522]
col 0 of X^T y = [ 44.7 2012.7 268.72 2300.2 ]
(+1.4480)*(+44.7000) + (-0.0008)*(+2012.7000) + (+0.2083)*(+268.7200) + (-0.0522)*(+2300.2000)
= -1.149744 | from array: -1.149744 | MATCH: True
fitted coefficients
term weight
------------------------------
1 (bias) -1.149744
line_length 0.034321
spike_rate 0.839160
age 0.013318
------------------------------
the fitted model:
y_hat = -1.1497 +0.0343*line_length + +0.8392*spike_rate + +0.0133*age
Reading a coefficient: holding the other features fixed, one extra unit of
spike_rate is associated with +0.8392 more minutes of seizure burden.
'Associated with', not 'causes' - this is a correlational fit.
The residuals are orthogonal to the features
If the fit is truly optimal, no column of $X$ can explain any of what is left over:
$$X^\top(\mathbf{y} - X\mathbf{w}^{*}) = \mathbf{0}$$
Any non-zero entry here would mean some feature still carries unused signal — and the loss could be reduced further, contradicting optimality.
id y y_hat residual -------------------------------------- rec00 0.20 0.0013 0.1987 rec01 0.50 0.7849 -0.2849 rec02 6.40 6.7705 -0.3705 rec03 4.80 5.0601 -0.2601 rec04 0.10 -0.2674 0.3674 rec05 9.10 8.4580 0.6420 rec06 3.50 2.2836 1.2164 rec07 7.20 7.3657 -0.1657 rec08 0.30 0.3750 -0.0750 rec09 6.90 6.5186 0.3814 rec10 1.60 2.9547 -1.3547 rec11 4.10 4.3951 -0.2951 -------------------------------------- SUM 44.70 44.7000 0.000000 X^T (y - Xw) = [0. 0. 0. 0.] all zero (to numerical precision)? True the first entry is the sum of residuals - it is zero because of the bias column, which is why a model WITH an intercept always has residuals summing to zero: 8.76e-13
4. How Good Is the Fit?
$$R^2 = 1 - \frac{\sum_i (y_i - \hat y_i)^2}{\sum_i (y_i - \bar y)^2} = 1 - \frac{SS_{\text{res}}}{SS_{\text{tot}}}$$
$R^2$ compares the model against the simplest possible baseline: always predicting the mean. $R^2 = 0$ means the model is no better than that; $R^2 = 1$ means it fits perfectly.
A warning that matters here: $R^2$ never decreases when you add a feature, even a random one, because the extra column can only ever be used or ignored. Adjusted $R^2$ penalises that.
SS_res = sum of squared residuals = 4.453102
SS_tot = sum of squared deviations = 111.762500
(from the mean y = 3.7250)
R^2 = 1 - 4.4531 / 111.7625 = 0.960156
adj. R^2 = 1 - (1 - 0.9602) * (12-1)/(12-3-1) = 0.945214
RMSE = sqrt(4.4531 / 12) = 0.609173 minutes/hour
MAE = 0.467647
for scale: std(y) = 3.0518, so the model's typical error is
20.0% of the natural spread in the target.
adding a column of PURE NOISE, to show R^2 cannot fall:
+1 random column(s): R^2 = 0.965113 adj R^2 = 0.945177
+3 random column(s): R^2 = 0.982147 adj R^2 = 0.960723
+6 random column(s): R^2 = 0.988970 adj R^2 = 0.939337
R^2 climbs toward 1 with meaningless columns; adjusted R^2 turns back.
Middle panel: any visible pattern (a curve, a funnel) would mean the linear form is wrong or the variance is not constant. With 12 points there is not enough data to judge that seriously. Right panel: these coefficients are in RAW units, so their sizes are not comparable - age runs to 60 while spike_rate runs to 8. Comparing importance requires standardising the features first.
5. How Learning Actually Happens
The closed form gave the answer in one step, which is convenient but hides the process. Every other model in this series — logistic regression, SVMs, neural networks — has to learn its weights by repeating one loop:
predict → measure the error → find which direction reduces it → take a small step → repeat
This section runs that loop with every number written out, so the mechanism is visible rather than asserted. To keep the arithmetic small enough to follow, it uses one feature plus a bias — two parameters, which also means the loss surface can be drawn.
The three pieces
1. The loss — how wrong we are right now (mean squared error):
$$\mathcal{L}(w_0, w_1) = \frac{1}{n}\sum_{i=1}^{n}\big(y_i - (w_0 + w_1 x_i)\big)^2$$
2. The derivatives — how the loss changes if each weight nudges up. Using the chain rule on $(y_i - w_0 - w_1x_i)^2$:
$$\frac{\partial\mathcal{L}}{\partial w_0} = \frac{2}{n}\sum_i \big(\hat{y}_i - y_i\big) \qquad\qquad \frac{\partial\mathcal{L}}{\partial w_1} = \frac{2}{n}\sum_i \big(\hat{y}_i - y_i\big)\,x_i$$
Read them directly: $\partial\mathcal{L}/\partial w_0$ is (twice) the average error — if predictions are too low overall, raise the intercept. $\partial\mathcal{L}/\partial w_1$ is (twice) the average error weighted by $x$ — if the errors are worse where $x$ is large, change the slope.
3. The update — step against the gradient, because the gradient points uphill:
$$w \leftarrow w - \eta\,\frac{\partial\mathcal{L}}{\partial w}$$
model : y_hat = w0 + w1 * spike_rate start at : w0 = 0.0, w1 = 0.0 (predicting 0 for every recording) learning rate eta = 0.02 loss at the start = 23.189167 for reference, the closed-form optimum for this 1-feature model is: w0* = -0.302061, w1* = +1.064421, loss* = 0.478401
Four epochs, every number
Each block below is one full pass: the current weights, what they predict for all 12 recordings, the error on each, the loss, both derivatives summed term by term, and the resulting update.
==============================================================================
EPOCH 1 w0 = +0.000000 w1 = +0.000000
==============================================================================
STEP 1 - predict, and measure the error
id x y y_hat = w0 + w1*x error (y_hat - y) error^2
--------------------------------------------------------------------------
rec00 0.5 0.20 +0.0000 + +0.0000*0.5 = 0.0000 -0.2000 0.0400
rec01 1.2 0.50 +0.0000 + +0.0000*1.2 = 0.0000 -0.5000 0.2500
rec02 6.8 6.40 +0.0000 + +0.0000*6.8 = 0.0000 -6.4000 40.9600
rec03 5.1 4.80 +0.0000 + +0.0000*5.1 = 0.0000 -4.8000 23.0400
rec04 0.3 0.10 +0.0000 + +0.0000*0.3 = 0.0000 -0.1000 0.0100
rec05 8.2 9.10 +0.0000 + +0.0000*8.2 = 0.0000 -9.1000 82.8100
rec06 1.9 3.50 +0.0000 + +0.0000*1.9 = 0.0000 -3.5000 12.2500
rec07 7.4 7.20 +0.0000 + +0.0000*7.4 = 0.0000 -7.2000 51.8400
rec08 0.8 0.30 +0.0000 + +0.0000*0.8 = 0.0000 -0.3000 0.0900
rec09 6.1 6.90 +0.0000 + +0.0000*6.1 = 0.0000 -6.9000 47.6100
rec10 2.4 1.60 +0.0000 + +0.0000*2.4 = 0.0000 -1.6000 2.5600
rec11 4.7 4.10 +0.0000 + +0.0000*4.7 = 0.0000 -4.1000 16.8100
--------------------------------------------------------------------------
loss = mean of the last column = 278.2700 / 12 = 23.189167
STEP 2 - the two derivatives
dL/dw0 = (2/n) * sum(error)
= (2/12) * (-0.200 + -0.500 + -6.400 + -4.800 + -0.100 + -9.100 + ...)
= (2/12) * -44.700000
= -7.450000
dL/dw1 = (2/n) * sum(error * x)
= (2/12) * (-0.10 + -0.60 + -43.52 + -24.48 + -0.03 + -74.62 + ...)
= (2/12) * -268.720000
= -44.786667
both negative -> the loss falls if we INCREASE both weights
STEP 3 - step against the gradient
w0 <- +0.000000 - 0.02 * (-7.450000) = +0.000000 +0.149000 = +0.149000
w1 <- +0.000000 - 0.02 * (-44.786667) = +0.000000 +0.895733 = +0.895733
loss: 23.189167 -> 0.735322 (DOWN by 22.453845)
==============================================================================
EPOCH 2 w0 = +0.149000 w1 = +0.895733
==============================================================================
STEP 1 - predict, and measure the error
id x y y_hat = w0 + w1*x error (y_hat - y) error^2
--------------------------------------------------------------------------
rec00 0.5 0.20 +0.1490 + +0.8957*0.5 = 0.5969 0.3969 0.1575
rec01 1.2 0.50 +0.1490 + +0.8957*1.2 = 1.2239 0.7239 0.5240
rec02 6.8 6.40 +0.1490 + +0.8957*6.8 = 6.2400 -0.1600 0.0256
rec03 5.1 4.80 +0.1490 + +0.8957*5.1 = 4.7172 -0.0828 0.0068
rec04 0.3 0.10 +0.1490 + +0.8957*0.3 = 0.4177 0.3177 0.1009
rec05 8.2 9.10 +0.1490 + +0.8957*8.2 = 7.4940 -1.6060 2.5792
rec06 1.9 3.50 +0.1490 + +0.8957*1.9 = 1.8509 -1.6491 2.7196
rec07 7.4 7.20 +0.1490 + +0.8957*7.4 = 6.7774 -0.4226 0.1786
rec08 0.8 0.30 +0.1490 + +0.8957*0.8 = 0.8656 0.5656 0.3199
rec09 6.1 6.90 +0.1490 + +0.8957*6.1 = 5.6130 -1.2870 1.6564
rec10 2.4 1.60 +0.1490 + +0.8957*2.4 = 2.2988 0.6988 0.4883
rec11 4.7 4.10 +0.1490 + +0.8957*4.7 = 4.3589 0.2589 0.0671
--------------------------------------------------------------------------
loss = mean of the last column = 8.8239 / 12 = 0.735322
STEP 2 - the two derivatives
dL/dw0 = (2/n) * sum(error)
= (2/12) * (+0.397 + +0.724 + -0.160 + -0.083 + +0.318 + -1.606 + ...)
= (2/12) * -2.245707
= -0.374284
dL/dw1 = (2/n) * sum(error * x)
= (2/12) * (+0.20 + +0.87 + -1.09 + -0.42 + +0.10 + -13.17 + ...)
= (2/12) * -24.281517
= -4.046920
both negative -> the loss falls if we INCREASE both weights
STEP 3 - step against the gradient
w0 <- +0.149000 - 0.02 * (-0.374284) = +0.149000 +0.007486 = +0.156486
w1 <- +0.895733 - 0.02 * (-4.046920) = +0.895733 +0.080938 = +0.976672
loss: 0.735322 -> 0.554464 (DOWN by 0.180858)
==============================================================================
EPOCH 3 w0 = +0.156486 w1 = +0.976672
==============================================================================
STEP 1 - predict, and measure the error
id x y y_hat = w0 + w1*x error (y_hat - y) error^2
--------------------------------------------------------------------------
rec00 0.5 0.20 +0.1565 + +0.9767*0.5 = 0.6448 0.4448 0.1979
rec01 1.2 0.50 +0.1565 + +0.9767*1.2 = 1.3285 0.8285 0.6864
rec02 6.8 6.40 +0.1565 + +0.9767*6.8 = 6.7979 0.3979 0.1583
rec03 5.1 4.80 +0.1565 + +0.9767*5.1 = 5.1375 0.3375 0.1139
rec04 0.3 0.10 +0.1565 + +0.9767*0.3 = 0.4495 0.3495 0.1221
rec05 8.2 9.10 +0.1565 + +0.9767*8.2 = 8.1652 -0.9348 0.8739
rec06 1.9 3.50 +0.1565 + +0.9767*1.9 = 2.0122 -1.4878 2.2137
rec07 7.4 7.20 +0.1565 + +0.9767*7.4 = 7.3839 0.1839 0.0338
rec08 0.8 0.30 +0.1565 + +0.9767*0.8 = 0.9378 0.6378 0.4068
rec09 6.1 6.90 +0.1565 + +0.9767*6.1 = 6.1142 -0.7858 0.6175
rec10 2.4 1.60 +0.1565 + +0.9767*2.4 = 2.5005 0.9005 0.8109
rec11 4.7 4.10 +0.1565 + +0.9767*4.7 = 4.7468 0.6468 0.4184
--------------------------------------------------------------------------
loss = mean of the last column = 6.6536 / 12 = 0.554464
STEP 2 - the two derivatives
dL/dw0 = (2/n) * sum(error)
= (2/12) * (+0.445 + +0.828 + +0.398 + +0.338 + +0.349 + -0.935 + ...)
= (2/12) * +1.518725
= +0.253121
dL/dw1 = (2/n) * sum(error * x)
= (2/12) * (+0.22 + +0.99 + +2.71 + +1.72 + +0.10 + -7.67 + ...)
= (2/12) * -2.465474
= -0.410912
STEP 3 - step against the gradient
w0 <- +0.156486 - 0.02 * (+0.253121) = +0.156486 -0.005062 = +0.151423
w1 <- +0.976672 - 0.02 * (-0.410912) = +0.976672 +0.008218 = +0.984890
loss: 0.554464 -> 0.551009 (DOWN by 0.003454)
==============================================================================
EPOCH 4 w0 = +0.151423 w1 = +0.984890
==============================================================================
STEP 1 - predict, and measure the error
id x y y_hat = w0 + w1*x error (y_hat - y) error^2
--------------------------------------------------------------------------
rec00 0.5 0.20 +0.1514 + +0.9849*0.5 = 0.6439 0.4439 0.1970
rec01 1.2 0.50 +0.1514 + +0.9849*1.2 = 1.3333 0.8333 0.6944
rec02 6.8 6.40 +0.1514 + +0.9849*6.8 = 6.8487 0.4487 0.2013
rec03 5.1 4.80 +0.1514 + +0.9849*5.1 = 5.1744 0.3744 0.1401
rec04 0.3 0.10 +0.1514 + +0.9849*0.3 = 0.4469 0.3469 0.1203
rec05 8.2 9.10 +0.1514 + +0.9849*8.2 = 8.2275 -0.8725 0.7612
rec06 1.9 3.50 +0.1514 + +0.9849*1.9 = 2.0227 -1.4773 2.1824
rec07 7.4 7.20 +0.1514 + +0.9849*7.4 = 7.4396 0.2396 0.0574
rec08 0.8 0.30 +0.1514 + +0.9849*0.8 = 0.9393 0.6393 0.4087
rec09 6.1 6.90 +0.1514 + +0.9849*6.1 = 6.1593 -0.7407 0.5487
rec10 2.4 1.60 +0.1514 + +0.9849*2.4 = 2.5152 0.9152 0.8375
rec11 4.7 4.10 +0.1514 + +0.9849*4.7 = 4.7804 0.6804 0.4630
--------------------------------------------------------------------------
loss = mean of the last column = 6.6121 / 12 = 0.551009
STEP 2 - the two derivatives
dL/dw0 = (2/n) * sum(error)
= (2/12) * (+0.444 + +0.833 + +0.449 + +0.374 + +0.347 + -0.872 + ...)
= (2/12) * +1.831084
= +0.305181
dL/dw1 = (2/n) * sum(error * x)
= (2/12) * (+0.22 + +1.00 + +3.05 + +1.91 + +0.10 + -7.15 + ...)
= (2/12) * -0.514678
= -0.085780
STEP 3 - step against the gradient
w0 <- +0.151423 - 0.02 * (+0.305181) = +0.151423 -0.006104 = +0.145320
w1 <- +0.984890 - 0.02 * (-0.085780) = +0.984890 +0.001716 = +0.986606
loss: 0.551009 -> 0.549023 (DOWN by 0.001987)
The same loop, continued to convergence
The gradient shrinks as the weights approach the optimum — at the minimum it is exactly zero, which is the same condition the normal equation solved directly in section 3.
epoch w0 w1 loss dL/dw0 dL/dw1 |gradient|
----------------------------------------------------------------------------------
0 0.000000 0.000000 23.189167 -7.450000 -44.786667 4.540e+01
1 0.149000 0.895733 0.735322 -0.374284 -4.046920 4.064e+00
2 0.156486 0.976672 0.554464 0.253121 -0.410912 4.826e-01
3 0.151423 0.984890 0.551009 0.305181 -0.085780 3.170e-01
4 0.145320 0.986606 0.549023 0.305955 -0.056094 3.111e-01
5 0.139201 0.987727 0.547101 0.302206 -0.052782 3.068e-01
10 0.109793 0.992844 0.538249 0.282106 -0.049028 2.863e-01
25 0.032815 1.006222 0.517968 0.229378 -0.039864 2.328e-01
50 -0.064860 1.023197 0.498253 0.162475 -0.028237 1.649e-01
100 -0.183051 1.043738 0.483398 0.081518 -0.014167 8.274e-02
250 -0.287030 1.061809 0.478481 0.010296 -0.001789 1.045e-02
500 -0.301583 1.064338 0.478401 0.000327 -0.000057 3.322e-04
1000 -0.302060 1.064421 0.478401 0.000000 -0.000000 3.358e-07
2000 -0.302061 1.064421 0.478401 0.000000 -0.000000 3.431e-13
3000 -0.302061 1.064421 0.478401 0.000000 -0.000000 5.488e-15
----------------------------------------------------------------------------------
exact -0.302061 1.064421 0.478401 0 0 0
distance from the closed-form answer: 3.017e-15
MATCH with the normal equation: True
The gradient went from 45.40 to 5.49e-15. That is what 'converged'
means: there is no longer a direction that reduces the loss.
Middle panel: the path runs perpendicular to the contour lines - that IS the gradient, which always points along the steepest direction. The steps shorten near the optimum because the gradient itself is shrinking.
What the learning rate does
$\eta$ scales every step. The three failure modes are all visible on the same surface:
- too small — correct direction, but it crawls,
- about right — steady approach,
- too large — each step overshoots to a point further up the far wall, and the loss increases.
For squared error the boundary is knowable: descent is stable while $\eta < 2/\lambda_{\max}$, with $\lambda_{\max}$ the largest eigenvalue of the Hessian $\frac{2}{n}X^\top X$.
Hessian eigenvalues : [ 0.685 45.5384]
largest : 45.538368
stable while eta < 2 / 45.538368 = 0.04391901
eta vs limit loss @10 loss @100 loss @1000 outcome
----------------------------------------------------------------------------------
0.0005 0.01x 14.8346 0.7781 0.5182 still going
0.0050 0.11x 0.6810 0.5181 0.4785 converged
0.0200 0.46x 0.5382 0.4834 0.4784 converged
0.0430 0.98x 10.1465 0.4830 0.4784 converged
0.0480 1.09x 684.8322 inf inf DIVERGED
----------------------------------------------------------------------------------
The crimson run uses eta = 0.048, just above the limit of 0.04392,
and its loss grows without bound. Nothing is wrong with the gradient -
the direction is right every single step. The step length is the problem.
Batch, stochastic, and mini-batch
Every update above used all 12 recordings. Two cheaper variants trade accuracy per step for far more steps:
| Variant | Rows per update | Character |
|---|---|---|
| Batch | all $n$ | smooth, exact direction, expensive |
| Stochastic (SGD) | 1 | very noisy, very cheap |
| Mini-batch | 8–512 | the practical compromise |
The noise is not purely a cost. On a non-convex surface it can shake the parameters out of a poor basin — which is why essentially all deep learning uses mini-batches.
variant rows/update updates final loss final w
----------------------------------------------------------------------------------
batch 12 60 0.493467 [-0.0954 1.0285]
mini-batch (4) 4 180 0.513319 [-0.2844 1.0217]
stochastic 1 720 0.752538 [-0.2737 0.9483]
----------------------------------------------------------------------------------
exact - - 0.478401 [-0.3021 1.0644]
SGD makes 12 updates per epoch instead of 1, so it drops fastest at the
start - but it never settles, because each step follows a single recording
rather than the average. That permanent jitter is why real training
schedules decay the learning rate over time.
6. Scaling, and the Multi-Feature Case
The step-by-step trace used one feature. With all three, one practical detail dominates:
feature scaling. age and spike_rate differ by an order of magnitude, which
stretches the loss surface into a long narrow valley and makes gradient descent zig-zag
across it instead of down it.
$$\nabla \mathcal{L} = -2X^\top(\mathbf{y} - X\mathbf{w}), \qquad \mathbf{w} \leftarrow \mathbf{w} - \eta \, \nabla\mathcal{L}$$
gradient descent on STANDARDISED features (eta = 0.05, 2000 steps)
term grad. descent closed form diff
----------------------------------------------------------
1 (bias) 3.725000 3.725000 1.78e-15
line_length 0.534488 0.534488 8.20e-09
spike_rate 2.343354 2.343354 2.11e-08
age 0.191786 0.191786 2.89e-08
----------------------------------------------------------
MATCH with the closed form: True
final SS_res: gradient descent 4.453102 vs closed form 4.453102
same thing on RAW features - it needs eta = 1e-5 to stay stable,
and after the same 2000 steps it has still not converged:
SS_res raw-feature GD : 28.5471
SS_res optimum : 4.4531
still 6.4x worse than optimal
The dashed curve on the right is past the stability limit and climbs away. For least squares that limit is knowable: eta < 2 / largest eigenvalue of (2/n) X^T X. largest eigenvalue = 5.5205 -> stable for eta < 0.3623
7. Ridge and Lasso
When features are correlated, $X^\top X$ is near-singular, its inverse blows up, and the fitted weights become large and unstable. Both remedies add a penalty on the size of $\mathbf{w}$:
$$\text{Ridge (L2):}\quad \lVert \mathbf{y} - X\mathbf{w}\rVert^2 + \alpha\lVert\mathbf{w}\rVert_2^2 \qquad\Longrightarrow\qquad \mathbf{w} = (X^\top X + \alpha I)^{-1}X^\top\mathbf{y}$$
$$\text{Lasso (L1):}\quad \lVert \mathbf{y} - X\mathbf{w}\rVert^2 + \alpha\lVert\mathbf{w}\rVert_1 \qquad\Longrightarrow\qquad \text{no closed form}$$
Ridge stays solvable in closed form — adding $\alpha I$ to the diagonal makes the matrix invertible again. Lasso does not, because $|w|$ is not differentiable at zero. That kink is precisely what drives coefficients to exactly zero, making lasso a feature selector while ridge only shrinks.
ridge, computed by hand from (X^T X + alpha I)^-1 X^T y
(the bias column is left unpenalised, as sklearn does)
alpha line_length spike_rate age ||w|| R^2
--------------------------------------------------------------------------
0.0 0.53449 2.34335 0.19179 2.4112 0.96016
0.1 0.54538 2.24472 0.26955 2.3257 0.95998
1.0 0.63211 1.74336 0.60086 1.9493 0.95355
10.0 0.68444 0.94379 0.72615 1.3735 0.88587
100.0 0.24061 0.27221 0.24989 0.4409 0.40543
--------------------------------------------------------------------------
our ridge (alpha=1) : [0.6321 1.7434 0.6009]
sklearn Ridge : [0.6321 1.7434 0.6009]
MATCH: True
alpha ridge: # exactly 0 lasso: # exactly 0
------------------------------------------------------
0.1 0 0
1.0 0 1
5.0 0 3
20.0 0 3
------------------------------------------------------
Ridge never reaches zero exactly; lasso does, and that is the whole
difference in one table.
8. Checking Against scikit-learn
term ours sklearn diff
--------------------------------------------------------
intercept -1.14974437 -1.14974437 3.55e-15
line_length 0.03432127 0.03432127 6.94e-18
spike_rate 0.83915992 0.83915992 1.03e-14
age 0.01331754 0.01331754 2.66e-15
--------------------------------------------------------
coefficients MATCH : True
predictions MATCH : True
R^2 ours 0.96015567 sklearn 0.96015567 MATCH: True
vs np.linalg.lstsq (QR-based, more stable than inverting): True
In production never form (X^T X)^-1 explicitly - solve the system instead.
The inverse was written out here only because the point was to show it.
9. Summary
| Stage | Formula | Value here |
|---|---|---|
| Model | $\hat{\mathbf{y}} = X\mathbf{w}$ | 4 weights (bias + 3) |
| Loss | $\lVert\mathbf{y} - X\mathbf{w}\rVert^2$ | convex quadratic |
| Solution | $\mathbf{w}^{*} = (X^\top X)^{-1}X^\top\mathbf{y}$ | closed form, one step |
| Optimality | $X^\top(\mathbf{y} - X\mathbf{w}) = \mathbf{0}$ | verified to $10^{-9}$ |
| Fit | $R^2 = 1 - SS_{res}/SS_{tot}$ | see output above |
| Ridge | $(X^\top X + \alpha I)^{-1}X^\top\mathbf{y}$ | shrinks, never zeroes |
| Lasso | L1 penalty, iterative | zeroes coefficients |
The one idea: least squares is a projection. $X\mathbf{w}$ can only reach the column space of $X$, so the best it can do is the closest point in that subspace to $\mathbf{y}$ — which is why the residual comes out perpendicular to every feature.
What is deliberately left out: standard errors, $t$- and $p$-values, confidence intervals, the Gauss-Markov assumptions and what breaks when they fail, heteroscedasticity, weighted least squares, polynomial and spline bases, and elastic net.
What comes next: logistic regression keeps this exact linear form, $\mathbf{x}^\top\mathbf{w}$, and only changes what happens to it afterwards — a sigmoid instead of the identity, log-loss instead of squared error. That single change destroys the closed form and forces the iterative approach shown in section 5.