braindecode 0.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Braindecode-0.5.1.dist-info/LICENSE.txt +31 -0
- Braindecode-0.5.1.dist-info/METADATA +133 -0
- Braindecode-0.5.1.dist-info/RECORD +69 -0
- Braindecode-0.5.1.dist-info/WHEEL +5 -0
- Braindecode-0.5.1.dist-info/top_level.txt +2 -0
- braindecode/__init__.py +4 -0
- braindecode/classifier.py +258 -0
- braindecode/datasets/__init__.py +7 -0
- braindecode/datasets/base.py +293 -0
- braindecode/datasets/bbci.py +679 -0
- braindecode/datasets/moabb.py +110 -0
- braindecode/datasets/sleep_physionet.py +103 -0
- braindecode/datasets/tuh.py +230 -0
- braindecode/datautil/__init__.py +11 -0
- braindecode/datautil/mne.py +136 -0
- braindecode/datautil/preprocess.py +364 -0
- braindecode/datautil/serialization.py +111 -0
- braindecode/datautil/util.py +36 -0
- braindecode/datautil/windowers.py +435 -0
- braindecode/datautil/xy.py +77 -0
- braindecode/models/__init__.py +12 -0
- braindecode/models/deep4.py +252 -0
- braindecode/models/eegnet.py +339 -0
- braindecode/models/eegresnet.py +232 -0
- braindecode/models/functions.py +43 -0
- braindecode/models/hybrid.py +103 -0
- braindecode/models/modules.py +150 -0
- braindecode/models/shallow_fbcsp.py +158 -0
- braindecode/models/sleep_stager_chambon_2018.py +113 -0
- braindecode/models/tcn.py +143 -0
- braindecode/models/util.py +66 -0
- braindecode/regressor.py +240 -0
- braindecode/samplers/__init__.py +5 -0
- braindecode/samplers/base.py +78 -0
- braindecode/samplers/ssl.py +118 -0
- braindecode/training/__init__.py +8 -0
- braindecode/training/callbacks.py +21 -0
- braindecode/training/losses.py +17 -0
- braindecode/training/scoring.py +296 -0
- braindecode/util.py +362 -0
- braindecode/version.py +1 -0
- braindecode/visualization/__init__.py +5 -0
- braindecode/visualization/gradients.py +53 -0
- test/__init__.py +0 -0
- test/acceptance_tests/__init__.py +0 -0
- test/acceptance_tests/test_cropped_decoding.py +171 -0
- test/acceptance_tests/test_eeg_classifier.py +255 -0
- test/acceptance_tests/test_model_architectures.py +54 -0
- test/acceptance_tests/test_trialwise_decoding.py +168 -0
- test/unit_tests/__init__.py +0 -0
- test/unit_tests/datasets/__init__.py +0 -0
- test/unit_tests/datasets/test_dataset.py +227 -0
- test/unit_tests/datasets/test_mne.py +145 -0
- test/unit_tests/datasets/test_sleep_physionet.py +23 -0
- test/unit_tests/datasets/test_tuh.py +97 -0
- test/unit_tests/datasets/test_xy.py +31 -0
- test/unit_tests/datautil/__init__.py +0 -0
- test/unit_tests/datautil/test_preprocess.py +270 -0
- test/unit_tests/datautil/test_serialization.py +127 -0
- test/unit_tests/datautil/test_windowers.py +519 -0
- test/unit_tests/models/__init__.py +0 -0
- test/unit_tests/models/test_models.py +136 -0
- test/unit_tests/models/test_util.py +22 -0
- test/unit_tests/test_eeg_classifier.py +73 -0
- test/unit_tests/test_eeg_regressor.py +72 -0
- test/unit_tests/test_scoring.py +282 -0
- test/unit_tests/test_util.py +30 -0
- test/unit_tests/visualization/__init__.py +0 -0
- test/unit_tests/visualization/test_gradients.py +38 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dataset classes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Authors: Hubert Banville <hubert.jbanville@gmail.com>
|
|
6
|
+
# Lukas Gemein <l.gemein@gmail.com>
|
|
7
|
+
# Simon Brandt <simonbrandt@protonmail.com>
|
|
8
|
+
# David Sabbagh <dav.sabbagh@gmail.com>
|
|
9
|
+
# Robin Schirrmeister <robintibor@gmail.com>
|
|
10
|
+
#
|
|
11
|
+
# License: BSD (3-clause)
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import json
|
|
15
|
+
import warnings
|
|
16
|
+
from glob import glob
|
|
17
|
+
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
from torch.utils.data import Dataset, ConcatDataset
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _create_description(description):
|
|
24
|
+
if description is not None:
|
|
25
|
+
if (not isinstance(description, pd.Series)
|
|
26
|
+
and not isinstance(description, dict)):
|
|
27
|
+
raise ValueError(f"'{description}' has to be either a "
|
|
28
|
+
f"pandas.Series or a dict.")
|
|
29
|
+
if isinstance(description, dict):
|
|
30
|
+
description = pd.Series(description)
|
|
31
|
+
return description
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BaseDataset(Dataset):
|
|
35
|
+
"""Returns samples from an mne.io.Raw object along with a target.
|
|
36
|
+
|
|
37
|
+
Dataset which serves samples from an mne.io.Raw object along with a target.
|
|
38
|
+
The target is unique for the dataset, and is obtained through the
|
|
39
|
+
`description` attribute.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
raw: mne.io.Raw
|
|
44
|
+
Continuous data.
|
|
45
|
+
description: dict | pandas.Series | None
|
|
46
|
+
Holds additional description about the continuous signal / subject.
|
|
47
|
+
target_name: str | None
|
|
48
|
+
Name of the index in `description` that should be used to provide the
|
|
49
|
+
target (e.g., to be used in a prediction task later on).
|
|
50
|
+
transform : callable | None
|
|
51
|
+
On-the-fly transform applied to the example before it is returned.
|
|
52
|
+
"""
|
|
53
|
+
def __init__(self, raw, description=None, target_name=None,
|
|
54
|
+
transform=None):
|
|
55
|
+
self.raw = raw
|
|
56
|
+
self.description = _create_description(description)
|
|
57
|
+
self.transform = transform
|
|
58
|
+
|
|
59
|
+
# save target name for load/save later
|
|
60
|
+
self.target_name = target_name
|
|
61
|
+
if target_name is None:
|
|
62
|
+
self.target = None
|
|
63
|
+
elif target_name in self.description:
|
|
64
|
+
self.target = self.description[target_name]
|
|
65
|
+
else:
|
|
66
|
+
raise ValueError(f"'{target_name}' not in description.")
|
|
67
|
+
|
|
68
|
+
def __getitem__(self, index):
|
|
69
|
+
X, y = self.raw[:, index][0], self.target
|
|
70
|
+
if self.transform is not None:
|
|
71
|
+
X = self.transform(X)
|
|
72
|
+
return X, y
|
|
73
|
+
|
|
74
|
+
def __len__(self):
|
|
75
|
+
return len(self.raw)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def transform(self):
|
|
79
|
+
return self._transform
|
|
80
|
+
|
|
81
|
+
@transform.setter
|
|
82
|
+
def transform(self, value):
|
|
83
|
+
if value is not None and not callable(value):
|
|
84
|
+
raise ValueError('Transform needs to be a callable.')
|
|
85
|
+
self._transform = value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class WindowsDataset(BaseDataset):
|
|
89
|
+
"""Returns windows from an mne.Epochs object along with a target.
|
|
90
|
+
|
|
91
|
+
Dataset which serves windows from an mne.Epochs object along with their
|
|
92
|
+
target and additional information. The `metadata` attribute of the Epochs
|
|
93
|
+
object must contain a column called `target`, which will be used to return
|
|
94
|
+
the target that corresponds to a window. Additional columns
|
|
95
|
+
`i_window_in_trial`, `i_start_in_trial`, `i_stop_in_trial` are also
|
|
96
|
+
required to serve information about the windowing (e.g., useful for cropped
|
|
97
|
+
training).
|
|
98
|
+
See `braindecode.datautil.windowers` to directly create a `WindowsDataset`
|
|
99
|
+
from a `BaseDataset` object.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
windows : mne.Epochs
|
|
104
|
+
Windows obtained through the application of a windower to a BaseDataset
|
|
105
|
+
(see `braindecode.datautil.windowers`).
|
|
106
|
+
description : dict | pandas.Series | None
|
|
107
|
+
Holds additional info about the windows.
|
|
108
|
+
transform : callable | None
|
|
109
|
+
On-the-fly transform applied to a window before it is returned.
|
|
110
|
+
"""
|
|
111
|
+
def __init__(self, windows, description=None, transform=None):
|
|
112
|
+
self.windows = windows
|
|
113
|
+
self.description = _create_description(description)
|
|
114
|
+
self.transform = transform
|
|
115
|
+
|
|
116
|
+
self.y = self.windows.metadata.loc[:, 'target'].to_numpy()
|
|
117
|
+
self.crop_inds = self.windows.metadata.loc[
|
|
118
|
+
:, ['i_window_in_trial', 'i_start_in_trial',
|
|
119
|
+
'i_stop_in_trial']].to_numpy()
|
|
120
|
+
|
|
121
|
+
def __getitem__(self, index):
|
|
122
|
+
X = self.windows.get_data(item=index)[0].astype('float32')
|
|
123
|
+
if self.transform is not None:
|
|
124
|
+
X = self.transform(X)
|
|
125
|
+
y = self.y[index]
|
|
126
|
+
# necessary to cast as list to get list of three tensors from batch,
|
|
127
|
+
# otherwise get single 2d-tensor...
|
|
128
|
+
crop_inds = self.crop_inds[index].tolist()
|
|
129
|
+
return X, y, crop_inds
|
|
130
|
+
|
|
131
|
+
def __len__(self):
|
|
132
|
+
return len(self.windows.events)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def transform(self):
|
|
136
|
+
return self._transform
|
|
137
|
+
|
|
138
|
+
@transform.setter
|
|
139
|
+
def transform(self, value):
|
|
140
|
+
if value is not None and not callable(value):
|
|
141
|
+
raise ValueError('Transform needs to be a callable.')
|
|
142
|
+
self._transform = value
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class BaseConcatDataset(ConcatDataset):
|
|
146
|
+
"""A base class for concatenated datasets. Holds either mne.Raw or
|
|
147
|
+
mne.Epoch in self.datasets and has a pandas DataFrame with additional
|
|
148
|
+
description.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
list_of_ds: list
|
|
153
|
+
list of BaseDataset, BaseConcatDataset or WindowsDataset
|
|
154
|
+
"""
|
|
155
|
+
def __init__(self, list_of_ds):
|
|
156
|
+
# if we get a list of BaseConcatDataset, get all the individual datasets
|
|
157
|
+
if list_of_ds and isinstance(list_of_ds[0], BaseConcatDataset):
|
|
158
|
+
list_of_ds = [d for ds in list_of_ds for d in ds.datasets]
|
|
159
|
+
super().__init__(list_of_ds)
|
|
160
|
+
self.description = pd.DataFrame([ds.description for ds in list_of_ds])
|
|
161
|
+
self.description.reset_index(inplace=True, drop=True)
|
|
162
|
+
|
|
163
|
+
def split(self, by=None, property=None, split_ids=None):
|
|
164
|
+
"""Split the dataset based on information listed in its description
|
|
165
|
+
DataFrame or based on indices.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
by: str | list(int) | list(list(int))
|
|
170
|
+
If by is a string, splitting is performed based on the description
|
|
171
|
+
DataFrame column with this name.
|
|
172
|
+
If by is a (list of) list of integers, the position in the first
|
|
173
|
+
list corresponds to the split id and the integers to the
|
|
174
|
+
datapoints of that split.
|
|
175
|
+
property: str
|
|
176
|
+
Some property which is listed in info DataFrame.
|
|
177
|
+
split_ids: list(int) | list(list(int))
|
|
178
|
+
List of indices to be combined in a subset.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
splits: dict{str: BaseConcatDataset}
|
|
183
|
+
A dictionary with the name of the split (a string) as key and the
|
|
184
|
+
dataset as value.
|
|
185
|
+
"""
|
|
186
|
+
args_not_none = [
|
|
187
|
+
by is not None, property is not None, split_ids is not None]
|
|
188
|
+
if sum(args_not_none) != 1:
|
|
189
|
+
raise ValueError("Splitting requires exactly one argument.")
|
|
190
|
+
|
|
191
|
+
if property is not None or split_ids is not None:
|
|
192
|
+
warnings.warn("Keyword arguments `property` and `split_ids` "
|
|
193
|
+
"are deprecated and will be removed in the future. "
|
|
194
|
+
"Use `by` instead.")
|
|
195
|
+
by = property if property is not None else split_ids
|
|
196
|
+
if isinstance(by, str):
|
|
197
|
+
split_ids = {
|
|
198
|
+
k: list(v)
|
|
199
|
+
for k, v in self.description.groupby(by).groups.items()
|
|
200
|
+
}
|
|
201
|
+
else:
|
|
202
|
+
# assume list(int)
|
|
203
|
+
if not isinstance(by[0], list):
|
|
204
|
+
by = [by]
|
|
205
|
+
# assume list(list(int))
|
|
206
|
+
split_ids = {split_i: split for split_i, split in enumerate(by)}
|
|
207
|
+
|
|
208
|
+
return {str(split_name): BaseConcatDataset(
|
|
209
|
+
[self.datasets[ds_ind] for ds_ind in ds_inds])
|
|
210
|
+
for split_name, ds_inds in split_ids.items()}
|
|
211
|
+
|
|
212
|
+
def get_metadata(self):
|
|
213
|
+
"""Concatenate the metadata and description of the wrapped Epochs.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
pd.DataFrame:
|
|
218
|
+
DataFrame containing as many rows as there are windows in the
|
|
219
|
+
BaseConcatDataset, with the metadata and description information
|
|
220
|
+
for each window.
|
|
221
|
+
"""
|
|
222
|
+
if not all([isinstance(ds, WindowsDataset) for ds in self.datasets]):
|
|
223
|
+
raise TypeError('Metadata dataframe can only be computed when all '
|
|
224
|
+
'datasets are WindowsDataset.')
|
|
225
|
+
|
|
226
|
+
all_dfs = list()
|
|
227
|
+
for ds in self.datasets:
|
|
228
|
+
df = ds.windows.metadata
|
|
229
|
+
for k, v in ds.description.items():
|
|
230
|
+
df[k] = v
|
|
231
|
+
all_dfs.append(df)
|
|
232
|
+
|
|
233
|
+
return pd.concat(all_dfs)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def transform(self):
|
|
237
|
+
return [ds.transform for ds in self.datasets]
|
|
238
|
+
|
|
239
|
+
@transform.setter
|
|
240
|
+
def transform(self, value):
|
|
241
|
+
for i in range(len(self.datasets)):
|
|
242
|
+
self.datasets[i].transform = value
|
|
243
|
+
|
|
244
|
+
def save(self, path, overwrite=False):
|
|
245
|
+
"""Save dataset to files.
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
path: str
|
|
250
|
+
Directory to which .fif / -epo.fif and .json files are stored.
|
|
251
|
+
overwrite: bool
|
|
252
|
+
Whether to delete old files (.json, .fif, -epo.fif) in specified directory
|
|
253
|
+
prior to saving.
|
|
254
|
+
"""
|
|
255
|
+
assert len(self.datasets) > 0, "Expect at least one dataset"
|
|
256
|
+
assert (hasattr(self.datasets[0], 'raw') + hasattr(
|
|
257
|
+
self.datasets[0], 'windows') == 1), (
|
|
258
|
+
"dataset should have either raw or windows attribute")
|
|
259
|
+
files = ['target_name.json', 'description.json']
|
|
260
|
+
file_names_ = ["{}-raw.fif", "{}-epo.fif"]
|
|
261
|
+
description_file_name = os.path.join(path, 'description.json')
|
|
262
|
+
target_file_name = os.path.join(path, 'target_name.json')
|
|
263
|
+
if not overwrite:
|
|
264
|
+
if (os.path.exists(description_file_name) or
|
|
265
|
+
os.path.exists(target_file_name)):
|
|
266
|
+
raise FileExistsError(
|
|
267
|
+
f'{description_file_name} or {target_file_name} exist in {path}.')
|
|
268
|
+
else:
|
|
269
|
+
for file_name in file_names_:
|
|
270
|
+
file_names = glob(os.path.join(path, f"*{file_name.lstrip('{}')}"))
|
|
271
|
+
_ = [os.remove(f) for f in file_names]
|
|
272
|
+
if os.path.isfile(target_file_name):
|
|
273
|
+
os.remove(target_file_name)
|
|
274
|
+
if os.path.isfile(description_file_name):
|
|
275
|
+
os.remove(description_file_name)
|
|
276
|
+
|
|
277
|
+
concat_of_raws = hasattr(self.datasets[0], 'raw')
|
|
278
|
+
file_name = file_names_[0] if concat_of_raws else file_names_[1]
|
|
279
|
+
if concat_of_raws:
|
|
280
|
+
# for checks that all have same target name and for
|
|
281
|
+
# saving later
|
|
282
|
+
target_name = self.datasets[0].target_name
|
|
283
|
+
for i_ds, ds in enumerate(self.datasets):
|
|
284
|
+
full_file_path = os.path.join(path, file_name.format(i_ds))
|
|
285
|
+
if concat_of_raws:
|
|
286
|
+
ds.raw.save(full_file_path, overwrite=overwrite)
|
|
287
|
+
assert ds.target_name == target_name, "All datasets should have same target name"
|
|
288
|
+
else:
|
|
289
|
+
ds.windows.save(full_file_path, overwrite=overwrite)
|
|
290
|
+
|
|
291
|
+
if concat_of_raws:
|
|
292
|
+
json.dump({'target_name': target_name}, open(target_file_name, 'w'))
|
|
293
|
+
self.description.to_json(description_file_name)
|