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,113 @@
|
|
|
1
|
+
# Authors: Hubert Banville <hubert.jbanville@gmail.com>
|
|
2
|
+
#
|
|
3
|
+
# License: BSD (3-clause)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
from torch import nn
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SleepStagerChambon2018(nn.Module):
|
|
12
|
+
"""
|
|
13
|
+
Sleep staging architecture from [1]_.
|
|
14
|
+
|
|
15
|
+
Convolutional neural network for sleep staging described in [1]_.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
n_channels : int
|
|
20
|
+
Number of EEG channels.
|
|
21
|
+
sfreq : float
|
|
22
|
+
EEG sampling frequency.
|
|
23
|
+
n_conv_chs : int
|
|
24
|
+
Number of convolutional channels. Set to 8 in [1]_.
|
|
25
|
+
time_conv_size_s : float
|
|
26
|
+
Size of filters in temporal convolution layers, in seconds. Set to 0.5
|
|
27
|
+
in [1]_ (64 samples at sfreq=128).
|
|
28
|
+
max_pool_size_s : float
|
|
29
|
+
Max pooling size, in seconds. Set to 0.125 in [1]_ (16 samples at
|
|
30
|
+
sfreq=128).
|
|
31
|
+
pad_size_s : float
|
|
32
|
+
Paddind size, in seconds. Set to 0.25 in [1]_ (half the temporal
|
|
33
|
+
convolution kernel size).
|
|
34
|
+
input_size_s : float
|
|
35
|
+
Size of the input, in seconds.
|
|
36
|
+
n_classes : int
|
|
37
|
+
Number of classes.
|
|
38
|
+
dropout : float
|
|
39
|
+
Dropout rate before the output dense layer.
|
|
40
|
+
apply_batch_norm : bool
|
|
41
|
+
If True, apply batch normalization after both temporal convolutional
|
|
42
|
+
layers.
|
|
43
|
+
|
|
44
|
+
References
|
|
45
|
+
----------
|
|
46
|
+
.. [1] Chambon, S., Galtier, M. N., Arnal, P. J., Wainrib, G., &
|
|
47
|
+
Gramfort, A. (2018). A deep learning architecture for temporal sleep
|
|
48
|
+
stage classification using multivariate and multimodal time series.
|
|
49
|
+
IEEE Transactions on Neural Systems and Rehabilitation Engineering,
|
|
50
|
+
26(4), 758-769.
|
|
51
|
+
"""
|
|
52
|
+
def __init__(self, n_channels, sfreq, n_conv_chs=8, time_conv_size_s=0.5,
|
|
53
|
+
max_pool_size_s=0.125, pad_size_s=0.25, input_size_s=30,
|
|
54
|
+
n_classes=5, dropout=0.25, apply_batch_norm=False):
|
|
55
|
+
super().__init__()
|
|
56
|
+
|
|
57
|
+
time_conv_size = np.ceil(time_conv_size_s * sfreq).astype(int)
|
|
58
|
+
max_pool_size = np.ceil(max_pool_size_s * sfreq).astype(int)
|
|
59
|
+
input_size = np.ceil(input_size_s * sfreq).astype(int)
|
|
60
|
+
pad_size = np.ceil(pad_size_s * sfreq).astype(int)
|
|
61
|
+
|
|
62
|
+
self.n_channels = n_channels
|
|
63
|
+
|
|
64
|
+
if n_channels > 1:
|
|
65
|
+
self.spatial_conv = nn.Conv2d(1, n_channels, (n_channels, 1))
|
|
66
|
+
|
|
67
|
+
batch_norm = nn.BatchNorm2d if apply_batch_norm else nn.Identity
|
|
68
|
+
|
|
69
|
+
self.feature_extractor = nn.Sequential(
|
|
70
|
+
nn.Conv2d(
|
|
71
|
+
1, n_conv_chs, (1, time_conv_size), padding=(0, pad_size)),
|
|
72
|
+
batch_norm(n_conv_chs),
|
|
73
|
+
nn.ReLU(),
|
|
74
|
+
nn.MaxPool2d((1, max_pool_size)),
|
|
75
|
+
nn.Conv2d(
|
|
76
|
+
n_conv_chs, n_conv_chs, (1, time_conv_size),
|
|
77
|
+
padding=(0, pad_size)),
|
|
78
|
+
batch_norm(n_conv_chs),
|
|
79
|
+
nn.ReLU(),
|
|
80
|
+
nn.MaxPool2d((1, max_pool_size))
|
|
81
|
+
)
|
|
82
|
+
len_last_layer = self._len_last_layer(n_channels, input_size)
|
|
83
|
+
self.fc = nn.Sequential(
|
|
84
|
+
nn.Dropout(dropout),
|
|
85
|
+
nn.Linear(len_last_layer, n_classes)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def _len_last_layer(self, n_channels, input_size):
|
|
89
|
+
self.feature_extractor.eval()
|
|
90
|
+
with torch.no_grad():
|
|
91
|
+
out = self.feature_extractor(
|
|
92
|
+
torch.Tensor(1, 1, n_channels, input_size))
|
|
93
|
+
self.feature_extractor.train()
|
|
94
|
+
return len(out.flatten())
|
|
95
|
+
|
|
96
|
+
def forward(self, x):
|
|
97
|
+
"""
|
|
98
|
+
Forward pass.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
---------
|
|
102
|
+
x: torch.Tensor
|
|
103
|
+
Batch of EEG windows of shape (batch_size, n_channels, n_times).
|
|
104
|
+
"""
|
|
105
|
+
if x.ndim == 3:
|
|
106
|
+
x = x.unsqueeze(1)
|
|
107
|
+
|
|
108
|
+
if self.n_channels > 1:
|
|
109
|
+
x = self.spatial_conv(x)
|
|
110
|
+
x = x.transpose(1, 2)
|
|
111
|
+
|
|
112
|
+
x = self.feature_extractor(x)
|
|
113
|
+
return self.fc(x.flatten(start_dim=1))
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Authors: Patryk Chrabaszcz
|
|
2
|
+
# Lukas Gemein <l.gemein@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# License: BSD-3
|
|
5
|
+
|
|
6
|
+
from torch import nn
|
|
7
|
+
from torch.nn import init
|
|
8
|
+
from torch.nn.utils import weight_norm
|
|
9
|
+
|
|
10
|
+
from .modules import Ensure4d, Expression
|
|
11
|
+
from .functions import squeeze_final_output
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TCN(nn.Module):
|
|
15
|
+
"""Temporal Convolutional Network (TCN) as described in [1]_.
|
|
16
|
+
Code adapted from https://github.com/locuslab/TCN/blob/master/TCN/tcn.py
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
n_in_chans: int
|
|
20
|
+
number of input EEG channels
|
|
21
|
+
n_outputs: int
|
|
22
|
+
number of outputs of the decoding task (for example number of classes in
|
|
23
|
+
classification)
|
|
24
|
+
n_filters: int
|
|
25
|
+
number of output filters of each convolution
|
|
26
|
+
n_blocks: int
|
|
27
|
+
number of temporal blocks in the network
|
|
28
|
+
kernel_size: int
|
|
29
|
+
kernel size of the convolutions
|
|
30
|
+
drop_prob: float
|
|
31
|
+
dropout probability
|
|
32
|
+
add_log_softmax: bool
|
|
33
|
+
whether to add a log softmax layer
|
|
34
|
+
References
|
|
35
|
+
----------
|
|
36
|
+
.. [1] Bai, S., Kolter, J. Z., & Koltun, V. (2018).
|
|
37
|
+
An empirical evaluation of generic convolutional and recurrent networks
|
|
38
|
+
for sequence modeling.
|
|
39
|
+
arXiv preprint arXiv:1803.01271.
|
|
40
|
+
"""
|
|
41
|
+
def __init__(self, n_in_chans, n_outputs, n_blocks, n_filters, kernel_size,
|
|
42
|
+
drop_prob, add_log_softmax):
|
|
43
|
+
super().__init__()
|
|
44
|
+
self.ensuredims = Ensure4d()
|
|
45
|
+
t_blocks = nn.Sequential()
|
|
46
|
+
for i in range(n_blocks):
|
|
47
|
+
n_inputs = n_in_chans if i == 0 else n_filters
|
|
48
|
+
dilation_size = 2 ** i
|
|
49
|
+
t_blocks.add_module("temporal_block_{:d}".format(i), TemporalBlock(
|
|
50
|
+
n_inputs=n_inputs,
|
|
51
|
+
n_outputs=n_filters,
|
|
52
|
+
kernel_size=kernel_size,
|
|
53
|
+
stride=1,
|
|
54
|
+
dilation=dilation_size,
|
|
55
|
+
padding=(kernel_size - 1) * dilation_size,
|
|
56
|
+
drop_prob=drop_prob
|
|
57
|
+
))
|
|
58
|
+
self.temporal_blocks = t_blocks
|
|
59
|
+
self.fc = nn.Linear(in_features=n_filters, out_features=n_outputs)
|
|
60
|
+
if add_log_softmax:
|
|
61
|
+
self.log_softmax = nn.LogSoftmax(dim=1)
|
|
62
|
+
self.squeeze = Expression(squeeze_final_output)
|
|
63
|
+
|
|
64
|
+
self.min_len = 1
|
|
65
|
+
for i in range(n_blocks):
|
|
66
|
+
dilation = 2 ** i
|
|
67
|
+
self.min_len += 2 * (kernel_size - 1) * dilation
|
|
68
|
+
|
|
69
|
+
# start in eval mode
|
|
70
|
+
self.eval()
|
|
71
|
+
|
|
72
|
+
def forward(self, x):
|
|
73
|
+
x = self.ensuredims(x)
|
|
74
|
+
# x is in format: B x C x T x 1
|
|
75
|
+
(batch_size, _, time_size, _) = x.size()
|
|
76
|
+
assert time_size >= self.min_len
|
|
77
|
+
# remove empty trailing dimension
|
|
78
|
+
x = x.squeeze(3)
|
|
79
|
+
x = self.temporal_blocks(x)
|
|
80
|
+
# Convert to: B x T x C
|
|
81
|
+
x = x.transpose(1, 2).contiguous()
|
|
82
|
+
|
|
83
|
+
fc_out = self.fc(x.view(batch_size * time_size, x.size(2)))
|
|
84
|
+
if hasattr(self, "log_softmax"):
|
|
85
|
+
fc_out = self.log_softmax(fc_out)
|
|
86
|
+
fc_out = fc_out.view(batch_size, time_size, fc_out.size(1))
|
|
87
|
+
|
|
88
|
+
out_size = 1 + max(0, time_size - self.min_len)
|
|
89
|
+
out = fc_out[:, -out_size:, :].transpose(1, 2)
|
|
90
|
+
# re-add 4th dimension for compatibility with braindecode
|
|
91
|
+
return self.squeeze(out[:, :, :, None])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TemporalBlock(nn.Module):
|
|
95
|
+
def __init__(self, n_inputs, n_outputs, kernel_size, stride, dilation,
|
|
96
|
+
padding, drop_prob):
|
|
97
|
+
super().__init__()
|
|
98
|
+
self.conv1 = weight_norm(nn.Conv1d(
|
|
99
|
+
n_inputs, n_outputs, kernel_size,
|
|
100
|
+
stride=stride, padding=padding, dilation=dilation))
|
|
101
|
+
self.chomp1 = Chomp1d(padding)
|
|
102
|
+
self.relu1 = nn.ReLU()
|
|
103
|
+
self.dropout1 = nn.Dropout2d(drop_prob)
|
|
104
|
+
|
|
105
|
+
self.conv2 = weight_norm(nn.Conv1d(
|
|
106
|
+
n_outputs, n_outputs, kernel_size,
|
|
107
|
+
stride=stride, padding=padding, dilation=dilation))
|
|
108
|
+
self.chomp2 = Chomp1d(padding)
|
|
109
|
+
self.relu2 = nn.ReLU()
|
|
110
|
+
self.dropout2 = nn.Dropout2d(drop_prob)
|
|
111
|
+
|
|
112
|
+
self.downsample = (nn.Conv1d(n_inputs, n_outputs, 1)
|
|
113
|
+
if n_inputs != n_outputs else None)
|
|
114
|
+
self.relu = nn.ReLU()
|
|
115
|
+
|
|
116
|
+
init.normal_(self.conv1.weight, 0, 0.01)
|
|
117
|
+
init.normal_(self.conv2.weight, 0, 0.01)
|
|
118
|
+
if self.downsample is not None:
|
|
119
|
+
init.normal_(self.downsample.weight, 0, 0.01)
|
|
120
|
+
|
|
121
|
+
def forward(self, x):
|
|
122
|
+
out = self.conv1(x)
|
|
123
|
+
out = self.chomp1(out)
|
|
124
|
+
out = self.relu1(out)
|
|
125
|
+
out = self.dropout1(out)
|
|
126
|
+
out = self.conv2(out)
|
|
127
|
+
out = self.chomp2(out)
|
|
128
|
+
out = self.relu2(out)
|
|
129
|
+
out = self.dropout2(out)
|
|
130
|
+
res = x if self.downsample is None else self.downsample(x)
|
|
131
|
+
return self.relu(out + res)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Chomp1d(nn.Module):
|
|
135
|
+
def __init__(self, chomp_size):
|
|
136
|
+
super().__init__()
|
|
137
|
+
self.chomp_size = chomp_size
|
|
138
|
+
|
|
139
|
+
def extra_repr(self):
|
|
140
|
+
return 'chomp_size={}'.format(self.chomp_size)
|
|
141
|
+
|
|
142
|
+
def forward(self, x):
|
|
143
|
+
return x[:, :, :-self.chomp_size].contiguous()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def to_dense_prediction_model(model, axis=(2, 3)):
|
|
6
|
+
"""
|
|
7
|
+
Transform a sequential model with strides to a model that outputs
|
|
8
|
+
dense predictions by removing the strides and instead inserting dilations.
|
|
9
|
+
Modifies model in-place.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
model: torch.nn.Module
|
|
14
|
+
Model which modules will be modified
|
|
15
|
+
axis: int or (int,int)
|
|
16
|
+
Axis to transform (in terms of intermediate output axes)
|
|
17
|
+
can either be 2, 3, or (2,3).
|
|
18
|
+
|
|
19
|
+
Notes
|
|
20
|
+
-----
|
|
21
|
+
Does not yet work correctly for average pooling.
|
|
22
|
+
Prior to version 0.1.7, there had been a bug that could move strides
|
|
23
|
+
backwards one layer.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
if not hasattr(axis, "__len__"):
|
|
27
|
+
axis = [axis]
|
|
28
|
+
assert all([ax in [2, 3] for ax in axis]), "Only 2 and 3 allowed for axis"
|
|
29
|
+
axis = np.array(axis) - 2
|
|
30
|
+
stride_so_far = np.array([1, 1])
|
|
31
|
+
for module in model.modules():
|
|
32
|
+
if hasattr(module, "dilation"):
|
|
33
|
+
assert module.dilation == 1 or (module.dilation == (1, 1)), (
|
|
34
|
+
"Dilation should equal 1 before conversion, maybe the model is "
|
|
35
|
+
"already converted?"
|
|
36
|
+
)
|
|
37
|
+
new_dilation = [1, 1]
|
|
38
|
+
for ax in axis:
|
|
39
|
+
new_dilation[ax] = int(stride_so_far[ax])
|
|
40
|
+
module.dilation = tuple(new_dilation)
|
|
41
|
+
if hasattr(module, "stride"):
|
|
42
|
+
if not hasattr(module.stride, "__len__"):
|
|
43
|
+
module.stride = (module.stride, module.stride)
|
|
44
|
+
stride_so_far *= np.array(module.stride)
|
|
45
|
+
new_stride = list(module.stride)
|
|
46
|
+
for ax in axis:
|
|
47
|
+
new_stride[ax] = 1
|
|
48
|
+
module.stride = tuple(new_stride)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_output_shape(model, in_chans, input_window_samples):
|
|
52
|
+
"""Returns shape of neural network output for batch size equal 1.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
output_shape: tuple
|
|
57
|
+
shape of the network output for `batch_size==1` (1, ...)
|
|
58
|
+
"""
|
|
59
|
+
with torch.no_grad():
|
|
60
|
+
dummy_input = torch.ones(
|
|
61
|
+
1, in_chans, input_window_samples,
|
|
62
|
+
dtype=next(model.parameters()).dtype,
|
|
63
|
+
device=next(model.parameters()).device,
|
|
64
|
+
)
|
|
65
|
+
output_shape = model(dummy_input).shape
|
|
66
|
+
return output_shape
|
braindecode/regressor.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from sklearn.metrics import get_scorer
|
|
3
|
+
from skorch.callbacks import EpochTimer, BatchScoring, PrintLog, EpochScoring
|
|
4
|
+
from skorch.classifier import NeuralNet
|
|
5
|
+
from skorch.regressor import NeuralNetRegressor
|
|
6
|
+
from skorch.utils import train_loss_score, valid_loss_score, noop, to_numpy
|
|
7
|
+
|
|
8
|
+
from .training.scoring import PostEpochTrainScoring, CroppedTrialEpochScoring
|
|
9
|
+
from .util import ThrowAwayIndexLoader, update_estimator_docstring
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EEGRegressor(NeuralNetRegressor):
|
|
13
|
+
doc = """Regressor that calls loss function directly.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
cropped: bool (default=False)
|
|
18
|
+
Defines whether torch model passed to this class is cropped or not.
|
|
19
|
+
Currently used for callbacks definition.
|
|
20
|
+
|
|
21
|
+
callbacks: None or list of strings or list of Callback instances (default=None)
|
|
22
|
+
More callbacks, in addition to those returned by
|
|
23
|
+
``get_default_callbacks``. Each callback should inherit from
|
|
24
|
+
:class:`skorch.callbacks.Callback`. If not ``None``, callbacks can be a
|
|
25
|
+
list of strings specifying `sklearn` scoring functions (for scoring
|
|
26
|
+
functions names see: https://scikit-learn.org/stable/modules/model_evaluation.html#scoring-parameter)
|
|
27
|
+
or a list of callbacks where the callback names are inferred from the
|
|
28
|
+
class name. Name conflicts are resolved by appending a count suffix
|
|
29
|
+
starting with 1, e.g. ``EpochScoring_1``. Alternatively,
|
|
30
|
+
a tuple ``(name, callback)`` can be passed, where ``name``
|
|
31
|
+
should be unique. Callbacks may or may not be instantiated.
|
|
32
|
+
The callback name can be used to set parameters on specific
|
|
33
|
+
callbacks (e.g., for the callback with name ``'print_log'``, use
|
|
34
|
+
``net.set_params(callbacks__print_log__keys_ignored=['epoch',
|
|
35
|
+
'train_loss'])``).
|
|
36
|
+
|
|
37
|
+
iterator_train__shuffle: bool (default=True)
|
|
38
|
+
Defines whether train dataset will be shuffled. As skorch does not
|
|
39
|
+
shuffle the train dataset by default this one overwrites this option.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
__doc__ = update_estimator_docstring(NeuralNetRegressor, doc)
|
|
43
|
+
|
|
44
|
+
def __init__(self, *args, cropped=False, callbacks=None,
|
|
45
|
+
iterator_train__shuffle=True, **kwargs):
|
|
46
|
+
self.cropped = cropped
|
|
47
|
+
callbacks = self._parse_callbacks(callbacks)
|
|
48
|
+
|
|
49
|
+
super().__init__(*args,
|
|
50
|
+
callbacks=callbacks,
|
|
51
|
+
iterator_train__shuffle=iterator_train__shuffle,
|
|
52
|
+
**kwargs)
|
|
53
|
+
|
|
54
|
+
def _parse_callbacks(self, callbacks):
|
|
55
|
+
callbacks_list = []
|
|
56
|
+
if callbacks is not None:
|
|
57
|
+
for callback in callbacks:
|
|
58
|
+
if isinstance(callback, tuple):
|
|
59
|
+
callbacks_list.append(callback)
|
|
60
|
+
else:
|
|
61
|
+
assert isinstance(callback, str)
|
|
62
|
+
scoring = get_scorer(callback)
|
|
63
|
+
scoring_name = scoring._score_func.__name__
|
|
64
|
+
assert scoring_name.endswith(
|
|
65
|
+
('_score', '_error', '_deviance', '_loss'))
|
|
66
|
+
if (scoring_name.endswith('_score') or
|
|
67
|
+
callback.startswith('neg_')):
|
|
68
|
+
lower_is_better = False
|
|
69
|
+
else:
|
|
70
|
+
lower_is_better = True
|
|
71
|
+
train_name = f'train_{callback}'
|
|
72
|
+
valid_name = f'valid_{callback}'
|
|
73
|
+
if self.cropped:
|
|
74
|
+
# In case of cropped decoding we are using braindecode
|
|
75
|
+
# specific scoring created for cropped decoding
|
|
76
|
+
train_scoring = CroppedTrialEpochScoring(
|
|
77
|
+
callback, lower_is_better, on_train=True, name=train_name
|
|
78
|
+
)
|
|
79
|
+
valid_scoring = CroppedTrialEpochScoring(
|
|
80
|
+
callback, lower_is_better, on_train=False, name=valid_name
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
train_scoring = PostEpochTrainScoring(
|
|
84
|
+
callback, lower_is_better, name=train_name
|
|
85
|
+
)
|
|
86
|
+
valid_scoring = EpochScoring(
|
|
87
|
+
callback, lower_is_better, on_train=False, name=valid_name
|
|
88
|
+
)
|
|
89
|
+
callbacks_list.extend([
|
|
90
|
+
(train_name, train_scoring),
|
|
91
|
+
(valid_name, valid_scoring)
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
return callbacks_list
|
|
95
|
+
|
|
96
|
+
# pylint: disable=arguments-differ
|
|
97
|
+
def get_loss(self, y_pred, y_true, *args, **kwargs):
|
|
98
|
+
"""Return the loss for this batch by calling NeuralNet get_loss.
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
y_pred : torch tensor
|
|
102
|
+
Predicted target values
|
|
103
|
+
y_true : torch tensor
|
|
104
|
+
True target values.
|
|
105
|
+
X : input data, compatible with skorch.dataset.Dataset
|
|
106
|
+
By default, you should be able to pass:
|
|
107
|
+
* numpy arrays
|
|
108
|
+
* torch tensors
|
|
109
|
+
* pandas DataFrame or Series
|
|
110
|
+
* scipy sparse CSR matrices
|
|
111
|
+
* a dictionary of the former three
|
|
112
|
+
* a list/tuple of the former three
|
|
113
|
+
* a Dataset
|
|
114
|
+
If this doesn't work with your data, you have to pass a
|
|
115
|
+
``Dataset`` that can deal with the data.
|
|
116
|
+
training : bool (default=False)
|
|
117
|
+
Whether train mode should be used or not.
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
return NeuralNet.get_loss(self, y_pred, y_true, *args, **kwargs)
|
|
121
|
+
|
|
122
|
+
def get_iterator(self, dataset, training=False, drop_index=True):
|
|
123
|
+
iterator = super().get_iterator(dataset, training=training)
|
|
124
|
+
if drop_index:
|
|
125
|
+
return ThrowAwayIndexLoader(self, iterator, is_regression=True)
|
|
126
|
+
else:
|
|
127
|
+
return iterator
|
|
128
|
+
|
|
129
|
+
def on_batch_end(self, net, X, y, training=False, **kwargs):
|
|
130
|
+
# If training is false, assume that our loader has indices for this
|
|
131
|
+
# batch
|
|
132
|
+
if not training:
|
|
133
|
+
cbs = self._default_callbacks + self.callbacks
|
|
134
|
+
epoch_cbs = []
|
|
135
|
+
for name, cb in cbs:
|
|
136
|
+
if (cb.__class__.__name__ == 'CroppedTrialEpochScoring') and (
|
|
137
|
+
hasattr(cb, 'window_inds_')) and (cb.on_train == False):
|
|
138
|
+
epoch_cbs.append(cb)
|
|
139
|
+
# for trialwise decoding stuffs it might also be we don't have
|
|
140
|
+
# cropped loader, so no indices there
|
|
141
|
+
if len(epoch_cbs) > 0:
|
|
142
|
+
assert hasattr(self, '_last_window_inds')
|
|
143
|
+
for cb in epoch_cbs:
|
|
144
|
+
cb.window_inds_.append(self._last_window_inds)
|
|
145
|
+
del self._last_window_inds
|
|
146
|
+
|
|
147
|
+
def predict_with_window_inds_and_ys(self, dataset):
|
|
148
|
+
preds = []
|
|
149
|
+
i_window_in_trials = []
|
|
150
|
+
i_window_stops = []
|
|
151
|
+
window_ys = []
|
|
152
|
+
for X, y, i in self.get_iterator(dataset, drop_index=False):
|
|
153
|
+
i_window_in_trials.append(i[0].cpu().numpy())
|
|
154
|
+
i_window_stops.append(i[2].cpu().numpy())
|
|
155
|
+
preds.append(to_numpy(self.forward(X)))
|
|
156
|
+
window_ys.append(y.cpu().numpy())
|
|
157
|
+
preds = np.concatenate(preds)
|
|
158
|
+
i_window_in_trials = np.concatenate(i_window_in_trials)
|
|
159
|
+
i_window_stops = np.concatenate(i_window_stops)
|
|
160
|
+
window_ys = np.concatenate(window_ys)
|
|
161
|
+
return dict(
|
|
162
|
+
preds=preds, i_window_in_trials=i_window_in_trials,
|
|
163
|
+
i_window_stops=i_window_stops, window_ys=window_ys)
|
|
164
|
+
|
|
165
|
+
# Removes default EpochScoring callback computing 'accuracy' to work properly
|
|
166
|
+
# with cropped decoding.
|
|
167
|
+
@property
|
|
168
|
+
def _default_callbacks(self):
|
|
169
|
+
return [
|
|
170
|
+
("epoch_timer", EpochTimer()),
|
|
171
|
+
(
|
|
172
|
+
"train_loss",
|
|
173
|
+
BatchScoring(
|
|
174
|
+
train_loss_score,
|
|
175
|
+
name="train_loss",
|
|
176
|
+
on_train=True,
|
|
177
|
+
target_extractor=noop,
|
|
178
|
+
),
|
|
179
|
+
),
|
|
180
|
+
(
|
|
181
|
+
"valid_loss",
|
|
182
|
+
BatchScoring(
|
|
183
|
+
valid_loss_score, name="valid_loss", target_extractor=noop,
|
|
184
|
+
),
|
|
185
|
+
),
|
|
186
|
+
("print_log", PrintLog()),
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
# Method added to fix `predict_proba` behavior to return proper values
|
|
190
|
+
# in cropped mode even if regressor does not return probabilities.
|
|
191
|
+
# We implement `predict_proba` because it exists in skorch.NeuralNetRegressor.
|
|
192
|
+
def predict_proba(self, X):
|
|
193
|
+
"""Return the output of the module's forward method as a numpy
|
|
194
|
+
array. In case of cropped decoding returns averaged values for
|
|
195
|
+
each trial.
|
|
196
|
+
|
|
197
|
+
If the module's forward method returns multiple outputs as a
|
|
198
|
+
tuple, it is assumed that the first output contains the
|
|
199
|
+
relevant information and the other values are ignored.
|
|
200
|
+
If all values are relevant or module's output for each crop
|
|
201
|
+
is needed, consider using :func:`~skorch.NeuralNet.forward`
|
|
202
|
+
instead.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
X : input data, compatible with skorch.dataset.Dataset
|
|
207
|
+
By default, you should be able to pass:
|
|
208
|
+
|
|
209
|
+
* numpy arrays
|
|
210
|
+
* torch tensors
|
|
211
|
+
* pandas DataFrame or Series
|
|
212
|
+
* scipy sparse CSR matrices
|
|
213
|
+
* a dictionary of the former three
|
|
214
|
+
* a list/tuple of the former three
|
|
215
|
+
* a Dataset
|
|
216
|
+
|
|
217
|
+
If this doesn't work with your data, you have to pass a
|
|
218
|
+
``Dataset`` that can deal with the data.
|
|
219
|
+
|
|
220
|
+
Warnings
|
|
221
|
+
--------
|
|
222
|
+
Regressors predict regression targets, so output of this method
|
|
223
|
+
can't be interpreted as probabilities. We advise you to use
|
|
224
|
+
`predict` method instead of `predict_proba`.
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
y_proba : numpy ndarray
|
|
229
|
+
|
|
230
|
+
"""
|
|
231
|
+
y_pred = super().predict_proba(X)
|
|
232
|
+
# Normally, we have to average the predictions across crops/timesteps
|
|
233
|
+
# to get one prediction per window/trial
|
|
234
|
+
# Predictions may be already averaged in CroppedTrialEpochScoring (y_pred.shape==2).
|
|
235
|
+
# However, when predictions are computed outside of CroppedTrialEpochScoring
|
|
236
|
+
# we have to average predictions, hence the check if len(y_pred.shape) == 3
|
|
237
|
+
if self.cropped and len(y_pred.shape) == 3:
|
|
238
|
+
return y_pred.mean(-1)
|
|
239
|
+
else:
|
|
240
|
+
return y_pred
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sampler classes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Authors: Hubert Banville <hubert.jbanville@gmail.com>
|
|
6
|
+
#
|
|
7
|
+
# License: BSD (3-clause)
|
|
8
|
+
|
|
9
|
+
from torch.utils.data.sampler import Sampler
|
|
10
|
+
from sklearn.utils import check_random_state
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RecordingSampler(Sampler):
|
|
14
|
+
"""Base sampler simplifying sampling from recordings.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
metadata : pd.DataFrame
|
|
19
|
+
DataFrame with at least one of {subject, session, run} columns for each
|
|
20
|
+
window in the BaseConcatDataset to sample examples from. Normally
|
|
21
|
+
obtained with `BaseConcatDataset.get_metadata()`. For instance,
|
|
22
|
+
`metadata.head()` might look like this:
|
|
23
|
+
|
|
24
|
+
i_window_in_trial i_start_in_trial i_stop_in_trial target subject session run
|
|
25
|
+
0 0 0 500 -1 4 session_T run_0
|
|
26
|
+
1 1 500 1000 -1 4 session_T run_0
|
|
27
|
+
2 2 1000 1500 -1 4 session_T run_0
|
|
28
|
+
3 3 1500 2000 -1 4 session_T run_0
|
|
29
|
+
4 4 2000 2500 -1 4 session_T run_0
|
|
30
|
+
|
|
31
|
+
random_state : np.RandomState | int | None
|
|
32
|
+
Random state.
|
|
33
|
+
|
|
34
|
+
Attributes
|
|
35
|
+
----------
|
|
36
|
+
info : pd.DataFrame
|
|
37
|
+
Series with MultiIndex index which contains the subject, session, run
|
|
38
|
+
and window indices information in an easily accessible structure for
|
|
39
|
+
quick sampling of windows.
|
|
40
|
+
"""
|
|
41
|
+
def __init__(self, metadata, random_state=None):
|
|
42
|
+
self.metadata = metadata
|
|
43
|
+
self._init_info()
|
|
44
|
+
self.rng = check_random_state(random_state)
|
|
45
|
+
|
|
46
|
+
def _init_info(self):
|
|
47
|
+
keys = [k for k in ['subject', 'session', 'run']
|
|
48
|
+
if k in self.metadata.columns]
|
|
49
|
+
if not keys:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
'metadata must contain at least one of the following columns: '
|
|
52
|
+
'subject, session or run.')
|
|
53
|
+
|
|
54
|
+
self.metadata = self.metadata.reset_index().rename(
|
|
55
|
+
columns={'index': 'window_index'})
|
|
56
|
+
self.info = self.metadata.reset_index().groupby(keys)[
|
|
57
|
+
['index', 'i_start_in_trial']].agg(['unique'])
|
|
58
|
+
self.info.columns = self.info.columns.get_level_values(0)
|
|
59
|
+
|
|
60
|
+
def sample_recording(self):
|
|
61
|
+
"""Return a random recording index.
|
|
62
|
+
"""
|
|
63
|
+
return self.rng.choice(self.n_recordings)
|
|
64
|
+
|
|
65
|
+
def sample_window(self, rec_ind=None):
|
|
66
|
+
"""Return a specific window.
|
|
67
|
+
"""
|
|
68
|
+
if rec_ind is None:
|
|
69
|
+
rec_ind = self.sample_recording()
|
|
70
|
+
win_ind = self.rng.choice(self.info.iloc[rec_ind]['index'])
|
|
71
|
+
return win_ind, rec_ind
|
|
72
|
+
|
|
73
|
+
def __iter__(self):
|
|
74
|
+
raise NotImplementedError
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def n_recordings(self):
|
|
78
|
+
return self.info.shape[0]
|