III-07 · Linear Regression

Follow a line from prediction to update

Move a sample, calculate its signed residual, and see how all five observations determine one simultaneous gradient update.

Chapters in Part II
  1. III-07 · Linear Regression
  2. III-08 · Least Squares and the Closed-Form Best Fit
  3. III-09 · Gradient Descent for Linear Regression
  4. III-10 · Multiple Linear Regression
  5. III-11 · Polynomial Features
  6. III-12 · Regularization
  7. III-13 · Logistic Regression
  8. III-14 · Decision Thresholds and Confusion Matrix
  9. III-15 · Precision, Recall, F1 and ROC
  10. III-16 · Multiclass Classification and Softmax Regression

III-07 · Inspect one update

Observations → predictions → residuals

Hand-authored toy data · original observations. Synthetic values, not measured data.

Affine regression coordinate planeFilled circles are observations, hollow diamonds are predictions, and dashed vertical segments are residuals. Both axes use the same scale. Focus a sample and use arrow keys, or drag its handle.

● Observation◇ Prediction┆ Vertical residual

Select any sample. Drag or use arrow keys in steps of 0.1; Shift + arrow moves 0.5. Dragging stops at ±5. Equal x/y scale; off-plane predictions are clipped and remain in the table.

Mean squared error
≈ 4.1
Weight gradient dw
≈ −5.2
Bias gradient db
≈ −1.6
Next weight
≈ 1.02
Next bias
≈ 0.16
Predictor parameters
Observation editor

An update can increase loss. Values outside the shown ranges are rejected, with the previous valid state preserved.

Frame 1 / 1 · 0 applied updates since edit or reset.

SamplesEach row is one observation; vectors are columns.
5
X · feature batch
5 × 1
w · weight matrix
1 × 1
y · target column
5 × 1
BiasBroadcast to every prediction; no extra column in X.
Scalar
y^=wx+b,w≈0.5,b≈0\hat y=wx+b,\quad w\approx0.5,\quad b\approx0
MSE⁡=15∑i=15(y^i−yi)2≈4.1\operatorname{MSE}=\frac{1}{5}\sum_{i=1}^{5}(\hat y_i-y_i)^2\approx 4.1
wnext=w−η∂L∂w≈(0.5)−(0.1)(−5.2)≈1.02bnext=b−η∂L∂b≈(0)−(0.1)(−1.6)≈0.16\begin{aligned}w_{\mathrm{next}}&=w-\eta\frac{\partial L}{\partial w}\\&\approx(0.5)-(0.1)(-5.2)\approx 1.02\\b_{\mathrm{next}}&=b-\eta\frac{\partial L}{\partial b}\\&\approx(0)-(0.1)(-1.6)\approx 0.16\end{aligned}
Active state · residual = prediction − target; squared error = residual²
SamplexTarget yPredictionResidualSquared error
s1-2-3≈ −1≈ 2≈ 4
s2-1-1≈ −0.5≈ 0.5≈ 0.25
s301≈ 0≈ −1≈ 1
s413≈ 0.5≈ −2.5≈ 6.25
s524≈ 1≈ −3≈ 9

One active state supplies the observations, predictor, signed residuals, equations and update. Filled circles are observations; hollow diamonds are predictions; dashed vertical segments connect them. Reset restores the five hand-authored samples.

A prediction is a claim about an observation

An observation supplies an input xix_i and a target yiy_i. An affine predictor uses two shared parameters to produce a number:

y^i=wxi+b.\hat y_i = wx_i+b.

The weight ww controls how much the prediction changes when the input increases by one. The bias bb is the prediction at x=0x=0. Changing either parameter changes the same line for every sample. Moving an observation changes the data; it does not directly move the predictor’s parameters.

Select a sample in the stage. Its filled circle is the observed pair (xi,yi)(x_i,y_i), and its hollow diamond is (xi,y^i)(x_i,\hat y_i). The dashed segment is vertical because this loss compares target and prediction at the same input. It does not measure the shortest distance to the line.

