IntoMindLearn

Chapter 5 · Middle school

Models that learn

How can a computer learn a rule that nobody told it?

Learning from examples

A model is a set of rules a computer learns from examples. Each example comes with its answer, called its label. Show a model many pictures of animals labeled cat or dog, and it can learn what makes cats and dogs different. Then it can decide about animals it has never seen.

Nobody writes the rule for it. The rule comes from the examples.

Try it

The sorting game

Here are twelve creatures, each labeled a Glorp or a Zib. Work out a rule that says which is which. Then use your rule on the eight new creatures.

CreatureSpotsLegsColorTailIt is a
1Yes4RedLongGlorp
2Yes6BlueShortGlorp
3No4RedLongZib
4Yes2RedShortZib
5No6BlueLongZib
6Yes4BlueShortGlorp
7No2BlueShortZib
8Yes6RedLongGlorp
9Yes2BlueLongZib
10No4BlueShortZib
11Yes4RedShortGlorp
12No6RedShortZib

Your ruleA creature is a Glorp when .

New creatureSpotsLegsColorTailYour rule says
13Yes6BlueLong
14No4RedShort
15Yes2RedLong
16Yes4BlueLong
17No2RedLong
18Yes6RedShort
19No6BlueShort
20Yes2BlueShort

Testing on new examples

A model that only remembers the examples it was shown isn't much use. It answers those perfectly, and is stuck on anything new.

So a model is always tested on examples it did not learn from. Training fits its rules to examples whose answers are known. Testing checks them on examples held back.

Try it

The memorizer

One student plays a memorizer. For each new creature, the memorizer may answer only if it matches one of the first twelve exactly, and must guess otherwise. Score the memorizer, and the rule-finders, on the eight new creatures.

Work it outThe memorizer got of 8 right. Our rule got of 8 right.

You should seeThe memorizer guessing on every new creature, since none matches one of the twelve exactly, and the rule-finders doing far better.

Your brain as examples

In the Mind-Controlled Light Switch, you will teach a model too, and the examples will be your own EEG, four seconds at a time. Each is labeled Off while you rest, or On while you count down in your head, by what the demo tells you to do.

The demo teaches in eight rounds, Off and On in turn. Then it tests the model on readings it held back, so its score comes from examples it never learned from.

Held back Eight rounds, Off and On in turn. In each, the first two readings after a switch are left out while the brain settles, and eleven are kept. Each round in turn is held back from training and tested on. The outlined round is one of them.

Talk about it

  1. What was the rule for Glorps? How did you find it?

  2. Why did the memorizer do badly on the new creatures?

  3. In the Mind-Controlled Light Switch, what are the examples, and what are the labels?

Chapter 5 · High school

Models that learn

How does a computer learn a rule from labeled examples, and how do you know whether it learned the rule or just the examples?

Learning from examples

A model that sorts examples into groups is a classifier. It is given numbers about each example, its features, and learns a rule that maps them to a label. Training fits the rule to examples whose labels are known. Testing measures it on examples held back.

Try it

The sorting game

Here are twelve creatures, each labeled a Glorp or a Zib. Find the rule, then use it on the eight new ones. Then one student plays a memorizer, who may answer for a new creature only if it matches one of the twelve exactly, and must guess otherwise.

CreatureSpotsLegsColorTailIt is a
1Yes4RedLongGlorp
2Yes6BlueShortGlorp
3No4RedLongZib
4Yes2RedShortZib
5No6BlueLongZib
6Yes4BlueShortGlorp
7No2BlueShortZib
8Yes6RedLongGlorp
9Yes2BlueLongZib
10No4BlueShortZib
11Yes4RedShortGlorp
12No6RedShortZib
New creatureSpotsLegsColorTailYour rule saysThe memorizer says
13Yes6BlueLong
14No4RedShort
15Yes2RedLong
16Yes4BlueLong
17No2RedLong
18Yes6RedShort
19No6BlueShort
20Yes2BlueShort

Work it outThe rule: a Glorp has . The features that don't matter: .

A line between two groups

The simplest useful classifier draws a line between two groups, or with more features, a flat plane. Each example gets a score: a weighted sum of its features, plus a constant. The weights are fitted so the scores come close to 0 for one group and 1 for the other, and everything above 0.5 is called the second group.

The usual way to fit them is least squares: choose the weights that make the squared differences between the scores and the labels as small as possible. The Mind-Controlled Light Switch does exactly this, with 76 features.

