← Back to the portfolio RSS

· 2 min read

Linear regression, low-level

A single neuron trained by gradient descent: the model, the cost function, the gradient, and the analytical solution.

Linear regression is the classic entry point to deep learning: a single neuron, no activation function, trained by gradient descent. Understanding how it works mechanically — not just model.fit() — lays the groundwork for everything that follows (deep networks, backprop, optimizers).

The model

We look for an affine function that approximates y y from x x :

y ^ = w x + b \hat{y} = w x + b

With several input variables, x ∈ ℝ n x \in \mathbb{R}^n becomes a vector, and so does w ∈ ℝ n w \in \mathbb{R}^n :

y ^ = w ⊤ x + b = ∑ i = 1 n w i x i + b \hat{y} = w^\top x + b = \sum_{i=1}^{n} w_i x_i + b

The cost function

We measure the error over the whole training set with the mean squared error:

J ( w , b ) = 1 m ∑ i = 1 m ( y ^ ( i ) − y ( i ) ) 2 J(w, b) = \frac{1}{m} \sum_{i=1}^{m} \left( \hat{y}^{(i)} - y^{(i)} \right)^2

where m m is the number of examples. Squaring penalises large errors more heavily and makes the function differentiable everywhere, unlike the absolute value.

The gradient

To minimise J J , we compute its derivative with respect to each parameter. For w w :

∂ J ∂ w = 2 m ∑ i = 1 m ( y ^ ( i ) − y ( i ) ) x ( i ) \frac{\partial J}{\partial w} = \frac{2}{m} \sum_{i=1}^{m} \left( \hat{y}^{(i)} - y^{(i)} \right) x^{(i)}

And for b b :

∂ J ∂ b = 2 m ∑ i = 1 m ( y ^ ( i ) − y ( i ) ) \frac{\partial J}{\partial b} = \frac{2}{m} \sum_{i=1}^{m} \left( \hat{y}^{(i)} - y^{(i)} \right)

Gradient descent

We update the parameters in the direction opposite to the gradient, scaled by a learning rate η \eta :

w ← w − η ∂ J ∂ w w \leftarrow w - \eta \frac{\partial J}{\partial w}

b ← b − η ∂ J ∂ b b \leftarrow b - \eta \frac{\partial J}{\partial b}

We repeat until convergence. An η \eta that is too large makes training diverge; too small, and it converges, but very slowly.

Minimal implementation

import numpy as np

def train(X, y, lr=0.01, epochs=1000):
    m, n = X.shape
    w = np.zeros(n)
    b = 0.0

    for _ in range(epochs):
        y_hat = X @ w + b
        error = y_hat - y

        dw = (2 / m) * X.T @ error
        db = (2 / m) * np.sum(error)

        w -= lr * dw
        b -= lr * db

    return w, b

The analytical solution

For this specific case, there is an exact solution, with no iteration: the normal equations.

w = ( X ⊤ X ) − 1 X ⊤ y w = (X^\top X)^{-1} X^\top y

It avoids gradient descent entirely, but becomes expensive when n n is large (matrix inversion is O ( n 3 ) O(n^3) ), or impossible if X ⊤ X X^\top X is not invertible. Gradient descent, on the other hand, generalises directly to deep networks — which is why we learn it first.