braindecode.models.BBL#

class braindecode.models.BBL(n_outputs=None, n_chans=None, chs_info=None, n_times=None, input_window_seconds=None, sfreq=None, *, n_sensors=306, n_datasets=4, n_subjects=4096, subject_dim=16, model_dim=512, n_filters=128, ratios=(4, 2, 2), kernel_size=7, last_kernel_size=7, activation=<class 'torch.nn.modules.activation.GELU'>)[source]#

BBL, the MEG encoder of The Brain’s Bitter Lesson from Jayalath et al. (2025) [bbl].

Foundation Model Convolution Recurrent

Architecture Overview

A dataset-conditional convolutional projector, an EnCodec SEANet encoder averaged over time, and a learned subject embedding feed a linear head:

(batch, n_chans, n_times) -> zero-pad to n_sensors -> projector ->
dataset gate -> SEANetEncoder -> mean over time -> concat subject
embedding -> (batch, n_outputs)

Macro Components

BBL.projector

Operations. Conv1d(n_sensors, n_datasets * model_dim, 3) -> activation -> Conv1d(stride 2); the one-hot dataset_index keeps one model_dim slice. Role. Harmonizes sensor layouts across datasets.

BBL.encoder

Operations. SEANetEncoder (residual convolutions, downsampling by prod(ratios), two-layer LSTM). Role. Self-supervised backbone (band, phase and amplitude pretext tasks in the paper).

BBL.subject_embedding

Operations. Embedding(n_subjects, subject_dim) concatenated to the pooled features. Without subject_index, the mean embedding of the rows flagged in the trained_subjects buffer (the paper’s novel-subject fallback).

BBL.final_layer

Operations. Linear(model_dim + subject_dim, n_outputs), the paper’s linear probe.

Temporal, Spatial, and Spectral Encoding

  • Temporal: strided convolutions and the LSTM, then a mean over time.

  • Spatial: the projector mixes all (zero-padded) sensors.

  • Spectral: learned by the band and phase pretext tasks.

Important

The released checkpoint was pretrained on CamCAN (dataset slot 0), band-passed 0.1-125 Hz with 50 and 100 Hz notches, then resampled to 250 Hz, on 0.5 s windows (125 samples). Each window is scaled per channel by the median and interquartile range fitted on its recording, clipped to [-5, 5] and has its first 0.5 s mean subtracted, so inputs are dimensionless (authors’ data/preprocess.py and data/utils.py; forward only does the zero-padding to 306 sensors). Its projector expects CamCAN’s 306 Elekta MEG* channels in FIF order. subject_index is offset per dataset slot: subject s of slot d uses row d * 1024 + s. Only CamCAN subject rows of subject_embedding are trained, and trained_subjects flags them; the head is not pretrained. Set trained_subjects to the rows your own training used before relying on the mean-subject fallback. The converted checkpoint is on the Hugging Face Hub at braindecode/bbl-pretrained (revision 5a19a369):

from braindecode.models import BBL
model = BBL.from_pretrained("braindecode/bbl-pretrained", n_outputs=2)

Added in version 2.1.

Parameters:
  • n_outputs (int) – Number of outputs of the model. This is the number of classes in the case of classification.

  • n_chans (int) – Number of EEG channels.

  • chs_info (list of dict) – Information about each individual EEG channel. This should be filled with info["chs"]. Refer to mne.Info for more details.

  • n_times (int) – Number of time samples of the input window.

  • input_window_seconds (float) – Length of the input window in seconds.

  • sfreq (float) – Sampling frequency of the EEG recordings.

  • n_sensors (int) – Sensor slots of the projector; inputs with fewer channels are zero-padded, as the reference collator does (306: Elekta MEG).

  • n_datasets (int) – Dataset slots of the projector gate.

  • n_subjects (int) – Rows of the subject embedding (the release: 1024 per dataset slot).

  • subject_dim (int) – Subject embedding dimension.

  • model_dim (int) – Projector and encoder output dimension.

  • n_filters (int) – Channels after the first SEANet convolution.

  • ratios (tuple[int, ...]) – SEANet downsampling ratios.

  • kernel_size (int) – Kernel size of the first SEANet convolution.

  • last_kernel_size (int) – Kernel size of the last SEANet convolution.

  • activation (type[Module]) – Activation of the projector (the SEANet keeps EnCodec’s ELU).

Raises:

ValueError – If some input signal-related parameters are not specified: and can not be inferred.

Notes

If some input signal-related parameters are not specified, there will be an attempt to infer them from the other parameters.

References

[bbl]

