nnodely 0.14.0__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.
- mplplots/__init__.py +0 -0
- mplplots/plots.py +131 -0
- nnodely/__init__.py +42 -0
- nnodely/activation.py +85 -0
- nnodely/arithmetic.py +203 -0
- nnodely/earlystopping.py +81 -0
- nnodely/exporter/__init__.py +3 -0
- nnodely/exporter/export.py +275 -0
- nnodely/exporter/exporter.py +45 -0
- nnodely/exporter/reporter.py +48 -0
- nnodely/exporter/standardexporter.py +108 -0
- nnodely/fir.py +150 -0
- nnodely/fuzzify.py +221 -0
- nnodely/initializer.py +31 -0
- nnodely/input.py +131 -0
- nnodely/linear.py +130 -0
- nnodely/localmodel.py +82 -0
- nnodely/logger.py +94 -0
- nnodely/loss.py +30 -0
- nnodely/model.py +263 -0
- nnodely/modeldef.py +205 -0
- nnodely/nnodely.py +1295 -0
- nnodely/optimizer.py +91 -0
- nnodely/output.py +23 -0
- nnodely/parameter.py +103 -0
- nnodely/parametricfunction.py +329 -0
- nnodely/part.py +201 -0
- nnodely/relation.py +149 -0
- nnodely/trigonometric.py +67 -0
- nnodely/utils.py +101 -0
- nnodely/visualizer/__init__.py +4 -0
- nnodely/visualizer/dynamicmpl/functionplot.py +34 -0
- nnodely/visualizer/dynamicmpl/fuzzyplot.py +31 -0
- nnodely/visualizer/dynamicmpl/resultsplot.py +28 -0
- nnodely/visualizer/dynamicmpl/trainingplot.py +46 -0
- nnodely/visualizer/mplnotebookvisualizer.py +66 -0
- nnodely/visualizer/mplvisualizer.py +215 -0
- nnodely/visualizer/textvisualizer.py +320 -0
- nnodely/visualizer/visualizer.py +84 -0
- nnodely-0.14.0.dist-info/LICENSE +21 -0
- nnodely-0.14.0.dist-info/METADATA +401 -0
- nnodely-0.14.0.dist-info/RECORD +44 -0
- nnodely-0.14.0.dist-info/WHEEL +5 -0
- nnodely-0.14.0.dist-info/top_level.txt +2 -0
nnodely/nnodely.py
ADDED
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
# Extern packages
|
|
2
|
+
import random, torch, copy, os
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
# nnodely packages
|
|
7
|
+
from nnodely.visualizer import TextVisualizer, Visualizer
|
|
8
|
+
from nnodely.loss import CustomLoss
|
|
9
|
+
from nnodely.model import Model
|
|
10
|
+
from nnodely.optimizer import Optimizer, SGD, Adam
|
|
11
|
+
from nnodely.exporter import Exporter, StandardExporter
|
|
12
|
+
from nnodely.modeldef import ModelDef
|
|
13
|
+
|
|
14
|
+
from nnodely.utils import check, argmax_max, argmin_min, tensor_to_list
|
|
15
|
+
|
|
16
|
+
from nnodely.logger import logging, nnLogger
|
|
17
|
+
log = nnLogger(__name__, logging.INFO)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Modely:
|
|
21
|
+
"""
|
|
22
|
+
Create the main object, the nnodely object, that will be used to create the network, train and export it.
|
|
23
|
+
|
|
24
|
+
:param seed: It is the seed used for the random number generator
|
|
25
|
+
:type seed: int or None
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> model = Modely()
|
|
29
|
+
"""
|
|
30
|
+
def __init__(self,
|
|
31
|
+
visualizer:str|Visualizer|None = 'Standard',
|
|
32
|
+
exporter:str|Exporter|None = 'Standard',
|
|
33
|
+
seed:int|None = None,
|
|
34
|
+
workspace:str|None = None,
|
|
35
|
+
log_internal:bool = False,
|
|
36
|
+
save_history:bool = False):
|
|
37
|
+
|
|
38
|
+
# Visualizer
|
|
39
|
+
if visualizer == 'Standard':
|
|
40
|
+
self.visualizer = TextVisualizer(1)
|
|
41
|
+
elif visualizer != None:
|
|
42
|
+
self.visualizer = visualizer
|
|
43
|
+
else:
|
|
44
|
+
self.visualizer = Visualizer()
|
|
45
|
+
self.visualizer.set_n4m(self)
|
|
46
|
+
|
|
47
|
+
# Exporter
|
|
48
|
+
if exporter == 'Standard':
|
|
49
|
+
self.exporter = StandardExporter(workspace, self.visualizer, save_history)
|
|
50
|
+
elif exporter != None:
|
|
51
|
+
self.exporter = exporter
|
|
52
|
+
else:
|
|
53
|
+
self.exporter = Exporter()
|
|
54
|
+
|
|
55
|
+
## Set the random seed for reproducibility
|
|
56
|
+
if seed is not None:
|
|
57
|
+
self.resetSeed(seed)
|
|
58
|
+
|
|
59
|
+
# Save internal
|
|
60
|
+
self.log_internal = log_internal
|
|
61
|
+
if self.log_internal == True:
|
|
62
|
+
self.internals = {}
|
|
63
|
+
|
|
64
|
+
# Models definition
|
|
65
|
+
self.model_def = ModelDef()
|
|
66
|
+
self.input_n_samples = {}
|
|
67
|
+
self.max_n_samples = 0
|
|
68
|
+
self.neuralized = False
|
|
69
|
+
self.traced = False
|
|
70
|
+
self.model = None
|
|
71
|
+
|
|
72
|
+
# Dataaset Parameters
|
|
73
|
+
self.data_loaded = False
|
|
74
|
+
self.file_count = 0
|
|
75
|
+
self.num_of_samples = {}
|
|
76
|
+
self.data = {}
|
|
77
|
+
self.n_datasets = 0
|
|
78
|
+
self.datasets_loaded = set()
|
|
79
|
+
|
|
80
|
+
# Training Parameters
|
|
81
|
+
self.standard_train_parameters = {
|
|
82
|
+
'models' : None,
|
|
83
|
+
'train_dataset' : None, 'validation_dataset' : None, 'test_dataset' : None, 'splits' : [70, 20, 10],
|
|
84
|
+
'closed_loop' : {}, 'connect' : {}, 'step' : 1, 'prediction_samples' : 0,
|
|
85
|
+
'shuffle_data' : True,
|
|
86
|
+
'early_stopping' : None, 'early_stopping_params' : {},
|
|
87
|
+
'select_model' : 'last', 'select_model_params' : {},
|
|
88
|
+
'minimize_gain' : {},
|
|
89
|
+
'num_of_epochs': 100,
|
|
90
|
+
'train_batch_size' : 128, 'val_batch_size' : None, 'test_batch_size' : None,
|
|
91
|
+
'optimizer' : 'Adam',
|
|
92
|
+
'lr' : 0.001, 'lr_param' : {},
|
|
93
|
+
'optimizer_params' : [], 'add_optimizer_params' : [],
|
|
94
|
+
'optimizer_defaults' : {}, 'add_optimizer_defaults' : {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Optimizer
|
|
98
|
+
self.optimizer = None
|
|
99
|
+
|
|
100
|
+
# Training Losses
|
|
101
|
+
self.loss_functions = {}
|
|
102
|
+
|
|
103
|
+
# Validation Parameters
|
|
104
|
+
self.training = {}
|
|
105
|
+
self.performance = {}
|
|
106
|
+
self.prediction = {}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resetSeed(self, seed):
|
|
110
|
+
"""
|
|
111
|
+
Resets the random seed for reproducibility.
|
|
112
|
+
|
|
113
|
+
This method sets the seed for various random number generators used in the project to ensure reproducibility of results.
|
|
114
|
+
|
|
115
|
+
:param seed: The seed value to be used for the random number generators.
|
|
116
|
+
:type seed: int
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
>>> model = nnodely()
|
|
120
|
+
>>> model.resetSeed(42)
|
|
121
|
+
"""
|
|
122
|
+
torch.manual_seed(seed) ## set the pytorch seed
|
|
123
|
+
torch.cuda.manual_seed_all(seed)
|
|
124
|
+
random.seed(seed) ## set the random module seed
|
|
125
|
+
np.random.seed(seed) ## set the numpy seed
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def __call__(self, inputs = {}, sampled = False, closed_loop = {}, connect = {}, prediction_samples = 'auto', num_of_samples = 'auto'):#, align_input = False):
|
|
129
|
+
## Copy dict for avoid python bug
|
|
130
|
+
inputs = copy.deepcopy(inputs)
|
|
131
|
+
closed_loop = copy.deepcopy(closed_loop)
|
|
132
|
+
connect = copy.deepcopy(connect)
|
|
133
|
+
|
|
134
|
+
## Check neuralize
|
|
135
|
+
check(self.neuralized, RuntimeError, "The network is not neuralized.")
|
|
136
|
+
|
|
137
|
+
## Bild the list of inputs
|
|
138
|
+
model_inputs = list(self.model_def['Inputs'].keys())
|
|
139
|
+
model_states = list(self.model_def['States'].keys())
|
|
140
|
+
provided_inputs = list(inputs.keys())
|
|
141
|
+
missing_inputs = list(set(model_inputs) - set(provided_inputs)) #- set(connect.keys()))
|
|
142
|
+
extra_inputs = list(set(provided_inputs) - set(model_inputs) - set(model_states))
|
|
143
|
+
if not set(provided_inputs).issubset(set(model_inputs) | set(model_states)):
|
|
144
|
+
## Ignoring extra inputs
|
|
145
|
+
log.warning(f'The complete model inputs are {model_inputs}, the provided input are {provided_inputs}. Ignoring {extra_inputs}...')
|
|
146
|
+
for key in extra_inputs:
|
|
147
|
+
del inputs[key]
|
|
148
|
+
provided_inputs = list(inputs.keys())
|
|
149
|
+
non_recurrent_inputs = list(set(provided_inputs) - set(closed_loop.keys()) - set(connect.keys()) - set(model_states))
|
|
150
|
+
recurrent_inputs = set(closed_loop.keys())|set(connect.keys())|set(model_states)
|
|
151
|
+
|
|
152
|
+
## Define input windows and check closed loop and connect
|
|
153
|
+
input_windows = {}
|
|
154
|
+
for in_var, out_var in (closed_loop.items() | connect.items()):
|
|
155
|
+
check(in_var in self.model_def['Inputs'], ValueError, f'the tag {in_var} is not an input variable.')
|
|
156
|
+
check(out_var in self.model_def['Outputs'], ValueError, f'the tag {out_var} is not an output of the network')
|
|
157
|
+
if in_var in inputs.keys():
|
|
158
|
+
input_windows[in_var] = len(inputs[in_var]) if sampled else len(inputs[in_var]) - self.input_n_samples[in_var] + 1
|
|
159
|
+
else:
|
|
160
|
+
input_windows[in_var] = 1
|
|
161
|
+
for key in model_states:
|
|
162
|
+
if key in inputs.keys():
|
|
163
|
+
input_windows[key] = len(inputs[key]) if sampled else len(inputs[key]) - self.input_n_samples[key] + 1
|
|
164
|
+
else:
|
|
165
|
+
input_windows[key] = 1
|
|
166
|
+
|
|
167
|
+
## Determine the Maximal number of samples that can be created
|
|
168
|
+
if non_recurrent_inputs:
|
|
169
|
+
if sampled:
|
|
170
|
+
min_dim_ind, min_dim = argmin_min([len(inputs[key]) for key in non_recurrent_inputs])
|
|
171
|
+
max_dim_ind, max_dim = argmax_max([len(inputs[key]) for key in non_recurrent_inputs])
|
|
172
|
+
else:
|
|
173
|
+
min_dim_ind, min_dim = argmin_min([len(inputs[key])-self.input_n_samples[key]+1 for key in non_recurrent_inputs])
|
|
174
|
+
max_dim_ind, max_dim = argmax_max([len(inputs[key])-self.input_n_samples[key]+1 for key in non_recurrent_inputs])
|
|
175
|
+
min_din_key = non_recurrent_inputs[min_dim_ind]
|
|
176
|
+
max_din_key = non_recurrent_inputs[max_dim_ind]
|
|
177
|
+
else:
|
|
178
|
+
if recurrent_inputs:
|
|
179
|
+
#ps = 0 if prediction_samples=='auto' or prediction_samples is None else prediction_samples
|
|
180
|
+
if provided_inputs:
|
|
181
|
+
min_dim_ind, min_dim = argmin_min([input_windows[key] for key in provided_inputs])
|
|
182
|
+
max_dim_ind, max_dim = argmax_max([input_windows[key] for key in provided_inputs])
|
|
183
|
+
min_din_key = provided_inputs[min_dim_ind]
|
|
184
|
+
max_din_key = provided_inputs[max_dim_ind]
|
|
185
|
+
else:
|
|
186
|
+
min_dim = max_dim = 1
|
|
187
|
+
else:
|
|
188
|
+
min_dim = max_dim = 0
|
|
189
|
+
|
|
190
|
+
## Define the number of samples
|
|
191
|
+
if num_of_samples != 'auto':
|
|
192
|
+
window_dim = min_dim = max_dim = num_of_samples
|
|
193
|
+
else:
|
|
194
|
+
# Use the minimum number of input samples if the net is not autonoma otherwise the minimum number of state samples
|
|
195
|
+
window_dim = min_dim
|
|
196
|
+
check(window_dim > 0, StopIteration, f'Missing at least {abs(min_dim)+1} samples in the input window')
|
|
197
|
+
|
|
198
|
+
## Autofill the missing inputs
|
|
199
|
+
if missing_inputs:
|
|
200
|
+
log.warning(f'Inputs not provided: {missing_inputs}. Autofilling with zeros..')
|
|
201
|
+
for key in missing_inputs:
|
|
202
|
+
inputs[key] = np.zeros(
|
|
203
|
+
shape=(self.input_n_samples[key] + window_dim - 1, self.model_def['Inputs'][key]['dim']),
|
|
204
|
+
dtype=np.float32).tolist()
|
|
205
|
+
|
|
206
|
+
n_samples_input = {}
|
|
207
|
+
for key in inputs.keys():
|
|
208
|
+
if key in missing_inputs:
|
|
209
|
+
n_samples_input[key] = 1
|
|
210
|
+
else:
|
|
211
|
+
n_samples_input[key] = len(inputs[key]) if sampled else len(inputs[key]) - self.input_n_samples[key] + 1
|
|
212
|
+
|
|
213
|
+
# Vettore di input
|
|
214
|
+
if num_of_samples != 'auto':
|
|
215
|
+
for key in inputs.keys():
|
|
216
|
+
if key in model_inputs:
|
|
217
|
+
input_dim = self.model_def['Inputs'][key]['dim']
|
|
218
|
+
elif key in model_states:
|
|
219
|
+
input_dim = self.model_def['States'][key]['dim']
|
|
220
|
+
if input_dim > 1:
|
|
221
|
+
inputs[key] += [[0 for val in range(input_dim)] for val in
|
|
222
|
+
range(num_of_samples - (len(inputs[key]) - self.input_n_samples[key] + 1))]
|
|
223
|
+
else:
|
|
224
|
+
inputs[key] += [0 for val in range(num_of_samples - (len(inputs[key]) - self.input_n_samples[key] + 1))]
|
|
225
|
+
#n_samples_input[key] = num_of_samples
|
|
226
|
+
|
|
227
|
+
## Warning the users about different time windows between samples
|
|
228
|
+
if min_dim != max_dim:
|
|
229
|
+
log.warning(f'Different number of samples between inputs [MAX {max_din_key} = {max_dim}; MIN {min_din_key} = {min_dim}]')
|
|
230
|
+
|
|
231
|
+
result_dict = {} ## initialize the resulting dictionary
|
|
232
|
+
for key in self.model_def['Outputs'].keys():
|
|
233
|
+
result_dict[key] = []
|
|
234
|
+
|
|
235
|
+
## Initialize the state variables
|
|
236
|
+
if prediction_samples == None:
|
|
237
|
+
# If the prediction sample is None the connection are removed
|
|
238
|
+
self.model.init_states({}, connect = connect)
|
|
239
|
+
else:
|
|
240
|
+
self.model.init_states(self.model_def['States'], connect = connect, reset_states = False)
|
|
241
|
+
|
|
242
|
+
## Cycle through all the samples provided
|
|
243
|
+
with torch.inference_mode():
|
|
244
|
+
X = {}
|
|
245
|
+
for i in range(window_dim):
|
|
246
|
+
for key, val in inputs.items():
|
|
247
|
+
# If the prediction sample is None take the input
|
|
248
|
+
# If the prediction sample is auto and the sample is less than the available samples take the input
|
|
249
|
+
# Every prediction sample take the input
|
|
250
|
+
# Otherwise if the key is a state or a connect or a closed_loop variable keep the same input
|
|
251
|
+
# If the key is a state or connect input remove the input
|
|
252
|
+
if not (prediction_samples is None \
|
|
253
|
+
or ((prediction_samples is not None and prediction_samples != 'auto') and i % (prediction_samples + 1) == 0) \
|
|
254
|
+
or (prediction_samples == 'auto' and i < n_samples_input[key])):
|
|
255
|
+
if key in (closed_loop|connect).keys() or key in model_states:
|
|
256
|
+
if (key in model_states or key in connect.keys()) and key in X.keys():
|
|
257
|
+
del X[key]
|
|
258
|
+
continue
|
|
259
|
+
X[key] = torch.from_numpy(np.array(val[i])).to(torch.float32) if sampled else torch.from_numpy(
|
|
260
|
+
np.array(val[i:i + self.input_n_samples[key]])).to(torch.float32)
|
|
261
|
+
|
|
262
|
+
if key in model_inputs:
|
|
263
|
+
input_dim = self.model_def['Inputs'][key]['dim']
|
|
264
|
+
elif key in model_states:
|
|
265
|
+
input_dim = self.model_def['States'][key]['dim']
|
|
266
|
+
|
|
267
|
+
if input_dim > 1:
|
|
268
|
+
check(len(X[key].shape) == 2, ValueError,
|
|
269
|
+
f'The input {key} must have two dimensions')
|
|
270
|
+
check(X[key].shape[1] == input_dim, ValueError,
|
|
271
|
+
f'The second dimension of the input "{key}" must be equal to {input_dim}')
|
|
272
|
+
|
|
273
|
+
if input_dim == 1 and X[key].shape[-1] != 1: ## add the input dimension
|
|
274
|
+
X[key] = X[key].unsqueeze(-1)
|
|
275
|
+
if X[key].ndim <= 1: ## add the batch dimension
|
|
276
|
+
X[key] = X[key].unsqueeze(0)
|
|
277
|
+
if X[key].ndim <= 2: ## add the time dimension
|
|
278
|
+
X[key] = X[key].unsqueeze(0)
|
|
279
|
+
|
|
280
|
+
## Reset the state variable
|
|
281
|
+
if prediction_samples is None:
|
|
282
|
+
## If prediction sample is None the state is reset every step
|
|
283
|
+
self.model.reset_states(X, only=False)
|
|
284
|
+
self.model.reset_connect_variables(connect, X, only=False)
|
|
285
|
+
elif prediction_samples == 'auto':
|
|
286
|
+
## If prediction sample is auto is reset with the available samples
|
|
287
|
+
self.model.reset_states(X)
|
|
288
|
+
self.model.reset_connect_variables(connect, X)
|
|
289
|
+
else:
|
|
290
|
+
## Otherwise the variable are reset every prediction samples
|
|
291
|
+
if i%(prediction_samples+1) == 0:
|
|
292
|
+
self.model.reset_states(X, only=False)
|
|
293
|
+
self.model.reset_connect_variables(connect, X, only=False)
|
|
294
|
+
|
|
295
|
+
result, _ = self.model(X)
|
|
296
|
+
|
|
297
|
+
## Update the recurrent variable
|
|
298
|
+
for close_in, out_var in closed_loop.items():
|
|
299
|
+
#if i >= input_windows[close_in]-1:
|
|
300
|
+
shift = result[out_var].shape[1] ## take the output time dimension
|
|
301
|
+
X[close_in] = torch.roll(X[close_in], shifts=-1, dims=1) ## Roll the time window
|
|
302
|
+
X[close_in][:, -shift:, :] = result[out_var] ## substitute with the predicted value
|
|
303
|
+
|
|
304
|
+
## Append the prediction of the current sample to the result dictionary
|
|
305
|
+
for key in self.model_def['Outputs'].keys():
|
|
306
|
+
if result[key].shape[-1] == 1:
|
|
307
|
+
result[key] = result[key].squeeze(-1)
|
|
308
|
+
if result[key].shape[-1] == 1:
|
|
309
|
+
result[key] = result[key].squeeze(-1)
|
|
310
|
+
result_dict[key].append(result[key].detach().squeeze(dim=0).tolist())
|
|
311
|
+
|
|
312
|
+
return result_dict
|
|
313
|
+
|
|
314
|
+
def getSamples(self, dataset, index = None, window=1):
|
|
315
|
+
if index is None:
|
|
316
|
+
index = random.randint(0, self.num_of_samples[dataset] - window)
|
|
317
|
+
check(self.data_loade, ValueError, 'The Dataset must first be loaded using <loadData> function!')
|
|
318
|
+
if self.data_loaded:
|
|
319
|
+
result_dict = {}
|
|
320
|
+
for key in (self.model_def['Inputs'].keys() | self.model_def['States'].keys()):
|
|
321
|
+
result_dict[key] = []
|
|
322
|
+
for idx in range(window):
|
|
323
|
+
for key ,samples in self.data[dataset].items():
|
|
324
|
+
if key in (self.model_def['Inputs'].keys() | self.model_def['States'].keys()):
|
|
325
|
+
result_dict[key].append(samples[index+idx])
|
|
326
|
+
return result_dict
|
|
327
|
+
|
|
328
|
+
def addConnect(self, stream_out, state_list_in):
|
|
329
|
+
self.model_def.addConnect(stream_out, state_list_in)
|
|
330
|
+
|
|
331
|
+
def addClosedLoop(self, stream_out, state_list_in):
|
|
332
|
+
self.model_def.addClosedLoop(stream_out, state_list_in)
|
|
333
|
+
|
|
334
|
+
def addModel(self, name, stream_list):
|
|
335
|
+
self.model_def.addModel(name, stream_list)
|
|
336
|
+
|
|
337
|
+
def removeModel(self, name_list):
|
|
338
|
+
self.model_def.removeModel(name_list)
|
|
339
|
+
|
|
340
|
+
def addMinimize(self, name, streamA, streamB, loss_function='mse'):
|
|
341
|
+
self.model_def.addMinimize(name, streamA, streamB, loss_function)
|
|
342
|
+
self.visualizer.showaddMinimize(name)
|
|
343
|
+
|
|
344
|
+
def removeMinimize(self, name_list):
|
|
345
|
+
self.model_def.removeMinimize(name_list)
|
|
346
|
+
|
|
347
|
+
def neuralizeModel(self, sample_time = None, clear_model = False, model_def = None):
|
|
348
|
+
if model_def is not None:
|
|
349
|
+
check(sample_time == None, ValueError, 'The sample_time must be None if a model_def is provided')
|
|
350
|
+
check(clear_model == False, ValueError, 'The clear_model must be False if a model_def is provided')
|
|
351
|
+
self.model_def = ModelDef(model_def)
|
|
352
|
+
else:
|
|
353
|
+
if clear_model:
|
|
354
|
+
self.model_def.update()
|
|
355
|
+
else:
|
|
356
|
+
self.model_def.updateParameters(self.model)
|
|
357
|
+
|
|
358
|
+
self.model_def.setBuildWindow(sample_time)
|
|
359
|
+
self.model = Model(self.model_def.json)
|
|
360
|
+
|
|
361
|
+
input_ns_backward = {key:value['ns'][0] for key, value in (self.model_def['Inputs']|self.model_def['States']).items()}
|
|
362
|
+
input_ns_forward = {key:value['ns'][1] for key, value in (self.model_def['Inputs']|self.model_def['States']).items()}
|
|
363
|
+
self.input_n_samples = {}
|
|
364
|
+
for key, value in (self.model_def['Inputs'] | self.model_def['States']).items():
|
|
365
|
+
self.input_n_samples[key] = input_ns_backward[key] + input_ns_forward[key]
|
|
366
|
+
self.max_n_samples = max(input_ns_backward.values()) + max(input_ns_forward.values())
|
|
367
|
+
|
|
368
|
+
self.neuralized = True
|
|
369
|
+
self.traced = False
|
|
370
|
+
self.visualizer.showModel(self.model_def.json)
|
|
371
|
+
self.visualizer.showModelInputWindow()
|
|
372
|
+
self.visualizer.showBuiltModel()
|
|
373
|
+
|
|
374
|
+
def loadData(self, name, source, format=None, skiplines=0, delimiter=',', header=None):
|
|
375
|
+
check(self.neuralized, ValueError, "The network is not neuralized.")
|
|
376
|
+
check(delimiter in ['\t', '\n', ';', ',', ' '], ValueError, 'delimiter not valid!')
|
|
377
|
+
|
|
378
|
+
json_inputs = self.model_def['Inputs'] | self.model_def['States']
|
|
379
|
+
model_inputs = list(json_inputs.keys())
|
|
380
|
+
## Initialize the dictionary containing the data
|
|
381
|
+
if name in list(self.data.keys()):
|
|
382
|
+
log.warning(f'Dataset named {name} already loaded! overriding the existing one..')
|
|
383
|
+
self.data[name] = {}
|
|
384
|
+
|
|
385
|
+
input_ns_backward = {key:value['ns'][0] for key, value in json_inputs.items()}
|
|
386
|
+
input_ns_forward = {key:value['ns'][1] for key, value in json_inputs.items()}
|
|
387
|
+
max_samples_backward = max(input_ns_backward.values())
|
|
388
|
+
max_samples_forward = max(input_ns_forward.values())
|
|
389
|
+
max_n_samples = max_samples_backward + max_samples_forward
|
|
390
|
+
|
|
391
|
+
num_of_samples = {}
|
|
392
|
+
if type(source) is str: ## we have a directory path containing the files
|
|
393
|
+
## collect column indexes
|
|
394
|
+
format_idx = {}
|
|
395
|
+
idx = 0
|
|
396
|
+
for item in format:
|
|
397
|
+
if isinstance(item, tuple):
|
|
398
|
+
for key in item:
|
|
399
|
+
if key not in model_inputs:
|
|
400
|
+
idx += 1
|
|
401
|
+
break
|
|
402
|
+
n_cols = json_inputs[key]['dim']
|
|
403
|
+
format_idx[key] = (idx, idx+n_cols)
|
|
404
|
+
idx += n_cols
|
|
405
|
+
else:
|
|
406
|
+
if item not in model_inputs:
|
|
407
|
+
idx += 1
|
|
408
|
+
continue
|
|
409
|
+
n_cols = json_inputs[item]['dim']
|
|
410
|
+
format_idx[item] = (idx, idx+n_cols)
|
|
411
|
+
idx += n_cols
|
|
412
|
+
|
|
413
|
+
## Initialize each input key
|
|
414
|
+
for key in format_idx.keys():
|
|
415
|
+
self.data[name][key] = []
|
|
416
|
+
|
|
417
|
+
## obtain the file names
|
|
418
|
+
try:
|
|
419
|
+
_,_,files = next(os.walk(source))
|
|
420
|
+
except StopIteration as e:
|
|
421
|
+
check(False,StopIteration, f'ERROR: The path "{source}" does not exist!')
|
|
422
|
+
return
|
|
423
|
+
self.file_count = len(files)
|
|
424
|
+
|
|
425
|
+
## Cycle through all the files
|
|
426
|
+
for file in files:
|
|
427
|
+
try:
|
|
428
|
+
## read the csv
|
|
429
|
+
df = pd.read_csv(os.path.join(source,file), skiprows=skiplines, delimiter=delimiter, header=header)
|
|
430
|
+
except:
|
|
431
|
+
log.warning(f'Cannot read file {os.path.join(source,file)}')
|
|
432
|
+
continue
|
|
433
|
+
## Cycle through all the windows
|
|
434
|
+
for key, idxs in format_idx.items():
|
|
435
|
+
back, forw = input_ns_backward[key], input_ns_forward[key]
|
|
436
|
+
## Save as numpy array the data
|
|
437
|
+
data = df.iloc[:, idxs[0]:idxs[1]].to_numpy()
|
|
438
|
+
self.data[name][key] += [data[i-back:i+forw] for i in range(max_samples_backward, len(df)-max_samples_forward+1)]
|
|
439
|
+
|
|
440
|
+
## Stack the files
|
|
441
|
+
for key in format_idx.keys():
|
|
442
|
+
self.data[name][key] = np.stack(self.data[name][key])
|
|
443
|
+
num_of_samples[key] = self.data[name][key].shape[0]
|
|
444
|
+
|
|
445
|
+
elif type(source) is dict: ## we have a crafted dataset
|
|
446
|
+
self.file_count = 1
|
|
447
|
+
|
|
448
|
+
## Check if the inputs are correct
|
|
449
|
+
#assert set(model_inputs).issubset(source.keys()), f'The dataset is missing some inputs. Inputs needed for the model: {model_inputs}'
|
|
450
|
+
|
|
451
|
+
# Merge a list of
|
|
452
|
+
for key in model_inputs:
|
|
453
|
+
if key not in source.keys():
|
|
454
|
+
continue
|
|
455
|
+
|
|
456
|
+
self.data[name][key] = [] ## Initialize the dataset
|
|
457
|
+
|
|
458
|
+
back, forw = input_ns_backward[key], input_ns_forward[key]
|
|
459
|
+
for idx in range(len(source[key]) - max_n_samples+1):
|
|
460
|
+
self.data[name][key].append(source[key][idx + (max_samples_backward - back):idx + (max_samples_backward + forw)])
|
|
461
|
+
|
|
462
|
+
## Stack the files
|
|
463
|
+
for key in model_inputs:
|
|
464
|
+
if key not in source.keys():
|
|
465
|
+
continue
|
|
466
|
+
self.data[name][key] = np.stack(self.data[name][key])
|
|
467
|
+
if self.data[name][key].ndim == 2: ## Add the sample dimension
|
|
468
|
+
self.data[name][key] = np.expand_dims(self.data[name][key], axis=-1)
|
|
469
|
+
if self.data[name][key].ndim > 3:
|
|
470
|
+
self.data[name][key] = np.squeeze(self.data[name][key], axis=1)
|
|
471
|
+
num_of_samples[key] = self.data[name][key].shape[0]
|
|
472
|
+
|
|
473
|
+
# Check dim of the samples
|
|
474
|
+
check(len(set(num_of_samples.values())) == 1, ValueError,
|
|
475
|
+
f"The number of the sample of the dataset {name} are not the same for all input in the dataset: {num_of_samples}")
|
|
476
|
+
self.num_of_samples[name] = num_of_samples[list(num_of_samples.keys())[0]]
|
|
477
|
+
|
|
478
|
+
## Set the Loaded flag to True
|
|
479
|
+
self.data_loaded = True
|
|
480
|
+
## Update the number of datasets loaded
|
|
481
|
+
self.n_datasets = len(self.data.keys())
|
|
482
|
+
self.datasets_loaded.add(name)
|
|
483
|
+
## Show the dataset
|
|
484
|
+
self.visualizer.showDataset(name=name)
|
|
485
|
+
|
|
486
|
+
def filterData(self, filter_function, dataset_name = None):
|
|
487
|
+
idx_to_remove = []
|
|
488
|
+
if dataset_name is None:
|
|
489
|
+
for name in self.data.keys():
|
|
490
|
+
dataset = self.data[name]
|
|
491
|
+
n_samples = len(dataset[list(dataset.keys())[0]])
|
|
492
|
+
|
|
493
|
+
data_for_filter = []
|
|
494
|
+
for i in range(n_samples):
|
|
495
|
+
new_sample = {key: val[i] for key, val in dataset.items()}
|
|
496
|
+
data_for_filter.append(new_sample)
|
|
497
|
+
|
|
498
|
+
for idx, sample in enumerate(data_for_filter):
|
|
499
|
+
if not filter_function(sample):
|
|
500
|
+
idx_to_remove.append(idx)
|
|
501
|
+
|
|
502
|
+
for key in self.data[name].keys():
|
|
503
|
+
self.data[name][key] = np.delete(self.data[name][key], idx_to_remove, axis=0)
|
|
504
|
+
self.num_of_samples[name] = self.data[name][key].shape[0]
|
|
505
|
+
self.visualizer.showDataset(name=name)
|
|
506
|
+
|
|
507
|
+
else:
|
|
508
|
+
dataset = self.data[dataset_name]
|
|
509
|
+
n_samples = len(dataset[list(dataset.keys())[0]])
|
|
510
|
+
|
|
511
|
+
data_for_filter = []
|
|
512
|
+
for i in range(n_samples):
|
|
513
|
+
new_sample = {key: val[i] for key, val in dataset.items()}
|
|
514
|
+
data_for_filter.append(new_sample)
|
|
515
|
+
|
|
516
|
+
for idx, sample in enumerate(data_for_filter):
|
|
517
|
+
if not filter_function(sample):
|
|
518
|
+
idx_to_remove.append(idx)
|
|
519
|
+
|
|
520
|
+
for key in self.data[dataset_name].keys():
|
|
521
|
+
self.data[dataset_name][key] = np.delete(self.data[dataset_name][key], idx_to_remove, axis=0)
|
|
522
|
+
self.num_of_samples[dataset_name] = self.data[dataset_name][key].shape[0]
|
|
523
|
+
self.visualizer.showDataset(name=dataset_name)
|
|
524
|
+
|
|
525
|
+
def resetStates(self, values = None, only = True):
|
|
526
|
+
self.model.init_states(self.model_def['States'], reset_states=False)
|
|
527
|
+
self.model.reset_states(values, only)
|
|
528
|
+
|
|
529
|
+
def __save_internal(self, key, value):
|
|
530
|
+
self.internals[key] = tensor_to_list(value)
|
|
531
|
+
|
|
532
|
+
def __get_train_parameters(self, training_params):
|
|
533
|
+
run_train_parameters = copy.deepcopy(self.standard_train_parameters)
|
|
534
|
+
if training_params is None:
|
|
535
|
+
return run_train_parameters
|
|
536
|
+
for key, value in training_params.items():
|
|
537
|
+
check(key in run_train_parameters, KeyError, f"The param {key} is not exist as standard parameters")
|
|
538
|
+
run_train_parameters[key] = value
|
|
539
|
+
return run_train_parameters
|
|
540
|
+
|
|
541
|
+
def __get_parameter(self, **parameter):
|
|
542
|
+
assert len(parameter) == 1
|
|
543
|
+
name = list(parameter.keys())[0]
|
|
544
|
+
self.run_training_params[name] = parameter[name] if parameter[name] is not None else self.run_training_params[name]
|
|
545
|
+
return self.run_training_params[name]
|
|
546
|
+
|
|
547
|
+
def __get_batch_sizes(self, train_batch_size, val_batch_size, test_batch_size):
|
|
548
|
+
## Check if the batch_size can be used for the current dataset, otherwise set the batch_size to the maximum value
|
|
549
|
+
self.__get_parameter(train_batch_size = train_batch_size)
|
|
550
|
+
self.__get_parameter(val_batch_size = val_batch_size)
|
|
551
|
+
self.__get_parameter(test_batch_size = test_batch_size)
|
|
552
|
+
|
|
553
|
+
if self.run_training_params['recurrent_train']:
|
|
554
|
+
if self.run_training_params['train_batch_size'] > self.run_training_params['n_samples_train']:
|
|
555
|
+
self.run_training_params['train_batch_size'] = self.run_training_params['n_samples_train'] - self.run_training_params['prediction_samples']
|
|
556
|
+
if self.run_training_params['val_batch_size'] is None or self.run_training_params['val_batch_size'] > self.run_training_params['n_samples_val']:
|
|
557
|
+
self.run_training_params['val_batch_size'] = max(0,self.run_training_params['n_samples_val'] - self.run_training_params['prediction_samples'])
|
|
558
|
+
if self.run_training_params['test_batch_size'] is None or self.run_training_params['test_batch_size'] > self.run_training_params['n_samples_test']:
|
|
559
|
+
self.run_training_params['test_batch_size'] = max(0,self.run_training_params['n_samples_test'] - self.run_training_params['prediction_samples'])
|
|
560
|
+
else:
|
|
561
|
+
if self.run_training_params['train_batch_size'] > self.run_training_params['n_samples_train']:
|
|
562
|
+
self.run_training_params['train_batch_size'] = self.run_training_params['n_samples_train']
|
|
563
|
+
if self.run_training_params['val_batch_size'] is None or self.run_training_params['val_batch_size'] > self.run_training_params['n_samples_val']:
|
|
564
|
+
self.run_training_params['val_batch_size'] = self.run_training_params['n_samples_val']
|
|
565
|
+
if self.run_training_params['test_batch_size'] is None or self.run_training_params['test_batch_size'] > self.run_training_params['n_samples_test']:
|
|
566
|
+
self.run_training_params['test_batch_size'] = self.run_training_params['n_samples_test']
|
|
567
|
+
|
|
568
|
+
check(self.run_training_params['train_batch_size'] > 0, ValueError, f'The auto train_batch_size ({self.run_training_params["train_batch_size"] }) = n_samples_train ({self.run_training_params["n_samples_train"]}) - prediction_samples ({self.run_training_params["prediction_samples"]}), must be greater than 0.')
|
|
569
|
+
|
|
570
|
+
return self.run_training_params['train_batch_size'], self.run_training_params['val_batch_size'], self.run_training_params['test_batch_size']
|
|
571
|
+
|
|
572
|
+
def __inizilize_optimizer(self, optimizer, optimizer_params, optimizer_defaults, add_optimizer_params, add_optimizer_defaults, models, lr, lr_param):
|
|
573
|
+
# Get optimizer and initialization parameters
|
|
574
|
+
optimizer = copy.deepcopy(self.__get_parameter(optimizer=optimizer))
|
|
575
|
+
optimizer_params = copy.deepcopy(self.__get_parameter(optimizer_params=optimizer_params))
|
|
576
|
+
optimizer_defaults = copy.deepcopy(self.__get_parameter(optimizer_defaults=optimizer_defaults))
|
|
577
|
+
add_optimizer_params = copy.deepcopy(self.__get_parameter(add_optimizer_params=add_optimizer_params))
|
|
578
|
+
add_optimizer_defaults = copy.deepcopy(self.__get_parameter(add_optimizer_defaults=add_optimizer_defaults))
|
|
579
|
+
|
|
580
|
+
## Get parameter to be trained
|
|
581
|
+
json_models = []
|
|
582
|
+
models = self.__get_parameter(models=models)
|
|
583
|
+
if 'Models' in self.model_def:
|
|
584
|
+
json_models = list(self.model_def['Models'].keys()) if type(self.model_def['Models']) is dict else [self.model_def['Models']]
|
|
585
|
+
if models is None:
|
|
586
|
+
models = json_models
|
|
587
|
+
self.run_training_params['models'] = models
|
|
588
|
+
params_to_train = set()
|
|
589
|
+
if isinstance(models, str):
|
|
590
|
+
models = [models]
|
|
591
|
+
for model in models:
|
|
592
|
+
check(model in json_models, ValueError, f'The model {model} is not in the model definition')
|
|
593
|
+
if type(self.model_def['Models']) is dict:
|
|
594
|
+
params_to_train |= set(self.model_def['Models'][model]['Parameters'])
|
|
595
|
+
else:
|
|
596
|
+
params_to_train |= set(self.model_def['Parameters'].keys())
|
|
597
|
+
|
|
598
|
+
# Get the optimizer
|
|
599
|
+
if type(optimizer) is str:
|
|
600
|
+
if optimizer == 'SGD':
|
|
601
|
+
optimizer = SGD({},[])
|
|
602
|
+
elif optimizer == 'Adam':
|
|
603
|
+
optimizer = Adam({},[])
|
|
604
|
+
else:
|
|
605
|
+
check(issubclass(type(optimizer), Optimizer), TypeError,
|
|
606
|
+
"The optimizer must be an Optimizer or str")
|
|
607
|
+
|
|
608
|
+
optimizer.set_params_to_train(self.model.all_parameters, params_to_train)
|
|
609
|
+
|
|
610
|
+
optimizer.add_defaults('lr', self.run_training_params['lr'])
|
|
611
|
+
optimizer.add_option_to_params('lr', self.run_training_params['lr_param'])
|
|
612
|
+
|
|
613
|
+
if optimizer_defaults != {}:
|
|
614
|
+
optimizer.set_defaults(optimizer_defaults)
|
|
615
|
+
if optimizer_params != []:
|
|
616
|
+
optimizer.set_params(optimizer_params)
|
|
617
|
+
|
|
618
|
+
for key, value in add_optimizer_defaults.items():
|
|
619
|
+
optimizer.add_defaults(key, value)
|
|
620
|
+
|
|
621
|
+
add_optimizer_params = optimizer.unfold(add_optimizer_params)
|
|
622
|
+
for param in add_optimizer_params:
|
|
623
|
+
par = param['params']
|
|
624
|
+
del param['params']
|
|
625
|
+
for key, value in param.items():
|
|
626
|
+
optimizer.add_option_to_params(key, {par:value})
|
|
627
|
+
|
|
628
|
+
# Modify the parameter
|
|
629
|
+
optimizer.add_defaults('lr', lr)
|
|
630
|
+
optimizer.add_option_to_params('lr', lr_param)
|
|
631
|
+
|
|
632
|
+
return optimizer
|
|
633
|
+
|
|
634
|
+
def trainModel(self,
|
|
635
|
+
models=None,
|
|
636
|
+
train_dataset = None, validation_dataset = None, test_dataset = None, splits = None,
|
|
637
|
+
closed_loop = None, connect = None, step = None, prediction_samples = None,
|
|
638
|
+
shuffle_data = None,
|
|
639
|
+
early_stopping = None, early_stopping_params = None,
|
|
640
|
+
select_model = None, select_model_params = None,
|
|
641
|
+
minimize_gain = None,
|
|
642
|
+
num_of_epochs = None,
|
|
643
|
+
train_batch_size = None, val_batch_size = None, test_batch_size = None,
|
|
644
|
+
optimizer = None,
|
|
645
|
+
lr = None, lr_param = None,
|
|
646
|
+
optimizer_params = None, optimizer_defaults = None,
|
|
647
|
+
training_params = None,
|
|
648
|
+
add_optimizer_params = None, add_optimizer_defaults = None
|
|
649
|
+
):
|
|
650
|
+
|
|
651
|
+
check(self.data_loaded, RuntimeError, 'There is no data loaded! The Training will stop.')
|
|
652
|
+
check(list(self.model.parameters()), RuntimeError, 'There are no modules with learnable parameters! The Training will stop.')
|
|
653
|
+
|
|
654
|
+
## Get running parameter from dict
|
|
655
|
+
self.run_training_params = copy.deepcopy(self.__get_train_parameters(training_params))
|
|
656
|
+
|
|
657
|
+
## Get connect and closed_loop
|
|
658
|
+
prediction_samples = self.__get_parameter(prediction_samples = prediction_samples)
|
|
659
|
+
check(prediction_samples >= 0, KeyError, 'The sample horizon must be positive!')
|
|
660
|
+
|
|
661
|
+
## Check close loop and connect
|
|
662
|
+
step = self.__get_parameter(step = step)
|
|
663
|
+
closed_loop = self.__get_parameter(closed_loop = closed_loop)
|
|
664
|
+
connect = self.__get_parameter(connect = connect)
|
|
665
|
+
recurrent_train = True
|
|
666
|
+
if closed_loop:
|
|
667
|
+
for input, output in closed_loop.items():
|
|
668
|
+
check(input in self.model_def['Inputs'], ValueError, f'the tag {input} is not an input variable.')
|
|
669
|
+
check(output in self.model_def['Outputs'], ValueError, f'the tag {output} is not an output of the network')
|
|
670
|
+
log.warning(f'Recurrent train: closing the loop between the the input ports {input} and the output ports {output} for {prediction_samples} samples')
|
|
671
|
+
elif connect:
|
|
672
|
+
for connect_in, connect_out in connect.items():
|
|
673
|
+
check(connect_in in self.model_def['Inputs'], ValueError, f'the tag {connect_in} is not an input variable.')
|
|
674
|
+
check(connect_out in self.model_def['Outputs'], ValueError, f'the tag {connect_out} is not an output of the network')
|
|
675
|
+
log.warning(f'Recurrent train: connecting the input ports {connect_in} with output ports {connect_out} for {prediction_samples} samples')
|
|
676
|
+
elif self.model_def['States']: ## if we have state variables we have to do the recurrent train
|
|
677
|
+
log.warning(f"Recurrent train: update States variables {list(self.model_def['States'].keys())} for {prediction_samples} samples")
|
|
678
|
+
else:
|
|
679
|
+
if prediction_samples != 0:
|
|
680
|
+
log.warning(
|
|
681
|
+
f"The value of the prediction_samples={prediction_samples} is not used in not recursive network.")
|
|
682
|
+
recurrent_train = False
|
|
683
|
+
self.run_training_params['recurrent_train'] = recurrent_train
|
|
684
|
+
|
|
685
|
+
## Get early stopping
|
|
686
|
+
early_stopping = self.__get_parameter(early_stopping = early_stopping)
|
|
687
|
+
if early_stopping:
|
|
688
|
+
self.run_training_params['early_stopping'] = early_stopping.__name__
|
|
689
|
+
early_stopping_params = self.__get_parameter(early_stopping_params = early_stopping_params)
|
|
690
|
+
|
|
691
|
+
## Get dataset for training
|
|
692
|
+
shuffle_data = self.__get_parameter(shuffle_data = shuffle_data)
|
|
693
|
+
|
|
694
|
+
## Get the dataset name
|
|
695
|
+
train_dataset = self.__get_parameter(train_dataset = train_dataset)
|
|
696
|
+
#TODO manage multiple datasets
|
|
697
|
+
if train_dataset is None: ## If we use all datasets with the splits
|
|
698
|
+
splits = self.__get_parameter(splits = splits)
|
|
699
|
+
check(len(splits)==3, ValueError, '3 elements must be inserted for the dataset split in training, validation and test')
|
|
700
|
+
check(sum(splits)==100, ValueError, 'Training, Validation and Test splits must sum up to 100.')
|
|
701
|
+
check(splits[0] > 0, ValueError, 'The training split cannot be zero.')
|
|
702
|
+
|
|
703
|
+
## Get the dataset name
|
|
704
|
+
dataset = list(self.data.keys())[0] ## take the dataset name
|
|
705
|
+
|
|
706
|
+
## Collect the split sizes
|
|
707
|
+
train_size = splits[0] / 100.0
|
|
708
|
+
val_size = splits[1] / 100.0
|
|
709
|
+
test_size = 1 - (train_size + val_size)
|
|
710
|
+
num_of_samples = self.num_of_samples[dataset]
|
|
711
|
+
n_samples_train = round(num_of_samples*train_size)
|
|
712
|
+
n_samples_val = round(num_of_samples*val_size)
|
|
713
|
+
n_samples_test = round(num_of_samples*test_size)
|
|
714
|
+
|
|
715
|
+
## Split into train, validation and test
|
|
716
|
+
XY_train, XY_val, XY_test = {}, {}, {}
|
|
717
|
+
for key, samples in self.data[dataset].items():
|
|
718
|
+
if val_size == 0.0 and test_size == 0.0: ## we have only training set
|
|
719
|
+
XY_train[key] = torch.from_numpy(samples).to(torch.float32)
|
|
720
|
+
elif val_size == 0.0 and test_size != 0.0: ## we have only training and test set
|
|
721
|
+
XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(torch.float32)
|
|
722
|
+
XY_test[key] = torch.from_numpy(samples[n_samples_train:]).to(torch.float32)
|
|
723
|
+
elif val_size != 0.0 and test_size == 0.0: ## we have only training and validation set
|
|
724
|
+
XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(torch.float32)
|
|
725
|
+
XY_val[key] = torch.from_numpy(samples[n_samples_train:]).to(torch.float32)
|
|
726
|
+
else: ## we have training, validation and test set
|
|
727
|
+
XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(torch.float32)
|
|
728
|
+
XY_val[key] = torch.from_numpy(samples[n_samples_train:-n_samples_test]).to(torch.float32)
|
|
729
|
+
XY_test[key] = torch.from_numpy(samples[n_samples_train+n_samples_val:]).to(torch.float32)
|
|
730
|
+
|
|
731
|
+
## Set name for resultsAnalysis
|
|
732
|
+
train_dataset = self.__get_parameter(train_dataset = f"train_{dataset}_{train_size:0.2f}")
|
|
733
|
+
validation_dataset = self.__get_parameter(validation_dataset =f"validation_{dataset}_{val_size:0.2f}")
|
|
734
|
+
test_dataset = self.__get_parameter(test_dataset = f"test_{dataset}_{test_size:0.2f}")
|
|
735
|
+
else: ## Multi-Dataset
|
|
736
|
+
## Get the names of the datasets
|
|
737
|
+
datasets = list(self.data.keys())
|
|
738
|
+
validation_dataset = self.__get_parameter(validation_dataset=validation_dataset)
|
|
739
|
+
test_dataset = self.__get_parameter(test_dataset=test_dataset)
|
|
740
|
+
|
|
741
|
+
## Collect the number of samples for each dataset
|
|
742
|
+
n_samples_train, n_samples_val, n_samples_test = 0, 0, 0
|
|
743
|
+
|
|
744
|
+
check(train_dataset in datasets, KeyError, f'{train_dataset} Not Loaded!')
|
|
745
|
+
if validation_dataset is not None and validation_dataset not in datasets:
|
|
746
|
+
log.warning(f'Validation Dataset [{validation_dataset}] Not Loaded. The training will continue without validation')
|
|
747
|
+
if test_dataset is not None and test_dataset not in datasets:
|
|
748
|
+
log.warning(f'Test Dataset [{test_dataset}] Not Loaded. The training will continue without test')
|
|
749
|
+
|
|
750
|
+
## Split into train, validation and test
|
|
751
|
+
XY_train, XY_val, XY_test = {}, {}, {}
|
|
752
|
+
n_samples_train = self.num_of_samples[train_dataset]
|
|
753
|
+
XY_train = {key: torch.from_numpy(val).to(torch.float32) for key, val in self.data[train_dataset].items()}
|
|
754
|
+
if validation_dataset in datasets:
|
|
755
|
+
n_samples_val = self.num_of_samples[validation_dataset]
|
|
756
|
+
XY_val = {key: torch.from_numpy(val).to(torch.float32) for key, val in self.data[validation_dataset].items()}
|
|
757
|
+
if test_dataset in datasets:
|
|
758
|
+
n_samples_test = self.num_of_samples[test_dataset]
|
|
759
|
+
XY_test = {key: torch.from_numpy(val).to(torch.float32) for key, val in self.data[test_dataset].items()}
|
|
760
|
+
|
|
761
|
+
for key in XY_train.keys():
|
|
762
|
+
assert n_samples_train == XY_train[key].shape[0], f'The number of train samples {n_samples_train}!={XY_train[key].shape[0]} not compliant.'
|
|
763
|
+
if key in XY_val:
|
|
764
|
+
assert n_samples_val == XY_val[key].shape[0], f'The number of val samples {n_samples_val}!={XY_val[key].shape[0]} not compliant.'
|
|
765
|
+
if key in XY_test:
|
|
766
|
+
assert n_samples_test == XY_test[key].shape[0], f'The number of test samples {n_samples_test}!={XY_test[key].shape[0]} not compliant.'
|
|
767
|
+
|
|
768
|
+
assert n_samples_train > 0, f'There are {n_samples_train} samples for training.'
|
|
769
|
+
self.run_training_params['n_samples_train'] = n_samples_train
|
|
770
|
+
self.run_training_params['n_samples_val'] = n_samples_val
|
|
771
|
+
self.run_training_params['n_samples_test'] = n_samples_test
|
|
772
|
+
train_batch_size, val_batch_size, test_batch_size = self.__get_batch_sizes(train_batch_size, val_batch_size, test_batch_size)
|
|
773
|
+
|
|
774
|
+
## Define the optimizer
|
|
775
|
+
optimizer = self.__inizilize_optimizer(optimizer, optimizer_params, optimizer_defaults, add_optimizer_params, add_optimizer_defaults, models, lr, lr_param)
|
|
776
|
+
self.run_training_params['optimizer'] = optimizer.name
|
|
777
|
+
self.run_training_params['optimizer_params'] = optimizer.optimizer_params
|
|
778
|
+
self.run_training_params['optimizer_defaults'] = optimizer.optimizer_defaults
|
|
779
|
+
self.optimizer = optimizer.get_torch_optimizer()
|
|
780
|
+
|
|
781
|
+
## Get num_of_epochs
|
|
782
|
+
num_of_epochs = self.__get_parameter(num_of_epochs = num_of_epochs)
|
|
783
|
+
|
|
784
|
+
## Define the loss functions
|
|
785
|
+
minimize_gain = self.__get_parameter(minimize_gain = minimize_gain)
|
|
786
|
+
self.run_training_params['minimizers'] = {}
|
|
787
|
+
for name, values in self.model_def['Minimizers'].items():
|
|
788
|
+
self.loss_functions[name] = CustomLoss(values['loss'])
|
|
789
|
+
self.run_training_params['minimizers'][name] = {}
|
|
790
|
+
self.run_training_params['minimizers'][name]['A'] = values['A']
|
|
791
|
+
self.run_training_params['minimizers'][name]['B'] = values['B']
|
|
792
|
+
self.run_training_params['minimizers'][name]['loss'] = values['loss']
|
|
793
|
+
if name in minimize_gain:
|
|
794
|
+
self.run_training_params['minimizers'][name]['gain'] = minimize_gain[name]
|
|
795
|
+
|
|
796
|
+
## Clean the dict of the training parameter
|
|
797
|
+
del self.run_training_params['minimize_gain']
|
|
798
|
+
del self.run_training_params['lr']
|
|
799
|
+
del self.run_training_params['lr_param']
|
|
800
|
+
if not recurrent_train:
|
|
801
|
+
del self.run_training_params['connect']
|
|
802
|
+
del self.run_training_params['closed_loop']
|
|
803
|
+
del self.run_training_params['step']
|
|
804
|
+
del self.run_training_params['prediction_samples']
|
|
805
|
+
if early_stopping is None:
|
|
806
|
+
del self.run_training_params['early_stopping']
|
|
807
|
+
del self.run_training_params['early_stopping_params']
|
|
808
|
+
|
|
809
|
+
## Create the train, validation and test loss dictionaries
|
|
810
|
+
train_losses, val_losses, test_losses = {}, {}, {}
|
|
811
|
+
for key in self.model_def['Minimizers'].keys():
|
|
812
|
+
train_losses[key] = []
|
|
813
|
+
if n_samples_val > 0:
|
|
814
|
+
val_losses[key] = []
|
|
815
|
+
|
|
816
|
+
## Check the needed keys are in the datasets
|
|
817
|
+
keys = set(self.model_def['Inputs'].keys())
|
|
818
|
+
keys |= {value['A'] for value in self.model_def['Minimizers'].values()}|{value['B'] for value in self.model_def['Minimizers'].values()}
|
|
819
|
+
keys -= set(self.model_def['Relations'].keys())
|
|
820
|
+
keys -= set(self.model_def['States'].keys())
|
|
821
|
+
keys -= set(self.model_def['Outputs'].keys())
|
|
822
|
+
if 'connect' in self.run_training_params:
|
|
823
|
+
keys -= set(self.run_training_params['connect'].keys())
|
|
824
|
+
if 'closed_loop' in self.run_training_params:
|
|
825
|
+
keys -= set(self.run_training_params['closed_loop'].keys())
|
|
826
|
+
check(set(keys).issubset(set(XY_train.keys())), KeyError, f"Not all the mandatory keys {keys} are present in the training dataset {set(XY_train.keys())}.")
|
|
827
|
+
|
|
828
|
+
# Evaluate the number of update for epochs and the unsued samples
|
|
829
|
+
if recurrent_train:
|
|
830
|
+
list_of_batch_indexes = range(0, (n_samples_train - train_batch_size - prediction_samples + 1), (train_batch_size + step - 1))
|
|
831
|
+
check(n_samples_train - train_batch_size - prediction_samples + 1 > 0, ValueError,
|
|
832
|
+
f"The number of available sample are (n_samples_train ({n_samples_train}) - train_batch_size ({train_batch_size}) - prediction_samples ({prediction_samples}) + 1) = {n_samples_train - train_batch_size - prediction_samples + 1}.")
|
|
833
|
+
update_per_epochs = (n_samples_train - train_batch_size - prediction_samples + 1)//(train_batch_size + step - 1) + 1
|
|
834
|
+
unused_samples = n_samples_train - list_of_batch_indexes[-1] - train_batch_size - prediction_samples
|
|
835
|
+
else:
|
|
836
|
+
update_per_epochs = (n_samples_train - train_batch_size)/train_batch_size + 1
|
|
837
|
+
unused_samples = n_samples_train - update_per_epochs * train_batch_size
|
|
838
|
+
|
|
839
|
+
self.run_training_params['update_per_epochs'] = update_per_epochs
|
|
840
|
+
self.run_training_params['unused_samples'] = unused_samples
|
|
841
|
+
|
|
842
|
+
## Select the model
|
|
843
|
+
select_model = self.__get_parameter(select_model = select_model)
|
|
844
|
+
select_model_params = self.__get_parameter(select_model_params = select_model_params)
|
|
845
|
+
selected_model_def = ModelDef(self.model_def.json)
|
|
846
|
+
|
|
847
|
+
## Show the training parameters
|
|
848
|
+
self.visualizer.showTrainParams()
|
|
849
|
+
|
|
850
|
+
import time
|
|
851
|
+
## start the train timer
|
|
852
|
+
start = time.time()
|
|
853
|
+
self.visualizer.showStartTraining()
|
|
854
|
+
|
|
855
|
+
for epoch in range(num_of_epochs):
|
|
856
|
+
## TRAIN
|
|
857
|
+
self.model.train()
|
|
858
|
+
if recurrent_train:
|
|
859
|
+
losses = self.__recurrentTrain(XY_train, n_samples_train, train_batch_size, minimize_gain, closed_loop, connect, prediction_samples, step, shuffle=shuffle_data, train=True)
|
|
860
|
+
else:
|
|
861
|
+
losses = self.__Train(XY_train,n_samples_train, train_batch_size, minimize_gain, shuffle=shuffle_data, train=True)
|
|
862
|
+
## save the losses
|
|
863
|
+
for ind, key in enumerate(self.model_def['Minimizers'].keys()):
|
|
864
|
+
train_losses[key].append(torch.mean(losses[ind]).tolist())
|
|
865
|
+
|
|
866
|
+
if n_samples_val > 0:
|
|
867
|
+
## VALIDATION
|
|
868
|
+
self.model.eval()
|
|
869
|
+
if recurrent_train:
|
|
870
|
+
losses = self.__recurrentTrain(XY_val, n_samples_val, val_batch_size, minimize_gain, closed_loop, connect, prediction_samples, step, shuffle=False, train=False)
|
|
871
|
+
else:
|
|
872
|
+
losses = self.__Train(XY_val, n_samples_val, val_batch_size, minimize_gain, shuffle=False, train=False)
|
|
873
|
+
## save the losses
|
|
874
|
+
for ind, key in enumerate(self.model_def['Minimizers'].keys()):
|
|
875
|
+
val_losses[key].append(torch.mean(losses[ind]).tolist())
|
|
876
|
+
|
|
877
|
+
## Early-stopping
|
|
878
|
+
if callable(early_stopping):
|
|
879
|
+
if early_stopping(train_losses, val_losses, early_stopping_params):
|
|
880
|
+
log.info(f'Stopping the training at epoch {epoch} due to early stopping.')
|
|
881
|
+
break
|
|
882
|
+
|
|
883
|
+
if callable(select_model):
|
|
884
|
+
if select_model(train_losses, val_losses, select_model_params):
|
|
885
|
+
best_model_epoch = epoch
|
|
886
|
+
selected_model_def.updateParameters(self.model)
|
|
887
|
+
|
|
888
|
+
## Visualize the training...
|
|
889
|
+
self.visualizer.showTraining(epoch, train_losses, val_losses)
|
|
890
|
+
self.visualizer.showWeightsInTrain(epoch = epoch)
|
|
891
|
+
|
|
892
|
+
## Save the training time
|
|
893
|
+
end = time.time()
|
|
894
|
+
## Visualize the training time
|
|
895
|
+
for key in self.model_def['Minimizers'].keys():
|
|
896
|
+
self.training[key] = {'train': train_losses[key]}
|
|
897
|
+
if n_samples_val > 0:
|
|
898
|
+
self.training[key]['val'] = val_losses[key]
|
|
899
|
+
self.visualizer.showEndTraining(num_of_epochs-1, train_losses, val_losses)
|
|
900
|
+
self.visualizer.showTrainingTime(end-start)
|
|
901
|
+
|
|
902
|
+
## Select the model
|
|
903
|
+
if callable(select_model):
|
|
904
|
+
log.info(f'Selected the model at the epoch {best_model_epoch+1}.')
|
|
905
|
+
self.model = Model(selected_model_def)
|
|
906
|
+
else:
|
|
907
|
+
log.info('The selected model is the LAST model of the training.')
|
|
908
|
+
|
|
909
|
+
self.resultAnalysis(train_dataset, XY_train, minimize_gain, closed_loop, connect, prediction_samples, step, train_batch_size)
|
|
910
|
+
if self.run_training_params['n_samples_val'] > 0:
|
|
911
|
+
self.resultAnalysis(validation_dataset, XY_val, minimize_gain, closed_loop, connect, prediction_samples, step, val_batch_size)
|
|
912
|
+
if self.run_training_params['n_samples_test'] > 0:
|
|
913
|
+
self.resultAnalysis(test_dataset, XY_test, minimize_gain, closed_loop, connect, prediction_samples, step, test_batch_size)
|
|
914
|
+
|
|
915
|
+
self.visualizer.showResults()
|
|
916
|
+
|
|
917
|
+
## Get trained model from torch and set the model_def
|
|
918
|
+
self.model_def.updateParameters(self.model)
|
|
919
|
+
|
|
920
|
+
def __recurrentTrain(self, data, n_samples, batch_size, loss_gains, closed_loop, connect, prediction_samples, step, shuffle=True, train=True):
|
|
921
|
+
## Sample Shuffle
|
|
922
|
+
initial_value = 0 #random.randint(0, step - 1) if shuffle else 0
|
|
923
|
+
|
|
924
|
+
n_available_samples = n_samples - batch_size - prediction_samples + 1
|
|
925
|
+
check(n_available_samples > 0, ValueError, f"The number of available sample are (n_samples_train - train_batch_size - prediction_samples + 1) = {n_available_samples}.")
|
|
926
|
+
list_of_batch_indexes = range(initial_value, n_available_samples, (batch_size + step - 1))
|
|
927
|
+
|
|
928
|
+
## Initialize the train losses vector
|
|
929
|
+
aux_losses = torch.zeros([len(self.model_def['Minimizers']), len(list_of_batch_indexes)])
|
|
930
|
+
|
|
931
|
+
json_inputs = self.model_def['Inputs'] | self.model_def['States']
|
|
932
|
+
|
|
933
|
+
## +1 means that n_samples = 1 - batch_size = 1 - prediction_samples = 1 + 1 = 0 # zero epochs
|
|
934
|
+
## +1 means that n_samples = 2 - batch_size = 1 - prediction_samples = 1 + 1 = 1 # one epochs
|
|
935
|
+
for batch_val, idx in enumerate(list_of_batch_indexes):
|
|
936
|
+
if train:
|
|
937
|
+
self.optimizer.zero_grad() ## Reset the gradient
|
|
938
|
+
|
|
939
|
+
## Build the input tensor
|
|
940
|
+
XY = {key: val[idx:idx+batch_size] for key, val in data.items()}
|
|
941
|
+
# Add missing inputs
|
|
942
|
+
for key in closed_loop:
|
|
943
|
+
if key not in XY:
|
|
944
|
+
XY[key] = torch.zeros([batch_size, json_inputs[key]['ntot'], json_inputs[key]['dim']]).to(torch.float32)
|
|
945
|
+
|
|
946
|
+
## collect the horizon labels
|
|
947
|
+
XY_horizon = {key: val[idx:idx+batch_size+prediction_samples] for key, val in data.items()}
|
|
948
|
+
horizon_losses = {ind: [] for ind in range(len(self.model_def['Minimizers']))}
|
|
949
|
+
|
|
950
|
+
## Reset state variables with zeros or using inputs
|
|
951
|
+
self.model.reset_states(XY, only = False)
|
|
952
|
+
self.model.reset_connect_variables(connect, XY, only= False)
|
|
953
|
+
|
|
954
|
+
for horizon_idx in range(prediction_samples + 1):
|
|
955
|
+
out, minimize_out = self.model(XY) ## Forward pass
|
|
956
|
+
if self.log_internal:
|
|
957
|
+
self.__save_internal('inout_'+str(idx)+'_'+str(horizon_idx),{'XY':XY,'out':out,'state':self.model.states,'param':self.model.all_parameters,'connect':self.model.connect_variables})
|
|
958
|
+
|
|
959
|
+
## Loss Calculation
|
|
960
|
+
for ind, (key, value) in enumerate(self.model_def['Minimizers'].items()):
|
|
961
|
+
loss = self.loss_functions[key](minimize_out[value['A']], minimize_out[value['B']])
|
|
962
|
+
loss = (loss * loss_gains[key]) if key in loss_gains.keys() else loss ## Multiply by the gain if necessary
|
|
963
|
+
horizon_losses[ind].append(loss)
|
|
964
|
+
|
|
965
|
+
## remove the states variables from the data
|
|
966
|
+
if prediction_samples > 1:
|
|
967
|
+
for state_key in self.model_def['States'].keys():
|
|
968
|
+
if state_key in XY.keys():
|
|
969
|
+
del XY[state_key]
|
|
970
|
+
|
|
971
|
+
## Update the input with the recurrent prediction
|
|
972
|
+
if horizon_idx < prediction_samples:
|
|
973
|
+
for key in XY.keys():
|
|
974
|
+
if key in closed_loop.keys(): ## the input is recurrent
|
|
975
|
+
shift = out[closed_loop[key]].shape[1] ## take the output time dimension
|
|
976
|
+
XY[key] = torch.roll(XY[key], shifts=-1, dims=1) ## Roll the time window
|
|
977
|
+
XY[key][:, -shift:, :] = out[closed_loop[key]] ## substitute with the predicted value
|
|
978
|
+
else: ## the input is not recurrent
|
|
979
|
+
XY[key] = torch.roll(XY[key], shifts=-1, dims=0) ## Roll the sample window
|
|
980
|
+
XY[key][-1] = XY_horizon[key][batch_size+horizon_idx] ## take the next sample from the dataset
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
## Calculate the total loss
|
|
984
|
+
total_loss = 0
|
|
985
|
+
for ind in range(len(self.model_def['Minimizers'])):
|
|
986
|
+
loss = sum(horizon_losses[ind])/(prediction_samples+1)
|
|
987
|
+
aux_losses[ind][batch_val] = loss.item()
|
|
988
|
+
total_loss += loss
|
|
989
|
+
|
|
990
|
+
## Gradient Step
|
|
991
|
+
if train:
|
|
992
|
+
total_loss.backward() ## Backpropagate the error
|
|
993
|
+
self.optimizer.step()
|
|
994
|
+
self.visualizer.showWeightsInTrain(batch = batch_val)
|
|
995
|
+
|
|
996
|
+
## return the losses
|
|
997
|
+
return aux_losses
|
|
998
|
+
|
|
999
|
+
def __Train(self, data, n_samples, batch_size, loss_gains, shuffle=True, train=True):
|
|
1000
|
+
check((n_samples - batch_size + 1) > 0, ValueError,
|
|
1001
|
+
f"The number of available sample are (n_samples_train - train_batch_size + 1) = {n_samples - batch_size + 1}.")
|
|
1002
|
+
if shuffle:
|
|
1003
|
+
randomize = torch.randperm(n_samples)
|
|
1004
|
+
data = {key: val[randomize] for key, val in data.items()}
|
|
1005
|
+
## Initialize the train losses vector
|
|
1006
|
+
aux_losses = torch.zeros([len(self.model_def['Minimizers']),n_samples//batch_size])
|
|
1007
|
+
for idx in range(0, (n_samples - batch_size + 1), batch_size):
|
|
1008
|
+
## Build the input tensor
|
|
1009
|
+
XY = {key: val[idx:idx+batch_size] for key, val in data.items()}
|
|
1010
|
+
## Reset gradient
|
|
1011
|
+
if train:
|
|
1012
|
+
self.optimizer.zero_grad()
|
|
1013
|
+
## Model Forward
|
|
1014
|
+
_, minimize_out = self.model(XY) ## Forward pass
|
|
1015
|
+
## Loss Calculation
|
|
1016
|
+
total_loss = 0
|
|
1017
|
+
for ind, (key, value) in enumerate(self.model_def['Minimizers'].items()):
|
|
1018
|
+
loss = self.loss_functions[key](minimize_out[value['A']], minimize_out[value['B']])
|
|
1019
|
+
loss = (loss * loss_gains[key]) if key in loss_gains.keys() else loss ## Multiply by the gain if necessary
|
|
1020
|
+
aux_losses[ind][idx//batch_size] = loss.item()
|
|
1021
|
+
total_loss += loss
|
|
1022
|
+
## Gradient step
|
|
1023
|
+
if train:
|
|
1024
|
+
total_loss.backward()
|
|
1025
|
+
self.optimizer.step()
|
|
1026
|
+
self.visualizer.showWeightsInTrain(batch = idx//batch_size)
|
|
1027
|
+
|
|
1028
|
+
## return the losses
|
|
1029
|
+
return aux_losses
|
|
1030
|
+
|
|
1031
|
+
def resultAnalysis(self, dataset, data = None, minimize_gain = {}, closed_loop = {}, connect = {}, prediction_samples = None, step = 1, batch_size = None):
|
|
1032
|
+
import warnings
|
|
1033
|
+
with torch.inference_mode():
|
|
1034
|
+
## Init model for retults analysis
|
|
1035
|
+
self.model.eval()
|
|
1036
|
+
self.performance[dataset] = {}
|
|
1037
|
+
self.prediction[dataset] = {}
|
|
1038
|
+
A = {}
|
|
1039
|
+
B = {}
|
|
1040
|
+
total_losses = {}
|
|
1041
|
+
|
|
1042
|
+
# Create the losses
|
|
1043
|
+
losses = {}
|
|
1044
|
+
for name, values in self.model_def['Minimizers'].items():
|
|
1045
|
+
losses[name] = CustomLoss(values['loss'])
|
|
1046
|
+
|
|
1047
|
+
recurrent = False
|
|
1048
|
+
if (closed_loop or connect or self.model_def['States']) and prediction_samples is not None:
|
|
1049
|
+
recurrent = True
|
|
1050
|
+
|
|
1051
|
+
if data is None:
|
|
1052
|
+
check(dataset in self.data.keys(), ValueError, f'The dataset {dataset} is not loaded!')
|
|
1053
|
+
data = {key: torch.from_numpy(val).to(torch.float32) for key, val in self.data[dataset].items()}
|
|
1054
|
+
n_samples = len(data[list(data.keys())[0]])
|
|
1055
|
+
|
|
1056
|
+
if recurrent:
|
|
1057
|
+
json_inputs = self.model_def['Inputs'] | self.model_def['States']
|
|
1058
|
+
batch_size = batch_size if batch_size is not None else n_samples - prediction_samples
|
|
1059
|
+
initial_value = 0
|
|
1060
|
+
|
|
1061
|
+
for key, value in self.model_def['Minimizers'].items():
|
|
1062
|
+
total_losses[key], A[key], B[key] = [], [], []
|
|
1063
|
+
for horizon_idx in range(prediction_samples + 1):
|
|
1064
|
+
A[key].append([])
|
|
1065
|
+
B[key].append([])
|
|
1066
|
+
|
|
1067
|
+
for idx in range(initial_value, (n_samples - batch_size - prediction_samples + 1), (batch_size + step - 1)):
|
|
1068
|
+
## Build the input tensor
|
|
1069
|
+
XY = {key: val[idx:idx + batch_size] for key, val in data.items()}
|
|
1070
|
+
# Add missing inputs
|
|
1071
|
+
for key in closed_loop:
|
|
1072
|
+
if key not in XY:
|
|
1073
|
+
XY[key] = torch.zeros([batch_size, json_inputs[key]['ntot'], json_inputs[key]['dim']]).to(
|
|
1074
|
+
torch.float32)
|
|
1075
|
+
## collect the horizon labels
|
|
1076
|
+
XY_horizon = {key: val[idx:idx + batch_size + prediction_samples] for key, val in data.items()}
|
|
1077
|
+
horizon_losses = {key: [] for key in self.model_def['Minimizers'].keys()}
|
|
1078
|
+
|
|
1079
|
+
## Reset state variables with zeros or using inputs
|
|
1080
|
+
self.model.reset_states(XY, only=False)
|
|
1081
|
+
self.model.reset_connect_variables(connect, XY, only=False)
|
|
1082
|
+
|
|
1083
|
+
for horizon_idx in range(prediction_samples + 1):
|
|
1084
|
+
out, minimize_out = self.model(XY) ## Forward pass
|
|
1085
|
+
|
|
1086
|
+
## Loss Calculation
|
|
1087
|
+
for key, value in self.model_def['Minimizers'].items():
|
|
1088
|
+
A[key][horizon_idx].append(minimize_out[value['A']])
|
|
1089
|
+
B[key][horizon_idx].append(minimize_out[value['B']])
|
|
1090
|
+
loss = losses[key](minimize_out[value['A']], minimize_out[value['B']])
|
|
1091
|
+
loss = (loss * minimize_gain[key]) if key in minimize_gain.keys() else loss ## Multiply by the gain if necessary
|
|
1092
|
+
horizon_losses[key].append(loss)
|
|
1093
|
+
|
|
1094
|
+
## remove the states variables from the data
|
|
1095
|
+
if prediction_samples > 1:
|
|
1096
|
+
for state_key in self.model_def['States'].keys():
|
|
1097
|
+
if state_key in XY.keys():
|
|
1098
|
+
del XY[state_key]
|
|
1099
|
+
|
|
1100
|
+
## Update the input with the recurrent prediction
|
|
1101
|
+
if horizon_idx < prediction_samples:
|
|
1102
|
+
for key in XY.keys():
|
|
1103
|
+
if key in closed_loop.keys(): ## the input is recurrent
|
|
1104
|
+
shift = out[closed_loop[key]].shape[1] ## take the output time dimension
|
|
1105
|
+
XY[key] = torch.roll(XY[key], shifts=-1, dims=1) ## Roll the time window
|
|
1106
|
+
XY[key][:, -shift:, :] = out[closed_loop[key]] ## substitute with the predicted value
|
|
1107
|
+
else: ## the input is not recurrent
|
|
1108
|
+
XY[key] = torch.roll(XY[key], shifts=-1, dims=0) ## Roll the sample window
|
|
1109
|
+
XY[key][-1] = XY_horizon[key][
|
|
1110
|
+
batch_size + horizon_idx] ## take the next sample from the dataset
|
|
1111
|
+
|
|
1112
|
+
## Calculate the total loss
|
|
1113
|
+
for key in self.model_def['Minimizers'].keys():
|
|
1114
|
+
loss = sum(horizon_losses[key]) / (prediction_samples + 1)
|
|
1115
|
+
total_losses[key].append(loss.detach().numpy())
|
|
1116
|
+
|
|
1117
|
+
for key, value in self.model_def['Minimizers'].items():
|
|
1118
|
+
for horizon_idx in range(prediction_samples + 1):
|
|
1119
|
+
A[key][horizon_idx] = np.concatenate(A[key][horizon_idx])
|
|
1120
|
+
B[key][horizon_idx] = np.concatenate(B[key][horizon_idx])
|
|
1121
|
+
total_losses[key] = np.mean(total_losses[key])
|
|
1122
|
+
|
|
1123
|
+
else:
|
|
1124
|
+
if batch_size is None:
|
|
1125
|
+
batch_size = n_samples
|
|
1126
|
+
|
|
1127
|
+
for key, value in self.model_def['Minimizers'].items():
|
|
1128
|
+
total_losses[key], A[key], B[key] = [], [], []
|
|
1129
|
+
|
|
1130
|
+
for idx in range(0, (n_samples - batch_size + 1), batch_size):
|
|
1131
|
+
## Build the input tensor
|
|
1132
|
+
XY = {key: val[idx:idx + batch_size] for key, val in data.items()}
|
|
1133
|
+
if (closed_loop or connect or self.model_def['States']):
|
|
1134
|
+
## Reset state variables with zeros or using inputs
|
|
1135
|
+
self.model.reset_states(XY, only=False)
|
|
1136
|
+
self.model.reset_connect_variables(connect, XY, only=False)
|
|
1137
|
+
|
|
1138
|
+
## Model Forward
|
|
1139
|
+
_, minimize_out = self.model(XY) ## Forward pass
|
|
1140
|
+
## Loss Calculation
|
|
1141
|
+
for key, value in self.model_def['Minimizers'].items():
|
|
1142
|
+
A[key].append(minimize_out[value['A']].numpy())
|
|
1143
|
+
B[key].append(minimize_out[value['B']].numpy())
|
|
1144
|
+
loss = losses[key](minimize_out[value['A']], minimize_out[value['B']])
|
|
1145
|
+
loss = (loss * minimize_gain[key]) if key in minimize_gain.keys() else loss
|
|
1146
|
+
total_losses[key].append(loss.detach().numpy())
|
|
1147
|
+
|
|
1148
|
+
for key, value in self.model_def['Minimizers'].items():
|
|
1149
|
+
A[key] = np.concatenate(A[key])
|
|
1150
|
+
B[key] = np.concatenate(B[key])
|
|
1151
|
+
total_losses[key] = np.mean(total_losses[key])
|
|
1152
|
+
|
|
1153
|
+
for ind, (key, value) in enumerate(self.model_def['Minimizers'].items()):
|
|
1154
|
+
A_np = np.array(A[key])
|
|
1155
|
+
B_np = np.array(B[key])
|
|
1156
|
+
self.performance[dataset][key] = {}
|
|
1157
|
+
self.performance[dataset][key][value['loss']] = np.mean(total_losses[key]).item()
|
|
1158
|
+
self.performance[dataset][key]['fvu'] = {}
|
|
1159
|
+
# Compute FVU
|
|
1160
|
+
residual = A_np - B_np
|
|
1161
|
+
error_var = np.var(residual)
|
|
1162
|
+
error_mean = np.mean(residual)
|
|
1163
|
+
#error_var_manual = np.sum((residual-error_mean) ** 2) / (len(self.prediction['B'][ind]) - 0)
|
|
1164
|
+
#print(f"{key} var np:{new_error_var} and var manual:{error_var_manual}")
|
|
1165
|
+
with warnings.catch_warnings(record=True) as w:
|
|
1166
|
+
self.performance[dataset][key]['fvu']['A'] = (error_var / np.var(A_np)).item()
|
|
1167
|
+
self.performance[dataset][key]['fvu']['B'] = (error_var / np.var(B_np)).item()
|
|
1168
|
+
if w and np.var(A_np) == 0.0 and np.var(B_np) == 0.0:
|
|
1169
|
+
self.performance[dataset][key]['fvu']['A'] = np.nan
|
|
1170
|
+
self.performance[dataset][key]['fvu']['B'] = np.nan
|
|
1171
|
+
self.performance[dataset][key]['fvu']['total'] = np.mean([self.performance[dataset][key]['fvu']['A'],self.performance[dataset][key]['fvu']['B']]).item()
|
|
1172
|
+
# Compute AIC
|
|
1173
|
+
#normal_dist = norm(0, error_var ** 0.5)
|
|
1174
|
+
#probability_of_residual = normal_dist.pdf(residual)
|
|
1175
|
+
#log_likelihood_first = sum(np.log(probability_of_residual))
|
|
1176
|
+
p1 = -len(residual)/2.0*np.log(2*np.pi)
|
|
1177
|
+
with warnings.catch_warnings(record=True) as w:
|
|
1178
|
+
p2 = -len(residual)/2.0*np.log(error_var)
|
|
1179
|
+
p3 = -1 / (2.0 * error_var) * np.sum(residual ** 2)
|
|
1180
|
+
if w and p2 == np.float32(np.inf) and p3 == np.float32(-np.inf):
|
|
1181
|
+
p2 = p3 = 0.0
|
|
1182
|
+
log_likelihood = p1+p2+p3
|
|
1183
|
+
#print(f"{key} log likelihood second mode:{log_likelihood} = {p1}+{p2}+{p3} first mode: {log_likelihood_first}")
|
|
1184
|
+
total_params = sum(p.numel() for p in self.model.parameters() if p.requires_grad) #TODO to be check the number is doubled
|
|
1185
|
+
#print(f"{key} total_params:{total_params}")
|
|
1186
|
+
aic = - 2 * log_likelihood + 2 * total_params
|
|
1187
|
+
#print(f"{key} aic:{aic}")
|
|
1188
|
+
self.performance[dataset][key]['aic'] = {'value':aic,'total_params':total_params,'log_likelihood':log_likelihood}
|
|
1189
|
+
# Prediction and target
|
|
1190
|
+
self.prediction[dataset][key] = {}
|
|
1191
|
+
self.prediction[dataset][key]['A'] = A_np.tolist()
|
|
1192
|
+
self.prediction[dataset][key]['B'] = B_np.tolist()
|
|
1193
|
+
|
|
1194
|
+
self.performance[dataset]['total'] = {}
|
|
1195
|
+
self.performance[dataset]['total']['mean_error'] = np.mean([value for key,value in total_losses.items()])
|
|
1196
|
+
self.performance[dataset]['total']['fvu'] = np.mean([self.performance[dataset][key]['fvu']['total'] for key in self.model_def['Minimizers'].keys()])
|
|
1197
|
+
self.performance[dataset]['total']['aic'] = np.mean([self.performance[dataset][key]['aic']['value']for key in self.model_def['Minimizers'].keys()])
|
|
1198
|
+
|
|
1199
|
+
self.visualizer.showResult(dataset)
|
|
1200
|
+
|
|
1201
|
+
def getWorkspace(self):
|
|
1202
|
+
return self.exporter.getWorkspace()
|
|
1203
|
+
|
|
1204
|
+
def saveTorchModel(self, name = 'net', model_folder = None, models = None):
|
|
1205
|
+
check(self.neuralized == True, RuntimeError, 'The model is not neuralized yet!')
|
|
1206
|
+
if models is not None:
|
|
1207
|
+
if name == 'net':
|
|
1208
|
+
name += '_' + '_'.join(models)
|
|
1209
|
+
model_def = ModelDef()
|
|
1210
|
+
model_def.update(model_dict = {key: self.model_dict[key] for key in models if key in self.model_dict})
|
|
1211
|
+
model_def.setBuildWindow(self.model_def['Info']['SampleTime'])
|
|
1212
|
+
model_def.updateParameters(self.model)
|
|
1213
|
+
model = Model(model_def.json)
|
|
1214
|
+
else:
|
|
1215
|
+
model = self.model
|
|
1216
|
+
self.exporter.saveTorchModel(model, name, model_folder)
|
|
1217
|
+
|
|
1218
|
+
def loadTorchModel(self, name = 'net', model_folder = None):
|
|
1219
|
+
check(self.neuralized == True, RuntimeError, 'The model is not neuralized yet.')
|
|
1220
|
+
self.exporter.loadTorchModel(self.model, name, model_folder)
|
|
1221
|
+
|
|
1222
|
+
def saveModel(self, name = 'net', model_path = None, models = None):
|
|
1223
|
+
if models is not None:
|
|
1224
|
+
if name == 'net':
|
|
1225
|
+
name += '_' + '_'.join(models)
|
|
1226
|
+
model_def = ModelDef()
|
|
1227
|
+
model_def.update(model_dict = {key: self.model_dict[key] for key in models if key in self.model_dict})
|
|
1228
|
+
model_def.setBuildWindow(self.model_def['Info']['SampleTime'])
|
|
1229
|
+
model_def.updateParameters(self.model)
|
|
1230
|
+
else:
|
|
1231
|
+
model_def = self.model_def
|
|
1232
|
+
check(model_def.isDefined(), RuntimeError, "The network has not been defined.")
|
|
1233
|
+
self.exporter.saveModel(model_def.json, name, model_path)
|
|
1234
|
+
|
|
1235
|
+
def loadModel(self, name = None, model_folder = None):
|
|
1236
|
+
if name is None:
|
|
1237
|
+
name = 'net'
|
|
1238
|
+
model_def = self.exporter.loadModel(name, model_folder)
|
|
1239
|
+
check(model_def, RuntimeError, "Error to load the network.")
|
|
1240
|
+
self.model_def = ModelDef(model_def)
|
|
1241
|
+
self.model = None
|
|
1242
|
+
self.neuralized = False
|
|
1243
|
+
self.traced = False
|
|
1244
|
+
|
|
1245
|
+
def exportPythonModel(self, name = 'net', model_path = None, models = None):
|
|
1246
|
+
if models is not None:
|
|
1247
|
+
if name == 'net':
|
|
1248
|
+
name += '_' + '_'.join(models)
|
|
1249
|
+
model_def = ModelDef()
|
|
1250
|
+
model_def.update(model_dict = {key: self.model_dict[key] for key in models if key in self.model_dict})
|
|
1251
|
+
model_def.setBuildWindow(self.model_def['Info']['SampleTime'])
|
|
1252
|
+
model_def.updateParameters(self.model)
|
|
1253
|
+
model = Model(model_def.json)
|
|
1254
|
+
else:
|
|
1255
|
+
model_def = self.model_def
|
|
1256
|
+
model = self.model
|
|
1257
|
+
check(model_def['States'] == {}, TypeError, "The network has state variables. The export to python is not possible.")
|
|
1258
|
+
check(model_def.isDefined(), RuntimeError, "The network has not been defined.")
|
|
1259
|
+
check(self.traced == False, RuntimeError,
|
|
1260
|
+
'The model is traced and cannot be exported to Python.\n Run neuralizeModel() to recreate a standard model.')
|
|
1261
|
+
check(self.neuralized == True, RuntimeError, 'The model is not neuralized yet.')
|
|
1262
|
+
self.exporter.saveModel(model_def.json, name, model_path)
|
|
1263
|
+
self.exporter.exportPythonModel(model_def, model, name, model_path)
|
|
1264
|
+
|
|
1265
|
+
def importPythonModel(self, name = None, model_folder = None):
|
|
1266
|
+
if name is None:
|
|
1267
|
+
name = 'net'
|
|
1268
|
+
model_def = self.exporter.loadModel(name, model_folder)
|
|
1269
|
+
check(model_def is not None, RuntimeError, "Error to load the network.")
|
|
1270
|
+
self.neuralizeModel(model_def=model_def)
|
|
1271
|
+
self.model = self.exporter.importPythonModel(name, model_folder)
|
|
1272
|
+
self.traced = True
|
|
1273
|
+
self.model_def.updateParameters(self.model)
|
|
1274
|
+
|
|
1275
|
+
def exportONNX(self, inputs_order, outputs_order, models = None, name = 'net', model_folder = None):
|
|
1276
|
+
check(self.model_def.isDefined(), RuntimeError, "The network has not been defined.")
|
|
1277
|
+
check(self.traced == False, RuntimeError, 'The model is traced and cannot be exported to ONNX.\n Run neuralizeModel() to recreate a standard model.')
|
|
1278
|
+
check(self.neuralized == True, RuntimeError, 'The model is not neuralized yet.')
|
|
1279
|
+
check(self.model_def.model_dict != {}, RuntimeError, 'The model is loaded and not created.')
|
|
1280
|
+
model_def = ModelDef()
|
|
1281
|
+
if models is not None:
|
|
1282
|
+
if name == 'net':
|
|
1283
|
+
name += '_' + '_'.join(models)
|
|
1284
|
+
model_def.update(model_dict = {key: self.model_def.model_dict[key] for key in models if key in self.model_def.model_dict})
|
|
1285
|
+
else:
|
|
1286
|
+
model_def.update(model_dict = self.model_def.model_dict)
|
|
1287
|
+
model_def.setBuildWindow(self.model_def['Info']['SampleTime'])
|
|
1288
|
+
model_def.updateParameters(self.model)
|
|
1289
|
+
model = Model(model_def.json)
|
|
1290
|
+
self.exporter.exportONNX(model_def, model, inputs_order, outputs_order, name, model_folder)
|
|
1291
|
+
|
|
1292
|
+
def exportReport(self, name = 'net', model_folder = None):
|
|
1293
|
+
self.exporter.exportReport(self, name, model_folder)
|
|
1294
|
+
|
|
1295
|
+
nnodely = Modely
|