Our residual convention is

ri=y^i−yi.r_i=\hat y_i-y_i.

A positive residual means overprediction; a negative residual means underprediction. The sign is useful even though the squared error ri2r_i^2 is nonnegative. Some books reverse the residual convention, so check the definition before copying a gradient.

Samples, columns and shapes

These five unitless observations were explicitly hand-authored for this experiment: inputs [−2,−1,0,1,2][-2,-1,0,1,2] and targets [−3,−1,1,3,4][-3,-1,1,3,4], with stable IDs s1 through s5. They are synthetic toy data, not measurements or evidence about a population. The last target is one below the otherwise exact line y=2x+1y=2x+1.

Mathematical vectors are columns. A batch puts one observation in each row of XX:

X=[−2−1012]∈R5×1,W=[w]∈R1×1,y=[−3−1134]∈R5×1.X=\begin{bmatrix}-2\\-1\\0\\1\\2\end{bmatrix}\in\mathbb R^{5\times1}, \quad W=\begin{bmatrix}w\end{bmatrix}\in\mathbb R^{1\times1}, \quad y=\begin{bmatrix}-3\\-1\\1\\3\\4\end{bmatrix}\in\mathbb R^{5\times1}.

The scalar bias broadcasts to all five entries: y^=XW+b1\hat y=XW+b\mathbf1, where 1\mathbf1 is a 5×15\times1 column of ones. Thus predictions and residuals also have shape 5×15\times1. There is no intercept column in this chapter’s XX; the bias remains a separate scalar. For nn observations, replace five with nn.

The inspector reports these actual shapes. Stable IDs identify observations when their coordinates change; their IDs are not extra features.

Turn all residuals into one objective

Mean squared error averages the squared vertical residuals:

L(w,b)=MSE⁡(w,b)=1n∑i=1n(wxi+b−yi)2.L(w,b)=\operatorname{MSE}(w,b)=\frac1n\sum_{i=1}^{n}(wx_i+b-y_i)^2.

Squaring prevents positive and negative errors from cancelling and makes larger residuals contribute more. Averaging distinguishes MSE from the sum of squared errors. This definition contains no factor of one half and no square root.

MSE is the training objective in this experiment. A low value says the predictor matches these targets; it does not establish causal influence, reliable extrapolation, or performance on unseen observations. We have no separate validation or test set here.

Work one update by hand

Reset to w=0.5w=0.5, b=0b=0 and learning rate η=0.1\eta=0.1. Compute each row before changing a parameter.

SamplexTarget yPredictionResidualSquared error
s1-2-3-124
s2-1-1-0.50.50.25
s3010-11
s4130.5-2.56.25
s5241-39

The squared errors sum to 20.520.5, giving L=20.5/5=4.1L=20.5/5=4.1. Their signed residuals sum to −4-4; the weighted residuals sum to

∑irixi=2(−2)+0.5(−1)+(−1)(0)+(−2.5)(1)+(−3)(2)=−13.\sum_i r_i x_i=2(-2)+0.5(-1)+(-1)(0)+(-2.5)(1)+(-3)(2)=-13.

The gradients below therefore give dw=(2/5)(−13)=−5.2dw=(2/5)(-13)=-5.2 and db=(2/5)(−4)=−1.6db=(2/5)(-4)=-1.6. Subtract both gradients, evaluated at the same old state:

wnext=0.5−0.1(−5.2)=1.02,bnext=0−0.1(−1.6)=0.16.\begin{aligned} w_{\mathrm{next}}&=0.5-0.1(-5.2)=1.02,\\ b_{\mathrm{next}}&=0-0.1(-1.6)=0.16. \end{aligned}

Press Apply one update and check the new rows:

SamplePredictionResidualSquared error
s1-1.881.121.2544
s2-0.860.140.0196
s30.16-0.840.7056
s41.18-1.823.3124
s52.2-1.83.24

