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.
Files changed (69) hide show
  1. Braindecode-0.5.1.dist-info/LICENSE.txt +31 -0
  2. Braindecode-0.5.1.dist-info/METADATA +133 -0
  3. Braindecode-0.5.1.dist-info/RECORD +69 -0
  4. Braindecode-0.5.1.dist-info/WHEEL +5 -0
  5. Braindecode-0.5.1.dist-info/top_level.txt +2 -0
  6. braindecode/__init__.py +4 -0
  7. braindecode/classifier.py +258 -0
  8. braindecode/datasets/__init__.py +7 -0
  9. braindecode/datasets/base.py +293 -0
  10. braindecode/datasets/bbci.py +679 -0
  11. braindecode/datasets/moabb.py +110 -0
  12. braindecode/datasets/sleep_physionet.py +103 -0
  13. braindecode/datasets/tuh.py +230 -0
  14. braindecode/datautil/__init__.py +11 -0
  15. braindecode/datautil/mne.py +136 -0
  16. braindecode/datautil/preprocess.py +364 -0
  17. braindecode/datautil/serialization.py +111 -0
  18. braindecode/datautil/util.py +36 -0
  19. braindecode/datautil/windowers.py +435 -0
  20. braindecode/datautil/xy.py +77 -0
  21. braindecode/models/__init__.py +12 -0
  22. braindecode/models/deep4.py +252 -0
  23. braindecode/models/eegnet.py +339 -0
  24. braindecode/models/eegresnet.py +232 -0
  25. braindecode/models/functions.py +43 -0
  26. braindecode/models/hybrid.py +103 -0
  27. braindecode/models/modules.py +150 -0
  28. braindecode/models/shallow_fbcsp.py +158 -0
  29. braindecode/models/sleep_stager_chambon_2018.py +113 -0
  30. braindecode/models/tcn.py +143 -0
  31. braindecode/models/util.py +66 -0
  32. braindecode/regressor.py +240 -0
  33. braindecode/samplers/__init__.py +5 -0
  34. braindecode/samplers/base.py +78 -0
  35. braindecode/samplers/ssl.py +118 -0
  36. braindecode/training/__init__.py +8 -0
  37. braindecode/training/callbacks.py +21 -0
  38. braindecode/training/losses.py +17 -0
  39. braindecode/training/scoring.py +296 -0
  40. braindecode/util.py +362 -0
  41. braindecode/version.py +1 -0
  42. braindecode/visualization/__init__.py +5 -0
  43. braindecode/visualization/gradients.py +53 -0
  44. test/__init__.py +0 -0
  45. test/acceptance_tests/__init__.py +0 -0
  46. test/acceptance_tests/test_cropped_decoding.py +171 -0
  47. test/acceptance_tests/test_eeg_classifier.py +255 -0
  48. test/acceptance_tests/test_model_architectures.py +54 -0
  49. test/acceptance_tests/test_trialwise_decoding.py +168 -0
  50. test/unit_tests/__init__.py +0 -0
  51. test/unit_tests/datasets/__init__.py +0 -0
  52. test/unit_tests/datasets/test_dataset.py +227 -0
  53. test/unit_tests/datasets/test_mne.py +145 -0
  54. test/unit_tests/datasets/test_sleep_physionet.py +23 -0
  55. test/unit_tests/datasets/test_tuh.py +97 -0
  56. test/unit_tests/datasets/test_xy.py +31 -0
  57. test/unit_tests/datautil/__init__.py +0 -0
  58. test/unit_tests/datautil/test_preprocess.py +270 -0
  59. test/unit_tests/datautil/test_serialization.py +127 -0
  60. test/unit_tests/datautil/test_windowers.py +519 -0
  61. test/unit_tests/models/__init__.py +0 -0
  62. test/unit_tests/models/test_models.py +136 -0
  63. test/unit_tests/models/test_util.py +22 -0
  64. test/unit_tests/test_eeg_classifier.py +73 -0
  65. test/unit_tests/test_eeg_regressor.py +72 -0
  66. test/unit_tests/test_scoring.py +282 -0
  67. test/unit_tests/test_util.py +30 -0
  68. test/unit_tests/visualization/__init__.py +0 -0
  69. test/unit_tests/visualization/test_gradients.py +38 -0
