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

Benchmark function #264

Merged
merged 39 commits into from
Jan 2, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
5852b40
benchmark init commit
Div12345 Feb 9, 2022
1e82974
minor doc string corr
Div12345 Feb 9, 2022
d583119
fixing a missing line in download.py
Div12345 Feb 9, 2022
3051a85
Update benchmark.py
Div12345 Feb 9, 2022
5a1dc44
Update utils.py
Div12345 Feb 9, 2022
aaf6e3b
adding a print statement for visibility in output
Div12345 Feb 9, 2022
abed66d
remove verbose, debug params; add evaluations save folder params
Div12345 Feb 9, 2022
4df5351
update run.py to use benchmark function
Div12345 Feb 9, 2022
d320910
correct analyze statement
Div12345 Feb 9, 2022
db01510
including, excluding datasets functionality
Div12345 Mar 6, 2022
f061c58
some descript edits
Div12345 Mar 6, 2022
3bbb028
Update benchmark.py
Div12345 Mar 6, 2022
faf0925
correct include datasets and return dataframe
Mar 9, 2022
ee835ff
correct n_jobs in run.py
Mar 9, 2022
0a161a7
Apply suggestions from code review
Div12345 Apr 20, 2022
0860dc8
whatsnew and remove MI result combo
Div12345 Nov 14, 2022
4fcd322
Merge branch 'develop' into benchmark
Div12345 Nov 14, 2022
94ee23f
Adding select_paradigms
Div12345 Nov 14, 2022
1c2f134
rectify select_paradigms implementation
Div12345 Nov 14, 2022
d72bdd7
Merge branch 'develop' into benchmark
sylvchev Dec 31, 2022
5415bb5
fix error when select_paradigms is None
Dec 31, 2022
b97c5ab
add benchmark to __init__ for direct call
Dec 31, 2022
c31903e
fix overwriting results for different paradigms, add printing results
Jan 1, 2023
2b060a9
unittest for benchmark
Jan 1, 2023
1de3dc4
fix docstring and error msg, remove unused code
Jan 1, 2023
c9f8dae
remove unwanted print
Jan 1, 2023
556d6f5
fix warning for dataframe
Jan 1, 2023
b59e9ea
abstractproperty is deprecated since 3.3
Jan 1, 2023
0e10c2f
fix doc typo, add is_valid to fake paradigms
Jan 1, 2023
2083c8d
update whatsnew
Jan 1, 2023
304396a
add an example for benchmark
Jan 2, 2023
a4512ce
update readme, correct typos, add link to wiki
Jan 2, 2023
ddd854e
fix typos, introduce example
Jan 2, 2023
e3f43d0
add link to wiki in dataset documentation
Jan 2, 2023
2494618
fix typos, rename force to overwrite, select_paradigms to paradigms
Jan 2, 2023
0ae71e4
fix refactoring typo
Jan 2, 2023
b4bdd46
fix refactoring typo 2
Jan 2, 2023
6611801
fix refactoring typo 3
Jan 2, 2023
9db00e1
fix example typo
Jan 2, 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
2 changes: 1 addition & 1 deletion docs/source/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Develop branch
Enhancements
~~~~~~~~~~~~

- None
- Adding a comprehensive benchmarking function (:gh:`264` by `Divyesh Narayanan`_)

Bugs
~~~~
Expand Down
2 changes: 1 addition & 1 deletion moabb/analysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def analyze(results, out_path, name="analysis", plot=False):

Given a results dataframe, generates a folder with
results and a dataframe of the exact data used to generate those results,
aswell as introspection to return information on the computer
as well as introspection to return information on the computer

parameters
----------
Expand Down
200 changes: 200 additions & 0 deletions moabb/benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import logging

import mne
import pandas as pd
import yaml

from moabb import paradigms as moabb_paradigms
from moabb.analysis import analyze
from moabb.evaluations import (
CrossSessionEvaluation,
CrossSubjectEvaluation,
WithinSessionEvaluation,
)
from moabb.pipelines.utils import generate_paradigms, parse_pipelines_from_directory


log = logging.getLogger(__name__)


