Note
Go to the end to download the full example code.
Filter Bank Motor Imagery with TensorCSPNet#
This tutorial demonstrates how to use TensorCSPNet for motor imagery classification with filter bank features. TensorCSPNet is designed to process multi-frequency EEG data by stacking covariance matrices from different frequency bands into a tensor structure.
Introduction#
Motor imagery (MI) is a mental process where a person imagines performing a motor action without actually executing it. EEG-based brain-computer interfaces (BCIs) can decode these imagined movements to control devices.
Filter bank approaches decompose the EEG signal into multiple frequency bands, allowing the model to capture frequency-specific spatial patterns. TensorCSPNet [Ju and Guan, 2023] leverages this by creating SPD (Symmetric Positive Definite) covariance matrices for each frequency band and processing them through a geometry-aware neural network.
Setup and Imports#
First, we import the necessary libraries. We use:
MOABB: For loading standardized EEG datasets
Braindecode: For the EEGClassifier wrapper
SPD Learn: For the TensorCSPNet model
scikit-learn: For evaluation metrics and pipelines
import warnings
import matplotlib.pyplot as plt
import moabb
import torch
from braindecode import EEGClassifier
from einops.layers.torch import Rearrange
from moabb.datasets import BNCI2014_001
from moabb.paradigms import FilterBankMotorImagery
from sklearn.metrics import ConfusionMatrixDisplay, accuracy_score, confusion_matrix
from sklearn.preprocessing import LabelEncoder
from skorch.callbacks import EpochScoring
from skorch.dataset import ValidSplit
from torch import nn
from spd_learn.models import TensorCSPNet
# Set logging and ignore warnings for cleaner output
moabb.set_log_level("info")
warnings.filterwarnings("ignore")
Loading the Dataset#
We use the BCI Competition IV Dataset 2a (BNCI2014_001) [Tangermann et al., 2012], which contains EEG recordings from 9 subjects performing 4 different motor imagery tasks:
Left hand movement
Right hand movement
Both feet movement
Tongue movement
The dataset has 22 EEG channels and was recorded at 250 Hz.
dataset = BNCI2014_001()
print(f"Dataset: {dataset.code}")
print(f"Number of subjects: {len(dataset.subject_list)}")
print("Number of sessions per subject: 2 (train + test)")
Dataset: BNCI2014-001
Number of subjects: 9
Number of sessions per subject: 2 (train + test)
Defining the Filter Bank#
We define a filter bank covering the mu (8-12 Hz) and beta (12-30 Hz) rhythms, which are known to be modulated during motor imagery [Pfurtscheller and Lopes da Silva, 1999]. Each filter extracts a specific frequency band from the EEG signal.
filters = [
[4, 8], # Theta band
[8, 12], # Mu/Alpha band
[12, 16], # Low beta
[16, 20], # Mid beta
[20, 24], # High beta
[24, 28], # Beta/Gamma transition
[28, 32], # Low gamma
[32, 36], # Gamma
[36, 40], # High gamma
]
print(f"Number of frequency bands: {len(filters)}")
print("Frequency bands (Hz):")
for i, (low, high) in enumerate(filters):
print(f" Band {i + 1}: {low}-{high} Hz")
Number of frequency bands: 9
Frequency bands (Hz):
Band 1: 4-8 Hz
Band 2: 8-12 Hz
Band 3: 12-16 Hz
Band 4: 16-20 Hz
Band 5: 20-24 Hz
Band 6: 24-28 Hz
Band 7: 28-32 Hz
Band 8: 32-36 Hz
Band 9: 36-40 Hz
Setting up the Paradigm#
The FilterBankMotorImagery paradigm from MOABB handles:
Filtering the data into multiple frequency bands
Extracting epochs around motor imagery events
Organizing data in the format (n_trials, n_channels, n_times, n_filters)
2026-09-14 10:30:33,515 WARNING MainThread moabb.paradigms.motor_imagery Choosing from all possible events
Paradigm: <moabb.paradigms.motor_imagery.FilterBankMotorImagery object at 0x7f79f0881590>
Number of classes: 4 (left hand, right hand, feet, tongue)
Creating the TensorCSPNet Model#
TensorCSPNet [Ju and Guan, 2023] is a deep learning architecture designed for filter bank EEG classification. The architecture consists of:
Tensor Stacking: Organizes multi-band covariance matrices
BiMap Layers: Learns spatial filters on the SPD manifold
Temporal Convolution: Captures temporal dynamics
Classification Head: Final prediction layer
Note
The input to TensorCSPNet has shape (batch, channels, time, frequencies). We use einops to rearrange from MOABB’s format (batch, channels, time, freq) to the expected format.
# Training hyperparameters
batch_size = 16
max_epochs = 15 # Reduced from 50 for faster documentation build
learning_rate = 1e-3
# Check for GPU availability
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"\nUsing device: {device}")
# Create the model pipeline
# We wrap TensorCSPNet with a Rearrange layer to handle the input format
model = nn.Sequential(
Rearrange("b c t f -> b f c t"), # Rearrange to (batch, freq, channels, time)
TensorCSPNet(
n_chans=22, # Number of EEG channels
n_outputs=4, # Number of classes
n_freqs=len(filters), # Number of frequency bands
),
)
print("\nModel architecture:")
print(model)
Using device: cpu
Model architecture:
Sequential(
(0): Rearrange('b c t f -> b f c t')
(1): TensorCSPNet(
(temporal_segmenter): Sequential(
(0): Rearrange('batch freq chans time-> (batch freq) chans time')
(1): PatchEmbeddingLayer()
)
(cov): CovLayer()
(tensor_stack): Rearrange('batch windows_index freq_index chans1 chans2 ->batch (windows_index freq_index) chans1 chans2')
(bimap_block): Sequential(
(0): BiMap(
(increase_dim): BiMapIncreaseDim()
)
(1): ReEig()
(2): BiMap()
(3): Rearrange('batch depth cov1 cov2 -> (batch depth) cov1 cov2')
(4): ParametrizedSPDBatchNormMean(
(parametrizations): ModuleDict(
(bias): ParametrizationList(
(0): SymmetricPositiveDefinite()
)
)
)
(5): ReEig()
)
(log_eig): LogEig()
(temporal_block): Conv2d(1, 16, kernel_size=(4, 4356), stride=(1, 484))
(final_layers): Linear(in_features=16, out_features=4, bias=True)
)
)
Setting up the Classifier#
We use Braindecode’s EEGClassifier, which is built on top of skorch and provides a scikit-learn compatible interface for training PyTorch models.
clf = EEGClassifier(
model,
criterion=torch.nn.CrossEntropyLoss,
optimizer=torch.optim.AdamW,
optimizer__lr=learning_rate,
optimizer__weight_decay=1e-4,
train_split=ValidSplit(0.1, stratified=True, random_state=42),
batch_size=batch_size,
max_epochs=max_epochs,
callbacks=[
(
"train_acc",
EpochScoring(
"accuracy", lower_is_better=False, on_train=True, name="train_acc"
),
),
],
device=device,
verbose=1,
)
Training and Evaluation Function#
We define a function to train and evaluate the model on a single subject. This function:
Loads the data for the subject
Splits into training and test sets (using session info)
Trains the model
Evaluates on both train and test sets
def evaluate_subject(subject: int) -> dict:
"""Train and evaluate TensorCSPNet on a single subject.
Parameters
----------
subject : int
Subject ID to evaluate.
Returns
-------
dict
Dictionary containing accuracy scores and predictions.
"""
print(f"\n{'=' * 50}")
print(f"Evaluating Subject {subject}")
print(f"{'=' * 50}")
# Cache configuration for faster repeated runs
cache_config = dict(
save_raw=True,
save_epochs=True,
save_array=True,
use=True,
overwrite_raw=False,
overwrite_epochs=False,
overwrite_array=False,
)
# Load data for this subject
X, labels, meta = paradigm.get_data(
dataset=dataset, subjects=[subject], cache_config=cache_config
)
# Encode labels to integers
le = LabelEncoder()
y = le.fit_transform(labels)
print(f"Data shape: {X.shape}")
print(f"Labels: {le.classes_}")
# Split into train and test using session information
# Session '0train' is for training, '1test' is for testing
train_idx = meta.query("session == '0train'").index.to_numpy()
test_idx = meta.query("session == '1test'").index.to_numpy()
print(f"Training samples: {len(train_idx)}")
print(f"Test samples: {len(test_idx)}")
# Train the model
clf.fit(X[train_idx], y[train_idx])
# Get predictions
y_pred_train = clf.predict(X[train_idx])
y_pred_test = clf.predict(X[test_idx])
# Calculate accuracies
train_acc = accuracy_score(y[train_idx], y_pred_train)
test_acc = accuracy_score(y[test_idx], y_pred_test)
print(f"\nResults for Subject {subject}:")
print(f" Train Accuracy: {train_acc * 100:.2f}%")
print(f" Test Accuracy: {test_acc * 100:.2f}%")
return {
"subject": subject,
"train_acc": train_acc,
"test_acc": test_acc,
"y_true_test": y[test_idx],
"y_pred_test": y_pred_test,
"label_encoder": le,
"history": clf.history,
}
Running the Evaluation#
For demonstration purposes, we evaluate on a single subject. In practice, you would loop over all subjects for a complete benchmark.
Note
Training deep learning models on EEG data can take several minutes per subject, depending on your hardware.
# Evaluate on subject 1 (you can change this or loop over all subjects)
subject_id = 1
results = evaluate_subject(subject_id)
==================================================
Evaluating Subject 1
==================================================
2026-09-14 10:30:33,578 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-92e930b...
2026-09-14 10:30:33,595 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:33,595 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-5cbba46...
2026-09-14 10:30:33,612 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:33,612 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-a8183bc...
2026-09-14 10:30:33,627 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:33,627 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-14 10:30:33,642 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:36,169 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-a8183bc
2026-09-14 10:30:37,245 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-a8183bc to disk.
2026-09-14 10:30:37,410 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-5cbba46
2026-09-14 10:30:37,593 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-5cbba46 to disk.
2026-09-14 10:30:37,611 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-92e930b
2026-09-14 10:30:37,728 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-92e930b to disk.
2026-09-14 10:30:37,810 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-f9436a6...
2026-09-14 10:30:37,834 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:37,835 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-73a7b50...
2026-09-14 10:30:37,859 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:37,859 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-7062246...
2026-09-14 10:30:37,881 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:37,882 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-14 10:30:37,903 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:40,371 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-7062246
2026-09-14 10:30:41,369 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-7062246 to disk.
2026-09-14 10:30:41,548 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-73a7b50
2026-09-14 10:30:41,750 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-73a7b50 to disk.
2026-09-14 10:30:41,769 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-f9436a6
2026-09-14 10:30:41,866 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-f9436a6 to disk.
2026-09-14 10:30:41,965 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-bc42dd1...
2026-09-14 10:30:41,992 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:41,992 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-efe5e4e...
2026-09-14 10:30:42,019 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:42,019 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-c308d79...
2026-09-14 10:30:42,044 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:42,044 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-14 10:30:42,069 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:44,535 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-c308d79
2026-09-14 10:30:45,533 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-c308d79 to disk.
2026-09-14 10:30:45,692 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-efe5e4e
2026-09-14 10:30:45,894 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-efe5e4e to disk.
2026-09-14 10:30:45,917 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-bc42dd1
2026-09-14 10:30:46,026 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-bc42dd1 to disk.
2026-09-14 10:30:46,146 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-03fe173...
2026-09-14 10:30:46,178 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:46,178 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-9989138...
2026-09-14 10:30:46,209 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:46,210 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-f682d0a...
2026-09-14 10:30:46,240 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:46,241 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-14 10:30:46,271 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:48,743 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-f682d0a
2026-09-14 10:30:49,782 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-f682d0a to disk.
2026-09-14 10:30:49,956 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-9989138
2026-09-14 10:30:50,140 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-9989138 to disk.
2026-09-14 10:30:50,159 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-03fe173
2026-09-14 10:30:50,254 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-03fe173 to disk.
2026-09-14 10:30:50,391 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-ac5538b...
2026-09-14 10:30:50,427 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:50,427 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-36eabdd...
2026-09-14 10:30:50,465 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:50,465 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-ce48bf3...
2026-09-14 10:30:50,499 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:50,500 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-14 10:30:50,534 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:53,001 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-ce48bf3
2026-09-14 10:30:54,029 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-ce48bf3 to disk.
2026-09-14 10:30:54,182 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-36eabdd
2026-09-14 10:30:54,367 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-36eabdd to disk.
2026-09-14 10:30:54,390 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-ac5538b
2026-09-14 10:30:54,487 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-ac5538b to disk.
2026-09-14 10:30:54,649 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-053b497...
2026-09-14 10:30:54,695 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:54,696 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-d54ce61...
2026-09-14 10:30:54,737 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:54,738 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-b69a3f0...
2026-09-14 10:30:54,780 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:54,781 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-14 10:30:54,823 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:57,308 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-b69a3f0
2026-09-14 10:30:58,341 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-b69a3f0 to disk.
2026-09-14 10:30:58,509 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-d54ce61
2026-09-14 10:30:58,752 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-d54ce61 to disk.
2026-09-14 10:30:58,770 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-053b497
2026-09-14 10:30:58,879 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-053b497 to disk.
2026-09-14 10:30:59,078 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-ffcc745...
2026-09-14 10:30:59,130 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:59,130 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-79e9b34...
2026-09-14 10:30:59,183 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:59,183 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-1139ab5...
2026-09-14 10:30:59,233 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:30:59,234 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-14 10:30:59,283 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:31:01,279 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-1139ab5
2026-09-14 10:31:02,158 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-1139ab5 to disk.
2026-09-14 10:31:02,258 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-79e9b34
2026-09-14 10:31:02,385 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-79e9b34 to disk.
2026-09-14 10:31:02,400 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-ffcc745
2026-09-14 10:31:02,714 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-ffcc745 to disk.
2026-09-14 10:31:02,925 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-6de603b...
2026-09-14 10:31:02,975 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:31:02,975 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-a0e7878...
2026-09-14 10:31:03,016 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:31:03,017 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-d1f4bca...
2026-09-14 10:31:03,048 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:31:03,048 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-14 10:31:03,085 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:31:04,987 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-d1f4bca
2026-09-14 10:31:06,025 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-d1f4bca to disk.
2026-09-14 10:31:06,187 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-a0e7878
2026-09-14 10:31:06,404 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-a0e7878 to disk.
2026-09-14 10:31:06,421 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-6de603b
2026-09-14 10:31:06,530 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-6de603b to disk.
2026-09-14 10:31:06,770 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-551e6d2...
2026-09-14 10:31:06,828 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:31:06,828 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-66a4913...
2026-09-14 10:31:06,887 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:31:06,888 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-33f537d...
2026-09-14 10:31:06,951 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:31:06,951 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-14 10:31:07,009 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-14 10:31:09,159 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-33f537d
2026-09-14 10:31:10,094 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-33f537d to disk.
2026-09-14 10:31:10,239 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-66a4913
2026-09-14 10:31:10,438 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-66a4913 to disk.
2026-09-14 10:31:10,457 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-551e6d2
2026-09-14 10:31:10,563 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-551e6d2 to disk.
Data shape: (576, 22, 1001, 9)
Labels: ['feet' 'left_hand' 'right_hand' 'tongue']
Training samples: 288
Test samples: 288
2026-09-14 10:31:11,465 INFO MainThread braindecode.eegneuralnet.EEGClassifier The module passed is already initialized which is not recommended. Instead, you can pass the module class and its parameters separately.
For more details, see https://skorch.readthedocs.io/en/stable/user/neuralnet.html#module
Skipping setting signal-related parameters from data.
epoch train_acc train_loss valid_acc valid_loss dur
------- ----------- ------------ ----------- ------------ ------
1 0.2812 2.0603 0.2759 1.8911 3.8735
2 0.6484 0.9449 0.4483 1.0051 4.7421
3 0.7969 0.6596 0.5862 0.9005 4.0796
4 0.8672 0.4737 0.5517 0.9228 3.8384
5 0.9805 0.2615 0.7931 0.6554 3.1556
6 1.0000 0.1856 0.5517 0.8386 3.1462
7 1.0000 0.1035 0.7931 0.5914 3.1708
8 1.0000 0.0711 0.7241 0.5712 3.1512
9 1.0000 0.0449 0.7586 0.5669 3.1808
10 1.0000 0.0338 0.7241 0.6416 3.1642
11 1.0000 0.0265 0.7931 0.5621 3.1587
12 1.0000 0.0188 0.7586 0.5135 3.4377
13 1.0000 0.0167 0.7586 0.5629 3.1937
14 1.0000 0.0136 0.7241 0.5298 3.1893
15 1.0000 0.0113 0.7931 0.5345 3.2230
Results for Subject 1:
Train Accuracy: 97.92%
Test Accuracy: 77.43%
Visualizing Training History#
Let’s plot the training and validation loss curves to understand how the model learned over epochs.
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# Extract history
history = results["history"]
epochs = range(1, len(history) + 1)
# Plot loss
ax1 = axes[0]
ax1.plot(epochs, history[:, "train_loss"], "b-", label="Train Loss", linewidth=2)
ax1.plot(epochs, history[:, "valid_loss"], "r--", label="Valid Loss", linewidth=2)
ax1.set_xlabel("Epoch", fontsize=12)
ax1.set_ylabel("Loss", fontsize=12)
ax1.set_title("Training and Validation Loss", fontsize=14)
ax1.legend(fontsize=10)
ax1.grid(True, alpha=0.3)
# Plot accuracy
ax2 = axes[1]
ax2.plot(epochs, history[:, "train_acc"], "b-", label="Train Acc", linewidth=2)
ax2.plot(epochs, history[:, "valid_acc"], "r--", label="Valid Acc", linewidth=2)
ax2.set_xlabel("Epoch", fontsize=12)
ax2.set_ylabel("Accuracy", fontsize=12)
ax2.set_title("Training and Validation Accuracy", fontsize=14)
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3)
ax2.set_ylim([0, 1])
plt.tight_layout()
plt.show()

Confusion Matrix#
The confusion matrix shows how well the model distinguishes between different motor imagery classes.
fig, ax = plt.subplots(figsize=(8, 6))
# Get class names
class_names = results["label_encoder"].classes_
# Compute confusion matrix
cm = confusion_matrix(results["y_true_test"], results["y_pred_test"])
# Plot
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=class_names)
disp.plot(ax=ax, cmap="Blues", values_format="d")
ax.set_title(
f"Confusion Matrix - Subject {subject_id}\n"
f"Test Accuracy: {results['test_acc'] * 100:.2f}%",
fontsize=14,
)
plt.tight_layout()
plt.show()

Summary#
In this tutorial, we demonstrated how to:
Load and prepare filter bank motor imagery data using MOABB
Create a TensorCSPNet model for multi-frequency EEG classification
Train and evaluate the model using Braindecode’s EEGClassifier
Visualize training history and confusion matrices
TensorCSPNet leverages the geometry of SPD matrices to learn discriminative spatial filters across multiple frequency bands, making it well-suited for motor imagery classification.
Total running time of the script: (1 minutes 34.830 seconds)