Their sum is 8.5328.532, so the new MSE is 1.70641.7064. Recomputing the gradients at this new state gives dw=−3.12dw=-3.12 and db=−1.28db=-1.28. A second update would use these values, not the old gradients. The first decrease is an observation about this update, not a promise that every permitted learning rate decreases loss.

Derive the analytical gradients

Hold the data fixed. The chain rule and the partial derivatives of the affine predictor give

∂ri∂w=xi,∂ri∂b=1.\frac{\partial r_i}{\partial w}=x_i, \qquad \frac{\partial r_i}{\partial b}=1.

For a squared residual, the outer derivative contributes 2ri2r_i. Differentiating the average term by term gives

∂L∂w=2n∑irixi=dw,∂L∂b=2n∑iri=db.\begin{aligned} \frac{\partial L}{\partial w}&=\frac2n\sum_i r_i x_i=dw,\\ \frac{\partial L}{\partial b}&=\frac2n\sum_i r_i=db. \end{aligned}

The gradient is the column ∇L=[dw,db]T\nabla L=[dw,db]^{\mathsf T}. The weight gradient includes the input because changing ww affects each prediction by xix_i times that change. The bias gradient weights every sample equally. For the observation at x=0x=0, the weight’s contribution is zero even if its residual is large.

Gradient descent takes the simultaneous update

[wnextbnext]=[wb]−η[dwdb].\begin{bmatrix}w_{\mathrm{next}}\\b_{\mathrm{next}}\end{bmatrix} =\begin{bmatrix}w\\b\end{bmatrix} -\eta\begin{bmatrix}dw\\db\end{bmatrix}.

The implementation calculates these analytical derivatives directly. It does not use automatic differentiation or a fitted-model library. A central finite difference, [L(w+h,b)−L(w−h,b)]/(2h)[L(w+h,b)-L(w-h,b)]/(2h), is a separate numerical check of dwdw; it is not the update rule used by the stage.

Edit and replay the same state

Use the numeric fields to change weight, bias, learning rate or the selected observation’s coordinates. Enter applies a valid value; a rejected entry gives a reason and preserves the model. Sample handles also accept pointer dragging and arrow keys. Each arrow changes a coordinate by 0.10.1; holding Shift changes it by 0.50.5. These paths all edit the observation used by the plot and table.

Replay 3 updates records the starting state and three successive updates. Frame 1 contains zero replay updates; frame 4 contains three. First, Previous and Next select immutable snapshots. The parameters, sample editor, plot, equations, table and inspector all switch to that exact selected frame. Editing or applying a new update starts from the displayed frame and clears the replay. Reset restores the original five observations and parameters.

Try these investigations:

  1. Set the bias to 11 and keep the weight fixed. Which predictions change? Check every row.
  2. Reset, then change only sample s3’s target. Explain why its direct contribution to dwdw is zero while its contribution to dbdb changes.
  3. Move s5 vertically and compare its squared error with its residual. Doubling a residual multiplies its squared error by four.
  4. Reset and replay three updates. Return to frame 2, edit the weight, and verify the new state begins from frame 2’s observations and bias.

Counterexamples clarify the limits

Constant inputs are still valid predictor data. If two samples have x=2x=2 and targets 11 and 33, the predictors (w,b)=(0,2)(w,b)=(0,2) and (1,0)(1,0) both predict 22 for both observations. Both have MSE 11. The data constrain 2w+b2w+b, so they do not identify a unique weight and bias. A single observation has the same kind of ambiguity. Neither case makes prediction, residuals or gradients undefined.

A perfect fit has zero gradient. For observations (−1,−1)(-1,-1) and (1,3)(1,3) with w=2w=2, b=1b=1, both residuals are zero. MSE and both gradients are zero, so an update leaves the parameters unchanged.

