↑↓ move↵ open[] previous / next lesson? all shortcuts
Appearance
Theme
Type size
Reading

Focus mode hides the sidebars. Press f to toggle it anywhere.

Keyboard shortcuts
/
Search
[ ]
Previous / next lesson
t
Jump to the table of contents
r
Review queue
f
Focus mode
?
This panel
Explainer18 min

Classification done honestly

Features, LDA, CSP, and Riemannian classifiers, then the part that matters more than the algorithm, which is how you split the data. Leakage, chance levels, calibration drift, and information transfer rate.

On this page18 min
  1. Features
  2. Three classifiers that cover the field
  3. Now the part that matters
  4. Information transfer rate

A learns a rule from labelled examples and applies it to new ones. Every is one. The algorithms are the easy part and this page covers them in a few paragraphs. The hard part, the part where most student projects and a distressing number of papers go wrong, is evaluation: making sure the number you report is the number a new person on a new day would get. That takes most of the page, because it deserves it.

Features

A wants a short vector of numbers per trial. For and oscillatory : band powers or log-variances per channel, sometimes after a spatial . For : the downsampled voltage waveform, channels concatenated. For : correlations with reference frequencies. Good features make simple work; no rescues bad features. The most reliable way to improve a is still to improve the signal.

Three classifiers that cover the field

fits a Gaussian to each class with a shared covariance and draws the boundary where they are equally likely. Few parameters, hard to overfit, fast, and the default in for two decades. With shrinkage regularization it handles more features than trials, which is the usual situation.

Each class gets a score from its mean, the shared covariance, and its prior:

δk(x)=x⊤Σ−1μk−12μk⊤Σ−1μk+log⁡πk\delta_k(x) = x^\top\Sigma^{-1}\mu_k - \tfrac{1}{2}\mu_k^\top\Sigma^{-1}\mu_k + \log \pi_k

Here μk\mu_k is the mean feature vector of class kk, Σ\Sigma the covariance shared across classes, and πk\pi_k the prior probability of class kk. A new trial goes to the class with the largest score, so the boundary sits where two scores tie.

is a feature extractor for two-class oscillatory problems: it finds spatial whose output variance is large for one class and small for the other. The log-variance of a few components, then , is the classic pipeline.

The objective is the ratio of the two class variances along the :

max⁡ww⊤Σ1ww⊤Σ2w\max_w \frac{w^\top \Sigma_1 w}{w^\top \Sigma_2 w}

Here Σ1\Sigma_1 and Σ2\Sigma_2 are the two classes’ channel covariance matrices and ww is the spatial . The optimum is a generalized eigenvector, computed in one call.

skip feature engineering: each trial is represented by its channel covariance matrix, which lives on a curved space where the natural distance is not Euclidean. Map the matrices to the tangent space at their mean and run logistic regression. This is the strongest simple baseline on nearly every public dataset and it is three lines in pyRiemann. Learn it after , and use it by default.

The distance between two trial covariance matrices on their curved space is:

dR(C1,C2)=∥log⁡(C1−1/2C2C1−1/2)∥Fd_R(C_1, C_2) = \lVert \log(C_1^{-1/2} C_2 C_1^{-1/2}) \rVert_F

Here C1C_1 and C2C_2 are the two trials’ covariance matrices, log⁡\log is the matrix logarithm, and FF is the Frobenius norm. Mapping every trial to the tangent space at the mean turns these distances into ordinary vectors for logistic regression.

from pyriemann.estimation import Covariances
from pyriemann.tangentspace import TangentSpace
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
clf = make_pipeline(Covariances('oas'), TangentSpace(), LogisticRegression())

Support vector machines, random forests, and neural networks exist too. On EEG-sized datasets they rarely beat the three above by more than noise, and they overfit more. The deep learning page is about when they do and do not help.

Now the part that matters

Figure 1. The same data, the same classifier, four ways of splitting it. The accuracy falls at every step to the right and only the rightmost one predicts what happens when you put the thing on a person tomorrow.
Predict before you look

You have 200 trials from one session. You shuffle them, train on 160, test on 40, and get 88 percent. Tomorrow, with the same classifier, what do you expect?

Substantially lower, and today’s data cannot tell you how much. Shuffled trials from one session share everything that drifts slowly: , the participant’s state, the exact cap position. The test set is not a new day; it is the same day with a few trials hidden. Only a second session tells you the cross-session number, and it is typically five to twenty points lower.

