braindecode 1.6.1.dev1052__tar.gz → 1.6.1.dev1053__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {braindecode-1.6.1.dev1052/braindecode.egg-info → braindecode-1.6.1.dev1053}/PKG-INFO +1 -1
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/eegneuralnet.py +41 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/__init__.py +2 -0
- braindecode-1.6.1.dev1053/braindecode/models/eegdino.py +533 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/summary.csv +1 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/util.py +1 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/__init__.py +2 -1
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/blocks.py +82 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/preprocessing/mne_preprocess.py +28 -1
- braindecode-1.6.1.dev1053/braindecode/version.py +1 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053/braindecode.egg-info}/PKG-INFO +1 -1
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode.egg-info/SOURCES.txt +1 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/api.rst +2 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/whats_new.rst +25 -0
- braindecode-1.6.1.dev1052/braindecode/version.py +0 -1
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/LICENSE.txt +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/MANIFEST.in +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/NOTICE.txt +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/README.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/__init__.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/augmentation/__init__.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/augmentation/base.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/augmentation/functional.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/augmentation/transforms.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/classifier.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/__init__.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/base.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/bbci.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/bcicomp.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/bids/__init__.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/bids/datasets.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/bids/format.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/bids/hub.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/bids/hub_format.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/bids/hub_io.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/bids/hub_validation.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/bids/iterable.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/chb_mit.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/mne.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/moabb.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/nmt.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/registry.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/siena.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/sleep_physio_challe_18.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/sleep_physionet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/tuh.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/utils.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datasets/xy.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datautil/__init__.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datautil/channel_utils.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datautil/hub_formats.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datautil/serialization.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/datautil/util.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/functional/__init__.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/functional/functions.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/functional/initialization.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/atcnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/attentionbasenet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/attn_sleep.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/base.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/bendr.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/biot.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/brainmodule.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/cbramod.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/codebrain.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/config.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/contrawr.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/ctnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/deep4.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/deepsleepnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/dgcnn.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/eegconformer.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/eeginception_erp.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/eeginception_mi.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/eegitnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/eegminer.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/eegnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/eegnex.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/eegpt.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/eegsimpleconv.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/eegsym.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/eegtcnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/emg2qwerty.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/fbcnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/fblightconvnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/fbmsnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/hybrid.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/ifnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/interpolated.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/labram.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/luna.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/medformer.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/meta_neuromotor.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/msvtnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/patchedtransformer.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/reve.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/sccnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/shallow_fbcsp.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/signal_jepa.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/sinc_shallow.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/sleep_stager_blanco_2020.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/sleep_stager_chambon_2018.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/sparcnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/sstdpn.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/syncnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/tcn.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/tidnet.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/tsinception.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/models/usleep.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/activation.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/attention.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/convolution.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/filter.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/interpolation.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/layers.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/linear.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/parametrization.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/stats.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/util.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/modules/wrapper.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/preprocessing/__init__.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/preprocessing/eegprep_preprocess.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/preprocessing/preprocess.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/preprocessing/util.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/preprocessing/windowers.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/regressor.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/samplers/__init__.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/samplers/base.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/samplers/ssl.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/training/__init__.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/training/callbacks.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/training/losses.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/training/scoring.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/util.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/visualization/__init__.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/visualization/attribution.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/visualization/confusion_matrices.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/visualization/frequency.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/visualization/metrics.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/visualization/sanity.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode/visualization/topology.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode.egg-info/dependency_links.txt +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode.egg-info/requires.txt +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/braindecode.egg-info/top_level.txt +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/Makefile +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/_templates/autosummary/class.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/_templates/autosummary/class_in_subdir.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/_templates/autosummary/function.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/_templates/autosummary/function_in_subdir.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/cite.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/conf.py +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/help.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/index.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/install/install.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/install/install_pip.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/install/install_source.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/categorization/attention.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/categorization/channel.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/categorization/convolution.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/categorization/filterbank.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/categorization/gnn.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/categorization/interpretable.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/categorization/lbm.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/categorization/recurrent.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/categorization/spd.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/models.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/models_categorization.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/models_table.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/models/models_visualization.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/docs/sg_execution_times.rst +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/pyproject.toml +0 -0
- {braindecode-1.6.1.dev1052 → braindecode-1.6.1.dev1053}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: braindecode
|
|
3
|
-
Version: 1.6.1.
|
|
3
|
+
Version: 1.6.1.dev1053
|
|
4
4
|
Summary: Deep learning software to decode EEG, ECG or MEG signals
|
|
5
5
|
Author-email: Robin Tibor Schirrmeister <robintibor@gmail.com>, Bruno Aristimunha Pinto <b.aristimunha@gmail.com>, Alexandre Gramfort <agramfort@meta.com>
|
|
6
6
|
Maintainer-email: Alexandre Gramfort <agramfort@meta.com>, Bruno Aristimunha Pinto <b.aristimunha@gmail.com>, Robin Tibor Schirrmeister <robintibor@gmail.com>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import abc
|
|
8
8
|
import inspect
|
|
9
9
|
import logging
|
|
10
|
+
import warnings
|
|
10
11
|
from typing import Literal
|
|
11
12
|
|
|
12
13
|
import mne
|
|
@@ -41,6 +42,7 @@ def _get_model(model: str):
|
|
|
41
42
|
|
|
42
43
|
class _EEGNeuralNet(NeuralNet, abc.ABC):
|
|
43
44
|
signal_args_set_ = False
|
|
45
|
+
_warned_drop_last_ = False
|
|
44
46
|
|
|
45
47
|
@property
|
|
46
48
|
def log(self):
|
|
@@ -267,6 +269,44 @@ class _EEGNeuralNet(NeuralNet, abc.ABC):
|
|
|
267
269
|
X = X.get_data(units="uV")
|
|
268
270
|
return super().get_dataset(X, y)
|
|
269
271
|
|
|
272
|
+
def get_iterator(self, dataset, training=False):
|
|
273
|
+
iterator = super().get_iterator(dataset, training=training)
|
|
274
|
+
# Warn (once per fit) if drop_last would discard every training batch,
|
|
275
|
+
# e.g. a training set smaller than batch_size. This silently skips
|
|
276
|
+
# training without raising any error, which is hard to diagnose.
|
|
277
|
+
if training and not self._warned_drop_last_:
|
|
278
|
+
self._warn_if_training_batches_dropped(iterator)
|
|
279
|
+
return iterator
|
|
280
|
+
|
|
281
|
+
def _warn_if_training_batches_dropped(self, loader):
|
|
282
|
+
"""Warn if ``drop_last`` discards all training batches.
|
|
283
|
+
|
|
284
|
+
With ``iterator_train__drop_last=True`` (the braindecode default), a
|
|
285
|
+
training set smaller than ``batch_size`` yields zero batches, so no
|
|
286
|
+
forward/backward pass runs and the model is left untrained without any
|
|
287
|
+
error. We inspect the resolved training ``DataLoader`` (after
|
|
288
|
+
``train_split`` has been applied), so the count reflects the actual
|
|
289
|
+
number of training examples.
|
|
290
|
+
"""
|
|
291
|
+
self._warned_drop_last_ = True
|
|
292
|
+
if not getattr(loader, "drop_last", False):
|
|
293
|
+
return
|
|
294
|
+
batch_size = getattr(loader, "batch_size", None)
|
|
295
|
+
try:
|
|
296
|
+
n_train = len(loader.dataset)
|
|
297
|
+
except TypeError:
|
|
298
|
+
return # e.g. an IterableDataset whose length is unknown
|
|
299
|
+
if not batch_size or n_train == 0 or n_train >= batch_size:
|
|
300
|
+
return
|
|
301
|
+
warnings.warn(
|
|
302
|
+
f"The training set has {n_train} example(s), which is smaller than "
|
|
303
|
+
f"batch_size={batch_size}. With iterator_train__drop_last=True "
|
|
304
|
+
f"(the braindecode default), all training batches are dropped and "
|
|
305
|
+
f"the model is not trained. Reduce batch_size to at most {n_train}, "
|
|
306
|
+
f"or pass iterator_train__drop_last=False.",
|
|
307
|
+
UserWarning,
|
|
308
|
+
)
|
|
309
|
+
|
|
270
310
|
def partial_fit(self, X, y=None, classes=None, **fit_params):
|
|
271
311
|
"""Fit the module.
|
|
272
312
|
|
|
@@ -366,6 +406,7 @@ class _EEGNeuralNet(NeuralNet, abc.ABC):
|
|
|
366
406
|
the module and to the ``self.train_split`` call.
|
|
367
407
|
"""
|
|
368
408
|
# this needs to be executed before the net is initialized:
|
|
409
|
+
self._warned_drop_last_ = False # re-check on every fresh fit
|
|
369
410
|
if not self.signal_args_set_:
|
|
370
411
|
self._set_signal_args(X, y, classes=None)
|
|
371
412
|
self.signal_args_set_ = True
|
|
@@ -15,6 +15,7 @@ from .deep4 import Deep4Net
|
|
|
15
15
|
from .deepsleepnet import DeepSleepNet
|
|
16
16
|
from .dgcnn import DGCNN
|
|
17
17
|
from .eegconformer import EEGConformer
|
|
18
|
+
from .eegdino import EEGDINO
|
|
18
19
|
from .eeginception_erp import EEGInceptionERP
|
|
19
20
|
from .eeginception_mi import EEGInceptionMI
|
|
20
21
|
from .eegitnet import EEGITNet
|
|
@@ -84,6 +85,7 @@ __all__ = [
|
|
|
84
85
|
"DeepSleepNet",
|
|
85
86
|
"BrainModule",
|
|
86
87
|
"EEGConformer",
|
|
88
|
+
"EEGDINO",
|
|
87
89
|
"EEGPT",
|
|
88
90
|
"InterpolatedEEGPT",
|
|
89
91
|
"EEGInceptionERP",
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
# Authors: Bruno Aristimunha <b.aristimunha@gmail.com>
|
|
2
|
+
#
|
|
3
|
+
# Code adapted from https://huggingface.co/eegdino/EEG-DINO
|
|
4
|
+
#
|
|
5
|
+
# License: BSD (3-clause)
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
from warnings import warn
|
|
10
|
+
|
|
11
|
+
import torch
|
|
12
|
+
import torch.nn.functional as F
|
|
13
|
+
from torch import nn
|
|
14
|
+
|
|
15
|
+
from braindecode.models.base import EEGModuleMixin
|
|
16
|
+
from braindecode.modules import DropPath, PatchTokenizer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EEGDINO(EEGModuleMixin, nn.Module):
|
|
20
|
+
r"""EEG-DINO from Wang et al. (2025) [eegdino]_.
|
|
21
|
+
|
|
22
|
+
:bdg-danger:`Foundation Model` :bdg-info:`Attention/Transformer`
|
|
23
|
+
|
|
24
|
+
.. figure:: https://braindecode.org/dev/_static/model/eegdino.png
|
|
25
|
+
:align: center
|
|
26
|
+
:alt: EEG-DINO Architecture
|
|
27
|
+
|
|
28
|
+
EEG-DINO is a ViT-style EEG foundation model pre-trained with DINO-v2
|
|
29
|
+
hierarchical self-distillation. Only the *encoder* (plus a classification
|
|
30
|
+
head) is integrated here; the self-distillation pre-training is out of scope
|
|
31
|
+
and it was not released by the authors.
|
|
32
|
+
|
|
33
|
+
The forward path is, end to end:
|
|
34
|
+
|
|
35
|
+
``(batch, n_chans, n_times)`` → patchify → time-frequency embedding →
|
|
36
|
+
decoupled positional embedding → transformer encoder (+ global token) →
|
|
37
|
+
pooling head → ``(batch, n_outputs)``.
|
|
38
|
+
|
|
39
|
+
.. rubric:: Step 1 -- Patchify
|
|
40
|
+
|
|
41
|
+
The signal is split along time into non-overlapping patches of
|
|
42
|
+
``patch_size`` samples (200 samples = 1 second at 200 Hz), giving one
|
|
43
|
+
*token* per (channel, patch). Inputs whose length is not a multiple of
|
|
44
|
+
``patch_size`` are zero-padded with a warning.
|
|
45
|
+
This is analogous to the patchification in ViT, BEiT (LaBraM), and CBraMod.
|
|
46
|
+
|
|
47
|
+
.. rubric:: Step 2 -- Time-Frequency Embedding (TFE)
|
|
48
|
+
|
|
49
|
+
Each patch is embedded by summing two branches, exactly as in
|
|
50
|
+
:class:`~braindecode.models.CBraMod` (the EEG-DINO paper reuses CBraMod's
|
|
51
|
+
TFE): a **time-domain** branch of stacked grouped convolutions
|
|
52
|
+
(``proj_in``), and a **frequency-domain** branch projecting the magnitude
|
|
53
|
+
of the patch's real FFT (``spectral_proj``). The embedding dimension
|
|
54
|
+
``emb_dim`` is therefore *derived* from the convolution configuration, not
|
|
55
|
+
set independently.
|
|
56
|
+
|
|
57
|
+
.. rubric:: Step 3 -- Decoupled Positional Embedding (DPE)
|
|
58
|
+
|
|
59
|
+
Where CBraMod uses a single convolutional positional encoding (ACPE),
|
|
60
|
+
EEG-DINO *decouples* space and time and adds both to every token: a
|
|
61
|
+
learnable **one-hot channel embedding** over a fixed vocabulary of
|
|
62
|
+
``n_channel_embeddings`` slots (input channel ``i`` maps to slot ``i``, so a
|
|
63
|
+
19-slot embedding serves any ``n_chans <= 19``) and a **depthwise temporal
|
|
64
|
+
convolution** over the patch axis (``time_encoding``).
|
|
65
|
+
|
|
66
|
+
.. rubric:: Step 4 -- Transformer Encoder & Global Token
|
|
67
|
+
|
|
68
|
+
Tokens are flattened to a single sequence and processed by ``n_layer``
|
|
69
|
+
pre-norm transformer blocks (BEiT-style attention with separate query/value
|
|
70
|
+
biases). A learnable ``global_tokens`` summary token is prepended after the
|
|
71
|
+
``global_token_layer``-th block and attends jointly with the patch tokens.
|
|
72
|
+
|
|
73
|
+
.. rubric:: Step 5 -- Classification Head (``final_layer``)
|
|
74
|
+
|
|
75
|
+
Following EEG-DINO's finetuning head, the patch tokens (global token
|
|
76
|
+
excluded) pass through a per-token projection, are mean-pooled over channels
|
|
77
|
+
then over patches (with an intermediate projection), and a three-layer MLP
|
|
78
|
+
(``final_layer``) maps them to ``n_outputs``
|
|
79
|
+
(:class:`~braindecode.EEGClassifier` applies the softmax). With
|
|
80
|
+
``return_encoder_output=True`` the mean-pooled encoder representation is
|
|
81
|
+
returned instead (linear probing).
|
|
82
|
+
|
|
83
|
+
.. warning::
|
|
84
|
+
EEG-DINO was pre-trained on EEG in microvolts scaled by ``1 / 100``. The
|
|
85
|
+
model does **not** rescale its input, so for the released weights to
|
|
86
|
+
behave as intended provide data on the same scale (e.g. microvolts
|
|
87
|
+
divided by 100) or normalize your signals consistently.
|
|
88
|
+
|
|
89
|
+
.. important::
|
|
90
|
+
**Pre-trained Weights Available**
|
|
91
|
+
|
|
92
|
+
Small and Medium encoders converted from the released checkpoints are
|
|
93
|
+
hosted on the Hugging Face Hub, one repository per size. Only the encoder
|
|
94
|
+
is pretrained; the classification head is randomly initialized, so
|
|
95
|
+
fine-tune or linear-probe before use::
|
|
96
|
+
|
|
97
|
+
from braindecode.models import EEGDINO
|
|
98
|
+
|
|
99
|
+
model = EEGDINO.from_pretrained(
|
|
100
|
+
"braindecode/eegdino-small-pretrained", # or -medium-pretrained
|
|
101
|
+
n_outputs=6,
|
|
102
|
+
n_chans=19,
|
|
103
|
+
sfreq=200,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
The Small/Medium/Large architectures are also available in
|
|
107
|
+
:data:`EEGDINO_CONFIGS`. Requires ``braindecode[hub]``.
|
|
108
|
+
|
|
109
|
+
.. versionadded:: 1.6.1
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
patch_size : int, default=200
|
|
114
|
+
Temporal patch size in samples (200 = 1 second at 200 Hz). Fixed at 200
|
|
115
|
+
for the released weights (the FFT branch uses ``patch_size // 2 + 1`` bins).
|
|
116
|
+
n_layer : int, default=12
|
|
117
|
+
Number of transformer encoder layers.
|
|
118
|
+
nhead : int, default=8
|
|
119
|
+
Number of attention heads.
|
|
120
|
+
dim_feedforward : int, default=512
|
|
121
|
+
Hidden size of the transformer feed-forward block.
|
|
122
|
+
channels_kernel_stride_padding_norm : sequence of tuple, optional
|
|
123
|
+
Configuration of the time-domain convolutions in the patch embedding,
|
|
124
|
+
as ``(out_channels, kernel, stride, padding, (groups, group_channels))``
|
|
125
|
+
per layer. The embedding dimension is derived from this (see
|
|
126
|
+
:class:`~braindecode.models.CBraMod`). Default is the EEG-DINO-Small /
|
|
127
|
+
CBraMod configuration.
|
|
128
|
+
n_channel_embeddings : int, default=19
|
|
129
|
+
Size of the one-hot channel-embedding vocabulary. ``n_chans`` must not
|
|
130
|
+
exceed it; the first ``n_chans`` slots are used. Default 19 (the released
|
|
131
|
+
montage), so the pretrained weights load for any ``n_chans <= 19``.
|
|
132
|
+
n_global_tokens : int, default=1
|
|
133
|
+
Number of learnable global summary tokens.
|
|
134
|
+
global_token_layer : int, default=1
|
|
135
|
+
1-based index of the encoder layer after which the global tokens are
|
|
136
|
+
inserted.
|
|
137
|
+
activation : type[nn.Module], default=nn.GELU
|
|
138
|
+
Activation function used in the transformer feed-forward blocks and the
|
|
139
|
+
classification head.
|
|
140
|
+
patch_activation : type[nn.Module], default=nn.GELU
|
|
141
|
+
Activation function used in the patch-embedding convolutions. Defaults to
|
|
142
|
+
``nn.GELU`` to match the released pretrained weights.
|
|
143
|
+
drop_prob : float, default=0.1
|
|
144
|
+
Dropout / stochastic-depth probability in the encoder.
|
|
145
|
+
return_features : bool, default=False
|
|
146
|
+
If True, ``forward`` returns ``{"features", "cls_token"}``.
|
|
147
|
+
return_encoder_output : bool, default=False
|
|
148
|
+
If True, ``final_layer`` is :class:`~torch.nn.Identity` and ``forward``
|
|
149
|
+
returns the pooled encoder representation (linear probing).
|
|
150
|
+
|
|
151
|
+
References
|
|
152
|
+
----------
|
|
153
|
+
.. [eegdino] Wang, X., Liu, X., Liu, X., Si, Q., Xu, Z., Li, Y., & Zhen, X.
|
|
154
|
+
(2025). EEG-DINO: Learning EEG Foundation Models via Hierarchical
|
|
155
|
+
Self-Distillation. In Medical Image Computing and Computer Assisted
|
|
156
|
+
Intervention (MICCAI 2025).
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
n_outputs=None,
|
|
162
|
+
n_chans=None,
|
|
163
|
+
chs_info=None,
|
|
164
|
+
n_times=None,
|
|
165
|
+
input_window_seconds=None,
|
|
166
|
+
sfreq=None,
|
|
167
|
+
patch_size: int = 200,
|
|
168
|
+
n_layer: int = 12,
|
|
169
|
+
nhead: int = 8,
|
|
170
|
+
dim_feedforward: int = 512,
|
|
171
|
+
channels_kernel_stride_padding_norm: Sequence[
|
|
172
|
+
tuple[int, int, int, int, tuple[int, int]]
|
|
173
|
+
] = (
|
|
174
|
+
(25, 49, 25, 24, (5, 25)),
|
|
175
|
+
(25, 3, 1, 1, (5, 25)),
|
|
176
|
+
(25, 3, 1, 1, (5, 25)),
|
|
177
|
+
),
|
|
178
|
+
n_channel_embeddings: int = 19,
|
|
179
|
+
n_global_tokens: int = 1,
|
|
180
|
+
global_token_layer: int = 1,
|
|
181
|
+
activation: type[nn.Module] = nn.GELU,
|
|
182
|
+
patch_activation: type[nn.Module] = nn.GELU,
|
|
183
|
+
drop_prob: float = 0.1,
|
|
184
|
+
return_features: bool = False,
|
|
185
|
+
return_encoder_output: bool = False,
|
|
186
|
+
):
|
|
187
|
+
super().__init__(
|
|
188
|
+
n_outputs=n_outputs,
|
|
189
|
+
n_chans=n_chans,
|
|
190
|
+
chs_info=chs_info,
|
|
191
|
+
n_times=n_times,
|
|
192
|
+
input_window_seconds=input_window_seconds,
|
|
193
|
+
sfreq=sfreq,
|
|
194
|
+
)
|
|
195
|
+
del n_outputs, n_chans, chs_info, n_times, input_window_seconds, sfreq
|
|
196
|
+
|
|
197
|
+
if not 1 <= global_token_layer <= n_layer:
|
|
198
|
+
raise ValueError(
|
|
199
|
+
f"global_token_layer must be in [1, n_layer={n_layer}], "
|
|
200
|
+
f"got {global_token_layer}."
|
|
201
|
+
)
|
|
202
|
+
if n_global_tokens < 1:
|
|
203
|
+
raise ValueError(f"n_global_tokens must be >= 1, got {n_global_tokens}.")
|
|
204
|
+
if self.n_chans > n_channel_embeddings:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"n_chans ({self.n_chans}) must not exceed n_channel_embeddings "
|
|
207
|
+
f"({n_channel_embeddings}); the released weights use 19."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if self._sfreq is not None and self.sfreq != 200:
|
|
211
|
+
warn(
|
|
212
|
+
f"EEG-DINO was trained at 200 Hz but sfreq={self.sfreq}. Inputs are "
|
|
213
|
+
"not resampled internally; results may be unreliable.",
|
|
214
|
+
UserWarning,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
self.tokenizer = PatchTokenizer(
|
|
218
|
+
patch_size, n_times=self.n_times, learnable=False
|
|
219
|
+
)
|
|
220
|
+
self.n_global_tokens = n_global_tokens
|
|
221
|
+
self.global_token_layer = global_token_layer
|
|
222
|
+
self.activation = activation
|
|
223
|
+
self.return_features = return_features
|
|
224
|
+
self.return_encoder_output = return_encoder_output
|
|
225
|
+
|
|
226
|
+
self.patch_embedding = _PatchEmbedding(
|
|
227
|
+
patch_size=patch_size,
|
|
228
|
+
channels_kernel_stride_padding_norm=channels_kernel_stride_padding_norm,
|
|
229
|
+
n_channel_embeddings=n_channel_embeddings,
|
|
230
|
+
activation=patch_activation,
|
|
231
|
+
drop_prob=drop_prob,
|
|
232
|
+
)
|
|
233
|
+
self.emb_dim = self.patch_embedding.emb_dim
|
|
234
|
+
|
|
235
|
+
self.encoder_layers = nn.ModuleList(
|
|
236
|
+
_TransformerEncoderLayer(
|
|
237
|
+
self.emb_dim, nhead, dim_feedforward, activation, drop_prob
|
|
238
|
+
)
|
|
239
|
+
for _ in range(n_layer)
|
|
240
|
+
)
|
|
241
|
+
self.global_tokens = nn.Parameter(torch.zeros(1, n_global_tokens, self.emb_dim))
|
|
242
|
+
|
|
243
|
+
self.final_layer = self._make_head()
|
|
244
|
+
|
|
245
|
+
self._init_weights()
|
|
246
|
+
|
|
247
|
+
def _init_weights(self):
|
|
248
|
+
nn.init.trunc_normal_(self.global_tokens, std=0.02)
|
|
249
|
+
|
|
250
|
+
def _make_head(self):
|
|
251
|
+
if self.return_encoder_output:
|
|
252
|
+
return nn.Identity() # ``n_outputs`` not needed for linear probing
|
|
253
|
+
return _ClassificationHead(self.emb_dim, self.n_outputs, self.activation)
|
|
254
|
+
|
|
255
|
+
def reset_head(self, n_outputs):
|
|
256
|
+
"""Replace ``final_layer`` with a fresh classification head for ``n_outputs``.
|
|
257
|
+
|
|
258
|
+
Asking for a head implies a classification model, so this also clears
|
|
259
|
+
``return_encoder_output`` (a linear-probe model thereby gains a real head),
|
|
260
|
+
mirroring :meth:`braindecode.models.CBraMod.reset_head`.
|
|
261
|
+
"""
|
|
262
|
+
self._n_outputs = n_outputs
|
|
263
|
+
self.return_encoder_output = False
|
|
264
|
+
self.final_layer = self._make_head()
|
|
265
|
+
|
|
266
|
+
def forward(self, x, return_features: bool | None = None):
|
|
267
|
+
"""Forward pass.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
x : torch.Tensor
|
|
272
|
+
Input of shape ``(batch, n_chans, n_times)``. The model does not
|
|
273
|
+
rescale ``x`` (see the amplitude-scale warning in the class docstring).
|
|
274
|
+
return_features : bool, optional
|
|
275
|
+
Overrides ``self.return_features`` for this call.
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
torch.Tensor or dict
|
|
280
|
+
Logits ``(batch, n_outputs)``; or, with features,
|
|
281
|
+
``{"features": (batch, n_chans * n_patches, emb_dim),
|
|
282
|
+
"cls_token": (batch, emb_dim)}``.
|
|
283
|
+
"""
|
|
284
|
+
if return_features is None:
|
|
285
|
+
return_features = self.return_features
|
|
286
|
+
x = self.tokenizer(x) # (batch, n_chans, n_patches, patch_size)
|
|
287
|
+
|
|
288
|
+
patch_emb = self.patch_embedding(x) # (batch, n_chans, n_patches, emb_dim)
|
|
289
|
+
n_chans = patch_emb.shape[1]
|
|
290
|
+
tokens = patch_emb.flatten(1, 2) # (batch, chans * patches, emb)
|
|
291
|
+
|
|
292
|
+
global_tokens = self.global_tokens.expand(tokens.shape[0], -1, -1)
|
|
293
|
+
for i, layer in enumerate(self.encoder_layers):
|
|
294
|
+
tokens = layer(tokens)
|
|
295
|
+
if i + 1 == self.global_token_layer:
|
|
296
|
+
tokens = torch.cat([global_tokens, tokens], dim=1)
|
|
297
|
+
|
|
298
|
+
global_out = tokens[:, : self.n_global_tokens]
|
|
299
|
+
patch_out = tokens[:, self.n_global_tokens :]
|
|
300
|
+
|
|
301
|
+
if return_features:
|
|
302
|
+
return {"features": patch_out, "cls_token": global_out.mean(dim=1)}
|
|
303
|
+
|
|
304
|
+
if self.return_encoder_output:
|
|
305
|
+
return patch_out.mean(dim=1) # pooled encoder features (linear probing)
|
|
306
|
+
return self.final_layer(patch_out, n_chans)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
#: Architecture presets. Small and Medium have released weights; Large does not.
|
|
310
|
+
EEGDINO_CONFIGS = {
|
|
311
|
+
"small": dict(
|
|
312
|
+
n_layer=12,
|
|
313
|
+
nhead=8,
|
|
314
|
+
dim_feedforward=512,
|
|
315
|
+
channels_kernel_stride_padding_norm=(
|
|
316
|
+
(25, 49, 25, 24, (5, 25)),
|
|
317
|
+
(25, 3, 1, 1, (5, 25)),
|
|
318
|
+
(25, 3, 1, 1, (5, 25)),
|
|
319
|
+
),
|
|
320
|
+
),
|
|
321
|
+
"medium": dict(
|
|
322
|
+
n_layer=16,
|
|
323
|
+
nhead=8,
|
|
324
|
+
dim_feedforward=1024,
|
|
325
|
+
channels_kernel_stride_padding_norm=(
|
|
326
|
+
(64, 49, 25, 24, (8, 64)),
|
|
327
|
+
(128, 3, 1, 1, (8, 128)),
|
|
328
|
+
(64, 3, 1, 1, (8, 64)),
|
|
329
|
+
),
|
|
330
|
+
),
|
|
331
|
+
"large": dict(
|
|
332
|
+
n_layer=24,
|
|
333
|
+
nhead=8,
|
|
334
|
+
dim_feedforward=2048,
|
|
335
|
+
channels_kernel_stride_padding_norm=(
|
|
336
|
+
(128, 49, 25, 24, (16, 128)),
|
|
337
|
+
(256, 3, 1, 1, (16, 256)),
|
|
338
|
+
(128, 3, 1, 1, (16, 128)),
|
|
339
|
+
),
|
|
340
|
+
),
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class _PatchEmbedding(nn.Module):
|
|
345
|
+
"""Time-frequency patch embedding with a decoupled positional embedding.
|
|
346
|
+
|
|
347
|
+
The time-domain (``proj_in``) and frequency-domain (``spectral_proj``)
|
|
348
|
+
branches are CBraMod's TFE (see :class:`~braindecode.models.CBraMod`); the
|
|
349
|
+
one-hot ``channel_embedding`` and depthwise ``time_encoding`` are EEG-DINO's
|
|
350
|
+
decoupled positional embedding.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
def __init__(
|
|
354
|
+
self,
|
|
355
|
+
patch_size,
|
|
356
|
+
channels_kernel_stride_padding_norm,
|
|
357
|
+
n_channel_embeddings,
|
|
358
|
+
activation=nn.GELU,
|
|
359
|
+
drop_prob=0.1,
|
|
360
|
+
):
|
|
361
|
+
super().__init__()
|
|
362
|
+
self.patch_size = patch_size
|
|
363
|
+
self.channels_kernel_stride_padding_norm = channels_kernel_stride_padding_norm
|
|
364
|
+
self.n_channel_embeddings = n_channel_embeddings
|
|
365
|
+
|
|
366
|
+
in_channels, conv_layers = 1, []
|
|
367
|
+
for (
|
|
368
|
+
out_channels,
|
|
369
|
+
kernel,
|
|
370
|
+
stride,
|
|
371
|
+
padding,
|
|
372
|
+
norm,
|
|
373
|
+
) in channels_kernel_stride_padding_norm:
|
|
374
|
+
conv_layers += [
|
|
375
|
+
nn.Conv2d(
|
|
376
|
+
in_channels, out_channels, (1, kernel), (1, stride), (0, padding)
|
|
377
|
+
),
|
|
378
|
+
nn.GroupNorm(*norm),
|
|
379
|
+
activation(),
|
|
380
|
+
]
|
|
381
|
+
in_channels = out_channels
|
|
382
|
+
self.proj_in = nn.Sequential(*conv_layers)
|
|
383
|
+
|
|
384
|
+
self.spectral_proj = nn.Sequential(
|
|
385
|
+
nn.Linear(patch_size // 2 + 1, self.emb_dim), nn.Dropout(drop_prob)
|
|
386
|
+
)
|
|
387
|
+
self.channel_embedding = nn.Linear(n_channel_embeddings, self.emb_dim)
|
|
388
|
+
# Sequential wrapper keeps the released-checkpoint key ``time_encoding.0.*``.
|
|
389
|
+
self.time_encoding = nn.Sequential(
|
|
390
|
+
nn.Conv2d(
|
|
391
|
+
self.emb_dim, self.emb_dim, (1, 5), (1, 1), (0, 2), groups=self.emb_dim
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def emb_dim(self):
|
|
397
|
+
"""Embedding dimension implied by the convolution configuration."""
|
|
398
|
+
reduced = self.patch_size
|
|
399
|
+
for _, kernel, stride, padding, _ in self.channels_kernel_stride_padding_norm:
|
|
400
|
+
reduced = (reduced + 2 * padding - kernel) // stride + 1
|
|
401
|
+
return self.channels_kernel_stride_padding_norm[-1][0] * reduced
|
|
402
|
+
|
|
403
|
+
def forward(self, x):
|
|
404
|
+
# x: (batch, n_chans, n_patches, patch_size)
|
|
405
|
+
n_chans = x.shape[1]
|
|
406
|
+
|
|
407
|
+
# Time-frequency embedding (CBraMod TFE): conv branch + FFT-magnitude branch.
|
|
408
|
+
time_tokens = self.proj_in(x.flatten(1, 2).unsqueeze(1))
|
|
409
|
+
batch, conv_dim, _, conv_len = time_tokens.shape
|
|
410
|
+
time_tokens = (
|
|
411
|
+
time_tokens.reshape(batch, conv_dim, n_chans, -1, conv_len)
|
|
412
|
+
.permute(0, 2, 3, 1, 4)
|
|
413
|
+
.flatten(3, 4)
|
|
414
|
+
)
|
|
415
|
+
spectrum = torch.fft.rfft(x, dim=-1, norm="forward").abs()
|
|
416
|
+
patch_emb = time_tokens + self.spectral_proj(spectrum)
|
|
417
|
+
|
|
418
|
+
# Decoupled positional embedding: one-hot channel + depthwise temporal conv.
|
|
419
|
+
# The one-hot uses the first ``n_chans`` of the ``n_channel_embeddings``
|
|
420
|
+
# slots, so the released 19-slot embedding serves any n_chans <= 19.
|
|
421
|
+
channel_ids = torch.arange(n_chans, device=x.device)
|
|
422
|
+
one_hot = F.one_hot(channel_ids, self.n_channel_embeddings).to(
|
|
423
|
+
self.channel_embedding.weight.dtype
|
|
424
|
+
)
|
|
425
|
+
channel_emb = self.channel_embedding(one_hot)
|
|
426
|
+
patch_emb = patch_emb + channel_emb.unsqueeze(0).unsqueeze(2)
|
|
427
|
+
temporal_emb = self.time_encoding(patch_emb.permute(0, 3, 1, 2))
|
|
428
|
+
return patch_emb + temporal_emb.permute(0, 2, 3, 1)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class _TransformerEncoderLayer(nn.Module):
|
|
432
|
+
"""Pre-norm transformer block (no LayerScale, matching the released config)."""
|
|
433
|
+
|
|
434
|
+
def __init__(
|
|
435
|
+
self, emb_dim, nhead, dim_feedforward, activation=nn.GELU, drop_prob=0.1
|
|
436
|
+
):
|
|
437
|
+
super().__init__()
|
|
438
|
+
self.norm1 = nn.LayerNorm(emb_dim)
|
|
439
|
+
self.attn = _Attention(emb_dim, nhead, attn_drop=drop_prob, proj_drop=drop_prob)
|
|
440
|
+
self.drop_path = DropPath(drop_prob) if drop_prob > 0.0 else nn.Identity()
|
|
441
|
+
self.norm2 = nn.LayerNorm(emb_dim)
|
|
442
|
+
self.mlp = _MLP(emb_dim, dim_feedforward, activation=activation, drop=drop_prob)
|
|
443
|
+
|
|
444
|
+
def forward(self, x):
|
|
445
|
+
x = x + self.drop_path(self.attn(self.norm1(x)))
|
|
446
|
+
x = x + self.drop_path(self.mlp(self.norm2(x)))
|
|
447
|
+
return x
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class _Attention(nn.Module):
|
|
451
|
+
"""BEiT-style attention: fused ``qkv`` without bias plus separate q/v biases.
|
|
452
|
+
|
|
453
|
+
``head_dim`` is decoupled from ``emb_dim`` (``all_head_dim = head_dim *
|
|
454
|
+
nhead``) so a custom ``nhead`` need not divide ``emb_dim``. For the released
|
|
455
|
+
presets ``nhead=8`` divides every ``emb_dim`` (200/512/1024), so this is a
|
|
456
|
+
no-op there and the released ``qkv``/``proj`` shapes are preserved.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
def __init__(self, emb_dim, nhead=8, attn_drop=0.0, proj_drop=0.0):
|
|
460
|
+
super().__init__()
|
|
461
|
+
if not 1 <= nhead <= emb_dim:
|
|
462
|
+
raise ValueError(
|
|
463
|
+
f"nhead ({nhead}) must be in [1, emb_dim={emb_dim}] so head_dim >= 1."
|
|
464
|
+
)
|
|
465
|
+
self.nhead = nhead
|
|
466
|
+
head_dim = emb_dim // nhead
|
|
467
|
+
all_head_dim = head_dim * nhead
|
|
468
|
+
self.scale = head_dim**-0.5
|
|
469
|
+
self.qkv = nn.Linear(emb_dim, all_head_dim * 3, bias=False)
|
|
470
|
+
self.q_bias = nn.Parameter(torch.zeros(all_head_dim))
|
|
471
|
+
self.v_bias = nn.Parameter(torch.zeros(all_head_dim))
|
|
472
|
+
self.attn_drop = nn.Dropout(attn_drop)
|
|
473
|
+
self.proj = nn.Linear(all_head_dim, emb_dim)
|
|
474
|
+
self.proj_drop = nn.Dropout(proj_drop)
|
|
475
|
+
|
|
476
|
+
def forward(self, x):
|
|
477
|
+
bias = torch.cat((self.q_bias, torch.zeros_like(self.v_bias), self.v_bias))
|
|
478
|
+
qkv = F.linear(x, self.qkv.weight, bias)
|
|
479
|
+
batch, tokens, _ = qkv.shape
|
|
480
|
+
query, key, value = (
|
|
481
|
+
qkv.reshape(batch, tokens, 3, self.nhead, -1)
|
|
482
|
+
.permute(2, 0, 3, 1, 4)
|
|
483
|
+
.unbind(0)
|
|
484
|
+
)
|
|
485
|
+
attn = (query * self.scale @ key.transpose(-2, -1)).softmax(dim=-1)
|
|
486
|
+
attn = self.attn_drop(attn)
|
|
487
|
+
out = (attn @ value).transpose(1, 2).flatten(2)
|
|
488
|
+
return self.proj_drop(self.proj(out))
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class _MLP(nn.Module):
|
|
492
|
+
"""Feed-forward block; ``fc1``/``fc2`` names match the released checkpoint."""
|
|
493
|
+
|
|
494
|
+
def __init__(self, emb_dim, hidden_dim, activation=nn.GELU, drop=0.0):
|
|
495
|
+
super().__init__()
|
|
496
|
+
self.fc1 = nn.Linear(emb_dim, hidden_dim)
|
|
497
|
+
self.act = activation()
|
|
498
|
+
self.fc2 = nn.Linear(hidden_dim, emb_dim)
|
|
499
|
+
self.drop = nn.Dropout(drop)
|
|
500
|
+
|
|
501
|
+
def forward(self, x):
|
|
502
|
+
return self.drop(self.fc2(self.act(self.fc1(x))))
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class _ClassificationHead(nn.Module):
|
|
506
|
+
"""EEG-DINO finetuning head (Wang et al., 2025).
|
|
507
|
+
|
|
508
|
+
Applies a per-token projection, mean-pools the patch tokens over channels
|
|
509
|
+
then over patches (with an intermediate projection), and maps the pooled
|
|
510
|
+
``emb_dim`` vector to ``n_outputs`` with a three-layer MLP.
|
|
511
|
+
"""
|
|
512
|
+
|
|
513
|
+
def __init__(
|
|
514
|
+
self, emb_dim, n_outputs, activation=nn.GELU, drop_prob_1=0.5, drop_prob_2=0.3
|
|
515
|
+
):
|
|
516
|
+
super().__init__()
|
|
517
|
+
self.token_proj = nn.Sequential(nn.Linear(emb_dim, emb_dim), activation())
|
|
518
|
+
self.time_proj = nn.Sequential(nn.Linear(emb_dim, emb_dim), activation())
|
|
519
|
+
self.classifier = nn.Sequential(
|
|
520
|
+
nn.Linear(emb_dim, emb_dim // 2),
|
|
521
|
+
activation(),
|
|
522
|
+
nn.Dropout(drop_prob_1),
|
|
523
|
+
nn.Linear(emb_dim // 2, emb_dim // 4),
|
|
524
|
+
activation(),
|
|
525
|
+
nn.Dropout(drop_prob_2),
|
|
526
|
+
nn.Linear(emb_dim // 4, n_outputs),
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
def forward(self, patch_tokens, n_chans):
|
|
530
|
+
x = self.token_proj(patch_tokens)
|
|
531
|
+
x = x.reshape(x.shape[0], n_chans, -1, x.shape[2]).mean(dim=1)
|
|
532
|
+
x = self.time_proj(x).mean(dim=1)
|
|
533
|
+
return self.classifier(x)
|
|
@@ -56,3 +56,4 @@ InterpolatedBIOT,"Sleep Staging, Epilepsy",Classification,200,"chs_info, n_outpu
|
|
|
56
56
|
InterpolatedEEGPT,General,Classification,256,"chs_info, n_outputs, n_times",25323137,"InterpolatedEEGPT(chs_info=<user>, n_outputs=4, n_times=1024)","Foundation Model,Attention/Transformer,Channel"
|
|
57
57
|
InterpolatedLaBraM,General,"Classification, Embedding",200,"chs_info, n_outputs, n_times",5866196,"InterpolatedLaBraM(chs_info=<user>, n_outputs=4, n_times=1000)","Convolution,Foundation Model,Channel"
|
|
58
58
|
InterpolatedSignalJEPA,"Motor Imagery, ERP, SSVEP",Embedding,128,"chs_info",3456882,"InterpolatedSignalJEPA(chs_info=<user>)","Convolution,Channel,Foundation Model"
|
|
59
|
+
EEGDINO,General,Classification,200,"n_chans, n_outputs, n_times",4539698,"EEGDINO(n_chans=19, n_outputs=4, n_times=1000)","Foundation Model,Attention/Transformer"
|
|
@@ -432,6 +432,7 @@ models_mandatory_parameters: list[
|
|
|
432
432
|
{"n_chans": 19, "n_times": 6000},
|
|
433
433
|
),
|
|
434
434
|
("DGCNN", ["n_chans", "n_outputs", "n_times", "chs_info"], None),
|
|
435
|
+
("EEGDINO", ["n_chans", "n_outputs", "n_times"], None),
|
|
435
436
|
]
|
|
436
437
|
|
|
437
438
|
################################################################
|
|
@@ -14,7 +14,7 @@ from .attention import (
|
|
|
14
14
|
MultiHeadAttention,
|
|
15
15
|
SqueezeAndExcitation,
|
|
16
16
|
)
|
|
17
|
-
from .blocks import MLP, FeedForwardBlock, InceptionBlock
|
|
17
|
+
from .blocks import MLP, FeedForwardBlock, InceptionBlock, PatchTokenizer
|
|
18
18
|
from .convolution import (
|
|
19
19
|
AvgPool2dWithConv,
|
|
20
20
|
CausalConv1d,
|
|
@@ -68,6 +68,7 @@ __all__ = [
|
|
|
68
68
|
"MLP",
|
|
69
69
|
"FeedForwardBlock",
|
|
70
70
|
"InceptionBlock",
|
|
71
|
+
"PatchTokenizer",
|
|
71
72
|
"AvgPool2dWithConv",
|
|
72
73
|
"CausalConv1d",
|
|
73
74
|
"CombinedConv",
|