Least squares Twenty-four points in two groups, from the table below. The line is fitted on the sixteen solid points. The eight hollow ones are held back to test it.

Try it

A line through examples

Plot the first sixteen points on graph paper, marking each group. Draw the line you think separates them best, and use it on the last eight. Then, in a spreadsheet, fit score = a·x + b·y + c by least squares on the first sixteen, with =LINEST(D2:D17, B2:C17). It lists the weights in reverse order: b, then a, then c.

PointxyGroup
11.92.21
20.91.90
32.41.51
42.51.21
51.31.60
60.51.80
72.41.41
82.80.81
90.92.60
102.02.31
112.02.11
121.81.51
13-0.12.10
14-0.12.20
151.82.00
161.22.40
172.01.9held back
182.51.0held back
191.42.1held back
200.41.9held back
211.32.8held back
221.11.9held back
231.10.9held back
241.21.5held back

Work it outa = , b = , c = . Right on the sixteen: . Right on the eight held back, once your teacher gives their groups: .

Overfitting

A rule flexible enough to fit every training example, noise and all, overfits: it scores well on what it saw, and worse on anything new. The gap between the training score and the held-out score measures it.

So a model is scored on examples it never learned from. The Mind-Controlled Light Switch teaches in eight rounds, and tests each round with a head taught on the other seven.

Held back Eight rounds of teaching, Off and On in turn. Each round in turn is held back from training and tested on, so the model's score comes from readings it never learned from. The outlined round is one of them.

Try it

A boundary that fits everything

On your plot of the first sixteen points, draw a boundary that weaves around every point, so that all sixteen are on the right side. Score it on the eight held back, and compare it with your straight line.

RuleRight on the sixteenRight on the eight
The straight line
The weaving boundary

You should seeA perfect score on the sixteen that promises more than it delivers on the eight.

The same line in Python

Try it, if you code

Fit it in Python

import numpy as np
train = np.array([[1.9, 2.2, 1], [0.9, 1.9, 0], [2.4, 1.5, 1], [2.5, 1.2, 1],
                  [1.3, 1.6, 0], [0.5, 1.8, 0], [2.4, 1.4, 1], [2.8, 0.8, 1],
                  [0.9, 2.6, 0], [2.0, 2.3, 1], [2.0, 2.1, 1], [1.8, 1.5, 1],
                  [-0.1, 2.1, 0], [-0.1, 2.2, 0], [1.8, 2.0, 0], [1.2, 2.4, 0]])
X = np.c_[train[:, :2], np.ones(len(train))]
w = np.linalg.lstsq(X, train[:, 2], rcond=None)[0]
print(w, ((X @ w > 0.5) == train[:, 2]).mean())

It prints the weights a, b and c, and the share of the sixteen it gets right. Add the eight held-back points, with the groups your teacher gives you, and score the line on them.

Talk about it

  1. Your line got 15 of 16 training points right, but fewer of the held-back ones. Why is the held-back score the one to report?

  2. The least-squares line gives almost no weight to y. What does that tell you about the feature?

  3. Describe a classifier that would score 16 of 16 on training and still be useless.

Chapter 5 · College

Models that learn

How does a linear model fit labeled examples, why does it fit noise when the features nearly outnumber the examples, and how do you choose a penalty without peeking at the test?

Learning from labeled examples

A model is a rule with parameters fitted to examples. Each example is a vector of features x with a label y. A linear classifier scores each example w·x + b and calls it the second group when the score passes 0.5.

A custom AI head is exactly this, with the encoder's 76 numbers as features and Off and On as 0 and 1, its weights rounded to bytes so the IntoMind One's chip can run it.

Least squares Twenty-four points in two groups, from the table below. The line is fitted on the sixteen solid points, and the eight hollow ones are held back to test it.
PointxyGroup
11.92.21
20.91.90
32.41.51
42.51.21
51.31.60
60.51.80
72.41.41
82.80.81
90.92.60
102.02.31
112.02.11
121.81.51
13−0.12.10
14−0.12.20
151.82.00
161.22.40
172.01.91
182.51.01
191.42.10
200.41.90
211.32.80
221.11.90
231.10.91
241.21.51

Least squares

Stack the examples as the rows of X, with a column of ones for the constant. Least squares chooses the weights that minimize the squared error ‖Xw − y‖², and setting the gradient to zero gives the normal equations:

w = (XᵀX)⁻¹ Xᵀy

Training fits w on some examples. Testing scores it on others it never saw, which estimates how it will do on new ones: training and testing.

