From b93513413b3b34d2c0fb315181b2be6064748fa6 Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Mon, 27 Feb 2023 16:55:48 +0100 Subject: [PATCH 01/34] Fix bug for MotorImagery All events, setting n_classes --- moabb/paradigms/motor_imagery.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/moabb/paradigms/motor_imagery.py b/moabb/paradigms/motor_imagery.py index 5e3e7b721..1dade7de8 100644 --- a/moabb/paradigms/motor_imagery.py +++ b/moabb/paradigms/motor_imagery.py @@ -373,8 +373,7 @@ def used_events(self, dataset): if self.events is None: for k, v in dataset.event_id.items(): out[k] = v - if len(out) == self.n_classes: - break + self.n_classes = len(out) else: for event in self.events: if event in dataset.event_id.keys(): From 70d053982d04395ae87e069ba3e407f4dc3669ed Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Mon, 27 Feb 2023 17:25:10 +0100 Subject: [PATCH 02/34] Fix bug for MotorImagery All events, setting n_classes --- docs/source/whats_new.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/whats_new.rst b/docs/source/whats_new.rst index 0cf8723b7..524133fd8 100644 --- a/docs/source/whats_new.rst +++ b/docs/source/whats_new.rst @@ -31,6 +31,7 @@ Bugs - Correct usage of name simplification function in analyze (:gh:`306` by `Divyesh Narayanan`_) - Fix downloading path issue for Weibo2014 and Zhou2016, numy error in DemonsP300 (:gh:`315` by `Sylvain Chevallier`_) - Fix unzip error for Huebner2017 and Huebner2018 (:gh:`318` by `Sylvain Chevallier`_) +- Fix n_classes when events set to None (:gh:`337` by `Igor Carrara`_) API changes ~~~~~~~~~~~ @@ -274,6 +275,7 @@ API changes +.. _Igor Carrara: https://github.com/carraraig .. _Bruno Aristimunha: https://github.com/bruAristimunha .. _Alexandre Barachant: https://github.com/alexandrebarachant .. _Quentin Barthelemy: https://github.com/qbarthelemy From a556c8ae8205de3788b3682f55da5b92b49e71a3 Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Mon, 27 Feb 2023 18:38:52 +0100 Subject: [PATCH 03/34] Fix is Valid Change default value n_classes=None --- moabb/paradigms/motor_imagery.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/moabb/paradigms/motor_imagery.py b/moabb/paradigms/motor_imagery.py index 1dade7de8..f34c5697a 100644 --- a/moabb/paradigms/motor_imagery.py +++ b/moabb/paradigms/motor_imagery.py @@ -346,25 +346,27 @@ class MotorImagery(SinglePass): If not None, resample the eeg data with the sampling rate provided. """ - def __init__(self, n_classes=2, **kwargs): + def __init__(self, n_classes=None, **kwargs): super().__init__(**kwargs) self.n_classes = n_classes if self.events is None: log.warning("Choosing from all possible events") - else: + elif self.n_classes is not None: assert n_classes <= len(self.events), "More classes than events specified" def is_valid(self, dataset): ret = True if not dataset.paradigm == "imagery": ret = False - if self.events is None: + elif self.n_classes is None and self.events is None: + pass + elif self.events is None: if not len(dataset.event_id) >= self.n_classes: ret = False else: overlap = len(set(self.events) & set(dataset.event_id.keys())) - if not overlap >= self.n_classes: + if self.n_classes is not None and not overlap >= self.n_classes: ret = False return ret @@ -373,7 +375,8 @@ def used_events(self, dataset): if self.events is None: for k, v in dataset.event_id.items(): out[k] = v - self.n_classes = len(out) + if self.n_classes is None: + self.n_classes = len(out) else: for event in self.events: if event in dataset.event_id.keys(): From ced2bda5d9246043781ac0e9967b90b73225bc56 Mon Sep 17 00:00:00 2001 From: Sylvain Chevallier Date: Tue, 28 Feb 2023 01:33:47 +0100 Subject: [PATCH 04/34] Update docs/source/whats_new.rst --- docs/source/whats_new.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/whats_new.rst b/docs/source/whats_new.rst index 71963f0b7..d498cabb9 100644 --- a/docs/source/whats_new.rst +++ b/docs/source/whats_new.rst @@ -32,7 +32,7 @@ Bugs - Correct usage of name simplification function in analyze (:gh:`306` by `Divyesh Narayanan`_) - Fix downloading path issue for Weibo2014 and Zhou2016, numy error in DemonsP300 (:gh:`315` by `Sylvain Chevallier`_) - Fix unzip error for Huebner2017 and Huebner2018 (:gh:`318` by `Sylvain Chevallier`_) -- Fix n_classes when events set to None (:gh:`337` by `Igor Carrara`_) +- Fix n_classes when events set to None (:gh:`337` by `Igor Carrara`_ and `Sylvain Chevallier`_) API changes ~~~~~~~~~~~ From 56fb30529995f4a0c3682cf133a0e1eb39369ae8 Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Mon, 6 Mar 2023 11:50:04 +0100 Subject: [PATCH 05/34] plot_BrainDecode first draft --- examples/plot_BrainDecode.py | 174 +++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 examples/plot_BrainDecode.py diff --git a/examples/plot_BrainDecode.py b/examples/plot_BrainDecode.py new file mode 100644 index 000000000..2107796b1 --- /dev/null +++ b/examples/plot_BrainDecode.py @@ -0,0 +1,174 @@ +import os.path as osp +# Set up the Directory for made it run on a server. +import sys +import os + +sys.path.append(r'/home/icarrara/Documents/Project/HolographicEEG') # Server + +import matplotlib.pyplot as plt +import mne +import seaborn as sns +import torch +from braindecode import EEGClassifier +from braindecode.models import ShallowFBCSPNet, EEGNetv4 +from braindecode.util import set_random_seeds +from moabb.datasets import BNCI2014001 +from moabb.evaluations import WithinSessionEvaluation, CrossSessionEvaluation +from moabb.paradigms import LeftRightImagery, MotorImagery +from moabb.utils import set_download_dir +from sklearn.pipeline import Pipeline +from skorch.callbacks import EarlyStopping, EpochScoring +from skorch.dataset import ValidSplit +from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin +from braindecode.datasets import create_from_X_y +from Feature_Extraction.Transformer import Transformer + +mne.set_log_level(False) + +set_download_dir(osp.join(osp.expanduser("~"), "mne_data")) + +# Set up GPU if it is there +cuda = torch.cuda.is_available() +print(cuda) +device = "cuda" if cuda else "cpu" +if cuda: + torch.backends.cudnn.benchmark = True + + +# Class to load the data +class Transformer(BaseEstimator, TransformerMixin): + def __init__(self, kw_args=None): + self.kw_args = kw_args + + def fit(self, X, y=None): + self.y = y + return self + + def transform(self, X, y=None): + dataset = create_from_X_y( + X.get_data(), + y=self.y, + window_size_samples=X.get_data().shape[2], + window_stride_samples=X.get_data().shape[2], + drop_last_window=False, + sfreq=X.info["sfreq"], + ) + + return dataset + + def __sklearn_is_fitted__(self): + """Return True since Transfomer is stateless.""" + return True + + +# Set random seed to be able to reproduce results +seed = 42 +set_random_seeds(seed=seed, cuda=cuda) + +# Hyperparameter ShallowEEG +lr = 0.0625 * 0.01 +weight_decay = 0 +batch_size = 64 +n_epochs = 10 +patience = 5 +fmin = 4 +fmax = 100 +tmin = 0 +tmax = None +sub_numb = 1 + +# Load the dataset +dataset = BNCI2014001() +events = ["right_hand", "left_hand"] +# events = ["right_hand", "left_hand", "tongue", "feet"] +paradigm = MotorImagery(events=events, n_classes=len(events), fmin=fmin, fmax=fmax, tmin=tmin, tmax=tmax) +subjects = [sub_numb] +X, _, _ = paradigm.get_data(dataset=dataset, subjects=subjects) +# Define Transformer of Dataset compatible with Brain Decode +create_dataset = Transformer() + +# ======================================================================================================== +# Define Model +# ======================================================================================================== +model = EEGNetv4( + in_chans=X.shape[1], + n_classes=len(events), + input_window_samples=X.shape[2], + final_conv_length="auto", + drop_prob=0.5 +) + +# Send model to GPU +if cuda: + model.cuda() + +clf = EEGClassifier( + model, + criterion=torch.nn.CrossEntropyLoss, + optimizer=torch.optim.Adam, + optimizer__lr=lr, + batch_size=batch_size, + max_epochs=n_epochs, + train_split=ValidSplit(0.2), + device=device, + callbacks=[EarlyStopping(monitor='valid_loss', patience=patience), + EpochScoring(scoring='accuracy', on_train=True, name='train_acc', lower_is_better=False), + EpochScoring(scoring='accuracy', on_train=False, name='valid_acc', lower_is_better=False)], + verbose=1 # Not printing the results foe each epoch +) + +# ======================================================================================================== +# Define the pipeline +# ======================================================================================================== +pipes = {} +pipes["ShallowFBCSPNet"] = Pipeline([ + ("Braindecode_dataset", create_dataset), + ("Net", clf)]) + +# ======================================================================================================== +# Evaluation For MOABB +# ======================================================================================================== +dataset.subject_list = dataset.subject_list[int(sub_numb) - 1:int(sub_numb)] + +evaluation = CrossSessionEvaluation( + paradigm=paradigm, + datasets=dataset, + suffix="braindecode_example", + overwrite=True, + return_epochs=True, + n_jobs=1 +) + +results = evaluation.process(pipes) + +print(results.head()) + +############################################################################## +# Plot Results +# ---------------- +# +# Here we plot the results. We the first plot is a pointplot with the average +# performance of each pipeline across session and subjects. +# The second plot is a paired scatter plot. Each point representing the score +# of a single session. An algorithm will outperforms another is most of the +# points are in its quadrant. + +fig, axes = plt.subplots(1, 1, figsize=[8, 4], sharey=True) + +sns.stripplot( + data=results, + y="score", + x="pipeline", + ax=axes, + jitter=True, + alpha=0.5, + palette="Set1", +) +sns.pointplot( + data=results, y="score", x="pipeline", ax=axes, palette="Set1" +) + +axes.set_ylabel("ROC AUC") +axes.set_ylim(0.4, 1.0) + +plt.show() From c39c67a45aad7defc140188774de7883c61bf43e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 10:58:56 +0000 Subject: [PATCH 06/34] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/plot_BrainDecode.py | 58 +++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/examples/plot_BrainDecode.py b/examples/plot_BrainDecode.py index 2107796b1..44e3eea56 100644 --- a/examples/plot_BrainDecode.py +++ b/examples/plot_BrainDecode.py @@ -1,27 +1,31 @@ +import os import os.path as osp + # Set up the Directory for made it run on a server. import sys -import os -sys.path.append(r'/home/icarrara/Documents/Project/HolographicEEG') # Server + +sys.path.append(r"/home/icarrara/Documents/Project/HolographicEEG") # Server import matplotlib.pyplot as plt import mne import seaborn as sns import torch from braindecode import EEGClassifier -from braindecode.models import ShallowFBCSPNet, EEGNetv4 +from braindecode.datasets import create_from_X_y +from braindecode.models import EEGNetv4, ShallowFBCSPNet from braindecode.util import set_random_seeds -from moabb.datasets import BNCI2014001 -from moabb.evaluations import WithinSessionEvaluation, CrossSessionEvaluation -from moabb.paradigms import LeftRightImagery, MotorImagery -from moabb.utils import set_download_dir +from Feature_Extraction.Transformer import Transformer +from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin from sklearn.pipeline import Pipeline from skorch.callbacks import EarlyStopping, EpochScoring from skorch.dataset import ValidSplit -from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin -from braindecode.datasets import create_from_X_y -from Feature_Extraction.Transformer import Transformer + +from moabb.datasets import BNCI2014001 +from moabb.evaluations import CrossSessionEvaluation, WithinSessionEvaluation +from moabb.paradigms import LeftRightImagery, MotorImagery +from moabb.utils import set_download_dir + mne.set_log_level(False) @@ -81,7 +85,9 @@ def __sklearn_is_fitted__(self): dataset = BNCI2014001() events = ["right_hand", "left_hand"] # events = ["right_hand", "left_hand", "tongue", "feet"] -paradigm = MotorImagery(events=events, n_classes=len(events), fmin=fmin, fmax=fmax, tmin=tmin, tmax=tmax) +paradigm = MotorImagery( + events=events, n_classes=len(events), fmin=fmin, fmax=fmax, tmin=tmin, tmax=tmax +) subjects = [sub_numb] X, _, _ = paradigm.get_data(dataset=dataset, subjects=subjects) # Define Transformer of Dataset compatible with Brain Decode @@ -95,7 +101,7 @@ def __sklearn_is_fitted__(self): n_classes=len(events), input_window_samples=X.shape[2], final_conv_length="auto", - drop_prob=0.5 + drop_prob=0.5, ) # Send model to GPU @@ -111,24 +117,30 @@ def __sklearn_is_fitted__(self): max_epochs=n_epochs, train_split=ValidSplit(0.2), device=device, - callbacks=[EarlyStopping(monitor='valid_loss', patience=patience), - EpochScoring(scoring='accuracy', on_train=True, name='train_acc', lower_is_better=False), - EpochScoring(scoring='accuracy', on_train=False, name='valid_acc', lower_is_better=False)], - verbose=1 # Not printing the results foe each epoch + callbacks=[ + EarlyStopping(monitor="valid_loss", patience=patience), + EpochScoring( + scoring="accuracy", on_train=True, name="train_acc", lower_is_better=False + ), + EpochScoring( + scoring="accuracy", on_train=False, name="valid_acc", lower_is_better=False + ), + ], + verbose=1, # Not printing the results foe each epoch ) # ======================================================================================================== # Define the pipeline # ======================================================================================================== pipes = {} -pipes["ShallowFBCSPNet"] = Pipeline([ - ("Braindecode_dataset", create_dataset), - ("Net", clf)]) +pipes["ShallowFBCSPNet"] = Pipeline( + [("Braindecode_dataset", create_dataset), ("Net", clf)] +) # ======================================================================================================== # Evaluation For MOABB # ======================================================================================================== -dataset.subject_list = dataset.subject_list[int(sub_numb) - 1:int(sub_numb)] +dataset.subject_list = dataset.subject_list[int(sub_numb) - 1 : int(sub_numb)] evaluation = CrossSessionEvaluation( paradigm=paradigm, @@ -136,7 +148,7 @@ def __sklearn_is_fitted__(self): suffix="braindecode_example", overwrite=True, return_epochs=True, - n_jobs=1 + n_jobs=1, ) results = evaluation.process(pipes) @@ -164,9 +176,7 @@ def __sklearn_is_fitted__(self): alpha=0.5, palette="Set1", ) -sns.pointplot( - data=results, y="score", x="pipeline", ax=axes, palette="Set1" -) +sns.pointplot(data=results, y="score", x="pipeline", ax=axes, palette="Set1") axes.set_ylabel("ROC AUC") axes.set_ylim(0.4, 1.0) From 52949fd4489fdf2de9abaaf81efeea4828e1a051 Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Mon, 6 Mar 2023 12:36:38 +0100 Subject: [PATCH 07/34] plot_BrainDecode --- docs/source/whats_new.rst | 1 + examples/plot_BrainDecode.py | 164 +++++++++++++----------------- moabb/pipelines/utilis_pytorch.py | 53 ++++++++++ 3 files changed, 124 insertions(+), 94 deletions(-) create mode 100644 moabb/pipelines/utilis_pytorch.py diff --git a/docs/source/whats_new.rst b/docs/source/whats_new.rst index 11cd34d6b..bbfd3f036 100644 --- a/docs/source/whats_new.rst +++ b/docs/source/whats_new.rst @@ -24,6 +24,7 @@ Enhancements - Add GridSearchCV for different evaluation procedure (:gh:`319` by `Igor Carrara`_) - Add six deep learning models (Tensorflow), and build a tutorial to show to used the deep learning model (:gh:`326` by `Igor Carrara`_, `Bruno Aristimunha`_ and `Sylvain Chevallier`_) - Add a augmentation model to the pipeline (:gh:`326` by `Igor Carrara`_) +- Add BrainDecode example(:gh:`340` by `Igor Carrara`_ and `Bruno Aristimunha`_) Bugs ~~~~ diff --git a/examples/plot_BrainDecode.py b/examples/plot_BrainDecode.py index 44e3eea56..ba9079859 100644 --- a/examples/plot_BrainDecode.py +++ b/examples/plot_BrainDecode.py @@ -1,124 +1,123 @@ -import os -import os.path as osp - -# Set up the Directory for made it run on a server. -import sys - +""" +===================================================== +Example of usage of BrainDecode with MOABB evaluation +===================================================== +This example shows how to use BrainDecode in combination with MOABB evaluation. +In this example we use the architecture ShallowFBCSPNet. +""" +# Authors: Igor Carrara +# Bruno Aristimunha +# +# License: BSD (3-clause) -sys.path.append(r"/home/icarrara/Documents/Project/HolographicEEG") # Server +import os.path as osp import matplotlib.pyplot as plt import mne + import seaborn as sns import torch from braindecode import EEGClassifier -from braindecode.datasets import create_from_X_y -from braindecode.models import EEGNetv4, ShallowFBCSPNet -from braindecode.util import set_random_seeds -from Feature_Extraction.Transformer import Transformer -from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin +from braindecode.models import ShallowFBCSPNet from sklearn.pipeline import Pipeline from skorch.callbacks import EarlyStopping, EpochScoring from skorch.dataset import ValidSplit - +from moabb.pipelines.utilis_pytorch import set_seed +from moabb.pipelines.utilis_pytorch import Transformer from moabb.datasets import BNCI2014001 -from moabb.evaluations import CrossSessionEvaluation, WithinSessionEvaluation -from moabb.paradigms import LeftRightImagery, MotorImagery -from moabb.utils import set_download_dir - +from moabb.evaluations import CrossSessionEvaluation +from moabb.paradigms import MotorImagery +from moabb.analysis.plotting import score_plot mne.set_log_level(False) -set_download_dir(osp.join(osp.expanduser("~"), "mne_data")) +# Print Information PyTorch +print(f"Tensorflow Version: {torch.__version__}") # Set up GPU if it is there cuda = torch.cuda.is_available() -print(cuda) device = "cuda" if cuda else "cpu" -if cuda: - torch.backends.cudnn.benchmark = True +print("GPU is", "AVAILABLE" if cuda else "NOT AVAILABLE") -# Class to load the data -class Transformer(BaseEstimator, TransformerMixin): - def __init__(self, kw_args=None): - self.kw_args = kw_args +############################################################################### +# In this example, we will use only the dataset ``BNCI2014001``. +# +# Running the benchmark +# --------------------- +# +# This example use the CrossSession evaluation procedure. We focus on the dataset BNCI2014001 and only on 1 subject +# to reduce the computational time. +# +# To keep the computational time low the number of epoch is reduced. In a real situation we suggest to use +# EPOCH = 1000 +# PATIENCE = 300 +# +# This code is implemented to run on CPU. If you're using a GPU, do not use multithreading +# (i.e. set n_jobs=1) - def fit(self, X, y=None): - self.y = y - return self - def transform(self, X, y=None): - dataset = create_from_X_y( - X.get_data(), - y=self.y, - window_size_samples=X.get_data().shape[2], - window_stride_samples=X.get_data().shape[2], - drop_last_window=False, - sfreq=X.info["sfreq"], - ) - return dataset +# Set random seed to be able to reproduce results +set_seed(42) - def __sklearn_is_fitted__(self): - """Return True since Transfomer is stateless.""" - return True +# Ensure that all operations are deterministic on GPU (if used) for reproducibility +torch.backends.cudnn.deterministic = True +torch.backends.cudnn.benchmark = False -# Set random seed to be able to reproduce results -seed = 42 -set_random_seeds(seed=seed, cuda=cuda) - -# Hyperparameter ShallowEEG -lr = 0.0625 * 0.01 -weight_decay = 0 -batch_size = 64 -n_epochs = 10 -patience = 5 +# Hyperparameter +LEARNING_RATE = 0.0625 * 0.01 # result taken from BrainDecode +WEIGHT_DECAY = 0 # result taken from BrainDecode +BATCH_SIZE = 64 # result taken from BrainDecode +EPOCH = 10 +PATIENCE = 3 fmin = 4 fmax = 100 tmin = 0 tmax = None -sub_numb = 1 # Load the dataset dataset = BNCI2014001() events = ["right_hand", "left_hand"] -# events = ["right_hand", "left_hand", "tongue", "feet"] paradigm = MotorImagery( events=events, n_classes=len(events), fmin=fmin, fmax=fmax, tmin=tmin, tmax=tmax ) -subjects = [sub_numb] +subjects = [1] X, _, _ = paradigm.get_data(dataset=dataset, subjects=subjects) # Define Transformer of Dataset compatible with Brain Decode create_dataset = Transformer() -# ======================================================================================================== -# Define Model -# ======================================================================================================== -model = EEGNetv4( +############################################################################## +# Create Pipelines +# ---------------- +# In order to create a pipeline we need to load a model from BrainDecode. +# the second step is to define a skorch model using EEGClassifier from BrainDecode +# that allow to convert the PyTorch model in a scikit-learn classifier. + +model = ShallowFBCSPNet( in_chans=X.shape[1], n_classes=len(events), input_window_samples=X.shape[2], - final_conv_length="auto", - drop_prob=0.5, + final_conv_length="auto" ) # Send model to GPU if cuda: model.cuda() +# Define a Skorch classifier clf = EEGClassifier( model, criterion=torch.nn.CrossEntropyLoss, optimizer=torch.optim.Adam, - optimizer__lr=lr, - batch_size=batch_size, - max_epochs=n_epochs, + optimizer__lr=LEARNING_RATE, + batch_size=BATCH_SIZE, + max_epochs=EPOCH, train_split=ValidSplit(0.2), device=device, callbacks=[ - EarlyStopping(monitor="valid_loss", patience=patience), + EarlyStopping(monitor="valid_loss", patience=PATIENCE), EpochScoring( scoring="accuracy", on_train=True, name="train_acc", lower_is_better=False ), @@ -129,18 +128,17 @@ def __sklearn_is_fitted__(self): verbose=1, # Not printing the results foe each epoch ) -# ======================================================================================================== -# Define the pipeline -# ======================================================================================================== +# Create the pipelines pipes = {} pipes["ShallowFBCSPNet"] = Pipeline( [("Braindecode_dataset", create_dataset), ("Net", clf)] ) -# ======================================================================================================== -# Evaluation For MOABB -# ======================================================================================================== -dataset.subject_list = dataset.subject_list[int(sub_numb) - 1 : int(sub_numb)] + +############################################################################## +# Evaluation +# ---------- +dataset.subject_list = dataset.subject_list[:2] evaluation = CrossSessionEvaluation( paradigm=paradigm, @@ -158,27 +156,5 @@ def __sklearn_is_fitted__(self): ############################################################################## # Plot Results # ---------------- -# -# Here we plot the results. We the first plot is a pointplot with the average -# performance of each pipeline across session and subjects. -# The second plot is a paired scatter plot. Each point representing the score -# of a single session. An algorithm will outperforms another is most of the -# points are in its quadrant. - -fig, axes = plt.subplots(1, 1, figsize=[8, 4], sharey=True) - -sns.stripplot( - data=results, - y="score", - x="pipeline", - ax=axes, - jitter=True, - alpha=0.5, - palette="Set1", -) -sns.pointplot(data=results, y="score", x="pipeline", ax=axes, palette="Set1") - -axes.set_ylabel("ROC AUC") -axes.set_ylim(0.4, 1.0) - +score_plot(results) plt.show() diff --git a/moabb/pipelines/utilis_pytorch.py b/moabb/pipelines/utilis_pytorch.py new file mode 100644 index 000000000..19cec8531 --- /dev/null +++ b/moabb/pipelines/utilis_pytorch.py @@ -0,0 +1,53 @@ +import numpy as np +import random +import torch +from braindecode.datasets import create_from_X_y +from sklearn.base import BaseEstimator, TransformerMixin + + +def set_seed(seed): + """ + Function that allow reproducibility on PyTorch + Parameters + ---------- + seed to be used + + Returns + ------- + + """ + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + + + +class Transformer(BaseEstimator, TransformerMixin): + """ + Class to Load the data from MOABB in a format compatible with braindecode + """ + def __init__(self, kw_args=None): + self.kw_args = kw_args + + def fit(self, X, y=None): + self.y = y + return self + + def transform(self, X, y=None): + dataset = create_from_X_y( + X.get_data(), + y=self.y, + window_size_samples=X.get_data().shape[2], + window_stride_samples=X.get_data().shape[2], + drop_last_window=False, + sfreq=X.info["sfreq"], + ) + + return dataset + + def __sklearn_is_fitted__(self): + """Return True since Transfomer is stateless.""" + return True \ No newline at end of file From b5ce192220c65f21f5dc843e49e63b06f5464862 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 11:36:56 +0000 Subject: [PATCH 08/34] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/plot_BrainDecode.py | 11 +++++------ moabb/pipelines/utilis_pytorch.py | 7 ++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/plot_BrainDecode.py b/examples/plot_BrainDecode.py index ba9079859..c8216c115 100644 --- a/examples/plot_BrainDecode.py +++ b/examples/plot_BrainDecode.py @@ -14,7 +14,6 @@ import matplotlib.pyplot as plt import mne - import seaborn as sns import torch from braindecode import EEGClassifier @@ -22,12 +21,13 @@ from sklearn.pipeline import Pipeline from skorch.callbacks import EarlyStopping, EpochScoring from skorch.dataset import ValidSplit -from moabb.pipelines.utilis_pytorch import set_seed -from moabb.pipelines.utilis_pytorch import Transformer + +from moabb.analysis.plotting import score_plot from moabb.datasets import BNCI2014001 from moabb.evaluations import CrossSessionEvaluation from moabb.paradigms import MotorImagery -from moabb.analysis.plotting import score_plot +from moabb.pipelines.utilis_pytorch import Transformer, set_seed + mne.set_log_level(False) @@ -57,7 +57,6 @@ # (i.e. set n_jobs=1) - # Set random seed to be able to reproduce results set_seed(42) @@ -99,7 +98,7 @@ in_chans=X.shape[1], n_classes=len(events), input_window_samples=X.shape[2], - final_conv_length="auto" + final_conv_length="auto", ) # Send model to GPU diff --git a/moabb/pipelines/utilis_pytorch.py b/moabb/pipelines/utilis_pytorch.py index 19cec8531..1799dc87b 100644 --- a/moabb/pipelines/utilis_pytorch.py +++ b/moabb/pipelines/utilis_pytorch.py @@ -1,5 +1,6 @@ -import numpy as np import random + +import numpy as np import torch from braindecode.datasets import create_from_X_y from sklearn.base import BaseEstimator, TransformerMixin @@ -24,11 +25,11 @@ def set_seed(seed): torch.cuda.manual_seed_all(seed) - class Transformer(BaseEstimator, TransformerMixin): """ Class to Load the data from MOABB in a format compatible with braindecode """ + def __init__(self, kw_args=None): self.kw_args = kw_args @@ -50,4 +51,4 @@ def transform(self, X, y=None): def __sklearn_is_fitted__(self): """Return True since Transfomer is stateless.""" - return True \ No newline at end of file + return True From 78a2c8305b17c02e9b9e9bf9f75bee17e75d5982 Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Mon, 6 Mar 2023 12:38:01 +0100 Subject: [PATCH 09/34] plot_BrainDecode --- examples/plot_BrainDecode.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/plot_BrainDecode.py b/examples/plot_BrainDecode.py index ba9079859..75d3f18c3 100644 --- a/examples/plot_BrainDecode.py +++ b/examples/plot_BrainDecode.py @@ -10,12 +10,8 @@ # # License: BSD (3-clause) -import os.path as osp - import matplotlib.pyplot as plt import mne - -import seaborn as sns import torch from braindecode import EEGClassifier from braindecode.models import ShallowFBCSPNet From 0baad7becb65b3dc4fbc629cbe80eb079c00ffd0 Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Mon, 6 Mar 2023 12:38:31 +0100 Subject: [PATCH 10/34] plot_BrainDecode --- examples/plot_BrainDecode.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/plot_BrainDecode.py b/examples/plot_BrainDecode.py index c8216c115..77a5defb7 100644 --- a/examples/plot_BrainDecode.py +++ b/examples/plot_BrainDecode.py @@ -10,11 +10,8 @@ # # License: BSD (3-clause) -import os.path as osp - import matplotlib.pyplot as plt import mne -import seaborn as sns import torch from braindecode import EEGClassifier from braindecode.models import ShallowFBCSPNet From 3b7d3a235bcbdcf393c9455e405885d643e77877 Mon Sep 17 00:00:00 2001 From: Igor Carrara <94047258+carraraig@users.noreply.github.com> Date: Mon, 6 Mar 2023 19:16:49 +0100 Subject: [PATCH 11/34] Update examples/plot_BrainDecode.py Co-authored-by: Sylvain Chevallier --- examples/plot_BrainDecode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/plot_BrainDecode.py b/examples/plot_BrainDecode.py index 77a5defb7..82975515f 100644 --- a/examples/plot_BrainDecode.py +++ b/examples/plot_BrainDecode.py @@ -1,6 +1,6 @@ """ ===================================================== -Example of usage of BrainDecode with MOABB evaluation +Benchmarking on MOABB with BrainDecode deep net architectures ===================================================== This example shows how to use BrainDecode in combination with MOABB evaluation. In this example we use the architecture ShallowFBCSPNet. From 188fe0c0f1aa380f80be55a522698bcc6999f977 Mon Sep 17 00:00:00 2001 From: Igor Carrara <94047258+carraraig@users.noreply.github.com> Date: Mon, 6 Mar 2023 19:17:10 +0100 Subject: [PATCH 12/34] Update examples/plot_BrainDecode.py Co-authored-by: Sylvain Chevallier --- examples/plot_BrainDecode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/plot_BrainDecode.py b/examples/plot_BrainDecode.py index 82975515f..8e262bc5d 100644 --- a/examples/plot_BrainDecode.py +++ b/examples/plot_BrainDecode.py @@ -29,7 +29,7 @@ mne.set_log_level(False) # Print Information PyTorch -print(f"Tensorflow Version: {torch.__version__}") +print(f"Torch Version: {torch.__version__}") # Set up GPU if it is there cuda = torch.cuda.is_available() From 3ed2dcd9857e1746b295ea680d8efe5ad2f5ea09 Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Mon, 6 Mar 2023 19:22:17 +0100 Subject: [PATCH 13/34] plot_BrainDecode --- examples/plot_BrainDecode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/plot_BrainDecode.py b/examples/plot_BrainDecode.py index 8e262bc5d..46246efd6 100644 --- a/examples/plot_BrainDecode.py +++ b/examples/plot_BrainDecode.py @@ -1,7 +1,7 @@ """ -===================================================== +============================================================= Benchmarking on MOABB with BrainDecode deep net architectures -===================================================== +============================================================= This example shows how to use BrainDecode in combination with MOABB evaluation. In this example we use the architecture ShallowFBCSPNet. """ From 400fe41f98fc124466ecac68a838da42a895047e Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Mon, 6 Mar 2023 19:25:34 +0100 Subject: [PATCH 14/34] plot_BrainDecode, definition of setup_seed in moabb.utils --- examples/plot_BrainDecode.py | 5 +++-- moabb/pipelines/utilis_pytorch.py | 19 ------------------- moabb/utils.py | 9 +++++++++ 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/examples/plot_BrainDecode.py b/examples/plot_BrainDecode.py index 46246efd6..4bf917d56 100644 --- a/examples/plot_BrainDecode.py +++ b/examples/plot_BrainDecode.py @@ -23,7 +23,8 @@ from moabb.datasets import BNCI2014001 from moabb.evaluations import CrossSessionEvaluation from moabb.paradigms import MotorImagery -from moabb.pipelines.utilis_pytorch import Transformer, set_seed +from moabb.pipelines.utilis_pytorch import Transformer +from moabb.utils import setup_seed mne.set_log_level(False) @@ -55,7 +56,7 @@ # Set random seed to be able to reproduce results -set_seed(42) +setup_seed(42) # Ensure that all operations are deterministic on GPU (if used) for reproducibility torch.backends.cudnn.deterministic = True diff --git a/moabb/pipelines/utilis_pytorch.py b/moabb/pipelines/utilis_pytorch.py index 1799dc87b..4ce3fcd77 100644 --- a/moabb/pipelines/utilis_pytorch.py +++ b/moabb/pipelines/utilis_pytorch.py @@ -6,25 +6,6 @@ from sklearn.base import BaseEstimator, TransformerMixin -def set_seed(seed): - """ - Function that allow reproducibility on PyTorch - Parameters - ---------- - seed to be used - - Returns - ------- - - """ - random.seed(seed) - np.random.seed(seed) - torch.manual_seed(seed) - if torch.cuda.is_available(): - torch.cuda.manual_seed(seed) - torch.cuda.manual_seed_all(seed) - - class Transformer(BaseEstimator, TransformerMixin): """ Class to Load the data from MOABB in a format compatible with braindecode diff --git a/moabb/utils.py b/moabb/utils.py index 91c71f0bf..aa32b2f3d 100644 --- a/moabb/utils.py +++ b/moabb/utils.py @@ -14,12 +14,21 @@ def setup_seed(seed): except ImportError as ierr: raise ImportError("Please install Tensorflow") from ierr + try: + import torch + except ImportError as ierr: + raise ImportError("Please install Tensorflow") from ierr + random.seed(seed) np.random.seed(seed) tf.random.set_seed(seed) # tf cpu fix seed os.environ[ "TF_DETERMINISTIC_OPS" ] = "1" # tf gpu fix seed, please `pip install tensorflow-determinism` first + torch.manual_seed(seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) def set_log_level(level="INFO"): From 0b1b1ff51604a679b559083e4a6ca7419169a281 Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Mon, 6 Mar 2023 19:28:02 +0100 Subject: [PATCH 15/34] plot_BrainDecode, definition of setup_seed in moabb.utils --- moabb/pipelines/utilis_pytorch.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/moabb/pipelines/utilis_pytorch.py b/moabb/pipelines/utilis_pytorch.py index 4ce3fcd77..47e4f4b5e 100644 --- a/moabb/pipelines/utilis_pytorch.py +++ b/moabb/pipelines/utilis_pytorch.py @@ -1,7 +1,3 @@ -import random - -import numpy as np -import torch from braindecode.datasets import create_from_X_y from sklearn.base import BaseEstimator, TransformerMixin From fc6744ba1eeffd93718a12ffece490adc1fd6323 Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Tue, 7 Mar 2023 15:28:30 +0100 Subject: [PATCH 16/34] brain decode try benchmark --- .../PyTorch_ShallowConvNet.yml | 12 ++ examples/plot_BrainDecode.py | 2 +- examples/plot_BrainDecode2.py | 149 ++++++++++++++ examples/plot_benchmark_BrainDecode.py | 123 ++++++++++++ moabb/pipelines/deep_learning_BrainDecode.py | 188 ++++++++++++++++++ 5 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 examples/pipelines_BrainDecode/PyTorch_ShallowConvNet.yml create mode 100644 examples/plot_BrainDecode2.py create mode 100644 examples/plot_benchmark_BrainDecode.py create mode 100644 moabb/pipelines/deep_learning_BrainDecode.py diff --git a/examples/pipelines_BrainDecode/PyTorch_ShallowConvNet.yml b/examples/pipelines_BrainDecode/PyTorch_ShallowConvNet.yml new file mode 100644 index 000000000..cb05151eb --- /dev/null +++ b/examples/pipelines_BrainDecode/PyTorch_ShallowConvNet.yml @@ -0,0 +1,12 @@ +name: ExamplePyTorch + +paradigms: + - LeftRightImagery + - MotorImagery + +pipeline: + - name: Transformer + from: moabb.pipelines.utilis_pytorch + + - name: BrainDecodeShallowConvNet + from: moabb.pipelines.deep_learning_BrainDecode diff --git a/examples/plot_BrainDecode.py b/examples/plot_BrainDecode.py index 4bf917d56..d72e9f78a 100644 --- a/examples/plot_BrainDecode.py +++ b/examples/plot_BrainDecode.py @@ -105,7 +105,7 @@ # Define a Skorch classifier clf = EEGClassifier( - model, + module=model, criterion=torch.nn.CrossEntropyLoss, optimizer=torch.optim.Adam, optimizer__lr=LEARNING_RATE, diff --git a/examples/plot_BrainDecode2.py b/examples/plot_BrainDecode2.py new file mode 100644 index 000000000..fcf7d081c --- /dev/null +++ b/examples/plot_BrainDecode2.py @@ -0,0 +1,149 @@ +""" +============================================================= +Benchmarking on MOABB with BrainDecode deep net architectures +============================================================= +This example shows how to use BrainDecode in combination with MOABB evaluation. +In this example we use the architecture ShallowFBCSPNet. +""" +# Authors: Igor Carrara +# Bruno Aristimunha +# +# License: BSD (3-clause) + +import matplotlib.pyplot as plt +import mne +import torch +from braindecode import EEGClassifier +from braindecode.models import ShallowFBCSPNet +from sklearn.pipeline import Pipeline +from skorch.callbacks import EarlyStopping, EpochScoring +from skorch.dataset import ValidSplit + +from moabb.analysis.plotting import score_plot +from moabb.datasets import BNCI2014001 +from moabb.evaluations import CrossSessionEvaluation +from moabb.paradigms import MotorImagery +from moabb.pipelines.utilis_pytorch import Transformer +from moabb.utils import setup_seed + + +mne.set_log_level(False) + +# Print Information PyTorch +print(f"Torch Version: {torch.__version__}") + +# Set up GPU if it is there +cuda = torch.cuda.is_available() +device = "cuda" if cuda else "cpu" +print("GPU is", "AVAILABLE" if cuda else "NOT AVAILABLE") + + +############################################################################### +# In this example, we will use only the dataset ``BNCI2014001``. +# +# Running the benchmark +# --------------------- +# +# This example use the CrossSession evaluation procedure. We focus on the dataset BNCI2014001 and only on 1 subject +# to reduce the computational time. +# +# To keep the computational time low the number of epoch is reduced. In a real situation we suggest to use +# EPOCH = 1000 +# PATIENCE = 300 +# +# This code is implemented to run on CPU. If you're using a GPU, do not use multithreading +# (i.e. set n_jobs=1) + + +# Set random seed to be able to reproduce results +setup_seed(42) + +# Ensure that all operations are deterministic on GPU (if used) for reproducibility +torch.backends.cudnn.deterministic = True +torch.backends.cudnn.benchmark = False + + +# Hyperparameter +LEARNING_RATE = 0.0625 * 0.01 # result taken from BrainDecode +WEIGHT_DECAY = 0 # result taken from BrainDecode +BATCH_SIZE = 64 # result taken from BrainDecode +EPOCH = 10 +PATIENCE = 3 +fmin = 4 +fmax = 100 +tmin = 0 +tmax = None + +# Load the dataset +dataset = BNCI2014001() +events = ["right_hand", "left_hand"] +paradigm = MotorImagery( + events=events, n_classes=len(events), fmin=fmin, fmax=fmax, tmin=tmin, tmax=tmax +) +subjects = [1] +X, _, _ = paradigm.get_data(dataset=dataset, subjects=subjects) +# Define Transformer of Dataset compatible with Brain Decode +create_dataset = Transformer() + +############################################################################## +# Create Pipelines +# ---------------- +# In order to create a pipeline we need to load a model from BrainDecode. +# the second step is to define a skorch model using EEGClassifier from BrainDecode +# that allow to convert the PyTorch model in a scikit-learn classifier. +from moabb.pipelines.deep_learning_BrainDecode import * +from braindecode.models import ShallowFBCSPNet + +clf = braindecodeObject(fun_model=ShallowFBCSPNet, + criterion=torch.nn.CrossEntropyLoss, + optimizer=torch.optim.Adam, + optimizer__lr=LEARNING_RATE, + batch_size=BATCH_SIZE, + max_epochs=EPOCH, + train_split=ValidSplit(0.2), + device=device, + callbacks=[ + EarlyStopping(monitor="valid_loss", patience=PATIENCE), + EpochScoring( + scoring="accuracy", on_train=True, name="train_acc", lower_is_better=False + ), + EpochScoring( + scoring="accuracy", on_train=False, name="valid_acc", lower_is_better=False + ), + ], + verbose=1, + ) +print(clf) + +## + +# Create the pipelines +pipes = {} +pipes["ShallowFBCSPNet"] = Pipeline( + [("Braindecode_dataset", create_dataset), ("Net", BrainDecodeShallowConvNet2)] +) + + +############################################################################## +# Evaluation +# ---------- +dataset.subject_list = dataset.subject_list[:2] + +evaluation = CrossSessionEvaluation( + paradigm=paradigm, + datasets=dataset, + suffix="braindecode_example", + overwrite=True, + return_epochs=True, + n_jobs=1, +) + +results = evaluation.process(pipes) + +print(results.head()) + +############################################################################## +# Plot Results +# ---------------- +score_plot(results) +plt.show() diff --git a/examples/plot_benchmark_BrainDecode.py b/examples/plot_benchmark_BrainDecode.py new file mode 100644 index 000000000..c48c3c74e --- /dev/null +++ b/examples/plot_benchmark_BrainDecode.py @@ -0,0 +1,123 @@ +""" +==================================================================== +Benchmarking on MOABB with Tensorflow deep net architectures +==================================================================== +This example shows how to use MOABB to benchmark a set of Deep Learning pipeline (Tensorflow) +on all available datasets. +For this example, we will use only one dataset to keep the computation time low, but this benchmark is designed +to easily scale to many datasets. +""" +# Authors: Igor Carrara +# +# License: BSD (3-clause) + +import os + +import matplotlib.pyplot as plt +import mne +import torch +from braindecode import EEGClassifier +from braindecode.models import ShallowFBCSPNet + +from moabb import benchmark +from moabb.analysis.plotting import score_plot +from moabb.datasets import BNCI2014001 +from moabb.utils import setup_seed + + +mne.set_log_level(False) + +# Print Information PyTorch +print(f"Torch Version: {torch.__version__}") + +# Set up GPU if it is there +cuda = torch.cuda.is_available() +device = "cuda" if cuda else "cpu" +print("GPU is", "AVAILABLE" if cuda else "NOT AVAILABLE") + +############################################################################### +# In this example, we will use only the dataset ``BNCI2014001``. +# +# Running the benchmark +# --------------------- +# +# The benchmark is run using the ``benchmark`` function. You need to specify the +# folder containing the pipelines to use, the kind of evaluation and the paradigm +# to use. By default, the benchmark will use all available datasets for all +# paradigms listed in the pipelines. You could restrict to specific evaluation and +# paradigm using the ``evaluations`` and ``paradigms`` arguments. +# +# To save computation time, the results are cached. If you want to re-run the +# benchmark, you can set the ``overwrite`` argument to ``True``. +# +# It is possible to indicate the folder to cache the results and the one to save +# the analysis & figures. By default, the results are saved in the ``results`` +# folder, and the analysis & figures are saved in the ``benchmark`` folder. +# +# This code is implemented to run on CPU. If you're using a GPU, do not use multithreading +# (i.e. set n_jobs=1) + +# Set up reproducibility of Tensorflow +setup_seed(42) + +# Restrict this example only on the first two subject of BNCI2014001 +dataset = BNCI2014001() +dataset.subject_list = dataset.subject_list[:2] +datasets = [dataset] + +results = benchmark( + pipelines="./pipelines_BrainDecode", + evaluations=["WithinSession"], + paradigms=["LeftRightImagery"], + include_datasets=datasets, + results="./results/", + overwrite=False, + plot=False, + output="./benchmark/", + n_jobs=-1, +) + +############################################################################### +# The deep learning architectures implemented in MOABB are: +# - Shallow Convolutional Network [1]_ +# - Deep Convolutional Network [1]_ +# - EEGNet [2]_ +# - EEGTCNet [3]_ +# - EEGNex [4]_ +# - EEGITNet [5]_ +# +# Benchmark prints a summary of the results. Detailed results are saved in a +# pandas dataframe, and can be used to generate figures. The analysis & figures +# are saved in the ``benchmark`` folder. + +score_plot(results) +plt.show() + +############################################################################## +# References +# ---------- +# .. [1] Schirrmeister, R. T., Springenberg, J. T., Fiederer, L. D. J., +# Glasstetter, M., Eggensperger, K., Tangermann, M., ... & Ball, T. (2017). +# `Deep learning with convolutional neural networks for EEG decoding and +# visualization `_. +# Human brain mapping, 38(11), 5391-5420. +# .. [2] Lawhern, V. J., Solon, A. J., Waytowich, N. R., Gordon, S. M., +# Hung, C. P., & Lance, B. J. (2018). `EEGNet: a compact convolutional neural +# network for EEG-based brain-computer interfaces. +# `_ +# Journal of neural engineering, 15(5), 056013. +# .. [3] Ingolfsson, T. M., Hersche, M., Wang, X., Kobayashi, N., Cavigelli, L., & +# Benini, L. (2020, October). `EEG-TCNet: An accurate temporal convolutional +# network for embedded motor-imagery brain-machine interfaces. +# `_ +# In 2020 IEEE International Conference on Systems, Man, and Cybernetics (SMC) +# (pp. 2958-2965). IEEE. +# .. [4] Chen, X., Teng, X., Chen, H., Pan, Y., & Geyer, P. (2022). `Toward reliable +# signals decoding for electroencephalogram: A benchmark study to EEGNeX. +# `_ +# arXiv preprint arXiv:2207.12369. +# .. [5] Salami, A., Andreu-Perez, J., & Gillmeister, H. (2022). `EEG-ITNet: An +# explainable inception temporal convolutional network for motor imagery +# classification +# `_. +# IEEE Access, 10, 36672-36685. diff --git a/moabb/pipelines/deep_learning_BrainDecode.py b/moabb/pipelines/deep_learning_BrainDecode.py new file mode 100644 index 000000000..7a9ef1aef --- /dev/null +++ b/moabb/pipelines/deep_learning_BrainDecode.py @@ -0,0 +1,188 @@ +import numpy as np +from braindecode import EEGClassifier +from braindecode.models import ShallowFBCSPNet +import torch.nn as nn +import torch.nn.functional as F +import torch +from skorch.dataset import ValidSplit +from skorch.classifier import NeuralNetClassifier + +class BrainDecodeShallowConvNet(EEGClassifier): + def __init__( + self, + module = ShallowFBCSPNet( + in_chans=30, + n_classes=2, + input_window_samples=300, + final_conv_length="auto", + ), + criterion=torch.nn.CrossEntropyLoss, + optimizer=torch.optim.Adam, + batch_size=64, + max_epochs=2, + train_split=ValidSplit(0.2), + device="cpu", + verbose=1, + **kwargs + ): + self.module = module + self.criterion = criterion + self.optimizer = optimizer + self.batch_size = batch_size + self.max_epochs = max_epochs + self.train_split = train_split + self.device = device + self.verbose = verbose + super().__init__(module=module, **kwargs) + + + def check_data(self, X, y): + + self.set_params(module__in_chans=X.shape[1]) + self.set_params(module__n_classes=len(np.unique(y))) + self.set_params(module__input_window_samples=X.shape[2]) + self.initialize() + super().check_data(X, y) + + +class BrainDecodeShallowConvNet2(NeuralNetClassifier): + def __init__( + self, + *args, + custom=ShallowFBCSPNet, + custom__in_chans=30, + custom__n_classes=2, + custom__input_window_samples=300, + custom__final_conv_length="auto", + criterion=torch.nn.CrossEntropyLoss, + optimizer=torch.optim.Adam, + batch_size=64, + max_epochs=2, + train_split=ValidSplit(0.2), + device="cpu", + verbose=1, + **kwargs + ): + self.custom = custom + super().__init__(*args, **kwargs) + self.criterion = criterion + self.optimizer = optimizer + self.batch_size = batch_size + self.max_epochs = max_epochs + self.train_split = train_split + self.device = device + self.verbose = verbose + self.custom__in_chans = custom__in_chans + self.custom__n_classes = custom__n_classes + self.custom__input_window_samples = custom__input_window_samples + self.custom__final_conv_length = custom__final_conv_length + + def initialize_module(self, *args, **kwargs): + + params = self.get_params_for('custom') + print(params) + self.custom_ = self.custom(**params) + + return super().initialize_module() + + + def check_data(self, X, y): + super().check_data(X, y) + + self.set_params(module__in_chans=X.shape[1]) + self.set_params(module__n_classes=len(np.unique(y))) + self.set_params(module__input_window_samples=X.shape[2]) + self.initialize() + + + +class BrainDecodeShallowConvNet3(EEGClassifier): + def __init__( + self, + module = ShallowFBCSPNet( + in_chans=30, + n_classes=2, + input_window_samples=300, + final_conv_length="auto", + ), + criterion=torch.nn.CrossEntropyLoss, + optimizer=torch.optim.Adam, + batch_size=64, + max_epochs=2, + train_split=ValidSplit(0.2), + device="cpu", + verbose=1, + **kwargs + ): + self.module = module + self.criterion = criterion + self.optimizer = optimizer + self.batch_size = batch_size + self.max_epochs = max_epochs + self.train_split = train_split + self.device = device + self.verbose = verbose + super().__init__(module=module, **kwargs) + + +import numpy as np +from functools import partial +from sklearn.base import BaseEstimator, ClassifierMixin +from braindecode import EEGClassifier +from braindecode.models import ShallowFBCSPNet + +class braindecodeObject(BaseEstimator, ClassifierMixin): + + def __init__(self, fun_model, criterion, optimizer, + train_split, optimizer__lr + , batch_size, max_epochs, verbose, + callbacks, device): + self.clf = None + self.fun_model = fun_model + self.criterion = criterion + self.optimizer = optimizer + self.batch_size = batch_size + self.max_epochs = max_epochs + self.optimizer__lr = optimizer__lr + self.train_split = train_split + self.device = device + self.verbose = verbose + self.callbacks = callbacks + + def build_torch_model(self, fun_module, in_chans, n_classes, + input_window_samples, **kargs): + self.module = fun_module(in_chans=in_chans, n_classes=n_classes, + input_window_samples=input_window_samples, **kargs) + return self + + def _build_EEGClassifier(self): + return EEGClassifier(module=self.module, + criterion=self.criterion, + optimizer=self.optimizer, + train_split=self.train_split, # using valid_set for validation + optimizer__lr=self.optimizer__lr, + batch_size=self.batch_size, + callbacks=self.callbacks, + device=self.device, ) + + def fit(self, X, y): + in_chans = X.shape[1] + n_classes = len(np.unique(y)) + input_window_samples = X.shape[2] + + _ = self._build_torch_model(self.fun_model, + in_chans=in_chans, + n_classes=n_classes, + input_window_samples=input_window_samples) + + self.clf = self._build_EEGClassifier() + + self.clf.initialize() + + return self.clf.fit(X, y) + + def predict(self, X): + return self.clf.predict(X) + + def predict_proba(self, X): + return self.clf.predict_proba(X) From d3482c658f567e342bac56a550f9d332e1ac72c5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 14:29:40 +0000 Subject: [PATCH 17/34] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/plot_BrainDecode2.py | 43 ++--- moabb/pipelines/deep_learning_BrainDecode.py | 171 ++++++++++--------- 2 files changed, 116 insertions(+), 98 deletions(-) diff --git a/examples/plot_BrainDecode2.py b/examples/plot_BrainDecode2.py index fcf7d081c..88d193e0f 100644 --- a/examples/plot_BrainDecode2.py +++ b/examples/plot_BrainDecode2.py @@ -85,6 +85,8 @@ # Define Transformer of Dataset compatible with Brain Decode create_dataset = Transformer() +from braindecode.models import ShallowFBCSPNet + ############################################################################## # Create Pipelines # ---------------- @@ -92,27 +94,28 @@ # the second step is to define a skorch model using EEGClassifier from BrainDecode # that allow to convert the PyTorch model in a scikit-learn classifier. from moabb.pipelines.deep_learning_BrainDecode import * -from braindecode.models import ShallowFBCSPNet -clf = braindecodeObject(fun_model=ShallowFBCSPNet, - criterion=torch.nn.CrossEntropyLoss, - optimizer=torch.optim.Adam, - optimizer__lr=LEARNING_RATE, - batch_size=BATCH_SIZE, - max_epochs=EPOCH, - train_split=ValidSplit(0.2), - device=device, - callbacks=[ - EarlyStopping(monitor="valid_loss", patience=PATIENCE), - EpochScoring( - scoring="accuracy", on_train=True, name="train_acc", lower_is_better=False - ), - EpochScoring( - scoring="accuracy", on_train=False, name="valid_acc", lower_is_better=False - ), - ], - verbose=1, - ) + +clf = braindecodeObject( + fun_model=ShallowFBCSPNet, + criterion=torch.nn.CrossEntropyLoss, + optimizer=torch.optim.Adam, + optimizer__lr=LEARNING_RATE, + batch_size=BATCH_SIZE, + max_epochs=EPOCH, + train_split=ValidSplit(0.2), + device=device, + callbacks=[ + EarlyStopping(monitor="valid_loss", patience=PATIENCE), + EpochScoring( + scoring="accuracy", on_train=True, name="train_acc", lower_is_better=False + ), + EpochScoring( + scoring="accuracy", on_train=False, name="valid_acc", lower_is_better=False + ), + ], + verbose=1, +) print(clf) ## diff --git a/moabb/pipelines/deep_learning_BrainDecode.py b/moabb/pipelines/deep_learning_BrainDecode.py index 7a9ef1aef..5b4fe083b 100644 --- a/moabb/pipelines/deep_learning_BrainDecode.py +++ b/moabb/pipelines/deep_learning_BrainDecode.py @@ -1,29 +1,30 @@ import numpy as np -from braindecode import EEGClassifier -from braindecode.models import ShallowFBCSPNet +import torch import torch.nn as nn import torch.nn.functional as F -import torch -from skorch.dataset import ValidSplit +from braindecode import EEGClassifier +from braindecode.models import ShallowFBCSPNet from skorch.classifier import NeuralNetClassifier +from skorch.dataset import ValidSplit + class BrainDecodeShallowConvNet(EEGClassifier): def __init__( - self, - module = ShallowFBCSPNet( - in_chans=30, - n_classes=2, - input_window_samples=300, - final_conv_length="auto", - ), - criterion=torch.nn.CrossEntropyLoss, - optimizer=torch.optim.Adam, - batch_size=64, - max_epochs=2, - train_split=ValidSplit(0.2), - device="cpu", - verbose=1, - **kwargs + self, + module=ShallowFBCSPNet( + in_chans=30, + n_classes=2, + input_window_samples=300, + final_conv_length="auto", + ), + criterion=torch.nn.CrossEntropyLoss, + optimizer=torch.optim.Adam, + batch_size=64, + max_epochs=2, + train_split=ValidSplit(0.2), + device="cpu", + verbose=1, + **kwargs, ): self.module = module self.criterion = criterion @@ -35,9 +36,7 @@ def __init__( self.verbose = verbose super().__init__(module=module, **kwargs) - def check_data(self, X, y): - self.set_params(module__in_chans=X.shape[1]) self.set_params(module__n_classes=len(np.unique(y))) self.set_params(module__input_window_samples=X.shape[2]) @@ -47,21 +46,21 @@ def check_data(self, X, y): class BrainDecodeShallowConvNet2(NeuralNetClassifier): def __init__( - self, - *args, - custom=ShallowFBCSPNet, - custom__in_chans=30, - custom__n_classes=2, - custom__input_window_samples=300, - custom__final_conv_length="auto", - criterion=torch.nn.CrossEntropyLoss, - optimizer=torch.optim.Adam, - batch_size=64, - max_epochs=2, - train_split=ValidSplit(0.2), - device="cpu", - verbose=1, - **kwargs + self, + *args, + custom=ShallowFBCSPNet, + custom__in_chans=30, + custom__n_classes=2, + custom__input_window_samples=300, + custom__final_conv_length="auto", + criterion=torch.nn.CrossEntropyLoss, + optimizer=torch.optim.Adam, + batch_size=64, + max_epochs=2, + train_split=ValidSplit(0.2), + device="cpu", + verbose=1, + **kwargs, ): self.custom = custom super().__init__(*args, **kwargs) @@ -78,14 +77,12 @@ def __init__( self.custom__final_conv_length = custom__final_conv_length def initialize_module(self, *args, **kwargs): - - params = self.get_params_for('custom') + params = self.get_params_for("custom") print(params) self.custom_ = self.custom(**params) return super().initialize_module() - def check_data(self, X, y): super().check_data(X, y) @@ -95,24 +92,23 @@ def check_data(self, X, y): self.initialize() - class BrainDecodeShallowConvNet3(EEGClassifier): def __init__( - self, - module = ShallowFBCSPNet( - in_chans=30, - n_classes=2, - input_window_samples=300, - final_conv_length="auto", - ), - criterion=torch.nn.CrossEntropyLoss, - optimizer=torch.optim.Adam, - batch_size=64, - max_epochs=2, - train_split=ValidSplit(0.2), - device="cpu", - verbose=1, - **kwargs + self, + module=ShallowFBCSPNet( + in_chans=30, + n_classes=2, + input_window_samples=300, + final_conv_length="auto", + ), + criterion=torch.nn.CrossEntropyLoss, + optimizer=torch.optim.Adam, + batch_size=64, + max_epochs=2, + train_split=ValidSplit(0.2), + device="cpu", + verbose=1, + **kwargs, ): self.module = module self.criterion = criterion @@ -125,18 +121,28 @@ def __init__( super().__init__(module=module, **kwargs) -import numpy as np from functools import partial -from sklearn.base import BaseEstimator, ClassifierMixin + +import numpy as np from braindecode import EEGClassifier from braindecode.models import ShallowFBCSPNet +from sklearn.base import BaseEstimator, ClassifierMixin -class braindecodeObject(BaseEstimator, ClassifierMixin): - def __init__(self, fun_model, criterion, optimizer, - train_split, optimizer__lr - , batch_size, max_epochs, verbose, - callbacks, device): +class braindecodeObject(BaseEstimator, ClassifierMixin): + def __init__( + self, + fun_model, + criterion, + optimizer, + train_split, + optimizer__lr, + batch_size, + max_epochs, + verbose, + callbacks, + device, + ): self.clf = None self.fun_model = fun_model self.criterion = criterion @@ -149,31 +155,40 @@ def __init__(self, fun_model, criterion, optimizer, self.verbose = verbose self.callbacks = callbacks - def build_torch_model(self, fun_module, in_chans, n_classes, - input_window_samples, **kargs): - self.module = fun_module(in_chans=in_chans, n_classes=n_classes, - input_window_samples=input_window_samples, **kargs) + def build_torch_model( + self, fun_module, in_chans, n_classes, input_window_samples, **kargs + ): + self.module = fun_module( + in_chans=in_chans, + n_classes=n_classes, + input_window_samples=input_window_samples, + **kargs, + ) return self def _build_EEGClassifier(self): - return EEGClassifier(module=self.module, - criterion=self.criterion, - optimizer=self.optimizer, - train_split=self.train_split, # using valid_set for validation - optimizer__lr=self.optimizer__lr, - batch_size=self.batch_size, - callbacks=self.callbacks, - device=self.device, ) + return EEGClassifier( + module=self.module, + criterion=self.criterion, + optimizer=self.optimizer, + train_split=self.train_split, # using valid_set for validation + optimizer__lr=self.optimizer__lr, + batch_size=self.batch_size, + callbacks=self.callbacks, + device=self.device, + ) def fit(self, X, y): in_chans = X.shape[1] n_classes = len(np.unique(y)) input_window_samples = X.shape[2] - _ = self._build_torch_model(self.fun_model, - in_chans=in_chans, - n_classes=n_classes, - input_window_samples=input_window_samples) + _ = self._build_torch_model( + self.fun_model, + in_chans=in_chans, + n_classes=n_classes, + input_window_samples=input_window_samples, + ) self.clf = self._build_EEGClassifier() From c1384b76bb1d08a022d9a37382505f9a9b73796b Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Tue, 7 Mar 2023 16:58:57 +0100 Subject: [PATCH 18/34] Plot_function --- .gitignore | 1 + .../PyTorch_EEGNetv4.yml | 17 ++ .../PyTorch_ShallowConvNet.yml | 12 -- examples/plot_BrainDecode2.py | 149 -------------- moabb/pipelines/deep_learning_BrainDecode.py | 188 ------------------ 5 files changed, 18 insertions(+), 349 deletions(-) create mode 100644 examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml delete mode 100644 examples/pipelines_BrainDecode/PyTorch_ShallowConvNet.yml delete mode 100644 examples/plot_BrainDecode2.py delete mode 100644 moabb/pipelines/deep_learning_BrainDecode.py diff --git a/.gitignore b/.gitignore index 3b75d0fa9..e8ec9aa5b 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,4 @@ analysis/* # mac os x stuff *DS_store* +/examples/pipelines_BrainDecode/ diff --git a/examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml b/examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml new file mode 100644 index 000000000..b58ee90de --- /dev/null +++ b/examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml @@ -0,0 +1,17 @@ +name: EEGNetV4 + +paradigms: + - LeftRightImagery + - MotorImagery + +pipeline: + - name: Transformer + from: moabb.pipelines.utilis_pytorch + + - name: EEGClassifier + from: braindecode.classifier + parameters: + module: EEGNetv4 + from: braindecode.models.eegnet + parameters: + diff --git a/examples/pipelines_BrainDecode/PyTorch_ShallowConvNet.yml b/examples/pipelines_BrainDecode/PyTorch_ShallowConvNet.yml deleted file mode 100644 index cb05151eb..000000000 --- a/examples/pipelines_BrainDecode/PyTorch_ShallowConvNet.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: ExamplePyTorch - -paradigms: - - LeftRightImagery - - MotorImagery - -pipeline: - - name: Transformer - from: moabb.pipelines.utilis_pytorch - - - name: BrainDecodeShallowConvNet - from: moabb.pipelines.deep_learning_BrainDecode diff --git a/examples/plot_BrainDecode2.py b/examples/plot_BrainDecode2.py deleted file mode 100644 index fcf7d081c..000000000 --- a/examples/plot_BrainDecode2.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -============================================================= -Benchmarking on MOABB with BrainDecode deep net architectures -============================================================= -This example shows how to use BrainDecode in combination with MOABB evaluation. -In this example we use the architecture ShallowFBCSPNet. -""" -# Authors: Igor Carrara -# Bruno Aristimunha -# -# License: BSD (3-clause) - -import matplotlib.pyplot as plt -import mne -import torch -from braindecode import EEGClassifier -from braindecode.models import ShallowFBCSPNet -from sklearn.pipeline import Pipeline -from skorch.callbacks import EarlyStopping, EpochScoring -from skorch.dataset import ValidSplit - -from moabb.analysis.plotting import score_plot -from moabb.datasets import BNCI2014001 -from moabb.evaluations import CrossSessionEvaluation -from moabb.paradigms import MotorImagery -from moabb.pipelines.utilis_pytorch import Transformer -from moabb.utils import setup_seed - - -mne.set_log_level(False) - -# Print Information PyTorch -print(f"Torch Version: {torch.__version__}") - -# Set up GPU if it is there -cuda = torch.cuda.is_available() -device = "cuda" if cuda else "cpu" -print("GPU is", "AVAILABLE" if cuda else "NOT AVAILABLE") - - -############################################################################### -# In this example, we will use only the dataset ``BNCI2014001``. -# -# Running the benchmark -# --------------------- -# -# This example use the CrossSession evaluation procedure. We focus on the dataset BNCI2014001 and only on 1 subject -# to reduce the computational time. -# -# To keep the computational time low the number of epoch is reduced. In a real situation we suggest to use -# EPOCH = 1000 -# PATIENCE = 300 -# -# This code is implemented to run on CPU. If you're using a GPU, do not use multithreading -# (i.e. set n_jobs=1) - - -# Set random seed to be able to reproduce results -setup_seed(42) - -# Ensure that all operations are deterministic on GPU (if used) for reproducibility -torch.backends.cudnn.deterministic = True -torch.backends.cudnn.benchmark = False - - -# Hyperparameter -LEARNING_RATE = 0.0625 * 0.01 # result taken from BrainDecode -WEIGHT_DECAY = 0 # result taken from BrainDecode -BATCH_SIZE = 64 # result taken from BrainDecode -EPOCH = 10 -PATIENCE = 3 -fmin = 4 -fmax = 100 -tmin = 0 -tmax = None - -# Load the dataset -dataset = BNCI2014001() -events = ["right_hand", "left_hand"] -paradigm = MotorImagery( - events=events, n_classes=len(events), fmin=fmin, fmax=fmax, tmin=tmin, tmax=tmax -) -subjects = [1] -X, _, _ = paradigm.get_data(dataset=dataset, subjects=subjects) -# Define Transformer of Dataset compatible with Brain Decode -create_dataset = Transformer() - -############################################################################## -# Create Pipelines -# ---------------- -# In order to create a pipeline we need to load a model from BrainDecode. -# the second step is to define a skorch model using EEGClassifier from BrainDecode -# that allow to convert the PyTorch model in a scikit-learn classifier. -from moabb.pipelines.deep_learning_BrainDecode import * -from braindecode.models import ShallowFBCSPNet - -clf = braindecodeObject(fun_model=ShallowFBCSPNet, - criterion=torch.nn.CrossEntropyLoss, - optimizer=torch.optim.Adam, - optimizer__lr=LEARNING_RATE, - batch_size=BATCH_SIZE, - max_epochs=EPOCH, - train_split=ValidSplit(0.2), - device=device, - callbacks=[ - EarlyStopping(monitor="valid_loss", patience=PATIENCE), - EpochScoring( - scoring="accuracy", on_train=True, name="train_acc", lower_is_better=False - ), - EpochScoring( - scoring="accuracy", on_train=False, name="valid_acc", lower_is_better=False - ), - ], - verbose=1, - ) -print(clf) - -## - -# Create the pipelines -pipes = {} -pipes["ShallowFBCSPNet"] = Pipeline( - [("Braindecode_dataset", create_dataset), ("Net", BrainDecodeShallowConvNet2)] -) - - -############################################################################## -# Evaluation -# ---------- -dataset.subject_list = dataset.subject_list[:2] - -evaluation = CrossSessionEvaluation( - paradigm=paradigm, - datasets=dataset, - suffix="braindecode_example", - overwrite=True, - return_epochs=True, - n_jobs=1, -) - -results = evaluation.process(pipes) - -print(results.head()) - -############################################################################## -# Plot Results -# ---------------- -score_plot(results) -plt.show() diff --git a/moabb/pipelines/deep_learning_BrainDecode.py b/moabb/pipelines/deep_learning_BrainDecode.py deleted file mode 100644 index 7a9ef1aef..000000000 --- a/moabb/pipelines/deep_learning_BrainDecode.py +++ /dev/null @@ -1,188 +0,0 @@ -import numpy as np -from braindecode import EEGClassifier -from braindecode.models import ShallowFBCSPNet -import torch.nn as nn -import torch.nn.functional as F -import torch -from skorch.dataset import ValidSplit -from skorch.classifier import NeuralNetClassifier - -class BrainDecodeShallowConvNet(EEGClassifier): - def __init__( - self, - module = ShallowFBCSPNet( - in_chans=30, - n_classes=2, - input_window_samples=300, - final_conv_length="auto", - ), - criterion=torch.nn.CrossEntropyLoss, - optimizer=torch.optim.Adam, - batch_size=64, - max_epochs=2, - train_split=ValidSplit(0.2), - device="cpu", - verbose=1, - **kwargs - ): - self.module = module - self.criterion = criterion - self.optimizer = optimizer - self.batch_size = batch_size - self.max_epochs = max_epochs - self.train_split = train_split - self.device = device - self.verbose = verbose - super().__init__(module=module, **kwargs) - - - def check_data(self, X, y): - - self.set_params(module__in_chans=X.shape[1]) - self.set_params(module__n_classes=len(np.unique(y))) - self.set_params(module__input_window_samples=X.shape[2]) - self.initialize() - super().check_data(X, y) - - -class BrainDecodeShallowConvNet2(NeuralNetClassifier): - def __init__( - self, - *args, - custom=ShallowFBCSPNet, - custom__in_chans=30, - custom__n_classes=2, - custom__input_window_samples=300, - custom__final_conv_length="auto", - criterion=torch.nn.CrossEntropyLoss, - optimizer=torch.optim.Adam, - batch_size=64, - max_epochs=2, - train_split=ValidSplit(0.2), - device="cpu", - verbose=1, - **kwargs - ): - self.custom = custom - super().__init__(*args, **kwargs) - self.criterion = criterion - self.optimizer = optimizer - self.batch_size = batch_size - self.max_epochs = max_epochs - self.train_split = train_split - self.device = device - self.verbose = verbose - self.custom__in_chans = custom__in_chans - self.custom__n_classes = custom__n_classes - self.custom__input_window_samples = custom__input_window_samples - self.custom__final_conv_length = custom__final_conv_length - - def initialize_module(self, *args, **kwargs): - - params = self.get_params_for('custom') - print(params) - self.custom_ = self.custom(**params) - - return super().initialize_module() - - - def check_data(self, X, y): - super().check_data(X, y) - - self.set_params(module__in_chans=X.shape[1]) - self.set_params(module__n_classes=len(np.unique(y))) - self.set_params(module__input_window_samples=X.shape[2]) - self.initialize() - - - -class BrainDecodeShallowConvNet3(EEGClassifier): - def __init__( - self, - module = ShallowFBCSPNet( - in_chans=30, - n_classes=2, - input_window_samples=300, - final_conv_length="auto", - ), - criterion=torch.nn.CrossEntropyLoss, - optimizer=torch.optim.Adam, - batch_size=64, - max_epochs=2, - train_split=ValidSplit(0.2), - device="cpu", - verbose=1, - **kwargs - ): - self.module = module - self.criterion = criterion - self.optimizer = optimizer - self.batch_size = batch_size - self.max_epochs = max_epochs - self.train_split = train_split - self.device = device - self.verbose = verbose - super().__init__(module=module, **kwargs) - - -import numpy as np -from functools import partial -from sklearn.base import BaseEstimator, ClassifierMixin -from braindecode import EEGClassifier -from braindecode.models import ShallowFBCSPNet - -class braindecodeObject(BaseEstimator, ClassifierMixin): - - def __init__(self, fun_model, criterion, optimizer, - train_split, optimizer__lr - , batch_size, max_epochs, verbose, - callbacks, device): - self.clf = None - self.fun_model = fun_model - self.criterion = criterion - self.optimizer = optimizer - self.batch_size = batch_size - self.max_epochs = max_epochs - self.optimizer__lr = optimizer__lr - self.train_split = train_split - self.device = device - self.verbose = verbose - self.callbacks = callbacks - - def build_torch_model(self, fun_module, in_chans, n_classes, - input_window_samples, **kargs): - self.module = fun_module(in_chans=in_chans, n_classes=n_classes, - input_window_samples=input_window_samples, **kargs) - return self - - def _build_EEGClassifier(self): - return EEGClassifier(module=self.module, - criterion=self.criterion, - optimizer=self.optimizer, - train_split=self.train_split, # using valid_set for validation - optimizer__lr=self.optimizer__lr, - batch_size=self.batch_size, - callbacks=self.callbacks, - device=self.device, ) - - def fit(self, X, y): - in_chans = X.shape[1] - n_classes = len(np.unique(y)) - input_window_samples = X.shape[2] - - _ = self._build_torch_model(self.fun_model, - in_chans=in_chans, - n_classes=n_classes, - input_window_samples=input_window_samples) - - self.clf = self._build_EEGClassifier() - - self.clf.initialize() - - return self.clf.fit(X, y) - - def predict(self, X): - return self.clf.predict(X) - - def predict_proba(self, X): - return self.clf.predict_proba(X) From 5e061564a401e7327182e410e016fc3b611d7033 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 16:00:09 +0000 Subject: [PATCH 19/34] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml b/examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml index b58ee90de..31a2fa386 100644 --- a/examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml +++ b/examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml @@ -14,4 +14,3 @@ pipeline: module: EEGNetv4 from: braindecode.models.eegnet parameters: - From 1540dd4299c9320ba9440921175b3273e75f09f2 Mon Sep 17 00:00:00 2001 From: Igor Carrara <94047258+carraraig@users.noreply.github.com> Date: Tue, 7 Mar 2023 17:00:26 +0100 Subject: [PATCH 20/34] Delete PyTorch_EEGNetv4.yml --- .../pipelines_BrainDecode/PyTorch_EEGNetv4.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml diff --git a/examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml b/examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml deleted file mode 100644 index 31a2fa386..000000000 --- a/examples/pipelines_BrainDecode/PyTorch_EEGNetv4.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: EEGNetV4 - -paradigms: - - LeftRightImagery - - MotorImagery - -pipeline: - - name: Transformer - from: moabb.pipelines.utilis_pytorch - - - name: EEGClassifier - from: braindecode.classifier - parameters: - module: EEGNetv4 - from: braindecode.models.eegnet - parameters: From fb2c66db1bf72ee5e30a7785ea3aaf3540b9ef54 Mon Sep 17 00:00:00 2001 From: Igor Carrara <94047258+carraraig@users.noreply.github.com> Date: Tue, 7 Mar 2023 17:00:41 +0100 Subject: [PATCH 21/34] Delete plot_benchmark_BrainDecode.py --- examples/plot_benchmark_BrainDecode.py | 123 ------------------------- 1 file changed, 123 deletions(-) delete mode 100644 examples/plot_benchmark_BrainDecode.py diff --git a/examples/plot_benchmark_BrainDecode.py b/examples/plot_benchmark_BrainDecode.py deleted file mode 100644 index c48c3c74e..000000000 --- a/examples/plot_benchmark_BrainDecode.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -==================================================================== -Benchmarking on MOABB with Tensorflow deep net architectures -==================================================================== -This example shows how to use MOABB to benchmark a set of Deep Learning pipeline (Tensorflow) -on all available datasets. -For this example, we will use only one dataset to keep the computation time low, but this benchmark is designed -to easily scale to many datasets. -""" -# Authors: Igor Carrara -# -# License: BSD (3-clause) - -import os - -import matplotlib.pyplot as plt -import mne -import torch -from braindecode import EEGClassifier -from braindecode.models import ShallowFBCSPNet - -from moabb import benchmark -from moabb.analysis.plotting import score_plot -from moabb.datasets import BNCI2014001 -from moabb.utils import setup_seed - - -mne.set_log_level(False) - -# Print Information PyTorch -print(f"Torch Version: {torch.__version__}") - -# Set up GPU if it is there -cuda = torch.cuda.is_available() -device = "cuda" if cuda else "cpu" -print("GPU is", "AVAILABLE" if cuda else "NOT AVAILABLE") - -############################################################################### -# In this example, we will use only the dataset ``BNCI2014001``. -# -# Running the benchmark -# --------------------- -# -# The benchmark is run using the ``benchmark`` function. You need to specify the -# folder containing the pipelines to use, the kind of evaluation and the paradigm -# to use. By default, the benchmark will use all available datasets for all -# paradigms listed in the pipelines. You could restrict to specific evaluation and -# paradigm using the ``evaluations`` and ``paradigms`` arguments. -# -# To save computation time, the results are cached. If you want to re-run the -# benchmark, you can set the ``overwrite`` argument to ``True``. -# -# It is possible to indicate the folder to cache the results and the one to save -# the analysis & figures. By default, the results are saved in the ``results`` -# folder, and the analysis & figures are saved in the ``benchmark`` folder. -# -# This code is implemented to run on CPU. If you're using a GPU, do not use multithreading -# (i.e. set n_jobs=1) - -# Set up reproducibility of Tensorflow -setup_seed(42) - -# Restrict this example only on the first two subject of BNCI2014001 -dataset = BNCI2014001() -dataset.subject_list = dataset.subject_list[:2] -datasets = [dataset] - -results = benchmark( - pipelines="./pipelines_BrainDecode", - evaluations=["WithinSession"], - paradigms=["LeftRightImagery"], - include_datasets=datasets, - results="./results/", - overwrite=False, - plot=False, - output="./benchmark/", - n_jobs=-1, -) - -############################################################################### -# The deep learning architectures implemented in MOABB are: -# - Shallow Convolutional Network [1]_ -# - Deep Convolutional Network [1]_ -# - EEGNet [2]_ -# - EEGTCNet [3]_ -# - EEGNex [4]_ -# - EEGITNet [5]_ -# -# Benchmark prints a summary of the results. Detailed results are saved in a -# pandas dataframe, and can be used to generate figures. The analysis & figures -# are saved in the ``benchmark`` folder. - -score_plot(results) -plt.show() - -############################################################################## -# References -# ---------- -# .. [1] Schirrmeister, R. T., Springenberg, J. T., Fiederer, L. D. J., -# Glasstetter, M., Eggensperger, K., Tangermann, M., ... & Ball, T. (2017). -# `Deep learning with convolutional neural networks for EEG decoding and -# visualization `_. -# Human brain mapping, 38(11), 5391-5420. -# .. [2] Lawhern, V. J., Solon, A. J., Waytowich, N. R., Gordon, S. M., -# Hung, C. P., & Lance, B. J. (2018). `EEGNet: a compact convolutional neural -# network for EEG-based brain-computer interfaces. -# `_ -# Journal of neural engineering, 15(5), 056013. -# .. [3] Ingolfsson, T. M., Hersche, M., Wang, X., Kobayashi, N., Cavigelli, L., & -# Benini, L. (2020, October). `EEG-TCNet: An accurate temporal convolutional -# network for embedded motor-imagery brain-machine interfaces. -# `_ -# In 2020 IEEE International Conference on Systems, Man, and Cybernetics (SMC) -# (pp. 2958-2965). IEEE. -# .. [4] Chen, X., Teng, X., Chen, H., Pan, Y., & Geyer, P. (2022). `Toward reliable -# signals decoding for electroencephalogram: A benchmark study to EEGNeX. -# `_ -# arXiv preprint arXiv:2207.12369. -# .. [5] Salami, A., Andreu-Perez, J., & Gillmeister, H. (2022). `EEG-ITNet: An -# explainable inception temporal convolutional network for motor imagery -# classification -# `_. -# IEEE Access, 10, 36672-36685. From c642d7848829157474ea15dafb538290f9ba44b1 Mon Sep 17 00:00:00 2001 From: bruAristimunha Date: Wed, 8 Mar 2023 04:29:33 +0100 Subject: [PATCH 22/34] Renaming the file --- moabb/pipelines/utilis_pytorch.py | 31 -------- moabb/pipelines/utils_pytorch.py | 118 ++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 31 deletions(-) delete mode 100644 moabb/pipelines/utilis_pytorch.py create mode 100644 moabb/pipelines/utils_pytorch.py diff --git a/moabb/pipelines/utilis_pytorch.py b/moabb/pipelines/utilis_pytorch.py deleted file mode 100644 index 47e4f4b5e..000000000 --- a/moabb/pipelines/utilis_pytorch.py +++ /dev/null @@ -1,31 +0,0 @@ -from braindecode.datasets import create_from_X_y -from sklearn.base import BaseEstimator, TransformerMixin - - -class Transformer(BaseEstimator, TransformerMixin): - """ - Class to Load the data from MOABB in a format compatible with braindecode - """ - - def __init__(self, kw_args=None): - self.kw_args = kw_args - - def fit(self, X, y=None): - self.y = y - return self - - def transform(self, X, y=None): - dataset = create_from_X_y( - X.get_data(), - y=self.y, - window_size_samples=X.get_data().shape[2], - window_stride_samples=X.get_data().shape[2], - drop_last_window=False, - sfreq=X.info["sfreq"], - ) - - return dataset - - def __sklearn_is_fitted__(self): - """Return True since Transfomer is stateless.""" - return True diff --git a/moabb/pipelines/utils_pytorch.py b/moabb/pipelines/utils_pytorch.py new file mode 100644 index 000000000..53849ec6f --- /dev/null +++ b/moabb/pipelines/utils_pytorch.py @@ -0,0 +1,118 @@ +from braindecode.datasets import BaseConcatDataset, create_from_X_y +from numpy import unique +from sklearn.base import BaseEstimator, TransformerMixin +from skorch.callbacks import Callback + + +class Transformer(BaseEstimator, TransformerMixin): + """ + Class to Load the data from MOABB in a format compatible with braindecode + """ + + def __init__(self, kw_args=None): + self.kw_args = kw_args + + def fit(self, X, y=None): + self.y = y + return self + + def transform(self, X, y=None): + dataset = create_from_X_y( + X.get_data(), + y=self.y, + window_size_samples=X.get_data().shape[2], + window_stride_samples=X.get_data().shape[2], + drop_last_window=False, + sfreq=X.info["sfreq"], + ) + + return dataset + + def __sklearn_is_fitted__(self): + """Return True since Transfomer is stateless.""" + return True + + +def get_shape_from_baseconcat(X): + """Get the shape of the data after BaseConcatDataset is applied""" + if isinstance(X, BaseConcatDataset): + in_channel = X[0][0].shape[0] + input_window_samples = X[0][0].shape[1] + return in_channel, input_window_samples + else: + return X.shape + + +class InputShapeSetterEEG(Callback): + """Sets the input dimension of the PyTorch module to the input dimension + of the training data. + This can be of use when the shape of X is not known beforehand, + e.g. when using a skorch model within an sklearn pipeline and + grid-searching feature transformers, or using feature selection + methods.InputShapeSetterEEG + Basic usage: + + Parameters + ---------- + param_name : str (default='input_dim') + The parameter name is the parameter your model uses to define the + input dimension in its ``__init__`` method. + input_dim_fn : callable, None (default=None) + In case your ``X`` value is more complex and deriving the input + dimension is not as easy as ``X.shape[-1]`` you can pass a callable + to this parameter which takes ``X`` and returns the input dimension. + module_name : str (default='module') + Only needs change when you are using more than one module in your + skorch model (e.g., in case of GANs). + """ + + def __init__( + self, + param_name_1="in_chans", + param_name_2="input_window_samples", + param_name_3="n_classes", + input_dim_fn=None, + module_name="module", + ): + self.module_name = module_name + self.param_name_1 = param_name_1 + self.param_name_2 = param_name_2 + self.param_name_3 = param_name_3 + self.input_dim_fn = input_dim_fn + + def get_input_dim(self, X): + if self.input_dim_fn is not None: + return self.input_dim_fn(X) + if len(X.shape) < 2: + raise ValueError( + "Expected at least two-dimensional input data for X. " + "If your data is one-dimensional, please use the " + "`input_dim_fn` parameter to infer the correct " + "input shape." + ) + return X.shape[-1] + + def on_train_begin(self, net, X, y, **kwargs): + params = net.get_params() + in_chans, input_window_samples = self.get_input_dim(X) + n_classes = len(unique(y)) + + param_name_1 = f"{self.module_name}__{self.param_name_1}" + param_name_2 = f"{self.module_name}__{self.param_name_2}" + param_name_3 = f"{self.module_name}__{self.param_name_3}" + + if ( + params["module"].in_chans + == in_chans & params["module"].input_window_samples + == input_window_samples & params["module"].n_classes + == n_classes + ): + return + + kwargs = { + param_name_1: in_chans, + param_name_2: input_window_samples, + param_name_3: n_classes, + } + + net.set_params(**kwargs) From 67cfeb5a74083825724bd5edbf4ec62b083c6db3 Mon Sep 17 00:00:00 2001 From: bruAristimunha Date: Wed, 8 Mar 2023 04:30:11 +0100 Subject: [PATCH 23/34] Changing init file --- moabb/pipelines/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/moabb/pipelines/__init__.py b/moabb/pipelines/__init__.py index 7be559161..e89b33f8d 100644 --- a/moabb/pipelines/__init__.py +++ b/moabb/pipelines/__init__.py @@ -21,3 +21,12 @@ from .utils_deep_model import EEGNet, TCN_block except ModuleNotFoundError as err: print("Tensorflow not install, you could not use deep learning pipelines") + +try: + from .utils_pytorch import InputShapeSetterEEG, get_shape_from_baseconcat +except ModuleNotFoundError as err: + print( + "To use the get_shape_from_baseconcar and InputShapeSetterEEG, " + "you need to install `braindecode`." + "`pip install braindecode` or Please refer to `https://braindecode.org`." + ) From 4c03f282b1fe4e53d4796d5aadac249d180f61f8 Mon Sep 17 00:00:00 2001 From: bruAristimunha Date: Wed, 8 Mar 2023 04:36:38 +0100 Subject: [PATCH 24/34] Changing the tutorial --- examples/plot_BrainDecode.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/examples/plot_BrainDecode.py b/examples/plot_BrainDecode.py index d72e9f78a..056ffc212 100644 --- a/examples/plot_BrainDecode.py +++ b/examples/plot_BrainDecode.py @@ -14,7 +14,7 @@ import mne import torch from braindecode import EEGClassifier -from braindecode.models import ShallowFBCSPNet +from braindecode.models import EEGNetv4 from sklearn.pipeline import Pipeline from skorch.callbacks import EarlyStopping, EpochScoring from skorch.dataset import ValidSplit @@ -23,7 +23,11 @@ from moabb.datasets import BNCI2014001 from moabb.evaluations import CrossSessionEvaluation from moabb.paradigms import MotorImagery -from moabb.pipelines.utilis_pytorch import Transformer +from moabb.pipelines.utils_pytorch import ( + InputShapeSetterEEG, + Transformer, + get_shape_from_baseconcat, +) from moabb.utils import setup_seed @@ -92,11 +96,10 @@ # the second step is to define a skorch model using EEGClassifier from BrainDecode # that allow to convert the PyTorch model in a scikit-learn classifier. -model = ShallowFBCSPNet( +model = EEGNetv4( in_chans=X.shape[1], n_classes=len(events), input_window_samples=X.shape[2], - final_conv_length="auto", ) # Send model to GPU @@ -121,15 +124,20 @@ EpochScoring( scoring="accuracy", on_train=False, name="valid_acc", lower_is_better=False ), + InputShapeSetterEEG( + param_name_1="in_chans", + param_name_2="input_window_samples", + param_name_3="n_classes", + input_dim_fn=get_shape_from_baseconcat, + module_name="module", + ), ], verbose=1, # Not printing the results foe each epoch ) # Create the pipelines pipes = {} -pipes["ShallowFBCSPNet"] = Pipeline( - [("Braindecode_dataset", create_dataset), ("Net", clf)] -) +pipes["EEGNet"] = Pipeline([("Braindecode_dataset", create_dataset), ("Net", clf)]) ############################################################################## From 607c2956d5f2ed0d60af4c59b3392daef3c632c9 Mon Sep 17 00:00:00 2001 From: bruAristimunha Date: Wed, 8 Mar 2023 04:37:22 +0100 Subject: [PATCH 25/34] Renaming the file to lower --- examples/{plot_BrainDecode.py => plot_braindecode.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{plot_BrainDecode.py => plot_braindecode.py} (100%) diff --git a/examples/plot_BrainDecode.py b/examples/plot_braindecode.py similarity index 100% rename from examples/plot_BrainDecode.py rename to examples/plot_braindecode.py From a43cc6b2bbc4fbdd7a4ee896eb77e7cdb78adcb9 Mon Sep 17 00:00:00 2001 From: bruAristimunha Date: Thu, 9 Mar 2023 19:27:33 +0100 Subject: [PATCH 26/34] Updating the InputSetterEEG --- examples/plot_braindecode.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/plot_braindecode.py b/examples/plot_braindecode.py index 056ffc212..96c7e8cc2 100644 --- a/examples/plot_braindecode.py +++ b/examples/plot_braindecode.py @@ -125,9 +125,7 @@ scoring="accuracy", on_train=False, name="valid_acc", lower_is_better=False ), InputShapeSetterEEG( - param_name_1="in_chans", - param_name_2="input_window_samples", - param_name_3="n_classes", + params_list=["in_chans", "input_window_samples", "n_classes"], input_dim_fn=get_shape_from_baseconcat, module_name="module", ), From 7b5a238078aeb03017b48d7ad1e0d03eb2ee2294 Mon Sep 17 00:00:00 2001 From: bruAristimunha Date: Thu, 9 Mar 2023 19:30:59 +0100 Subject: [PATCH 27/34] Adding new function to get the models from braindecode, and forcing the reinitialize model if the shape change. --- moabb/pipelines/utils_pytorch.py | 48 +++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/moabb/pipelines/utils_pytorch.py b/moabb/pipelines/utils_pytorch.py index 53849ec6f..a3f138f00 100644 --- a/moabb/pipelines/utils_pytorch.py +++ b/moabb/pipelines/utils_pytorch.py @@ -1,7 +1,10 @@ +import inspect + from braindecode.datasets import BaseConcatDataset, create_from_X_y from numpy import unique from sklearn.base import BaseEstimator, TransformerMixin from skorch.callbacks import Callback +from torch.nn import Module class Transformer(BaseEstimator, TransformerMixin): @@ -43,6 +46,22 @@ def get_shape_from_baseconcat(X): return X.shape +def _find_model_from_braindecode(model_name): + # soft dependency on braindecode + model_list = [] + import braindecode.models as models + + for ds in inspect.getmembers(models, inspect.isclass): + if issubclass(ds[1], Module): + model_list.append(ds[1]) + + for model in model_list: + if model_name == model.__name__: + # return an instance of the found model not initialized + return model + raise ValueError(f"{model_name} not found in braindecode models") + + class InputShapeSetterEEG(Callback): """Sets the input dimension of the PyTorch module to the input dimension of the training data. @@ -54,9 +73,10 @@ class InputShapeSetterEEG(Callback): Parameters ---------- - param_name : str (default='input_dim') - The parameter name is the parameter your model uses to define the + params_list : list + The list of parameters that define the input dimension in its ``__init__`` method. + Usually the mandatory parameters from the model. input_dim_fn : callable, None (default=None) In case your ``X`` value is more complex and deriving the input dimension is not as easy as ``X.shape[-1]`` you can pass a callable @@ -68,16 +88,12 @@ class InputShapeSetterEEG(Callback): def __init__( self, - param_name_1="in_chans", - param_name_2="input_window_samples", - param_name_3="n_classes", + params_list=None, input_dim_fn=None, module_name="module", ): self.module_name = module_name - self.param_name_1 = param_name_1 - self.param_name_2 = param_name_2 - self.param_name_3 = param_name_3 + self.params_list = params_list self.input_dim_fn = input_dim_fn def get_input_dim(self, X): @@ -97,10 +113,6 @@ def on_train_begin(self, net, X, y, **kwargs): in_chans, input_window_samples = self.get_input_dim(X) n_classes = len(unique(y)) - param_name_1 = f"{self.module_name}__{self.param_name_1}" - param_name_2 = f"{self.module_name}__{self.param_name_2}" - param_name_3 = f"{self.module_name}__{self.param_name_3}" - if ( params["module"].in_chans == in_chans & params["module"].input_window_samples @@ -108,11 +120,13 @@ def on_train_begin(self, net, X, y, **kwargs): == n_classes ): return - kwargs = { - param_name_1: in_chans, - param_name_2: input_window_samples, - param_name_3: n_classes, + "input_window_samples": input_window_samples, + "n_classes": n_classes, + "in_chans": in_chans, } - net.set_params(**kwargs) + new_module = _find_model_from_braindecode(net.module.__class__.__name__) + module_initilized = new_module(**kwargs) + net.set_params(**{"module": module_initilized}) + net.initialize_module() From 27d5aa2d836ae54724da5d7aa8842f1f49e64efa Mon Sep 17 00:00:00 2001 From: bruAristimunha Date: Thu, 9 Mar 2023 19:49:39 +0100 Subject: [PATCH 28/34] Partially init the shallow model --- moabb/pipelines/utils_pytorch.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/moabb/pipelines/utils_pytorch.py b/moabb/pipelines/utils_pytorch.py index a3f138f00..baf8fd749 100644 --- a/moabb/pipelines/utils_pytorch.py +++ b/moabb/pipelines/utils_pytorch.py @@ -1,4 +1,5 @@ import inspect +from functools import partial from braindecode.datasets import BaseConcatDataset, create_from_X_y from numpy import unique @@ -58,6 +59,9 @@ def _find_model_from_braindecode(model_name): for model in model_list: if model_name == model.__name__: # return an instance of the found model not initialized + if model_name == "ShallowFBCSPNet": + model = partial(model, final_conv_length="auto") + return model raise ValueError(f"{model_name} not found in braindecode models") From 57af30337a9ec0764452eceaac8a1a142ea9b7f7 Mon Sep 17 00:00:00 2001 From: bruAristimunha Date: Thu, 9 Mar 2023 19:50:19 +0100 Subject: [PATCH 29/34] Changing to ShallowNet --- examples/plot_braindecode.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/plot_braindecode.py b/examples/plot_braindecode.py index 96c7e8cc2..eb4e8e44b 100644 --- a/examples/plot_braindecode.py +++ b/examples/plot_braindecode.py @@ -14,7 +14,7 @@ import mne import torch from braindecode import EEGClassifier -from braindecode.models import EEGNetv4 +from braindecode.models import ShallowFBCSPNet from sklearn.pipeline import Pipeline from skorch.callbacks import EarlyStopping, EpochScoring from skorch.dataset import ValidSplit @@ -96,10 +96,11 @@ # the second step is to define a skorch model using EEGClassifier from BrainDecode # that allow to convert the PyTorch model in a scikit-learn classifier. -model = EEGNetv4( +model = ShallowFBCSPNet( in_chans=X.shape[1], n_classes=len(events), input_window_samples=X.shape[2], + final_conv_length="auto", ) # Send model to GPU @@ -135,7 +136,7 @@ # Create the pipelines pipes = {} -pipes["EEGNet"] = Pipeline([("Braindecode_dataset", create_dataset), ("Net", clf)]) +pipes["ShallowNet"] = Pipeline([("Braindecode_dataset", create_dataset), ("Net", clf)]) ############################################################################## From 29e0b4c9185704688d218dfdb8386ef13a39e985 Mon Sep 17 00:00:00 2001 From: bruAristimunha Date: Mon, 13 Mar 2023 04:10:19 +0100 Subject: [PATCH 30/34] =) Solving the issue with hardcode variable and changing the way we compare the variable. --- moabb/pipelines/utils_pytorch.py | 42 +++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/moabb/pipelines/utils_pytorch.py b/moabb/pipelines/utils_pytorch.py index baf8fd749..ea7440333 100644 --- a/moabb/pipelines/utils_pytorch.py +++ b/moabb/pipelines/utils_pytorch.py @@ -1,5 +1,5 @@ -import inspect from functools import partial +from inspect import getmembers, isclass, isroutine from braindecode.datasets import BaseConcatDataset, create_from_X_y from numpy import unique @@ -42,7 +42,7 @@ def get_shape_from_baseconcat(X): if isinstance(X, BaseConcatDataset): in_channel = X[0][0].shape[0] input_window_samples = X[0][0].shape[1] - return in_channel, input_window_samples + return {"in_chans": in_channel, "input_window_samples": input_window_samples} else: return X.shape @@ -52,7 +52,7 @@ def _find_model_from_braindecode(model_name): model_list = [] import braindecode.models as models - for ds in inspect.getmembers(models, inspect.isclass): + for ds in getmembers(models, isclass): if issubclass(ds[1], Module): model_list.append(ds[1]) @@ -113,24 +113,32 @@ def get_input_dim(self, X): return X.shape[-1] def on_train_begin(self, net, X, y, **kwargs): + # Get the parameters of the neural network params = net.get_params() - in_chans, input_window_samples = self.get_input_dim(X) - n_classes = len(unique(y)) - - if ( - params["module"].in_chans - == in_chans & params["module"].input_window_samples - == input_window_samples & params["module"].n_classes - == n_classes + # Get the input dimensions from the BaseConcat dataset + params_get_from_dataset: dict = self.get_input_dim(X) + # Get the number of classes in the output labels + params_get_from_dataset["n_classes"] = len(unique(y)) + + # Get all the parameters of the neural network module + all_params_module = getmembers(params["module"], lambda x: not (isroutine(x))) + # Filter the parameters to only include the selected ones + selected_params_module = list( + filter(lambda x: x in self.params_list, all_params_module) + ) + + # Check if the selected parameters from the model already match the dataset parameters + if all( + self.params_get_from_dataset[name] == value + for name, value in selected_params_module ): return - kwargs = { - "input_window_samples": input_window_samples, - "n_classes": n_classes, - "in_chans": in_chans, - } + # Find the new module based on the current module's class name new_module = _find_model_from_braindecode(net.module.__class__.__name__) - module_initilized = new_module(**kwargs) + # Initialize the new module with the dataset parameters + module_initilized = new_module(**params_get_from_dataset) + # Set the neural network module to the new initialized module net.set_params(**{"module": module_initilized}) + # Initialize the new module net.initialize_module() From 5c4fa8853b217f384d744745a79fc9a00ef29f28 Mon Sep 17 00:00:00 2001 From: bruAristimunha Date: Mon, 13 Mar 2023 04:57:48 +0100 Subject: [PATCH 31/34] draft test --- moabb/tests/util_braindecode.py | 134 ++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 moabb/tests/util_braindecode.py diff --git a/moabb/tests/util_braindecode.py b/moabb/tests/util_braindecode.py new file mode 100644 index 000000000..dbaffb097 --- /dev/null +++ b/moabb/tests/util_braindecode.py @@ -0,0 +1,134 @@ +import numpy as np +import pytest +from braindecode.datasets import BaseConcatDataset, create_from_X_y +from mne import EpochsArray, create_info +from sklearn.preprocessing import LabelEncoder + +from moabb.datasets.fake import FakeDataset +from moabb.pipelines.utils_pytorch import Transformer +from tests import SimpleMotorImagery + + +@pytest.fixture(scope="module") +def data(): + """Return EEG data from dataset to test transformer""" + paradigm = SimpleMotorImagery() + dataset = FakeDataset(paradigm="imagery") + X, labels, metadata = paradigm.get_data(dataset, subjects=[1]) + y = LabelEncoder().fit_transform(labels) + return X, y, labels, metadata + + +class TestTransformer: + def test_transform_input_and_output_shape(self, data): + X, _, info = data + transformer = Transformer() + braindecode_dataset = transformer.fit_transform(X, y=None) + assert ( + len(braindecode_dataset) == X.shape[0] + and braindecode_dataset[0][0].shape[0] == X.shape[0] + and braindecode_dataset[0][0].shape[1] == X.shape[1] + ) + + def test_sklearn_is_fitted(self, data): + transformer = Transformer() + assert transformer.__sklearn_is_fitted__() + + def test_transform_with_missing_y_values(self, data): + # remove every other label to simulate missing labels + X, y, labels, metadata = data + transformer = Transformer() + with pytest.warns(UserWarning): + transformed_X = transformer.fit_transform(X, y=y) + assert ( + len(transformed_X.datasets) == 1 + and transformed_X.datasets[0][0].shape[0] == np.count_nonzero(y != -1) + and transformed_X.datasets[0][0].shape[1] == X.shape[1] + and transformed_X.datasets[0][0].shape[2] == X.shape[2] + ) + + def test_transformer_fit(self, data): + """Test whether transformer can fit to some training data""" + X_train, y_train, _, _ = data + transformer = Transformer() + assert transformer.fit(X_train, y_train) == transformer + + def test_transformer_transform_returns_dataset(self, data): + """Test whether the output of the transform method is a BaseConcatDataset""" + X_train, y_train, _, _ = data + transformer = Transformer() + dataset = transformer.fit(X_train, y_train).transform(X_train, y_train) + assert isinstance(dataset, BaseConcatDataset) + + def test_transformer_transform_contents(self, data): + """Test whether the contents and metadata of a transformed dataset are correct""" + X_train, y_train, _, _ = data + transformer = Transformer() + dataset = transformer.fit(X_train, y_train).transform(X_train, y_train) + assert len(dataset.datasets[0].windows.datasets[0]) == len(X_train) + # test the properties of one epoch - that they match the input MNE Epoch object + sample_epoch = dataset.datasets[0][0] + assert np.array_equal(sample_epoch.X, X_train.get_data()[0]) + assert np.array_equal(sample_epoch.y, y_train) + assert np.array_equal(sample_epoch.info, X_train.info) + + def test_sfreq_passed_through(self, data): + """Test if the sfreq parameter makes it through the transformer""" + sfreq = 128.0 + info = create_info(ch_names=["test"], sfreq=sfreq, ch_types=["eeg"]) + data = ( + np.random.normal(size=(2, 1, 10 * int(sfreq))) * 1e-6 + ) # create some noise data in a 10s window + epochs = EpochsArray(data, info=info) + y_train = np.array([0]) + transformer = Transformer() + dataset = transformer.fit(epochs, y_train).transform(epochs, y_train) + assert dataset.datasets[0].windows.datasets[0].sfreq == sfreq + + def test_transformer_transform_with_no_y(self, data): + """ + Test whether the transform method works when no y variable + is provided. This essentially tests that the y variable does + not affect the returned dataset. + """ + X_train, _, _, _ = data + transformer = Transformer() + dataset = transformer.fit(X_train).transform(X_train) + assert isinstance(dataset, BaseConcatDataset) + + def test_kw_args_initialization(self): + """Test initializing the transformer with kw_args""" + kw_args = {"sampling_rate": 128} + transformer = Transformer(kw_args=kw_args) + assert transformer.kw_args == kw_args + + def test_is_fitted_method(self): + """Test __sklearn_is_fitted__ returns True""" + transformer = Transformer() + is_fitter = transformer.__sklearn_is_fitted__() + assert is_fitter + + def test_assert_raises_value_error(self, data): + """Test that an invalid argument gives a ValueError""" + X_train, y_train, _, _ = data + transformer = Transformer() + invalid_param_name = "invalid" + with pytest.raises(ValueError): + transformer.fit(X_train, y=y_train, **{invalid_param_name: None}) + + def test_type_create_from_X_y_vs_transfomer(self, data): + """Test that the output of create_from_X_y() can be used as input""" + X_train, y_train, _, _ = data + + dataset = create_from_X_y( + X_train.get_data(), + y=y_train, + window_size_samples=X_train.get_data().shape[2], + window_stride_samples=X_train.get_data().shape[2], + drop_last_window=False, + sfreq=X_train.info["sfreq"], + ) + transformer = Transformer() + dataset_trans = transformer.fit(dataset).transform(dataset) + assert isinstance(dataset_trans, BaseConcatDataset) + assert type(dataset_trans) == type(dataset) From fb7b43f9f76e53e8f7bd783604876b645cbab75c Mon Sep 17 00:00:00 2001 From: bruAristimunha Date: Mon, 13 Mar 2023 05:00:21 +0100 Subject: [PATCH 32/34] removing test --- moabb/tests/util_braindecode.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/moabb/tests/util_braindecode.py b/moabb/tests/util_braindecode.py index dbaffb097..f7020fef4 100644 --- a/moabb/tests/util_braindecode.py +++ b/moabb/tests/util_braindecode.py @@ -6,7 +6,7 @@ from moabb.datasets.fake import FakeDataset from moabb.pipelines.utils_pytorch import Transformer -from tests import SimpleMotorImagery +from moabb.tests import SimpleMotorImagery @pytest.fixture(scope="module") @@ -34,19 +34,6 @@ def test_sklearn_is_fitted(self, data): transformer = Transformer() assert transformer.__sklearn_is_fitted__() - def test_transform_with_missing_y_values(self, data): - # remove every other label to simulate missing labels - X, y, labels, metadata = data - transformer = Transformer() - with pytest.warns(UserWarning): - transformed_X = transformer.fit_transform(X, y=y) - assert ( - len(transformed_X.datasets) == 1 - and transformed_X.datasets[0][0].shape[0] == np.count_nonzero(y != -1) - and transformed_X.datasets[0][0].shape[1] == X.shape[1] - and transformed_X.datasets[0][0].shape[2] == X.shape[2] - ) - def test_transformer_fit(self, data): """Test whether transformer can fit to some training data""" X_train, y_train, _, _ = data @@ -70,7 +57,6 @@ def test_transformer_transform_contents(self, data): sample_epoch = dataset.datasets[0][0] assert np.array_equal(sample_epoch.X, X_train.get_data()[0]) assert np.array_equal(sample_epoch.y, y_train) - assert np.array_equal(sample_epoch.info, X_train.info) def test_sfreq_passed_through(self, data): """Test if the sfreq parameter makes it through the transformer""" @@ -117,7 +103,7 @@ def test_assert_raises_value_error(self, data): transformer.fit(X_train, y=y_train, **{invalid_param_name: None}) def test_type_create_from_X_y_vs_transfomer(self, data): - """Test that the output of create_from_X_y() can be used as input""" + """Test the type from create_from_X_y() and the transfomer""" X_train, y_train, _, _ = data dataset = create_from_X_y( From 47c24545907ff8c8e268a208367252000cd57440 Mon Sep 17 00:00:00 2001 From: CARRARA Igor Date: Mon, 13 Mar 2023 09:27:02 +0100 Subject: [PATCH 33/34] Plot_function --- examples/plot_braindecode.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/examples/plot_braindecode.py b/examples/plot_braindecode.py index eb4e8e44b..5067b1039 100644 --- a/examples/plot_braindecode.py +++ b/examples/plot_braindecode.py @@ -14,7 +14,7 @@ import mne import torch from braindecode import EEGClassifier -from braindecode.models import ShallowFBCSPNet +from braindecode.models import EEGNetv4 from sklearn.pipeline import Pipeline from skorch.callbacks import EarlyStopping, EpochScoring from skorch.dataset import ValidSplit @@ -96,12 +96,15 @@ # the second step is to define a skorch model using EEGClassifier from BrainDecode # that allow to convert the PyTorch model in a scikit-learn classifier. -model = ShallowFBCSPNet( - in_chans=X.shape[1], - n_classes=len(events), - input_window_samples=X.shape[2], - final_conv_length="auto", -) +model = EEGNetv4(in_chans=X.shape[1], + n_classes=len(events), + input_window_samples=X.shape[2] + ) + +"""model = EEGNetv4(in_chans=1, + n_classes=2, + input_window_samples=100 + )""" # Send model to GPU if cuda: @@ -136,7 +139,7 @@ # Create the pipelines pipes = {} -pipes["ShallowNet"] = Pipeline([("Braindecode_dataset", create_dataset), ("Net", clf)]) +pipes["EEGNetV4"] = Pipeline([("Braindecode_dataset", create_dataset), ("Net", clf)]) ############################################################################## From 8171bad064a0fb2b81b295944a61d48499c28c20 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 08:27:21 +0000 Subject: [PATCH 34/34] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/plot_braindecode.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/plot_braindecode.py b/examples/plot_braindecode.py index 5067b1039..f1c5cd7df 100644 --- a/examples/plot_braindecode.py +++ b/examples/plot_braindecode.py @@ -96,10 +96,9 @@ # the second step is to define a skorch model using EEGClassifier from BrainDecode # that allow to convert the PyTorch model in a scikit-learn classifier. -model = EEGNetv4(in_chans=X.shape[1], - n_classes=len(events), - input_window_samples=X.shape[2] - ) +model = EEGNetv4( + in_chans=X.shape[1], n_classes=len(events), input_window_samples=X.shape[2] +) """model = EEGNetv4(in_chans=1, n_classes=2,