def benchmark(
pipelines="./pipelines/",
evaluations=None,
select_paradigms=None,
results="./results/",
force=False,
output="./",
n_jobs=-1,
plot=False,
contexts=None,
include_datasets=None,
exclude_datasets=None,
):
"""Run benchmarks for selected pipelines and datasets

Load from saved pipeline configurations to determine associated paradigms. It is
possible to include or exclude specific datasets and to choose the type of
evaluation.

If particular paradigms are mentioned through select_paradigms, only the pipelines corresponding to those paradigms
will be run

To define the include_datasets or exclude_dataset, you could start from the full dataset list,
using for example the following code:
> # Choose your paradigm
> p = moabb.paradigms.SSVEP()
> # Get the class names
> print(p.datasets)
> # Get the dataset code
> print([d.code for d in p.datasets])

Parameters
----------
pipelines: str
Folder containing the pipelines to evaluate
evaluations: list of str
If to restrict the types of evaluations to be run. By default all 3 base types are run
Can be a list of these elements ["WithinSession", "CrossSession", "CrossSubject"]
select_paradigms: list of str
To restrict the paradigms on which evaluations should be run.
Can be a list of these elements ['LeftRightImagery', 'MotorImagery', 'FilterBankSSVEP', 'SSVEP',
'FilterBankMotorImagery']
results: str
Folder to store the results
force: bool
Force evaluation of cached pipelines
output: str
Folder to store the analysis results
n_jobs: int
Number of threads to use for running parallel jobs
plot: bool
Plot results after computing
contexts: str
File path to context.yml file that describes context parameters.
If none, assumes all defaults. Must contain an entry for all
paradigms described in the pipelines
include_datasets: list of str or Dataset object
Datasets to include in the benchmark run. By default all suitable datasets are taken.
If arguments are given for both include_datasets as well as exclude_datasets,
include_datasets will take precedence and exclude_datasets will be neglected.
exclude_datasets: list of str or Dataset object
Datasets to exclude from the benchmark run.

Returns
-------

"""
# set logs
if evaluations is None:
evaluations = ["WithinSession", "CrossSession", "CrossSubject"]

eval_type = {
"WithinSession": WithinSessionEvaluation,
"CrossSession": CrossSessionEvaluation,
"CrossSubject": CrossSubjectEvaluation,
}

mne.set_log_level(False)
# logging.basicConfig(level=logging.WARNING)

pipeline_configs = parse_pipelines_from_directory(pipelines)

context_params = {}
if contexts is not None:
with open(contexts, "r") as cfile:
context_params = yaml.load(cfile.read(), Loader=yaml.FullLoader)

paradigms = generate_paradigms(pipeline_configs, context_params, log)
print(paradigms)
if select_paradigms is not None:
paradigms = {p: paradigms[p] for p in select_paradigms}

log.debug(f"The paradigms being run are {paradigms}")

if len(context_params) == 0:
for paradigm in paradigms:
context_params[paradigm] = {}

# Looping over the evaluations to be done
df_eval = []
for evaluation in evaluations:
eval_results = dict()
for paradigm in paradigms:
# get the context
log.debug(f"{paradigm}: {context_params[paradigm]}")
p = getattr(moabb_paradigms, paradigm)(**context_params[paradigm])
# List of dataset class instances
datasets = p.datasets
d = _inc_exc_datasets(datasets, include_datasets, exclude_datasets)
log.debug(
f"Datasets considered for {paradigm} paradigm {[dt.code for dt in d]}"
)
print(f"Datasets considered for {paradigm} paradigm {[dt.code for dt in d]}")

# if len(d) = 0, raise warning that no suitable datasets were present after the
# arguments were satisfied
if len(d) == 0:
log.debug("No datasets matched the include_datasets or exclude_datasets")
print("No datasets matched the include_datasets or exclude_datasets")

context = eval_type[evaluation](
paradigm=p,
datasets=d,
random_state=42,
hdf5_path=results,
n_jobs=n_jobs,
overwrite=force,
)
paradigm_results = context.process(pipelines=paradigms[paradigm])
eval_results[f"{paradigm}"] = paradigm_results
paradigm_results["paradigm"] = f"{paradigm}"
paradigm_results["evaluation"] = f"{evaluation}"
df_eval.append(paradigm_results)

# Combining the FilterBank and the base Paradigm
combine_paradigms = ["SSVEP"]
for p in combine_paradigms:
if f"FilterBank{p}" in eval_results.keys() and f"{p}" in eval_results.keys():
eval_results[f"{p}"] = pd.concat(
[eval_results[f"{p}"], eval_results[f"FilterBank{p}"]]
)
del eval_results[f"FilterBank{p}"]

for paradigm_result in eval_results.values():
analyze(paradigm_result, output, plot=plot)

return pd.concat(df_eval)


def _inc_exc_datasets(datasets, include_datasets, exclude_datasets):
d = list()
if include_datasets is not None:
# Assert if the inputs are key_codes
if isinstance(include_datasets[0], str):
# Map from key_codes to class instances
datasets_codes = [d.code for d in datasets]
# Get the indices of the matching datasets
for incdat in include_datasets:
if incdat in datasets_codes:
d.append(datasets[datasets_codes.index(incdat)])
else:
# The case where the class instances have been given
# can be passed on directly
d = include_datasets
if exclude_datasets is not None:
raise AttributeError("You could specify both include and exclude datasets")