Try it

A line through examples

Fit the line on the first sixteen points by the normal equations, and score it on those and on the eight held back.

import numpy as np

# point, x, y, group, from the table: the first sixteen to fit, the last eight held back
P = np.array([[1.9, 2.2, 1], [0.9, 1.9, 0], [2.4, 1.5, 1], [2.5, 1.2, 1],
              [1.3, 1.6, 0], [0.5, 1.8, 0], [2.4, 1.4, 1], [2.8, 0.8, 1],
              [0.9, 2.6, 0], [2.0, 2.3, 1], [2.0, 2.1, 1], [1.8, 1.5, 1],
              [-0.1, 2.1, 0], [-0.1, 2.2, 0], [1.8, 2.0, 0], [1.2, 2.4, 0],
              [2.0, 1.9, 1], [2.5, 1.0, 1], [1.4, 2.1, 0], [0.4, 1.9, 0],
              [1.3, 2.8, 0], [1.1, 1.9, 0], [1.1, 0.9, 1], [1.2, 1.5, 1]])
A = np.c_[P[:, :2], np.ones(len(P))]            # x, y and a constant
w = np.linalg.solve(A[:16].T @ A[:16], A[:16].T @ P[:16, 2])
right = (A @ w > 0.5) == P[:, 2]
print(np.round(w, 3), right[:16].sum(), "of 16 fitted,", right[16:].sum(), "of 8 held back")

You should seeWeights of about 0.469 on x, −0.003 on y and −0.204 for the constant: 15 of 16 right on the points it was fitted on, and 6 of 8 on the points held back.

Too many features, and ridge regression

As the number of features d approaches the number of examples n, some combinations of features are barely determined by the data. XᵀX gets tiny eigenvalues, their inverses blow up the weights, and the fit chases noise: overfitting. The Mind-Controlled Light Switch fits 77 parameters to 88 readings, squarely in that regime.

Ridge regression adds a penalty λ on the squared weights, leaving the constant unpenalized:

w = (XᵀX + λI)⁻¹ Xᵀy

Every eigenvalue rises by λ, which bounds the inverse. A larger λ shrinks the weights, adding bias and removing variance.

Held back The Mind-Controlled Light Switch teaches in eight rounds, Off and On in turn, eleven readings in each, and tests on the outlined round, which it never learned from.

Choosing λ without the test

The best λ minimizes error on data the fit hasn't seen, but choosing it by its score on the test fits λ to the test, and flatters that score. So λ is chosen inside the training data, by cross-validation nested inside each training set, or by a rule that uses no labels at all.

The demos' trainer uses such a rule: the geometric mean of the two smallest eigenvalues of XᵀX for the centered embeddings. Embeddings barely vary along one direction, and a penalty of that size removes it while leaving the rest almost unshrunk.

Try it

The ridge path

The demos' source holds the trainer's test data: 150 rows of 76 numbers in tests/fixtures/train/embeddings.json, with labels in targets.json. They aren't EEG: they are random numbers shaped like embeddings, labeled by a rule with noise, and stored as the integers the IntoMind One sends, so λ is in those units. From the top of the demos' source, trace training and held-out error against λ, holding out blocks of consecutive rows, then choose λ by nested cross-validation.

import json
import numpy as np
import matplotlib.pyplot as plt

X = np.array(json.load(open("tests/fixtures/train/embeddings.json")), float)
y = np.array(json.load(open("tests/fixtures/train/targets.json")), float)

def fit(X, y, lam):
    """Ridge on the weights, none on the intercept."""
    A = np.c_[X, np.ones(len(X))]
    P = lam * np.eye(A.shape[1])
    P[-1, -1] = 0
    return np.linalg.solve(A.T @ A + P, A.T @ y)

def mse(X, y, w):
    return np.mean((np.c_[X, np.ones(len(X))] @ w - y) ** 2)

def blocked(X, y, lam, k=5):
    """Training and held-out error, holding out k blocks of consecutive rows in turn."""
    edges = np.linspace(0, len(X), k + 1).astype(int)
    train, held = [], []
    for a, b in zip(edges[:-1], edges[1:]):
        out = np.zeros(len(X), bool)
        out[a:b] = True
        w = fit(X[~out], y[~out], lam)
        train.append(mse(X[~out], y[~out], w))
        held.append(mse(X[out], y[out], w))
    return np.mean(train), np.mean(held)

