Measure bias and variance

LabMedium30 min14 checks

bias-varianceoverfittingmodel-selection

Problem

A model can miss the truth in two ways. It can be too simple and miss on every training set: that is bias. It can be too flexible and change a lot from one training set to the next: that is variance. In this lab you fit polynomials of degree 1 to 12 to many resampled training sets and measure both parts of the error.

support.py gives the true function f(x) = \sin(1.5x) + 0.5x, the data generator, a least squares polynomial fit and the test points X_TEST, 100 points from -2.5 to 2.5. generate_data(n_samples, noise_std, seed) draws x uniform in [-3, 3] and y = f(x) + \epsilon with \epsilon \sim N(0, \sigma^2).

Task

Write five helper functions and answers(). Let predictions have one row for each training round b = 1 \dots B and one column for each test point t = 1 \dots T. Let \bar p_t be the mean of column t.

  1. decompose(predictions, y_true) returns the tuple (bias_sq, variance).

- \text{bias}^2 = \frac{1}{T} \sum_t (\bar p_t - f_t)^2.

- \text{variance} = \frac{1}{T} \sum_t \frac{1}{B} \sum_b (p_{bt} - \bar p_t)^2. Divide by B, not by B - 1.

  1. expected_test_error(predictions, y_true, noise_std) returns \frac{1}{BT} \sum_{b,t} (p_{bt} - f_t)^2 + \sigma^2.
  2. sweep_degrees(degrees, n_rounds, n_train, noise_std, seed=0) returns a dict. Each key is a degree. Each value is a dict with the keys "bias_sq", "variance" and "test_error". For each degree and each round b from 0 to n_rounds - 1:

1. Draw a training set with generate_data(n_train, noise_std, seed=seed + b). Every degree uses the same training sets.

2. Fit it with fit_polynomial and predict X_TEST with predict_polynomial.

3. Compare the predictions with true_function(X_TEST).

  1. best_degree(results) returns the degree with the lowest test_error. If two degrees tie, return the lower degree.
  2. crossover_degree(results) returns the lowest degree whose variance is greater than or equal to its bias_sq. If no degree meets this rule, return None.

Then do the experiment in answers().

  1. Change the degree over DEGREES, from 1 to 12. Use N_ROUNDS, N_TRAIN, NOISE_STD and seed=0.
  2. Measure the bias squared, the variance and the test error at each degree.
  3. Report best_degree and its test error as best_test_error.
  4. Report crossover_degree.
  5. Report the bias squared at degree 1 as bias_sq_degree_1 and the variance at degree 12 as variance_degree_12.

Input and output

NameTypeShapeMeaning
predictionsfloat array(B, T)one row of test predictions for each training round
y_truefloat array(T,)the true function at the test points, without noise
noise_stdfloatscalarthe noise standard deviation \sigma
degreeslist of ints(k,)the polynomial degrees to fit
n_roundsintscalarthe number of training sets B
n_trainintscalarthe number of points in each training set
resultsdictk keysthe output of sweep_degrees
  • decompose returns a tuple of two floats.
  • expected_test_error returns a float.
  • best_degree and crossover_degree return an int from the keys of results. crossover_degree can return None.
  • answers() returns a dict with the five keys above. Each value is a Python float.

Constraints

  • 1 <= B <= 200 and 1 <= T <= 200.
  • Do not change the input arrays.
  • Do not round the answers. The checks accept a relative difference of 1e-6 from the reference run.
  • Use numpy and the standard library only.

Examples

pythonP = np.array([[1.0, 2.0, 3.0], [3.0, 2.0, 1.0]])
Y = np.array([1.5, 2.0, 2.0])
decompose(P, Y)
text(0.08333333333333333, 0.6666666666666666)
pythonexpected_test_error(P, Y, 0.5)
text1.0

The error is the sum 0.0833 + 0.6667 + 0.25.

pythonresult = sweep_degrees([1, 3], n_rounds=5, n_train=20)
result[3]
text{'bias_sq': 0.06179704932177359, 'variance': 0.052499400227697696, 'test_error': 0.3642964495494713}
Provided code: support.py

Your code and the checks import this module with from support import .... You do not change it.

import numpy as np

NOISE_STD = 0.5
N_TRAIN = 30
N_ROUNDS = 100
DEGREES = list(range(1, 13))
X_TEST = np.linspace(-2.5, 2.5, 100)


def true_function(x):
    return np.sin(1.5 * x) + 0.5 * x


def generate_data(n_samples=N_TRAIN, noise_std=NOISE_STD, seed=0):
    rng = np.random.default_rng(seed)
    x = rng.uniform(-3.0, 3.0, n_samples)
    y = true_function(x) + rng.normal(0.0, noise_std, n_samples)
    return x, y


def fit_polynomial(x_train, y_train, degree):
    X = np.column_stack([x_train ** d for d in range(degree + 1)])
    return np.linalg.lstsq(X, y_train, rcond=None)[0]


def predict_polynomial(x, w):
    X = np.column_stack([x ** d for d in range(len(w))])
    return X @ w

Hints

4 hints

    Solution and notes

    Locked

    Pass every check with Submit to open the reference solution and the notes.

    Run it on your computer

    Clone the repository, install numpy and run the same checks in a terminal:

    python3 scripts/practice/run.py lab-bias-variance my_answer.py
    python3 scripts/practice/run.py lab-bias-variance my_answer.py --examples

    Start my_answer.py from starter.py.

    Your code

    Python loads on the first run

    Your code is saved in this browser as you type. On a phone you can read and edit the code, but the checks are easier to use on a desktop.

    Keyboard shortcuts
    Ctrl Enter
    Run the examples
    Ctrl Shift Enter
    Submit
    Tab / Shift Tab
    Indent or outdent the lines
    Esc then Tab
    Move the focus out of the editor
    Alt H
    Show the next hint

    On a Mac, use Cmd in place of Ctrl.

    Select Run to check the examples, or Submit to run every check.