@@ -0,0 +1,118 @@
1
+ """
2
+ Self-supervised learning samplers.
3
+ """
4
+
5
+ # Authors: Hubert Banville <hubert.jbanville@gmail.com>
6
+ #
7
+ # License: BSD (3-clause)
8
+
9
+ import numpy as np
10
+
11
+ from . import RecordingSampler
12
+
13
+
14
+ class RelativePositioningSampler(RecordingSampler):
15
+ """Sample examples for the relative positioning task from [1]_.
16
+
17
+ Sample examples as tuples of two window indices, with a label indicating
18
+ whether the windows are close or far, as defined by tau_pos and tau_neg.
19
+
20
+ Parameters
21
+ ----------
22
+ metadata : pd.DataFrame
23
+ See RecordingSampler.
24
+ tau_pos : int
25
+ Size of the positive context, in samples. A positive pair contains two
26
+ windows x1 and x2 which are separated by at most `tau_pos` samples.
27
+ tau_neg : int
28
+ Size of the negative context, in samples. A negative pair contains two
29
+ windows x1 and x2 which are separated by at least `tau_neg` samples and
30
+ at most `tau_max` samples. Ignored if `same_rec_neg` is False.
31
+ n_examples : int
32
+ Number of pairs to extract.
33
+ tau_max : int | None
34
+ See `tau_neg`.
35
+ same_rec_neg : bool
36
+ If True, sample negative pairs from within the same recording. If
37
+ False, sample negative pairs from two different recordings.
38
+ random_state : None | np.RandomState | int
39
+ Random state.
40
+
41
+ References
42
+ ----------
43
+ .. [1] Banville, H., Chehab, O., Hyvärinen, A., Engemann, D. A., &
44
+ Gramfort, A. (2020). Uncovering the structure of clinical EEG
45
+ signals with self-supervised learning.
46
+ arXiv preprint arXiv:2007.16104.
47
+ """
48
+ def __init__(self, metadata, tau_pos, tau_neg, n_examples, tau_max=None,
49
+ same_rec_neg=True, random_state=None):
50
+ super().__init__(metadata, random_state=random_state)
51
+
52
+ self.tau_pos = tau_pos
53
+ self.tau_neg = tau_neg
54
+ self.tau_max = np.inf if tau_max is None else tau_max
55
+ self.n_examples = n_examples
56
+ self.same_rec_neg = same_rec_neg
57
+
58
+ if not same_rec_neg and self.n_recordings < 2:
59
+ raise ValueError('More than one recording must be available when '
60
+ 'using across-recording negative sampling.')
61
+
62
+ def _sample_pair(self):
63
+ """Sample a pair of two windows.
64
+ """
65
+ # Sample first window
66
+ win_ind1, rec_ind1 = self.sample_window()
67
+ ts1 = self.metadata.iloc[win_ind1]['i_start_in_trial']
68
+ ts = self.info.iloc[rec_ind1]['i_start_in_trial']
69
+
70
+ # Decide whether the pair will be positive or negative
71
+ pair_type = self.rng.binomial(1, 0.5)
72
+ win_ind2 = None
73
+ if pair_type == 0: # Negative example
74
+ if self.same_rec_neg:
75
+ mask = (
76
+ ((ts <= ts1 - self.tau_neg) & (ts >= ts1 - self.tau_max)) |
77
+ ((ts >= ts1 + self.tau_neg) & (ts <= ts1 + self.tau_max))
78
+ )
79
+ else:
80
+ rec_ind2 = rec_ind1
81
+ while rec_ind2 == rec_ind1:
82
+ win_ind2, rec_ind2 = self.sample_window()
83
+ elif pair_type == 1: # Positive example
84
+ mask = (ts >= ts1 - self.tau_pos) & (ts <= ts1 + self.tau_pos)
85
+
86
+ if win_ind2 is None:
87
+ mask[ts == ts1] = False # same window cannot be sampled twice
88
+ if sum(mask) == 0:
89
+ raise NotImplementedError
90
+ win_ind2 = self.rng.choice(self.info.iloc[rec_ind1]['index'][mask])
91
+
92
+ return win_ind1, win_ind2, float(pair_type)
93
+
94
+ def presample(self):
95
+ """Presample examples.
96
+
97
+ Once presampled, the examples are the same from one epoch to another.
98
+ """
99
+ self.examples = [self._sample_pair() for _ in range(self.n_examples)]
100
+ return self
101
+
102
+ def __iter__(self):
103
+ """Iterate over pairs.
104
+
105
+ Yields
106
+ ------
107
+ (int): position of the first window in the dataset.
108
+ (int): position of the second window in the dataset.
109
+ (float): 0 for negative pair, 1 for positive pair.
110
+ """
111
+ for i in range(self.n_examples):
112
+ if hasattr(self, 'examples'):
113
+ yield self.examples[i]
114
+ else:
115
+ yield self._sample_pair()
116
+
117
+ def __len__(self):
118
+ return self.n_examples
@@ -0,0 +1,8 @@
1
+ """
2
+ Functionality for skorch-based training.
3
+ """
4
+
5
+
6
+ from .losses import CroppedLoss
7
+ from .scoring import (CroppedTrialEpochScoring, PostEpochTrainScoring,
8
+ trial_preds_from_window_preds,)
@@ -0,0 +1,21 @@
1
+ from skorch.callbacks import ProgressBar, Callback
2
+ import torch
3
+
4
+
5
+ class MaxNormConstraintCallback(Callback):
6
+ def on_batch_end(self, net, training, *args, **kwargs):
7
+ if training:
8
+ model = net.module_
9
+ last_weight = None
10
+ for name, module in list(model.named_children()):
11
+ if hasattr(module, "weight") and (
12
+ not module.__class__.__name__.startswith("BatchNorm")
13
+ ):
14
+ module.weight.data = torch.renorm(
15
+ module.weight.data, 2, 0, maxnorm=2
16
+ )
17
+ last_weight = module.weight
18
+ if last_weight is not None:
19
+ last_weight.data = torch.renorm(
20
+ last_weight.data, 2, 0, maxnorm=0.5
21
+ )
@@ -0,0 +1,17 @@
1
+ import torch
2
+ from torch import nn
3
+
4
+
5
+ class CroppedLoss(nn.Module):
6
+ """Compute Loss after averaging predictions across time.
7
+ Assumes predictions are in shape:
8
+ n_batch size x n_classes x n_predictions (in time)"""
9
+
10
+ def __init__(self, loss_function):
11
+ super().__init__()
12
+ self.loss_function = loss_function
13
+
14
+ def forward(self, preds, targets):
15
+ avg_preds = torch.mean(preds, dim=2)
16
+ avg_preds = avg_preds.squeeze(dim=1)
17
+ return self.loss_function(avg_preds, targets)
@@ -0,0 +1,296 @@
1
+ # Authors: Maciej Sliwowski
2
+ # Robin Tibor Schirrmeister
3
+ # Alexandre Gramfort
4
+ #
5
+ # License: BSD-3
6
+
7
+ from contextlib import contextmanager
8
+
9
+ import numpy as np
10
+ import torch
11
+ from skorch.callbacks.scoring import EpochScoring
12
+ from skorch.utils import to_numpy
13
+ from skorch.dataset import unpack_data
14
+
15
+
16
+ def trial_preds_from_window_preds(
17
+ preds, i_window_in_trials, i_stop_in_trials):
18
+ """
19
+ Assigning window predictions to trials while removing duplicate
20
+ predictions.
21
+
22
+ Parameters
23
+ ----------
24
+ preds: list of ndarrays (atleast 2darrays)
25
+ List of window predictions, in each window prediction
26
+ time is in axis=1
27
+ i_window_in_trials: list
28
+ Index/number of window in trial
29
+ i_stop_in_trials: list
30
+ stop position of window in trial
31
+
32
+ Returns
33
+ -------
34
+ preds_per_trial: list of ndarrays
35
+ Predictions in each trial, duplicates removed
36
+
37
+ """
38
+ assert len(preds) == len(i_window_in_trials)
39
+ assert len(i_window_in_trials) == len(i_stop_in_trials)
40
+
41
+ # Algorithm for assigning window predictions to trials
42
+ # while removing duplicate predictions:
43
+ # Loop through windows:
44
+ # In each iteration you have predictions (assumed: #classes x #timesteps,
45
+ # or at least #timesteps must be in axis=1)
46
+ # and you have i_window_in_trial, i_stop_in_trial
47
+ # (i_trial removed from variable names for brevity)
48
+ # You first check if the i_window_in_trial is 1 larger
49
+ # than in last iteration, then you are still in the same trial
50
+ # Otherwise you are in a new trial
51
+ # If you are in the same trial, you check for duplicate predictions
52
+ # Only take predictions that are after (inclusive)
53
+ # the stop of the last iteration (i.e., the index of final prediction
54
+ # in the last iteration)
55
+ # Then add the duplicate-removed predictions from this window
56
+ # to predictions for current trial
57
+ preds_per_trial = []
58
+ cur_trial_preds = []
59
+ i_last_stop = None
60
+ i_last_window = -1
61
+ for window_preds, i_window, i_stop in zip(
62
+ preds, i_window_in_trials, i_stop_in_trials):
63
+ window_preds = np.array(window_preds)
64
+ if i_window != (i_last_window + 1):
65
+ assert i_window == 0, (
66
+ "window numbers in new trial should start from 0")
67
+ preds_per_trial.append(np.concatenate(cur_trial_preds, axis=1))
68
+ cur_trial_preds = []
69
+ i_last_stop = None
70
+
71
+ if i_last_stop is not None:
72
+ # Remove duplicates
73
+ n_needed_preds = i_stop - i_last_stop
74
+ window_preds = window_preds[:, -n_needed_preds:]
75
+ cur_trial_preds.append(window_preds)
76
+ i_last_window = i_window
77
+ i_last_stop = i_stop
78
+ # add last trial preds
79
+ preds_per_trial.append(np.concatenate(cur_trial_preds, axis=1))
80
+ return preds_per_trial
81
+
82
+
83
+ @contextmanager
84
+ def _cache_net_forward_iter(net, use_caching, y_preds):
85
+ """Caching context for ``skorch.NeuralNet`` instance.
86
+ Returns a modified version of the net whose ``forward_iter``
87
+ method will subsequently return cached predictions. Leaving the
88
+ context will undo the overwrite of the ``forward_iter`` method.
89
+ """
90
+ if not use_caching:
91
+ yield net
92
+ return
93
+ y_preds = iter(y_preds)
94
+
95
+ # pylint: disable=unused-argument
96
+ def cached_forward_iter(*args, device=net.device, **kwargs):
97
+ for yp in y_preds:
98
+ yield yp.to(device=device)
99
+
100
+ net.forward_iter = cached_forward_iter
101
+ try:
102
+ yield net
103
+ finally:
104
+ # By setting net.forward_iter we define an attribute
105
+ # `forward_iter` that precedes the bound method
106
+ # `forward_iter`. By deleting the entry from the attribute
107
+ # dict we undo this.
108
+ del net.__dict__["forward_iter"]
109
+
110
+
111
+ class CroppedTrialEpochScoring(EpochScoring):
112
+ """
113
+ Class to compute scores for trials from a model that predicts (super)crops.
114
+ """
115
+
116
+ def __init__(
117
+ self,
118
+ scoring,
119
+ lower_is_better=True,
120
+ on_train=False,
121
+ name=None,
122
+ target_extractor=to_numpy,
123
+ use_caching=True,
124
+ ):
125
+ super().__init__(
126
+ scoring=scoring,
127
+ lower_is_better=lower_is_better,
128
+ on_train=on_train,
129
+ name=name,
130
+ target_extractor=target_extractor,
131
+ use_caching=use_caching,
132
+ )
133
+ if not self.on_train:
134
+ self.window_inds_ = []
135
+
136
+ def _initialize_cache(self):
137
+ super()._initialize_cache()
138
+ self.crops_to_trials_computed = False
139
+ self.y_trues_ = []
140
+ self.y_preds_ = []
141
+ if not self.on_train:
142
+ self.window_inds_ = []
143
+
144
+ def on_epoch_end(self, net, dataset_train, dataset_valid, **kwargs):
145
+ assert self.use_caching == True
146
+ if not self.crops_to_trials_computed:
147
+ if self.on_train:
148
+ # Prevent that rng state of torch is changed by
149
+ # creation+usage of iterator
150
+ rng_state = torch.random.get_rng_state()
151
+ pred_results = net.predict_with_window_inds_and_ys(
152
+ dataset_train)
153
+ torch.random.set_rng_state(rng_state)
154
+ else:
155
+ pred_results = {}
156
+ pred_results['i_window_in_trials'] = np.concatenate(
157
+ [i[0].cpu().numpy() for i in self.window_inds_]
158
+ )
159
+ pred_results['i_window_stops'] = np.concatenate(
160
+ [i[2].cpu().numpy() for i in self.window_inds_]
161
+ )
162
+ pred_results['preds'] = np.concatenate(
163
+ [y_pred.cpu().numpy() for y_pred in self.y_preds_])
164
+ pred_results['window_ys'] = np.concatenate(
165
+ [y.cpu().numpy() for y in self.y_trues_])
166
+
167
+ # A new trial starts
168
+ # when the index of the window in trials
169
+ # does not increment by 1
170
+ # Add dummy infinity at start
171
+ window_0_per_trial_mask = np.diff(
172
+ pred_results['i_window_in_trials'], prepend=[np.inf]) != 1
173
+ trial_ys = pred_results['window_ys'][window_0_per_trial_mask]
174
+ trial_preds = trial_preds_from_window_preds(
175
+ pred_results['preds'],
176
+ pred_results['i_window_in_trials'],
177
+ pred_results['i_window_stops'])
178
+
179
+ # Average across the timesteps of each trial so we have per-trial
180
+ # predictions already, these will be just passed through the forward
181
+ # method of the classifier/regressor to the skorch scoring function.
182
+ # trial_preds is a list, each item is a 2d array classes x time
183
+ y_preds_per_trial = np.array(
184
+ [np.mean(p, axis=1) for p in trial_preds]
185
+ )
186
+ # Move into format expected by skorch (list of torch tensors)
187
+ y_preds_per_trial = [torch.tensor(y_preds_per_trial)]
188
+
189
+ # Store the computed trial preds for all Cropped Callbacks
190
+ # that are also on same set
191
+ cbs = net._default_callbacks + net.callbacks
192
+ epoch_cbs = [
193
+ cb for name, cb in cbs if
194
+ isinstance(cb, CroppedTrialEpochScoring) and (
195
+ cb.on_train == self.on_train)
196
+ ]
197
+ for cb in epoch_cbs:
198
+ cb.y_preds_ = y_preds_per_trial
199
+ cb.y_trues_ = trial_ys
200
+ cb.crops_to_trials_computed = True
201
+
202
+ dataset = dataset_train if self.on_train else dataset_valid
203
+
204
+ with _cache_net_forward_iter(
205
+ net, self.use_caching, self.y_preds_
206
+ ) as cached_net:
207
+ current_score = self._scoring(cached_net, dataset, self.y_trues_)
208
+ self._record_score(net.history, current_score)
209
+
210
+ return
211
+
212
+
213
+ class PostEpochTrainScoring(EpochScoring):
214
+ """
215
+ Epoch Scoring class that recomputes predictions after the epoch
216
+ on the training in validation mode.
217
+
218
+ Note: For unknown reasons, this affects global random generator and
219
+ therefore all results may change slightly if you add this scoring callback.
220
+
221
+ Parameters
222
+ ----------
223
+ scoring : None, str, or callable (default=None)
224
+ If None, use the ``score`` method of the model. If str, it
225
+ should be a valid sklearn scorer (e.g. "f1", "accuracy"). If a
226
+ callable, it should have the signature (model, X, y), and it
227
+ should return a scalar. This works analogously to the
228
+ ``scoring`` parameter in sklearn's ``GridSearchCV`` et al.
229
+ lower_is_better : bool (default=True)
230
+ Whether lower scores should be considered better or worse.
231
+ name : str or None (default=None)
232
+ If not an explicit string, tries to infer the name from the
233
+ ``scoring`` argument.
234
+ target_extractor : callable (default=to_numpy)
235
+ This is called on y before it is passed to scoring.
236
+ """
237
+
238
+ def __init__(
239
+ self,
240
+ scoring,
241
+ lower_is_better=True,
242
+ name=None,
243
+ target_extractor=to_numpy,
244
+ ):
245
+ super().__init__(
246
+ scoring=scoring,
247
+ lower_is_better=lower_is_better,
248
+ on_train=True,
249
+ name=name,
250
+ target_extractor=target_extractor,
251
+ use_caching=False,
252
+ )
253
+
254
+ def on_epoch_end(self, net, dataset_train, dataset_valid, **kwargs):
255
+ if len(self.y_preds_) == 0:
256
+ dataset = net.get_dataset(dataset_train)
257
+ # Prevent that rng state of torch is changed by
258
+ # creation+usage of iterator
259
+ # Unfortunatenly calling __iter__() of a pytorch
260
+ # DataLoader will change the random state
261
+ # Note line below setting rng state back
262
+ rng_state = torch.random.get_rng_state()
263
+ iterator = net.get_iterator(dataset, training=False)
264
+ y_preds = []
265
+ y_test = []
266
+ for data in iterator:
267
+ batch_X, batch_y = unpack_data(data)
268
+ yp = net.evaluation_step(batch_X, training=False)
269
+ yp = yp.to(device="cpu")
270
+ y_test.append(self.target_extractor(batch_y))
271
+ y_preds.append(yp)
272
+ y_test = np.concatenate(y_test)
273
+ torch.random.set_rng_state(rng_state)
274
+
275
+ # Adding the recomputed preds to all other
276
+ # instances of PostEpochTrainScoring of this
277
+ # Skorch-Net (NeuralNet, BraindecodeClassifier etc.)
278
+ # (They will be reinitialized to empty lists by skorch
279
+ # each epoch)
280
+ cbs = net._default_callbacks + net.callbacks
281
+ epoch_cbs = [
282
+ cb for name, cb in cbs if isinstance(cb, PostEpochTrainScoring)
283
+ ]
284
+ for cb in epoch_cbs:
285
+ cb.y_preds_ = y_preds
286
+ cb.y_trues_ = y_test
287
+ # y pred should be same as self.y_preds_
288
+ # Unclear if this also leads to any
289
+ # random generator call?
290
+ with _cache_net_forward_iter(
291
+ net, use_caching=True, y_preds=self.y_preds_
292
+ ) as cached_net:
293
+ current_score = self._scoring(
294
+ cached_net, dataset_train, self.y_trues_
295
+ )
296
+ self._record_score(net.history, current_score)