lams = 10.0 ** np.arange(4, 13)
errors = np.array([blocked(X, y, lam) for lam in lams])
plt.semilogx(lams, errors[:, 0], label="training")
plt.semilogx(lams, errors[:, 1], label="held out")
plt.xlabel("λ")
plt.ylabel("mean squared error")
plt.legend()
plt.show()

# Nested: each outer block's λ is chosen inside the other blocks only.
edges = np.linspace(0, len(X), 6).astype(int)
for a, b in zip(edges[:-1], edges[1:]):
    out = np.zeros(len(X), bool)
    out[a:b] = True
    best = min(lams, key=lambda lam: blocked(X[~out], y[~out], lam)[1])
    w = fit(X[~out], y[~out], best)
    print(f"rows {a} to {b}: λ {best:.0e}, held-out error {mse(X[out], y[out], w):.3f}")
print("the trainer's λ:", round(json.load(open("tests/fixtures/train/ridge.json"))["ridge"]))

You should seeTraining error rising as λ grows, and held-out error falling to its lowest near 10⁸ to 10⁹, then rising. Nested cross-validation chooses λ there in every block, thousands of times the trainer's λ of about 20,000.

Problems

  1. Why is the held-back score the one to report, not the training score?

  2. Fitted on sixteen points, y gets a weight of almost zero. Fitted on all 24, about −0.29. What does that say about small samples?

  3. Why does XᵀX become nearly singular as the number of features approaches the number of examples, and what does λI fix?

  4. Why must λ be chosen without the held-out blocks?

  5. The demos' trainer refuses to fit a head on 76 or fewer readings. Why?

Sources

Hoerl, A. E., and Kennard, R. W. (1970). Ridge regression: biased estimation for nonorthogonal problems. Technometrics.

Hastie, T., Tibshirani, R., and Friedman, J. (2009). The Elements of Statistical Learning, second edition. Springer.

Varoquaux, G., and others (2017). Assessing and tuning brain decoders: cross-validation, caveats, and guidelines. NeuroImage.

Chapter 5 · Graduate

Models that learn

How do you fit a decoder with nearly as many weights as examples, what does the penalty you choose do to it and to its test, and what do its weights say about the data?

Linear decoders

For two classes with means μ0 and μ1 and a shared covariance, Fisher's discriminant (1936) projects each example x onto

w ∝ SW−1(μ1 − μ0)

where SW is the pooled within-class scatter. Least squares on labels of 0 and 1 finds the same direction: the total scatter is the within-class scatter plus a term along μ1 − μ0, and inverting it leaves the direction unchanged (Duda, Hart and Stork, 2001). Logistic regression fits the same family by likelihood. All three need SW or its like, and with 76 features and 88 windows, as in the Mind-Controlled Light Switch, its smallest eigenvalues are estimated far too small and its inverse is mostly noise.

Ridge and shrinkage

Ridge regression adds a penalty λ on the weights' squared size, with the intercept left free (Hoerl and Kennard, 1970). On centered data Xc,

ŵ = (XcᵀXc + λI)−1 Xcᵀy

In the eigenbasis of XcᵀXc, with eigenvalues dj, ridge keeps a fraction dj/(dj + λ) of the least-squares fit along each direction, and the effective degrees of freedom are Σ dj/(dj + λ) (Hastie, Tibshirani and Friedman, 2009). On labels, ridge is the discriminant with SW + λI in place of SW.

Shrinkage LDA replaces the covariance with (1 − γ)Σ̂ + γνI, where ν is its mean eigenvalue and γ is set analytically from the data (Ledoit and Wolf, 2004). It is the standard decoder for EEG with few trials (Blankertz and others, 2011).

Choosing the penalty, honestly

A penalty chosen by cross-validation is fitted to the data like any weight, and the cross-validated score at the chosen penalty is optimistic. Nested cross-validation chooses the penalty inside each outer training set and scores the outer test set with it (Varma and Simon, 2006). Selection overfits too, and the more settings are compared, the more it does (Cawley and Talbot, 2010). With windows from one session, every fold must also respect time, which is the next chapter's subject.

Too little penalty has its own peak. As the number of training examples approaches the number of weights, an unpenalized fit's variance grows without bound, and its error on new data peaks: the top of the curve Belkin and others (2019) called double descent, analyzed for least squares by Hastie and others (2022). A training set of 77 windows for 76 weights sits right on it.

What the weights mean

