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
braindecode/util.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# Authors: Robin Schirrmeister <robintibor@gmail.com>
|
|
2
|
+
#
|
|
3
|
+
# License: BSD (3-clause)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import glob
|
|
8
|
+
import random
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import mne
|
|
12
|
+
import h5py
|
|
13
|
+
import torch
|
|
14
|
+
from sklearn.utils import check_random_state
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def set_random_seeds(seed, cuda):
|
|
18
|
+
"""Set seeds for python random module numpy.random and torch.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
seed: int
|
|
23
|
+
Random seed.
|
|
24
|
+
cuda: bool
|
|
25
|
+
Whether to set cuda seed with torch.
|
|
26
|
+
"""
|
|
27
|
+
random.seed(seed)
|
|
28
|
+
torch.manual_seed(seed)
|
|
29
|
+
if cuda:
|
|
30
|
+
torch.cuda.manual_seed_all(seed)
|
|
31
|
+
np.random.seed(seed)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def np_to_var(
|
|
35
|
+
X, requires_grad=False, dtype=None, pin_memory=False, **tensor_kwargs
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Convenience function to transform numpy array to `torch.Tensor`.
|
|
39
|
+
|
|
40
|
+
Converts `X` to ndarray using asarray if necessary.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
X: ndarray or list or number
|
|
45
|
+
Input arrays
|
|
46
|
+
requires_grad: bool
|
|
47
|
+
passed on to Variable constructor
|
|
48
|
+
dtype: numpy dtype, optional
|
|
49
|
+
var_kwargs:
|
|
50
|
+
passed on to Variable constructor
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
var: `torch.Tensor`
|
|
55
|
+
"""
|
|
56
|
+
if not hasattr(X, "__len__"):
|
|
57
|
+
X = [X]
|
|
58
|
+
X = np.asarray(X)
|
|
59
|
+
if dtype is not None:
|
|
60
|
+
X = X.astype(dtype)
|
|
61
|
+
X_tensor = torch.tensor(X, requires_grad=requires_grad, **tensor_kwargs)
|
|
62
|
+
if pin_memory:
|
|
63
|
+
X_tensor = X_tensor.pin_memory()
|
|
64
|
+
return X_tensor
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def var_to_np(var):
|
|
68
|
+
"""Convenience function to transform `torch.Tensor` to numpy
|
|
69
|
+
array.
|
|
70
|
+
|
|
71
|
+
Should work both for CPU and GPU."""
|
|
72
|
+
return var.cpu().data.numpy()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def corr(a, b):
|
|
76
|
+
"""
|
|
77
|
+
Computes correlation only between terms of a and terms of b, not within
|
|
78
|
+
a and b.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
a, b: 2darray, features x samples
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
Correlation between features in x and features in y
|
|
87
|
+
"""
|
|
88
|
+
# Difference to numpy:
|
|
89
|
+
# Correlation only between terms of x and y
|
|
90
|
+
# not between x and x or y and y
|
|
91
|
+
this_cov = cov(a, b)
|
|
92
|
+
return _cov_to_corr(this_cov, a, b)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cov(a, b):
|
|
96
|
+
"""
|
|
97
|
+
Computes covariance only between terms of a and terms of b, not within
|
|
98
|
+
a and b.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
a, b: 2darray, features x samples
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
Covariance between features in x and features in y
|
|
107
|
+
"""
|
|
108
|
+
demeaned_a = a - np.mean(a, axis=1, keepdims=True)
|
|
109
|
+
demeaned_b = b - np.mean(b, axis=1, keepdims=True)
|
|
110
|
+
this_cov = np.dot(demeaned_a, demeaned_b.T) / (b.shape[1] - 1)
|
|
111
|
+
return this_cov
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _cov_to_corr(this_cov, a, b):
|
|
115
|
+
# computing "unbiased" corr
|
|
116
|
+
# ddof=1 for unbiased..
|
|
117
|
+
var_a = np.var(a, axis=1, ddof=1)
|
|
118
|
+
var_b = np.var(b, axis=1, ddof=1)
|
|
119
|
+
return _cov_and_var_to_corr(this_cov, var_a, var_b)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _cov_and_var_to_corr(this_cov, var_a, var_b):
|
|
123
|
+
divisor = np.outer(np.sqrt(var_a), np.sqrt(var_b))
|
|
124
|
+
return this_cov / divisor
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def wrap_reshape_apply_fn(stat_fn, a, b, axis_a, axis_b):
|
|
128
|
+
"""
|
|
129
|
+
Reshape two nd-arrays into 2d-arrays, apply function and reshape
|
|
130
|
+
result back.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
stat_fn: function
|
|
135
|
+
Function to apply to 2d-arrays
|
|
136
|
+
a: nd-array: nd-array
|
|
137
|
+
b: nd-array
|
|
138
|
+
axis_a: int or list of int
|
|
139
|
+
sample axis
|
|
140
|
+
axis_b: int or list of int
|
|
141
|
+
sample axis
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
result: nd-array
|
|
146
|
+
The result reshaped to remaining_dims_a + remaining_dims_b
|
|
147
|
+
"""
|
|
148
|
+
if not hasattr(axis_a, "__len__"):
|
|
149
|
+
axis_a = [axis_a]
|
|
150
|
+
if not hasattr(axis_b, "__len__"):
|
|
151
|
+
axis_b = [axis_b]
|
|
152
|
+
other_axis_a = [i for i in range(a.ndim) if i not in axis_a]
|
|
153
|
+
other_axis_b = [i for i in range(b.ndim) if i not in axis_b]
|
|
154
|
+
transposed_topo_a = a.transpose(tuple(other_axis_a) + tuple(axis_a))
|
|
155
|
+
n_stat_axis_a = [a.shape[i] for i in axis_a]
|
|
156
|
+
n_other_axis_a = [a.shape[i] for i in other_axis_a]
|
|
157
|
+
flat_topo_a = transposed_topo_a.reshape(
|
|
158
|
+
np.prod(n_other_axis_a), np.prod(n_stat_axis_a)
|
|
159
|
+
)
|
|
160
|
+
transposed_topo_b = b.transpose(tuple(other_axis_b) + tuple(axis_b))
|
|
161
|
+
n_stat_axis_b = [b.shape[i] for i in axis_b]
|
|
162
|
+
n_other_axis_b = [b.shape[i] for i in other_axis_b]
|
|
163
|
+
flat_topo_b = transposed_topo_b.reshape(
|
|
164
|
+
np.prod(n_other_axis_b), np.prod(n_stat_axis_b)
|
|
165
|
+
)
|
|
166
|
+
assert np.array_equal(n_stat_axis_a, n_stat_axis_b)
|
|
167
|
+
stat_result = stat_fn(flat_topo_a, flat_topo_b)
|
|
168
|
+
topo_result = stat_result.reshape(
|
|
169
|
+
tuple(n_other_axis_a) + tuple(n_other_axis_b)
|
|
170
|
+
)
|
|
171
|
+
return topo_result
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_balanced_batches(
|
|
175
|
+
n_trials, rng, shuffle, n_batches=None, batch_size=None
|
|
176
|
+
):
|
|
177
|
+
"""Create indices for batches balanced in size
|
|
178
|
+
(batches will have maximum size difference of 1).
|
|
179
|
+
Supply either batch size or number of batches. Resulting batches
|
|
180
|
+
will not have the given batch size but rather the next largest batch size
|
|
181
|
+
that allows to split the set into balanced batches (maximum size difference 1).
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
n_trials : int
|
|
186
|
+
Size of set.
|
|
187
|
+
rng : RandomState
|
|
188
|
+
shuffle : bool
|
|
189
|
+
Whether to shuffle indices before splitting set.
|
|
190
|
+
n_batches : int, optional
|
|
191
|
+
batch_size : int, optional
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
batches: list of list of int
|
|
196
|
+
Indices for each batch.
|
|
197
|
+
"""
|
|
198
|
+
assert batch_size is not None or n_batches is not None
|
|
199
|
+
if n_batches is None:
|
|
200
|
+
n_batches = int(np.round(n_trials / float(batch_size)))
|
|
201
|
+
|
|
202
|
+
if n_batches > 0:
|
|
203
|
+
min_batch_size = n_trials // n_batches
|
|
204
|
+
n_batches_with_extra_trial = n_trials % n_batches
|
|
205
|
+
else:
|
|
206
|
+
n_batches = 1
|
|
207
|
+
min_batch_size = n_trials
|
|
208
|
+
n_batches_with_extra_trial = 0
|
|
209
|
+
assert n_batches_with_extra_trial < n_batches
|
|
210
|
+
all_inds = np.array(range(n_trials))
|
|
211
|
+
if shuffle:
|
|
212
|
+
rng.shuffle(all_inds)
|
|
213
|
+
i_start_trial = 0
|
|
214
|
+
i_stop_trial = 0
|
|
215
|
+
batches = []
|
|
216
|
+
for i_batch in range(n_batches):
|
|
217
|
+
i_stop_trial += min_batch_size
|
|
218
|
+
if i_batch < n_batches_with_extra_trial:
|
|
219
|
+
i_stop_trial += 1
|
|
220
|
+
batch_inds = all_inds[range(i_start_trial, i_stop_trial)]
|
|
221
|
+
batches.append(batch_inds)
|
|
222
|
+
i_start_trial = i_stop_trial
|
|
223
|
+
assert i_start_trial == n_trials
|
|
224
|
+
return batches
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def create_mne_dummy_raw(n_channels, n_times, sfreq, include_anns=True,
|
|
228
|
+
description=None, savedir=None, save_format='fif',
|
|
229
|
+
overwrite=True, random_state=None):
|
|
230
|
+
"""Create an mne.io.RawArray with fake data, and optionally save it.
|
|
231
|
+
|
|
232
|
+
This will overwrite already existing files.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
n_channels : int
|
|
237
|
+
Number of channels.
|
|
238
|
+
n_times : int
|
|
239
|
+
Number of samples.
|
|
240
|
+
sfreq : float
|
|
241
|
+
Sampling frequency.
|
|
242
|
+
include_anns : bool
|
|
243
|
+
If True, also create annotations.
|
|
244
|
+
description : list | None
|
|
245
|
+
List of descriptions used for creating annotations. It should contain
|
|
246
|
+
10 elements.
|
|
247
|
+
savedir : str | None
|
|
248
|
+
If provided as a string, the file will be saved under that directory.
|
|
249
|
+
save_format : str | list
|
|
250
|
+
If `savedir` is provided, this specifies the file format the data should
|
|
251
|
+
be saved to. Can be 'raw' or 'hdf5', or a list containing both.
|
|
252
|
+
random_state : int | RandomState
|
|
253
|
+
Random state for the generation of random data.
|
|
254
|
+
|
|
255
|
+
Returns
|
|
256
|
+
-------
|
|
257
|
+
raw : mne.io.Raw
|
|
258
|
+
The created Raw object.
|
|
259
|
+
save_fname : dict | None
|
|
260
|
+
Dictionary containing the name the raw data was saved to.
|
|
261
|
+
"""
|
|
262
|
+
random_state = check_random_state(random_state)
|
|
263
|
+
data = random_state.rand(n_channels, n_times)
|
|
264
|
+
ch_names = [f'ch{i}' for i in range(n_channels)]
|
|
265
|
+
ch_types = ['eeg'] * n_channels
|
|
266
|
+
info = mne.create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)
|
|
267
|
+
|
|
268
|
+
raw = mne.io.RawArray(data, info)
|
|
269
|
+
|
|
270
|
+
if include_anns:
|
|
271
|
+
n_anns = 10
|
|
272
|
+
inds = np.linspace(
|
|
273
|
+
int(sfreq * 2), int(n_times - sfreq * 2), num=n_anns).astype(int)
|
|
274
|
+
onset = raw.times[inds]
|
|
275
|
+
duration = [1] * n_anns
|
|
276
|
+
if description is None:
|
|
277
|
+
description = ['test'] * n_anns
|
|
278
|
+
anns = mne.Annotations(onset, duration, description)
|
|
279
|
+
raw = raw.set_annotations(anns)
|
|
280
|
+
|
|
281
|
+
save_fname = dict()
|
|
282
|
+
if savedir is not None:
|
|
283
|
+
if not isinstance(save_format, list):
|
|
284
|
+
save_format = [save_format]
|
|
285
|
+
fname = os.path.join(savedir, 'fake_eeg_raw')
|
|
286
|
+
|
|
287
|
+
if 'fif' in save_format:
|
|
288
|
+
fif_fname = fname + '.fif'
|
|
289
|
+
raw.save(fif_fname, overwrite=overwrite)
|
|
290
|
+
save_fname['fif'] = fif_fname
|
|
291
|
+
if 'hdf5' in save_format:
|
|
292
|
+
h5_fname = fname + '.h5'
|
|
293
|
+
with h5py.File(h5_fname, 'w') as f:
|
|
294
|
+
f.create_dataset(
|
|
295
|
+
'fake_raw', dtype='f8', data=raw.get_data())
|
|
296
|
+
save_fname['hdf5'] = h5_fname
|
|
297
|
+
|
|
298
|
+
return raw, save_fname
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class ThrowAwayIndexLoader(object):
|
|
302
|
+
def __init__(self, net, loader, is_regression):
|
|
303
|
+
self.net = net
|
|
304
|
+
self.loader = loader
|
|
305
|
+
self.last_i = None
|
|
306
|
+
self.is_regression = is_regression
|
|
307
|
+
|
|
308
|
+
def __iter__(self, ):
|
|
309
|
+
normal_iter = self.loader.__iter__()
|
|
310
|
+
for batch in normal_iter:
|
|
311
|
+
if len(batch) == 3:
|
|
312
|
+
x, y, i = batch
|
|
313
|
+
# Store for scoring callbacks
|
|
314
|
+
self.net._last_window_inds = i
|
|
315
|
+
else:
|
|
316
|
+
x, y = batch
|
|
317
|
+
|
|
318
|
+
# TODO: should be on dataset side
|
|
319
|
+
if hasattr(x, 'type'):
|
|
320
|
+
x = x.type(torch.float32)
|
|
321
|
+
if self.is_regression:
|
|
322
|
+
y = y.type(torch.float32)
|
|
323
|
+
else:
|
|
324
|
+
y = y.type(torch.int64)
|
|
325
|
+
yield x, y
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def update_estimator_docstring(base_class, docstring):
|
|
329
|
+
base_doc = base_class.__doc__.replace(' : ', ': ')
|
|
330
|
+
idx = base_doc.find('callbacks:')
|
|
331
|
+
idx_end = idx + base_doc[idx:].find('\n\n')
|
|
332
|
+
# remove callback descripiton already included in braindecode docstring
|
|
333
|
+
filtered_doc = base_doc[:idx] + base_doc[idx_end+6:]
|
|
334
|
+
splitted = docstring.split('Parameters\n ----------\n ')
|
|
335
|
+
out_docstring = splitted[0] + \
|
|
336
|
+
filtered_doc[filtered_doc.find('Parameters'):filtered_doc.find('Attributes')] + \
|
|
337
|
+
splitted[1] + \
|
|
338
|
+
filtered_doc[filtered_doc.find('Attributes'):]
|
|
339
|
+
return out_docstring
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def read_all_file_names(directory, extension):
|
|
343
|
+
"""Read all files with specified extension from given path and sorts them
|
|
344
|
+
based on a given sorting key.
|
|
345
|
+
|
|
346
|
+
Parameters
|
|
347
|
+
----------
|
|
348
|
+
directory: str
|
|
349
|
+
Parent directory to be searched for files of the specified type.
|
|
350
|
+
extension: str
|
|
351
|
+
File extension, i.e. ".edf" or ".txt".
|
|
352
|
+
|
|
353
|
+
Returns
|
|
354
|
+
-------
|
|
355
|
+
file_paths: list(str)
|
|
356
|
+
List of all files found in (sub)directories of path.
|
|
357
|
+
"""
|
|
358
|
+
assert extension.startswith('.')
|
|
359
|
+
file_paths = glob.glob(directory + '**/*' + extension, recursive=True)
|
|
360
|
+
assert len(file_paths) > 0, (
|
|
361
|
+
f'something went wrong. Found no {extension} files in {directory}')
|
|
362
|
+
return file_paths
|
braindecode/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.1"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Authors: Robin Schirrmeister <robintibor@gmail.com>
|
|
2
|
+
#
|
|
3
|
+
# License: BSD (3-clause)
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from skorch.utils import to_numpy, to_tensor
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def compute_amplitude_gradients(model, dataset, batch_size):
|
|
11
|
+
loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
|
|
12
|
+
drop_last=False, shuffle=False)
|
|
13
|
+
all_amp_grads = []
|
|
14
|
+
for batch_X, _, _ in loader:
|
|
15
|
+
this_amp_grads = compute_amplitude_gradients_for_X(model, batch_X, )
|
|
16
|
+
all_amp_grads.append(this_amp_grads)
|
|
17
|
+
all_amp_grads = np.concatenate(all_amp_grads, axis=1)
|
|
18
|
+
return all_amp_grads
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def compute_amplitude_gradients_for_X(model, X):
|
|
22
|
+
device = next(model.parameters()).device
|
|
23
|
+
ffted = np.fft.rfft(X, axis=2)
|
|
24
|
+
amps = np.abs(ffted)
|
|
25
|
+
phases = np.angle(ffted)
|
|
26
|
+
amps_th = to_tensor(amps.astype(np.float32), device=device).requires_grad_(True)
|
|
27
|
+
phases_th = to_tensor(phases.astype(np.float32), device=device).requires_grad_(True)
|
|
28
|
+
|
|
29
|
+
fft_coefs = amps_th.unsqueeze(-1) * torch.stack(
|
|
30
|
+
(torch.cos(phases_th), torch.sin(phases_th)), dim=-1)
|
|
31
|
+
fft_coefs = fft_coefs.squeeze(3)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
complex_fft_coefs = torch.view_as_complex(fft_coefs)
|
|
35
|
+
iffted = torch.fft.irfft(
|
|
36
|
+
complex_fft_coefs, n=X.shape[2], dim=2)
|
|
37
|
+
except AttributeError:
|
|
38
|
+
iffted = torch.irfft( # Deprecated since 1.7
|
|
39
|
+
fft_coefs, signal_ndim=1, signal_sizes=(X.shape[2],))
|
|
40
|
+
|
|
41
|
+
outs = model(iffted)
|
|
42
|
+
|
|
43
|
+
n_filters = outs.shape[1]
|
|
44
|
+
amp_grads_per_filter = np.full((n_filters,) + ffted.shape,
|
|
45
|
+
np.nan, dtype=np.float32)
|
|
46
|
+
for i_filter in range(n_filters):
|
|
47
|
+
mean_out = torch.mean(outs[:, i_filter])
|
|
48
|
+
mean_out.backward(retain_graph=True)
|
|
49
|
+
amp_grads = to_numpy(amps_th.grad.clone())
|
|
50
|
+
amp_grads_per_filter[i_filter] = amp_grads
|
|
51
|
+
amps_th.grad.zero_()
|
|
52
|
+
assert not np.any(np.isnan(amp_grads_per_filter))
|
|
53
|
+
return amp_grads_per_filter
|
test/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Authors: Maciej Sliwowski
|
|
2
|
+
# Robin Tibor Schirrmeister
|
|
3
|
+
#
|
|
4
|
+
# License: BSD-3
|
|
5
|
+
|
|
6
|
+
import mne
|
|
7
|
+
import numpy as np
|
|
8
|
+
import torch
|
|
9
|
+
from mne.io import concatenate_raws
|
|
10
|
+
from skorch.helper import predefined_split
|
|
11
|
+
from torch import optim
|
|
12
|
+
|
|
13
|
+
from braindecode import EEGClassifier
|
|
14
|
+
from braindecode.datautil.xy import create_from_X_y
|
|
15
|
+
from braindecode.training.losses import CroppedLoss
|
|
16
|
+
from braindecode.models import ShallowFBCSPNet
|
|
17
|
+
from braindecode.models.util import to_dense_prediction_model, get_output_shape
|
|
18
|
+
from braindecode.util import set_random_seeds
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_cropped_decoding():
|
|
22
|
+
# 5,6,7,10,13,14 are codes for executed and imagined hands/feet
|
|
23
|
+
subject_id = 1
|
|
24
|
+
event_codes = [5, 6, 9, 10, 13, 14]
|
|
25
|
+
|
|
26
|
+
# This will download the files if you don't have them yet,
|
|
27
|
+
# and then return the paths to the files.
|
|
28
|
+
physionet_paths = mne.datasets.eegbci.load_data(
|
|
29
|
+
subject_id, event_codes, update_path=False
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Load each of the files
|
|
33
|
+
parts = [
|
|
34
|
+
mne.io.read_raw_edf(
|
|
35
|
+
path, preload=True, stim_channel="auto", verbose="WARNING"
|
|
36
|
+
)
|
|
37
|
+
for path in physionet_paths
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# Concatenate them
|
|
41
|
+
raw = concatenate_raws(parts)
|
|
42
|
+
|
|
43
|
+
# Find the events in this dataset
|
|
44
|
+
events, _ = mne.events_from_annotations(raw)
|
|
45
|
+
# Use only EEG channels
|
|
46
|
+
eeg_channel_inds = mne.pick_types(
|
|
47
|
+
raw.info, meg=False, eeg=True, stim=False, eog=False, exclude="bads"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Extract trials, only using EEG channels
|
|
51
|
+
epoched = mne.Epochs(
|
|
52
|
+
raw,
|
|
53
|
+
events,
|
|
54
|
+
dict(hands=2, feet=3),
|
|
55
|
+
tmin=1,
|
|
56
|
+
tmax=4.1,
|
|
57
|
+
proj=False,
|
|
58
|
+
picks=eeg_channel_inds,
|
|
59
|
+
baseline=None,
|
|
60
|
+
preload=True,
|
|
61
|
+
)
|
|
62
|
+
# Convert data from volt to millivolt
|
|
63
|
+
# Pytorch expects float32 for input and int64 for labels.
|
|
64
|
+
X = (epoched.get_data() * 1e6).astype(np.float32)
|
|
65
|
+
y = (epoched.events[:, 2] - 2).astype(np.int64) # 2,3 -> 0,1
|
|
66
|
+
|
|
67
|
+
# Set if you want to use GPU
|
|
68
|
+
# You can also use torch.cuda.is_available() to determine if cuda is available on your machine.
|
|
69
|
+
cuda = False
|
|
70
|
+
set_random_seeds(seed=20170629, cuda=cuda)
|
|
71
|
+
|
|
72
|
+
# This will determine how many crops are processed in parallel
|
|
73
|
+
input_window_samples = 450
|
|
74
|
+
n_classes = 2
|
|
75
|
+
in_chans = X.shape[1]
|
|
76
|
+
# final_conv_length determines the size of the receptive field of the ConvNet
|
|
77
|
+
model = ShallowFBCSPNet(
|
|
78
|
+
in_chans=in_chans,
|
|
79
|
+
n_classes=n_classes,
|
|
80
|
+
input_window_samples=input_window_samples,
|
|
81
|
+
final_conv_length=12,
|
|
82
|
+
)
|
|
83
|
+
to_dense_prediction_model(model)
|
|
84
|
+
|
|
85
|
+
if cuda:
|
|
86
|
+
model.cuda()
|
|
87
|
+
|
|
88
|
+
# Perform forward pass to determine how many outputs per input
|
|
89
|
+
n_preds_per_input = get_output_shape(model, in_chans, input_window_samples)[2]
|
|
90
|
+
|
|
91
|
+
train_set = create_from_X_y(X[:60], y[:60],
|
|
92
|
+
drop_last_window=False,
|
|
93
|
+
window_size_samples=input_window_samples,
|
|
94
|
+
window_stride_samples=n_preds_per_input)
|
|
95
|
+
|
|
96
|
+
valid_set = create_from_X_y(X[60:], y[60:],
|
|
97
|
+
drop_last_window=False,
|
|
98
|
+
window_size_samples=input_window_samples,
|
|
99
|
+
window_stride_samples=n_preds_per_input)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
train_split = predefined_split(valid_set)
|
|
103
|
+
|
|
104
|
+
clf = EEGClassifier(
|
|
105
|
+
model,
|
|
106
|
+
cropped=True,
|
|
107
|
+
criterion=CroppedLoss,
|
|
108
|
+
criterion__loss_function=torch.nn.functional.nll_loss,
|
|
109
|
+
optimizer=optim.Adam,
|
|
110
|
+
train_split=train_split,
|
|
111
|
+
batch_size=32,
|
|
112
|
+
callbacks=['accuracy'],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
clf.fit(train_set, y=None, epochs=4)
|
|
116
|
+
|
|
117
|
+
np.testing.assert_allclose(
|
|
118
|
+
clf.history[:, 'train_loss'],
|
|
119
|
+
np.array(
|
|
120
|
+
[
|
|
121
|
+
1.6666231592496237,
|
|
122
|
+
1.2292670885721841,
|
|
123
|
+
1.1270817518234253,
|
|
124
|
+
1.1752660751342774
|
|
125
|
+
]
|
|
126
|
+
),
|
|
127
|
+
rtol=1e-3,
|
|
128
|
+
atol=1e-4,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
np.testing.assert_allclose(
|
|
132
|
+
clf.history[:, 'valid_loss'],
|
|
133
|
+
np.array(
|
|
134
|
+
[
|
|
135
|
+
1.5687058925628663,
|
|
136
|
+
0.8510023872057597,
|
|
137
|
+
2.087181798617045,
|
|
138
|
+
0.7100235184033712
|
|
139
|
+
]
|
|
140
|
+
),
|
|
141
|
+
rtol=1e-3,
|
|
142
|
+
atol=1e-3,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
np.testing.assert_allclose(
|
|
146
|
+
clf.history[:, 'train_accuracy'],
|
|
147
|
+
np.array(
|
|
148
|
+
[
|
|
149
|
+
0.48333333333333334,
|
|
150
|
+
0.5,
|
|
151
|
+
0.5,
|
|
152
|
+
0.6333333333333333
|
|
153
|
+
]
|
|
154
|
+
),
|
|
155
|
+
rtol=1e-3,
|
|
156
|
+
atol=1e-4,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
np.testing.assert_allclose(
|
|
160
|
+
clf.history[:, 'valid_accuracy'],
|
|
161
|
+
np.array(
|
|
162
|
+
[
|
|
163
|
+
0.533333,
|
|
164
|
+
0.5,
|
|
165
|
+
0.466667,
|
|
166
|
+
0.666667
|
|
167
|
+
]
|
|
168
|
+
),
|
|
169
|
+
rtol=1e-3,
|
|
170
|
+
atol=1e-4,
|
|
171
|
+
)
|