A valid rate can overshoot. With the single observation (2,1)(2,1), start at w=b=0w=b=0 and η=1\eta=1. MSE is initially 11, and gradients are dw=−4dw=-4, db=−2db=-2. The next parameters (4,2)(4,2) predict 1010, so the next MSE is (10−1)2=81(10-1)^2=81. A positive bounded rate alone is not a convergence certificate. Input scale and curvature matter.

Execute the actual Python reference

This is the source file used to generate the golden calculations. NumPy constructs XX, WW and yy as column arrays, then computes predictions, residuals, MSE and analytical gradients from scratch. Running it prints the canonical before/after results and an initial-plus-three-updates trace.

From the book root, with the provisioned NumPy environment:

.venv/bin/python -B code/python/p02/linear_regression.py
code/python/p02/linear_regression.py
"""III-07: affine predictions, MSE and analytical gradient descent using NumPy."""
from copy import deepcopy
import json
import math

import numpy as np


CANONICAL_REGRESSION = {
    "samples": [
        {"id": "s1", "x": -2, "y": -3},
        {"id": "s2", "x": -1, "y": -1},
        {"id": "s3", "x": 0, "y": 1},
        {"id": "s4", "x": 1, "y": 3},
        {"id": "s5", "x": 2, "y": 4},
    ],
    "weight": 0.5,
    "bias": 0,
    "learningRate": 0.1,
}


def _finite(value, label):
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise ValueError(f"{label} must be a finite number")
    try:
        valid = math.isfinite(value)
    except OverflowError:
        valid = False
    if not valid:
        raise ValueError(f"{label} must be finite; input or arithmetic overflow is invalid")
    return value


def derive_regression(state):
    samples = state.get("samples") if isinstance(state, dict) else None
    if not isinstance(samples, (list, tuple)) or not 1 <= len(samples) <= 64:
        raise ValueError("Regression requires 1 to 64 samples")
    weight = _finite(state.get("weight"), "Weight")
    bias = _finite(state.get("bias"), "Bias")
    rate = _finite(state.get("learningRate"), "Learning rate")
    if not 0 < rate <= 1:
        raise ValueError("Learning rate must be greater than 0 and at most 1")
    ids = set()
    for sample in samples:
        identifier = sample.get("id") if isinstance(sample, dict) else None
        if not isinstance(identifier, str) or not identifier.strip() or identifier in ids:
            raise ValueError("Sample IDs must be nonempty strings and unique")
        ids.add(identifier)
        for key in ("x", "y"):
            _finite(sample.get(key), f"Sample {identifier} {key}")

    # Mathematical vectors are columns; each row of X is one scalar-feature sample.
    X = np.array([[sample["x"]] for sample in samples], dtype=np.float64)
    weights = np.array([[weight]], dtype=np.float64)
    targets = np.array([[sample["y"]] for sample in samples], dtype=np.float64)
    n = len(samples)
    try:
        with np.errstate(over="raise", invalid="raise", divide="raise", under="ignore"):
            products = X @ weights
            predictions = products + bias
            residuals = predictions - targets
            squared = residuals * residuals
            squared_sum = np.sum(squared)
            residual_sum = np.sum(residuals)
            weighted_sum = (X.T @ residuals)[0, 0]
            mse = float(squared_sum / n)
            dw = float((2 / n) * weighted_sum)
            db = float((2 / n) * residual_sum)
            next_weight = weight - rate * dw
            next_bias = bias - rate * db
    except FloatingPointError as error:
        raise ValueError(f"Regression arithmetic overflowed or produced a non-finite value: {error}") from error
    for values, label in ((products, "Weight × x"), (predictions, "Predictions"),
                          (residuals, "Residuals"), (squared, "Squared errors")):
        if not np.isfinite(values).all():
            raise ValueError(f"{label} overflowed or produced a non-finite value")
    for value, label in ((squared_sum, "Squared-error sum"), (residual_sum, "Residual sum"),
                         (weighted_sum, "Weighted-residual sum"), (mse, "MSE"),
                         (dw, "Weight gradient"), (db, "Bias gradient"),
                         (next_weight, "Updated weight"), (next_bias, "Updated bias")):
        _finite(float(value), label)
    return {
        "rows": [dict(id=sample["id"], x=sample["x"], y=sample["y"],
                      prediction=float(predictions[i, 0]), residual=float(residuals[i, 0]),
                      squaredError=float(squared[i, 0])) for i, sample in enumerate(samples)],
        "mse": mse,
        "gradients": {"weight": dw, "bias": db},
        "next": {"weight": next_weight, "bias": next_bias},
        "shapes": {"X": list(X.shape), "weights": list(weights.shape), "targets": list(targets.shape)},
    }