A decoder's weights are a filter: they combine features to recover the label, and they weigh a feature heavily as readily to cancel noise it shares with another as to pick up signal. The activation pattern, a ∝ Σxw, says how the decoded quantity shows in each feature, and only the pattern can be read as where the signal is (Haufe and others, 2014). On a pretrained encoder's embedding, neither maps to the cortex: the features are learned, and each mixes everything the encoder kept.

In the demo

The Mind-Controlled Light Switch fits ridge regression of labels 0 and 1 on the device's embeddings as integers, each value times 4,096 and rounded, with the intercept unpenalized. It refuses to fit unless there are more windows than the embedding's 76 numbers. No label enters its penalty: λ is the geometric mean of the two smallest eigenvalues of the centered Gram matrix, the smallest floored at 1. The device's embeddings, like the trainer's test fixture, sum to about zero, so one direction, every feature equal, carries almost no variance. The smallest eigenvalue is that direction's, and the penalty sits between it and the next, suppressing it and barely touching the rest.

Analysis

Analysis

The trainer's penalty, and better ones

In the demos' source, the trainer's fixture holds 150 windows of embeddings as integers, balanced labels, and the penalty the Python API chose for them. Reproduce the penalty, then compare it with others by contiguous five-fold cross-validation, choose one by nested cross-validation, and fit shrinkage LDA. Then cut the data to the light switch's size.

import json
import numpy as np
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis

F = "tests/fixtures/train/"                  # in the demos' source
X = np.array(json.load(open(F + "embeddings.json")), float)   # 150 windows of 76, as integers
y = np.array(json.load(open(F + "targets.json")), float)      # 0 and 1
trainer = json.load(open(F + "ridge.json"))["ridge"]

def gap_ridge(X):
    """The demo's penalty: the geometric mean of the two smallest eigenvalues."""
    ev = np.linalg.eigvalsh((X - X.mean(axis=0)).T @ (X - X.mean(axis=0)))
    return np.sqrt(max(ev[0], 1) * ev[1])

Xc = X - X.mean(axis=0)
ev, vecs = np.linalg.eigh(Xc.T @ Xc)                          # ascending
gap = gap_ridge(X)
print(f"smallest eigenvalues {ev[0]:.2f} and {ev[1]:.3g}: "
      f"gap ridge {gap:.2f}, the trainer's {trainer:.2f}")
cosine = abs(vecs[:, 0].sum()) / 76 ** 0.5
print(f"the smallest one's direction against all ones: cosine {cosine:.3f}")

def fit(X, y, lam):
    """Ridge with the intercept left unpenalized, as the demo's fitLinear does."""
    m = X.mean(axis=0)
    xc = X - m
    w = np.linalg.solve(xc.T @ xc + lam * np.eye(X.shape[1]), xc.T @ (y - y.mean()))
    return w, y.mean() - m @ w