Jayalath, D., Landau, G., Shillingford, B., Woolrich, M., Parker Jones, O. (2025). The Brain’s Bitter Lesson: Scaling Speech Decoding With Self-Supervised Learning. ICML 2025. https://arxiv.org/abs/2406.04328

License

Hugging Face Hub integration

When the optional huggingface_hub package is installed, all models automatically gain the ability to be pushed to and loaded from the Hugging Face Hub. Install with:

pip install braindecode[hub]

Pushing a model to the Hub:

from braindecode.models import BBL

# Train your model
model = BBL(n_chans=22, n_outputs=4, n_times=1000)
# ... training code ...

# Push to the Hub; ``datasets`` and ``base_model`` go to the card
model.push_to_hub(
    repo_id="username/my-bbl-model",
    commit_message="Initial model upload",
    model_card_kwargs={"datasets": ["BNCI2014_001"]},
)

Loading a model from the Hub:

from braindecode.models import BBL

# Load pretrained model
model = BBL.from_pretrained("username/my-bbl-model")
# Data the weights were trained on, from the Hub model card
print(model.card_data.datasets if model.card_data else None)

# Load with a different number of outputs (head is rebuilt automatically)
model = BBL.from_pretrained("username/my-bbl-model", n_outputs=4)

Extracting features and replacing the head:

import torch

x = torch.randn(1, model.n_chans, model.n_times)
# Extract encoder features (consistent dict across all models)
out = model(x, return_features=True)
features = out["features"]

# Replace the classification head
model.reset_head(n_outputs=10)

Saving and restoring full configuration:

import json

config = model.get_config()            # all __init__ params
with open("config.json", "w") as f:
    json.dump(config, f)

model2 = BBL.from_config(config)    # reconstruct (no weights)

All model parameters (both EEG-specific and model-specific such as dropout rates, activation functions, number of filters) are automatically saved to the Hub and restored when loading.

See Loading and Adapting Pretrained Foundation Models for a complete tutorial.

Methods

forward(x, subject_index=None, dataset_index=None, return_features=False, ch_mask=None, temporal_mask=None, return_temporal_features=False)[source]#

Classify x of shape (batch, n_chans, n_times).

subject_index and dataset_index are (batch,) integer tensors; dataset_index defaults to slot 0. ch_mask is an optional (batch, n_chans) mask of real channels, see check_ch_mask(): padded channels are set to zero, the empty sensor slots of the reference collator, so a recording padded in a batch gives its output alone. The projector mixes every sensor slot into each feature, so the model has no per-channel tokens. Shorter recordings occupy the first sensor slots; interior missing sensors must retain their original slots.

temporal_mask is an optional (batch, n_times) mask: True hides a raw time sample on every channel, unlike ch_mask where True means real. Hidden samples (including NaNs) become zero before any convolution or LSTM and receive no input gradient. All-hidden windows are allowed. Keep clean targets and mask sampling downstream; this is input corruption, not the paper’s band/amp/phase objectives.

With both return_features and return_temporal_features True, the dictionary also contains temporal_features, the actual encoder output before pooling, (batch, model_dim, n_steps). Here n_steps = ceil(n_times / temporal_stride) and temporal_stride is a scalar integer tensor, 2 * prod(ratios). temporal_bounds is (n_steps, 2): raw-time alignment bins [start, stop) clipped to n_times. temporal_valid is (batch, n_steps) and all True: no temporal padding/variable lengths are accepted, and the final partial bin is valid. These bins are not independent receptive fields: the LSTM carries prefix context and the noncausal/reflect convolutions mix neighboring/boundary samples. temporal_context_bounds, (n_steps, 2), conservatively bounds each feature’s raw-input receptive field by [0, n_times). It is not a reconstruction target alignment or an exact per-position attribution.

Parameters:
  • x (Tensor) – The description is missing.

  • subject_index (Optional[Tensor]) – The description is missing.

  • dataset_index (Optional[Tensor]) – The description is missing.

  • return_features (bool) – The description is missing.

  • ch_mask (Optional[Tensor]) – The description is missing.

  • temporal_mask (Optional[Tensor]) – The description is missing.

  • return_temporal_features (bool) – The description is missing.

Return type:

Union[Tensor, Dict[str, Optional[Tensor]]]

reset_head(n_outputs)[source]#

Replace the linear probe for n_outputs classes.

Parameters:

n_outputs (int) – New number of output classes.

Return type:

None

Examples

>>> from braindecode.models import BENDR
>>> model = BENDR(n_chans=22, n_times=1000, n_outputs=4)
>>> model.reset_head(10)
>>> model.n_outputs
10

Added in version 1.4.