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)

paradigm = FilterBankMotorImagery(n_classes=4, filters=filters)
print(f"\nParadigm: {paradigm}")
print("Number of classes: 4 (left hand, right hand, feet, tongue)")
2026-09-27 20:08:57,505 WARNING MainThread moabb.paradigms.motor_imagery Choosing from all possible events

Paradigm: <moabb.paradigms.motor_imagery.FilterBankMotorImagery object at 0x7fa38765c210>
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:

  1. Tensor Stacking: Organizes multi-band covariance matrices

  2. BiMap Layers: Learns spatial filters on the SPD manifold

  3. Temporal Convolution: Captures temporal dynamics

  4. 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:

  1. Loads the data for the subject

  2. Splits into training and test sets (using session info)

  3. Trains the model

  4. 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-27 20:08:57,557 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-92e930b...
2026-09-27 20:08:57,569 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:08:57,569 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-5cbba46...
2026-09-27 20:08:57,580 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:08:57,580 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-a8183bc...
2026-09-27 20:08:57,589 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:08:57,590 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-27 20:08:57,600 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:08:59,704 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-a8183bc
2026-09-27 20:09:00,526 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-a8183bc to disk.
2026-09-27 20:09:00,648 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-5cbba46
2026-09-27 20:09:00,785 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-5cbba46 to disk.
2026-09-27 20:09:00,799 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-92e930b
2026-09-27 20:09:00,863 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-92e930b to disk.
2026-09-27 20:09:00,913 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-f9436a6...
2026-09-27 20:09:00,926 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:00,927 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-73a7b50...
2026-09-27 20:09:00,940 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:00,940 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-7062246...
2026-09-27 20:09:00,953 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:00,953 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-27 20:09:00,966 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:02,945 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-7062246
2026-09-27 20:09:03,738 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-7062246 to disk.
2026-09-27 20:09:03,883 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-73a7b50
2026-09-27 20:09:04,062 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-73a7b50 to disk.
2026-09-27 20:09:04,084 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-f9436a6
2026-09-27 20:09:04,169 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-f9436a6 to disk.
2026-09-27 20:09:04,266 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-bc42dd1...
2026-09-27 20:09:04,291 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:04,291 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-efe5e4e...
2026-09-27 20:09:04,317 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:04,317 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-c308d79...
2026-09-27 20:09:04,342 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:04,343 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-27 20:09:04,367 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:06,871 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-c308d79
2026-09-27 20:09:07,630 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-c308d79 to disk.
2026-09-27 20:09:07,738 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-efe5e4e
2026-09-27 20:09:07,857 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-efe5e4e to disk.
2026-09-27 20:09:07,872 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-bc42dd1
2026-09-27 20:09:07,944 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-bc42dd1 to disk.
2026-09-27 20:09:08,047 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-03fe173...
2026-09-27 20:09:08,072 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:08,072 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-9989138...
2026-09-27 20:09:08,093 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:08,093 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-f682d0a...
2026-09-27 20:09:08,111 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:08,112 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-27 20:09:08,130 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:10,660 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-f682d0a
2026-09-27 20:09:11,679 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-f682d0a to disk.
2026-09-27 20:09:11,833 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-9989138
2026-09-27 20:09:11,999 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-9989138 to disk.
2026-09-27 20:09:12,018 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-03fe173
2026-09-27 20:09:12,102 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-03fe173 to disk.
2026-09-27 20:09:12,192 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-ac5538b...
2026-09-27 20:09:12,224 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:12,224 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-36eabdd...
2026-09-27 20:09:12,257 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:12,258 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-ce48bf3...
2026-09-27 20:09:12,290 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:12,291 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-27 20:09:12,314 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:14,309 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-ce48bf3
2026-09-27 20:09:15,294 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-ce48bf3 to disk.
2026-09-27 20:09:15,436 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-36eabdd
2026-09-27 20:09:15,629 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-36eabdd to disk.
2026-09-27 20:09:15,645 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-ac5538b
2026-09-27 20:09:15,732 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-ac5538b to disk.
2026-09-27 20:09:15,889 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-053b497...
2026-09-27 20:09:15,928 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:15,929 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-d54ce61...
2026-09-27 20:09:15,970 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:15,970 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-b69a3f0...
2026-09-27 20:09:16,010 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:16,010 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-27 20:09:16,048 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:18,584 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-b69a3f0
2026-09-27 20:09:19,746 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-b69a3f0 to disk.
2026-09-27 20:09:19,887 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-d54ce61
2026-09-27 20:09:20,061 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-d54ce61 to disk.
2026-09-27 20:09:20,079 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-053b497
2026-09-27 20:09:20,549 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-053b497 to disk.
2026-09-27 20:09:20,731 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-ffcc745...
2026-09-27 20:09:20,774 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:20,774 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-79e9b34...
2026-09-27 20:09:20,818 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:20,818 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-1139ab5...
2026-09-27 20:09:20,862 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:20,862 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-27 20:09:20,907 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:23,444 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-1139ab5
2026-09-27 20:09:24,561 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-1139ab5 to disk.
2026-09-27 20:09:24,691 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-79e9b34
2026-09-27 20:09:24,858 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-79e9b34 to disk.
2026-09-27 20:09:24,873 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-ffcc745
2026-09-27 20:09:25,032 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-ffcc745 to disk.
2026-09-27 20:09:25,224 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-6de603b...
2026-09-27 20:09:25,273 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:25,274 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-a0e7878...
2026-09-27 20:09:25,327 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:25,328 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-d1f4bca...
2026-09-27 20:09:25,376 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:25,376 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-27 20:09:25,423 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:27,937 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-d1f4bca
2026-09-27 20:09:29,215 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-d1f4bca to disk.
2026-09-27 20:09:29,348 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-a0e7878
2026-09-27 20:09:29,581 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-a0e7878 to disk.
2026-09-27 20:09:29,598 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-6de603b
2026-09-27 20:09:29,689 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-array desc-6de603b to disk.
2026-09-27 20:09:29,904 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-array desc-551e6d2...
2026-09-27 20:09:29,959 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:29,960 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-epo desc-66a4913...
2026-09-27 20:09:30,016 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:30,016 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-33f537d...
2026-09-27 20:09:30,071 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:30,071 INFO MainThread moabb.datasets.bids_interface Attempting to retrieve cache of 'BNCI2014-001' sub-1 suffix-eeg desc-8b6883c...
2026-09-27 20:09:30,124 INFO MainThread moabb.datasets.bids_interface No cache found at /home/runner/mne_data/MNE-BIDS-bnci2014-001/code.
2026-09-27 20:09:32,666 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-eeg desc-33f537d
2026-09-27 20:09:33,734 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-eeg desc-33f537d to disk.
2026-09-27 20:09:33,862 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-epo desc-66a4913
2026-09-27 20:09:34,034 INFO MainThread moabb.datasets.bids_interface Finished caching 'BNCI2014-001' sub-1 suffix-epo desc-66a4913 to disk.
2026-09-27 20:09:34,054 INFO MainThread moabb.datasets.bids_interface Starting caching 'BNCI2014-001' sub-1 suffix-array desc-551e6d2
2026-09-27 20:09:34,151 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-27 20:09:35,229 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.2422        3.5565       0.2414        1.6054  4.8997
      2       0.4453        1.2223       0.4828        1.4418  4.9057
      3       0.5469        1.1198       0.4828        0.9706  4.9102
      4       0.7695        0.6544       0.4483        1.0012  4.9623
      5       0.8789        0.4465       0.6207        0.7824  4.6106
      6       0.9844        0.2925       0.7241        0.7248  4.5189
      7       0.9883        0.2219       0.6552        0.7694  4.0231
      8       0.9961        0.1521       0.6552        0.7455  4.5234
      9       0.9961        0.1104       0.7241        0.6381  4.0444
     10       1.0000        0.0802       0.5517        0.8872  5.1779
     11       1.0000        0.0751       0.6897        0.6918  5.1953
     12       1.0000        0.0464       0.7931        0.5959  5.0497
     13       1.0000        0.0328       0.6552        0.6192  4.5883
     14       1.0000        0.0268       0.6897        0.6777  4.9941
     15       1.0000        0.0215       0.6897        0.6176  4.3162

Results for Subject 1:
  Train Accuracy: 96.88%
  Test Accuracy:  78.47%

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()
Training and Validation Loss, Training and Validation Accuracy

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()
Confusion Matrix - Subject 1 Test Accuracy: 78.47%

Summary#

In this tutorial, we demonstrated how to:

  1. Load and prepare filter bank motor imagery data using MOABB

  2. Create a TensorCSPNet model for multi-frequency EEG classification

  3. Train and evaluate the model using Braindecode’s EEGClassifier

  4. 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 55.285 seconds)