Leakage. Any information from the test set that reaches training is , and it inflates the number. Shuffling trials across sessions. Fitting the , the normalization, or the on all the data before splitting. Choosing hyperparameters on the test set. Choosing which subjects to include after seeing results. Each of these is common, each is easy to do by accident, and scikit-learn’s pipelines exist to prevent the second one: put every fitted step inside the pipeline so fits it on training folds only.

The right splits, in increasing honesty. Shuffled trials within a session: the wrong way, useful only to check the code runs. Grouped by run or block within a session: honest within-session. Train on session one, test on session two: the cross-session number, which is what a user experiences. Leave one subject out: the cross-subject number, which is what a -free product would need. Report the one that matches the claim you are making, and say which.

Chance is not 50 percent. With two classes and forty trials, a coin-flip scores above 62 percent one time in twenty. The binomial distribution gives the upper bound for any trial count; the statistics page has it. Always report the chance bound next to the accuracy. With unbalanced classes, use balanced accuracy or report the confusion matrix, because 80 percent on a task with 80 percent standards is chance.

Balanced accuracy averages the two per-class rates instead:

BA=sensitivity+specificity2\text{BA} = \frac{\text{sensitivity} + \text{specificity}}{2}

A majority-class guesser scores 50 percent on this measure no matter how unbalanced the task is.

and drift. Signals change within a session ( settle, attention fades) and between sessions (cap position, mood, sleep). A deployed recalibrates at the start of each session and often adapts during it. If your evaluation does not include the cost of that, it is not evaluating the system.

Information transfer rate

Accuracy alone misleads: 90 percent on two targets in ten seconds is worse than 70 percent on eight targets in three. Wolpaw’s information transfer rateBrain-computer interfaces for communication and controlWolpaw, Birbaumer, McFarland, et al. · Clin. Neurophysiol. · 2002The review that defined information transfer rate, the one number that compares spellers honestly. combines them. Bits per selection:

B=log⁡2N+Plog⁡2P+(1−P)log⁡21−PN−1B = \log_2 N + P\log_2 P + (1-P)\log_2\frac{1-P}{N-1}

for N targets and accuracy P, and ITR is B times selections per minute. A good speller reaches 40 to 60 bits per minute; a speller 10 to 25; under 10. Report it, and report the time per selection it assumes.

As a rate, with TT seconds per selection:

ITR=B×60T\text{ITR} = B \times \frac{60}{T}
Deep dive Why LDA works so well on EEG 3 min

features are roughly Gaussian, classes differ mostly in their means, and trials are few. ’s assumptions match, and its parameter count is small (a mean per class and one shared covariance), so it does not overfit. Shrinkage regularizes the covariance toward the identity, which is essential when features outnumber trials, and the shrinkage amount can be set analytically (Ledoit-Wolf) with no . It is hard to beat, and any method that claims to should be compared against it with the same split.

Deep dive Nested cross-validation for hyperparameters 3 min

If you tune anything (the number of components, the regularization strength) you must tune it inside each training fold, using an inner , and evaluate on the outer test fold that never saw the tuning. Tuning on the same folds you report is leakage and adds several points. scikit-learn’s GridSearchCV inside cross_val_score does this correctly.

Deep dive Transfer learning and calibration-free BCIs 3 min

Aligning each session’s covariance to a common reference (Riemannian alignment) before classification lets data from other sessions and other people help, and cuts time. It works better for and than for . The dream of a that works for a new person with no is the field’s current frontier, and it is measured by leave-one-subject-out accuracy, which is currently far below within-subject for everything but .

Recall
Name four ways data leakage happens in a BCI evaluation.
Shuffling trials across sessions into train and test; fitting preprocessing (CSP, normalization, ICA) on all data before splitting; tuning hyperparameters on the test set; selecting subjects or trials after seeing results.
Recall
Which split answers 'what will a user experience tomorrow', and which answers 'could this work without calibration'?
Train on session one, test on session two (cross-session); leave one subject out (cross-subject).
Recall
Why report information transfer rate rather than accuracy alone?
Accuracy ignores the number of targets and the time per selection; ITR combines all three into bits per minute so systems with different designs can be compared.
Explain it to your roommate

Explain what this page was about to your roommate in three sentences. No jargon they would not know.