rapidtide 3.0.10__py3-none-any.whl → 3.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.
- rapidtide/Colortables.py +492 -27
- rapidtide/OrthoImageItem.py +1053 -47
- rapidtide/RapidtideDataset.py +1533 -86
- rapidtide/_version.py +3 -3
- rapidtide/calccoherence.py +196 -29
- rapidtide/calcnullsimfunc.py +191 -40
- rapidtide/calcsimfunc.py +245 -42
- rapidtide/correlate.py +1210 -393
- rapidtide/data/examples/src/testLD +56 -0
- rapidtide/data/examples/src/testalign +1 -1
- rapidtide/data/examples/src/testdelayvar +0 -1
- rapidtide/data/examples/src/testfmri +19 -1
- rapidtide/data/examples/src/testglmfilt +5 -5
- rapidtide/data/examples/src/testhappy +30 -1
- rapidtide/data/examples/src/testppgproc +17 -0
- rapidtide/data/examples/src/testrolloff +11 -0
- rapidtide/data/models/model_cnn_pytorch/best_model.pth +0 -0
- rapidtide/data/models/model_cnn_pytorch/loss.png +0 -0
- rapidtide/data/models/model_cnn_pytorch/loss.txt +1 -0
- rapidtide/data/models/model_cnn_pytorch/model.pth +0 -0
- rapidtide/data/models/model_cnn_pytorch/model_meta.json +68 -0
- rapidtide/data/reference/JHU-ArterialTerritoriesNoVent-LVL1_space-MNI152NLin2009cAsym_2mm.nii.gz +0 -0
- rapidtide/data/reference/JHU-ArterialTerritoriesNoVent-LVL1_space-MNI152NLin2009cAsym_2mm_mask.nii.gz +0 -0
- rapidtide/decorators.py +91 -0
- rapidtide/dlfilter.py +2225 -108
- rapidtide/dlfiltertorch.py +4843 -0
- rapidtide/externaltools.py +327 -12
- rapidtide/fMRIData_class.py +79 -40
- rapidtide/filter.py +1899 -810
- rapidtide/fit.py +2004 -574
- rapidtide/genericmultiproc.py +93 -18
- rapidtide/happy_supportfuncs.py +2044 -171
- rapidtide/helper_classes.py +584 -43
- rapidtide/io.py +2363 -370
- rapidtide/linfitfiltpass.py +341 -75
- rapidtide/makelaggedtcs.py +211 -20
- rapidtide/maskutil.py +423 -53
- rapidtide/miscmath.py +827 -121
- rapidtide/multiproc.py +210 -22
- rapidtide/patchmatch.py +234 -33
- rapidtide/peakeval.py +32 -30
- rapidtide/ppgproc.py +2203 -0
- rapidtide/qualitycheck.py +352 -39
- rapidtide/refinedelay.py +422 -57
- rapidtide/refineregressor.py +498 -184
- rapidtide/resample.py +671 -185
- rapidtide/scripts/applyppgproc.py +28 -0
- rapidtide/simFuncClasses.py +1052 -77
- rapidtide/simfuncfit.py +260 -46
- rapidtide/stats.py +540 -238
- rapidtide/tests/happycomp +9 -0
- rapidtide/tests/test_dlfiltertorch.py +627 -0
- rapidtide/tests/test_findmaxlag.py +24 -8
- rapidtide/tests/test_fullrunhappy_v1.py +0 -2
- rapidtide/tests/test_fullrunhappy_v2.py +0 -2
- rapidtide/tests/test_fullrunhappy_v3.py +1 -0
- rapidtide/tests/test_fullrunhappy_v4.py +2 -2
- rapidtide/tests/test_fullrunrapidtide_v7.py +1 -1
- rapidtide/tests/test_simroundtrip.py +8 -8
- rapidtide/tests/utils.py +9 -8
- rapidtide/tidepoolTemplate.py +142 -38
- rapidtide/tidepoolTemplate_alt.py +165 -44
- rapidtide/tidepoolTemplate_big.py +189 -52
- rapidtide/util.py +1217 -118
- rapidtide/voxelData.py +684 -37
- rapidtide/wiener.py +19 -12
- rapidtide/wiener2.py +113 -7
- rapidtide/wiener_doc.py +255 -0
- rapidtide/workflows/adjustoffset.py +105 -3
- rapidtide/workflows/aligntcs.py +85 -2
- rapidtide/workflows/applydlfilter.py +87 -10
- rapidtide/workflows/applyppgproc.py +522 -0
- rapidtide/workflows/atlasaverage.py +210 -47
- rapidtide/workflows/atlastool.py +100 -3
- rapidtide/workflows/calcSimFuncMap.py +294 -64
- rapidtide/workflows/calctexticc.py +201 -9
- rapidtide/workflows/ccorrica.py +97 -4
- rapidtide/workflows/cleanregressor.py +168 -29
- rapidtide/workflows/delayvar.py +163 -10
- rapidtide/workflows/diffrois.py +81 -3
- rapidtide/workflows/endtidalproc.py +144 -4
- rapidtide/workflows/fdica.py +195 -15
- rapidtide/workflows/filtnifti.py +70 -3
- rapidtide/workflows/filttc.py +74 -3
- rapidtide/workflows/fitSimFuncMap.py +206 -48
- rapidtide/workflows/fixtr.py +73 -3
- rapidtide/workflows/gmscalc.py +113 -3
- rapidtide/workflows/happy.py +813 -201
- rapidtide/workflows/happy2std.py +144 -12
- rapidtide/workflows/happy_parser.py +149 -8
- rapidtide/workflows/histnifti.py +118 -2
- rapidtide/workflows/histtc.py +84 -3
- rapidtide/workflows/linfitfilt.py +117 -4
- rapidtide/workflows/localflow.py +328 -28
- rapidtide/workflows/mergequality.py +79 -3
- rapidtide/workflows/niftidecomp.py +322 -18
- rapidtide/workflows/niftistats.py +174 -4
- rapidtide/workflows/pairproc.py +88 -2
- rapidtide/workflows/pairwisemergenifti.py +85 -2
- rapidtide/workflows/parser_funcs.py +1421 -40
- rapidtide/workflows/physiofreq.py +137 -11
- rapidtide/workflows/pixelcomp.py +208 -5
- rapidtide/workflows/plethquality.py +103 -21
- rapidtide/workflows/polyfitim.py +151 -11
- rapidtide/workflows/proj2flow.py +75 -2
- rapidtide/workflows/rankimage.py +111 -4
- rapidtide/workflows/rapidtide.py +272 -15
- rapidtide/workflows/rapidtide2std.py +98 -2
- rapidtide/workflows/rapidtide_parser.py +109 -9
- rapidtide/workflows/refineDelayMap.py +143 -33
- rapidtide/workflows/refineRegressor.py +682 -93
- rapidtide/workflows/regressfrommaps.py +152 -31
- rapidtide/workflows/resamplenifti.py +85 -3
- rapidtide/workflows/resampletc.py +91 -3
- rapidtide/workflows/retrolagtcs.py +98 -6
- rapidtide/workflows/retroregress.py +165 -9
- rapidtide/workflows/roisummarize.py +173 -5
- rapidtide/workflows/runqualitycheck.py +71 -3
- rapidtide/workflows/showarbcorr.py +147 -4
- rapidtide/workflows/showhist.py +86 -2
- rapidtide/workflows/showstxcorr.py +160 -3
- rapidtide/workflows/showtc.py +159 -3
- rapidtide/workflows/showxcorrx.py +184 -4
- rapidtide/workflows/showxy.py +185 -15
- rapidtide/workflows/simdata.py +262 -36
- rapidtide/workflows/spatialfit.py +77 -2
- rapidtide/workflows/spatialmi.py +251 -27
- rapidtide/workflows/spectrogram.py +305 -32
- rapidtide/workflows/synthASL.py +154 -3
- rapidtide/workflows/tcfrom2col.py +76 -2
- rapidtide/workflows/tcfrom3col.py +74 -2
- rapidtide/workflows/tidepool.py +2972 -133
- rapidtide/workflows/utils.py +19 -14
- rapidtide/workflows/utils_doc.py +293 -0
- rapidtide/workflows/variabilityizer.py +116 -3
- {rapidtide-3.0.10.dist-info → rapidtide-3.1.dist-info}/METADATA +10 -9
- {rapidtide-3.0.10.dist-info → rapidtide-3.1.dist-info}/RECORD +141 -122
- {rapidtide-3.0.10.dist-info → rapidtide-3.1.dist-info}/entry_points.txt +1 -0
- {rapidtide-3.0.10.dist-info → rapidtide-3.1.dist-info}/WHEEL +0 -0
- {rapidtide-3.0.10.dist-info → rapidtide-3.1.dist-info}/licenses/LICENSE +0 -0
- {rapidtide-3.0.10.dist-info → rapidtide-3.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,4843 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# Copyright 2016-2025 Blaise Frederick
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
#
|
|
18
|
+
#
|
|
19
|
+
import glob
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import warnings
|
|
24
|
+
|
|
25
|
+
import matplotlib as mpl
|
|
26
|
+
import matplotlib.pyplot as plt
|
|
27
|
+
import numpy as np
|
|
28
|
+
import tqdm
|
|
29
|
+
|
|
30
|
+
with warnings.catch_warnings():
|
|
31
|
+
warnings.simplefilter("ignore")
|
|
32
|
+
try:
|
|
33
|
+
import pyfftw
|
|
34
|
+
except ImportError:
|
|
35
|
+
pyfftwpresent = False
|
|
36
|
+
else:
|
|
37
|
+
pyfftwpresent = True
|
|
38
|
+
|
|
39
|
+
from scipy import fftpack
|
|
40
|
+
from statsmodels.robust.scale import mad
|
|
41
|
+
|
|
42
|
+
if pyfftwpresent:
|
|
43
|
+
fftpack = pyfftw.interfaces.scipy_fftpack
|
|
44
|
+
pyfftw.interfaces.cache.enable()
|
|
45
|
+
|
|
46
|
+
import torch
|
|
47
|
+
import torch.nn as nn
|
|
48
|
+
import torch.optim as optim
|
|
49
|
+
from torch.utils.data import DataLoader, TensorDataset
|
|
50
|
+
|
|
51
|
+
import rapidtide.io as tide_io
|
|
52
|
+
|
|
53
|
+
LGR = logging.getLogger("GENERAL")
|
|
54
|
+
LGR.debug("setting backend to Agg")
|
|
55
|
+
mpl.use("Agg")
|
|
56
|
+
|
|
57
|
+
# Disable GPU if desired
|
|
58
|
+
if torch.cuda.is_available():
|
|
59
|
+
device = torch.device("cuda")
|
|
60
|
+
LGR.info(f"Using CUDA device: {torch.cuda.get_device_name(0)}")
|
|
61
|
+
elif torch.backends.mps.is_available():
|
|
62
|
+
device = torch.device("mps")
|
|
63
|
+
LGR.info(f"Using MPS device")
|
|
64
|
+
else:
|
|
65
|
+
device = torch.device("cpu")
|
|
66
|
+
LGR.info("Using CPU")
|
|
67
|
+
|
|
68
|
+
LGR.debug(f"pytorch version: >>>{torch.__version__}<<<")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DeepLearningFilter:
|
|
72
|
+
"""Base class for deep learning filter"""
|
|
73
|
+
|
|
74
|
+
thesuffix = "sliceres"
|
|
75
|
+
thedatadir = "/Users/frederic/Documents/MR_data/physioconn/timecourses"
|
|
76
|
+
inputfrag = "abc"
|
|
77
|
+
targetfrag = "xyz"
|
|
78
|
+
namesuffix = None
|
|
79
|
+
modelroot = "."
|
|
80
|
+
excludethresh = 4.0
|
|
81
|
+
modelname = None
|
|
82
|
+
intermediatemodelpath = None
|
|
83
|
+
usebadpts = False
|
|
84
|
+
activation = "tanh"
|
|
85
|
+
dofft = False
|
|
86
|
+
readlim = None
|
|
87
|
+
countlim = None
|
|
88
|
+
lossfilename = None
|
|
89
|
+
train_x = None
|
|
90
|
+
train_y = None
|
|
91
|
+
val_x = None
|
|
92
|
+
val_y = None
|
|
93
|
+
model = None
|
|
94
|
+
modelpath = None
|
|
95
|
+
inputsize = None
|
|
96
|
+
infodict = {}
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
window_size: int = 128,
|
|
101
|
+
num_layers: int = 5,
|
|
102
|
+
dropout_rate: float = 0.3,
|
|
103
|
+
num_pretrain_epochs: int = 0,
|
|
104
|
+
num_epochs: int = 1,
|
|
105
|
+
activation: str = "relu",
|
|
106
|
+
modelroot: str = ".",
|
|
107
|
+
dofft: bool = False,
|
|
108
|
+
excludethresh: float = 4.0,
|
|
109
|
+
usebadpts: bool = False,
|
|
110
|
+
thesuffix: str = "25.0Hz",
|
|
111
|
+
modelpath: str = ".",
|
|
112
|
+
thedatadir: str = "/Users/frederic/Documents/MR_data/physioconn/timecourses",
|
|
113
|
+
inputfrag: str = "abc",
|
|
114
|
+
targetfrag: str = "xyz",
|
|
115
|
+
corrthresh: float = 0.5,
|
|
116
|
+
excludebysubject: bool = True,
|
|
117
|
+
startskip: int = 200,
|
|
118
|
+
endskip: int = 200,
|
|
119
|
+
step: int = 1,
|
|
120
|
+
namesuffix: str | None = None,
|
|
121
|
+
readlim: int | None = None,
|
|
122
|
+
readskip: int | None = None,
|
|
123
|
+
countlim: int | None = None,
|
|
124
|
+
**kwargs,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Initialize the DeepLearningFilter with specified parameters.
|
|
128
|
+
|
|
129
|
+
This constructor sets up the configuration for a deep learning model used
|
|
130
|
+
for filtering physiological timecourses. It initializes various hyperparameters,
|
|
131
|
+
paths, and flags that control the behavior of the model and data processing.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
window_size : int, optional
|
|
136
|
+
Size of the sliding window used for processing time series data. Default is 128.
|
|
137
|
+
num_layers : int, optional
|
|
138
|
+
Number of layers in the neural network model. Default is 5.
|
|
139
|
+
dropout_rate : float, optional
|
|
140
|
+
Dropout rate for regularization during training. Default is 0.3.
|
|
141
|
+
num_pretrain_epochs : int, optional
|
|
142
|
+
Number of pre-training epochs. Default is 0.
|
|
143
|
+
num_epochs : int, optional
|
|
144
|
+
Number of training epochs. Default is 1.
|
|
145
|
+
activation : str, optional
|
|
146
|
+
Activation function to use in the model. Default is "relu".
|
|
147
|
+
modelroot : str, optional
|
|
148
|
+
Root directory for model storage. Default is ".".
|
|
149
|
+
dofft : bool, optional
|
|
150
|
+
Whether to apply FFT transformation to input data. Default is False.
|
|
151
|
+
excludethresh : float, optional
|
|
152
|
+
Threshold for excluding data points based on correlation. Default is 4.0.
|
|
153
|
+
usebadpts : bool, optional
|
|
154
|
+
Whether to include bad points in the input. Default is False.
|
|
155
|
+
thesuffix : str, optional
|
|
156
|
+
Suffix to append to filenames. Default is "25.0Hz".
|
|
157
|
+
modelpath : str, optional
|
|
158
|
+
Path to save or load the model. Default is ".".
|
|
159
|
+
thedatadir : str, optional
|
|
160
|
+
Directory containing the physiological data files. Default is
|
|
161
|
+
"/Users/frederic/Documents/MR_data/physioconn/timecourses".
|
|
162
|
+
inputfrag : str, optional
|
|
163
|
+
Fragment identifier for input data. Default is "abc".
|
|
164
|
+
targetfrag : str, optional
|
|
165
|
+
Fragment identifier for target data. Default is "xyz".
|
|
166
|
+
corrthresh : float, optional
|
|
167
|
+
Correlation threshold for filtering. Default is 0.5.
|
|
168
|
+
excludebysubject : bool, optional
|
|
169
|
+
Whether to exclude data by subject. Default is True.
|
|
170
|
+
startskip : int, optional
|
|
171
|
+
Number of samples to skip at the beginning of each timecourse. Default is 200.
|
|
172
|
+
endskip : int, optional
|
|
173
|
+
Number of samples to skip at the end of each timecourse. Default is 200.
|
|
174
|
+
step : int, optional
|
|
175
|
+
Step size for sliding window. Default is 1.
|
|
176
|
+
namesuffix : str, optional
|
|
177
|
+
Suffix to append to model name. Default is None.
|
|
178
|
+
readlim : int, optional
|
|
179
|
+
Limit on number of samples to read. Default is None.
|
|
180
|
+
readskip : int, optional
|
|
181
|
+
Number of samples to skip when reading data. Default is None.
|
|
182
|
+
countlim : int, optional
|
|
183
|
+
Limit on number of timecourses to process. Default is None.
|
|
184
|
+
**kwargs
|
|
185
|
+
Additional keyword arguments passed to the parent class.
|
|
186
|
+
|
|
187
|
+
Notes
|
|
188
|
+
-----
|
|
189
|
+
The `inputsize` is dynamically set based on the `usebadpts` flag:
|
|
190
|
+
- If `usebadpts` is True, input size is 2.
|
|
191
|
+
- Otherwise, input size is 1.
|
|
192
|
+
|
|
193
|
+
Examples
|
|
194
|
+
--------
|
|
195
|
+
>>> filter = DeepLearningFilter(
|
|
196
|
+
... window_size=256,
|
|
197
|
+
... num_layers=6,
|
|
198
|
+
... dropout_rate=0.2,
|
|
199
|
+
... modelroot="/models",
|
|
200
|
+
... dofft=True
|
|
201
|
+
... )
|
|
202
|
+
"""
|
|
203
|
+
self.window_size = window_size
|
|
204
|
+
self.dropout_rate = dropout_rate
|
|
205
|
+
self.num_pretrain_epochs = num_pretrain_epochs
|
|
206
|
+
self.num_epochs = num_epochs
|
|
207
|
+
self.usebadpts = usebadpts
|
|
208
|
+
self.num_layers = num_layers
|
|
209
|
+
if self.usebadpts:
|
|
210
|
+
self.inputsize = 2
|
|
211
|
+
else:
|
|
212
|
+
self.inputsize = 1
|
|
213
|
+
self.activation = activation
|
|
214
|
+
self.modelroot = modelroot
|
|
215
|
+
self.dofft = dofft
|
|
216
|
+
self.thesuffix = thesuffix
|
|
217
|
+
self.thedatadir = thedatadir
|
|
218
|
+
self.modelpath = modelpath
|
|
219
|
+
LGR.info(f"modeldir from DeepLearningFilter: {self.modelpath}")
|
|
220
|
+
self.corrthresh = corrthresh
|
|
221
|
+
self.excludethresh = excludethresh
|
|
222
|
+
self.readlim = readlim
|
|
223
|
+
self.readskip = readskip
|
|
224
|
+
self.countlim = countlim
|
|
225
|
+
self.model = None
|
|
226
|
+
self.initialized = False
|
|
227
|
+
self.trained = False
|
|
228
|
+
self.usetensorboard = False
|
|
229
|
+
self.inputfrag = inputfrag
|
|
230
|
+
self.targetfrag = targetfrag
|
|
231
|
+
self.namesuffix = namesuffix
|
|
232
|
+
self.startskip = startskip
|
|
233
|
+
self.endskip = endskip
|
|
234
|
+
self.step = step
|
|
235
|
+
self.excludebysubject = excludebysubject
|
|
236
|
+
self.device = device
|
|
237
|
+
|
|
238
|
+
# populate infodict
|
|
239
|
+
self.infodict["window_size"] = self.window_size
|
|
240
|
+
self.infodict["usebadpts"] = self.usebadpts
|
|
241
|
+
self.infodict["dofft"] = self.dofft
|
|
242
|
+
self.infodict["corrthresh"] = self.corrthresh
|
|
243
|
+
self.infodict["excludethresh"] = self.excludethresh
|
|
244
|
+
self.infodict["num_pretrain_epochs"] = self.num_pretrain_epochs
|
|
245
|
+
self.infodict["num_epochs"] = self.num_epochs
|
|
246
|
+
self.infodict["modelname"] = self.modelname
|
|
247
|
+
self.infodict["dropout_rate"] = self.dropout_rate
|
|
248
|
+
self.infodict["startskip"] = self.startskip
|
|
249
|
+
self.infodict["endskip"] = self.endskip
|
|
250
|
+
self.infodict["step"] = self.step
|
|
251
|
+
self.infodict["train_arch"] = sys.platform
|
|
252
|
+
|
|
253
|
+
def loaddata(self) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Load and preprocess data for training and validation.
|
|
256
|
+
|
|
257
|
+
This method initializes the data loading process by calling the `prep` function
|
|
258
|
+
with a set of parameters derived from the instance attributes. It handles both
|
|
259
|
+
FFT and non-FFT modes of data preprocessing. The loaded data is stored in
|
|
260
|
+
instance variables for use in subsequent training steps.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
self : object
|
|
265
|
+
The instance of the class containing the following attributes:
|
|
266
|
+
- initialized : bool
|
|
267
|
+
Indicates whether the model has been initialized.
|
|
268
|
+
- dofft : bool
|
|
269
|
+
Whether to apply FFT transformation to the data.
|
|
270
|
+
- window_size : int
|
|
271
|
+
Size of the sliding window used for data segmentation.
|
|
272
|
+
- thesuffix : str
|
|
273
|
+
Suffix to append to filenames when reading data.
|
|
274
|
+
- thedatadir : str
|
|
275
|
+
Directory path where the data files are located.
|
|
276
|
+
- inputfrag : str
|
|
277
|
+
Fragment identifier for input data.
|
|
278
|
+
- targetfrag : str
|
|
279
|
+
Fragment identifier for target data.
|
|
280
|
+
- startskip : int
|
|
281
|
+
Number of samples to skip at the beginning of each file.
|
|
282
|
+
- endskip : int
|
|
283
|
+
Number of samples to skip at the end of each file.
|
|
284
|
+
- corrthresh : float
|
|
285
|
+
Correlation threshold for filtering data.
|
|
286
|
+
- step : int
|
|
287
|
+
Step size for sliding window.
|
|
288
|
+
- usebadpts : bool
|
|
289
|
+
Whether to include bad points in the data.
|
|
290
|
+
- excludethresh : float
|
|
291
|
+
Threshold for excluding data points.
|
|
292
|
+
- excludebysubject : bool
|
|
293
|
+
Whether to exclude data by subject.
|
|
294
|
+
- readlim : int
|
|
295
|
+
Limit on the number of samples to read.
|
|
296
|
+
- readskip : int
|
|
297
|
+
Number of samples to skip while reading.
|
|
298
|
+
- countlim : int
|
|
299
|
+
Limit on the number of data points to process.
|
|
300
|
+
|
|
301
|
+
Returns
|
|
302
|
+
-------
|
|
303
|
+
None
|
|
304
|
+
This method does not return any value. It modifies the instance attributes
|
|
305
|
+
in place.
|
|
306
|
+
|
|
307
|
+
Raises
|
|
308
|
+
------
|
|
309
|
+
Exception
|
|
310
|
+
If the model is not initialized prior to calling this method.
|
|
311
|
+
|
|
312
|
+
Notes
|
|
313
|
+
-----
|
|
314
|
+
The method assigns the following attributes to the instance after loading:
|
|
315
|
+
- train_x : array-like
|
|
316
|
+
Training input data.
|
|
317
|
+
- train_y : array-like
|
|
318
|
+
Training target data.
|
|
319
|
+
- val_x : array-like
|
|
320
|
+
Validation input data.
|
|
321
|
+
- val_y : array-like
|
|
322
|
+
Validation target data.
|
|
323
|
+
- Ns : int
|
|
324
|
+
Number of samples.
|
|
325
|
+
- tclen : int
|
|
326
|
+
Length of time series.
|
|
327
|
+
- thebatchsize : int
|
|
328
|
+
Batch size for training.
|
|
329
|
+
|
|
330
|
+
Examples
|
|
331
|
+
--------
|
|
332
|
+
>>> model = MyModel()
|
|
333
|
+
>>> model.initialized = True
|
|
334
|
+
>>> model.loaddata()
|
|
335
|
+
>>> print(model.train_x.shape)
|
|
336
|
+
(1000, 10)
|
|
337
|
+
"""
|
|
338
|
+
if not self.initialized:
|
|
339
|
+
raise Exception("model must be initialized prior to loading data")
|
|
340
|
+
|
|
341
|
+
if self.dofft:
|
|
342
|
+
(
|
|
343
|
+
self.train_x,
|
|
344
|
+
self.train_y,
|
|
345
|
+
self.val_x,
|
|
346
|
+
self.val_y,
|
|
347
|
+
self.Ns,
|
|
348
|
+
self.tclen,
|
|
349
|
+
self.thebatchsize,
|
|
350
|
+
dummy,
|
|
351
|
+
dummy,
|
|
352
|
+
) = prep(
|
|
353
|
+
self.window_size,
|
|
354
|
+
thesuffix=self.thesuffix,
|
|
355
|
+
thedatadir=self.thedatadir,
|
|
356
|
+
inputfrag=self.inputfrag,
|
|
357
|
+
targetfrag=self.targetfrag,
|
|
358
|
+
startskip=self.startskip,
|
|
359
|
+
endskip=self.endskip,
|
|
360
|
+
corrthresh=self.corrthresh,
|
|
361
|
+
step=self.step,
|
|
362
|
+
dofft=self.dofft,
|
|
363
|
+
usebadpts=self.usebadpts,
|
|
364
|
+
excludethresh=self.excludethresh,
|
|
365
|
+
excludebysubject=self.excludebysubject,
|
|
366
|
+
readlim=self.readlim,
|
|
367
|
+
readskip=self.readskip,
|
|
368
|
+
countlim=self.countlim,
|
|
369
|
+
)
|
|
370
|
+
else:
|
|
371
|
+
(
|
|
372
|
+
self.train_x,
|
|
373
|
+
self.train_y,
|
|
374
|
+
self.val_x,
|
|
375
|
+
self.val_y,
|
|
376
|
+
self.Ns,
|
|
377
|
+
self.tclen,
|
|
378
|
+
self.thebatchsize,
|
|
379
|
+
) = prep(
|
|
380
|
+
self.window_size,
|
|
381
|
+
thesuffix=self.thesuffix,
|
|
382
|
+
thedatadir=self.thedatadir,
|
|
383
|
+
inputfrag=self.inputfrag,
|
|
384
|
+
targetfrag=self.targetfrag,
|
|
385
|
+
startskip=self.startskip,
|
|
386
|
+
endskip=self.endskip,
|
|
387
|
+
corrthresh=self.corrthresh,
|
|
388
|
+
step=self.step,
|
|
389
|
+
dofft=self.dofft,
|
|
390
|
+
usebadpts=self.usebadpts,
|
|
391
|
+
excludethresh=self.excludethresh,
|
|
392
|
+
excludebysubject=self.excludebysubject,
|
|
393
|
+
readlim=self.readlim,
|
|
394
|
+
readskip=self.readskip,
|
|
395
|
+
countlim=self.countlim,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def predict_model(self, X: np.ndarray) -> np.ndarray:
|
|
399
|
+
"""
|
|
400
|
+
Make predictions using the trained model.
|
|
401
|
+
|
|
402
|
+
Parameters
|
|
403
|
+
----------
|
|
404
|
+
X : np.ndarray
|
|
405
|
+
Input features for prediction. Shape should be (n_samples, n_features)
|
|
406
|
+
where n_samples is the number of samples and n_features is the number
|
|
407
|
+
of features expected by the model.
|
|
408
|
+
|
|
409
|
+
Returns
|
|
410
|
+
-------
|
|
411
|
+
np.ndarray
|
|
412
|
+
Model predictions. Shape will depend on the specific model type but
|
|
413
|
+
typically follows (n_samples,) for regression or (n_samples, n_classes)
|
|
414
|
+
for classification.
|
|
415
|
+
|
|
416
|
+
Notes
|
|
417
|
+
-----
|
|
418
|
+
This method sets the model to inference mode by calling with training=False.
|
|
419
|
+
The predictions are made without computing gradients, making it efficient
|
|
420
|
+
for inference tasks. Input data is automatically converted to PyTorch tensors
|
|
421
|
+
and moved to the appropriate device. Special handling is included for
|
|
422
|
+
tensor dimension permutation to match model expectations.
|
|
423
|
+
|
|
424
|
+
Examples
|
|
425
|
+
--------
|
|
426
|
+
>>> # Assuming model is already trained
|
|
427
|
+
>>> X_test = np.array([[1.0, 2.0], [3.0, 4.0]])
|
|
428
|
+
>>> predictions = model.predict_model(X_test)
|
|
429
|
+
>>> print(predictions)
|
|
430
|
+
"""
|
|
431
|
+
self.model.eval()
|
|
432
|
+
with torch.no_grad():
|
|
433
|
+
if isinstance(X, np.ndarray):
|
|
434
|
+
X = torch.from_numpy(X).float().to(self.device)
|
|
435
|
+
# PyTorch expects (batch, channels, length) but we have (batch, length, channels)
|
|
436
|
+
X = X.permute(0, 2, 1)
|
|
437
|
+
output = self.model(X)
|
|
438
|
+
# Convert back to (batch, length, channels)
|
|
439
|
+
output = output.permute(0, 2, 1)
|
|
440
|
+
return output.cpu().numpy()
|
|
441
|
+
|
|
442
|
+
def evaluate(self) -> tuple[list, list, float, float]:
|
|
443
|
+
"""
|
|
444
|
+
Evaluate the model performance on validation data and compute loss metrics.
|
|
445
|
+
|
|
446
|
+
This method performs model evaluation by computing prediction errors and
|
|
447
|
+
saving training/validation loss curves. It calculates both prediction error
|
|
448
|
+
(difference between predicted and actual values) and raw error (difference
|
|
449
|
+
between input and actual values). The method also generates and saves a
|
|
450
|
+
plot of the training and validation loss over epochs.
|
|
451
|
+
|
|
452
|
+
Parameters
|
|
453
|
+
----------
|
|
454
|
+
self : object
|
|
455
|
+
The instance of the class containing the model and data attributes.
|
|
456
|
+
|
|
457
|
+
Returns
|
|
458
|
+
-------
|
|
459
|
+
tuple[list, list, float, float]
|
|
460
|
+
A tuple containing:
|
|
461
|
+
- training_loss : list
|
|
462
|
+
List of training loss values per epoch
|
|
463
|
+
- validation_loss : list
|
|
464
|
+
List of validation loss values per epoch
|
|
465
|
+
- prediction_error : float
|
|
466
|
+
Mean squared error between predicted and actual values
|
|
467
|
+
- raw_error : float
|
|
468
|
+
Mean squared error between input features and actual values
|
|
469
|
+
|
|
470
|
+
Notes
|
|
471
|
+
-----
|
|
472
|
+
This method modifies the instance attributes:
|
|
473
|
+
- self.lossfilename: Path to the saved loss plot
|
|
474
|
+
- self.pred_error: Computed prediction error
|
|
475
|
+
- self.raw_error: Computed raw error
|
|
476
|
+
- self.loss: Training loss history
|
|
477
|
+
- self.val_loss: Validation loss history
|
|
478
|
+
|
|
479
|
+
The method saves:
|
|
480
|
+
- Loss plot as PNG file
|
|
481
|
+
- Loss metrics as text file
|
|
482
|
+
|
|
483
|
+
Examples
|
|
484
|
+
--------
|
|
485
|
+
>>> model = MyModel()
|
|
486
|
+
>>> train_loss, val_loss, pred_error, raw_error = model.evaluate()
|
|
487
|
+
>>> print(f"Prediction Error: {pred_error}")
|
|
488
|
+
Prediction Error: 0.1234
|
|
489
|
+
"""
|
|
490
|
+
self.lossfilename = os.path.join(self.modelname, "loss.png")
|
|
491
|
+
LGR.info(f"lossfilename: {self.lossfilename}")
|
|
492
|
+
|
|
493
|
+
YPred = self.predict_model(self.val_x)
|
|
494
|
+
|
|
495
|
+
error = self.val_y - YPred
|
|
496
|
+
self.pred_error = np.mean(np.square(error))
|
|
497
|
+
|
|
498
|
+
error2 = self.val_x - self.val_y
|
|
499
|
+
self.raw_error = np.mean(np.square(error2))
|
|
500
|
+
LGR.info(f"Prediction Error: {self.pred_error}\tRaw Error: {self.raw_error}")
|
|
501
|
+
|
|
502
|
+
f = open(os.path.join(self.modelname, "loss.txt"), "w")
|
|
503
|
+
f.write(
|
|
504
|
+
self.modelname
|
|
505
|
+
+ ": Prediction Error: "
|
|
506
|
+
+ str(self.pred_error)
|
|
507
|
+
+ " Raw Error: "
|
|
508
|
+
+ str(self.raw_error)
|
|
509
|
+
+ "\n"
|
|
510
|
+
)
|
|
511
|
+
f.close()
|
|
512
|
+
|
|
513
|
+
epochs = range(len(self.loss))
|
|
514
|
+
|
|
515
|
+
self.updatemetadata()
|
|
516
|
+
|
|
517
|
+
plt.figure()
|
|
518
|
+
plt.plot(epochs, self.loss, "bo", label="Training loss")
|
|
519
|
+
plt.plot(epochs, self.val_loss, "b", label="Validation loss")
|
|
520
|
+
plt.title("Training and validation loss")
|
|
521
|
+
plt.legend()
|
|
522
|
+
plt.savefig(self.lossfilename)
|
|
523
|
+
plt.close()
|
|
524
|
+
|
|
525
|
+
return self.loss, self.val_loss, self.pred_error, self.raw_error
|
|
526
|
+
|
|
527
|
+
def initmetadata(self) -> None:
|
|
528
|
+
"""
|
|
529
|
+
Initialize and store metadata information for the model.
|
|
530
|
+
|
|
531
|
+
This function creates a dictionary containing various model configuration parameters
|
|
532
|
+
and writes them to a JSON file for future reference and reproducibility.
|
|
533
|
+
|
|
534
|
+
Parameters
|
|
535
|
+
----------
|
|
536
|
+
self : object
|
|
537
|
+
The instance of the class containing the metadata attributes. Expected to have
|
|
538
|
+
the following attributes:
|
|
539
|
+
- `nettype`: Type of neural network
|
|
540
|
+
- `window_size`: Size of the window used for processing
|
|
541
|
+
- `usebadpts`: Flag indicating whether bad points are handled
|
|
542
|
+
- `dofft`: Flag indicating whether FFT is used
|
|
543
|
+
- `excludethresh`: Threshold for exclusion
|
|
544
|
+
- `num_epochs`: Number of training epochs
|
|
545
|
+
- `num_layers`: Number of layers in the model
|
|
546
|
+
- `dropout_rate`: Dropout rate for regularization
|
|
547
|
+
- `modelname`: Name of the model
|
|
548
|
+
|
|
549
|
+
Returns
|
|
550
|
+
-------
|
|
551
|
+
None
|
|
552
|
+
This function does not return any value but writes metadata to a JSON file.
|
|
553
|
+
|
|
554
|
+
Notes
|
|
555
|
+
-----
|
|
556
|
+
The metadata includes:
|
|
557
|
+
- Window size for processing
|
|
558
|
+
- Bad point handling flag
|
|
559
|
+
- FFT usage flag
|
|
560
|
+
- Exclusion threshold
|
|
561
|
+
- Number of epochs and layers
|
|
562
|
+
- Dropout rate
|
|
563
|
+
- Operating system platform
|
|
564
|
+
- Model name
|
|
565
|
+
|
|
566
|
+
The metadata is saved to ``{modelname}/model_meta.json`` where ``modelname``
|
|
567
|
+
is the model's name attribute.
|
|
568
|
+
|
|
569
|
+
Examples
|
|
570
|
+
--------
|
|
571
|
+
>>> model = MyModel()
|
|
572
|
+
>>> model.initmetadata()
|
|
573
|
+
>>> # Metadata stored in modelname/model_meta.json
|
|
574
|
+
"""
|
|
575
|
+
|
|
576
|
+
self.infodict = {}
|
|
577
|
+
self.infodict["nettype"] = self.nettype
|
|
578
|
+
self.infodict["window_size"] = self.window_size
|
|
579
|
+
self.infodict["usebadpts"] = self.usebadpts
|
|
580
|
+
self.infodict["dofft"] = self.dofft
|
|
581
|
+
self.infodict["excludethresh"] = self.excludethresh
|
|
582
|
+
self.infodict["num_epochs"] = self.num_epochs
|
|
583
|
+
self.infodict["num_layers"] = self.num_layers
|
|
584
|
+
self.infodict["dropout_rate"] = self.dropout_rate
|
|
585
|
+
self.infodict["train_arch"] = sys.platform
|
|
586
|
+
self.infodict["modelname"] = self.modelname
|
|
587
|
+
tide_io.writedicttojson(self.infodict, os.path.join(self.modelname, "model_meta.json"))
|
|
588
|
+
|
|
589
|
+
def updatemetadata(self) -> None:
|
|
590
|
+
"""
|
|
591
|
+
Update metadata dictionary with model metrics and save to JSON file.
|
|
592
|
+
|
|
593
|
+
This method updates the internal information dictionary with various model
|
|
594
|
+
performance metrics and writes the complete metadata to a JSON file for
|
|
595
|
+
model persistence and tracking.
|
|
596
|
+
|
|
597
|
+
Parameters
|
|
598
|
+
----------
|
|
599
|
+
self : object
|
|
600
|
+
The instance of the class containing the metadata and model information.
|
|
601
|
+
Expected to have the following attributes:
|
|
602
|
+
- infodict : dict
|
|
603
|
+
Dictionary containing model metadata.
|
|
604
|
+
- loss : float
|
|
605
|
+
Training loss value.
|
|
606
|
+
- val_loss : float
|
|
607
|
+
Validation loss value.
|
|
608
|
+
- raw_error : float
|
|
609
|
+
Raw error metric.
|
|
610
|
+
- pred_error : float
|
|
611
|
+
Prediction error metric.
|
|
612
|
+
- modelname : str
|
|
613
|
+
Name/path of the model for file output.
|
|
614
|
+
|
|
615
|
+
Returns
|
|
616
|
+
-------
|
|
617
|
+
None
|
|
618
|
+
This method does not return any value but modifies the `infodict` in-place
|
|
619
|
+
and writes to a JSON file.
|
|
620
|
+
|
|
621
|
+
Notes
|
|
622
|
+
-----
|
|
623
|
+
The method writes metadata to ``{modelname}/model_meta.json`` where
|
|
624
|
+
``modelname`` is the model name attribute of the instance.
|
|
625
|
+
|
|
626
|
+
Examples
|
|
627
|
+
--------
|
|
628
|
+
>>> model = MyModel()
|
|
629
|
+
>>> model.updatemetadata()
|
|
630
|
+
>>> # Creates model_meta.json with loss, val_loss, raw_error, and pred_error
|
|
631
|
+
"""
|
|
632
|
+
self.infodict["loss"] = self.loss
|
|
633
|
+
self.infodict["val_loss"] = self.val_loss
|
|
634
|
+
self.infodict["raw_error"] = self.raw_error
|
|
635
|
+
self.infodict["prediction_error"] = self.pred_error
|
|
636
|
+
tide_io.writedicttojson(self.infodict, os.path.join(self.modelname, "model_meta.json"))
|
|
637
|
+
|
|
638
|
+
def savemodel(self, altname: str | None = None) -> None:
|
|
639
|
+
"""
|
|
640
|
+
Save the model to disk with the specified name.
|
|
641
|
+
|
|
642
|
+
This method saves the current model to a Keras file format (.keras) in a
|
|
643
|
+
directory named according to the model name or an alternative name provided.
|
|
644
|
+
|
|
645
|
+
Parameters
|
|
646
|
+
----------
|
|
647
|
+
altname : str, optional
|
|
648
|
+
Alternative name to use for saving the model. If None, uses the
|
|
649
|
+
model's default name stored in `self.modelname`. Default is None.
|
|
650
|
+
|
|
651
|
+
Returns
|
|
652
|
+
-------
|
|
653
|
+
None
|
|
654
|
+
This method does not return any value.
|
|
655
|
+
|
|
656
|
+
Notes
|
|
657
|
+
-----
|
|
658
|
+
The model is saved in the Keras format (.keras) and stored in a directory
|
|
659
|
+
with the same name as the model. The method logs the saving operation
|
|
660
|
+
using the logger instance `LGR`.
|
|
661
|
+
|
|
662
|
+
Examples
|
|
663
|
+
--------
|
|
664
|
+
>>> # Save model with default name
|
|
665
|
+
>>> savemodel()
|
|
666
|
+
>>>
|
|
667
|
+
>>> # Save model with alternative name
|
|
668
|
+
>>> savemodel(altname="my_custom_model")
|
|
669
|
+
"""
|
|
670
|
+
if altname is None:
|
|
671
|
+
modelsavename = self.modelname
|
|
672
|
+
else:
|
|
673
|
+
modelsavename = altname
|
|
674
|
+
LGR.info(f"saving {modelsavename}")
|
|
675
|
+
torch.save(
|
|
676
|
+
{
|
|
677
|
+
"model_state_dict": self.model.state_dict(),
|
|
678
|
+
"model_config": (
|
|
679
|
+
self.model.get_config() if hasattr(self.model, "get_config") else None
|
|
680
|
+
),
|
|
681
|
+
},
|
|
682
|
+
os.path.join(modelsavename, "model.pth"),
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
def loadmodel(self, modelname: str, verbose: bool = False) -> None:
|
|
686
|
+
"""
|
|
687
|
+
Load a trained model from disk and initialize model parameters.
|
|
688
|
+
|
|
689
|
+
Load a Keras model from the specified model directory, along with associated
|
|
690
|
+
metadata and configuration information. The function attempts to load the model
|
|
691
|
+
in Keras format first, falling back to HDF5 format if the Keras format is not found.
|
|
692
|
+
|
|
693
|
+
Parameters
|
|
694
|
+
----------
|
|
695
|
+
modelname : str
|
|
696
|
+
Name of the model to load, corresponding to a subdirectory in ``self.modelpath``.
|
|
697
|
+
verbose : bool, optional
|
|
698
|
+
If True, print model summary and metadata information. Default is False.
|
|
699
|
+
|
|
700
|
+
Returns
|
|
701
|
+
-------
|
|
702
|
+
None
|
|
703
|
+
This method modifies the instance attributes in-place and does not return anything.
|
|
704
|
+
|
|
705
|
+
Notes
|
|
706
|
+
-----
|
|
707
|
+
The function attempts to load the model in the following order:
|
|
708
|
+
1. Keras format (model.keras)
|
|
709
|
+
2. HDF5 format (model.h5)
|
|
710
|
+
|
|
711
|
+
If neither format is found, the function exits with an error message.
|
|
712
|
+
|
|
713
|
+
The loaded model metadata is stored in ``self.infodict``, and model configuration
|
|
714
|
+
is stored in ``self.config``. Additional attributes like ``window_size`` and
|
|
715
|
+
``usebadpts`` are extracted from the metadata and stored as instance attributes.
|
|
716
|
+
|
|
717
|
+
Examples
|
|
718
|
+
--------
|
|
719
|
+
>>> loader = ModelLoader()
|
|
720
|
+
>>> loader.loadmodel("my_model", verbose=True)
|
|
721
|
+
loading my_model
|
|
722
|
+
Model: "sequential"
|
|
723
|
+
_________________________________________________________________
|
|
724
|
+
Layer (type) Output Shape Param #
|
|
725
|
+
=================================================================
|
|
726
|
+
...
|
|
727
|
+
>>> print(loader.window_size)
|
|
728
|
+
100
|
|
729
|
+
"""
|
|
730
|
+
# read in the data
|
|
731
|
+
LGR.info(f"loading {modelname}")
|
|
732
|
+
|
|
733
|
+
# load additional information first to reconstruct model
|
|
734
|
+
self.infodict = tide_io.readdictfromjson(
|
|
735
|
+
os.path.join(self.modelpath, modelname, "model_meta.json")
|
|
736
|
+
)
|
|
737
|
+
if verbose:
|
|
738
|
+
print(self.infodict)
|
|
739
|
+
self.window_size = self.infodict["window_size"]
|
|
740
|
+
self.usebadpts = self.infodict["usebadpts"]
|
|
741
|
+
|
|
742
|
+
# Load the model as a dict
|
|
743
|
+
checkpoint = torch.load(
|
|
744
|
+
os.path.join(self.modelpath, modelname, "model.pth"), map_location=self.device
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# Reconstruct the model architecture (must be done by subclass)
|
|
748
|
+
if self.infodict["nettype"] == "cnn":
|
|
749
|
+
self.num_filters = checkpoint["model_config"]["num_filters"]
|
|
750
|
+
self.kernel_size = checkpoint["model_config"]["kernel_size"]
|
|
751
|
+
self.num_layers = checkpoint["model_config"]["num_layers"]
|
|
752
|
+
self.dropout_rate = checkpoint["model_config"]["dropout_rate"]
|
|
753
|
+
self.dilation_rate = checkpoint["model_config"]["dilation_rate"]
|
|
754
|
+
self.activation = checkpoint["model_config"]["activation"]
|
|
755
|
+
self.inputsize = checkpoint["model_config"]["inputsize"]
|
|
756
|
+
|
|
757
|
+
self.model = CNNModel(
|
|
758
|
+
self.num_filters,
|
|
759
|
+
self.kernel_size,
|
|
760
|
+
self.num_layers,
|
|
761
|
+
self.dropout_rate,
|
|
762
|
+
self.dilation_rate,
|
|
763
|
+
self.activation,
|
|
764
|
+
self.inputsize,
|
|
765
|
+
)
|
|
766
|
+
elif self.infodict["nettype"] == "autoencoder":
|
|
767
|
+
self.encoding_dim = checkpoint["model_config"]["encoding_dim"]
|
|
768
|
+
self.num_layers = checkpoint["model_config"]["num_layers"]
|
|
769
|
+
self.dropout_rate = checkpoint["model_config"]["dropout_rate"]
|
|
770
|
+
self.activation = checkpoint["model_config"]["activation"]
|
|
771
|
+
self.inputsize = checkpoint["model_config"]["inputsize"]
|
|
772
|
+
|
|
773
|
+
self.model = DenseAutoencoderModel(
|
|
774
|
+
self.window_size,
|
|
775
|
+
self.encoding_dim,
|
|
776
|
+
self.num_layers,
|
|
777
|
+
self.dropout_rate,
|
|
778
|
+
self.activation,
|
|
779
|
+
self.inputsize,
|
|
780
|
+
)
|
|
781
|
+
elif self.infodict["nettype"] == "multiscalecnn":
|
|
782
|
+
self.num_filters = checkpoint["model_config"]["num_filters"]
|
|
783
|
+
self.kernel_sizes = checkpoint["model_config"]["kernel_sizes"]
|
|
784
|
+
self.input_lens = checkpoint["model_config"]["input_lens"]
|
|
785
|
+
self.input_width = checkpoint["model_config"]["input_width"]
|
|
786
|
+
self.dilation_rate = checkpoint["model_config"]["dilation_rate"]
|
|
787
|
+
|
|
788
|
+
self.model = MultiscaleCNNModel(
|
|
789
|
+
self.num_filters,
|
|
790
|
+
self.kernel_sizes,
|
|
791
|
+
self.input_lens,
|
|
792
|
+
self.input_width,
|
|
793
|
+
self.dilation_rate,
|
|
794
|
+
)
|
|
795
|
+
elif self.infodict["nettype"] == "convautoencoder":
|
|
796
|
+
self.encoding_dim = checkpoint["model_config"]["encoding_dim"]
|
|
797
|
+
self.num_filters = checkpoint["model_config"]["num_filters"]
|
|
798
|
+
self.kernel_size = checkpoint["model_config"]["kernel_size"]
|
|
799
|
+
self.dropout_rate = checkpoint["model_config"]["dropout_rate"]
|
|
800
|
+
self.activation = checkpoint["model_config"]["activation"]
|
|
801
|
+
self.inputsize = checkpoint["model_config"]["inputsize"]
|
|
802
|
+
|
|
803
|
+
self.model = ConvAutoencoderModel(
|
|
804
|
+
self.window_size,
|
|
805
|
+
self.encoding_dim,
|
|
806
|
+
self.num_filters,
|
|
807
|
+
self.kernel_size,
|
|
808
|
+
self.dropout_rate,
|
|
809
|
+
self.activation,
|
|
810
|
+
self.inputsize,
|
|
811
|
+
)
|
|
812
|
+
elif self.infodict["nettype"] == "crnn":
|
|
813
|
+
self.num_filters = checkpoint["model_config"]["num_filters"]
|
|
814
|
+
self.kernel_size = checkpoint["model_config"]["kernel_size"]
|
|
815
|
+
self.encoding_dim = checkpoint["model_config"]["encoding_dim"]
|
|
816
|
+
self.dropout_rate = checkpoint["model_config"]["dropout_rate"]
|
|
817
|
+
self.activation = checkpoint["model_config"]["activation"]
|
|
818
|
+
self.inputsize = checkpoint["model_config"]["inputsize"]
|
|
819
|
+
|
|
820
|
+
self.model = CRNNModel(
|
|
821
|
+
self.num_filters,
|
|
822
|
+
self.kernel_size,
|
|
823
|
+
self.encoding_dim,
|
|
824
|
+
self.dropout_rate,
|
|
825
|
+
self.activation,
|
|
826
|
+
self.inputsize,
|
|
827
|
+
)
|
|
828
|
+
elif self.infodict["nettype"] == "lstm":
|
|
829
|
+
self.num_units = checkpoint["model_config"]["num_units"]
|
|
830
|
+
self.num_layers = checkpoint["model_config"]["num_layers"]
|
|
831
|
+
self.dropout_rate = checkpoint["model_config"]["dropout_rate"]
|
|
832
|
+
self.inputsize = checkpoint["model_config"]["inputsize"]
|
|
833
|
+
|
|
834
|
+
self.model = LSTMModel(
|
|
835
|
+
self.num_units,
|
|
836
|
+
self.num_layers,
|
|
837
|
+
self.dropout_rate,
|
|
838
|
+
self.window_size,
|
|
839
|
+
self.inputsize,
|
|
840
|
+
)
|
|
841
|
+
elif self.infodict["nettype"] == "hybrid":
|
|
842
|
+
self.num_filters = checkpoint["model_config"]["num_filters"]
|
|
843
|
+
self.kernel_size = checkpoint["model_config"]["kernel_size"]
|
|
844
|
+
self.num_units = checkpoint["model_config"]["num_units"]
|
|
845
|
+
self.num_layers = checkpoint["model_config"]["num_layers"]
|
|
846
|
+
self.dropout_rate = checkpoint["model_config"]["dropout_rate"]
|
|
847
|
+
self.activation = checkpoint["model_config"]["activation"]
|
|
848
|
+
self.inputsize = checkpoint["model_config"]["inputsize"]
|
|
849
|
+
self.invert = checkpoint["model_config"]["invert"]
|
|
850
|
+
|
|
851
|
+
self.model = HybridModel(
|
|
852
|
+
self.num_filters,
|
|
853
|
+
self.kernel_size,
|
|
854
|
+
self.num_units,
|
|
855
|
+
self.num_layers,
|
|
856
|
+
self.dropout_rate,
|
|
857
|
+
self.activation,
|
|
858
|
+
self.inputsize,
|
|
859
|
+
self.window_size,
|
|
860
|
+
self.invert,
|
|
861
|
+
)
|
|
862
|
+
else:
|
|
863
|
+
print(f"nettype {self.infodict['nettype']} is not supported!")
|
|
864
|
+
sys.exit()
|
|
865
|
+
|
|
866
|
+
self.model.load_state_dict(checkpoint["model_state_dict"])
|
|
867
|
+
self.model.to(self.device)
|
|
868
|
+
|
|
869
|
+
def initialize(self) -> None:
|
|
870
|
+
"""
|
|
871
|
+
Initialize the model by setting up network architecture and metadata.
|
|
872
|
+
|
|
873
|
+
This method performs a series of initialization steps including retrieving
|
|
874
|
+
the model name, creating the network architecture, displaying model summary,
|
|
875
|
+
saving the model configuration, initializing metadata, and setting appropriate
|
|
876
|
+
flags to indicate initialization status.
|
|
877
|
+
|
|
878
|
+
Parameters
|
|
879
|
+
----------
|
|
880
|
+
self : object
|
|
881
|
+
The instance of the model class being initialized.
|
|
882
|
+
|
|
883
|
+
Returns
|
|
884
|
+
-------
|
|
885
|
+
None
|
|
886
|
+
This method does not return any value.
|
|
887
|
+
|
|
888
|
+
Notes
|
|
889
|
+
-----
|
|
890
|
+
This method should be called before any training or prediction operations.
|
|
891
|
+
The initialization process sets `self.initialized` to True and `self.trained`
|
|
892
|
+
to False, indicating that the model is ready for training but has not been
|
|
893
|
+
trained yet.
|
|
894
|
+
|
|
895
|
+
Examples
|
|
896
|
+
--------
|
|
897
|
+
>>> model = MyModel()
|
|
898
|
+
>>> model.initialize()
|
|
899
|
+
>>> print(model.initialized)
|
|
900
|
+
True
|
|
901
|
+
>>> print(model.trained)
|
|
902
|
+
False
|
|
903
|
+
"""
|
|
904
|
+
self.getname()
|
|
905
|
+
self.makenet()
|
|
906
|
+
print(self.model)
|
|
907
|
+
self.savemodel()
|
|
908
|
+
self.initmetadata()
|
|
909
|
+
self.initialized = True
|
|
910
|
+
self.trained = False
|
|
911
|
+
|
|
912
|
+
def train(self) -> None:
|
|
913
|
+
"""
|
|
914
|
+
Train the model using the provided training and validation datasets.
|
|
915
|
+
|
|
916
|
+
This method performs model training with optional pretraining and logging. It supports
|
|
917
|
+
TensorBoard logging, model checkpointing, early stopping, and NaN termination. The trained
|
|
918
|
+
model is saved at the end of training.
|
|
919
|
+
|
|
920
|
+
Parameters
|
|
921
|
+
----------
|
|
922
|
+
self : object
|
|
923
|
+
The instance of the class containing the model and training configuration.
|
|
924
|
+
Expected attributes include:
|
|
925
|
+
- `model`: PyTorch model to be trained.
|
|
926
|
+
- `train_x`, `train_y`, `val_x`, `val_y`: Training and validation data as numpy arrays.
|
|
927
|
+
- `device`: Device to run the training on (e.g., 'cpu' or 'cuda').
|
|
928
|
+
- `num_pretrain_epochs`: Number of pretraining epochs (default: 0).
|
|
929
|
+
- `num_epochs`: Number of main training epochs.
|
|
930
|
+
- `modelname`: Directory name to save model checkpoints and logs.
|
|
931
|
+
- `usetensorboard`: Boolean flag to enable TensorBoard logging.
|
|
932
|
+
- `savemodel()`: Method to save the final trained model.
|
|
933
|
+
|
|
934
|
+
Returns
|
|
935
|
+
-------
|
|
936
|
+
None
|
|
937
|
+
This function does not return any value.
|
|
938
|
+
|
|
939
|
+
Notes
|
|
940
|
+
-----
|
|
941
|
+
- If `self.usetensorboard` is True, TensorBoard logging is enabled.
|
|
942
|
+
- If `self.num_pretrain_epochs` is greater than 0, a pretraining phase is performed
|
|
943
|
+
before the main training loop.
|
|
944
|
+
- The model is saved after training using the `savemodel()` method.
|
|
945
|
+
- Training uses `ModelCheckpoint`, `EarlyStopping`, and `TerminateOnNaN` callbacks
|
|
946
|
+
to manage training process and prevent overfitting or NaN issues.
|
|
947
|
+
- Intermediate model checkpoints are saved during training.
|
|
948
|
+
- The best model (based on validation loss) is retained and restored upon early stopping.
|
|
949
|
+
|
|
950
|
+
Examples
|
|
951
|
+
--------
|
|
952
|
+
>>> trainer = ModelTrainer(model, train_x, train_y, val_x, val_y)
|
|
953
|
+
>>> trainer.train()
|
|
954
|
+
"""
|
|
955
|
+
self.model.train()
|
|
956
|
+
self.model.to(self.device)
|
|
957
|
+
|
|
958
|
+
# Convert numpy arrays to PyTorch tensors and transpose for Conv1d
|
|
959
|
+
print("converting tensors")
|
|
960
|
+
train_x_tensor = torch.from_numpy(self.train_x).float().permute(0, 2, 1)
|
|
961
|
+
train_y_tensor = torch.from_numpy(self.train_y).float().permute(0, 2, 1)
|
|
962
|
+
val_x_tensor = torch.from_numpy(self.val_x).float().permute(0, 2, 1)
|
|
963
|
+
val_y_tensor = torch.from_numpy(self.val_y).float().permute(0, 2, 1)
|
|
964
|
+
|
|
965
|
+
print("setting data")
|
|
966
|
+
train_dataset = TensorDataset(train_x_tensor, train_y_tensor)
|
|
967
|
+
val_dataset = TensorDataset(val_x_tensor, val_y_tensor)
|
|
968
|
+
|
|
969
|
+
train_loader = DataLoader(train_dataset, batch_size=1024, shuffle=True)
|
|
970
|
+
val_loader = DataLoader(val_dataset, batch_size=1024, shuffle=False)
|
|
971
|
+
|
|
972
|
+
print("setting criterion")
|
|
973
|
+
criterion = nn.MSELoss()
|
|
974
|
+
|
|
975
|
+
print("setting optimizer")
|
|
976
|
+
optimizer = optim.RMSprop(self.model.parameters())
|
|
977
|
+
|
|
978
|
+
self.loss = []
|
|
979
|
+
self.val_loss = []
|
|
980
|
+
|
|
981
|
+
best_val_loss = float("inf")
|
|
982
|
+
patience = 10
|
|
983
|
+
patience_counter = 0
|
|
984
|
+
|
|
985
|
+
total_epochs = self.num_pretrain_epochs + self.num_epochs
|
|
986
|
+
|
|
987
|
+
for epoch in range(total_epochs):
|
|
988
|
+
print(f"Epoch {epoch+1}/{total_epochs}")
|
|
989
|
+
# Training phase
|
|
990
|
+
self.model.train()
|
|
991
|
+
train_loss_epoch = 0.0
|
|
992
|
+
# for batch_x, batch_y in train_loader:
|
|
993
|
+
for batch_x, batch_y in tqdm.tqdm(
|
|
994
|
+
train_loader,
|
|
995
|
+
desc="Batch",
|
|
996
|
+
unit="batches",
|
|
997
|
+
disable=False,
|
|
998
|
+
):
|
|
999
|
+
batch_x, batch_y = batch_x.to(self.device), batch_y.to(self.device)
|
|
1000
|
+
|
|
1001
|
+
optimizer.zero_grad()
|
|
1002
|
+
outputs = self.model(batch_x)
|
|
1003
|
+
loss = criterion(outputs, batch_y)
|
|
1004
|
+
|
|
1005
|
+
if torch.isnan(loss):
|
|
1006
|
+
LGR.error("NaN loss detected, terminating training")
|
|
1007
|
+
break
|
|
1008
|
+
|
|
1009
|
+
loss.backward()
|
|
1010
|
+
optimizer.step()
|
|
1011
|
+
train_loss_epoch += loss.item()
|
|
1012
|
+
|
|
1013
|
+
train_loss_epoch /= len(train_loader)
|
|
1014
|
+
self.loss.append(train_loss_epoch)
|
|
1015
|
+
|
|
1016
|
+
# Validation phase
|
|
1017
|
+
self.model.eval()
|
|
1018
|
+
val_loss_epoch = 0.0
|
|
1019
|
+
with torch.no_grad():
|
|
1020
|
+
for batch_x, batch_y in val_loader:
|
|
1021
|
+
batch_x, batch_y = batch_x.to(self.device), batch_y.to(self.device)
|
|
1022
|
+
outputs = self.model(batch_x)
|
|
1023
|
+
loss = criterion(outputs, batch_y)
|
|
1024
|
+
val_loss_epoch += loss.item()
|
|
1025
|
+
|
|
1026
|
+
val_loss_epoch /= len(val_loader)
|
|
1027
|
+
self.val_loss.append(val_loss_epoch)
|
|
1028
|
+
|
|
1029
|
+
LGR.info(
|
|
1030
|
+
f"Epoch {epoch+1}/{total_epochs} - Loss: {train_loss_epoch:.4f} - Val Loss: {val_loss_epoch:.4f}"
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
# Save checkpoint
|
|
1034
|
+
self.intermediatemodelpath = os.path.join(
|
|
1035
|
+
self.modelname, f"model_e{epoch+1:02d}_v{val_loss_epoch:.4f}.pth"
|
|
1036
|
+
)
|
|
1037
|
+
torch.save(
|
|
1038
|
+
{
|
|
1039
|
+
"epoch": epoch,
|
|
1040
|
+
"model_state_dict": self.model.state_dict(),
|
|
1041
|
+
"optimizer_state_dict": optimizer.state_dict(),
|
|
1042
|
+
"loss": train_loss_epoch,
|
|
1043
|
+
"val_loss": val_loss_epoch,
|
|
1044
|
+
},
|
|
1045
|
+
self.intermediatemodelpath,
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
# Early stopping
|
|
1049
|
+
if val_loss_epoch < best_val_loss:
|
|
1050
|
+
best_val_loss = val_loss_epoch
|
|
1051
|
+
patience_counter = 0
|
|
1052
|
+
# Save best model
|
|
1053
|
+
torch.save(self.model.state_dict(), os.path.join(self.modelname, "best_model.pth"))
|
|
1054
|
+
else:
|
|
1055
|
+
patience_counter += 1
|
|
1056
|
+
if patience_counter >= patience:
|
|
1057
|
+
LGR.info(f"Early stopping triggered after {epoch+1} epochs")
|
|
1058
|
+
# Restore best weights
|
|
1059
|
+
self.model.load_state_dict(
|
|
1060
|
+
torch.load(
|
|
1061
|
+
os.path.join(self.modelname, "best_model.pth"), weights_only=True
|
|
1062
|
+
)
|
|
1063
|
+
)
|
|
1064
|
+
break
|
|
1065
|
+
self.evaluate()
|
|
1066
|
+
|
|
1067
|
+
self.savemodel()
|
|
1068
|
+
self.trained = True
|
|
1069
|
+
|
|
1070
|
+
def apply(self, inputdata: np.ndarray, badpts: np.ndarray | None = None) -> np.ndarray:
|
|
1071
|
+
"""
|
|
1072
|
+
Apply a sliding-window prediction model to the input data, optionally incorporating bad points.
|
|
1073
|
+
|
|
1074
|
+
This function performs a sliding-window prediction using a pre-trained model. It scales the input
|
|
1075
|
+
data using the median absolute deviation (MAD), applies the model to overlapping windows of data,
|
|
1076
|
+
and aggregates predictions with a weighted scheme. Optionally, bad points can be included in
|
|
1077
|
+
the prediction process to influence the model's behavior.
|
|
1078
|
+
|
|
1079
|
+
Parameters
|
|
1080
|
+
----------
|
|
1081
|
+
inputdata : np.ndarray
|
|
1082
|
+
Input data array of shape (N,) to be processed.
|
|
1083
|
+
badpts : np.ndarray | None, optional
|
|
1084
|
+
Array of same shape as `inputdata` indicating bad or invalid points. If None, no bad points
|
|
1085
|
+
are considered. Default is None.
|
|
1086
|
+
|
|
1087
|
+
Returns
|
|
1088
|
+
-------
|
|
1089
|
+
np.ndarray
|
|
1090
|
+
Predicted data array of the same shape as `inputdata`, with predictions aggregated and
|
|
1091
|
+
weighted across overlapping windows.
|
|
1092
|
+
|
|
1093
|
+
Notes
|
|
1094
|
+
-----
|
|
1095
|
+
- The function uses a sliding window of size `self.window_size` to process input data.
|
|
1096
|
+
- Predictions are aggregated by summing over overlapping windows.
|
|
1097
|
+
- A triangular weight scheme is applied to the aggregated predictions to reduce edge effects.
|
|
1098
|
+
- If `self.usebadpts` is True, `badpts` are included as an additional feature in the model input.
|
|
1099
|
+
|
|
1100
|
+
Examples
|
|
1101
|
+
--------
|
|
1102
|
+
>>> model = MyModel(window_size=10, usebadpts=True)
|
|
1103
|
+
>>> input_data = np.random.randn(100)
|
|
1104
|
+
>>> bad_points = np.zeros_like(input_data)
|
|
1105
|
+
>>> result = model.apply(input_data, bad_points)
|
|
1106
|
+
"""
|
|
1107
|
+
initscale = mad(inputdata)
|
|
1108
|
+
scaleddata = inputdata / initscale
|
|
1109
|
+
predicteddata = scaleddata * 0.0
|
|
1110
|
+
weightarray = scaleddata * 0.0
|
|
1111
|
+
N_pts = len(scaleddata)
|
|
1112
|
+
if self.usebadpts:
|
|
1113
|
+
if badpts is None:
|
|
1114
|
+
badpts = scaleddata * 0.0
|
|
1115
|
+
X = np.zeros(((N_pts - self.window_size - 1), self.window_size, 2))
|
|
1116
|
+
for i in range(X.shape[0]):
|
|
1117
|
+
X[i, :, 0] = scaleddata[i : i + self.window_size]
|
|
1118
|
+
X[i, :, 1] = badpts[i : i + self.window_size]
|
|
1119
|
+
else:
|
|
1120
|
+
X = np.zeros(((N_pts - self.window_size - 1), self.window_size, 1))
|
|
1121
|
+
for i in range(X.shape[0]):
|
|
1122
|
+
X[i, :, 0] = scaleddata[i : i + self.window_size]
|
|
1123
|
+
|
|
1124
|
+
Y = self.predict_model(X)
|
|
1125
|
+
for i in range(X.shape[0]):
|
|
1126
|
+
predicteddata[i : i + self.window_size] += Y[i, :, 0]
|
|
1127
|
+
|
|
1128
|
+
weightarray[:] = self.window_size
|
|
1129
|
+
weightarray[0 : self.window_size] = np.linspace(
|
|
1130
|
+
1.0, self.window_size, self.window_size, endpoint=False
|
|
1131
|
+
)
|
|
1132
|
+
weightarray[-(self.window_size + 1) : -1] = np.linspace(
|
|
1133
|
+
self.window_size, 1.0, self.window_size, endpoint=False
|
|
1134
|
+
)
|
|
1135
|
+
return initscale * predicteddata / weightarray
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
class CNNModel(nn.Module):
|
|
1139
|
+
def __init__(
|
|
1140
|
+
self,
|
|
1141
|
+
num_filters: int,
|
|
1142
|
+
kernel_size: int,
|
|
1143
|
+
num_layers: int,
|
|
1144
|
+
dropout_rate: float,
|
|
1145
|
+
dilation_rate: int,
|
|
1146
|
+
activation: str,
|
|
1147
|
+
inputsize: int,
|
|
1148
|
+
) -> None:
|
|
1149
|
+
"""
|
|
1150
|
+
Initialize the CNNModel with specified architecture parameters.
|
|
1151
|
+
|
|
1152
|
+
Parameters
|
|
1153
|
+
----------
|
|
1154
|
+
num_filters : int
|
|
1155
|
+
Number of convolutional filters in each layer.
|
|
1156
|
+
kernel_size : int
|
|
1157
|
+
Size of the convolutional kernel.
|
|
1158
|
+
num_layers : int
|
|
1159
|
+
Total number of layers in the network.
|
|
1160
|
+
dropout_rate : float
|
|
1161
|
+
Dropout rate for regularization.
|
|
1162
|
+
dilation_rate : int
|
|
1163
|
+
Dilation rate for dilated convolutions in intermediate layers.
|
|
1164
|
+
activation : str
|
|
1165
|
+
Activation function to use; options are 'relu' or 'tanh'.
|
|
1166
|
+
inputsize : int
|
|
1167
|
+
Size of the input features.
|
|
1168
|
+
|
|
1169
|
+
Returns
|
|
1170
|
+
-------
|
|
1171
|
+
None
|
|
1172
|
+
This method initializes the model in-place and does not return any value.
|
|
1173
|
+
|
|
1174
|
+
Notes
|
|
1175
|
+
-----
|
|
1176
|
+
The model consists of an input layer, intermediate layers with dilated convolutions,
|
|
1177
|
+
and an output layer. Batch normalization and dropout are applied after each convolutional
|
|
1178
|
+
layer except the output layer. The activation function is applied after each convolutional
|
|
1179
|
+
layer based on the `activation` parameter.
|
|
1180
|
+
|
|
1181
|
+
Examples
|
|
1182
|
+
--------
|
|
1183
|
+
>>> model = CNNModel(
|
|
1184
|
+
... num_filters=64,
|
|
1185
|
+
... kernel_size=3,
|
|
1186
|
+
... num_layers=4,
|
|
1187
|
+
... dropout_rate=0.2,
|
|
1188
|
+
... dilation_rate=2,
|
|
1189
|
+
... activation="relu",
|
|
1190
|
+
... inputsize=10
|
|
1191
|
+
... )
|
|
1192
|
+
"""
|
|
1193
|
+
super(CNNModel, self).__init__()
|
|
1194
|
+
|
|
1195
|
+
self.num_filters = num_filters
|
|
1196
|
+
self.kernel_size = kernel_size
|
|
1197
|
+
self.num_layers = num_layers
|
|
1198
|
+
self.dropout_rate = dropout_rate
|
|
1199
|
+
self.dilation_rate = dilation_rate
|
|
1200
|
+
self.activation = activation
|
|
1201
|
+
self.inputsize = inputsize
|
|
1202
|
+
|
|
1203
|
+
self.layers = nn.ModuleList()
|
|
1204
|
+
|
|
1205
|
+
# Input layer
|
|
1206
|
+
self.layers.append(nn.Conv1d(inputsize, num_filters, kernel_size, padding="same"))
|
|
1207
|
+
self.layers.append(nn.BatchNorm1d(num_filters))
|
|
1208
|
+
self.layers.append(nn.Dropout(dropout_rate))
|
|
1209
|
+
if activation == "relu":
|
|
1210
|
+
self.layers.append(nn.ReLU())
|
|
1211
|
+
elif activation == "tanh":
|
|
1212
|
+
self.layers.append(nn.Tanh())
|
|
1213
|
+
else:
|
|
1214
|
+
self.layers.append(nn.ReLU())
|
|
1215
|
+
|
|
1216
|
+
# Intermediate layers
|
|
1217
|
+
for _ in range(num_layers - 2):
|
|
1218
|
+
self.layers.append(
|
|
1219
|
+
nn.Conv1d(
|
|
1220
|
+
num_filters,
|
|
1221
|
+
num_filters,
|
|
1222
|
+
kernel_size,
|
|
1223
|
+
dilation=dilation_rate,
|
|
1224
|
+
padding="same",
|
|
1225
|
+
)
|
|
1226
|
+
)
|
|
1227
|
+
self.layers.append(nn.BatchNorm1d(num_filters))
|
|
1228
|
+
self.layers.append(nn.Dropout(dropout_rate))
|
|
1229
|
+
if activation == "relu":
|
|
1230
|
+
self.layers.append(nn.ReLU())
|
|
1231
|
+
elif activation == "tanh":
|
|
1232
|
+
self.layers.append(nn.Tanh())
|
|
1233
|
+
else:
|
|
1234
|
+
self.layers.append(nn.ReLU())
|
|
1235
|
+
|
|
1236
|
+
# Output layer
|
|
1237
|
+
self.layers.append(nn.Conv1d(num_filters, inputsize, kernel_size, padding="same"))
|
|
1238
|
+
|
|
1239
|
+
def forward(self, x):
|
|
1240
|
+
"""
|
|
1241
|
+
Forward pass through all layers.
|
|
1242
|
+
|
|
1243
|
+
Applies each layer in the network sequentially to the input tensor.
|
|
1244
|
+
|
|
1245
|
+
Parameters
|
|
1246
|
+
----------
|
|
1247
|
+
x : torch.Tensor
|
|
1248
|
+
Input tensor to the forward pass. Shape should be compatible with the
|
|
1249
|
+
first layer's expected input dimensions.
|
|
1250
|
+
|
|
1251
|
+
Returns
|
|
1252
|
+
-------
|
|
1253
|
+
torch.Tensor
|
|
1254
|
+
Output tensor after passing through all layers. Shape will depend on
|
|
1255
|
+
the output dimensions of the last layer in the network.
|
|
1256
|
+
|
|
1257
|
+
Notes
|
|
1258
|
+
-----
|
|
1259
|
+
This method applies layers in the order they were added to the network.
|
|
1260
|
+
Each layer's forward method is called sequentially, with the output of
|
|
1261
|
+
one layer becoming the input to the next.
|
|
1262
|
+
|
|
1263
|
+
Examples
|
|
1264
|
+
--------
|
|
1265
|
+
>>> import torch
|
|
1266
|
+
>>> model = MyNetwork()
|
|
1267
|
+
>>> input_tensor = torch.randn(32, 10)
|
|
1268
|
+
>>> output = model.forward(input_tensor)
|
|
1269
|
+
>>> print(output.shape)
|
|
1270
|
+
torch.Size([32, 5])
|
|
1271
|
+
"""
|
|
1272
|
+
for layer in self.layers:
|
|
1273
|
+
x = layer(x)
|
|
1274
|
+
return x
|
|
1275
|
+
|
|
1276
|
+
def get_config(self):
|
|
1277
|
+
"""
|
|
1278
|
+
Get the configuration parameters of the model.
|
|
1279
|
+
|
|
1280
|
+
Returns
|
|
1281
|
+
-------
|
|
1282
|
+
dict
|
|
1283
|
+
A dictionary containing all configuration parameters with their current values:
|
|
1284
|
+
- num_filters: int, number of filters in the convolutional layers
|
|
1285
|
+
- kernel_size: int, size of the convolutional kernel
|
|
1286
|
+
- num_layers: int, number of layers in the network
|
|
1287
|
+
- dropout_rate: float, dropout rate for regularization
|
|
1288
|
+
- dilation_rate: int, dilation rate for dilated convolution
|
|
1289
|
+
- activation: str, activation function used in layers
|
|
1290
|
+
- inputsize: tuple, input dimensions of the model
|
|
1291
|
+
|
|
1292
|
+
Notes
|
|
1293
|
+
-----
|
|
1294
|
+
This method returns a copy of the current configuration. Modifications to the
|
|
1295
|
+
returned dictionary will not affect the original model configuration.
|
|
1296
|
+
|
|
1297
|
+
Examples
|
|
1298
|
+
--------
|
|
1299
|
+
>>> config = model.get_config()
|
|
1300
|
+
>>> print(config['num_filters'])
|
|
1301
|
+
32
|
|
1302
|
+
"""
|
|
1303
|
+
return {
|
|
1304
|
+
"num_filters": self.num_filters,
|
|
1305
|
+
"kernel_size": self.kernel_size,
|
|
1306
|
+
"num_layers": self.num_layers,
|
|
1307
|
+
"dropout_rate": self.dropout_rate,
|
|
1308
|
+
"dilation_rate": self.dilation_rate,
|
|
1309
|
+
"activation": self.activation,
|
|
1310
|
+
"inputsize": self.inputsize,
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
class CNNDLFilter(DeepLearningFilter):
|
|
1315
|
+
def __init__(
|
|
1316
|
+
self,
|
|
1317
|
+
num_filters: int = 10,
|
|
1318
|
+
kernel_size: int = 5,
|
|
1319
|
+
dilation_rate: int = 1,
|
|
1320
|
+
*args,
|
|
1321
|
+
**kwargs,
|
|
1322
|
+
) -> None:
|
|
1323
|
+
"""
|
|
1324
|
+
Initialize CNN deep learning filter.
|
|
1325
|
+
|
|
1326
|
+
Parameters
|
|
1327
|
+
----------
|
|
1328
|
+
num_filters : int, optional
|
|
1329
|
+
Number of convolutional filters to use, by default 10
|
|
1330
|
+
kernel_size : int, optional
|
|
1331
|
+
Size of the convolutional kernel, by default 5
|
|
1332
|
+
dilation_rate : int, optional
|
|
1333
|
+
Dilation rate for the convolutional layers, by default 1
|
|
1334
|
+
*args
|
|
1335
|
+
Variable length argument list passed to parent class
|
|
1336
|
+
**kwargs
|
|
1337
|
+
Arbitrary keyword arguments passed to parent class
|
|
1338
|
+
|
|
1339
|
+
Returns
|
|
1340
|
+
-------
|
|
1341
|
+
None
|
|
1342
|
+
This method initializes the instance and does not return any value
|
|
1343
|
+
|
|
1344
|
+
Notes
|
|
1345
|
+
-----
|
|
1346
|
+
This constructor sets up the basic configuration for a CNN filter with
|
|
1347
|
+
specified number of filters, kernel size, and dilation rate. The network
|
|
1348
|
+
type is automatically set to "cnn" and information is stored in infodict
|
|
1349
|
+
for later reference.
|
|
1350
|
+
|
|
1351
|
+
Examples
|
|
1352
|
+
--------
|
|
1353
|
+
>>> filter = CNNDLFilter(num_filters=32, kernel_size=3, dilation_rate=2)
|
|
1354
|
+
>>> print(filter.num_filters)
|
|
1355
|
+
32
|
|
1356
|
+
"""
|
|
1357
|
+
self.num_filters = num_filters
|
|
1358
|
+
self.kernel_size = kernel_size
|
|
1359
|
+
self.dilation_rate = dilation_rate
|
|
1360
|
+
self.nettype = "cnn"
|
|
1361
|
+
self.infodict["nettype"] = self.nettype
|
|
1362
|
+
self.infodict["num_filters"] = self.num_filters
|
|
1363
|
+
self.infodict["kernel_size"] = self.kernel_size
|
|
1364
|
+
super(CNNDLFilter, self).__init__(*args, **kwargs)
|
|
1365
|
+
|
|
1366
|
+
def getname(self):
|
|
1367
|
+
"""
|
|
1368
|
+
Generate and configure the model name and path based on current parameters.
|
|
1369
|
+
|
|
1370
|
+
This method constructs a descriptive model name string using various instance
|
|
1371
|
+
attributes and creates the corresponding directory path. The generated name
|
|
1372
|
+
includes information about model architecture, hyperparameters, and configuration
|
|
1373
|
+
options. The method also ensures the model directory exists by creating it if
|
|
1374
|
+
necessary.
|
|
1375
|
+
|
|
1376
|
+
Parameters
|
|
1377
|
+
----------
|
|
1378
|
+
self : object
|
|
1379
|
+
The instance containing model configuration parameters.
|
|
1380
|
+
|
|
1381
|
+
Returns
|
|
1382
|
+
-------
|
|
1383
|
+
None
|
|
1384
|
+
This method does not return a value but modifies instance attributes:
|
|
1385
|
+
- self.modelname: Generated model name string
|
|
1386
|
+
- self.modelpath: Full path to the model directory
|
|
1387
|
+
|
|
1388
|
+
Notes
|
|
1389
|
+
-----
|
|
1390
|
+
The generated model name follows a specific format:
|
|
1391
|
+
"model_cnn_pytorch_wXXX_lYY_fnZZ_flZZ_eXXX_tY_ctZ_sZ_dZ_activation[options]"
|
|
1392
|
+
|
|
1393
|
+
Where:
|
|
1394
|
+
- XXX: window_size (3 digits zero-padded)
|
|
1395
|
+
- YY: num_layers (2 digits zero-padded)
|
|
1396
|
+
- ZZ: num_filters (2 digits zero-padded)
|
|
1397
|
+
- ZZ: kernel_size (2 digits zero-padded)
|
|
1398
|
+
- XXX: num_epochs (3 digits zero-padded)
|
|
1399
|
+
- Y: excludethresh (single digit)
|
|
1400
|
+
- Z: corrthresh (single digit)
|
|
1401
|
+
- Z: step (single digit)
|
|
1402
|
+
- Z: dilation_rate (single digit)
|
|
1403
|
+
|
|
1404
|
+
Options are appended if corresponding boolean flags are True:
|
|
1405
|
+
- _usebadpts: when usebadpts is True
|
|
1406
|
+
- _excludebysubject: when excludebysubject is True
|
|
1407
|
+
|
|
1408
|
+
Examples
|
|
1409
|
+
--------
|
|
1410
|
+
>>> model = MyModel()
|
|
1411
|
+
>>> model.window_size = 128
|
|
1412
|
+
>>> model.num_layers = 3
|
|
1413
|
+
>>> model.num_filters = 16
|
|
1414
|
+
>>> model.kernel_size = 3
|
|
1415
|
+
>>> model.num_epochs = 100
|
|
1416
|
+
>>> model.excludethresh = 0.5
|
|
1417
|
+
>>> model.corrthresh = 0.8
|
|
1418
|
+
>>> model.step = 1
|
|
1419
|
+
>>> model.dilation_rate = 2
|
|
1420
|
+
>>> model.activation = "relu"
|
|
1421
|
+
>>> model.usebadpts = True
|
|
1422
|
+
>>> model.excludebysubject = False
|
|
1423
|
+
>>> model.namesuffix = "test"
|
|
1424
|
+
>>> model.getname()
|
|
1425
|
+
>>> print(model.modelname)
|
|
1426
|
+
'model_cnn_pytorch_w128_l03_fn16_fl03_e100_t0_ct0_s1_d2_relu_usebadpts_test'
|
|
1427
|
+
"""
|
|
1428
|
+
self.modelname = "_".join(
|
|
1429
|
+
[
|
|
1430
|
+
"model",
|
|
1431
|
+
"cnn",
|
|
1432
|
+
"pytorch",
|
|
1433
|
+
"w" + str(self.window_size).zfill(3),
|
|
1434
|
+
"l" + str(self.num_layers).zfill(2),
|
|
1435
|
+
"fn" + str(self.num_filters).zfill(2),
|
|
1436
|
+
"fl" + str(self.kernel_size).zfill(2),
|
|
1437
|
+
"e" + str(self.num_epochs).zfill(3),
|
|
1438
|
+
"t" + str(self.excludethresh),
|
|
1439
|
+
"ct" + str(self.corrthresh),
|
|
1440
|
+
"s" + str(self.step),
|
|
1441
|
+
"d" + str(self.dilation_rate),
|
|
1442
|
+
self.activation,
|
|
1443
|
+
]
|
|
1444
|
+
)
|
|
1445
|
+
if self.usebadpts:
|
|
1446
|
+
self.modelname += "_usebadpts"
|
|
1447
|
+
if self.excludebysubject:
|
|
1448
|
+
self.modelname += "_excludebysubject"
|
|
1449
|
+
if self.namesuffix is not None:
|
|
1450
|
+
self.modelname += "_" + self.namesuffix
|
|
1451
|
+
self.modelpath = os.path.join(self.modelroot, self.modelname)
|
|
1452
|
+
|
|
1453
|
+
try:
|
|
1454
|
+
os.makedirs(self.modelpath)
|
|
1455
|
+
except OSError:
|
|
1456
|
+
pass
|
|
1457
|
+
|
|
1458
|
+
def makenet(self):
|
|
1459
|
+
"""
|
|
1460
|
+
Create and configure a CNN model for neural network training.
|
|
1461
|
+
|
|
1462
|
+
This method initializes a CNNModel with the specified parameters and moves
|
|
1463
|
+
it to the designated device (CPU or GPU). The model configuration is
|
|
1464
|
+
determined by the instance attributes set prior to calling this method.
|
|
1465
|
+
|
|
1466
|
+
Parameters
|
|
1467
|
+
----------
|
|
1468
|
+
self : object
|
|
1469
|
+
The instance containing the following attributes:
|
|
1470
|
+
- num_filters : int
|
|
1471
|
+
Number of filters in each convolutional layer
|
|
1472
|
+
- kernel_size : int or tuple
|
|
1473
|
+
Size of the convolutional kernel
|
|
1474
|
+
- num_layers : int
|
|
1475
|
+
Number of convolutional layers in the network
|
|
1476
|
+
- dropout_rate : float
|
|
1477
|
+
Dropout rate for regularization
|
|
1478
|
+
- dilation_rate : int or tuple
|
|
1479
|
+
Dilation rate for dilated convolutions
|
|
1480
|
+
- activation : str or callable
|
|
1481
|
+
Activation function to use
|
|
1482
|
+
- inputsize : tuple
|
|
1483
|
+
Input dimensions for the model
|
|
1484
|
+
- device : torch.device
|
|
1485
|
+
Device to move the model to (CPU or GPU)
|
|
1486
|
+
|
|
1487
|
+
Returns
|
|
1488
|
+
-------
|
|
1489
|
+
None
|
|
1490
|
+
This method does not return any value. It modifies the instance
|
|
1491
|
+
by setting the `model` attribute to the created CNNModel.
|
|
1492
|
+
|
|
1493
|
+
Notes
|
|
1494
|
+
-----
|
|
1495
|
+
The method assumes that all required attributes are properly initialized
|
|
1496
|
+
before calling. The model is automatically moved to the specified device
|
|
1497
|
+
using the `.to()` method.
|
|
1498
|
+
|
|
1499
|
+
Examples
|
|
1500
|
+
--------
|
|
1501
|
+
>>> # Assuming all required attributes are set
|
|
1502
|
+
>>> makenet()
|
|
1503
|
+
>>> # Model is now available as self.model
|
|
1504
|
+
>>> print(self.model)
|
|
1505
|
+
CNNModel(...)
|
|
1506
|
+
"""
|
|
1507
|
+
self.model = CNNModel(
|
|
1508
|
+
self.num_filters,
|
|
1509
|
+
self.kernel_size,
|
|
1510
|
+
self.num_layers,
|
|
1511
|
+
self.dropout_rate,
|
|
1512
|
+
self.dilation_rate,
|
|
1513
|
+
self.activation,
|
|
1514
|
+
self.inputsize,
|
|
1515
|
+
)
|
|
1516
|
+
self.model.to(self.device)
|
|
1517
|
+
|
|
1518
|
+
|
|
1519
|
+
class DenseAutoencoderModel(nn.Module):
|
|
1520
|
+
def __init__(
|
|
1521
|
+
self,
|
|
1522
|
+
window_size: int,
|
|
1523
|
+
encoding_dim: int,
|
|
1524
|
+
num_layers: int,
|
|
1525
|
+
dropout_rate: float,
|
|
1526
|
+
activation: str,
|
|
1527
|
+
inputsize: int,
|
|
1528
|
+
) -> None:
|
|
1529
|
+
"""
|
|
1530
|
+
Initialize a dense autoencoder model with configurable architecture.
|
|
1531
|
+
|
|
1532
|
+
This constructor builds a symmetric dense autoencoder with customizable number of layers,
|
|
1533
|
+
encoding dimension, dropout rate, and activation function. The model architecture follows
|
|
1534
|
+
a symmetric encoder-decoder structure, where the bottleneck layer reduces the feature space
|
|
1535
|
+
to `encoding_dim`, and then expands back to the original input size.
|
|
1536
|
+
|
|
1537
|
+
Parameters
|
|
1538
|
+
----------
|
|
1539
|
+
window_size : int
|
|
1540
|
+
The size of the input window (number of time steps or features per sample).
|
|
1541
|
+
encoding_dim : int
|
|
1542
|
+
The dimension of the latent space (bottleneck layer size).
|
|
1543
|
+
num_layers : int
|
|
1544
|
+
Total number of layers in the autoencoder (including input and output).
|
|
1545
|
+
dropout_rate : float
|
|
1546
|
+
Dropout rate applied to all hidden layers to prevent overfitting.
|
|
1547
|
+
activation : str
|
|
1548
|
+
Activation function to use in hidden layers. Supported values: 'relu', 'tanh'.
|
|
1549
|
+
Defaults to 'relu' if an unsupported value is provided.
|
|
1550
|
+
inputsize : int
|
|
1551
|
+
The number of features per time step in the input data.
|
|
1552
|
+
|
|
1553
|
+
Returns
|
|
1554
|
+
-------
|
|
1555
|
+
None
|
|
1556
|
+
This method initializes the model in-place and does not return any value.
|
|
1557
|
+
|
|
1558
|
+
Notes
|
|
1559
|
+
-----
|
|
1560
|
+
- The model uses batch normalization after each linear layer except the output layer.
|
|
1561
|
+
- The architecture is symmetric: the number of neurons decreases from input to bottleneck
|
|
1562
|
+
and then increases back to the output size.
|
|
1563
|
+
- Logging is performed at various stages to track layer sizes during construction.
|
|
1564
|
+
|
|
1565
|
+
Examples
|
|
1566
|
+
--------
|
|
1567
|
+
>>> model = DenseAutoencoderModel(
|
|
1568
|
+
... window_size=10,
|
|
1569
|
+
... encoding_dim=5,
|
|
1570
|
+
... num_layers=4,
|
|
1571
|
+
... dropout_rate=0.2,
|
|
1572
|
+
... activation="relu",
|
|
1573
|
+
... inputsize=3
|
|
1574
|
+
... )
|
|
1575
|
+
"""
|
|
1576
|
+
super(DenseAutoencoderModel, self).__init__()
|
|
1577
|
+
|
|
1578
|
+
self.window_size = window_size
|
|
1579
|
+
self.encoding_dim = encoding_dim
|
|
1580
|
+
self.num_layers = num_layers
|
|
1581
|
+
self.dropout_rate = dropout_rate
|
|
1582
|
+
self.activation = activation
|
|
1583
|
+
self.inputsize = inputsize
|
|
1584
|
+
|
|
1585
|
+
self.layers = nn.ModuleList()
|
|
1586
|
+
|
|
1587
|
+
# Calculate initial size factor
|
|
1588
|
+
sizefac = 2
|
|
1589
|
+
for i in range(1, num_layers - 1):
|
|
1590
|
+
sizefac = int(sizefac * 2)
|
|
1591
|
+
LGR.info(f"input layer - sizefac: {sizefac}")
|
|
1592
|
+
|
|
1593
|
+
# Input layer
|
|
1594
|
+
self.layers.append(nn.Linear(window_size * inputsize, sizefac * encoding_dim))
|
|
1595
|
+
self.layers.append(nn.BatchNorm1d(sizefac * encoding_dim))
|
|
1596
|
+
self.layers.append(nn.Dropout(dropout_rate))
|
|
1597
|
+
if activation == "relu":
|
|
1598
|
+
self.layers.append(nn.ReLU())
|
|
1599
|
+
elif activation == "tanh":
|
|
1600
|
+
self.layers.append(nn.Tanh())
|
|
1601
|
+
else:
|
|
1602
|
+
self.layers.append(nn.ReLU())
|
|
1603
|
+
|
|
1604
|
+
# Encoding layers
|
|
1605
|
+
for i in range(1, num_layers - 1):
|
|
1606
|
+
sizefac = int(sizefac // 2)
|
|
1607
|
+
LGR.info(f"encoder layer {i + 1}, sizefac: {sizefac}")
|
|
1608
|
+
self.layers.append(nn.Linear(sizefac * 2 * encoding_dim, sizefac * encoding_dim))
|
|
1609
|
+
self.layers.append(nn.BatchNorm1d(sizefac * encoding_dim))
|
|
1610
|
+
self.layers.append(nn.Dropout(dropout_rate))
|
|
1611
|
+
if activation == "relu":
|
|
1612
|
+
self.layers.append(nn.ReLU())
|
|
1613
|
+
elif activation == "tanh":
|
|
1614
|
+
self.layers.append(nn.Tanh())
|
|
1615
|
+
else:
|
|
1616
|
+
self.layers.append(nn.ReLU())
|
|
1617
|
+
|
|
1618
|
+
# Encoding layer (bottleneck)
|
|
1619
|
+
sizefac = int(sizefac // 2)
|
|
1620
|
+
LGR.info(f"encoding layer - sizefac: {sizefac}")
|
|
1621
|
+
self.layers.append(nn.Linear(sizefac * 2 * encoding_dim, encoding_dim))
|
|
1622
|
+
self.layers.append(nn.BatchNorm1d(encoding_dim))
|
|
1623
|
+
self.layers.append(nn.Dropout(dropout_rate))
|
|
1624
|
+
if activation == "relu":
|
|
1625
|
+
self.layers.append(nn.ReLU())
|
|
1626
|
+
elif activation == "tanh":
|
|
1627
|
+
self.layers.append(nn.Tanh())
|
|
1628
|
+
else:
|
|
1629
|
+
self.layers.append(nn.ReLU())
|
|
1630
|
+
|
|
1631
|
+
# Decoding layers
|
|
1632
|
+
for i in range(1, num_layers):
|
|
1633
|
+
sizefac = int(sizefac * 2)
|
|
1634
|
+
LGR.info(f"decoding layer {i}, sizefac: {sizefac}")
|
|
1635
|
+
if i == 1:
|
|
1636
|
+
self.layers.append(nn.Linear(encoding_dim, sizefac * encoding_dim))
|
|
1637
|
+
else:
|
|
1638
|
+
self.layers.append(nn.Linear(sizefac // 2 * encoding_dim, sizefac * encoding_dim))
|
|
1639
|
+
self.layers.append(nn.BatchNorm1d(sizefac * encoding_dim))
|
|
1640
|
+
self.layers.append(nn.Dropout(dropout_rate))
|
|
1641
|
+
if activation == "relu":
|
|
1642
|
+
self.layers.append(nn.ReLU())
|
|
1643
|
+
elif activation == "tanh":
|
|
1644
|
+
self.layers.append(nn.Tanh())
|
|
1645
|
+
else:
|
|
1646
|
+
self.layers.append(nn.ReLU())
|
|
1647
|
+
|
|
1648
|
+
# Output layer
|
|
1649
|
+
self.layers.append(nn.Linear(sizefac * encoding_dim, window_size * inputsize))
|
|
1650
|
+
|
|
1651
|
+
def forward(self, x):
|
|
1652
|
+
"""
|
|
1653
|
+
Forward pass through the network layers.
|
|
1654
|
+
|
|
1655
|
+
Applies a series of layers to the input tensor, flattening and reshaping
|
|
1656
|
+
as needed for processing.
|
|
1657
|
+
|
|
1658
|
+
Parameters
|
|
1659
|
+
----------
|
|
1660
|
+
x : torch.Tensor
|
|
1661
|
+
Input tensor with shape (batch, channels, length) where batch is the
|
|
1662
|
+
number of samples, channels is the number of channels, and length is
|
|
1663
|
+
the sequence length.
|
|
1664
|
+
|
|
1665
|
+
Returns
|
|
1666
|
+
-------
|
|
1667
|
+
torch.Tensor
|
|
1668
|
+
Output tensor with shape (batch, channels, length) where the
|
|
1669
|
+
dimensions match the input shape after processing through the layers.
|
|
1670
|
+
|
|
1671
|
+
Notes
|
|
1672
|
+
-----
|
|
1673
|
+
The function first flattens the input from (batch, channels, length) to
|
|
1674
|
+
(batch, channels*length) to enable processing through linear layers,
|
|
1675
|
+
then reshapes the output back to the original format.
|
|
1676
|
+
|
|
1677
|
+
Examples
|
|
1678
|
+
--------
|
|
1679
|
+
>>> import torch
|
|
1680
|
+
>>> # Assuming self.layers contains appropriate layer definitions
|
|
1681
|
+
>>> x = torch.randn(32, 3, 100) # batch_size=32, channels=3, length=100
|
|
1682
|
+
>>> output = model.forward(x)
|
|
1683
|
+
>>> output.shape
|
|
1684
|
+
torch.Size([32, 3, 100])
|
|
1685
|
+
"""
|
|
1686
|
+
# Flatten input from (batch, channels, length) to (batch, channels*length)
|
|
1687
|
+
batch_size = x.shape[0]
|
|
1688
|
+
x = x.reshape(batch_size, -1)
|
|
1689
|
+
|
|
1690
|
+
for layer in self.layers:
|
|
1691
|
+
x = layer(x)
|
|
1692
|
+
|
|
1693
|
+
# Reshape back to (batch, channels, length)
|
|
1694
|
+
x = x.reshape(batch_size, self.inputsize, self.window_size)
|
|
1695
|
+
return x
|
|
1696
|
+
|
|
1697
|
+
def get_config(self):
|
|
1698
|
+
"""
|
|
1699
|
+
Get the configuration parameters of the model.
|
|
1700
|
+
|
|
1701
|
+
Returns
|
|
1702
|
+
-------
|
|
1703
|
+
dict
|
|
1704
|
+
A dictionary containing the model configuration parameters with the following keys:
|
|
1705
|
+
- "window_size" (int): The size of the sliding window used for input sequences
|
|
1706
|
+
- "encoding_dim" (int): The dimensionality of the encoding layer
|
|
1707
|
+
- "num_layers" (int): The number of layers in the model
|
|
1708
|
+
- "dropout_rate" (float): The dropout rate for regularization
|
|
1709
|
+
- "activation" (str): The activation function used in the model
|
|
1710
|
+
- "inputsize" (int): The size of the input features
|
|
1711
|
+
|
|
1712
|
+
Notes
|
|
1713
|
+
-----
|
|
1714
|
+
This method returns a copy of the internal configuration parameters. Modifications
|
|
1715
|
+
to the returned dictionary will not affect the original model configuration.
|
|
1716
|
+
|
|
1717
|
+
Examples
|
|
1718
|
+
--------
|
|
1719
|
+
>>> model = MyModel()
|
|
1720
|
+
>>> config = model.get_config()
|
|
1721
|
+
>>> print(config['window_size'])
|
|
1722
|
+
10
|
|
1723
|
+
"""
|
|
1724
|
+
return {
|
|
1725
|
+
"window_size": self.window_size,
|
|
1726
|
+
"encoding_dim": self.encoding_dim,
|
|
1727
|
+
"num_layers": self.num_layers,
|
|
1728
|
+
"dropout_rate": self.dropout_rate,
|
|
1729
|
+
"activation": self.activation,
|
|
1730
|
+
"inputsize": self.inputsize,
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
class DenseAutoencoderDLFilter(DeepLearningFilter):
|
|
1735
|
+
def __init__(self, encoding_dim: int = 10, *args, **kwargs) -> None:
|
|
1736
|
+
"""
|
|
1737
|
+
Initialize DenseAutoencoderDLFilter instance.
|
|
1738
|
+
|
|
1739
|
+
Parameters
|
|
1740
|
+
----------
|
|
1741
|
+
encoding_dim : int, default=10
|
|
1742
|
+
Dimension of the encoding layer in the autoencoder. This determines
|
|
1743
|
+
the size of the latent representation learned by the model.
|
|
1744
|
+
*args
|
|
1745
|
+
Variable length argument list passed to the parent class constructor.
|
|
1746
|
+
**kwargs
|
|
1747
|
+
Arbitrary keyword arguments passed to the parent class constructor.
|
|
1748
|
+
|
|
1749
|
+
Returns
|
|
1750
|
+
-------
|
|
1751
|
+
None
|
|
1752
|
+
This method initializes the instance and does not return any value.
|
|
1753
|
+
|
|
1754
|
+
Notes
|
|
1755
|
+
-----
|
|
1756
|
+
This constructor sets up the autoencoder architecture by:
|
|
1757
|
+
1. Storing the encoding dimension as an instance attribute
|
|
1758
|
+
2. Setting the network type to "autoencoder"
|
|
1759
|
+
3. Updating the info dictionary with network type and encoding dimension
|
|
1760
|
+
4. Calling the parent class constructor with any additional arguments
|
|
1761
|
+
|
|
1762
|
+
Examples
|
|
1763
|
+
--------
|
|
1764
|
+
>>> filter = DenseAutoencoderDLFilter(encoding_dim=15)
|
|
1765
|
+
>>> print(filter.encoding_dim)
|
|
1766
|
+
15
|
|
1767
|
+
>>> print(filter.nettype)
|
|
1768
|
+
'autoencoder'
|
|
1769
|
+
"""
|
|
1770
|
+
self.encoding_dim = encoding_dim
|
|
1771
|
+
self.nettype = "autoencoder"
|
|
1772
|
+
self.infodict["nettype"] = self.nettype
|
|
1773
|
+
self.infodict["encoding_dim"] = self.encoding_dim
|
|
1774
|
+
super(DenseAutoencoderDLFilter, self).__init__(*args, **kwargs)
|
|
1775
|
+
|
|
1776
|
+
def getname(self):
|
|
1777
|
+
"""
|
|
1778
|
+
Generate and return the model name and path based on current configuration parameters.
|
|
1779
|
+
|
|
1780
|
+
This method constructs a descriptive model name string using various configuration
|
|
1781
|
+
parameters and creates the corresponding directory path for model storage. The
|
|
1782
|
+
generated name includes information about window size, encoding dimensions, epochs,
|
|
1783
|
+
thresholds, step size, activation function, and additional flags.
|
|
1784
|
+
|
|
1785
|
+
Parameters
|
|
1786
|
+
----------
|
|
1787
|
+
self : object
|
|
1788
|
+
The instance containing the following attributes:
|
|
1789
|
+
- window_size : int
|
|
1790
|
+
Size of the sliding window
|
|
1791
|
+
- encoding_dim : int
|
|
1792
|
+
Dimension of the encoding layer
|
|
1793
|
+
- num_epochs : int
|
|
1794
|
+
Number of training epochs
|
|
1795
|
+
- excludethresh : float
|
|
1796
|
+
Threshold for excluding data points
|
|
1797
|
+
- corrthresh : float
|
|
1798
|
+
Correlation threshold for filtering
|
|
1799
|
+
- step : int
|
|
1800
|
+
Step size for sliding window
|
|
1801
|
+
- activation : str
|
|
1802
|
+
Activation function name
|
|
1803
|
+
- usebadpts : bool
|
|
1804
|
+
Flag indicating whether to use bad points
|
|
1805
|
+
- excludebysubject : bool
|
|
1806
|
+
Flag indicating whether to exclude by subject
|
|
1807
|
+
- namesuffix : str, optional
|
|
1808
|
+
Additional suffix to append to model name
|
|
1809
|
+
- modelroot : str
|
|
1810
|
+
Root directory for model storage
|
|
1811
|
+
|
|
1812
|
+
Returns
|
|
1813
|
+
-------
|
|
1814
|
+
None
|
|
1815
|
+
This method modifies the instance attributes in-place:
|
|
1816
|
+
- self.modelname : str
|
|
1817
|
+
Generated model name string
|
|
1818
|
+
- self.modelpath : str
|
|
1819
|
+
Full path to the model directory
|
|
1820
|
+
|
|
1821
|
+
Notes
|
|
1822
|
+
-----
|
|
1823
|
+
The generated model name follows a specific format:
|
|
1824
|
+
"model_denseautoencoder_pytorch_wXXX_enXXX_eXXX_tX.XX_ctX.XX_sX_activation[flags]"
|
|
1825
|
+
|
|
1826
|
+
Examples
|
|
1827
|
+
--------
|
|
1828
|
+
>>> model = MyModel()
|
|
1829
|
+
>>> model.window_size = 100
|
|
1830
|
+
>>> model.encoding_dim = 50
|
|
1831
|
+
>>> model.num_epochs = 1000
|
|
1832
|
+
>>> model.excludethresh = 0.5
|
|
1833
|
+
>>> model.corrthresh = 0.8
|
|
1834
|
+
>>> model.step = 10
|
|
1835
|
+
>>> model.activation = 'relu'
|
|
1836
|
+
>>> model.usebadpts = True
|
|
1837
|
+
>>> model.excludebysubject = False
|
|
1838
|
+
>>> model.namesuffix = 'test'
|
|
1839
|
+
>>> model.modelroot = '/path/to/models'
|
|
1840
|
+
>>> model.getname()
|
|
1841
|
+
>>> print(model.modelname)
|
|
1842
|
+
'model_denseautoencoder_pytorch_w100_en050_e1000_t0.5_ct0.8_s10_relu_usebadpts_test'
|
|
1843
|
+
"""
|
|
1844
|
+
self.modelname = "_".join(
|
|
1845
|
+
[
|
|
1846
|
+
"model",
|
|
1847
|
+
"denseautoencoder",
|
|
1848
|
+
"pytorch",
|
|
1849
|
+
"w" + str(self.window_size).zfill(3),
|
|
1850
|
+
"en" + str(self.encoding_dim).zfill(3),
|
|
1851
|
+
"e" + str(self.num_epochs).zfill(3),
|
|
1852
|
+
"t" + str(self.excludethresh),
|
|
1853
|
+
"ct" + str(self.corrthresh),
|
|
1854
|
+
"s" + str(self.step),
|
|
1855
|
+
self.activation,
|
|
1856
|
+
]
|
|
1857
|
+
)
|
|
1858
|
+
if self.usebadpts:
|
|
1859
|
+
self.modelname += "_usebadpts"
|
|
1860
|
+
if self.excludebysubject:
|
|
1861
|
+
self.modelname += "_excludebysubject"
|
|
1862
|
+
if self.namesuffix is not None:
|
|
1863
|
+
self.modelname += "_" + self.namesuffix
|
|
1864
|
+
self.modelpath = os.path.join(self.modelroot, self.modelname)
|
|
1865
|
+
|
|
1866
|
+
try:
|
|
1867
|
+
os.makedirs(self.modelpath)
|
|
1868
|
+
except OSError:
|
|
1869
|
+
pass
|
|
1870
|
+
|
|
1871
|
+
def makenet(self):
|
|
1872
|
+
"""
|
|
1873
|
+
Create and configure a dense autoencoder model.
|
|
1874
|
+
|
|
1875
|
+
This method initializes a DenseAutoencoderModel with the specified parameters
|
|
1876
|
+
and moves it to the designated device (CPU or GPU).
|
|
1877
|
+
|
|
1878
|
+
Parameters
|
|
1879
|
+
----------
|
|
1880
|
+
self : object
|
|
1881
|
+
The instance of the class containing this method. Expected to have the
|
|
1882
|
+
following attributes:
|
|
1883
|
+
- window_size : int
|
|
1884
|
+
- encoding_dim : int
|
|
1885
|
+
- num_layers : int
|
|
1886
|
+
- dropout_rate : float
|
|
1887
|
+
- activation : str or callable
|
|
1888
|
+
- inputsize : int
|
|
1889
|
+
- device : torch.device
|
|
1890
|
+
|
|
1891
|
+
Returns
|
|
1892
|
+
-------
|
|
1893
|
+
None
|
|
1894
|
+
This method does not return any value. It sets the model attribute
|
|
1895
|
+
of the instance to the created DenseAutoencoderModel.
|
|
1896
|
+
|
|
1897
|
+
Notes
|
|
1898
|
+
-----
|
|
1899
|
+
The model is automatically moved to the device specified by self.device.
|
|
1900
|
+
This method should be called after all required parameters have been set
|
|
1901
|
+
on the instance.
|
|
1902
|
+
|
|
1903
|
+
Examples
|
|
1904
|
+
--------
|
|
1905
|
+
>>> # Assuming a class with the makenet method
|
|
1906
|
+
>>> instance = MyClass()
|
|
1907
|
+
>>> instance.window_size = 100
|
|
1908
|
+
>>> instance.encoding_dim = 32
|
|
1909
|
+
>>> instance.num_layers = 3
|
|
1910
|
+
>>> instance.dropout_rate = 0.2
|
|
1911
|
+
>>> instance.activation = 'relu'
|
|
1912
|
+
>>> instance.inputsize = 100
|
|
1913
|
+
>>> instance.device = torch.device('cuda')
|
|
1914
|
+
>>> instance.makenet()
|
|
1915
|
+
>>> # Model is now available as instance.model
|
|
1916
|
+
"""
|
|
1917
|
+
self.model = DenseAutoencoderModel(
|
|
1918
|
+
self.window_size,
|
|
1919
|
+
self.encoding_dim,
|
|
1920
|
+
self.num_layers,
|
|
1921
|
+
self.dropout_rate,
|
|
1922
|
+
self.activation,
|
|
1923
|
+
self.inputsize,
|
|
1924
|
+
)
|
|
1925
|
+
self.model.to(self.device)
|
|
1926
|
+
|
|
1927
|
+
|
|
1928
|
+
class MultiscaleCNNModel(nn.Module):
|
|
1929
|
+
def __init__(
|
|
1930
|
+
self,
|
|
1931
|
+
num_filters,
|
|
1932
|
+
kernel_sizes,
|
|
1933
|
+
input_lens,
|
|
1934
|
+
input_width,
|
|
1935
|
+
dilation_rate,
|
|
1936
|
+
):
|
|
1937
|
+
"""
|
|
1938
|
+
Initialize the MultiscaleCNNModel.
|
|
1939
|
+
|
|
1940
|
+
This constructor initializes a multiscale CNN model with three parallel branches
|
|
1941
|
+
processing input at different scales. Each branch uses dilated convolutions to
|
|
1942
|
+
capture features at different receptive fields.
|
|
1943
|
+
|
|
1944
|
+
Parameters
|
|
1945
|
+
----------
|
|
1946
|
+
num_filters : int
|
|
1947
|
+
Number of filters (channels) to use in the convolutional layers.
|
|
1948
|
+
kernel_sizes : list of int
|
|
1949
|
+
List of three kernel sizes for the three branches (small, medium, large scales).
|
|
1950
|
+
input_lens : list of int
|
|
1951
|
+
List of input lengths for each branch, corresponding to the input sequence lengths.
|
|
1952
|
+
input_width : int
|
|
1953
|
+
Width of the input features (number of input channels).
|
|
1954
|
+
dilation_rate : int
|
|
1955
|
+
Dilation rate to use in the dilated convolutional layers.
|
|
1956
|
+
|
|
1957
|
+
Returns
|
|
1958
|
+
-------
|
|
1959
|
+
None
|
|
1960
|
+
This method initializes the model instance and does not return any value.
|
|
1961
|
+
|
|
1962
|
+
Notes
|
|
1963
|
+
-----
|
|
1964
|
+
The model creates three parallel branches with different kernel sizes to capture
|
|
1965
|
+
multi-scale temporal features. Each branch uses dilated convolutions to increase
|
|
1966
|
+
the receptive field without increasing the number of parameters significantly.
|
|
1967
|
+
|
|
1968
|
+
The final dense layer reduces the combined features to a single output value,
|
|
1969
|
+
followed by a sigmoid activation for binary classification.
|
|
1970
|
+
|
|
1971
|
+
Examples
|
|
1972
|
+
--------
|
|
1973
|
+
>>> model = MultiscaleCNNModel(
|
|
1974
|
+
... num_filters=64,
|
|
1975
|
+
... kernel_sizes=[3, 5, 7],
|
|
1976
|
+
... input_lens=[100, 100, 100],
|
|
1977
|
+
... input_width=10,
|
|
1978
|
+
... dilation_rate=2
|
|
1979
|
+
... )
|
|
1980
|
+
"""
|
|
1981
|
+
super(MultiscaleCNNModel, self).__init__()
|
|
1982
|
+
|
|
1983
|
+
self.num_filters = num_filters
|
|
1984
|
+
self.kernel_sizes = kernel_sizes
|
|
1985
|
+
self.input_lens = input_lens
|
|
1986
|
+
self.input_width = input_width
|
|
1987
|
+
self.dilation_rate = dilation_rate
|
|
1988
|
+
|
|
1989
|
+
# Create three separate branches for different scales
|
|
1990
|
+
self.branch_small = self._make_branch(kernel_sizes[0])
|
|
1991
|
+
self.branch_med = self._make_branch(kernel_sizes[1])
|
|
1992
|
+
self.branch_large = self._make_branch(kernel_sizes[2])
|
|
1993
|
+
|
|
1994
|
+
# Final dense layer
|
|
1995
|
+
self.fc = nn.Linear(150, 1)
|
|
1996
|
+
self.sigmoid = nn.Sigmoid()
|
|
1997
|
+
|
|
1998
|
+
def _make_branch(self, kernel_size):
|
|
1999
|
+
"""
|
|
2000
|
+
Create a convolutional branch for the neural network architecture.
|
|
2001
|
+
|
|
2002
|
+
This method constructs a sequential neural network branch consisting of
|
|
2003
|
+
convolutional, pooling, flattening, linear, activation, and dropout layers.
|
|
2004
|
+
|
|
2005
|
+
Parameters
|
|
2006
|
+
----------
|
|
2007
|
+
kernel_size : int
|
|
2008
|
+
The size of the convolutional kernel to be used in the Conv1d layer.
|
|
2009
|
+
|
|
2010
|
+
Returns
|
|
2011
|
+
-------
|
|
2012
|
+
torch.nn.Sequential
|
|
2013
|
+
A sequential container containing the following layers:
|
|
2014
|
+
- Conv1d: 1D convolutional layer with input_width as input channels
|
|
2015
|
+
- AdaptiveMaxPool1d: 1D adaptive max pooling with output size 1
|
|
2016
|
+
- Flatten: Flattens the tensor for linear layer input
|
|
2017
|
+
- Linear: Linear layer with num_filters input features and 50 output features
|
|
2018
|
+
- Tanh: Hyperbolic tangent activation function
|
|
2019
|
+
- Dropout: 30% dropout regularization
|
|
2020
|
+
|
|
2021
|
+
Notes
|
|
2022
|
+
-----
|
|
2023
|
+
The branch is designed to process 1D input data through a convolutional
|
|
2024
|
+
feature extraction pathway followed by a fully connected classifier head.
|
|
2025
|
+
The padding="same" parameter ensures the output size matches the input size
|
|
2026
|
+
for the convolutional layer.
|
|
2027
|
+
|
|
2028
|
+
Examples
|
|
2029
|
+
--------
|
|
2030
|
+
>>> branch = self._make_branch(kernel_size=3)
|
|
2031
|
+
>>> print(type(branch))
|
|
2032
|
+
<class 'torch.nn.modules.container.Sequential'>
|
|
2033
|
+
"""
|
|
2034
|
+
return nn.Sequential(
|
|
2035
|
+
nn.Conv1d(self.input_width, self.num_filters, kernel_size, padding="same"),
|
|
2036
|
+
nn.AdaptiveMaxPool1d(1),
|
|
2037
|
+
nn.Flatten(),
|
|
2038
|
+
nn.Linear(self.num_filters, 50),
|
|
2039
|
+
nn.Tanh(),
|
|
2040
|
+
nn.Dropout(0.3),
|
|
2041
|
+
)
|
|
2042
|
+
|
|
2043
|
+
def forward(self, x_small, x_med, x_large):
|
|
2044
|
+
"""
|
|
2045
|
+
Forward pass of the multi-scale feature extraction network.
|
|
2046
|
+
|
|
2047
|
+
This function processes input tensors through three parallel branches with different
|
|
2048
|
+
receptive fields and concatenates the outputs before applying a final fully connected
|
|
2049
|
+
layer with sigmoid activation.
|
|
2050
|
+
|
|
2051
|
+
Parameters
|
|
2052
|
+
----------
|
|
2053
|
+
x_small : torch.Tensor
|
|
2054
|
+
Input tensor for the small-scale branch with shape (batch_size, channels, height, width)
|
|
2055
|
+
x_med : torch.Tensor
|
|
2056
|
+
Input tensor for the medium-scale branch with shape (batch_size, channels, height, width)
|
|
2057
|
+
x_large : torch.Tensor
|
|
2058
|
+
Input tensor for the large-scale branch with shape (batch_size, channels, height, width)
|
|
2059
|
+
|
|
2060
|
+
Returns
|
|
2061
|
+
-------
|
|
2062
|
+
torch.Tensor
|
|
2063
|
+
Output tensor with shape (batch_size, num_classes) containing sigmoid-activated
|
|
2064
|
+
predictions for each class
|
|
2065
|
+
|
|
2066
|
+
Notes
|
|
2067
|
+
-----
|
|
2068
|
+
The function assumes that `self.branch_small`, `self.branch_med`, `self.branch_large`,
|
|
2069
|
+
`self.fc`, and `self.sigmoid` are properly initialized components of the class.
|
|
2070
|
+
|
|
2071
|
+
Examples
|
|
2072
|
+
--------
|
|
2073
|
+
>>> import torch
|
|
2074
|
+
>>> # Assuming model is initialized
|
|
2075
|
+
>>> x_small = torch.randn(1, 3, 32, 32)
|
|
2076
|
+
>>> x_med = torch.randn(1, 3, 64, 64)
|
|
2077
|
+
>>> x_large = torch.randn(1, 3, 128, 128)
|
|
2078
|
+
>>> output = model.forward(x_small, x_med, x_large)
|
|
2079
|
+
>>> print(output.shape)
|
|
2080
|
+
torch.Size([1, num_classes])
|
|
2081
|
+
"""
|
|
2082
|
+
# Process each branch
|
|
2083
|
+
out_small = self.branch_small(x_small)
|
|
2084
|
+
out_med = self.branch_med(x_med)
|
|
2085
|
+
out_large = self.branch_large(x_large)
|
|
2086
|
+
|
|
2087
|
+
# Concatenate outputs
|
|
2088
|
+
merged = torch.cat([out_small, out_med, out_large], dim=1)
|
|
2089
|
+
|
|
2090
|
+
# Final output
|
|
2091
|
+
out = self.fc(merged)
|
|
2092
|
+
out = self.sigmoid(out)
|
|
2093
|
+
|
|
2094
|
+
return out
|
|
2095
|
+
|
|
2096
|
+
def get_config(self):
|
|
2097
|
+
"""
|
|
2098
|
+
Get the configuration parameters of the layer.
|
|
2099
|
+
|
|
2100
|
+
Returns
|
|
2101
|
+
-------
|
|
2102
|
+
dict
|
|
2103
|
+
A dictionary containing the layer configuration parameters with the following keys:
|
|
2104
|
+
- "num_filters" (int): Number of filters in the layer
|
|
2105
|
+
- "kernel_sizes" (list of int): Size of the convolutional kernels
|
|
2106
|
+
- "input_lens" (list of int): Lengths of input sequences
|
|
2107
|
+
- "input_width" (int): Width of the input data
|
|
2108
|
+
- "dilation_rate" (int): Dilation rate for dilated convolution
|
|
2109
|
+
|
|
2110
|
+
Notes
|
|
2111
|
+
-----
|
|
2112
|
+
This method returns a copy of the internal configuration parameters
|
|
2113
|
+
that can be used to reconstruct the layer with the same settings.
|
|
2114
|
+
|
|
2115
|
+
Examples
|
|
2116
|
+
--------
|
|
2117
|
+
>>> config = layer.get_config()
|
|
2118
|
+
>>> print(config['num_filters'])
|
|
2119
|
+
32
|
|
2120
|
+
"""
|
|
2121
|
+
return {
|
|
2122
|
+
"num_filters": self.num_filters,
|
|
2123
|
+
"kernel_sizes": self.kernel_sizes,
|
|
2124
|
+
"input_lens": self.input_lens,
|
|
2125
|
+
"input_width": self.input_width,
|
|
2126
|
+
"dilation_rate": self.dilation_rate,
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
class MultiscaleCNNDLFilter(DeepLearningFilter):
|
|
2131
|
+
def __init__(
|
|
2132
|
+
self,
|
|
2133
|
+
num_filters=10,
|
|
2134
|
+
kernel_sizes=[4, 8, 12],
|
|
2135
|
+
input_lens=[64, 128, 192],
|
|
2136
|
+
input_width=1,
|
|
2137
|
+
dilation_rate=1,
|
|
2138
|
+
*args,
|
|
2139
|
+
**kwargs,
|
|
2140
|
+
):
|
|
2141
|
+
"""
|
|
2142
|
+
Initialize the MultiscaleCNNDLFilter layer.
|
|
2143
|
+
|
|
2144
|
+
This constructor initializes a multiscale CNN filter with configurable
|
|
2145
|
+
kernel sizes, input lengths, and dilation rates for multi-scale feature extraction.
|
|
2146
|
+
|
|
2147
|
+
Parameters
|
|
2148
|
+
----------
|
|
2149
|
+
num_filters : int, optional
|
|
2150
|
+
Number of filters to use in each convolutional layer, default is 10
|
|
2151
|
+
kernel_sizes : list of int, optional
|
|
2152
|
+
List of kernel sizes for different convolutional layers, default is [4, 8, 12]
|
|
2153
|
+
input_lens : list of int, optional
|
|
2154
|
+
List of input sequence lengths for different scales, default is [64, 128, 192]
|
|
2155
|
+
input_width : int, optional
|
|
2156
|
+
Width of the input data, default is 1
|
|
2157
|
+
dilation_rate : int, optional
|
|
2158
|
+
Dilation rate for the convolutional layers, default is 1
|
|
2159
|
+
*args : tuple
|
|
2160
|
+
Variable length argument list passed to parent class
|
|
2161
|
+
**kwargs : dict
|
|
2162
|
+
Arbitrary keyword arguments passed to parent class
|
|
2163
|
+
|
|
2164
|
+
Returns
|
|
2165
|
+
-------
|
|
2166
|
+
None
|
|
2167
|
+
This method initializes the object and does not return any value
|
|
2168
|
+
|
|
2169
|
+
Notes
|
|
2170
|
+
-----
|
|
2171
|
+
The multiscale CNN filter uses multiple convolutional layers with different
|
|
2172
|
+
kernel sizes and dilation rates to capture features at multiple scales.
|
|
2173
|
+
The input data is processed through parallel convolutional branches,
|
|
2174
|
+
each with different kernel sizes and dilation rates.
|
|
2175
|
+
|
|
2176
|
+
Examples
|
|
2177
|
+
--------
|
|
2178
|
+
>>> filter_layer = MultiscaleCNNDLFilter(
|
|
2179
|
+
... num_filters=20,
|
|
2180
|
+
... kernel_sizes=[3, 6, 9],
|
|
2181
|
+
... input_lens=[32, 64, 128],
|
|
2182
|
+
... input_width=2,
|
|
2183
|
+
... dilation_rate=2
|
|
2184
|
+
... )
|
|
2185
|
+
"""
|
|
2186
|
+
self.num_filters = num_filters
|
|
2187
|
+
self.kernel_sizes = kernel_sizes
|
|
2188
|
+
self.input_lens = input_lens
|
|
2189
|
+
self.input_width = input_width
|
|
2190
|
+
self.dilation_rate = dilation_rate
|
|
2191
|
+
self.nettype = "multiscalecnn"
|
|
2192
|
+
self.infodict["nettype"] = self.nettype
|
|
2193
|
+
self.infodict["num_filters"] = self.num_filters
|
|
2194
|
+
self.infodict["kernel_sizes"] = self.kernel_sizes
|
|
2195
|
+
self.infodict["input_lens"] = self.input_lens
|
|
2196
|
+
self.infodict["input_width"] = self.input_width
|
|
2197
|
+
super(MultiscaleCNNDLFilter, self).__init__(*args, **kwargs)
|
|
2198
|
+
|
|
2199
|
+
def getname(self):
|
|
2200
|
+
"""
|
|
2201
|
+
Generate and return the model name and path based on current configuration parameters.
|
|
2202
|
+
|
|
2203
|
+
This method constructs a descriptive model name string by joining various configuration
|
|
2204
|
+
parameters with specific prefixes and zero-padded numeric values. The resulting name
|
|
2205
|
+
is used to create a unique directory path for model storage.
|
|
2206
|
+
|
|
2207
|
+
Parameters
|
|
2208
|
+
----------
|
|
2209
|
+
self : object
|
|
2210
|
+
The instance containing model configuration parameters.
|
|
2211
|
+
|
|
2212
|
+
Returns
|
|
2213
|
+
-------
|
|
2214
|
+
None
|
|
2215
|
+
This method does not return a value but sets the following attributes:
|
|
2216
|
+
- self.modelname: str, the generated model name
|
|
2217
|
+
- self.modelpath: str, the full path to the model directory
|
|
2218
|
+
|
|
2219
|
+
Notes
|
|
2220
|
+
-----
|
|
2221
|
+
The generated model name includes the following components:
|
|
2222
|
+
- Model type: "model_multiscalecnn_pytorch"
|
|
2223
|
+
- Window size: "w" + zero-padded window size
|
|
2224
|
+
- Number of layers: "l" + zero-padded layer count
|
|
2225
|
+
- Number of filters: "fn" + zero-padded filter count
|
|
2226
|
+
- First kernel size: "fl" + zero-padded kernel size
|
|
2227
|
+
- Number of epochs: "e" + zero-padded epoch count
|
|
2228
|
+
- Exclusion threshold: "t" + threshold value
|
|
2229
|
+
- Correlation threshold: "ct" + threshold value
|
|
2230
|
+
- Step size: "s" + zero-padded step value
|
|
2231
|
+
- Dilation rate: "d" + dilation rate value
|
|
2232
|
+
- Activation function name
|
|
2233
|
+
|
|
2234
|
+
Additional suffixes are appended if:
|
|
2235
|
+
- usebadpts is True: "_usebadpts"
|
|
2236
|
+
- excludebysubject is True: "_excludebysubject"
|
|
2237
|
+
- namesuffix is not None: "_{namesuffix}"
|
|
2238
|
+
|
|
2239
|
+
Examples
|
|
2240
|
+
--------
|
|
2241
|
+
>>> model = MyModel()
|
|
2242
|
+
>>> model.window_size = 128
|
|
2243
|
+
>>> model.num_layers = 5
|
|
2244
|
+
>>> model.num_filters = 32
|
|
2245
|
+
>>> model.kernel_sizes = [3, 5, 7]
|
|
2246
|
+
>>> model.num_epochs = 100
|
|
2247
|
+
>>> model.excludethresh = 0.5
|
|
2248
|
+
>>> model.corrthresh = 0.8
|
|
2249
|
+
>>> model.step = 16
|
|
2250
|
+
>>> model.dilation_rate = 2
|
|
2251
|
+
>>> model.activation = "relu"
|
|
2252
|
+
>>> model.usebadpts = True
|
|
2253
|
+
>>> model.excludebysubject = False
|
|
2254
|
+
>>> model.namesuffix = "exp1"
|
|
2255
|
+
>>> model.getname()
|
|
2256
|
+
>>> print(model.modelname)
|
|
2257
|
+
'model_multiscalecnn_pytorch_w128_l05_fn32_fl03_e100_t0.5_ct0.8_s16_d2_relu_usebadpts_exp1'
|
|
2258
|
+
"""
|
|
2259
|
+
self.modelname = "_".join(
|
|
2260
|
+
[
|
|
2261
|
+
"model",
|
|
2262
|
+
"multiscalecnn",
|
|
2263
|
+
"pytorch",
|
|
2264
|
+
"w" + str(self.window_size).zfill(3),
|
|
2265
|
+
"l" + str(self.num_layers).zfill(2),
|
|
2266
|
+
"fn" + str(self.num_filters).zfill(2),
|
|
2267
|
+
"fl" + str(self.kernel_sizes[0]).zfill(2),
|
|
2268
|
+
"e" + str(self.num_epochs).zfill(3),
|
|
2269
|
+
"t" + str(self.excludethresh),
|
|
2270
|
+
"ct" + str(self.corrthresh),
|
|
2271
|
+
"s" + str(self.step),
|
|
2272
|
+
"d" + str(self.dilation_rate),
|
|
2273
|
+
self.activation,
|
|
2274
|
+
]
|
|
2275
|
+
)
|
|
2276
|
+
if self.usebadpts:
|
|
2277
|
+
self.modelname += "_usebadpts"
|
|
2278
|
+
if self.excludebysubject:
|
|
2279
|
+
self.modelname += "_excludebysubject"
|
|
2280
|
+
if self.namesuffix is not None:
|
|
2281
|
+
self.modelname += "_" + self.namesuffix
|
|
2282
|
+
self.modelpath = os.path.join(self.modelroot, self.modelname)
|
|
2283
|
+
|
|
2284
|
+
try:
|
|
2285
|
+
os.makedirs(self.modelpath)
|
|
2286
|
+
except OSError:
|
|
2287
|
+
pass
|
|
2288
|
+
|
|
2289
|
+
def makenet(self):
|
|
2290
|
+
"""
|
|
2291
|
+
Create and initialize a multiscale CNN model for network construction.
|
|
2292
|
+
|
|
2293
|
+
This method initializes a MultiscaleCNNModel with the specified parameters
|
|
2294
|
+
and moves the model to the designated device (CPU or GPU).
|
|
2295
|
+
|
|
2296
|
+
Parameters
|
|
2297
|
+
----------
|
|
2298
|
+
self : object
|
|
2299
|
+
The instance containing the following attributes:
|
|
2300
|
+
- num_filters : int
|
|
2301
|
+
Number of filters for the CNN layers
|
|
2302
|
+
- kernel_sizes : list of int
|
|
2303
|
+
List of kernel sizes for different scales
|
|
2304
|
+
- input_lens : list of int
|
|
2305
|
+
List of input lengths for different scales
|
|
2306
|
+
- input_width : int
|
|
2307
|
+
Width of the input data
|
|
2308
|
+
- dilation_rate : int
|
|
2309
|
+
Dilation rate for the convolutional layers
|
|
2310
|
+
- device : torch.device
|
|
2311
|
+
Device to move the model to (e.g., 'cuda' or 'cpu')
|
|
2312
|
+
|
|
2313
|
+
Returns
|
|
2314
|
+
-------
|
|
2315
|
+
None
|
|
2316
|
+
This method does not return any value but modifies the instance
|
|
2317
|
+
by setting the `model` attribute to the created MultiscaleCNNModel.
|
|
2318
|
+
|
|
2319
|
+
Notes
|
|
2320
|
+
-----
|
|
2321
|
+
The method assumes that all required attributes are properly initialized
|
|
2322
|
+
in the instance before calling this method. The model is automatically
|
|
2323
|
+
moved to the specified device using the `.to()` method.
|
|
2324
|
+
|
|
2325
|
+
Examples
|
|
2326
|
+
--------
|
|
2327
|
+
>>> # Assuming instance with required attributes is created
|
|
2328
|
+
>>> instance.makenet()
|
|
2329
|
+
>>> print(instance.model)
|
|
2330
|
+
MultiscaleCNNModel(...)
|
|
2331
|
+
"""
|
|
2332
|
+
self.model = MultiscaleCNNModel(
|
|
2333
|
+
self.num_filters,
|
|
2334
|
+
self.kernel_sizes,
|
|
2335
|
+
self.input_lens,
|
|
2336
|
+
self.input_width,
|
|
2337
|
+
self.dilation_rate,
|
|
2338
|
+
)
|
|
2339
|
+
self.model.to(self.device)
|
|
2340
|
+
|
|
2341
|
+
|
|
2342
|
+
class ConvAutoencoderModel(nn.Module):
|
|
2343
|
+
def __init__(
|
|
2344
|
+
self,
|
|
2345
|
+
window_size,
|
|
2346
|
+
encoding_dim,
|
|
2347
|
+
num_filters,
|
|
2348
|
+
kernel_size,
|
|
2349
|
+
dropout_rate,
|
|
2350
|
+
activation,
|
|
2351
|
+
inputsize,
|
|
2352
|
+
):
|
|
2353
|
+
"""
|
|
2354
|
+
Initialize the ConvAutoencoderModel.
|
|
2355
|
+
|
|
2356
|
+
This class implements a convolutional autoencoder for time series data. The model
|
|
2357
|
+
consists of an encoder and a decoder, with symmetric architecture. The encoder
|
|
2358
|
+
reduces the input dimensionality through convolutional and pooling layers, while
|
|
2359
|
+
the decoder reconstructs the input from the encoded representation.
|
|
2360
|
+
|
|
2361
|
+
Parameters
|
|
2362
|
+
----------
|
|
2363
|
+
window_size : int
|
|
2364
|
+
The length of the input time series window.
|
|
2365
|
+
encoding_dim : int
|
|
2366
|
+
The dimensionality of the latent space representation.
|
|
2367
|
+
num_filters : int
|
|
2368
|
+
The number of filters in the first convolutional layer.
|
|
2369
|
+
kernel_size : int
|
|
2370
|
+
The size of the convolutional kernels.
|
|
2371
|
+
dropout_rate : float
|
|
2372
|
+
The dropout rate applied after each convolutional layer.
|
|
2373
|
+
activation : str
|
|
2374
|
+
The activation function to use. Supported values are "relu" and "tanh".
|
|
2375
|
+
inputsize : int
|
|
2376
|
+
The number of input channels (e.g., number of features in the time series).
|
|
2377
|
+
|
|
2378
|
+
Returns
|
|
2379
|
+
-------
|
|
2380
|
+
None
|
|
2381
|
+
This method initializes the model in-place and does not return any value.
|
|
2382
|
+
|
|
2383
|
+
Notes
|
|
2384
|
+
-----
|
|
2385
|
+
The model uses a symmetric encoder-decoder architecture. The encoder reduces
|
|
2386
|
+
the input size through 4 max-pooling layers, and the decoder reconstructs
|
|
2387
|
+
the input using upsample and convolutional layers. The final layer uses
|
|
2388
|
+
a convolution with padding to match the input size.
|
|
2389
|
+
|
|
2390
|
+
Examples
|
|
2391
|
+
--------
|
|
2392
|
+
>>> model = ConvAutoencoderModel(
|
|
2393
|
+
... window_size=100,
|
|
2394
|
+
... encoding_dim=32,
|
|
2395
|
+
... num_filters=32,
|
|
2396
|
+
... kernel_size=3,
|
|
2397
|
+
... dropout_rate=0.2,
|
|
2398
|
+
... activation="relu",
|
|
2399
|
+
... inputsize=1
|
|
2400
|
+
... )
|
|
2401
|
+
"""
|
|
2402
|
+
super(ConvAutoencoderModel, self).__init__()
|
|
2403
|
+
|
|
2404
|
+
self.window_size = window_size
|
|
2405
|
+
self.encoding_dim = encoding_dim
|
|
2406
|
+
self.num_filters = num_filters
|
|
2407
|
+
self.kernel_size = kernel_size
|
|
2408
|
+
self.dropout_rate = dropout_rate
|
|
2409
|
+
self.activation = activation
|
|
2410
|
+
self.inputsize = inputsize
|
|
2411
|
+
|
|
2412
|
+
# Get activation function
|
|
2413
|
+
if activation == "relu":
|
|
2414
|
+
act_fn = nn.ReLU
|
|
2415
|
+
elif activation == "tanh":
|
|
2416
|
+
act_fn = nn.Tanh
|
|
2417
|
+
else:
|
|
2418
|
+
act_fn = nn.ReLU
|
|
2419
|
+
|
|
2420
|
+
# Initial conv block
|
|
2421
|
+
self.encoder_layers = nn.ModuleList()
|
|
2422
|
+
self.encoder_layers.append(nn.Conv1d(inputsize, num_filters, kernel_size, padding="same"))
|
|
2423
|
+
self.encoder_layers.append(nn.BatchNorm1d(num_filters))
|
|
2424
|
+
self.encoder_layers.append(nn.Dropout(dropout_rate))
|
|
2425
|
+
self.encoder_layers.append(act_fn())
|
|
2426
|
+
self.encoder_layers.append(nn.MaxPool1d(2, padding=1))
|
|
2427
|
+
|
|
2428
|
+
# Encoding path (3 layers)
|
|
2429
|
+
nfilters = num_filters
|
|
2430
|
+
self.filter_list = []
|
|
2431
|
+
for _ in range(3):
|
|
2432
|
+
nfilters *= 2
|
|
2433
|
+
self.filter_list.append(nfilters)
|
|
2434
|
+
self.encoder_layers.append(
|
|
2435
|
+
nn.Conv1d(nfilters // 2, nfilters, kernel_size, padding="same")
|
|
2436
|
+
)
|
|
2437
|
+
self.encoder_layers.append(nn.BatchNorm1d(nfilters))
|
|
2438
|
+
self.encoder_layers.append(nn.Dropout(dropout_rate))
|
|
2439
|
+
self.encoder_layers.append(act_fn())
|
|
2440
|
+
self.encoder_layers.append(nn.MaxPool1d(2, padding=1))
|
|
2441
|
+
|
|
2442
|
+
# Calculate size after pooling
|
|
2443
|
+
self.encoded_size = window_size
|
|
2444
|
+
for _ in range(4): # 4 pooling layers
|
|
2445
|
+
self.encoded_size = (self.encoded_size + 1) // 2
|
|
2446
|
+
|
|
2447
|
+
# Bottleneck
|
|
2448
|
+
self.flatten = nn.Flatten()
|
|
2449
|
+
self.encode_fc = nn.Linear(nfilters * self.encoded_size, encoding_dim)
|
|
2450
|
+
self.encode_act = act_fn()
|
|
2451
|
+
self.decode_fc = nn.Linear(encoding_dim, nfilters * self.encoded_size)
|
|
2452
|
+
self.decode_act = act_fn()
|
|
2453
|
+
self.unflatten_size = (nfilters, self.encoded_size)
|
|
2454
|
+
|
|
2455
|
+
# Decoding path (mirror)
|
|
2456
|
+
self.decoder_layers = nn.ModuleList()
|
|
2457
|
+
for i, filters in enumerate(reversed(self.filter_list)):
|
|
2458
|
+
self.decoder_layers.append(nn.Upsample(scale_factor=2, mode="nearest"))
|
|
2459
|
+
if i == 0:
|
|
2460
|
+
self.decoder_layers.append(
|
|
2461
|
+
nn.Conv1d(nfilters, filters, kernel_size, padding="same")
|
|
2462
|
+
)
|
|
2463
|
+
else:
|
|
2464
|
+
self.decoder_layers.append(
|
|
2465
|
+
nn.Conv1d(self.filter_list[-i], filters, kernel_size, padding="same")
|
|
2466
|
+
)
|
|
2467
|
+
self.decoder_layers.append(nn.BatchNorm1d(filters))
|
|
2468
|
+
self.decoder_layers.append(nn.Dropout(dropout_rate))
|
|
2469
|
+
self.decoder_layers.append(act_fn())
|
|
2470
|
+
|
|
2471
|
+
# Final upsampling
|
|
2472
|
+
self.decoder_layers.append(nn.Upsample(scale_factor=2, mode="nearest"))
|
|
2473
|
+
self.decoder_layers.append(nn.Conv1d(num_filters, inputsize, kernel_size, padding="same"))
|
|
2474
|
+
|
|
2475
|
+
def forward(self, x):
|
|
2476
|
+
"""
|
|
2477
|
+
Forward pass of the autoencoder.
|
|
2478
|
+
|
|
2479
|
+
Applies encoding, bottleneck processing, and decoding to the input tensor.
|
|
2480
|
+
|
|
2481
|
+
Parameters
|
|
2482
|
+
----------
|
|
2483
|
+
x : torch.Tensor
|
|
2484
|
+
Input tensor of shape (batch_size, channels, height, width) where
|
|
2485
|
+
height and width should match the expected window size.
|
|
2486
|
+
|
|
2487
|
+
Returns
|
|
2488
|
+
-------
|
|
2489
|
+
torch.Tensor
|
|
2490
|
+
Reconstructed tensor of shape (batch_size, channels, window_size, width)
|
|
2491
|
+
with the same spatial dimensions as the input.
|
|
2492
|
+
|
|
2493
|
+
Notes
|
|
2494
|
+
-----
|
|
2495
|
+
The forward pass consists of three main stages:
|
|
2496
|
+
1. Encoding: Input is passed through encoder layers
|
|
2497
|
+
2. Bottleneck: Flattening, encoding, and decoding with activation functions
|
|
2498
|
+
3. Decoding: Reconstructed features are passed through decoder layers
|
|
2499
|
+
|
|
2500
|
+
The output is cropped or padded to match the original window size.
|
|
2501
|
+
|
|
2502
|
+
Examples
|
|
2503
|
+
--------
|
|
2504
|
+
>>> import torch
|
|
2505
|
+
>>> model = AutoEncoder()
|
|
2506
|
+
>>> x = torch.randn(1, 3, 64, 64)
|
|
2507
|
+
>>> output = model.forward(x)
|
|
2508
|
+
>>> print(output.shape)
|
|
2509
|
+
torch.Size([1, 3, 64, 64])
|
|
2510
|
+
"""
|
|
2511
|
+
# Encoding
|
|
2512
|
+
for layer in self.encoder_layers:
|
|
2513
|
+
x = layer(x)
|
|
2514
|
+
|
|
2515
|
+
# Bottleneck
|
|
2516
|
+
x = self.flatten(x)
|
|
2517
|
+
x = self.encode_fc(x)
|
|
2518
|
+
x = self.encode_act(x)
|
|
2519
|
+
x = self.decode_fc(x)
|
|
2520
|
+
x = self.decode_act(x)
|
|
2521
|
+
x = x.view(x.size(0), *self.unflatten_size)
|
|
2522
|
+
|
|
2523
|
+
# Decoding
|
|
2524
|
+
for layer in self.decoder_layers:
|
|
2525
|
+
x = layer(x)
|
|
2526
|
+
|
|
2527
|
+
# Crop/pad to original window size
|
|
2528
|
+
if x.size(2) > self.window_size:
|
|
2529
|
+
x = x[:, :, : self.window_size]
|
|
2530
|
+
elif x.size(2) < self.window_size:
|
|
2531
|
+
pad_size = self.window_size - x.size(2)
|
|
2532
|
+
x = nn.functional.pad(x, (0, pad_size))
|
|
2533
|
+
|
|
2534
|
+
return x
|
|
2535
|
+
|
|
2536
|
+
def get_config(self):
|
|
2537
|
+
"""
|
|
2538
|
+
Get the configuration parameters of the model.
|
|
2539
|
+
|
|
2540
|
+
Returns
|
|
2541
|
+
-------
|
|
2542
|
+
dict
|
|
2543
|
+
A dictionary containing all configuration parameters with their current values:
|
|
2544
|
+
- "window_size" (int): Size of the sliding window
|
|
2545
|
+
- "encoding_dim" (int): Dimension of the encoding layer
|
|
2546
|
+
- "num_filters" (int): Number of filters in the convolutional layers
|
|
2547
|
+
- "kernel_size" (int): Size of the convolutional kernel
|
|
2548
|
+
- "dropout_rate" (float): Dropout rate for regularization
|
|
2549
|
+
- "activation" (str): Activation function to use
|
|
2550
|
+
- "inputsize" (int): Size of the input data
|
|
2551
|
+
|
|
2552
|
+
Notes
|
|
2553
|
+
-----
|
|
2554
|
+
This method returns a copy of the current configuration. Modifications to the
|
|
2555
|
+
returned dictionary will not affect the original model configuration.
|
|
2556
|
+
|
|
2557
|
+
Examples
|
|
2558
|
+
--------
|
|
2559
|
+
>>> config = model.get_config()
|
|
2560
|
+
>>> print(config['window_size'])
|
|
2561
|
+
100
|
|
2562
|
+
"""
|
|
2563
|
+
return {
|
|
2564
|
+
"window_size": self.window_size,
|
|
2565
|
+
"encoding_dim": self.encoding_dim,
|
|
2566
|
+
"num_filters": self.num_filters,
|
|
2567
|
+
"kernel_size": self.kernel_size,
|
|
2568
|
+
"dropout_rate": self.dropout_rate,
|
|
2569
|
+
"activation": self.activation,
|
|
2570
|
+
"inputsize": self.inputsize,
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
|
|
2574
|
+
class ConvAutoencoderDLFilter(DeepLearningFilter):
|
|
2575
|
+
def __init__(
|
|
2576
|
+
self,
|
|
2577
|
+
encoding_dim: int = 10,
|
|
2578
|
+
num_filters: int = 5,
|
|
2579
|
+
kernel_size: int = 5,
|
|
2580
|
+
dilation_rate: int = 1,
|
|
2581
|
+
*args,
|
|
2582
|
+
**kwargs,
|
|
2583
|
+
) -> None:
|
|
2584
|
+
"""
|
|
2585
|
+
Initialize ConvAutoencoderDLFilter instance.
|
|
2586
|
+
|
|
2587
|
+
Parameters
|
|
2588
|
+
----------
|
|
2589
|
+
encoding_dim : int, optional
|
|
2590
|
+
Dimension of the encoded representation, by default 10
|
|
2591
|
+
num_filters : int, optional
|
|
2592
|
+
Number of filters in the convolutional layers, by default 5
|
|
2593
|
+
kernel_size : int, optional
|
|
2594
|
+
Size of the convolutional kernel, by default 5
|
|
2595
|
+
dilation_rate : int, optional
|
|
2596
|
+
Dilation rate for the convolutional layers, by default 1
|
|
2597
|
+
*args
|
|
2598
|
+
Variable length argument list
|
|
2599
|
+
**kwargs
|
|
2600
|
+
Arbitrary keyword arguments
|
|
2601
|
+
|
|
2602
|
+
Returns
|
|
2603
|
+
-------
|
|
2604
|
+
None
|
|
2605
|
+
This method does not return any value
|
|
2606
|
+
|
|
2607
|
+
Notes
|
|
2608
|
+
-----
|
|
2609
|
+
This constructor initializes a convolutional autoencoder with dilated filters.
|
|
2610
|
+
The network type is set to "convautoencoder" and various configuration parameters
|
|
2611
|
+
are stored in the infodict for later reference.
|
|
2612
|
+
|
|
2613
|
+
Examples
|
|
2614
|
+
--------
|
|
2615
|
+
>>> autoencoder = ConvAutoencoderDLFilter(
|
|
2616
|
+
... encoding_dim=15,
|
|
2617
|
+
... num_filters=8,
|
|
2618
|
+
... kernel_size=3,
|
|
2619
|
+
... dilation_rate=2
|
|
2620
|
+
... )
|
|
2621
|
+
"""
|
|
2622
|
+
self.encoding_dim = encoding_dim
|
|
2623
|
+
self.num_filters = num_filters
|
|
2624
|
+
self.kernel_size = kernel_size
|
|
2625
|
+
self.dilation_rate = dilation_rate
|
|
2626
|
+
self.nettype = "convautoencoder"
|
|
2627
|
+
self.infodict["num_filters"] = self.num_filters
|
|
2628
|
+
self.infodict["kernel_size"] = self.kernel_size
|
|
2629
|
+
self.infodict["nettype"] = self.nettype
|
|
2630
|
+
self.infodict["encoding_dim"] = self.encoding_dim
|
|
2631
|
+
super(ConvAutoencoderDLFilter, self).__init__(*args, **kwargs)
|
|
2632
|
+
|
|
2633
|
+
def getname(self):
|
|
2634
|
+
"""
|
|
2635
|
+
Generate and configure the model name and path based on current parameters.
|
|
2636
|
+
|
|
2637
|
+
This method constructs a descriptive model name string using various
|
|
2638
|
+
configuration parameters and creates the corresponding directory path.
|
|
2639
|
+
The generated name includes information about window size, encoding dimensions,
|
|
2640
|
+
filters, kernel size, epochs, thresholds, and other model configuration options.
|
|
2641
|
+
|
|
2642
|
+
Parameters
|
|
2643
|
+
----------
|
|
2644
|
+
self : object
|
|
2645
|
+
The instance of the class containing model configuration parameters.
|
|
2646
|
+
Expected attributes include:
|
|
2647
|
+
- window_size : int
|
|
2648
|
+
- encoding_dim : int
|
|
2649
|
+
- num_filters : int
|
|
2650
|
+
- kernel_size : int
|
|
2651
|
+
- num_epochs : int
|
|
2652
|
+
- excludethresh : float
|
|
2653
|
+
- corrthresh : float
|
|
2654
|
+
- step : int
|
|
2655
|
+
- activation : str
|
|
2656
|
+
- usebadpts : bool
|
|
2657
|
+
- excludebysubject : bool
|
|
2658
|
+
- namesuffix : str, optional
|
|
2659
|
+
- modelroot : str
|
|
2660
|
+
|
|
2661
|
+
Returns
|
|
2662
|
+
-------
|
|
2663
|
+
None
|
|
2664
|
+
This method does not return a value but modifies the instance attributes:
|
|
2665
|
+
- self.modelname : str
|
|
2666
|
+
- self.modelpath : str
|
|
2667
|
+
|
|
2668
|
+
Notes
|
|
2669
|
+
-----
|
|
2670
|
+
The model name is constructed with the following components:
|
|
2671
|
+
- "model_convautoencoder_pytorch" as base identifier
|
|
2672
|
+
- Window size with 3-digit zero-padded formatting (wXXX)
|
|
2673
|
+
- Encoding dimension with 3-digit zero-padded formatting (enXXX)
|
|
2674
|
+
- Number of filters with 2-digit zero-padded formatting (fnXX)
|
|
2675
|
+
- Kernel size with 2-digit zero-padded formatting (flXX)
|
|
2676
|
+
- Number of epochs with 3-digit zero-padded formatting (eXXX)
|
|
2677
|
+
- Exclusion threshold (tX.XX)
|
|
2678
|
+
- Correlation threshold (ctX.XX)
|
|
2679
|
+
- Step size (sX)
|
|
2680
|
+
- Activation function name
|
|
2681
|
+
|
|
2682
|
+
Additional suffixes are appended based on:
|
|
2683
|
+
- usebadpts flag
|
|
2684
|
+
- excludebysubject flag
|
|
2685
|
+
- namesuffix parameter
|
|
2686
|
+
|
|
2687
|
+
Examples
|
|
2688
|
+
--------
|
|
2689
|
+
>>> model = MyModel()
|
|
2690
|
+
>>> model.window_size = 100
|
|
2691
|
+
>>> model.encoding_dim = 50
|
|
2692
|
+
>>> model.getname()
|
|
2693
|
+
>>> print(model.modelname)
|
|
2694
|
+
'model_convautoencoder_pytorch_w100_en050_fn10_fl05_e001_t0.5_ct0.8_s1_relu'
|
|
2695
|
+
"""
|
|
2696
|
+
self.modelname = "_".join(
|
|
2697
|
+
[
|
|
2698
|
+
"model",
|
|
2699
|
+
"convautoencoder",
|
|
2700
|
+
"pytorch",
|
|
2701
|
+
"w" + str(self.window_size).zfill(3),
|
|
2702
|
+
"en" + str(self.encoding_dim).zfill(3),
|
|
2703
|
+
"fn" + str(self.num_filters).zfill(2),
|
|
2704
|
+
"fl" + str(self.kernel_size).zfill(2),
|
|
2705
|
+
"e" + str(self.num_epochs).zfill(3),
|
|
2706
|
+
"t" + str(self.excludethresh),
|
|
2707
|
+
"ct" + str(self.corrthresh),
|
|
2708
|
+
"s" + str(self.step),
|
|
2709
|
+
self.activation,
|
|
2710
|
+
]
|
|
2711
|
+
)
|
|
2712
|
+
if self.usebadpts:
|
|
2713
|
+
self.modelname += "_usebadpts"
|
|
2714
|
+
if self.excludebysubject:
|
|
2715
|
+
self.modelname += "_excludebysubject"
|
|
2716
|
+
if self.namesuffix is not None:
|
|
2717
|
+
self.modelname += "_" + self.namesuffix
|
|
2718
|
+
self.modelpath = os.path.join(self.modelroot, self.modelname)
|
|
2719
|
+
|
|
2720
|
+
try:
|
|
2721
|
+
os.makedirs(self.modelpath)
|
|
2722
|
+
except OSError:
|
|
2723
|
+
pass
|
|
2724
|
+
|
|
2725
|
+
def makenet(self):
|
|
2726
|
+
"""
|
|
2727
|
+
Create and initialize a convolutional autoencoder model.
|
|
2728
|
+
|
|
2729
|
+
This method constructs a ConvAutoencoderModel with the specified parameters
|
|
2730
|
+
and moves it to the designated device (CPU or GPU).
|
|
2731
|
+
|
|
2732
|
+
Parameters
|
|
2733
|
+
----------
|
|
2734
|
+
self : object
|
|
2735
|
+
The instance of the class containing this method. Expected to have the
|
|
2736
|
+
following attributes:
|
|
2737
|
+
- window_size : int
|
|
2738
|
+
Size of the input window
|
|
2739
|
+
- encoding_dim : int
|
|
2740
|
+
Dimension of the encoded representation
|
|
2741
|
+
- num_filters : int
|
|
2742
|
+
Number of filters in the convolutional layers
|
|
2743
|
+
- kernel_size : int
|
|
2744
|
+
Size of the convolutional kernel
|
|
2745
|
+
- dropout_rate : float
|
|
2746
|
+
Dropout rate for regularization
|
|
2747
|
+
- activation : str or callable
|
|
2748
|
+
Activation function to use
|
|
2749
|
+
- inputsize : tuple
|
|
2750
|
+
Input size dimensions
|
|
2751
|
+
- device : torch.device
|
|
2752
|
+
Device to move the model to (CPU or GPU)
|
|
2753
|
+
|
|
2754
|
+
Returns
|
|
2755
|
+
-------
|
|
2756
|
+
None
|
|
2757
|
+
This method does not return any value. It initializes the model
|
|
2758
|
+
attribute of the class instance.
|
|
2759
|
+
|
|
2760
|
+
Notes
|
|
2761
|
+
-----
|
|
2762
|
+
The method assumes that `ConvAutoencoderModel` is a valid class that accepts
|
|
2763
|
+
the specified parameters. The model is automatically moved to the device
|
|
2764
|
+
specified by `self.device`.
|
|
2765
|
+
|
|
2766
|
+
Examples
|
|
2767
|
+
--------
|
|
2768
|
+
>>> class MyModel:
|
|
2769
|
+
... def __init__(self):
|
|
2770
|
+
... self.window_size = 100
|
|
2771
|
+
... self.encoding_dim = 32
|
|
2772
|
+
... self.num_filters = 64
|
|
2773
|
+
... self.kernel_size = 3
|
|
2774
|
+
... self.dropout_rate = 0.2
|
|
2775
|
+
... self.activation = 'relu'
|
|
2776
|
+
... self.inputsize = (1, 100)
|
|
2777
|
+
... self.device = torch.device('cpu')
|
|
2778
|
+
... self.model = None
|
|
2779
|
+
...
|
|
2780
|
+
... def makenet(self):
|
|
2781
|
+
... self.model = ConvAutoencoderModel(
|
|
2782
|
+
... self.window_size,
|
|
2783
|
+
... self.encoding_dim,
|
|
2784
|
+
... self.num_filters,
|
|
2785
|
+
... self.kernel_size,
|
|
2786
|
+
... self.dropout_rate,
|
|
2787
|
+
... self.activation,
|
|
2788
|
+
... self.inputsize,
|
|
2789
|
+
... )
|
|
2790
|
+
... self.model.to(self.device)
|
|
2791
|
+
...
|
|
2792
|
+
>>> model = MyModel()
|
|
2793
|
+
>>> model.makenet()
|
|
2794
|
+
>>> print(model.model)
|
|
2795
|
+
"""
|
|
2796
|
+
self.model = ConvAutoencoderModel(
|
|
2797
|
+
self.window_size,
|
|
2798
|
+
self.encoding_dim,
|
|
2799
|
+
self.num_filters,
|
|
2800
|
+
self.kernel_size,
|
|
2801
|
+
self.dropout_rate,
|
|
2802
|
+
self.activation,
|
|
2803
|
+
self.inputsize,
|
|
2804
|
+
)
|
|
2805
|
+
self.model.to(self.device)
|
|
2806
|
+
|
|
2807
|
+
|
|
2808
|
+
class CRNNModel(nn.Module):
|
|
2809
|
+
def __init__(
|
|
2810
|
+
self, num_filters, kernel_size, encoding_dim, dropout_rate, activation, inputsize
|
|
2811
|
+
):
|
|
2812
|
+
"""
|
|
2813
|
+
Initialize the CRNNModel.
|
|
2814
|
+
|
|
2815
|
+
This function initializes a Convolutional Recurrent Neural Network (CRNN) model
|
|
2816
|
+
with convolutional front-end, bidirectional LSTM layers, and output mapping.
|
|
2817
|
+
The model processes sequential data through convolutional layers, applies
|
|
2818
|
+
bidirectional LSTM encoding, and maps the output back to the original input size.
|
|
2819
|
+
|
|
2820
|
+
Parameters
|
|
2821
|
+
----------
|
|
2822
|
+
num_filters : int
|
|
2823
|
+
Number of filters in the convolutional layers
|
|
2824
|
+
kernel_size : int
|
|
2825
|
+
Size of the convolutional kernel
|
|
2826
|
+
encoding_dim : int
|
|
2827
|
+
Dimension of the LSTM encoding (hidden state size)
|
|
2828
|
+
dropout_rate : float
|
|
2829
|
+
Dropout rate for regularization
|
|
2830
|
+
activation : str
|
|
2831
|
+
Activation function to use ('relu' or 'tanh')
|
|
2832
|
+
inputsize : int
|
|
2833
|
+
Size of the input features
|
|
2834
|
+
|
|
2835
|
+
Returns
|
|
2836
|
+
-------
|
|
2837
|
+
None
|
|
2838
|
+
Initializes the CRNNModel instance
|
|
2839
|
+
|
|
2840
|
+
Notes
|
|
2841
|
+
-----
|
|
2842
|
+
The model uses a bidirectional LSTM with batch_first=True.
|
|
2843
|
+
The convolutional layers use 'same' padding to maintain sequence length.
|
|
2844
|
+
Default activation function is ReLU if an invalid activation is provided.
|
|
2845
|
+
|
|
2846
|
+
Examples
|
|
2847
|
+
--------
|
|
2848
|
+
>>> model = CRNNModel(
|
|
2849
|
+
... num_filters=32,
|
|
2850
|
+
... kernel_size=3,
|
|
2851
|
+
... encoding_dim=64,
|
|
2852
|
+
... dropout_rate=0.2,
|
|
2853
|
+
... activation='relu',
|
|
2854
|
+
... inputsize=128
|
|
2855
|
+
... )
|
|
2856
|
+
"""
|
|
2857
|
+
super(CRNNModel, self).__init__()
|
|
2858
|
+
|
|
2859
|
+
self.num_filters = num_filters
|
|
2860
|
+
self.kernel_size = kernel_size
|
|
2861
|
+
self.encoding_dim = encoding_dim
|
|
2862
|
+
self.dropout_rate = dropout_rate
|
|
2863
|
+
self.activation = activation
|
|
2864
|
+
self.inputsize = inputsize
|
|
2865
|
+
|
|
2866
|
+
# Get activation function
|
|
2867
|
+
if activation == "relu":
|
|
2868
|
+
act_fn = nn.ReLU
|
|
2869
|
+
elif activation == "tanh":
|
|
2870
|
+
act_fn = nn.Tanh
|
|
2871
|
+
else:
|
|
2872
|
+
act_fn = nn.ReLU
|
|
2873
|
+
|
|
2874
|
+
# Convolutional front-end
|
|
2875
|
+
self.conv1 = nn.Conv1d(inputsize, num_filters, kernel_size, padding="same")
|
|
2876
|
+
self.bn1 = nn.BatchNorm1d(num_filters)
|
|
2877
|
+
self.dropout1 = nn.Dropout(dropout_rate)
|
|
2878
|
+
self.act1 = act_fn()
|
|
2879
|
+
|
|
2880
|
+
self.conv2 = nn.Conv1d(num_filters, num_filters * 2, kernel_size, padding="same")
|
|
2881
|
+
self.bn2 = nn.BatchNorm1d(num_filters * 2)
|
|
2882
|
+
self.dropout2 = nn.Dropout(dropout_rate)
|
|
2883
|
+
self.act2 = act_fn()
|
|
2884
|
+
|
|
2885
|
+
# Bidirectional LSTM
|
|
2886
|
+
self.lstm = nn.LSTM(num_filters * 2, encoding_dim, batch_first=True, bidirectional=True)
|
|
2887
|
+
|
|
2888
|
+
# Output mapping
|
|
2889
|
+
self.fc_out = nn.Linear(encoding_dim * 2, inputsize)
|
|
2890
|
+
|
|
2891
|
+
def forward(self, x):
|
|
2892
|
+
"""
|
|
2893
|
+
Forward pass through the neural network architecture.
|
|
2894
|
+
|
|
2895
|
+
This function processes input data through a convolutional neural network
|
|
2896
|
+
followed by an LSTM layer and a fully connected output layer. The input
|
|
2897
|
+
is first processed through two convolutional blocks, then reshaped for
|
|
2898
|
+
LSTM processing, and finally converted back to the original output format.
|
|
2899
|
+
|
|
2900
|
+
Parameters
|
|
2901
|
+
----------
|
|
2902
|
+
x : torch.Tensor
|
|
2903
|
+
Input tensor of shape (batch_size, channels, length) containing
|
|
2904
|
+
the input sequence data to be processed.
|
|
2905
|
+
|
|
2906
|
+
Returns
|
|
2907
|
+
-------
|
|
2908
|
+
torch.Tensor
|
|
2909
|
+
Output tensor of shape (batch_size, channels, length) containing
|
|
2910
|
+
the processed sequence data after passing through all layers.
|
|
2911
|
+
|
|
2912
|
+
Notes
|
|
2913
|
+
-----
|
|
2914
|
+
The function performs the following operations in sequence:
|
|
2915
|
+
1. Two convolutional blocks with batch normalization, dropout, and activation
|
|
2916
|
+
2. Permute operation to reshape data for LSTM processing (batch, seq_len, features)
|
|
2917
|
+
3. LSTM layer processing
|
|
2918
|
+
4. Fully connected output layer
|
|
2919
|
+
5. Final permutation to restore original shape (batch, channels, length)
|
|
2920
|
+
|
|
2921
|
+
Examples
|
|
2922
|
+
--------
|
|
2923
|
+
>>> import torch
|
|
2924
|
+
>>> model = YourModelClass()
|
|
2925
|
+
>>> x = torch.randn(32, 1, 100) # batch_size=32, channels=1, length=100
|
|
2926
|
+
>>> output = model.forward(x)
|
|
2927
|
+
>>> print(output.shape) # torch.Size([32, 1, 100])
|
|
2928
|
+
"""
|
|
2929
|
+
# Conv layers expect (batch, channels, length)
|
|
2930
|
+
x = self.conv1(x)
|
|
2931
|
+
x = self.bn1(x)
|
|
2932
|
+
x = self.dropout1(x)
|
|
2933
|
+
x = self.act1(x)
|
|
2934
|
+
|
|
2935
|
+
x = self.conv2(x)
|
|
2936
|
+
x = self.bn2(x)
|
|
2937
|
+
x = self.dropout2(x)
|
|
2938
|
+
x = self.act2(x)
|
|
2939
|
+
|
|
2940
|
+
# LSTM expects (batch, seq_len, features)
|
|
2941
|
+
x = x.permute(0, 2, 1)
|
|
2942
|
+
x, _ = self.lstm(x)
|
|
2943
|
+
|
|
2944
|
+
# Output layer
|
|
2945
|
+
x = self.fc_out(x)
|
|
2946
|
+
|
|
2947
|
+
# Convert back to (batch, channels, length)
|
|
2948
|
+
x = x.permute(0, 2, 1)
|
|
2949
|
+
|
|
2950
|
+
return x
|
|
2951
|
+
|
|
2952
|
+
def get_config(self):
|
|
2953
|
+
"""
|
|
2954
|
+
Get the configuration parameters of the model.
|
|
2955
|
+
|
|
2956
|
+
Returns
|
|
2957
|
+
-------
|
|
2958
|
+
dict
|
|
2959
|
+
A dictionary containing the model configuration parameters with the following keys:
|
|
2960
|
+
- "num_filters" (int): Number of filters in the convolutional layers
|
|
2961
|
+
- "kernel_size" (int): Size of the convolutional kernel
|
|
2962
|
+
- "encoding_dim" (int): Dimension of the encoding layer
|
|
2963
|
+
- "dropout_rate" (float): Dropout rate for regularization
|
|
2964
|
+
- "activation" (str): Activation function used in the layers
|
|
2965
|
+
- "inputsize" (int): Size of the input data
|
|
2966
|
+
|
|
2967
|
+
Notes
|
|
2968
|
+
-----
|
|
2969
|
+
This method returns a copy of the current configuration parameters. Modifications
|
|
2970
|
+
to the returned dictionary will not affect the original model configuration.
|
|
2971
|
+
|
|
2972
|
+
Examples
|
|
2973
|
+
--------
|
|
2974
|
+
>>> model = MyModel()
|
|
2975
|
+
>>> config = model.get_config()
|
|
2976
|
+
>>> print(config['num_filters'])
|
|
2977
|
+
32
|
|
2978
|
+
"""
|
|
2979
|
+
return {
|
|
2980
|
+
"num_filters": self.num_filters,
|
|
2981
|
+
"kernel_size": self.kernel_size,
|
|
2982
|
+
"encoding_dim": self.encoding_dim,
|
|
2983
|
+
"dropout_rate": self.dropout_rate,
|
|
2984
|
+
"activation": self.activation,
|
|
2985
|
+
"inputsize": self.inputsize,
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
|
|
2989
|
+
class CRNNDLFilter(DeepLearningFilter):
|
|
2990
|
+
def __init__(
|
|
2991
|
+
self,
|
|
2992
|
+
encoding_dim: int = 10,
|
|
2993
|
+
num_filters: int = 10,
|
|
2994
|
+
kernel_size: int = 5,
|
|
2995
|
+
dilation_rate: int = 1,
|
|
2996
|
+
*args,
|
|
2997
|
+
**kwargs,
|
|
2998
|
+
) -> None:
|
|
2999
|
+
"""
|
|
3000
|
+
Initialize CRNNDLFilter layer.
|
|
3001
|
+
|
|
3002
|
+
Parameters
|
|
3003
|
+
----------
|
|
3004
|
+
encoding_dim : int, optional
|
|
3005
|
+
Dimension of the encoding layer, by default 10
|
|
3006
|
+
num_filters : int, optional
|
|
3007
|
+
Number of filters in the convolutional layer, by default 10
|
|
3008
|
+
kernel_size : int, optional
|
|
3009
|
+
Size of the convolutional kernel, by default 5
|
|
3010
|
+
dilation_rate : int, optional
|
|
3011
|
+
Dilation rate for the convolutional layer, by default 1
|
|
3012
|
+
*args : tuple
|
|
3013
|
+
Variable length argument list
|
|
3014
|
+
**kwargs : dict
|
|
3015
|
+
Arbitrary keyword arguments
|
|
3016
|
+
|
|
3017
|
+
Returns
|
|
3018
|
+
-------
|
|
3019
|
+
None
|
|
3020
|
+
This method does not return any value
|
|
3021
|
+
|
|
3022
|
+
Notes
|
|
3023
|
+
-----
|
|
3024
|
+
This constructor initializes a CRNN (Convolutional Recurrent Neural Network)
|
|
3025
|
+
with dilated filters. The layer type is set to "crnn" and configuration
|
|
3026
|
+
parameters are stored in infodict for later reference.
|
|
3027
|
+
|
|
3028
|
+
Examples
|
|
3029
|
+
--------
|
|
3030
|
+
>>> layer = CRNNDLFilter(encoding_dim=20, num_filters=15, kernel_size=3)
|
|
3031
|
+
>>> print(layer.nettype)
|
|
3032
|
+
'crnn'
|
|
3033
|
+
"""
|
|
3034
|
+
self.num_filters = num_filters
|
|
3035
|
+
self.kernel_size = kernel_size
|
|
3036
|
+
self.dilation_rate = dilation_rate
|
|
3037
|
+
self.encoding_dim = encoding_dim
|
|
3038
|
+
self.nettype = "crnn"
|
|
3039
|
+
self.infodict["nettype"] = self.nettype
|
|
3040
|
+
self.infodict["num_filters"] = self.num_filters
|
|
3041
|
+
self.infodict["kernel_size"] = self.kernel_size
|
|
3042
|
+
self.infodict["encoding_dim"] = self.encoding_dim
|
|
3043
|
+
super(CRNNDLFilter, self).__init__(*args, **kwargs)
|
|
3044
|
+
|
|
3045
|
+
def getname(self):
|
|
3046
|
+
"""
|
|
3047
|
+
Generate and configure model name and path based on configuration parameters.
|
|
3048
|
+
|
|
3049
|
+
This method constructs a descriptive model name string based on various configuration
|
|
3050
|
+
parameters and creates the corresponding model directory path. The generated name
|
|
3051
|
+
includes information about window size, encoding dimensions, filters, kernel size,
|
|
3052
|
+
epochs, thresholds, step size, and activation function.
|
|
3053
|
+
|
|
3054
|
+
Parameters
|
|
3055
|
+
----------
|
|
3056
|
+
self : object
|
|
3057
|
+
The instance containing configuration parameters for model naming.
|
|
3058
|
+
|
|
3059
|
+
Returns
|
|
3060
|
+
-------
|
|
3061
|
+
None
|
|
3062
|
+
This method modifies instance attributes in-place and does not return a value.
|
|
3063
|
+
|
|
3064
|
+
Notes
|
|
3065
|
+
-----
|
|
3066
|
+
The generated model name follows a consistent naming convention:
|
|
3067
|
+
'model_crnn_pytorch_wXXX_enXXX_fnXX_flXX_eXXX_tX_ctX_sX_activation'
|
|
3068
|
+
where XXX represents zero-padded numeric values and X represents single digits.
|
|
3069
|
+
|
|
3070
|
+
Additional suffixes are appended based on:
|
|
3071
|
+
- usebadpts: '_usebadpts' if True
|
|
3072
|
+
- excludebysubject: '_excludebysubject' if True
|
|
3073
|
+
- namesuffix: '_{suffix}' if not None
|
|
3074
|
+
|
|
3075
|
+
Examples
|
|
3076
|
+
--------
|
|
3077
|
+
>>> model = ModelClass()
|
|
3078
|
+
>>> model.window_size = 100
|
|
3079
|
+
>>> model.encoding_dim = 128
|
|
3080
|
+
>>> model.num_filters = 32
|
|
3081
|
+
>>> model.kernel_size = 5
|
|
3082
|
+
>>> model.num_epochs = 100
|
|
3083
|
+
>>> model.excludethresh = 0.5
|
|
3084
|
+
>>> model.corrthresh = 0.8
|
|
3085
|
+
>>> model.step = 10
|
|
3086
|
+
>>> model.activation = 'relu'
|
|
3087
|
+
>>> model.modelroot = '/path/to/models'
|
|
3088
|
+
>>> model.getname()
|
|
3089
|
+
>>> print(model.modelname)
|
|
3090
|
+
'model_crnn_pytorch_w100_en128_fn32_fl05_e100_t0.5_ct0.8_s10_relu'
|
|
3091
|
+
"""
|
|
3092
|
+
self.modelname = "_".join(
|
|
3093
|
+
[
|
|
3094
|
+
"model",
|
|
3095
|
+
"crnn",
|
|
3096
|
+
"pytorch",
|
|
3097
|
+
"w" + str(self.window_size).zfill(3),
|
|
3098
|
+
"en" + str(self.encoding_dim).zfill(3),
|
|
3099
|
+
"fn" + str(self.num_filters).zfill(2),
|
|
3100
|
+
"fl" + str(self.kernel_size).zfill(2),
|
|
3101
|
+
"e" + str(self.num_epochs).zfill(3),
|
|
3102
|
+
"t" + str(self.excludethresh),
|
|
3103
|
+
"ct" + str(self.corrthresh),
|
|
3104
|
+
"s" + str(self.step),
|
|
3105
|
+
self.activation,
|
|
3106
|
+
]
|
|
3107
|
+
)
|
|
3108
|
+
if self.usebadpts:
|
|
3109
|
+
self.modelname += "_usebadpts"
|
|
3110
|
+
if self.excludebysubject:
|
|
3111
|
+
self.modelname += "_excludebysubject"
|
|
3112
|
+
if self.namesuffix is not None:
|
|
3113
|
+
self.modelname += "_" + self.namesuffix
|
|
3114
|
+
self.modelpath = os.path.join(self.modelroot, self.modelname)
|
|
3115
|
+
|
|
3116
|
+
try:
|
|
3117
|
+
os.makedirs(self.modelpath)
|
|
3118
|
+
except OSError:
|
|
3119
|
+
pass
|
|
3120
|
+
|
|
3121
|
+
def makenet(self):
|
|
3122
|
+
"""
|
|
3123
|
+
Create and initialize a CRNN model for neural network training.
|
|
3124
|
+
|
|
3125
|
+
This method initializes a CRNN (Convolutional Recurrent Neural Network) model
|
|
3126
|
+
using the specified configuration parameters and moves it to the designated
|
|
3127
|
+
device (CPU or GPU).
|
|
3128
|
+
|
|
3129
|
+
Parameters
|
|
3130
|
+
----------
|
|
3131
|
+
self : object
|
|
3132
|
+
The instance of the class containing this method. Expected to have the
|
|
3133
|
+
following attributes:
|
|
3134
|
+
- num_filters : int
|
|
3135
|
+
Number of filters in the convolutional layers
|
|
3136
|
+
- kernel_size : int or tuple
|
|
3137
|
+
Size of the convolutional kernel
|
|
3138
|
+
- encoding_dim : int
|
|
3139
|
+
Dimension of the encoding layer
|
|
3140
|
+
- dropout_rate : float
|
|
3141
|
+
Dropout rate for regularization
|
|
3142
|
+
- activation : str or callable
|
|
3143
|
+
Activation function to use
|
|
3144
|
+
- inputsize : tuple
|
|
3145
|
+
Input dimensions for the model
|
|
3146
|
+
- device : torch.device
|
|
3147
|
+
Device to move the model to (CPU or GPU)
|
|
3148
|
+
|
|
3149
|
+
Returns
|
|
3150
|
+
-------
|
|
3151
|
+
None
|
|
3152
|
+
This method does not return any value. It initializes the model attribute
|
|
3153
|
+
of the class instance.
|
|
3154
|
+
|
|
3155
|
+
Notes
|
|
3156
|
+
-----
|
|
3157
|
+
The method assumes that the CRNNModel class is properly imported and available
|
|
3158
|
+
in the namespace. The model is automatically moved to the device specified
|
|
3159
|
+
in self.device.
|
|
3160
|
+
|
|
3161
|
+
Examples
|
|
3162
|
+
--------
|
|
3163
|
+
>>> class MyModel:
|
|
3164
|
+
... def __init__(self):
|
|
3165
|
+
... self.num_filters = 32
|
|
3166
|
+
... self.kernel_size = 3
|
|
3167
|
+
... self.encoding_dim = 128
|
|
3168
|
+
... self.dropout_rate = 0.2
|
|
3169
|
+
... self.activation = 'relu'
|
|
3170
|
+
... self.inputsize = (1, 28, 28)
|
|
3171
|
+
... self.device = torch.device('cpu')
|
|
3172
|
+
...
|
|
3173
|
+
... def makenet(self):
|
|
3174
|
+
... self.model = CRNNModel(
|
|
3175
|
+
... self.num_filters,
|
|
3176
|
+
... self.kernel_size,
|
|
3177
|
+
... self.encoding_dim,
|
|
3178
|
+
... self.dropout_rate,
|
|
3179
|
+
... self.activation,
|
|
3180
|
+
... self.inputsize,
|
|
3181
|
+
... )
|
|
3182
|
+
... self.model.to(self.device)
|
|
3183
|
+
...
|
|
3184
|
+
>>> model = MyModel()
|
|
3185
|
+
>>> model.makenet()
|
|
3186
|
+
>>> print(model.model)
|
|
3187
|
+
"""
|
|
3188
|
+
self.model = CRNNModel(
|
|
3189
|
+
self.num_filters,
|
|
3190
|
+
self.kernel_size,
|
|
3191
|
+
self.encoding_dim,
|
|
3192
|
+
self.dropout_rate,
|
|
3193
|
+
self.activation,
|
|
3194
|
+
self.inputsize,
|
|
3195
|
+
)
|
|
3196
|
+
self.model.to(self.device)
|
|
3197
|
+
|
|
3198
|
+
|
|
3199
|
+
class LSTMModel(nn.Module):
|
|
3200
|
+
def __init__(self, num_units, num_layers, dropout_rate, window_size, inputsize):
|
|
3201
|
+
"""
|
|
3202
|
+
Initialize the LSTMModel with specified architecture parameters.
|
|
3203
|
+
|
|
3204
|
+
Parameters
|
|
3205
|
+
----------
|
|
3206
|
+
num_units : int
|
|
3207
|
+
Number of units in each LSTM layer
|
|
3208
|
+
num_layers : int
|
|
3209
|
+
Number of LSTM layers in the model
|
|
3210
|
+
dropout_rate : float
|
|
3211
|
+
Dropout rate for LSTM layers (applied only if num_layers > 1)
|
|
3212
|
+
window_size : int
|
|
3213
|
+
Size of the sliding window used for sequence processing
|
|
3214
|
+
inputsize : int
|
|
3215
|
+
Dimensionality of input features
|
|
3216
|
+
|
|
3217
|
+
Returns
|
|
3218
|
+
-------
|
|
3219
|
+
None
|
|
3220
|
+
Initializes the LSTMModel instance with the specified architecture
|
|
3221
|
+
|
|
3222
|
+
Notes
|
|
3223
|
+
-----
|
|
3224
|
+
This constructor creates a bidirectional LSTM model with residual connections.
|
|
3225
|
+
The model uses LSTM layers with bidirectional processing and time-distributed
|
|
3226
|
+
dense layers for output transformation. Dropout is applied between layers
|
|
3227
|
+
when multiple layers are present.
|
|
3228
|
+
|
|
3229
|
+
Examples
|
|
3230
|
+
--------
|
|
3231
|
+
>>> model = LSTMModel(num_units=128, num_layers=2, dropout_rate=0.2,
|
|
3232
|
+
... window_size=10, inputsize=20)
|
|
3233
|
+
>>> print(model)
|
|
3234
|
+
"""
|
|
3235
|
+
super(LSTMModel, self).__init__()
|
|
3236
|
+
|
|
3237
|
+
self.num_units = num_units
|
|
3238
|
+
self.num_layers = num_layers
|
|
3239
|
+
self.dropout_rate = dropout_rate
|
|
3240
|
+
self.window_size = window_size
|
|
3241
|
+
self.inputsize = inputsize
|
|
3242
|
+
|
|
3243
|
+
self.lstm_layers = nn.ModuleList()
|
|
3244
|
+
self.dense_layers = nn.ModuleList()
|
|
3245
|
+
|
|
3246
|
+
for _ in range(num_layers):
|
|
3247
|
+
# Bidirectional LSTM
|
|
3248
|
+
self.lstm_layers.append(
|
|
3249
|
+
nn.LSTM(
|
|
3250
|
+
inputsize if len(self.lstm_layers) == 0 else inputsize,
|
|
3251
|
+
num_units,
|
|
3252
|
+
batch_first=True,
|
|
3253
|
+
bidirectional=True,
|
|
3254
|
+
dropout=dropout_rate if num_layers > 1 else 0,
|
|
3255
|
+
)
|
|
3256
|
+
)
|
|
3257
|
+
# Time-distributed dense layer
|
|
3258
|
+
self.dense_layers.append(nn.Linear(num_units * 2, inputsize))
|
|
3259
|
+
|
|
3260
|
+
def forward(self, x):
|
|
3261
|
+
"""
|
|
3262
|
+
Forward pass through LSTM and dense layers.
|
|
3263
|
+
|
|
3264
|
+
Apply a sequence of LSTM layers followed by dense layers to the input tensor,
|
|
3265
|
+
with appropriate dimension permutations to maintain correct data flow.
|
|
3266
|
+
|
|
3267
|
+
Parameters
|
|
3268
|
+
----------
|
|
3269
|
+
x : torch.Tensor
|
|
3270
|
+
Input tensor with shape (batch, channels, length) containing the sequential data.
|
|
3271
|
+
|
|
3272
|
+
Returns
|
|
3273
|
+
-------
|
|
3274
|
+
torch.Tensor
|
|
3275
|
+
Output tensor with shape (batch, channels, length) after processing through
|
|
3276
|
+
LSTM and dense layers.
|
|
3277
|
+
|
|
3278
|
+
Notes
|
|
3279
|
+
-----
|
|
3280
|
+
The function performs the following operations:
|
|
3281
|
+
1. Permutes input from (batch, channels, length) to (batch, length, channels)
|
|
3282
|
+
2. Processes through LSTM layers sequentially
|
|
3283
|
+
3. Applies dense layers to each time step
|
|
3284
|
+
4. Permutes output back to (batch, channels, length)
|
|
3285
|
+
|
|
3286
|
+
Examples
|
|
3287
|
+
--------
|
|
3288
|
+
>>> import torch
|
|
3289
|
+
>>> # Assuming self.lstm_layers and self.dense_layers are initialized
|
|
3290
|
+
>>> x = torch.randn(32, 128, 100) # batch=32, channels=128, length=100
|
|
3291
|
+
>>> output = model.forward(x)
|
|
3292
|
+
>>> output.shape
|
|
3293
|
+
torch.Size([32, 128, 100])
|
|
3294
|
+
"""
|
|
3295
|
+
# x is (batch, channels, length), convert to (batch, length, channels)
|
|
3296
|
+
x = x.permute(0, 2, 1)
|
|
3297
|
+
|
|
3298
|
+
for lstm, dense in zip(self.lstm_layers, self.dense_layers):
|
|
3299
|
+
x, _ = lstm(x)
|
|
3300
|
+
# Apply dense layer across time steps
|
|
3301
|
+
x = dense(x)
|
|
3302
|
+
|
|
3303
|
+
# Convert back to (batch, channels, length)
|
|
3304
|
+
x = x.permute(0, 2, 1)
|
|
3305
|
+
|
|
3306
|
+
return x
|
|
3307
|
+
|
|
3308
|
+
def get_config(self):
|
|
3309
|
+
"""
|
|
3310
|
+
Get the configuration parameters of the model.
|
|
3311
|
+
|
|
3312
|
+
Returns
|
|
3313
|
+
-------
|
|
3314
|
+
dict
|
|
3315
|
+
A dictionary containing the model configuration parameters with the following keys:
|
|
3316
|
+
- "num_units" (int): Number of units in each layer
|
|
3317
|
+
- "num_layers" (int): Number of layers in the model
|
|
3318
|
+
- "dropout_rate" (float): Dropout rate for regularization
|
|
3319
|
+
- "window_size" (int): Size of the sliding window for sequence processing
|
|
3320
|
+
- "inputsize" (int): Size of the input features
|
|
3321
|
+
|
|
3322
|
+
Notes
|
|
3323
|
+
-----
|
|
3324
|
+
This method returns a copy of the internal configuration parameters.
|
|
3325
|
+
The returned dictionary can be used to recreate the model with the same configuration.
|
|
3326
|
+
|
|
3327
|
+
Examples
|
|
3328
|
+
--------
|
|
3329
|
+
>>> config = model.get_config()
|
|
3330
|
+
>>> print(config['num_units'])
|
|
3331
|
+
128
|
|
3332
|
+
>>> new_model = ModelClass(**config)
|
|
3333
|
+
"""
|
|
3334
|
+
return {
|
|
3335
|
+
"num_units": self.num_units,
|
|
3336
|
+
"num_layers": self.num_layers,
|
|
3337
|
+
"dropout_rate": self.dropout_rate,
|
|
3338
|
+
"window_size": self.window_size,
|
|
3339
|
+
"inputsize": self.inputsize,
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
|
|
3343
|
+
class LSTMDLFilter(DeepLearningFilter):
|
|
3344
|
+
def __init__(self, num_units: int = 16, *args, **kwargs) -> None:
|
|
3345
|
+
"""
|
|
3346
|
+
Initialize the LSTMDLFilter layer.
|
|
3347
|
+
|
|
3348
|
+
Parameters
|
|
3349
|
+
----------
|
|
3350
|
+
num_units : int, optional
|
|
3351
|
+
Number of units in the LSTM layer, by default 16
|
|
3352
|
+
*args
|
|
3353
|
+
Variable length argument list passed to parent class
|
|
3354
|
+
**kwargs
|
|
3355
|
+
Arbitrary keyword arguments passed to parent class
|
|
3356
|
+
|
|
3357
|
+
Returns
|
|
3358
|
+
-------
|
|
3359
|
+
None
|
|
3360
|
+
This method initializes the instance and does not return any value
|
|
3361
|
+
|
|
3362
|
+
Notes
|
|
3363
|
+
-----
|
|
3364
|
+
This constructor sets up the LSTM layer with specified number of units and
|
|
3365
|
+
initializes the network type identifier. The infodict is updated with both
|
|
3366
|
+
the network type and number of units for tracking purposes.
|
|
3367
|
+
|
|
3368
|
+
Examples
|
|
3369
|
+
--------
|
|
3370
|
+
>>> layer = LSTMDLFilter(num_units=32)
|
|
3371
|
+
>>> print(layer.num_units)
|
|
3372
|
+
32
|
|
3373
|
+
>>> print(layer.nettype)
|
|
3374
|
+
'lstm'
|
|
3375
|
+
"""
|
|
3376
|
+
self.num_units = num_units
|
|
3377
|
+
self.nettype = "lstm"
|
|
3378
|
+
self.infodict["nettype"] = self.nettype
|
|
3379
|
+
self.infodict["num_units"] = self.num_units
|
|
3380
|
+
super(LSTMDLFilter, self).__init__(*args, **kwargs)
|
|
3381
|
+
|
|
3382
|
+
def getname(self):
|
|
3383
|
+
"""
|
|
3384
|
+
Generate and configure model name and path based on current parameters.
|
|
3385
|
+
|
|
3386
|
+
This method constructs a descriptive model name string using various
|
|
3387
|
+
hyperparameters and configuration settings. It then creates the
|
|
3388
|
+
corresponding directory path and ensures it exists.
|
|
3389
|
+
|
|
3390
|
+
Parameters
|
|
3391
|
+
----------
|
|
3392
|
+
self : object
|
|
3393
|
+
The instance containing model configuration attributes.
|
|
3394
|
+
|
|
3395
|
+
Returns
|
|
3396
|
+
-------
|
|
3397
|
+
None
|
|
3398
|
+
This method modifies instance attributes in-place and does not return a value.
|
|
3399
|
+
|
|
3400
|
+
Notes
|
|
3401
|
+
-----
|
|
3402
|
+
The generated model name follows a specific format:
|
|
3403
|
+
"model_lstm_pytorch_wXXX_lYY_nuZZZ_dDD_rdDD_eFFF_tT_ctTT_sS"
|
|
3404
|
+
where XXX, YY, ZZZ, DD, FF, T, TT, S represent formatted parameter values.
|
|
3405
|
+
|
|
3406
|
+
Examples
|
|
3407
|
+
--------
|
|
3408
|
+
>>> model = MyModel()
|
|
3409
|
+
>>> model.window_size = 100
|
|
3410
|
+
>>> model.num_layers = 2
|
|
3411
|
+
>>> model.num_units = 128
|
|
3412
|
+
>>> model.dropout_rate = 0.2
|
|
3413
|
+
>>> model.num_epochs = 100
|
|
3414
|
+
>>> model.excludethresh = 0.5
|
|
3415
|
+
>>> model.corrthresh = 0.8
|
|
3416
|
+
>>> model.step = 1
|
|
3417
|
+
>>> model.excludebysubject = True
|
|
3418
|
+
>>> model.getname()
|
|
3419
|
+
>>> print(model.modelname)
|
|
3420
|
+
'model_lstm_pytorch_w100_l02_nu128_d02_rd02_e100_t05_ct08_s1_excludebysubject'
|
|
3421
|
+
"""
|
|
3422
|
+
self.modelname = "_".join(
|
|
3423
|
+
[
|
|
3424
|
+
"model",
|
|
3425
|
+
"lstm",
|
|
3426
|
+
"pytorch",
|
|
3427
|
+
"w" + str(self.window_size).zfill(3),
|
|
3428
|
+
"l" + str(self.num_layers).zfill(2),
|
|
3429
|
+
"nu" + str(self.num_units),
|
|
3430
|
+
"d" + str(self.dropout_rate),
|
|
3431
|
+
"rd" + str(self.dropout_rate),
|
|
3432
|
+
"e" + str(self.num_epochs).zfill(3),
|
|
3433
|
+
"t" + str(self.excludethresh),
|
|
3434
|
+
"ct" + str(self.corrthresh),
|
|
3435
|
+
"s" + str(self.step),
|
|
3436
|
+
]
|
|
3437
|
+
)
|
|
3438
|
+
if self.excludebysubject:
|
|
3439
|
+
self.modelname += "_excludebysubject"
|
|
3440
|
+
self.modelpath = os.path.join(self.modelroot, self.modelname)
|
|
3441
|
+
|
|
3442
|
+
try:
|
|
3443
|
+
os.makedirs(self.modelpath)
|
|
3444
|
+
except OSError:
|
|
3445
|
+
pass
|
|
3446
|
+
|
|
3447
|
+
def makenet(self):
|
|
3448
|
+
"""
|
|
3449
|
+
Create and initialize an LSTM model for neural network training.
|
|
3450
|
+
|
|
3451
|
+
This method initializes an LSTMModel with the specified architecture parameters
|
|
3452
|
+
and moves the model to the designated device (CPU or GPU).
|
|
3453
|
+
|
|
3454
|
+
Parameters
|
|
3455
|
+
----------
|
|
3456
|
+
self : object
|
|
3457
|
+
The instance containing the following attributes:
|
|
3458
|
+
- num_units : int
|
|
3459
|
+
Number of units in each LSTM layer
|
|
3460
|
+
- num_layers : int
|
|
3461
|
+
Number of LSTM layers in the model
|
|
3462
|
+
- dropout_rate : float
|
|
3463
|
+
Dropout rate for regularization
|
|
3464
|
+
- window_size : int
|
|
3465
|
+
Size of the input window for time series data
|
|
3466
|
+
- inputsize : int
|
|
3467
|
+
Size of the input features
|
|
3468
|
+
- device : torch.device
|
|
3469
|
+
Device to move the model to (e.g., 'cpu' or 'cuda')
|
|
3470
|
+
|
|
3471
|
+
Returns
|
|
3472
|
+
-------
|
|
3473
|
+
None
|
|
3474
|
+
This method does not return any value. It initializes the model attribute
|
|
3475
|
+
and moves it to the specified device.
|
|
3476
|
+
|
|
3477
|
+
Notes
|
|
3478
|
+
-----
|
|
3479
|
+
The method creates an LSTMModel instance with the following parameters:
|
|
3480
|
+
- num_units: Number of hidden units in LSTM layers
|
|
3481
|
+
- num_layers: Number of stacked LSTM layers
|
|
3482
|
+
- dropout_rate: Dropout probability for regularization
|
|
3483
|
+
- window_size: Input sequence length
|
|
3484
|
+
- inputsize: Feature dimension of input data
|
|
3485
|
+
|
|
3486
|
+
Examples
|
|
3487
|
+
--------
|
|
3488
|
+
>>> # Assuming self is an instance with required attributes
|
|
3489
|
+
>>> self.makenet()
|
|
3490
|
+
>>> # Model is now initialized and moved to specified device
|
|
3491
|
+
"""
|
|
3492
|
+
self.model = LSTMModel(
|
|
3493
|
+
self.num_units,
|
|
3494
|
+
self.num_layers,
|
|
3495
|
+
self.dropout_rate,
|
|
3496
|
+
self.window_size,
|
|
3497
|
+
self.inputsize,
|
|
3498
|
+
)
|
|
3499
|
+
self.model.to(self.device)
|
|
3500
|
+
|
|
3501
|
+
|
|
3502
|
+
class HybridModel(nn.Module):
|
|
3503
|
+
def __init__(
|
|
3504
|
+
self,
|
|
3505
|
+
num_filters,
|
|
3506
|
+
kernel_size,
|
|
3507
|
+
num_units,
|
|
3508
|
+
num_layers,
|
|
3509
|
+
dropout_rate,
|
|
3510
|
+
activation,
|
|
3511
|
+
inputsize,
|
|
3512
|
+
window_size,
|
|
3513
|
+
invert,
|
|
3514
|
+
):
|
|
3515
|
+
"""
|
|
3516
|
+
Initialize the HybridModel with configurable CNN-LSTM architecture.
|
|
3517
|
+
|
|
3518
|
+
Parameters
|
|
3519
|
+
----------
|
|
3520
|
+
num_filters : int
|
|
3521
|
+
Number of filters in the convolutional layers.
|
|
3522
|
+
kernel_size : int
|
|
3523
|
+
Size of the convolutional kernel.
|
|
3524
|
+
num_units : int
|
|
3525
|
+
Number of units in the LSTM layers.
|
|
3526
|
+
num_layers : int
|
|
3527
|
+
Total number of layers in the model.
|
|
3528
|
+
dropout_rate : float
|
|
3529
|
+
Dropout rate for regularization.
|
|
3530
|
+
activation : str
|
|
3531
|
+
Activation function to use; options are 'relu' or 'tanh'.
|
|
3532
|
+
inputsize : int
|
|
3533
|
+
Size of the input features.
|
|
3534
|
+
window_size : int
|
|
3535
|
+
Size of the sliding window for input data.
|
|
3536
|
+
invert : bool
|
|
3537
|
+
If True, applies CNN first followed by LSTM. Otherwise, applies LSTM first followed by CNN.
|
|
3538
|
+
|
|
3539
|
+
Returns
|
|
3540
|
+
-------
|
|
3541
|
+
None
|
|
3542
|
+
This method initializes the model's layers and components but does not return any value.
|
|
3543
|
+
|
|
3544
|
+
Notes
|
|
3545
|
+
-----
|
|
3546
|
+
The model supports two architectures:
|
|
3547
|
+
- If `invert=False`: LSTM → CNN
|
|
3548
|
+
- If `invert=True`: CNN → LSTM
|
|
3549
|
+
|
|
3550
|
+
Examples
|
|
3551
|
+
--------
|
|
3552
|
+
>>> model = HybridModel(
|
|
3553
|
+
... num_filters=64,
|
|
3554
|
+
... kernel_size=3,
|
|
3555
|
+
... num_units=128,
|
|
3556
|
+
... num_layers=3,
|
|
3557
|
+
... dropout_rate=0.2,
|
|
3558
|
+
... activation="relu",
|
|
3559
|
+
... inputsize=10,
|
|
3560
|
+
... window_size=100,
|
|
3561
|
+
... invert=True
|
|
3562
|
+
... )
|
|
3563
|
+
"""
|
|
3564
|
+
super(HybridModel, self).__init__()
|
|
3565
|
+
|
|
3566
|
+
self.num_filters = num_filters
|
|
3567
|
+
self.kernel_size = kernel_size
|
|
3568
|
+
self.num_units = num_units
|
|
3569
|
+
self.num_layers = num_layers
|
|
3570
|
+
self.dropout_rate = dropout_rate
|
|
3571
|
+
self.activation = activation
|
|
3572
|
+
self.inputsize = inputsize
|
|
3573
|
+
self.window_size = window_size
|
|
3574
|
+
self.invert = invert
|
|
3575
|
+
|
|
3576
|
+
# Get activation function
|
|
3577
|
+
if activation == "relu":
|
|
3578
|
+
act_fn = nn.ReLU
|
|
3579
|
+
elif activation == "tanh":
|
|
3580
|
+
act_fn = nn.Tanh
|
|
3581
|
+
else:
|
|
3582
|
+
act_fn = nn.ReLU
|
|
3583
|
+
|
|
3584
|
+
self.layers = nn.ModuleList()
|
|
3585
|
+
|
|
3586
|
+
if invert:
|
|
3587
|
+
# CNN first, then LSTM
|
|
3588
|
+
# Input layer
|
|
3589
|
+
self.layers.append(nn.Conv1d(inputsize, num_filters, kernel_size, padding="same"))
|
|
3590
|
+
self.layers.append(nn.BatchNorm1d(num_filters))
|
|
3591
|
+
self.layers.append(nn.Dropout(dropout_rate))
|
|
3592
|
+
self.layers.append(act_fn())
|
|
3593
|
+
|
|
3594
|
+
# Intermediate CNN layers
|
|
3595
|
+
for _ in range(num_layers - 2):
|
|
3596
|
+
self.layers.append(
|
|
3597
|
+
nn.Conv1d(num_filters, num_filters, kernel_size, padding="same")
|
|
3598
|
+
)
|
|
3599
|
+
self.layers.append(nn.BatchNorm1d(num_filters))
|
|
3600
|
+
self.layers.append(nn.Dropout(dropout_rate))
|
|
3601
|
+
self.layers.append(act_fn())
|
|
3602
|
+
|
|
3603
|
+
# LSTM layer
|
|
3604
|
+
self.lstm = nn.LSTM(
|
|
3605
|
+
num_filters, num_units, batch_first=True, bidirectional=True, dropout=dropout_rate
|
|
3606
|
+
)
|
|
3607
|
+
self.lstm_dense = nn.Linear(num_units * 2, inputsize)
|
|
3608
|
+
|
|
3609
|
+
else:
|
|
3610
|
+
# LSTM first, then CNN
|
|
3611
|
+
self.lstm = nn.LSTM(
|
|
3612
|
+
inputsize, num_units, batch_first=True, bidirectional=True, dropout=dropout_rate
|
|
3613
|
+
)
|
|
3614
|
+
self.lstm_dense = nn.Linear(num_units * 2, inputsize)
|
|
3615
|
+
self.lstm_dropout = nn.Dropout(dropout_rate)
|
|
3616
|
+
|
|
3617
|
+
# Intermediate CNN layers
|
|
3618
|
+
for _ in range(num_layers - 2):
|
|
3619
|
+
self.layers.append(nn.Conv1d(inputsize, num_filters, kernel_size, padding="same"))
|
|
3620
|
+
self.layers.append(nn.BatchNorm1d(num_filters))
|
|
3621
|
+
self.layers.append(nn.Dropout(dropout_rate))
|
|
3622
|
+
self.layers.append(act_fn())
|
|
3623
|
+
|
|
3624
|
+
# Output layer
|
|
3625
|
+
self.output_conv = nn.Conv1d(
|
|
3626
|
+
num_filters if num_layers > 2 else inputsize,
|
|
3627
|
+
inputsize,
|
|
3628
|
+
kernel_size,
|
|
3629
|
+
padding="same",
|
|
3630
|
+
)
|
|
3631
|
+
|
|
3632
|
+
def forward(self, x):
|
|
3633
|
+
"""
|
|
3634
|
+
Forward pass of the model with optional CNN-LSTM hybrid architecture.
|
|
3635
|
+
|
|
3636
|
+
This method implements a flexible forward pass that can operate in two modes
|
|
3637
|
+
depending on the `invert` flag. When `invert` is True, the sequence processing
|
|
3638
|
+
follows CNN → LSTM → CNN structure. When `invert` is False, the sequence
|
|
3639
|
+
processing follows LSTM → CNN structure.
|
|
3640
|
+
|
|
3641
|
+
Parameters
|
|
3642
|
+
----------
|
|
3643
|
+
x : torch.Tensor
|
|
3644
|
+
Input tensor of shape (batch_size, channels, sequence_length) or
|
|
3645
|
+
(batch_size, sequence_length, channels) depending on the processing path.
|
|
3646
|
+
|
|
3647
|
+
Returns
|
|
3648
|
+
-------
|
|
3649
|
+
torch.Tensor
|
|
3650
|
+
Output tensor with the same batch dimension as input, with shape
|
|
3651
|
+
dependent on the specific layers and processing path used.
|
|
3652
|
+
|
|
3653
|
+
Notes
|
|
3654
|
+
-----
|
|
3655
|
+
The function handles different tensor permutations based on the processing
|
|
3656
|
+
order:
|
|
3657
|
+
- CNN → LSTM path: permutes from (B, C, L) to (B, L, C) for LSTM, then back
|
|
3658
|
+
- LSTM → CNN path: permutes from (B, C, L) to (B, L, C) for LSTM, then back
|
|
3659
|
+
The `invert` flag determines which processing order is used.
|
|
3660
|
+
|
|
3661
|
+
Examples
|
|
3662
|
+
--------
|
|
3663
|
+
>>> model = MyModel()
|
|
3664
|
+
>>> x = torch.randn(32, 10, 128) # batch_size=32, seq_len=10, features=128
|
|
3665
|
+
>>> output = model.forward(x)
|
|
3666
|
+
>>> print(output.shape)
|
|
3667
|
+
torch.Size([32, 10, 256])
|
|
3668
|
+
"""
|
|
3669
|
+
if self.invert:
|
|
3670
|
+
# Apply CNN layers
|
|
3671
|
+
for layer in self.layers:
|
|
3672
|
+
x = layer(x)
|
|
3673
|
+
|
|
3674
|
+
# LSTM expects (batch, seq_len, features)
|
|
3675
|
+
x = x.permute(0, 2, 1)
|
|
3676
|
+
x, _ = self.lstm(x)
|
|
3677
|
+
x = self.lstm_dense(x)
|
|
3678
|
+
|
|
3679
|
+
# Convert back to (batch, channels, length)
|
|
3680
|
+
x = x.permute(0, 2, 1)
|
|
3681
|
+
|
|
3682
|
+
else:
|
|
3683
|
+
# LSTM first
|
|
3684
|
+
x = x.permute(0, 2, 1)
|
|
3685
|
+
x, _ = self.lstm(x)
|
|
3686
|
+
x = self.lstm_dense(x)
|
|
3687
|
+
x = self.lstm_dropout(x)
|
|
3688
|
+
x = x.permute(0, 2, 1)
|
|
3689
|
+
|
|
3690
|
+
# CNN layers
|
|
3691
|
+
for layer in self.layers:
|
|
3692
|
+
x = layer(x)
|
|
3693
|
+
|
|
3694
|
+
# Output layer
|
|
3695
|
+
if hasattr(self, "output_conv"):
|
|
3696
|
+
x = self.output_conv(x)
|
|
3697
|
+
|
|
3698
|
+
return x
|
|
3699
|
+
|
|
3700
|
+
def get_config(self):
|
|
3701
|
+
"""
|
|
3702
|
+
Get the configuration parameters of the model.
|
|
3703
|
+
|
|
3704
|
+
Returns
|
|
3705
|
+
-------
|
|
3706
|
+
dict
|
|
3707
|
+
A dictionary containing all configuration parameters with their current values:
|
|
3708
|
+
- num_filters: int, number of filters in the convolutional layers
|
|
3709
|
+
- kernel_size: int, size of the convolutional kernel
|
|
3710
|
+
- num_units: int, number of units in the dense layers
|
|
3711
|
+
- num_layers: int, number of layers in the model
|
|
3712
|
+
- dropout_rate: float, dropout rate for regularization
|
|
3713
|
+
- activation: str or callable, activation function to use
|
|
3714
|
+
- inputsize: int, size of the input features
|
|
3715
|
+
- window_size: int, size of the sliding window
|
|
3716
|
+
- invert: bool, whether to invert the input data
|
|
3717
|
+
|
|
3718
|
+
Notes
|
|
3719
|
+
-----
|
|
3720
|
+
This method returns a copy of the internal configuration dictionary.
|
|
3721
|
+
Modifications to the returned dictionary will not affect the original model configuration.
|
|
3722
|
+
|
|
3723
|
+
Examples
|
|
3724
|
+
--------
|
|
3725
|
+
>>> config = model.get_config()
|
|
3726
|
+
>>> print(config['num_filters'])
|
|
3727
|
+
32
|
|
3728
|
+
"""
|
|
3729
|
+
return {
|
|
3730
|
+
"num_filters": self.num_filters,
|
|
3731
|
+
"kernel_size": self.kernel_size,
|
|
3732
|
+
"num_units": self.num_units,
|
|
3733
|
+
"num_layers": self.num_layers,
|
|
3734
|
+
"dropout_rate": self.dropout_rate,
|
|
3735
|
+
"activation": self.activation,
|
|
3736
|
+
"inputsize": self.inputsize,
|
|
3737
|
+
"window_size": self.window_size,
|
|
3738
|
+
"invert": self.invert,
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
|
|
3742
|
+
class HybridDLFilter(DeepLearningFilter):
|
|
3743
|
+
def __init__(
|
|
3744
|
+
self,
|
|
3745
|
+
invert: bool = False,
|
|
3746
|
+
num_filters: int = 10,
|
|
3747
|
+
kernel_size: int = 5,
|
|
3748
|
+
num_units: int = 16,
|
|
3749
|
+
*args,
|
|
3750
|
+
**kwargs,
|
|
3751
|
+
) -> None:
|
|
3752
|
+
"""
|
|
3753
|
+
Initialize HybridDLFilter layer.
|
|
3754
|
+
|
|
3755
|
+
Parameters
|
|
3756
|
+
----------
|
|
3757
|
+
invert : bool, default=False
|
|
3758
|
+
If True, inverts the filter response. If False, applies normal filtering.
|
|
3759
|
+
num_filters : int, default=10
|
|
3760
|
+
Number of filters to apply in the convolutional layer.
|
|
3761
|
+
kernel_size : int, default=5
|
|
3762
|
+
Size of the convolutional kernel.
|
|
3763
|
+
num_units : int, default=16
|
|
3764
|
+
Number of units in the dense layer.
|
|
3765
|
+
*args
|
|
3766
|
+
Variable length argument list.
|
|
3767
|
+
**kwargs
|
|
3768
|
+
Arbitrary keyword arguments.
|
|
3769
|
+
|
|
3770
|
+
Returns
|
|
3771
|
+
-------
|
|
3772
|
+
None
|
|
3773
|
+
This method initializes the HybridDLFilter instance and does not return any value.
|
|
3774
|
+
|
|
3775
|
+
Notes
|
|
3776
|
+
-----
|
|
3777
|
+
This constructor sets up a hybrid deep learning filter that combines convolutional
|
|
3778
|
+
and dense layers. The infodict dictionary is populated with configuration parameters
|
|
3779
|
+
for tracking and logging purposes.
|
|
3780
|
+
|
|
3781
|
+
Examples
|
|
3782
|
+
--------
|
|
3783
|
+
>>> filter_layer = HybridDLFilter(
|
|
3784
|
+
... invert=True,
|
|
3785
|
+
... num_filters=20,
|
|
3786
|
+
... kernel_size=3,
|
|
3787
|
+
... num_units=32
|
|
3788
|
+
... )
|
|
3789
|
+
"""
|
|
3790
|
+
self.invert = invert
|
|
3791
|
+
self.num_filters = num_filters
|
|
3792
|
+
self.kernel_size = kernel_size
|
|
3793
|
+
self.num_units = num_units
|
|
3794
|
+
self.nettype = "hybrid"
|
|
3795
|
+
self.infodict["nettype"] = self.nettype
|
|
3796
|
+
self.infodict["num_filters"] = self.num_filters
|
|
3797
|
+
self.infodict["kernel_size"] = self.kernel_size
|
|
3798
|
+
self.infodict["invert"] = self.invert
|
|
3799
|
+
self.infodict["num_units"] = self.num_units
|
|
3800
|
+
super(HybridDLFilter, self).__init__(*args, **kwargs)
|
|
3801
|
+
|
|
3802
|
+
def getname(self):
|
|
3803
|
+
"""
|
|
3804
|
+
Generate and configure the model name and path based on current parameters.
|
|
3805
|
+
|
|
3806
|
+
This method constructs a descriptive model name string using various
|
|
3807
|
+
hyperparameters and configuration settings. The generated name follows
|
|
3808
|
+
a standardized format that includes window size, layer count, filter count,
|
|
3809
|
+
kernel size, number of units, dropout rates, number of epochs, threshold
|
|
3810
|
+
values, step size, and activation function. The method also creates the
|
|
3811
|
+
corresponding model directory path and ensures it exists.
|
|
3812
|
+
|
|
3813
|
+
Parameters
|
|
3814
|
+
----------
|
|
3815
|
+
self : object
|
|
3816
|
+
The instance of the class containing the model configuration attributes.
|
|
3817
|
+
Required attributes include:
|
|
3818
|
+
- window_size : int
|
|
3819
|
+
- num_layers : int
|
|
3820
|
+
- num_filters : int
|
|
3821
|
+
- kernel_size : int
|
|
3822
|
+
- num_units : int
|
|
3823
|
+
- dropout_rate : float
|
|
3824
|
+
- num_epochs : int
|
|
3825
|
+
- excludethresh : float
|
|
3826
|
+
- corrthresh : float
|
|
3827
|
+
- step : int
|
|
3828
|
+
- activation : str
|
|
3829
|
+
- invert : bool
|
|
3830
|
+
- excludebysubject : bool
|
|
3831
|
+
- modelroot : str
|
|
3832
|
+
|
|
3833
|
+
Returns
|
|
3834
|
+
-------
|
|
3835
|
+
None
|
|
3836
|
+
This method modifies the instance attributes in-place:
|
|
3837
|
+
- self.modelname : str
|
|
3838
|
+
- self.modelpath : str
|
|
3839
|
+
|
|
3840
|
+
Notes
|
|
3841
|
+
-----
|
|
3842
|
+
The model name format follows this pattern:
|
|
3843
|
+
"model_hybrid_pytorch_wXXX_lYY_fnZZ_flZZ_nuZZ_dZZ_rdZZ_eXXX_tX_ctX_sX_activation"
|
|
3844
|
+
where XXX, YY, ZZ, etc. represent zero-padded numerical values.
|
|
3845
|
+
|
|
3846
|
+
Additional suffixes are appended based on:
|
|
3847
|
+
- "_invert" if self.invert is True
|
|
3848
|
+
- "_excludebysubject" if self.excludebysubject is True
|
|
3849
|
+
|
|
3850
|
+
Examples
|
|
3851
|
+
--------
|
|
3852
|
+
>>> model = MyModel()
|
|
3853
|
+
>>> model.window_size = 100
|
|
3854
|
+
>>> model.num_layers = 2
|
|
3855
|
+
>>> model.getname()
|
|
3856
|
+
>>> print(model.modelname)
|
|
3857
|
+
'model_hybrid_pytorch_w100_l02_fn08_fl08_nu128_d05_rd05_e100_t05_ct08_s1_relu'
|
|
3858
|
+
"""
|
|
3859
|
+
self.modelname = "_".join(
|
|
3860
|
+
[
|
|
3861
|
+
"model",
|
|
3862
|
+
"hybrid",
|
|
3863
|
+
"pytorch",
|
|
3864
|
+
"w" + str(self.window_size).zfill(3),
|
|
3865
|
+
"l" + str(self.num_layers).zfill(2),
|
|
3866
|
+
"fn" + str(self.num_filters).zfill(2),
|
|
3867
|
+
"fl" + str(self.kernel_size).zfill(2),
|
|
3868
|
+
"nu" + str(self.num_units),
|
|
3869
|
+
"d" + str(self.dropout_rate),
|
|
3870
|
+
"rd" + str(self.dropout_rate),
|
|
3871
|
+
"e" + str(self.num_epochs).zfill(3),
|
|
3872
|
+
"t" + str(self.excludethresh),
|
|
3873
|
+
"ct" + str(self.corrthresh),
|
|
3874
|
+
"s" + str(self.step),
|
|
3875
|
+
self.activation,
|
|
3876
|
+
]
|
|
3877
|
+
)
|
|
3878
|
+
if self.invert:
|
|
3879
|
+
self.modelname += "_invert"
|
|
3880
|
+
if self.excludebysubject:
|
|
3881
|
+
self.modelname += "_excludebysubject"
|
|
3882
|
+
self.modelpath = os.path.join(self.modelroot, self.modelname)
|
|
3883
|
+
|
|
3884
|
+
try:
|
|
3885
|
+
os.makedirs(self.modelpath)
|
|
3886
|
+
except OSError:
|
|
3887
|
+
pass
|
|
3888
|
+
|
|
3889
|
+
def makenet(self):
|
|
3890
|
+
"""
|
|
3891
|
+
Create and initialize a hybrid neural network model.
|
|
3892
|
+
|
|
3893
|
+
This method constructs a HybridModel with the specified architecture parameters
|
|
3894
|
+
and moves it to the designated device (CPU or GPU).
|
|
3895
|
+
|
|
3896
|
+
Parameters
|
|
3897
|
+
----------
|
|
3898
|
+
self : object
|
|
3899
|
+
The instance containing the following attributes:
|
|
3900
|
+
- num_filters : int
|
|
3901
|
+
Number of filters in the convolutional layers
|
|
3902
|
+
- kernel_size : int
|
|
3903
|
+
Size of the convolutional kernels
|
|
3904
|
+
- num_units : int
|
|
3905
|
+
Number of units in the dense layers
|
|
3906
|
+
- num_layers : int
|
|
3907
|
+
Number of layers in the model
|
|
3908
|
+
- dropout_rate : float
|
|
3909
|
+
Dropout rate for regularization
|
|
3910
|
+
- activation : str or callable
|
|
3911
|
+
Activation function to use
|
|
3912
|
+
- inputsize : int
|
|
3913
|
+
Size of the input features
|
|
3914
|
+
- window_size : int
|
|
3915
|
+
Size of the sliding window
|
|
3916
|
+
- invert : bool
|
|
3917
|
+
Whether to invert the model architecture
|
|
3918
|
+
|
|
3919
|
+
Returns
|
|
3920
|
+
-------
|
|
3921
|
+
None
|
|
3922
|
+
This method does not return any value. It initializes the model attribute
|
|
3923
|
+
and moves it to the specified device.
|
|
3924
|
+
|
|
3925
|
+
Notes
|
|
3926
|
+
-----
|
|
3927
|
+
The method assumes that the instance has all required attributes set before
|
|
3928
|
+
calling. The model is moved to the device specified by `self.device`.
|
|
3929
|
+
|
|
3930
|
+
Examples
|
|
3931
|
+
--------
|
|
3932
|
+
>>> model = MyModel()
|
|
3933
|
+
>>> model.num_filters = 32
|
|
3934
|
+
>>> model.kernel_size = 3
|
|
3935
|
+
>>> model.num_units = 64
|
|
3936
|
+
>>> model.num_layers = 2
|
|
3937
|
+
>>> model.dropout_rate = 0.2
|
|
3938
|
+
>>> model.activation = 'relu'
|
|
3939
|
+
>>> model.inputsize = 10
|
|
3940
|
+
>>> model.window_size = 5
|
|
3941
|
+
>>> model.invert = False
|
|
3942
|
+
>>> model.device = 'cuda'
|
|
3943
|
+
>>> model.makenet()
|
|
3944
|
+
>>> print(model.model)
|
|
3945
|
+
"""
|
|
3946
|
+
self.model = HybridModel(
|
|
3947
|
+
self.num_filters,
|
|
3948
|
+
self.kernel_size,
|
|
3949
|
+
self.num_units,
|
|
3950
|
+
self.num_layers,
|
|
3951
|
+
self.dropout_rate,
|
|
3952
|
+
self.activation,
|
|
3953
|
+
self.inputsize,
|
|
3954
|
+
self.window_size,
|
|
3955
|
+
self.invert,
|
|
3956
|
+
)
|
|
3957
|
+
self.model.to(self.device)
|
|
3958
|
+
|
|
3959
|
+
|
|
3960
|
+
def filtscale(
|
|
3961
|
+
data: np.ndarray,
|
|
3962
|
+
scalefac: float = 1.0,
|
|
3963
|
+
reverse: bool = False,
|
|
3964
|
+
hybrid: bool = False,
|
|
3965
|
+
lognormalize: bool = True,
|
|
3966
|
+
epsilon: float = 1e-10,
|
|
3967
|
+
numorders: int = 6,
|
|
3968
|
+
) -> tuple[np.ndarray, float] | np.ndarray:
|
|
3969
|
+
"""
|
|
3970
|
+
Apply or reverse a scaling transformation to spectral data.
|
|
3971
|
+
|
|
3972
|
+
This function performs either forward or inverse scaling of input data,
|
|
3973
|
+
typically used in signal processing or spectral analysis. In forward mode,
|
|
3974
|
+
it computes the FFT of the input data and applies normalization and scaling
|
|
3975
|
+
to the magnitude and phase components. In reverse mode, it reconstructs
|
|
3976
|
+
the original time-domain signal from scaled magnitude and phase components.
|
|
3977
|
+
|
|
3978
|
+
Parameters
|
|
3979
|
+
----------
|
|
3980
|
+
data : np.ndarray
|
|
3981
|
+
Input time-domain signal or scaled spectral data depending on `reverse` flag.
|
|
3982
|
+
scalefac : float, optional
|
|
3983
|
+
Scaling factor used in normalization. Default is 1.0.
|
|
3984
|
+
reverse : bool, optional
|
|
3985
|
+
If True, performs inverse transformation to reconstruct the original signal.
|
|
3986
|
+
If False, performs forward transformation. Default is False.
|
|
3987
|
+
hybrid : bool, optional
|
|
3988
|
+
If True, returns a hybrid output combining original signal and magnitude.
|
|
3989
|
+
Only applicable in forward mode. Default is False.
|
|
3990
|
+
lognormalize : bool, optional
|
|
3991
|
+
If True, applies logarithmic normalization to the magnitude. Default is True.
|
|
3992
|
+
epsilon : float, optional
|
|
3993
|
+
Small constant added to magnitude before log to avoid log(0). Default is 1e-10.
|
|
3994
|
+
numorders : int, optional
|
|
3995
|
+
Number of orders used in normalization scaling. Default is 6.
|
|
3996
|
+
|
|
3997
|
+
Returns
|
|
3998
|
+
-------
|
|
3999
|
+
tuple[np.ndarray, float] or np.ndarray
|
|
4000
|
+
- If `reverse` is False: Returns a tuple of (scaled_data, scalefac).
|
|
4001
|
+
`scaled_data` is a stacked array of magnitude and phase (or original signal
|
|
4002
|
+
and magnitude in hybrid mode).
|
|
4003
|
+
- If `reverse` is True: Returns the reconstructed time-domain signal as
|
|
4004
|
+
a numpy array.
|
|
4005
|
+
|
|
4006
|
+
Notes
|
|
4007
|
+
-----
|
|
4008
|
+
- In forward mode, the function computes the FFT of `data`, normalizes the
|
|
4009
|
+
magnitude, and scales it to a range suitable for further processing.
|
|
4010
|
+
- In reverse mode, the function reconstructs the time-domain signal using
|
|
4011
|
+
inverse FFT from the provided scaled magnitude and phase components.
|
|
4012
|
+
- The `hybrid` mode is useful for certain types of signal visualization or
|
|
4013
|
+
feature extraction where both time-domain and frequency-domain information
|
|
4014
|
+
are needed.
|
|
4015
|
+
|
|
4016
|
+
Examples
|
|
4017
|
+
--------
|
|
4018
|
+
>>> import numpy as np
|
|
4019
|
+
>>> from scipy import fftpack
|
|
4020
|
+
>>> x = np.random.randn(1024)
|
|
4021
|
+
>>> scaled_data, scalefac = filtscale(x)
|
|
4022
|
+
>>> reconstructed = filtscale(scaled_data, scalefac=scalefac, reverse=True)
|
|
4023
|
+
"""
|
|
4024
|
+
if not reverse:
|
|
4025
|
+
specvals = fftpack.fft(data)
|
|
4026
|
+
if lognormalize:
|
|
4027
|
+
themag = np.log(np.absolute(specvals) + epsilon)
|
|
4028
|
+
scalefac = np.max(themag)
|
|
4029
|
+
themag = (themag - scalefac + numorders) / numorders
|
|
4030
|
+
themag[np.where(themag < 0.0)] = 0.0
|
|
4031
|
+
else:
|
|
4032
|
+
scalefac = np.std(data)
|
|
4033
|
+
themag = np.absolute(specvals) / scalefac
|
|
4034
|
+
thephase = np.angle(specvals)
|
|
4035
|
+
thephase = thephase / (2.0 * np.pi) - 0.5
|
|
4036
|
+
if hybrid:
|
|
4037
|
+
return np.stack((data, themag), axis=1), scalefac
|
|
4038
|
+
else:
|
|
4039
|
+
return np.stack((themag, thephase), axis=1), scalefac
|
|
4040
|
+
else:
|
|
4041
|
+
if hybrid:
|
|
4042
|
+
return data[:, 0]
|
|
4043
|
+
else:
|
|
4044
|
+
thephase = (data[:, 1] + 0.5) * 2.0 * np.pi
|
|
4045
|
+
if lognormalize:
|
|
4046
|
+
themag = np.exp(data[:, 0] * numorders - numorders + scalefac)
|
|
4047
|
+
else:
|
|
4048
|
+
themag = data[:, 0] * scalefac
|
|
4049
|
+
specvals = themag * np.exp(1.0j * thephase)
|
|
4050
|
+
return fftpack.ifft(specvals).real
|
|
4051
|
+
|
|
4052
|
+
|
|
4053
|
+
def tobadpts(name: str) -> str:
|
|
4054
|
+
"""
|
|
4055
|
+
Convert a filename to its corresponding bad points filename.
|
|
4056
|
+
|
|
4057
|
+
This function takes a filename string and replaces the '.txt' extension
|
|
4058
|
+
with '_badpts.txt' to create a new filename for bad points data.
|
|
4059
|
+
|
|
4060
|
+
Parameters
|
|
4061
|
+
----------
|
|
4062
|
+
name : str
|
|
4063
|
+
The input filename string, typically ending with '.txt'.
|
|
4064
|
+
|
|
4065
|
+
Returns
|
|
4066
|
+
-------
|
|
4067
|
+
str
|
|
4068
|
+
The converted filename with '_badpts.txt' extension instead of '.txt'.
|
|
4069
|
+
|
|
4070
|
+
Notes
|
|
4071
|
+
-----
|
|
4072
|
+
This function is useful for creating consistent naming conventions for
|
|
4073
|
+
bad points data files that correspond to original data files.
|
|
4074
|
+
|
|
4075
|
+
Examples
|
|
4076
|
+
--------
|
|
4077
|
+
>>> tobadpts("data.txt")
|
|
4078
|
+
'data_badpts.txt'
|
|
4079
|
+
|
|
4080
|
+
>>> tobadpts("results.txt")
|
|
4081
|
+
'results_badpts.txt'
|
|
4082
|
+
|
|
4083
|
+
>>> tobadpts("output.txt")
|
|
4084
|
+
'output_badpts.txt'
|
|
4085
|
+
"""
|
|
4086
|
+
return name.replace(".txt", "_badpts.txt")
|
|
4087
|
+
|
|
4088
|
+
|
|
4089
|
+
def targettoinput(name: str, targetfrag: str = "xyz", inputfrag: str = "abc") -> str:
|
|
4090
|
+
"""
|
|
4091
|
+
Replace target fragment with input fragment in a string.
|
|
4092
|
+
|
|
4093
|
+
Parameters
|
|
4094
|
+
----------
|
|
4095
|
+
name : str
|
|
4096
|
+
The input string to perform replacement on.
|
|
4097
|
+
targetfrag : str, default='xyz'
|
|
4098
|
+
The fragment to be replaced in the input string.
|
|
4099
|
+
inputfrag : str, default='abc'
|
|
4100
|
+
The fragment to replace the target fragment with.
|
|
4101
|
+
|
|
4102
|
+
Returns
|
|
4103
|
+
-------
|
|
4104
|
+
str
|
|
4105
|
+
The modified string with targetfrag replaced by inputfrag.
|
|
4106
|
+
|
|
4107
|
+
Notes
|
|
4108
|
+
-----
|
|
4109
|
+
This function uses Python's built-in string replace method, which replaces
|
|
4110
|
+
all occurrences of the target fragment with the input fragment.
|
|
4111
|
+
|
|
4112
|
+
Examples
|
|
4113
|
+
--------
|
|
4114
|
+
>>> targettoinput("hello xyz world")
|
|
4115
|
+
'hello abc world'
|
|
4116
|
+
|
|
4117
|
+
>>> targettoinput("test xyz xyz test", "xyz", "123")
|
|
4118
|
+
'test 123 123 test'
|
|
4119
|
+
|
|
4120
|
+
>>> targettoinput("abcdef", "cde", "XXX")
|
|
4121
|
+
'abXXXf'
|
|
4122
|
+
"""
|
|
4123
|
+
LGR.debug(f"replacing {targetfrag} with {inputfrag}")
|
|
4124
|
+
return name.replace(targetfrag, inputfrag)
|
|
4125
|
+
|
|
4126
|
+
|
|
4127
|
+
def getmatchedtcs(
|
|
4128
|
+
searchstring: str,
|
|
4129
|
+
usebadpts: bool = False,
|
|
4130
|
+
targetfrag: str = "xyz",
|
|
4131
|
+
inputfrag: str = "abc",
|
|
4132
|
+
debug: bool = False,
|
|
4133
|
+
) -> tuple[list[str], int]:
|
|
4134
|
+
"""
|
|
4135
|
+
Find and validate matched timecourse files based on a search pattern.
|
|
4136
|
+
|
|
4137
|
+
This function searches for timecourse files matching the given search string,
|
|
4138
|
+
verifies their completeness by checking for associated info files, and
|
|
4139
|
+
determines the length of the timecourses from the first valid file.
|
|
4140
|
+
|
|
4141
|
+
Parameters
|
|
4142
|
+
----------
|
|
4143
|
+
searchstring : str
|
|
4144
|
+
A glob pattern to match target timecourse files.
|
|
4145
|
+
usebadpts : bool, optional
|
|
4146
|
+
Flag indicating whether bad points should be used (default is False).
|
|
4147
|
+
targetfrag : str, optional
|
|
4148
|
+
Target fragment identifier (default is "xyz").
|
|
4149
|
+
inputfrag : str, optional
|
|
4150
|
+
Input fragment identifier (default is "abc").
|
|
4151
|
+
debug : bool, optional
|
|
4152
|
+
If True, prints debug information including matched files (default is False).
|
|
4153
|
+
|
|
4154
|
+
Returns
|
|
4155
|
+
-------
|
|
4156
|
+
tuple[list[str], int]
|
|
4157
|
+
A tuple containing:
|
|
4158
|
+
- List of matched and validated file paths.
|
|
4159
|
+
- Length of the timecourses (number of timepoints).
|
|
4160
|
+
|
|
4161
|
+
Notes
|
|
4162
|
+
-----
|
|
4163
|
+
The function expects timecourse files to have a corresponding info file
|
|
4164
|
+
with the same base name but with "_info" appended. Only files with complete
|
|
4165
|
+
info files are considered valid.
|
|
4166
|
+
|
|
4167
|
+
Examples
|
|
4168
|
+
--------
|
|
4169
|
+
>>> matched_files, tc_length = getmatchedtcs("data/*cardiac*.tsv")
|
|
4170
|
+
>>> print(f"Found {len(matched_files)} files with {tc_length} timepoints")
|
|
4171
|
+
"""
|
|
4172
|
+
# list all of the target files
|
|
4173
|
+
fromfile = sorted(glob.glob(searchstring))
|
|
4174
|
+
if debug:
|
|
4175
|
+
print(f"searchstring: {searchstring} -> {fromfile}")
|
|
4176
|
+
|
|
4177
|
+
# make sure all timecourses exist
|
|
4178
|
+
# we need cardiacfromfmri_25.0Hz as x, normpleth as y, and perhaps badpts
|
|
4179
|
+
matchedfilelist = []
|
|
4180
|
+
for targetname in fromfile:
|
|
4181
|
+
infofile = targetname.replace("_desc-stdrescardfromfmri_timeseries", "_info")
|
|
4182
|
+
if os.path.isfile(infofile):
|
|
4183
|
+
matchedfilelist.append(targetname)
|
|
4184
|
+
print(f"{targetname} is complete")
|
|
4185
|
+
LGR.debug(matchedfilelist[-1])
|
|
4186
|
+
else:
|
|
4187
|
+
print(f"{targetname} is incomplete")
|
|
4188
|
+
print(f"found {len(matchedfilelist)} matched files")
|
|
4189
|
+
|
|
4190
|
+
# find out how long the files are
|
|
4191
|
+
(
|
|
4192
|
+
samplerate,
|
|
4193
|
+
starttime,
|
|
4194
|
+
columns,
|
|
4195
|
+
inputarray,
|
|
4196
|
+
compression,
|
|
4197
|
+
columnsource,
|
|
4198
|
+
) = tide_io.readbidstsv(
|
|
4199
|
+
matchedfilelist[0],
|
|
4200
|
+
colspec="cardiacfromfmri_25.0Hz,normpleth",
|
|
4201
|
+
)
|
|
4202
|
+
print(f"{inputarray.shape=}")
|
|
4203
|
+
tclen = inputarray.shape[1]
|
|
4204
|
+
LGR.info(f"tclen set to {tclen}")
|
|
4205
|
+
return matchedfilelist, tclen
|
|
4206
|
+
|
|
4207
|
+
|
|
4208
|
+
def readindata(
|
|
4209
|
+
matchedfilelist: list[str],
|
|
4210
|
+
tclen: int,
|
|
4211
|
+
targetfrag: str = "xyz",
|
|
4212
|
+
inputfrag: str = "abc",
|
|
4213
|
+
usebadpts: bool = False,
|
|
4214
|
+
startskip: int = 0,
|
|
4215
|
+
endskip: int = 0,
|
|
4216
|
+
corrthresh: float = 0.5,
|
|
4217
|
+
readlim: int | None = None,
|
|
4218
|
+
readskip: int | None = None,
|
|
4219
|
+
debug: bool = False,
|
|
4220
|
+
) -> (
|
|
4221
|
+
tuple[np.ndarray, np.ndarray, list[str]] | tuple[np.ndarray, np.ndarray, list[str], np.ndarray]
|
|
4222
|
+
):
|
|
4223
|
+
"""
|
|
4224
|
+
Read and process time-series data from a list of matched files.
|
|
4225
|
+
|
|
4226
|
+
This function reads cardiac and plethysmographic time-series data from a list of
|
|
4227
|
+
files, performs quality checks, and returns the data in arrays suitable for
|
|
4228
|
+
training or analysis. It supports filtering based on correlation thresholds,
|
|
4229
|
+
NaN values, and signal standard deviations, and allows for optional skipping
|
|
4230
|
+
of data at the start and end of each time series.
|
|
4231
|
+
|
|
4232
|
+
Parameters
|
|
4233
|
+
----------
|
|
4234
|
+
matchedfilelist : list of str
|
|
4235
|
+
List of file paths to be processed. Each file should contain time-series data
|
|
4236
|
+
in a format compatible with `tide_io.readbidstsv`.
|
|
4237
|
+
tclen : int
|
|
4238
|
+
Length of the time series to be read from each file.
|
|
4239
|
+
targetfrag : str, optional
|
|
4240
|
+
Fragment identifier for target files, used in naming conversions. Default is "xyz".
|
|
4241
|
+
inputfrag : str, optional
|
|
4242
|
+
Fragment identifier for input files, used in naming conversions. Default is "abc".
|
|
4243
|
+
usebadpts : bool, optional
|
|
4244
|
+
If True, include a third array with bad point indicators. Default is False.
|
|
4245
|
+
startskip : int, optional
|
|
4246
|
+
Number of samples to skip at the beginning of each time series. Default is 0.
|
|
4247
|
+
endskip : int, optional
|
|
4248
|
+
Number of samples to skip at the end of each time series. Default is 0.
|
|
4249
|
+
corrthresh : float, optional
|
|
4250
|
+
Minimum correlation threshold between raw and plethysmographic signals.
|
|
4251
|
+
Files with lower correlation are excluded. Default is 0.5.
|
|
4252
|
+
readlim : int, optional
|
|
4253
|
+
Maximum number of files to read. If None, all files are read. Default is None.
|
|
4254
|
+
readskip : int, optional
|
|
4255
|
+
Number of files to skip at the beginning of the file list. If None, no files are skipped. Default is None.
|
|
4256
|
+
debug : bool, optional
|
|
4257
|
+
If True, print debug information for each file. Default is False.
|
|
4258
|
+
|
|
4259
|
+
Returns
|
|
4260
|
+
-------
|
|
4261
|
+
tuple of (np.ndarray, np.ndarray, list[str]) or (np.ndarray, np.ndarray, list[str], np.ndarray)
|
|
4262
|
+
- `x1`: Array of shape `(tclen, count)` containing x-time series data.
|
|
4263
|
+
- `y1`: Array of shape `(tclen, count)` containing y-time series data.
|
|
4264
|
+
- `names`: List of file names that passed quality checks.
|
|
4265
|
+
- `bad1`: Optional array of shape `(tclen, count)` with bad point indicators if `usebadpts=True`.
|
|
4266
|
+
|
|
4267
|
+
Notes
|
|
4268
|
+
-----
|
|
4269
|
+
- Files with NaNs, short data, extreme standard deviations, or low correlation are excluded.
|
|
4270
|
+
- The function logs information about excluded files for debugging and quality control.
|
|
4271
|
+
- The `startskip` and `endskip` parameters are applied after filtering and before returning the data.
|
|
4272
|
+
|
|
4273
|
+
Examples
|
|
4274
|
+
--------
|
|
4275
|
+
>>> x, y, names = readindata(filelist, tclen=1000)
|
|
4276
|
+
>>> x, y, names, bad = readindata(filelist, tclen=1000, usebadpts=True)
|
|
4277
|
+
"""
|
|
4278
|
+
LGR.info(
|
|
4279
|
+
"readindata called with usebadpts, startskip, endskip, readlim, readskip, targetfrag, inputfrag = "
|
|
4280
|
+
f"{usebadpts} {startskip} {endskip} {readlim} {readskip} {targetfrag} {inputfrag}"
|
|
4281
|
+
)
|
|
4282
|
+
# allocate target arrays
|
|
4283
|
+
LGR.info("allocating arrays")
|
|
4284
|
+
s = len(matchedfilelist[readskip:])
|
|
4285
|
+
if readlim is not None:
|
|
4286
|
+
if s > readlim:
|
|
4287
|
+
LGR.info(f"trimming read list to {readlim} from {s}")
|
|
4288
|
+
s = readlim
|
|
4289
|
+
x1 = np.zeros((tclen, s))
|
|
4290
|
+
y1 = np.zeros((tclen, s))
|
|
4291
|
+
names = []
|
|
4292
|
+
if usebadpts:
|
|
4293
|
+
bad1 = np.zeros((tclen, s))
|
|
4294
|
+
|
|
4295
|
+
# now read the data in
|
|
4296
|
+
count = 0
|
|
4297
|
+
LGR.info("checking data")
|
|
4298
|
+
lowcorrfiles = []
|
|
4299
|
+
nanfiles = []
|
|
4300
|
+
shortfiles = []
|
|
4301
|
+
strangemagfiles = []
|
|
4302
|
+
for i in range(readskip, readskip + s):
|
|
4303
|
+
lowcorrfound = False
|
|
4304
|
+
nanfound = False
|
|
4305
|
+
LGR.info(f"processing {matchedfilelist[i]}")
|
|
4306
|
+
|
|
4307
|
+
# read the info dict first
|
|
4308
|
+
infodict = tide_io.readdictfromjson(
|
|
4309
|
+
matchedfilelist[i].replace("_desc-stdrescardfromfmri_timeseries", "_info")
|
|
4310
|
+
)
|
|
4311
|
+
if infodict["corrcoeff_raw2pleth"] < corrthresh:
|
|
4312
|
+
lowcorrfound = True
|
|
4313
|
+
lowcorrfiles.append(matchedfilelist[i])
|
|
4314
|
+
(
|
|
4315
|
+
samplerate,
|
|
4316
|
+
starttime,
|
|
4317
|
+
columns,
|
|
4318
|
+
inputarray,
|
|
4319
|
+
compression,
|
|
4320
|
+
columnsource,
|
|
4321
|
+
) = tide_io.readbidstsv(
|
|
4322
|
+
matchedfilelist[i],
|
|
4323
|
+
colspec="cardiacfromfmri_25.0Hz,normpleth",
|
|
4324
|
+
)
|
|
4325
|
+
tempy = inputarray[1, :]
|
|
4326
|
+
tempx = inputarray[0, :]
|
|
4327
|
+
|
|
4328
|
+
if np.any(np.isnan(tempy)):
|
|
4329
|
+
LGR.info(f"NaN found in file {matchedfilelist[i]} - discarding")
|
|
4330
|
+
nanfound = True
|
|
4331
|
+
nanfiles.append(matchedfilelist[i])
|
|
4332
|
+
if np.any(np.isnan(tempx)):
|
|
4333
|
+
nan_fname = targettoinput(
|
|
4334
|
+
matchedfilelist[i], targetfrag=targetfrag, inputfrag=inputfrag
|
|
4335
|
+
)
|
|
4336
|
+
LGR.info(f"NaN found in file {nan_fname} - discarding")
|
|
4337
|
+
nanfound = True
|
|
4338
|
+
nanfiles.append(nan_fname)
|
|
4339
|
+
strangefound = False
|
|
4340
|
+
if not (0.5 < np.std(tempx) < 20.0):
|
|
4341
|
+
strange_fname = matchedfilelist[i]
|
|
4342
|
+
LGR.info(
|
|
4343
|
+
f"file {strange_fname} has an extreme cardiacfromfmri standard deviation - discarding"
|
|
4344
|
+
)
|
|
4345
|
+
strangefound = True
|
|
4346
|
+
strangemagfiles.append(strange_fname)
|
|
4347
|
+
if not (0.5 < np.std(tempy) < 20.0):
|
|
4348
|
+
LGR.info(
|
|
4349
|
+
f"file {matchedfilelist[i]} has an extreme normpleth standard deviation - discarding"
|
|
4350
|
+
)
|
|
4351
|
+
strangefound = True
|
|
4352
|
+
strangemagfiles.append(matchedfilelist[i])
|
|
4353
|
+
shortfound = False
|
|
4354
|
+
ntempx = tempx.shape[0]
|
|
4355
|
+
ntempy = tempy.shape[0]
|
|
4356
|
+
if ntempx < tclen:
|
|
4357
|
+
short_fname = matchedfilelist[i]
|
|
4358
|
+
LGR.info(f"file {short_fname} is short - discarding")
|
|
4359
|
+
shortfound = True
|
|
4360
|
+
shortfiles.append(short_fname)
|
|
4361
|
+
if ntempy < tclen:
|
|
4362
|
+
LGR.info(f"file {matchedfilelist[i]} is short - discarding")
|
|
4363
|
+
shortfound = True
|
|
4364
|
+
shortfiles.append(matchedfilelist[i])
|
|
4365
|
+
if (
|
|
4366
|
+
(ntempx >= tclen)
|
|
4367
|
+
and (ntempy >= tclen)
|
|
4368
|
+
and (not nanfound)
|
|
4369
|
+
and (not shortfound)
|
|
4370
|
+
and (not strangefound)
|
|
4371
|
+
and (not lowcorrfound)
|
|
4372
|
+
):
|
|
4373
|
+
x1[:tclen, count] = tempx[:tclen]
|
|
4374
|
+
y1[:tclen, count] = tempy[:tclen]
|
|
4375
|
+
names.append(matchedfilelist[i])
|
|
4376
|
+
if debug:
|
|
4377
|
+
print(f"{matchedfilelist[i]} included:")
|
|
4378
|
+
if usebadpts:
|
|
4379
|
+
bad1[:tclen, count] = inputarray[2, :]
|
|
4380
|
+
count += 1
|
|
4381
|
+
else:
|
|
4382
|
+
print(f"{matchedfilelist[i]} excluded:")
|
|
4383
|
+
if ntempx < tclen:
|
|
4384
|
+
print("\tx data too short")
|
|
4385
|
+
if ntempy < tclen:
|
|
4386
|
+
print("\ty data too short")
|
|
4387
|
+
print(f"\t{nanfound=}")
|
|
4388
|
+
print(f"\t{shortfound=}")
|
|
4389
|
+
print(f"\t{strangefound=}")
|
|
4390
|
+
print(f"\t{lowcorrfound=}")
|
|
4391
|
+
LGR.info(f"{count} runs pass file length check")
|
|
4392
|
+
if len(lowcorrfiles) > 0:
|
|
4393
|
+
LGR.info("files with low raw/pleth correlations:")
|
|
4394
|
+
for thefile in lowcorrfiles:
|
|
4395
|
+
LGR.info(f"\t{thefile}")
|
|
4396
|
+
if len(nanfiles) > 0:
|
|
4397
|
+
LGR.info("files with NaNs:")
|
|
4398
|
+
for thefile in nanfiles:
|
|
4399
|
+
LGR.info(f"\t{thefile}")
|
|
4400
|
+
if len(shortfiles) > 0:
|
|
4401
|
+
LGR.info("short files:")
|
|
4402
|
+
for thefile in shortfiles:
|
|
4403
|
+
LGR.info(f"\t{thefile}")
|
|
4404
|
+
if len(strangemagfiles) > 0:
|
|
4405
|
+
LGR.info("files with extreme standard deviations:")
|
|
4406
|
+
for thefile in strangemagfiles:
|
|
4407
|
+
LGR.info(f"\t{thefile}")
|
|
4408
|
+
|
|
4409
|
+
print(f"training set contains {count} runs of length {tclen}")
|
|
4410
|
+
if usebadpts:
|
|
4411
|
+
return (
|
|
4412
|
+
x1[startskip:-endskip, :count],
|
|
4413
|
+
y1[startskip:-endskip, :count],
|
|
4414
|
+
names[:count],
|
|
4415
|
+
bad1[startskip:-endskip, :count],
|
|
4416
|
+
)
|
|
4417
|
+
else:
|
|
4418
|
+
return (
|
|
4419
|
+
x1[startskip:-endskip, :count],
|
|
4420
|
+
y1[startskip:-endskip, :count],
|
|
4421
|
+
names[:count],
|
|
4422
|
+
)
|
|
4423
|
+
|
|
4424
|
+
|
|
4425
|
+
def prep(
|
|
4426
|
+
window_size: int,
|
|
4427
|
+
step: int = 1,
|
|
4428
|
+
excludethresh: float = 4.0,
|
|
4429
|
+
usebadpts: bool = False,
|
|
4430
|
+
startskip: int = 200,
|
|
4431
|
+
endskip: int = 200,
|
|
4432
|
+
excludebysubject: bool = True,
|
|
4433
|
+
thesuffix: str = "sliceres",
|
|
4434
|
+
thedatadir: str = "/data/frederic/physioconn/output_2025",
|
|
4435
|
+
inputfrag: str = "abc",
|
|
4436
|
+
targetfrag: str = "xyz",
|
|
4437
|
+
corrthresh: float = 0.5,
|
|
4438
|
+
dofft: bool = False,
|
|
4439
|
+
readlim: int | None = None,
|
|
4440
|
+
readskip: int | None = None,
|
|
4441
|
+
countlim: int | None = None,
|
|
4442
|
+
debug: bool = False,
|
|
4443
|
+
) -> (
|
|
4444
|
+
tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, int, int, int]
|
|
4445
|
+
| tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, int, int, int, np.ndarray, np.ndarray]
|
|
4446
|
+
):
|
|
4447
|
+
"""
|
|
4448
|
+
Prepare time-series data for training and validation by reading, normalizing,
|
|
4449
|
+
windowing, and splitting into batches.
|
|
4450
|
+
|
|
4451
|
+
This function reads physiological time-series data from JSON files, normalizes
|
|
4452
|
+
the data, and organizes it into overlapping windows for model training and
|
|
4453
|
+
validation. It supports filtering by subject or by window, and can optionally
|
|
4454
|
+
apply FFT transformations to the data.
|
|
4455
|
+
|
|
4456
|
+
Parameters
|
|
4457
|
+
----------
|
|
4458
|
+
window_size : int
|
|
4459
|
+
Size of the sliding window used to segment time series data.
|
|
4460
|
+
step : int, optional
|
|
4461
|
+
Step size for sliding window (default is 1).
|
|
4462
|
+
excludethresh : float, optional
|
|
4463
|
+
Threshold for excluding data points based on maximum absolute value
|
|
4464
|
+
(default is 4.0).
|
|
4465
|
+
usebadpts : bool, optional
|
|
4466
|
+
If True, includes bad points in the data processing (default is False).
|
|
4467
|
+
startskip : int, optional
|
|
4468
|
+
Number of time points to skip at the beginning of each time series
|
|
4469
|
+
(default is 200).
|
|
4470
|
+
endskip : int, optional
|
|
4471
|
+
Number of time points to skip at the end of each time series
|
|
4472
|
+
(default is 200).
|
|
4473
|
+
excludebysubject : bool, optional
|
|
4474
|
+
If True, exclude subjects with any region exceeding `excludethresh`;
|
|
4475
|
+
otherwise, exclude windows (default is True).
|
|
4476
|
+
thesuffix : str, optional
|
|
4477
|
+
Suffix used in file search pattern (default is "sliceres").
|
|
4478
|
+
thedatadir : str, optional
|
|
4479
|
+
Directory path where the data files are stored (default is
|
|
4480
|
+
"/data/frederic/physioconn/output_2025").
|
|
4481
|
+
inputfrag : str, optional
|
|
4482
|
+
Fragment identifier for input data (default is "abc").
|
|
4483
|
+
targetfrag : str, optional
|
|
4484
|
+
Fragment identifier for target data (default is "xyz").
|
|
4485
|
+
corrthresh : float, optional
|
|
4486
|
+
Correlation threshold for data filtering (default is 0.5).
|
|
4487
|
+
dofft : bool, optional
|
|
4488
|
+
If True, apply FFT transformation to the data (default is False).
|
|
4489
|
+
readlim : int, optional
|
|
4490
|
+
Limit on number of time points to read (default is None).
|
|
4491
|
+
readskip : int, optional
|
|
4492
|
+
Number of time points to skip when reading data (default is None).
|
|
4493
|
+
countlim : int, optional
|
|
4494
|
+
Maximum number of subjects to include (default is None).
|
|
4495
|
+
debug : bool, optional
|
|
4496
|
+
If True, enable debug logging (default is False).
|
|
4497
|
+
|
|
4498
|
+
Returns
|
|
4499
|
+
-------
|
|
4500
|
+
tuple of (np.ndarray, np.ndarray, np.ndarray, np.ndarray, int, int, int)
|
|
4501
|
+
If `dofft` is False:
|
|
4502
|
+
- train_x : Training input data (shape: [n_windows, window_size, 1])
|
|
4503
|
+
- train_y : Training target data (shape: [n_windows, window_size, 1])
|
|
4504
|
+
- val_x : Validation input data (shape: [n_windows, window_size, 1])
|
|
4505
|
+
- val_y : Validation target data (shape: [n_windows, window_size, 1])
|
|
4506
|
+
- N_subjs : Number of subjects
|
|
4507
|
+
- tclen : Total time points after skipping
|
|
4508
|
+
- batchsize : Number of windows per subject
|
|
4509
|
+
|
|
4510
|
+
tuple of (np.ndarray, np.ndarray, np.ndarray, np.ndarray, int, int, int, np.ndarray, np.ndarray)
|
|
4511
|
+
If `dofft` is True:
|
|
4512
|
+
- train_x : Training input data (shape: [n_windows, window_size, 2])
|
|
4513
|
+
- train_y : Training target data (shape: [n_windows, window_size, 2])
|
|
4514
|
+
- val_x : Validation input data (shape: [n_windows, window_size, 2])
|
|
4515
|
+
- val_y : Validation target data (shape: [n_windows, window_size, 2])
|
|
4516
|
+
- N_subjs : Number of subjects
|
|
4517
|
+
- tclen : Total time points after skipping
|
|
4518
|
+
- batchsize : Number of windows per subject
|
|
4519
|
+
- Xscale_fourier : Fourier scaling for input data
|
|
4520
|
+
- Yscale_fourier : Fourier scaling for target data
|
|
4521
|
+
|
|
4522
|
+
Notes
|
|
4523
|
+
-----
|
|
4524
|
+
- Data normalization is performed using median absolute deviation (MAD).
|
|
4525
|
+
- The function supports both window-based and subject-based exclusion strategies.
|
|
4526
|
+
- If `usebadpts` is True, bad points are included in the output arrays.
|
|
4527
|
+
- FFT transformations are applied using a helper function `filtscale`.
|
|
4528
|
+
|
|
4529
|
+
Examples
|
|
4530
|
+
--------
|
|
4531
|
+
>>> train_x, train_y, val_x, val_y, N_subjs, tclen, batchsize = prep(
|
|
4532
|
+
... window_size=100,
|
|
4533
|
+
... step=10,
|
|
4534
|
+
... excludethresh=3.0,
|
|
4535
|
+
... excludebysubject=True,
|
|
4536
|
+
... dofft=False
|
|
4537
|
+
... )
|
|
4538
|
+
"""
|
|
4539
|
+
searchstring = os.path.join(thedatadir, "*", "*_desc-stdrescardfromfmri_timeseries.json")
|
|
4540
|
+
|
|
4541
|
+
# find matched files
|
|
4542
|
+
matchedfilelist, tclen = getmatchedtcs(
|
|
4543
|
+
searchstring,
|
|
4544
|
+
usebadpts=usebadpts,
|
|
4545
|
+
targetfrag=targetfrag,
|
|
4546
|
+
inputfrag=inputfrag,
|
|
4547
|
+
debug=debug,
|
|
4548
|
+
)
|
|
4549
|
+
# print("matchedfilelist", matchedfilelist)
|
|
4550
|
+
print("tclen", tclen)
|
|
4551
|
+
|
|
4552
|
+
# read in the data from the matched files
|
|
4553
|
+
print("about to read in data")
|
|
4554
|
+
if usebadpts:
|
|
4555
|
+
x, y, names, bad = readindata(
|
|
4556
|
+
matchedfilelist,
|
|
4557
|
+
tclen,
|
|
4558
|
+
corrthresh=corrthresh,
|
|
4559
|
+
targetfrag=targetfrag,
|
|
4560
|
+
inputfrag=inputfrag,
|
|
4561
|
+
usebadpts=True,
|
|
4562
|
+
startskip=startskip,
|
|
4563
|
+
endskip=endskip,
|
|
4564
|
+
readlim=readlim,
|
|
4565
|
+
readskip=readskip,
|
|
4566
|
+
)
|
|
4567
|
+
else:
|
|
4568
|
+
x, y, names = readindata(
|
|
4569
|
+
matchedfilelist,
|
|
4570
|
+
tclen,
|
|
4571
|
+
corrthresh=corrthresh,
|
|
4572
|
+
targetfrag=targetfrag,
|
|
4573
|
+
inputfrag=inputfrag,
|
|
4574
|
+
startskip=startskip,
|
|
4575
|
+
endskip=endskip,
|
|
4576
|
+
readlim=readlim,
|
|
4577
|
+
readskip=readskip,
|
|
4578
|
+
)
|
|
4579
|
+
print("finished reading in data")
|
|
4580
|
+
LGR.info(f"xshape, yshape: {x.shape} {y.shape}")
|
|
4581
|
+
|
|
4582
|
+
# normalize input and output data
|
|
4583
|
+
LGR.info("normalizing data")
|
|
4584
|
+
LGR.info(f"count: {x.shape[1]}")
|
|
4585
|
+
if LGR.getEffectiveLevel() <= logging.DEBUG:
|
|
4586
|
+
# Only take these steps if the logger is set to DEBUG.
|
|
4587
|
+
for thesubj in range(x.shape[1]):
|
|
4588
|
+
LGR.debug(
|
|
4589
|
+
f"prenorm sub {thesubj} min, max, mean, std, MAD x, y: "
|
|
4590
|
+
f"{thesubj} "
|
|
4591
|
+
f"{np.min(x[:, thesubj])} {np.max(x[:, thesubj])} {np.mean(x[:, thesubj])} "
|
|
4592
|
+
f"{np.std(x[:, thesubj])} {mad(x[:, thesubj])} {np.min(y[:, thesubj])} "
|
|
4593
|
+
f"{np.max(y[:, thesubj])} {np.mean(y[:, thesubj])} {np.std(x[:, thesubj])} "
|
|
4594
|
+
f"{mad(y[:, thesubj])}"
|
|
4595
|
+
)
|
|
4596
|
+
|
|
4597
|
+
y -= np.mean(y, axis=0)
|
|
4598
|
+
themad = mad(y, axis=0)
|
|
4599
|
+
for thesubj in range(themad.shape[0]):
|
|
4600
|
+
if themad[thesubj] > 0.0:
|
|
4601
|
+
y[:, thesubj] /= themad[thesubj]
|
|
4602
|
+
|
|
4603
|
+
x -= np.mean(x, axis=0)
|
|
4604
|
+
themad = mad(x, axis=0)
|
|
4605
|
+
for thesubj in range(themad.shape[0]):
|
|
4606
|
+
if themad[thesubj] > 0.0:
|
|
4607
|
+
x[:, thesubj] /= themad[thesubj]
|
|
4608
|
+
|
|
4609
|
+
if LGR.getEffectiveLevel() <= logging.DEBUG:
|
|
4610
|
+
# Only take these steps if the logger is set to DEBUG.
|
|
4611
|
+
for thesubj in range(x.shape[1]):
|
|
4612
|
+
LGR.debug(
|
|
4613
|
+
f"postnorm sub {thesubj} min, max, mean, std, MAD x, y: "
|
|
4614
|
+
f"{thesubj} "
|
|
4615
|
+
f"{np.min(x[:, thesubj])} {np.max(x[:, thesubj])} {np.mean(x[:, thesubj])} "
|
|
4616
|
+
f"{np.std(x[:, thesubj])} {mad(x[:, thesubj])} {np.min(y[:, thesubj])} "
|
|
4617
|
+
f"{np.max(y[:, thesubj])} {np.mean(y[:, thesubj])} {np.std(x[:, thesubj])} "
|
|
4618
|
+
f"{mad(y[:, thesubj])}"
|
|
4619
|
+
)
|
|
4620
|
+
|
|
4621
|
+
# now decide what to keep and what to exclude
|
|
4622
|
+
thefabs = np.fabs(x)
|
|
4623
|
+
if not excludebysubject:
|
|
4624
|
+
N_pts = x.shape[0]
|
|
4625
|
+
N_subjs = x.shape[1]
|
|
4626
|
+
windowspersubject = np.int64((N_pts - window_size - 1) // step)
|
|
4627
|
+
LGR.info(
|
|
4628
|
+
f"{N_subjs} subjects with {N_pts} points will be evaluated with "
|
|
4629
|
+
f"{windowspersubject} windows per subject with step {step}"
|
|
4630
|
+
)
|
|
4631
|
+
usewindow = np.zeros(N_subjs * windowspersubject, dtype=np.int64)
|
|
4632
|
+
subjectstarts = np.zeros(N_subjs, dtype=np.int64)
|
|
4633
|
+
# check each window
|
|
4634
|
+
numgoodwindows = 0
|
|
4635
|
+
LGR.info("checking windows")
|
|
4636
|
+
subjectnames = []
|
|
4637
|
+
for subj in range(N_subjs):
|
|
4638
|
+
subjectstarts[subj] = numgoodwindows
|
|
4639
|
+
subjectnames.append(names[subj])
|
|
4640
|
+
LGR.info(f"{names[subj]} starts at {numgoodwindows}")
|
|
4641
|
+
for windownumber in range(windowspersubject):
|
|
4642
|
+
if (
|
|
4643
|
+
np.max(
|
|
4644
|
+
thefabs[
|
|
4645
|
+
step * windownumber : (step * windownumber + window_size),
|
|
4646
|
+
subj,
|
|
4647
|
+
]
|
|
4648
|
+
)
|
|
4649
|
+
<= excludethresh
|
|
4650
|
+
):
|
|
4651
|
+
usewindow[subj * windowspersubject + windownumber] = 1
|
|
4652
|
+
numgoodwindows += 1
|
|
4653
|
+
LGR.info(
|
|
4654
|
+
f"found {numgoodwindows} out of a potential {N_subjs * windowspersubject} "
|
|
4655
|
+
f"({100.0 * numgoodwindows / (N_subjs * windowspersubject)}%)"
|
|
4656
|
+
)
|
|
4657
|
+
|
|
4658
|
+
for subj in range(N_subjs):
|
|
4659
|
+
LGR.info(f"{names[subj]} starts at {subjectstarts[subj]}")
|
|
4660
|
+
|
|
4661
|
+
LGR.info("copying data into windows")
|
|
4662
|
+
Xb = np.zeros((numgoodwindows, window_size, 1))
|
|
4663
|
+
Yb = np.zeros((numgoodwindows, window_size, 1))
|
|
4664
|
+
if usebadpts:
|
|
4665
|
+
Xb_withbad = np.zeros((numgoodwindows, window_size, 1))
|
|
4666
|
+
LGR.info(f"dimensions of Xb: {Xb.shape}")
|
|
4667
|
+
thiswindow = 0
|
|
4668
|
+
for subj in range(N_subjs):
|
|
4669
|
+
for windownumber in range(windowspersubject):
|
|
4670
|
+
if usewindow[subj * windowspersubject + windownumber] == 1:
|
|
4671
|
+
Xb[thiswindow, :, 0] = x[
|
|
4672
|
+
step * windownumber : (step * windownumber + window_size), subj
|
|
4673
|
+
]
|
|
4674
|
+
Yb[thiswindow, :, 0] = y[
|
|
4675
|
+
step * windownumber : (step * windownumber + window_size), subj
|
|
4676
|
+
]
|
|
4677
|
+
if usebadpts:
|
|
4678
|
+
Xb_withbad[thiswindow, :, 0] = bad[
|
|
4679
|
+
step * windownumber : (step * windownumber + window_size),
|
|
4680
|
+
subj,
|
|
4681
|
+
]
|
|
4682
|
+
thiswindow += 1
|
|
4683
|
+
|
|
4684
|
+
else:
|
|
4685
|
+
# now check for subjects that have regions that exceed the target
|
|
4686
|
+
themax = np.max(thefabs, axis=0)
|
|
4687
|
+
|
|
4688
|
+
cleansubjs = np.where(themax < excludethresh)[0]
|
|
4689
|
+
|
|
4690
|
+
totalcount = x.shape[1] + 0
|
|
4691
|
+
cleancount = len(cleansubjs)
|
|
4692
|
+
if countlim is not None:
|
|
4693
|
+
if cleancount > countlim:
|
|
4694
|
+
LGR.info(f"reducing count to {countlim} from {cleancount}")
|
|
4695
|
+
cleansubjs = cleansubjs[:countlim]
|
|
4696
|
+
|
|
4697
|
+
x = x[:, cleansubjs]
|
|
4698
|
+
y = y[:, cleansubjs]
|
|
4699
|
+
cleannames = []
|
|
4700
|
+
for theindex in cleansubjs:
|
|
4701
|
+
cleannames.append(names[theindex])
|
|
4702
|
+
if usebadpts:
|
|
4703
|
+
bad = bad[:, cleansubjs]
|
|
4704
|
+
subjectnames = cleannames
|
|
4705
|
+
|
|
4706
|
+
LGR.info(f"after filtering, shape of x is {x.shape}")
|
|
4707
|
+
|
|
4708
|
+
N_pts = y.shape[0]
|
|
4709
|
+
N_subjs = y.shape[1]
|
|
4710
|
+
|
|
4711
|
+
X = np.zeros((1, N_pts, N_subjs))
|
|
4712
|
+
Y = np.zeros((1, N_pts, N_subjs))
|
|
4713
|
+
if usebadpts:
|
|
4714
|
+
BAD = np.zeros((1, N_pts, N_subjs))
|
|
4715
|
+
|
|
4716
|
+
X[0, :, :] = x
|
|
4717
|
+
Y[0, :, :] = y
|
|
4718
|
+
if usebadpts:
|
|
4719
|
+
BAD[0, :, :] = bad
|
|
4720
|
+
|
|
4721
|
+
windowspersubject = int((N_pts - window_size - 1) // step)
|
|
4722
|
+
LGR.info(
|
|
4723
|
+
f"found {windowspersubject * cleancount} out of a potential "
|
|
4724
|
+
f"{windowspersubject * totalcount} "
|
|
4725
|
+
f"({100.0 * cleancount / totalcount}%)"
|
|
4726
|
+
)
|
|
4727
|
+
LGR.info(f"{windowspersubject} {cleancount} {totalcount}")
|
|
4728
|
+
|
|
4729
|
+
Xb = np.zeros((N_subjs * windowspersubject, window_size, 1))
|
|
4730
|
+
LGR.info(f"dimensions of Xb: {Xb.shape}")
|
|
4731
|
+
for j in range(N_subjs):
|
|
4732
|
+
LGR.info(
|
|
4733
|
+
f"sub {j} ({cleannames[j]}) min, max X, Y: "
|
|
4734
|
+
f"{j} {np.min(X[0, :, j])} {np.max(X[0, :, j])} {np.min(Y[0, :, j])} "
|
|
4735
|
+
f"{np.max(Y[0, :, j])}"
|
|
4736
|
+
)
|
|
4737
|
+
for i in range(windowspersubject):
|
|
4738
|
+
Xb[j * windowspersubject + i, :, 0] = X[0, step * i : (step * i + window_size), j]
|
|
4739
|
+
|
|
4740
|
+
Yb = np.zeros((N_subjs * windowspersubject, window_size, 1))
|
|
4741
|
+
LGR.info(f"dimensions of Yb: {Yb.shape}")
|
|
4742
|
+
for j in range(N_subjs):
|
|
4743
|
+
for i in range(windowspersubject):
|
|
4744
|
+
Yb[j * windowspersubject + i, :, 0] = Y[0, step * i : (step * i + window_size), j]
|
|
4745
|
+
|
|
4746
|
+
if usebadpts:
|
|
4747
|
+
Xb_withbad = np.zeros((N_subjs * windowspersubject, window_size, 2))
|
|
4748
|
+
LGR.info(f"dimensions of Xb_withbad: {Xb_withbad.shape}")
|
|
4749
|
+
for j in range(N_subjs):
|
|
4750
|
+
LGR.info(f"packing data for subject {j}")
|
|
4751
|
+
for i in range(windowspersubject):
|
|
4752
|
+
Xb_withbad[j * windowspersubject + i, :, 0] = X[
|
|
4753
|
+
0, step * i : (step * i + window_size), j
|
|
4754
|
+
]
|
|
4755
|
+
Xb_withbad[j * windowspersubject + i, :, 1] = BAD[
|
|
4756
|
+
0, step * i : (step * i + window_size), j
|
|
4757
|
+
]
|
|
4758
|
+
Xb = Xb_withbad
|
|
4759
|
+
|
|
4760
|
+
subjectstarts = [i * windowspersubject for i in range(N_subjs)]
|
|
4761
|
+
for subj in range(N_subjs):
|
|
4762
|
+
LGR.info(f"{names[subj]} starts at {subjectstarts[subj]}")
|
|
4763
|
+
|
|
4764
|
+
LGR.info(f"Xb.shape: {Xb.shape}")
|
|
4765
|
+
LGR.info(f"Yb.shape: {Yb.shape}")
|
|
4766
|
+
|
|
4767
|
+
if dofft:
|
|
4768
|
+
Xb_fourier = np.zeros((N_subjs * windowspersubject, window_size, 2))
|
|
4769
|
+
LGR.info(f"dimensions of Xb_fourier: {Xb_fourier.shape}")
|
|
4770
|
+
Xscale_fourier = np.zeros((N_subjs, windowspersubject))
|
|
4771
|
+
LGR.info(f"dimensions of Xscale_fourier: {Xscale_fourier.shape}")
|
|
4772
|
+
Yb_fourier = np.zeros((N_subjs * windowspersubject, window_size, 2))
|
|
4773
|
+
LGR.info(f"dimensions of Yb_fourier: {Yb_fourier.shape}")
|
|
4774
|
+
Yscale_fourier = np.zeros((N_subjs, windowspersubject))
|
|
4775
|
+
LGR.info(f"dimensions of Yscale_fourier: {Yscale_fourier.shape}")
|
|
4776
|
+
for j in range(N_subjs):
|
|
4777
|
+
LGR.info(f"transforming subject {j}")
|
|
4778
|
+
for i in range((N_pts - window_size - 1)):
|
|
4779
|
+
(
|
|
4780
|
+
Xb_fourier[j * windowspersubject + i, :, :],
|
|
4781
|
+
Xscale_fourier[j, i],
|
|
4782
|
+
) = filtscale(X[0, step * i : (step * i + window_size), j])
|
|
4783
|
+
(
|
|
4784
|
+
Yb_fourier[j * windowspersubject + i, :, :],
|
|
4785
|
+
Yscale_fourier[j, i],
|
|
4786
|
+
) = filtscale(Y[0, step * i : (step * i + window_size), j])
|
|
4787
|
+
|
|
4788
|
+
limit = np.int64(0.8 * Xb.shape[0])
|
|
4789
|
+
LGR.info(f"limit: {limit} out of {len(subjectstarts)}")
|
|
4790
|
+
# find nearest subject start
|
|
4791
|
+
firstvalsubject = np.abs(subjectstarts - limit).argmin()
|
|
4792
|
+
LGR.info(f"firstvalsubject: {firstvalsubject}")
|
|
4793
|
+
perm_train = np.random.permutation(np.int64(np.arange(subjectstarts[firstvalsubject])))
|
|
4794
|
+
perm_val = np.random.permutation(
|
|
4795
|
+
np.int64(np.arange(subjectstarts[firstvalsubject], Xb.shape[0]))
|
|
4796
|
+
)
|
|
4797
|
+
|
|
4798
|
+
LGR.info("training subjects:")
|
|
4799
|
+
for i in range(0, firstvalsubject):
|
|
4800
|
+
LGR.info(f"\t{i} {subjectnames[i]}")
|
|
4801
|
+
LGR.info("validation subjects:")
|
|
4802
|
+
for i in range(firstvalsubject, len(subjectstarts)):
|
|
4803
|
+
LGR.info(f"\t{i} {subjectnames[i]}")
|
|
4804
|
+
|
|
4805
|
+
perm = range(Xb.shape[0])
|
|
4806
|
+
|
|
4807
|
+
batchsize = windowspersubject
|
|
4808
|
+
|
|
4809
|
+
if dofft:
|
|
4810
|
+
train_x = Xb_fourier[perm[:limit], :, :]
|
|
4811
|
+
train_y = Yb_fourier[perm[:limit], :, :]
|
|
4812
|
+
|
|
4813
|
+
val_x = Xb_fourier[perm[limit:], :, :]
|
|
4814
|
+
val_y = Yb_fourier[perm[limit:], :, :]
|
|
4815
|
+
LGR.info(f"train, val dims: {train_x.shape} {train_y.shape} {val_x.shape} {val_y.shape}")
|
|
4816
|
+
return (
|
|
4817
|
+
train_x,
|
|
4818
|
+
train_y,
|
|
4819
|
+
val_x,
|
|
4820
|
+
val_y,
|
|
4821
|
+
N_subjs,
|
|
4822
|
+
tclen - startskip - endskip,
|
|
4823
|
+
batchsize,
|
|
4824
|
+
Xscale_fourier,
|
|
4825
|
+
Yscale_fourier,
|
|
4826
|
+
)
|
|
4827
|
+
else:
|
|
4828
|
+
train_x = Xb[perm_train, :, :]
|
|
4829
|
+
train_y = Yb[perm_train, :, :]
|
|
4830
|
+
|
|
4831
|
+
val_x = Xb[perm_val, :, :]
|
|
4832
|
+
val_y = Yb[perm_val, :, :]
|
|
4833
|
+
|
|
4834
|
+
LGR.info(f"train, val dims: {train_x.shape} {train_y.shape} {val_x.shape} {val_y.shape}")
|
|
4835
|
+
return (
|
|
4836
|
+
train_x,
|
|
4837
|
+
train_y,
|
|
4838
|
+
val_x,
|
|
4839
|
+
val_y,
|
|
4840
|
+
N_subjs,
|
|
4841
|
+
tclen - startskip - endskip,
|
|
4842
|
+
batchsize,
|
|
4843
|
+
)
|