def blocks(n, k=5):
    size = -(-n // k)
    return [np.arange(i * size, min(n, (i + 1) * size)) for i in range(k)]

def blocked(X, y, lam, k=5):
    """Accuracy and squared error on contiguous held-out blocks: a score above 0.5 means 1.
    lam="gap" chooses the demo's penalty afresh from each training set."""
    hits = err = 0.0
    for test in blocks(len(y), k):
        train = np.setdiff1d(np.arange(len(y)), test)
        penalty = gap_ridge(X[train]) if lam == "gap" else lam
        w, b = fit(X[train], y[train], penalty)
        s = X[test] @ w + b
        hits += np.sum((s > 0.5) == (y[test] == 1))
        err += np.sum((s - y[test]) ** 2)
    return hits / len(y), err / len(y)

grid = [10.0 ** p for p in range(5, 11)]
for lam in [gap] + grid:
    acc, mse = blocked(X, y, lam)
    print(f"lambda {lam:8.2g}  effective df {np.sum(ev / (ev + lam)):5.1f}  "
          f"accuracy {acc:.2f}  mse {mse:.3f}")

# Nested: choose lambda inside each outer training set, then score the outer block.
hits, chosen = 0, []
for test in blocks(len(y)):
    train = np.setdiff1d(np.arange(len(y)), test)
    best = min(grid, key=lambda lam: blocked(X[train], y[train], lam, k=4)[1])
    w, b = fit(X[train], y[train], best)
    hits += np.sum((X[test] @ w + b > 0.5) == (y[test] == 1))
    chosen.append(f"{best:.0e}")
print(f"nested: accuracy {hits / len(y):.2f}, lambdas chosen {chosen}")

# Shrinkage LDA, its shrinkage set analytically from each training set (Ledoit and Wolf).
hits = 0
for test in blocks(len(y)):
    train = np.setdiff1d(np.arange(len(y)), test)
    lda = LinearDiscriminantAnalysis(solver="lsqr", shrinkage="auto").fit(X[train], y[train])
    hits += np.sum(lda.predict(X[test]) == y[test])
print(f"shrinkage LDA: accuracy {hits / len(y):.2f}")

# Cut to the light switch's size: 88 windows held out a round of 11 at a time, so every
# training set has 77 windows for 76 weights.
for lam in ("gap", 1e8, 1e9):
    acc = blocked(X[:88], y[:88], lam, k=8)[0]
    name = lam if lam == "gap" else f"{lam:.0e}"
    print(f"88 windows in 8 folds, lambda {name}: accuracy {acc:.2f}")

You should seeThe trainer's penalty reproduced exactly, 20,131.58, between eigenvalues of 5.07 and 7.99 × 10⁷, and the smallest one's direction all ones. At that penalty the fit spends 75 of its 76 degrees of freedom and scores 0.67. Squared error is lowest near 10⁹, at 31 degrees of freedom and 0.70. Nested cross-validation chooses 10⁸ or 10⁹ and scores 0.71, and shrinkage LDA 0.69. Cut to 88 windows held out a round of 11 at a time, the trainer's penalty scores 0.50, chance, where 10⁸ scores 0.61 and 10⁹ 0.66.

Then report: how large a difference in accuracy 150 windows can resolve, why the trainer's penalty does well at 150 windows and fails at 88, and what penalty you would choose without labels.

Open problems

  1. Small data. With the few trials of a calibration session, linear decoders with shrinkage remain hard to beat, and whether more flexible models help depends on how much data a person can give (Lotte and others, 2018).
  2. Reading a head. Patterns in a pretrained embedding don't map to sources, and how to say what a head on such an embedding responds to is open.
  3. The penalty over time. The best penalty for one session needn't be the best for the next, when the data have shifted. How to regularize for the session to come rather than the one at hand is unsettled.

Problems

  1. Show that least squares on labels of 0 and 1 gives a weight vector parallel to Fisher's discriminant.

  2. How much of the least-squares fit does ridge keep along a direction whose eigenvalue equals λ? Along one ten times larger, and ten times smaller?

  3. The demo's penalty uses no labels. Why does that matter for its test against chance?

  4. A decoder puts its largest weight on feature 3. Can you conclude that feature 3 carries the signal? What would you compute instead?

  5. The light switch keeps 88 windows in eight rounds of 11. Its test holds out one round at a time. How many windows does each training set have, and why does the trainer refuse fewer than 77?

Sources

Fisher, R. A. (1936). The use of multiple measurements in taxonomic problems. Annals of Eugenics.

Hoerl, A. E., and Kennard, R. W. (1970). Ridge regression: biased estimation for nonorthogonal problems. Technometrics.

Duda, R. O., Hart, P. E., and Stork, D. G. (2001). Pattern Classification, second edition. Wiley.

Ledoit, O., and Wolf, M. (2004). A well-conditioned estimator for large-dimensional covariance matrices. Journal of Multivariate Analysis.

Varma, S., and Simon, R. (2006). Bias in error estimation when using cross-validation for model selection. BMC Bioinformatics.

Hastie, T., Tibshirani, R., and Friedman, J. (2009). The Elements of Statistical Learning, second edition. Springer.

Cawley, G. C., and Talbot, N. L. C. (2010). On over-fitting in model selection and subsequent selection bias in performance evaluation. Journal of Machine Learning Research.

Blankertz, B., Lemm, S., Treder, M., Haufe, S., and Müller, K.-R. (2011). Single-trial analysis and classification of ERP components: a tutorial. NeuroImage.

Haufe, S., and others (2014). On the interpretation of weight vectors of linear models in multivariate neuroimaging. NeuroImage.

Lotte, F., and others (2018). A review of classification algorithms for EEG-based brain-computer interfaces: a 10 year update. Journal of Neural Engineering.

Belkin, M., Hsu, D., Ma, S., and Mandal, S. (2019). Reconciling modern machine-learning practice and the classical bias-variance trade-off. Proceedings of the National Academy of Sciences.

Hastie, T., Montanari, A., Rosset, S., and Tibshirani, R. J. (2022). Surprises in high-dimensional ridgeless least squares interpolation. Annals of Statistics.