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,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,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)
|