elif exclude_datasets is not None:
d = datasets
# Assert if the inputs are not key_codes i.e expected to be dataset class objects
if not isinstance(exclude_datasets[0], str):
# Convert the input to key_codes
exclude_datasets = [e.code for e in exclude_datasets]

# Map from key_codes to class instances
datasets_codes = [d.code for d in datasets]
for excdat in exclude_datasets:
del d[datasets_codes.index(excdat)]
else:
d = datasets
return d
1 change: 1 addition & 0 deletions moabb/datasets/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ def fs_get_file_list(article_id, version=None):
return response
else:
url = fsurl + "/articles/{}/versions/{}".format(article_id, version)
headers = {"Content-Type": "application/json"}
request = fs_issue_request("GET", url, headers=headers)
return request["files"]

Expand Down
123 changes: 123 additions & 0 deletions moabb/pipelines/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import importlib
import logging
import os
from collections import OrderedDict
from copy import deepcopy
from glob import glob

import numpy as np
import scipy.signal as scp
import yaml
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import make_pipeline

from moabb.analysis.results import get_string_rep


log = logging.getLogger(__name__)


def create_pipeline_from_config(config):
"""Create a pipeline from a config file.
Expand Down Expand Up @@ -39,6 +50,118 @@ def create_pipeline_from_config(config):
return pipeline


def parse_pipelines_from_directory(dir_path):
"""
Takes in the path to a directory with pipeline configuration files and returns a dictionary
of pipelines.
Parameters
----------
dir_path: str
Path to directory containing pipeline config .yml or .py files

Returns
-------
pipeline_configs: dict
Generated pipeline config dictionaries. Each entry has structure:
'name': string
'pipeline': sklearn.BaseEstimator
'paradigms': list of class names that are compatible with said pipeline
"""
assert os.path.isdir(
os.path.abspath(dir_path)
), "Given pipeline path {} is not valid".format(dir_path)

# get list of config files
yaml_files = glob(os.path.join(dir_path, "*.yml"))

pipeline_configs = []
for yaml_file in yaml_files:
with open(yaml_file, "r") as _file:
content = _file.read()

# load config
config_dict = yaml.load(content, Loader=yaml.FullLoader)
ppl = create_pipeline_from_config(config_dict["pipeline"])
pipeline_configs.append(
{
"paradigms": config_dict["paradigms"],
"pipeline": ppl,
"name": config_dict["name"],
}
)

# we can do the same for python defined pipeline
python_files = glob(os.path.join(dir_path, "*.py"))

for python_file in python_files:
spec = importlib.util.spec_from_file_location("custom", python_file)
foo = importlib.util.module_from_spec(spec)
spec.loader.exec_module(foo)

pipeline_configs.append(foo.PIPELINE)
return pipeline_configs


def generate_paradigms(pipeline_configs, context=None, logger=log):
"""
Takes in a dictionary of pipelines configurations as returned by
parse_pipelines_from_directory and returns a dictionary of unique paradigms with all pipeline
configurations compatible with that paradigm.
Parameters
----------
pipeline_configs:
dictionary of pipeline configurations
context:
TODO:add description
logger:
logger

Returns
-------
paradigms: dict
Dictionary of dictionaries with the unique paradigms and the configuration of the
pipelines compatible with the paradigm

"""
context = context or {}
paradigms = OrderedDict()
for config in pipeline_configs:

if "paradigms" not in config.keys():
logger.error("{} must have a 'paradigms' key.".format(config))
continue

# iterate over paradigms

for paradigm in config["paradigms"]:

# check if it is in the context parameters file
if len(context) > 0:
if paradigm not in context.keys():
logger.debug(context)
logger.warning(
"Paradigm {} not in context file {}".format(
paradigm, context.keys()
)
)

if isinstance(config["pipeline"], BaseEstimator):
pipeline = deepcopy(config["pipeline"])
else:
logger.error(config["pipeline"])
raise (ValueError("pipeline must be a sklearn estimator"))

# append the pipeline in the paradigm list
if paradigm not in paradigms.keys():
paradigms[paradigm] = {}

# FIXME name are not unique
logger.debug("Pipeline: \n\n {} \n".format(get_string_rep(pipeline)))
paradigms[paradigm][config["name"]] = pipeline

return paradigms


class FilterBank(BaseEstimator, TransformerMixin):
"""Apply a given indentical pipeline over a bank of filter.

Expand Down
Loading