def regression_step(state):
    next_state = {**deepcopy(state), **derive_regression(state)["next"]}
    derive_regression(next_state)
    return next_state


def regression_trace(state, steps):
    if type(steps) is not int or not 0 <= steps <= 20:
        raise ValueError("Trace steps must be an integer from 0 to 20")
    derive_regression(state)
    state = deepcopy(state)
    frames = [{"index": 0, "state": state}]
    for index in range(1, steps + 1):
        state = regression_step(state)
        frames.append({"index": index, "state": state})
    return frames


if __name__ == "__main__":
    print(json.dumps({
        "before": derive_regression(CANONICAL_REGRESSION),
        "after": derive_regression(regression_step(CANONICAL_REGRESSION)),
        "trace": regression_trace(CANONICAL_REGRESSION, 3),
    }, indent=2, allow_nan=False))

The independent oracle uses exact rational arithmetic and an expanded loss based on data moments. It imports neither NumPy nor this reference. Its literal tests independently confirm the canonical MSE, gradients, first update and following loss; numerical tests also compare NumPy with that oracle and check the gradients by finite differences.

Numerical considerations

State uses binary64 numbers. Given sample coordinates use full round-trip decimal text in the table and accessible names. Computed predictions, residuals, squared errors, MSE and update values are displayed to four decimal places and marked approximately. A displayed approximate zero can therefore hide a small nonzero loss or gradient. Display rounding never changes the values used for an update. Keyboard movement adds its increment to the existing coordinate with ordinary binary64 arithmetic, preserving fine precision without extra decimal truncation; pointer movement explicitly snaps to the stated 0.1 lattice. Python/TypeScript parity is checked with absolute and relative tolerances of 10−1210^{-12}, rather than exact equality between independently rounded calculations.

The numerical API requires 1–64 observations, unique nonempty IDs, finite inputs and 0<η≤10<\eta\leq1. Finite inputs can still overflow during products, squared residuals or reductions. Such arithmetic is rejected with an informative error; an overflowing replay returns no partial history. Subnormal values and underflow follow binary64 arithmetic, and a very small update can round to no parameter change.

The plot has equal x/y scale and a fixed domain from −6 to 6. Predictions outside that plane are clipped visually but remain visible numerically in the table. Pointer movement stops at the observation editor’s boundary; numeric entry instead rejects an out-of-range value. Large touch targets expand with the rendered plot size, and only sample handles capture touch gestures, so the rest of the page remains scrollable.

Check your understanding

Each question grades the chosen answer and explains the misconception behind the alternatives. Use the canonical state unless the question says otherwise.

At x = 1, the target is 3 and the canonical prediction is 0.5. What is the signed residual?
The five squared errors sum to 20.5. What is the MSE used here?
At the canonical state, Σ residual × x = −13 and Σ residual = −4. Which gradients are correct?
Using learning rate 0.1 and canonical gradients −5.2 and −1.6, what are the next parameters?
For the five scalar-feature samples, what are the shapes of X, W and y in this chapter?
Does a learning rate between 0.001 and 1 guarantee that an accepted update decreases MSE?

Checkpoints

You can now identify the observation, prediction and signed residual; account for batch and column shapes; calculate MSE and both analytical gradients; apply one simultaneous update; and inspect immutable replay without confusing an input bound with a convergence guarantee. Closed-form fitting, multiple features and classification belong to later chapters.