09 · Linear Regression as a Math Example¶
This module ties together every tool from Level 1 — vectors, dot products, matrix multiplication, derivatives, partial derivatives, and gradients — in one real algorithm: linear regression, both its cost function and its closed-form solution.
The model¶
For a single feature \(x\), the model predicts
(\(w\) = weight/slope, \(b\) = bias/intercept — the same line from Module 5, now wearing ML terminology). With \(n\) training examples \((x_i, y_i)\), we want \(w\) and \(b\) that make \(\hat{y}_i\) close to \(y_i\) for every \(i\).
The cost function¶
We measure "closeness" with mean squared error (MSE):
This is a function of two variables, \(w\) and \(b\) — exactly the setting Module 8 built tools for. \(J\) is a sum of squared terms, each of which is a composite function (an "outer" square applied to an "inner" linear expression) — exactly the chain-rule setting from Module 7.
Deriving the gradient of \(J\) by hand¶
Treat each summand \((wx_i+b-y_i)^2\) as \(u_i^2\) where \(u_i = wx_i+b-y_i\). By the chain rule, \(\frac{\partial}{\partial w}\left[u_i^2\right] = 2u_i \cdot \frac{\partial u_i}{\partial w}\). Since \(u_i = wx_i+b-y_i\), we have \(\frac{\partial u_i}{\partial w} = x_i\) (treating \(b, y_i\) as constants w.r.t. \(w\)) and \(\frac{\partial u_i}{\partial b} = 1\).
So the gradient is
This is precisely the "capstone formula" we will apply numerically in Module 10, and derive again for the general multi-feature case in Level 2.
The closed-form solution¶
Because \(J\) is quadratic in \((w,b)\) (a bowl shape, per Module 5), it has a single global minimum, found by setting both partial derivatives to zero and solving. For a single feature, this gives the classic formulas (derivation skipped here — it's straightforward algebra on the two equations above set to zero):
where \(\bar{x}, \bar{y}\) are the means of \(x\) and \(y\). This is the "least-squares" formula — no iteration needed, unlike gradient descent (Level 2), because the problem is simple enough to solve directly.
In matrix form, with \(\mathbf{X}\) the design matrix (a column of features plus a column of 1s for the bias) and \(\mathbf{w}\) the stacked \([w, b]^T\), the same solution is the normal equation:
— note the \(\mathbf{X}^T\mathbf{X}\) term, exactly the transpose-then-multiply pattern introduced in Module 4.
Worked numeric example¶
Using our running example points \((1,2), (2,3), (3,5)\) from Module 1:
\(\bar{x} = \frac{1+2+3}{3}=2\), \(\bar{y}=\frac{2+3+5}{3}=\frac{10}{3}\approx3.333\).
So the best-fit line is \(\hat{y} = 1.5x + 0.333\).
import numpy as np
x = np.array([1.0, 2.0, 3.0])
y = np.array([2.0, 3.0, 5.0])
# closed-form via means (matches hand derivation)
x_bar, y_bar = x.mean(), y.mean()
w_star = np.sum((x - x_bar) * (y - y_bar)) / np.sum((x - x_bar) ** 2)
b_star = y_bar - w_star * x_bar
print("w* =", round(w_star, 3), " b* =", round(b_star, 3))
# cross-check via the normal equation in matrix form
X = np.column_stack([x, np.ones_like(x)]) # design matrix [x, 1]
w_matrix = np.linalg.inv(X.T @ X) @ X.T @ y
print("normal-equation [w, b]:", w_matrix)
# cross-check the gradient at (w*, b*) should be ~[0, 0] -- the minimum
def grad(w, b):
pred = w * x + b
dJ_dw = np.mean(2 * x * (pred - y))
dJ_db = np.mean(2 * (pred - y))
return np.array([dJ_dw, dJ_db])
print("gradient at optimum:", grad(w_star, b_star))
Expected output (matches hand computation; gradient at the optimum should be essentially zero, confirming \((w^*,b^*)\) really is where \(J\) is flat):
How It Actually Works¶
The closed-form solution \(\boldsymbol{\beta} = (X^TX)^{-1}X^Ty\) is
mathematically correct but is almost never computed that way in real
numerical software — for two computational reasons. First, forming
\(X^TX\) and inverting it explicitly squares the condition number of the
problem: if \(X\) has condition number \(\kappa\), then \(X^TX\) has condition
number \(\kappa^2\), which means floating-point rounding error in the
solution gets amplified quadratically compared to working with \(X\)
directly. If \(X\)'s columns are even mildly correlated (a common real-world
case — this is literally why Level 3 has a whole module on
regularization), \(X^TX\) can become numerically close to singular, and
inv() on it returns a wildly inaccurate result even though it doesn't
throw an error.
Second, computing a matrix inverse is wasteful: LAPACK's actual solvers
(and what np.linalg.lstsq calls) instead compute a QR decomposition
\(X = QR\) (\(Q\) orthogonal, \(R\) upper triangular) and solve
\(R\boldsymbol{\beta} = Q^Ty\) by back-substitution, or use the SVD
\(X = U\Sigma V^T\) and solve via \(\boldsymbol{\beta} = V\Sigma^{+}U^Ty\)
(Level 4's SVD module covers this pseudo-inverse construction). Both avoid
ever forming \(X^TX\), keep the effective condition number at \(\kappa\) instead
of \(\kappa^2\), and are what scikit-learn's LinearRegression actually
calls under the hood — the formula you derived by hand is exact algebra,
but production code solves the same problem through a numerically
safer route.
Exercise¶
Using the points \((1,1), (2,2), (3,2), (4,3)\):
- Compute \(\bar{x}\) and \(\bar{y}\) by hand.
- Use the closed-form formulas above to compute \(w^*\) and \(b^*\) by hand.
- Confirm your answer with the NumPy
w_star/b_starcode above (swap in the new data). - Compute the gradient \(\nabla J\) at your \((w^*, b^*)\) using the
gradfunction and confirm it is approximately \([0, 0]\).