Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BrainDecode Plot #340

Closed
wants to merge 41 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b935134
Fix bug for MotorImagery All events, setting n_classes
carraraig Feb 27, 2023
ecc0499
Merge branch 'develop' into develop
carraraig Feb 27, 2023
70d0539
Fix bug for MotorImagery All events, setting n_classes
carraraig Feb 27, 2023
989f38d
Merge remote-tracking branch 'origin/develop' into develop
carraraig Feb 27, 2023
a556c8a
Fix is Valid
carraraig Feb 27, 2023
ced2bda
Update docs/source/whats_new.rst
sylvchev Feb 28, 2023
8aeb748
Merge branch 'develop' into develop
sylvchev Mar 3, 2023
4ad6970
Merge branch 'NeuroTechX:develop' into develop
carraraig Mar 6, 2023
56fb305
plot_BrainDecode first draft
carraraig Mar 6, 2023
c39c67a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 6, 2023
52949fd
plot_BrainDecode
carraraig Mar 6, 2023
b5ce192
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 6, 2023
78a2c83
plot_BrainDecode
carraraig Mar 6, 2023
084af1f
Merge remote-tracking branch 'origin/develop' into develop
carraraig Mar 6, 2023
0baad7b
plot_BrainDecode
carraraig Mar 6, 2023
3b7d3a2
Update examples/plot_BrainDecode.py
carraraig Mar 6, 2023
188fe0c
Update examples/plot_BrainDecode.py
carraraig Mar 6, 2023
3ed2dcd
plot_BrainDecode
carraraig Mar 6, 2023
400fe41
plot_BrainDecode, definition of setup_seed in moabb.utils
carraraig Mar 6, 2023
0b1b1ff
plot_BrainDecode, definition of setup_seed in moabb.utils
carraraig Mar 6, 2023
fc6744b
brain decode try benchmark
carraraig Mar 7, 2023
d3482c6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 7, 2023
c1384b7
Plot_function
carraraig Mar 7, 2023
dede68c
Merge remote-tracking branch 'origin/develop' into develop
carraraig Mar 7, 2023
5e06156
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 7, 2023
1540dd4
Delete PyTorch_EEGNetv4.yml
carraraig Mar 7, 2023
fb2c66d
Delete plot_benchmark_BrainDecode.py
carraraig Mar 7, 2023
c642d78
Renaming the file
bruAristimunha Mar 8, 2023
67cfeb5
Changing init file
bruAristimunha Mar 8, 2023
4c03f28
Changing the tutorial
bruAristimunha Mar 8, 2023
607c295
Renaming the file to lower
bruAristimunha Mar 8, 2023
a43cc6b
Updating the InputSetterEEG
bruAristimunha Mar 9, 2023
7b5a238
Adding new function to get the models from braindecode, and forcing t…
bruAristimunha Mar 9, 2023
27d5aa2
Partially init the shallow model
bruAristimunha Mar 9, 2023
57af303
Changing to ShallowNet
bruAristimunha Mar 9, 2023
29e0b4c
=)
bruAristimunha Mar 13, 2023
5c4fa88
draft test
bruAristimunha Mar 13, 2023
fb7b43f
removing test
bruAristimunha Mar 13, 2023
2753caa
Merge branch 'develop' into develop
bruAristimunha Mar 13, 2023
47c2454
Plot_function
carraraig Mar 13, 2023
8171bad
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,4 @@ analysis/*

# mac os x stuff
*DS_store*
/examples/pipelines_BrainDecode/
1 change: 1 addition & 0 deletions docs/source/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Enhancements
- Adding new tutorial to benchmark with GridSearchCV (:gh:`323` by `Igor Carrara`_)
- Add six deep learning models (Tensorflow), and build a tutorial to show to use 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`_)
- Add Google Analytics to the documentation (:gh:`335` by `Bruno Aristimunha`_)

Bugs
Expand Down
166 changes: 166 additions & 0 deletions examples/plot_braindecode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""
=============================================================
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 <igor.carrara@inria.fr>
# Bruno Aristimunha <b.aristimunha@gmail.com>
#
# License: BSD (3-clause)

import matplotlib.pyplot as plt
import mne
import torch
from braindecode import EEGClassifier
from braindecode.models import EEGNetv4
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.utils_pytorch import (
InputShapeSetterEEG,
Transformer,
get_shape_from_baseconcat,
)
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.

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:
model.cuda()

# Define a Skorch classifier
clf = EEGClassifier(
module=model,
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
),
InputShapeSetterEEG(
params_list=["in_chans", "input_window_samples", "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["EEGNetV4"] = Pipeline([("Braindecode_dataset", create_dataset), ("Net", clf)])


##############################################################################
# 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()
9 changes: 9 additions & 0 deletions moabb/pipelines/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`."
)
144 changes: 144 additions & 0 deletions moabb/pipelines/utils_pytorch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from functools import partial
from inspect import getmembers, isclass, isroutine

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):
"""
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_chans": in_channel, "input_window_samples": input_window_samples}
else:
return X.shape


def _find_model_from_braindecode(model_name):
# soft dependency on braindecode
model_list = []
import braindecode.models as models

for ds in getmembers(models, 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
if model_name == "ShallowFBCSPNet":
model = partial(model, final_conv_length="auto")

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.
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
----------
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
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,
params_list=None,
input_dim_fn=None,
module_name="module",
):
self.module_name = module_name
self.params_list = params_list
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):
# Get the parameters of the neural network
params = net.get_params()
# 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

# Find the new module based on the current module's class name
new_module = _find_model_from_braindecode(net.module.__class__.__name__)
# 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()
Loading