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.
Files changed (44) hide show
  1. mplplots/__init__.py +0 -0
  2. mplplots/plots.py +131 -0
  3. nnodely/__init__.py +42 -0
  4. nnodely/activation.py +85 -0
  5. nnodely/arithmetic.py +203 -0
  6. nnodely/earlystopping.py +81 -0
  7. nnodely/exporter/__init__.py +3 -0
  8. nnodely/exporter/export.py +275 -0
  9. nnodely/exporter/exporter.py +45 -0
  10. nnodely/exporter/reporter.py +48 -0
  11. nnodely/exporter/standardexporter.py +108 -0
  12. nnodely/fir.py +150 -0
  13. nnodely/fuzzify.py +221 -0
  14. nnodely/initializer.py +31 -0
  15. nnodely/input.py +131 -0
  16. nnodely/linear.py +130 -0
  17. nnodely/localmodel.py +82 -0
  18. nnodely/logger.py +94 -0
  19. nnodely/loss.py +30 -0
  20. nnodely/model.py +263 -0
  21. nnodely/modeldef.py +205 -0
  22. nnodely/nnodely.py +1295 -0
  23. nnodely/optimizer.py +91 -0
  24. nnodely/output.py +23 -0
  25. nnodely/parameter.py +103 -0
  26. nnodely/parametricfunction.py +329 -0
  27. nnodely/part.py +201 -0
  28. nnodely/relation.py +149 -0
  29. nnodely/trigonometric.py +67 -0
  30. nnodely/utils.py +101 -0
  31. nnodely/visualizer/__init__.py +4 -0
  32. nnodely/visualizer/dynamicmpl/functionplot.py +34 -0
  33. nnodely/visualizer/dynamicmpl/fuzzyplot.py +31 -0
  34. nnodely/visualizer/dynamicmpl/resultsplot.py +28 -0
  35. nnodely/visualizer/dynamicmpl/trainingplot.py +46 -0
  36. nnodely/visualizer/mplnotebookvisualizer.py +66 -0
  37. nnodely/visualizer/mplvisualizer.py +215 -0
  38. nnodely/visualizer/textvisualizer.py +320 -0
  39. nnodely/visualizer/visualizer.py +84 -0
  40. nnodely-0.14.0.dist-info/LICENSE +21 -0
  41. nnodely-0.14.0.dist-info/METADATA +401 -0
  42. nnodely-0.14.0.dist-info/RECORD +44 -0
  43. nnodely-0.14.0.dist-info/WHEEL +5 -0
  44. 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