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,232 @@
|
|
|
1
|
+
# Authors: Robin Tibor Schirrmeister
|
|
2
|
+
# Tonio Ball
|
|
3
|
+
#
|
|
4
|
+
# License: BSD-3
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
from torch import nn
|
|
10
|
+
from torch.nn import init
|
|
11
|
+
from torch.nn.functional import elu
|
|
12
|
+
|
|
13
|
+
from .functions import transpose_time_to_spat, squeeze_final_output
|
|
14
|
+
from ..util import np_to_var
|
|
15
|
+
from .modules import Expression, AvgPool2dWithConv, Ensure4d
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EEGResNet(nn.Sequential):
|
|
19
|
+
"""
|
|
20
|
+
Residual Network for EEG.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self,
|
|
23
|
+
in_chans,
|
|
24
|
+
n_classes,
|
|
25
|
+
input_window_samples,
|
|
26
|
+
final_pool_length,
|
|
27
|
+
n_first_filters,
|
|
28
|
+
n_layers_per_block=2,
|
|
29
|
+
first_filter_length=3,
|
|
30
|
+
nonlinearity=elu,
|
|
31
|
+
split_first_layer=True,
|
|
32
|
+
batch_norm_alpha=0.1,
|
|
33
|
+
batch_norm_epsilon=1e-4,
|
|
34
|
+
conv_weight_init_fn=lambda w: init.kaiming_normal_(w, a=0)):
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.in_chans = in_chans
|
|
37
|
+
self.n_classes = n_classes
|
|
38
|
+
self.input_window_samples = input_window_samples
|
|
39
|
+
if final_pool_length == 'auto':
|
|
40
|
+
assert input_window_samples is not None
|
|
41
|
+
assert first_filter_length % 2 == 1
|
|
42
|
+
self.final_pool_length = final_pool_length
|
|
43
|
+
self.n_first_filters = n_first_filters
|
|
44
|
+
self.n_layers_per_block = n_layers_per_block
|
|
45
|
+
self.first_filter_length = first_filter_length
|
|
46
|
+
self.nonlinearity = nonlinearity
|
|
47
|
+
self.split_first_layer = split_first_layer
|
|
48
|
+
self.batch_norm_alpha = batch_norm_alpha
|
|
49
|
+
self.batch_norm_epsilon = batch_norm_epsilon
|
|
50
|
+
self.conv_weight_init_fn = conv_weight_init_fn
|
|
51
|
+
|
|
52
|
+
self.add_module("ensuredims", Ensure4d())
|
|
53
|
+
if self.split_first_layer:
|
|
54
|
+
self.add_module('dimshuffle', Expression(transpose_time_to_spat))
|
|
55
|
+
self.add_module('conv_time', nn.Conv2d(1, self.n_first_filters,
|
|
56
|
+
(self.first_filter_length, 1),
|
|
57
|
+
stride=1,
|
|
58
|
+
padding=(self.first_filter_length // 2, 0)))
|
|
59
|
+
self.add_module('conv_spat',
|
|
60
|
+
nn.Conv2d(self.n_first_filters, self.n_first_filters,
|
|
61
|
+
(1, self.in_chans),
|
|
62
|
+
stride=(1, 1),
|
|
63
|
+
bias=False))
|
|
64
|
+
else:
|
|
65
|
+
self.add_module('conv_time',
|
|
66
|
+
nn.Conv2d(self.in_chans, self.n_first_filters,
|
|
67
|
+
(self.first_filter_length, 1),
|
|
68
|
+
stride=(1, 1),
|
|
69
|
+
padding=(self.first_filter_length // 2, 0),
|
|
70
|
+
bias=False,))
|
|
71
|
+
n_filters_conv = self.n_first_filters
|
|
72
|
+
self.add_module('bnorm',
|
|
73
|
+
nn.BatchNorm2d(n_filters_conv,
|
|
74
|
+
momentum=self.batch_norm_alpha,
|
|
75
|
+
affine=True,
|
|
76
|
+
eps=1e-5),)
|
|
77
|
+
self.add_module('conv_nonlin', Expression(self.nonlinearity))
|
|
78
|
+
cur_dilation = np.array([1, 1])
|
|
79
|
+
n_cur_filters = n_filters_conv
|
|
80
|
+
i_block = 1
|
|
81
|
+
for i_layer in range(self.n_layers_per_block):
|
|
82
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, i_layer),
|
|
83
|
+
_ResidualBlock(n_cur_filters, n_cur_filters,
|
|
84
|
+
dilation=cur_dilation))
|
|
85
|
+
i_block += 1
|
|
86
|
+
cur_dilation[0] *= 2
|
|
87
|
+
n_out_filters = int(2 * n_cur_filters)
|
|
88
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, 0),
|
|
89
|
+
_ResidualBlock(n_cur_filters, n_out_filters,
|
|
90
|
+
dilation=cur_dilation,))
|
|
91
|
+
n_cur_filters = n_out_filters
|
|
92
|
+
for i_layer in range(1, self.n_layers_per_block):
|
|
93
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, i_layer),
|
|
94
|
+
_ResidualBlock(n_cur_filters, n_cur_filters,
|
|
95
|
+
dilation=cur_dilation))
|
|
96
|
+
|
|
97
|
+
i_block += 1
|
|
98
|
+
cur_dilation[0] *= 2
|
|
99
|
+
n_out_filters = int(1.5 * n_cur_filters)
|
|
100
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, 0),
|
|
101
|
+
_ResidualBlock(n_cur_filters, n_out_filters,
|
|
102
|
+
dilation=cur_dilation,))
|
|
103
|
+
n_cur_filters = n_out_filters
|
|
104
|
+
for i_layer in range(1, self.n_layers_per_block):
|
|
105
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, i_layer),
|
|
106
|
+
_ResidualBlock(n_cur_filters, n_cur_filters,
|
|
107
|
+
dilation=cur_dilation))
|
|
108
|
+
|
|
109
|
+
i_block += 1
|
|
110
|
+
cur_dilation[0] *= 2
|
|
111
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, 0),
|
|
112
|
+
_ResidualBlock(n_cur_filters, n_cur_filters,
|
|
113
|
+
dilation=cur_dilation,))
|
|
114
|
+
for i_layer in range(1, self.n_layers_per_block):
|
|
115
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, i_layer),
|
|
116
|
+
_ResidualBlock(n_cur_filters, n_cur_filters,
|
|
117
|
+
dilation=cur_dilation))
|
|
118
|
+
|
|
119
|
+
i_block += 1
|
|
120
|
+
cur_dilation[0] *= 2
|
|
121
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, 0),
|
|
122
|
+
_ResidualBlock(n_cur_filters, n_cur_filters,
|
|
123
|
+
dilation=cur_dilation,))
|
|
124
|
+
for i_layer in range(1, self.n_layers_per_block):
|
|
125
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, i_layer),
|
|
126
|
+
_ResidualBlock(n_cur_filters, n_cur_filters,
|
|
127
|
+
dilation=cur_dilation))
|
|
128
|
+
|
|
129
|
+
i_block += 1
|
|
130
|
+
cur_dilation[0] *= 2
|
|
131
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, 0),
|
|
132
|
+
_ResidualBlock(n_cur_filters, n_cur_filters,
|
|
133
|
+
dilation=cur_dilation,))
|
|
134
|
+
for i_layer in range(1, self.n_layers_per_block):
|
|
135
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, i_layer),
|
|
136
|
+
_ResidualBlock(n_cur_filters, n_cur_filters,
|
|
137
|
+
dilation=cur_dilation))
|
|
138
|
+
i_block += 1
|
|
139
|
+
cur_dilation[0] *= 2
|
|
140
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, 0),
|
|
141
|
+
_ResidualBlock(n_cur_filters, n_cur_filters,
|
|
142
|
+
dilation=cur_dilation,))
|
|
143
|
+
for i_layer in range(1, self.n_layers_per_block):
|
|
144
|
+
self.add_module('res_{:d}_{:d}'.format(i_block, i_layer),
|
|
145
|
+
_ResidualBlock(n_cur_filters, n_cur_filters,
|
|
146
|
+
dilation=cur_dilation))
|
|
147
|
+
|
|
148
|
+
self.eval()
|
|
149
|
+
if self.final_pool_length == 'auto':
|
|
150
|
+
self.add_module('mean_pool', nn.AdaptiveAvgPool2d((1, 1)))
|
|
151
|
+
else:
|
|
152
|
+
pool_dilation = int(cur_dilation[0]), int(cur_dilation[1])
|
|
153
|
+
self.add_module('mean_pool', AvgPool2dWithConv(
|
|
154
|
+
(self.final_pool_length, 1), (1, 1),
|
|
155
|
+
dilation=pool_dilation))
|
|
156
|
+
self.add_module('conv_classifier',
|
|
157
|
+
nn.Conv2d(n_cur_filters, self.n_classes,
|
|
158
|
+
(1, 1), bias=True))
|
|
159
|
+
self.add_module('softmax', nn.LogSoftmax(dim=1))
|
|
160
|
+
self.add_module('squeeze', Expression(squeeze_final_output))
|
|
161
|
+
|
|
162
|
+
# Initialize all weights
|
|
163
|
+
self.apply(lambda module: _weights_init(module, self.conv_weight_init_fn))
|
|
164
|
+
|
|
165
|
+
# Start in eval mode
|
|
166
|
+
self.eval()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _weights_init(module, conv_weight_init_fn):
|
|
170
|
+
"""
|
|
171
|
+
initialize weights
|
|
172
|
+
"""
|
|
173
|
+
classname = module.__class__.__name__
|
|
174
|
+
if 'Conv' in classname and classname != "AvgPool2dWithConv":
|
|
175
|
+
conv_weight_init_fn(module.weight)
|
|
176
|
+
if module.bias is not None:
|
|
177
|
+
init.constant_(module.bias, 0)
|
|
178
|
+
elif 'BatchNorm' in classname:
|
|
179
|
+
init.constant_(module.weight, 1)
|
|
180
|
+
init.constant_(module.bias, 0)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class _ResidualBlock(nn.Module):
|
|
184
|
+
"""
|
|
185
|
+
create a residual learning building block with two stacked 3x3 convlayers as in paper
|
|
186
|
+
"""
|
|
187
|
+
def __init__(self, in_filters,
|
|
188
|
+
out_num_filters,
|
|
189
|
+
dilation,
|
|
190
|
+
filter_time_length=3,
|
|
191
|
+
nonlinearity=elu,
|
|
192
|
+
batch_norm_alpha=0.1, batch_norm_epsilon=1e-4):
|
|
193
|
+
super(_ResidualBlock, self).__init__()
|
|
194
|
+
time_padding = int((filter_time_length - 1) * dilation[0])
|
|
195
|
+
assert time_padding % 2 == 0
|
|
196
|
+
time_padding = int(time_padding // 2)
|
|
197
|
+
dilation = (int(dilation[0]), int(dilation[1]))
|
|
198
|
+
assert (out_num_filters - in_filters) % 2 == 0, (
|
|
199
|
+
"Need even number of extra channels in order to be able to "
|
|
200
|
+
"pad correctly")
|
|
201
|
+
self.n_pad_chans = out_num_filters - in_filters
|
|
202
|
+
|
|
203
|
+
self.conv_1 = nn.Conv2d(
|
|
204
|
+
in_filters, out_num_filters, (filter_time_length, 1), stride=(1, 1),
|
|
205
|
+
dilation=dilation,
|
|
206
|
+
padding=(time_padding, 0))
|
|
207
|
+
self.bn1 = nn.BatchNorm2d(
|
|
208
|
+
out_num_filters, momentum=batch_norm_alpha, affine=True,
|
|
209
|
+
eps=batch_norm_epsilon)
|
|
210
|
+
self.conv_2 = nn.Conv2d(
|
|
211
|
+
out_num_filters, out_num_filters, (filter_time_length, 1), stride=(1, 1),
|
|
212
|
+
dilation=dilation,
|
|
213
|
+
padding=(time_padding, 0))
|
|
214
|
+
self.bn2 = nn.BatchNorm2d(
|
|
215
|
+
out_num_filters, momentum=batch_norm_alpha,
|
|
216
|
+
affine=True, eps=batch_norm_epsilon)
|
|
217
|
+
# also see https://mail.google.com/mail/u/0/#search/ilya+joos/1576137dd34c3127
|
|
218
|
+
# for resnet options as ilya used them
|
|
219
|
+
self.nonlinearity = nonlinearity
|
|
220
|
+
|
|
221
|
+
def forward(self, x):
|
|
222
|
+
stack_1 = self.nonlinearity(self.bn1(self.conv_1(x)))
|
|
223
|
+
stack_2 = self.bn2(self.conv_2(stack_1)) # next nonlin after sum
|
|
224
|
+
if self.n_pad_chans != 0:
|
|
225
|
+
zeros_for_padding = torch.autograd.Variable(
|
|
226
|
+
torch.zeros(x.size()[0], self.n_pad_chans // 2,
|
|
227
|
+
x.size()[2], x.size()[3]))
|
|
228
|
+
if x.is_cuda:
|
|
229
|
+
zeros_for_padding = zeros_for_padding.cuda()
|
|
230
|
+
x = torch.cat((zeros_for_padding, x, zeros_for_padding), dim=1)
|
|
231
|
+
out = self.nonlinearity(x + stack_2)
|
|
232
|
+
return out
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def square(x):
|
|
5
|
+
return x * x
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def safe_log(x, eps=1e-6):
|
|
9
|
+
""" Prevents :math:`log(0)` by using :math:`log(max(x, eps))`."""
|
|
10
|
+
return torch.log(torch.clamp(x, min=eps))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def identity(x):
|
|
14
|
+
return x
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def squeeze_final_output(x):
|
|
18
|
+
"""Removes empty dimension at end and potentially removes empty time
|
|
19
|
+
dimension. It does not just use squeeze as we never want to remove
|
|
20
|
+
first dimension.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
x: torch.Tensor
|
|
25
|
+
squeezed tensor
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
assert x.size()[3] == 1
|
|
29
|
+
x = x[:, :, :, 0]
|
|
30
|
+
if x.size()[2] == 1:
|
|
31
|
+
x = x[:, :, 0]
|
|
32
|
+
return x
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def transpose_time_to_spat(x):
|
|
36
|
+
"""Swap time and spatial dimensions.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
x: torch.Tensor
|
|
41
|
+
tensor in which last and first dimensions are swapped
|
|
42
|
+
"""
|
|
43
|
+
return x.permute(0, 3, 2, 1)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch import nn
|
|
3
|
+
from torch.nn import ConstantPad2d
|
|
4
|
+
|
|
5
|
+
from .deep4 import Deep4Net
|
|
6
|
+
from .util import to_dense_prediction_model
|
|
7
|
+
from .shallow_fbcsp import ShallowFBCSPNet
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HybridNet(nn.Module):
|
|
11
|
+
"""Hybrid ConvNet model from [3]_.
|
|
12
|
+
|
|
13
|
+
Very hardcoded at the moment.
|
|
14
|
+
|
|
15
|
+
References
|
|
16
|
+
----------
|
|
17
|
+
|
|
18
|
+
.. [3] Schirrmeister, R. T., Springenberg, J. T., Fiederer, L. D. J.,
|
|
19
|
+
Glasstetter, M., Eggensperger, K., Tangermann, M., Hutter, F. & Ball, T. (2017).
|
|
20
|
+
Deep learning with convolutional neural networks for EEG decoding and
|
|
21
|
+
visualization.
|
|
22
|
+
Human Brain Mapping , Aug. 2017. Online: http://dx.doi.org/10.1002/hbm.23730
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, in_chans, n_classes, input_window_samples):
|
|
26
|
+
super(HybridNet, self).__init__()
|
|
27
|
+
deep_model = Deep4Net(
|
|
28
|
+
in_chans,
|
|
29
|
+
n_classes,
|
|
30
|
+
n_filters_time=20,
|
|
31
|
+
n_filters_spat=30,
|
|
32
|
+
n_filters_2=40,
|
|
33
|
+
n_filters_3=50,
|
|
34
|
+
n_filters_4=60,
|
|
35
|
+
input_window_samples=input_window_samples,
|
|
36
|
+
final_conv_length=2,
|
|
37
|
+
)
|
|
38
|
+
shallow_model = ShallowFBCSPNet(
|
|
39
|
+
in_chans,
|
|
40
|
+
n_classes,
|
|
41
|
+
input_window_samples=input_window_samples,
|
|
42
|
+
n_filters_time=30,
|
|
43
|
+
n_filters_spat=40,
|
|
44
|
+
filter_time_length=28,
|
|
45
|
+
final_conv_length=29,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
reduced_deep_model = nn.Sequential()
|
|
49
|
+
for name, module in deep_model.named_children():
|
|
50
|
+
if name == "conv_classifier":
|
|
51
|
+
new_conv_layer = nn.Conv2d(
|
|
52
|
+
module.in_channels,
|
|
53
|
+
60,
|
|
54
|
+
kernel_size=module.kernel_size,
|
|
55
|
+
stride=module.stride,
|
|
56
|
+
)
|
|
57
|
+
reduced_deep_model.add_module("deep_final_conv", new_conv_layer)
|
|
58
|
+
break
|
|
59
|
+
reduced_deep_model.add_module(name, module)
|
|
60
|
+
|
|
61
|
+
reduced_shallow_model = nn.Sequential()
|
|
62
|
+
for name, module in shallow_model.named_children():
|
|
63
|
+
if name == "conv_classifier":
|
|
64
|
+
new_conv_layer = nn.Conv2d(
|
|
65
|
+
module.in_channels,
|
|
66
|
+
40,
|
|
67
|
+
kernel_size=module.kernel_size,
|
|
68
|
+
stride=module.stride,
|
|
69
|
+
)
|
|
70
|
+
reduced_shallow_model.add_module(
|
|
71
|
+
"shallow_final_conv", new_conv_layer
|
|
72
|
+
)
|
|
73
|
+
break
|
|
74
|
+
reduced_shallow_model.add_module(name, module)
|
|
75
|
+
|
|
76
|
+
to_dense_prediction_model(reduced_deep_model)
|
|
77
|
+
to_dense_prediction_model(reduced_shallow_model)
|
|
78
|
+
self.reduced_deep_model = reduced_deep_model
|
|
79
|
+
self.reduced_shallow_model = reduced_shallow_model
|
|
80
|
+
self.final_conv = nn.Conv2d(
|
|
81
|
+
100, n_classes, kernel_size=(1, 1), stride=1
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def forward(self, x):
|
|
85
|
+
deep_out = self.reduced_deep_model(x)
|
|
86
|
+
shallow_out = self.reduced_shallow_model(x)
|
|
87
|
+
|
|
88
|
+
n_diff_deep_shallow = deep_out.size()[2] - shallow_out.size()[2]
|
|
89
|
+
|
|
90
|
+
if n_diff_deep_shallow < 0:
|
|
91
|
+
deep_out = ConstantPad2d((0, 0, -n_diff_deep_shallow, 0), 0)(
|
|
92
|
+
deep_out
|
|
93
|
+
)
|
|
94
|
+
elif n_diff_deep_shallow > 0:
|
|
95
|
+
shallow_out = ConstantPad2d((0, 0, n_diff_deep_shallow, 0), 0)(
|
|
96
|
+
shallow_out
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
merged_out = torch.cat((deep_out, shallow_out), dim=1)
|
|
100
|
+
linear_out = self.final_conv(merged_out)
|
|
101
|
+
softmaxed = nn.LogSoftmax(dim=1)(linear_out)
|
|
102
|
+
squeezed = softmaxed.squeeze(3)
|
|
103
|
+
return squeezed
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
import torch.nn.functional as F
|
|
5
|
+
|
|
6
|
+
from ..util import np_to_var
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Ensure4d(torch.nn.Module):
|
|
10
|
+
def forward(self, x):
|
|
11
|
+
while(len(x.shape) < 4):
|
|
12
|
+
x = x.unsqueeze(-1)
|
|
13
|
+
return x
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Expression(torch.nn.Module):
|
|
17
|
+
"""Compute given expression on forward pass.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
expression_fn : callable
|
|
22
|
+
Should accept variable number of objects of type
|
|
23
|
+
`torch.autograd.Variable` to compute its output.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, expression_fn):
|
|
27
|
+
super(Expression, self).__init__()
|
|
28
|
+
self.expression_fn = expression_fn
|
|
29
|
+
|
|
30
|
+
def forward(self, *x):
|
|
31
|
+
return self.expression_fn(*x)
|
|
32
|
+
|
|
33
|
+
def __repr__(self):
|
|
34
|
+
if hasattr(self.expression_fn, "func") and hasattr(
|
|
35
|
+
self.expression_fn, "kwargs"
|
|
36
|
+
):
|
|
37
|
+
expression_str = "{:s} {:s}".format(
|
|
38
|
+
self.expression_fn.func.__name__, str(self.expression_fn.kwargs)
|
|
39
|
+
)
|
|
40
|
+
elif hasattr(self.expression_fn, "__name__"):
|
|
41
|
+
expression_str = self.expression_fn.__name__
|
|
42
|
+
else:
|
|
43
|
+
expression_str = repr(self.expression_fn)
|
|
44
|
+
return (
|
|
45
|
+
self.__class__.__name__ +
|
|
46
|
+
"(expression=%s) " % expression_str
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AvgPool2dWithConv(torch.nn.Module):
|
|
51
|
+
"""
|
|
52
|
+
Compute average pooling using a convolution, to have the dilation parameter.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
kernel_size: (int,int)
|
|
57
|
+
Size of the pooling region.
|
|
58
|
+
stride: (int,int)
|
|
59
|
+
Stride of the pooling operation.
|
|
60
|
+
dilation: int or (int,int)
|
|
61
|
+
Dilation applied to the pooling filter.
|
|
62
|
+
padding: int or (int,int)
|
|
63
|
+
Padding applied before the pooling operation.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, kernel_size, stride, dilation=1, padding=0):
|
|
67
|
+
super(AvgPool2dWithConv, self).__init__()
|
|
68
|
+
self.kernel_size = kernel_size
|
|
69
|
+
self.stride = stride
|
|
70
|
+
self.dilation = dilation
|
|
71
|
+
self.padding = padding
|
|
72
|
+
# don't name them "weights" to
|
|
73
|
+
# make sure these are not accidentally used by some procedure
|
|
74
|
+
# that initializes parameters or something
|
|
75
|
+
self._pool_weights = None
|
|
76
|
+
|
|
77
|
+
def forward(self, x):
|
|
78
|
+
# Create weights for the convolution on demand:
|
|
79
|
+
# size or type of x changed...
|
|
80
|
+
in_channels = x.size()[1]
|
|
81
|
+
weight_shape = (
|
|
82
|
+
in_channels,
|
|
83
|
+
1,
|
|
84
|
+
self.kernel_size[0],
|
|
85
|
+
self.kernel_size[1],
|
|
86
|
+
)
|
|
87
|
+
if self._pool_weights is None or (
|
|
88
|
+
(tuple(self._pool_weights.size()) != tuple(weight_shape)) or
|
|
89
|
+
(self._pool_weights.is_cuda != x.is_cuda) or
|
|
90
|
+
(self._pool_weights.data.type() != x.data.type())
|
|
91
|
+
):
|
|
92
|
+
n_pool = np.prod(self.kernel_size)
|
|
93
|
+
weights = np_to_var(
|
|
94
|
+
np.ones(weight_shape, dtype=np.float32) / float(n_pool)
|
|
95
|
+
)
|
|
96
|
+
weights = weights.type_as(x)
|
|
97
|
+
if x.is_cuda:
|
|
98
|
+
weights = weights.cuda()
|
|
99
|
+
self._pool_weights = weights
|
|
100
|
+
|
|
101
|
+
pooled = F.conv2d(
|
|
102
|
+
x,
|
|
103
|
+
self._pool_weights,
|
|
104
|
+
bias=None,
|
|
105
|
+
stride=self.stride,
|
|
106
|
+
dilation=self.dilation,
|
|
107
|
+
padding=self.padding,
|
|
108
|
+
groups=in_channels,
|
|
109
|
+
)
|
|
110
|
+
return pooled
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class IntermediateOutputWrapper(torch.nn.Module):
|
|
114
|
+
"""Wraps network model such that outputs of intermediate layers can be returned.
|
|
115
|
+
forward() returns list of intermediate activations in a network during forward pass.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
to_select : list
|
|
120
|
+
list of module names for which activation should be returned
|
|
121
|
+
model : model object
|
|
122
|
+
network model
|
|
123
|
+
|
|
124
|
+
Examples
|
|
125
|
+
--------
|
|
126
|
+
>>> model = Deep4Net()
|
|
127
|
+
>>> select_modules = ['conv_spat','conv_2','conv_3','conv_4'] # Specify intermediate outputs
|
|
128
|
+
>>> model_pert = IntermediateOutputWrapper(select_modules,model) # Wrap model
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(self, to_select, model):
|
|
132
|
+
if not len(list(model.children())) == len(list(model.named_children())):
|
|
133
|
+
raise Exception("All modules in model need to have names!")
|
|
134
|
+
|
|
135
|
+
super(IntermediateOutputWrapper, self).__init__()
|
|
136
|
+
|
|
137
|
+
modules_list = model.named_children()
|
|
138
|
+
for key, module in modules_list:
|
|
139
|
+
self.add_module(key, module)
|
|
140
|
+
self._modules[key].load_state_dict(module.state_dict())
|
|
141
|
+
self._to_select = to_select
|
|
142
|
+
|
|
143
|
+
def forward(self, x):
|
|
144
|
+
# Call modules individually and append activation to output if module is in to_select
|
|
145
|
+
o = []
|
|
146
|
+
for name, module in self._modules.items():
|
|
147
|
+
x = module(x)
|
|
148
|
+
if name in self._to_select:
|
|
149
|
+
o.append(x)
|
|
150
|
+
return o
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from torch import nn
|
|
3
|
+
from torch.nn import init
|
|
4
|
+
|
|
5
|
+
from ..util import np_to_var
|
|
6
|
+
from .modules import Expression, Ensure4d
|
|
7
|
+
from .functions import (
|
|
8
|
+
safe_log, square, transpose_time_to_spat, squeeze_final_output
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ShallowFBCSPNet(nn.Sequential):
|
|
13
|
+
"""Shallow ConvNet model from [2]_.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
in_chans : int
|
|
18
|
+
XXX
|
|
19
|
+
|
|
20
|
+
References
|
|
21
|
+
----------
|
|
22
|
+
.. [2] Schirrmeister, R. T., Springenberg, J. T., Fiederer, L. D. J.,
|
|
23
|
+
Glasstetter, M., Eggensperger, K., Tangermann, M., Hutter, F. & Ball, T. (2017).
|
|
24
|
+
Deep learning with convolutional neural networks for EEG decoding and
|
|
25
|
+
visualization.
|
|
26
|
+
Human Brain Mapping , Aug. 2017. Online: http://dx.doi.org/10.1002/hbm.23730
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
in_chans,
|
|
32
|
+
n_classes,
|
|
33
|
+
input_window_samples=None,
|
|
34
|
+
n_filters_time=40,
|
|
35
|
+
filter_time_length=25,
|
|
36
|
+
n_filters_spat=40,
|
|
37
|
+
pool_time_length=75,
|
|
38
|
+
pool_time_stride=15,
|
|
39
|
+
final_conv_length=30,
|
|
40
|
+
conv_nonlin=square,
|
|
41
|
+
pool_mode="mean",
|
|
42
|
+
pool_nonlin=safe_log,
|
|
43
|
+
split_first_layer=True,
|
|
44
|
+
batch_norm=True,
|
|
45
|
+
batch_norm_alpha=0.1,
|
|
46
|
+
drop_prob=0.5,
|
|
47
|
+
):
|
|
48
|
+
super().__init__()
|
|
49
|
+
if final_conv_length == "auto":
|
|
50
|
+
assert input_window_samples is not None
|
|
51
|
+
self.in_chans = in_chans
|
|
52
|
+
self.n_classes = n_classes
|
|
53
|
+
self.input_window_samples = input_window_samples
|
|
54
|
+
self.n_filters_time = n_filters_time
|
|
55
|
+
self.filter_time_length = filter_time_length
|
|
56
|
+
self.n_filters_spat = n_filters_spat
|
|
57
|
+
self.pool_time_length = pool_time_length
|
|
58
|
+
self.pool_time_stride = pool_time_stride
|
|
59
|
+
self.final_conv_length = final_conv_length
|
|
60
|
+
self.conv_nonlin = conv_nonlin
|
|
61
|
+
self.pool_mode = pool_mode
|
|
62
|
+
self.pool_nonlin = pool_nonlin
|
|
63
|
+
self.split_first_layer = split_first_layer
|
|
64
|
+
self.batch_norm = batch_norm
|
|
65
|
+
self.batch_norm_alpha = batch_norm_alpha
|
|
66
|
+
self.drop_prob = drop_prob
|
|
67
|
+
|
|
68
|
+
self.add_module("ensuredims", Ensure4d())
|
|
69
|
+
pool_class = dict(max=nn.MaxPool2d, mean=nn.AvgPool2d)[self.pool_mode]
|
|
70
|
+
if self.split_first_layer:
|
|
71
|
+
self.add_module("dimshuffle", Expression(transpose_time_to_spat))
|
|
72
|
+
self.add_module(
|
|
73
|
+
"conv_time",
|
|
74
|
+
nn.Conv2d(
|
|
75
|
+
1,
|
|
76
|
+
self.n_filters_time,
|
|
77
|
+
(self.filter_time_length, 1),
|
|
78
|
+
stride=1,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
self.add_module(
|
|
82
|
+
"conv_spat",
|
|
83
|
+
nn.Conv2d(
|
|
84
|
+
self.n_filters_time,
|
|
85
|
+
self.n_filters_spat,
|
|
86
|
+
(1, self.in_chans),
|
|
87
|
+
stride=1,
|
|
88
|
+
bias=not self.batch_norm,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
n_filters_conv = self.n_filters_spat
|
|
92
|
+
else:
|
|
93
|
+
self.add_module(
|
|
94
|
+
"conv_time",
|
|
95
|
+
nn.Conv2d(
|
|
96
|
+
self.in_chans,
|
|
97
|
+
self.n_filters_time,
|
|
98
|
+
(self.filter_time_length, 1),
|
|
99
|
+
stride=1,
|
|
100
|
+
bias=not self.batch_norm,
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
n_filters_conv = self.n_filters_time
|
|
104
|
+
if self.batch_norm:
|
|
105
|
+
self.add_module(
|
|
106
|
+
"bnorm",
|
|
107
|
+
nn.BatchNorm2d(
|
|
108
|
+
n_filters_conv, momentum=self.batch_norm_alpha, affine=True
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
self.add_module("conv_nonlin_exp", Expression(self.conv_nonlin))
|
|
112
|
+
self.add_module(
|
|
113
|
+
"pool",
|
|
114
|
+
pool_class(
|
|
115
|
+
kernel_size=(self.pool_time_length, 1),
|
|
116
|
+
stride=(self.pool_time_stride, 1),
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
self.add_module("pool_nonlin_exp", Expression(self.pool_nonlin))
|
|
120
|
+
self.add_module("drop", nn.Dropout(p=self.drop_prob))
|
|
121
|
+
self.eval()
|
|
122
|
+
if self.final_conv_length == "auto":
|
|
123
|
+
out = self(
|
|
124
|
+
np_to_var(
|
|
125
|
+
np.ones(
|
|
126
|
+
(1, self.in_chans, self.input_window_samples, 1),
|
|
127
|
+
dtype=np.float32,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
n_out_time = out.cpu().data.numpy().shape[2]
|
|
132
|
+
self.final_conv_length = n_out_time
|
|
133
|
+
self.add_module(
|
|
134
|
+
"conv_classifier",
|
|
135
|
+
nn.Conv2d(
|
|
136
|
+
n_filters_conv,
|
|
137
|
+
self.n_classes,
|
|
138
|
+
(self.final_conv_length, 1),
|
|
139
|
+
bias=True,
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
self.add_module("softmax", nn.LogSoftmax(dim=1))
|
|
143
|
+
self.add_module("squeeze", Expression(squeeze_final_output))
|
|
144
|
+
|
|
145
|
+
# Initialization, xavier is same as in paper...
|
|
146
|
+
init.xavier_uniform_(self.conv_time.weight, gain=1)
|
|
147
|
+
# maybe no bias in case of no split layer and batch norm
|
|
148
|
+
if self.split_first_layer or (not self.batch_norm):
|
|
149
|
+
init.constant_(self.conv_time.bias, 0)
|
|
150
|
+
if self.split_first_layer:
|
|
151
|
+
init.xavier_uniform_(self.conv_spat.weight, gain=1)
|
|
152
|
+
if not self.batch_norm:
|
|
153
|
+
init.constant_(self.conv_spat.bias, 0)
|
|
154
|
+
if self.batch_norm:
|
|
155
|
+
init.constant_(self.bnorm.weight, 1)
|
|
156
|
+
init.constant_(self.bnorm.bias, 0)
|
|
157
|
+
init.xavier_uniform_(self.conv_classifier.weight, gain=1)
|
|
158
|
+
init.constant_(self.conv_classifier.bias, 0)
|