• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tonegas / nnodely / 14359872492

09 Apr 2025 02:33PM UTC coverage: 97.602% (+0.6%) from 97.035%
14359872492

Pull #86

github

web-flow
Merge ec719935a into e9c323c4f
Pull Request #86: Smallclasses

2291 of 2418 new or added lines in 54 files covered. (94.75%)

3 existing lines in 1 file now uncovered.

11683 of 11970 relevant lines covered (97.6%)

0.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.9
/nnodely/operators/trainer.py
1
import copy, torch, random
1✔
2

3
from collections.abc import Callable
1✔
4

5
from nnodely.basic.modeldef import ModelDef
1✔
6
from nnodely.basic.model import Model
1✔
7
from nnodely.basic.optimizer import Optimizer, SGD, Adam
1✔
8
from nnodely.basic.loss import CustomLoss
1✔
9
from nnodely.support.utils import tensor_to_list, check, log, TORCH_DTYPE, check_gradient_operations, enforce_types
1✔
10
from nnodely.basic.relation import Stream
1✔
11
from nnodely.layers.output import Output
1✔
12

13
class Trainer():
1✔
14
    def __init__(self):
1✔
15
        check(type(self) is not Trainer, TypeError, "Trainer class cannot be instantiated directly")
1✔
16
        # Training Parameters
17
        self.__standard_train_parameters = {
1✔
18
            'models' : None,
19
            'train_dataset' : None, 'validation_dataset' : None, 'test_dataset' : None, 'splits' : [70, 20, 10],
20
            'closed_loop' : {}, 'connect' : {}, 'step' : 0, 'prediction_samples' : 0,
21
            'shuffle_data' : True,
22
            'early_stopping' : None, 'early_stopping_params' : {},
23
            'select_model' : 'last', 'select_model_params' : {},
24
            'minimize_gain' : {},
25
            'num_of_epochs': 100,
26
            'train_batch_size' : 128, 'val_batch_size' : None, 'test_batch_size' : None,
27
            'optimizer' : 'Adam',
28
            'lr' : 0.001, 'lr_param' : {},
29
            'optimizer_params' : [], 'add_optimizer_params' : [],
30
            'optimizer_defaults' : {}, 'add_optimizer_defaults' : {}
31
        }
32

33
        # Training Losses
34
        self.__loss_functions = {}
1✔
35

36
        # Optimizer
37
        self.__optimizer = None
1✔
38

39
    def __save_internal(self, key, value):
1✔
40
        self.internals[key] = tensor_to_list(value)
1✔
41

42
    def __get_train_parameters(self, training_params):
1✔
43
        run_train_parameters = copy.deepcopy(self.__standard_train_parameters)
1✔
44
        if training_params is None:
1✔
45
            return run_train_parameters
1✔
46
        for key, value in training_params.items():
1✔
47
            check(key in run_train_parameters, KeyError, f"The param {key} is not exist as standard parameters")
1✔
48
            run_train_parameters[key] = value
1✔
49
        return run_train_parameters
1✔
50

51
    def __get_parameter(self, **parameter):
1✔
52
        assert len(parameter) == 1
1✔
53
        name = list(parameter.keys())[0]
1✔
54
        self.run_training_params[name] = parameter[name] if parameter[name] is not None else self.run_training_params[
1✔
55
            name]
56
        return self.run_training_params[name]
1✔
57

58
    def __get_batch_sizes(self, train_batch_size, val_batch_size, test_batch_size):
1✔
59
        ## Check if the batch_size can be used for the current dataset, otherwise set the batch_size to the maximum value
60
        self.__get_parameter(train_batch_size=train_batch_size)
1✔
61
        self.__get_parameter(val_batch_size=val_batch_size)
1✔
62
        self.__get_parameter(test_batch_size=test_batch_size)
1✔
63

64
        if self.run_training_params['recurrent_train']:
1✔
65
            if self.run_training_params['train_batch_size'] > self.run_training_params['n_samples_train']:
1✔
66
                self.run_training_params['train_batch_size'] = self.run_training_params['n_samples_train'] - \
1✔
67
                                                               self.run_training_params['prediction_samples']
68
            if self.run_training_params['val_batch_size'] is None or self.run_training_params['val_batch_size'] > \
1✔
69
                    self.run_training_params['n_samples_val']:
70
                self.run_training_params['val_batch_size'] = max(0, self.run_training_params['n_samples_val'] -
1✔
71
                                                                 self.run_training_params['prediction_samples'])
72
            if self.run_training_params['test_batch_size'] is None or self.run_training_params['test_batch_size'] > \
1✔
73
                    self.run_training_params['n_samples_test']:
74
                self.run_training_params['test_batch_size'] = max(0, self.run_training_params['n_samples_test'] -
1✔
75
                                                                  self.run_training_params['prediction_samples'])
76
        else:
77
            if self.run_training_params['train_batch_size'] > self.run_training_params['n_samples_train']:
1✔
78
                self.run_training_params['train_batch_size'] = self.run_training_params['n_samples_train']
1✔
79
            if self.run_training_params['val_batch_size'] is None or self.run_training_params['val_batch_size'] > \
1✔
80
                    self.run_training_params['n_samples_val']:
81
                self.run_training_params['val_batch_size'] = self.run_training_params['n_samples_val']
1✔
82
            if self.run_training_params['test_batch_size'] is None or self.run_training_params['test_batch_size'] > \
1✔
83
                    self.run_training_params['n_samples_test']:
84
                self.run_training_params['test_batch_size'] = self.run_training_params['n_samples_test']
1✔
85

86
        check(self.run_training_params['train_batch_size'] > 0, ValueError,
1✔
87
              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.')
88

89
        return self.run_training_params['train_batch_size'], self.run_training_params['val_batch_size'], \
1✔
90
        self.run_training_params['test_batch_size']
91

92
    def __inizilize_optimizer(self, optimizer, optimizer_params, optimizer_defaults, add_optimizer_params,
1✔
93
                              add_optimizer_defaults, models, lr, lr_param):
94
        # Get optimizer and initialization parameters
95
        optimizer = copy.deepcopy(self.__get_parameter(optimizer=optimizer))
1✔
96
        optimizer_params = copy.deepcopy(self.__get_parameter(optimizer_params=optimizer_params))
1✔
97
        optimizer_defaults = copy.deepcopy(self.__get_parameter(optimizer_defaults=optimizer_defaults))
1✔
98
        add_optimizer_params = copy.deepcopy(self.__get_parameter(add_optimizer_params=add_optimizer_params))
1✔
99
        add_optimizer_defaults = copy.deepcopy(self.__get_parameter(add_optimizer_defaults=add_optimizer_defaults))
1✔
100

101
        ## Get parameter to be trained
102
        json_models = []
1✔
103
        models = self.__get_parameter(models=models)
1✔
104
        if 'Models' in self._model_def:
1✔
105
            json_models = list(self._model_def['Models'].keys()) if type(self._model_def['Models']) is dict else [
1✔
106
                self._model_def['Models']]
107
        if models is None:
1✔
108
            models = json_models
1✔
109
        self.run_training_params['models'] = models
1✔
110
        params_to_train = set()
1✔
111
        if isinstance(models, str):
1✔
112
            models = [models]
1✔
113
        for model in models:
1✔
114
            check(model in json_models, ValueError, f'The model {model} is not in the model definition')
1✔
115
            if type(self._model_def['Models']) is dict:
1✔
116
                params_to_train |= set(self._model_def['Models'][model]['Parameters'])
1✔
117
            else:
118
                params_to_train |= set(self._model_def['Parameters'].keys())
1✔
119

120
        # Get the optimizer
121
        if type(optimizer) is str:
1✔
122
            if optimizer == 'SGD':
1✔
123
                optimizer = SGD({}, [])
1✔
124
            elif optimizer == 'Adam':
1✔
125
                optimizer = Adam({}, [])
1✔
126
        else:
127
            check(issubclass(type(optimizer), Optimizer), TypeError,
1✔
128
                  "The optimizer must be an Optimizer or str")
129

130
        optimizer.set_params_to_train(self._model.all_parameters, params_to_train)
1✔
131

132
        optimizer.add_defaults('lr', self.run_training_params['lr'])
1✔
133
        optimizer.add_option_to_params('lr', self.run_training_params['lr_param'])
1✔
134

135
        if optimizer_defaults != {}:
1✔
136
            optimizer.set_defaults(optimizer_defaults)
1✔
137
        if optimizer_params != []:
1✔
138
            optimizer.set_params(optimizer_params)
1✔
139

140
        for key, value in add_optimizer_defaults.items():
1✔
141
            optimizer.add_defaults(key, value)
1✔
142

143
        add_optimizer_params = optimizer.unfold(add_optimizer_params)
1✔
144
        for param in add_optimizer_params:
1✔
145
            par = param['params']
1✔
146
            del param['params']
1✔
147
            for key, value in param.items():
1✔
148
                optimizer.add_option_to_params(key, {par: value})
1✔
149

150
        # Modify the parameter
151
        optimizer.add_defaults('lr', lr)
1✔
152
        optimizer.add_option_to_params('lr', lr_param)
1✔
153

154
        return optimizer
1✔
155

156

157
    def __get_batch_indexes(self, dataset_name, n_samples, prediction_samples, batch_size, step, type='train'):
1✔
158
        available_samples = n_samples - prediction_samples
1✔
159
        batch_indexes = list(range(available_samples))
1✔
160
        if dataset_name in self._multifile.keys():
1✔
161
            if type == 'train':
1✔
162
                start_idx, end_idx = 0, n_samples
1✔
163
            elif type == 'val':
1✔
164
                start_idx, end_idx = self.run_training_params['n_samples_train'], self.run_training_params[
1✔
165
                                                                                      'n_samples_train'] + n_samples
NEW
166
            elif type == 'test':
×
NEW
167
                start_idx, end_idx = self.run_training_params['n_samples_train'] + self.run_training_params[
×
168
                    'n_samples_val'], self.run_training_params['n_samples_train'] + self.run_training_params[
169
                                         'n_samples_val'] + n_samples
170

171
            forbidden_idxs = []
1✔
172
            for i in self._multifile[dataset_name]:
1✔
173
                if i < end_idx and i > start_idx:
1✔
174
                    forbidden_idxs.extend(range(i - prediction_samples, i, 1))
1✔
175
            batch_indexes = [idx for idx in batch_indexes if idx not in forbidden_idxs]
1✔
176

177
        ## Clip the step
178
        clipped_step = copy.deepcopy(step)
1✔
179
        if clipped_step < 0:  ## clip the step to zero
1✔
180
            log.warning(f"The step is negative ({clipped_step}). The step is set to zero.", stacklevel=5)
1✔
181
            clipped_step = 0
1✔
182
        if clipped_step > (len(batch_indexes) - batch_size):  ## Clip the step to the maximum number of samples
1✔
183
            log.warning(
1✔
184
                f"The step ({clipped_step}) is greater than the number of available samples ({len(batch_indexes) - batch_size}). The step is set to the maximum number.",
185
                stacklevel=5)
186
            clipped_step = len(batch_indexes) - batch_size
1✔
187
        ## Loss vector
188
        check((batch_size + clipped_step) > 0, ValueError,
1✔
189
              f"The sum of batch_size={batch_size} and the step={clipped_step} must be greater than 0.")
190

191
        return batch_indexes, clipped_step
1✔
192

193
    def __recurrentTrain(self, data, batch_indexes, batch_size, loss_gains, closed_loop, connect, prediction_samples,
1✔
194
                         step, non_mandatory_inputs, mandatory_inputs, shuffle=False, train=True):
195
        indexes = copy.deepcopy(batch_indexes)
1✔
196
        json_inputs = self._model_def['States'] | self._model_def['Inputs']
1✔
197
        aux_losses = torch.zeros(
1✔
198
            [len(self._model_def['Minimizers']), round((len(indexes) + step) / (batch_size + step))])
199
        ## Update with virtual states
200
        self._model.update(closed_loop=closed_loop, connect=connect)
1✔
201
        X = {}
1✔
202
        batch_val = 0
1✔
203
        while len(indexes) >= batch_size:
1✔
204
            idxs = random.sample(indexes, batch_size) if shuffle else indexes[:batch_size]
1✔
205
            for num in idxs:
1✔
206
                indexes.remove(num)
1✔
207
            if step > 0:
1✔
208
                if len(indexes) >= step:
1✔
209
                    step_idxs = random.sample(indexes, step) if shuffle else indexes[:step]
1✔
210
                    for num in step_idxs:
1✔
211
                        indexes.remove(num)
1✔
212
                else:
213
                    indexes = []
1✔
214
            if train:
1✔
215
                self.__optimizer.zero_grad()  ## Reset the gradient
1✔
216
            ## Reset
217
            horizon_losses = {ind: [] for ind in range(len(self._model_def['Minimizers']))}
1✔
218
            for key in non_mandatory_inputs:
1✔
219
                if key in data.keys():
1✔
220
                    ## with data
221
                    X[key] = data[key][idxs]
1✔
222
                else:  ## with zeros
223
                    window_size = self._input_n_samples[key]
1✔
224
                    dim = json_inputs[key]['dim']
1✔
225
                    if 'type' in json_inputs[key]:
1✔
226
                        X[key] = torch.zeros(size=(batch_size, window_size, dim), dtype=TORCH_DTYPE, requires_grad=True)
1✔
227
                    else:
228
                        X[key] = torch.zeros(size=(batch_size, window_size, dim), dtype=TORCH_DTYPE,
1✔
229
                                             requires_grad=False)
230
                    self._states[key] = X[key]
1✔
231

232

233
            for horizon_idx in range(prediction_samples + 1):
1✔
234
                ## Get data
235
                for key in mandatory_inputs:
1✔
236
                    X[key] = data[key][[idx + horizon_idx for idx in idxs]]
1✔
237
                ## Forward pass
238
                out, minimize_out, out_closed_loop, out_connect = self._model(X)
1✔
239

240
                if self.log_internal and train:
1✔
241
                    assert (check_gradient_operations(self._states) == 0)
1✔
242
                    assert (check_gradient_operations(data) == 0)
1✔
243
                    internals_dict = {'XY': tensor_to_list(X), 'out': out, 'param': self._model.all_parameters,
1✔
244
                                      'closedLoop': self._model.closed_loop_update, 'connect': self._model.connect_update}
245

246
                ## Loss Calculation
247
                for ind, (key, value) in enumerate(self._model_def['Minimizers'].items()):
1✔
248
                    loss = self.__loss_functions[key](minimize_out[value['A']], minimize_out[value['B']])
1✔
249
                    loss = (loss * loss_gains[
1✔
250
                        key]) if key in loss_gains.keys() else loss  ## Multiply by the gain if necessary
251
                    horizon_losses[ind].append(loss)
1✔
252

253
                ## Update
254
                self._updateState(X, out_closed_loop, out_connect)
1✔
255

256
                if self.log_internal and train:
1✔
257
                    internals_dict['state'] = self._states
1✔
258
                    self.__save_internal('inout_' + str(batch_val) + '_' + str(horizon_idx), internals_dict)
1✔
259

260
            ## Calculate the total loss
261
            total_loss = 0
1✔
262
            for ind in range(len(self._model_def['Minimizers'])):
1✔
263
                loss = sum(horizon_losses[ind]) / (prediction_samples + 1)
1✔
264
                aux_losses[ind][batch_val] = loss.item()
1✔
265
                total_loss += loss
1✔
266

267
            ## Gradient Step
268
            if train:
1✔
269
                total_loss.backward()  ## Backpropagate the error
1✔
270
                self.__optimizer.step()
1✔
271
                self.visualizer.showWeightsInTrain(batch=batch_val)
1✔
272
            batch_val += 1
1✔
273

274
        ## Remove virtual states
275
        self._removeVirtualStates(connect, closed_loop)
1✔
276

277
        ## return the losses
278
        return aux_losses
1✔
279

280
    def __Train(self, data, n_samples, batch_size, loss_gains, shuffle=True, train=True):
1✔
281
        check((n_samples - batch_size + 1) > 0, ValueError,
1✔
282
              f"The number of available sample are (n_samples_train - train_batch_size + 1) = {n_samples - batch_size + 1}.")
283
        if shuffle:
1✔
284
            randomize = torch.randperm(n_samples)
1✔
285
            data = {key: val[randomize] for key, val in data.items()}
1✔
286
        ## Initialize the train losses vector
287
        aux_losses = torch.zeros([len(self._model_def['Minimizers']), n_samples // batch_size])
1✔
288
        for idx in range(0, (n_samples - batch_size + 1), batch_size):
1✔
289
            ## Build the input tensor
290
            XY = {key: val[idx:idx + batch_size] for key, val in data.items()}
1✔
291
            ## Reset gradient
292
            if train:
1✔
293
                self.__optimizer.zero_grad()
1✔
294
            ## Model Forward
295
            _, minimize_out, _, _ = self._model(XY)  ## Forward pass
1✔
296
            ## Loss Calculation
297
            total_loss = 0
1✔
298
            for ind, (key, value) in enumerate(self._model_def['Minimizers'].items()):
1✔
299
                loss = self.__loss_functions[key](minimize_out[value['A']], minimize_out[value['B']])
1✔
300
                loss = (loss * loss_gains[
1✔
301
                    key]) if key in loss_gains.keys() else loss  ## Multiply by the gain if necessary
302
                aux_losses[ind][idx // batch_size] = loss.item()
1✔
303
                total_loss += loss
1✔
304
            ## Gradient step
305
            if train:
1✔
306
                total_loss.backward()
1✔
307
                self.__optimizer.step()
1✔
308
                self.visualizer.showWeightsInTrain(batch=idx // batch_size)
1✔
309

310
        ## return the losses
311
        return aux_losses
1✔
312

313
    @enforce_types
1✔
314
    def addMinimize(self, name:str, streamA:Stream|Output, streamB:Stream|Output, loss_function:str='mse') -> None:
1✔
315
        """
316
        Adds a minimize loss function to the model.
317

318
        Parameters
319
        ----------
320
        name : str
321
            The name of the cost function.
322
        streamA : Stream
323
            The first relation stream for the minimize operation.
324
        streamB : Stream
325
            The second relation stream for the minimize operation.
326
        loss_function : str, optional
327
            The loss function to use from the ones provided. Default is 'mse'.
328

329
        Example
330
        -------
331
        Example usage:
332
            >>> model.addMinimize('minimize_op', streamA, streamB, loss_function='mse')
333
        """
334
        self._model_def.addMinimize(name, streamA, streamB, loss_function)
1✔
335
        self.visualizer.showaddMinimize(name)
1✔
336

337
    @enforce_types
1✔
338
    def removeMinimize(self, name_list:list|str) -> None:
1✔
339
        """
340
        Removes minimize loss functions using the given list of names.
341

342
        Parameters
343
        ----------
344
        name_list : list of str
345
            The list of minimize operation names to remove.
346

347
        Example
348
        -------
349
        Example usage:
350
            >>> model.removeMinimize(['minimize_op1', 'minimize_op2'])
351
        """
352
        self._model_def.removeMinimize(name_list)
1✔
353

354
    @enforce_types
1✔
355
    def trainModel(self,
1✔
356
                   models: str | list | None = None,
357
                   train_dataset: str | None = None, validation_dataset: str | None = None, test_dataset: str | None = None, splits: list | None = None,
358
                   closed_loop: dict | None = None, connect: dict | None = None, step: int | None = None, prediction_samples: int | None = None,
359
                   shuffle_data: bool | None = None,
360
                   early_stopping: Callable | None = None, early_stopping_params: dict | None = None,
361
                   select_model: Callable | None = None, select_model_params: dict | None = None,
362
                   minimize_gain: dict | None = None,
363
                   num_of_epochs: int = None,
364
                   train_batch_size: int = None, val_batch_size: int = None, test_batch_size: int = None,
365
                   optimizer: str | Optimizer | None = None,
366
                   lr: int | float | None = None, lr_param: dict | None = None,
367
                   optimizer_params: list | None = None, optimizer_defaults: dict | None = None,
368
                   training_params: dict | None = None,
369
                   add_optimizer_params: list | None = None, add_optimizer_defaults: dict | None = None
370
                   ) -> None:
371
        """
372
        Trains the model using the provided datasets and parameters.
373

374
        Parameters
375
        ----------
376
        models : list or None, optional
377
            A list of models to train. Default is None.
378
        train_dataset : str or None, optional
379
            The name of the training dataset. Default is None.
380
        validation_dataset : str or None, optional
381
            The name of the validation dataset. Default is None.
382
        test_dataset : str or None, optional
383
            The name of the test dataset. Default is None.
384
        splits : list or None, optional
385
            A list of 3 elements specifying the percentage of splits for training, validation, and testing. The three elements must sum up to 100!
386
            The parameter splits is only used when there is only 1 dataset loaded. Default is None.
387
        closed_loop : dict or None, optional
388
            A dictionary specifying closed loop connections. The keys are input names and the values are output names. Default is None.
389
        connect : dict or None, optional
390
            A dictionary specifying connections. The keys are input names and the values are output names. Default is None.
391
        step : int or None, optional
392
            The step size for training. A big value will result in less data used for each epochs and a faster train. Default is None.
393
        prediction_samples : int or None, optional
394
            The size of the prediction horizon. Number of samples at each recurrent window Default is None.
395
        shuffle_data : bool or None, optional
396
            Whether to shuffle the data during training. Default is None.
397
        early_stopping : Callable or None, optional
398
            A callable for early stopping. Default is None.
399
        early_stopping_params : dict or None, optional
400
            A dictionary of parameters for early stopping. Default is None.
401
        select_model : Callable or None, optional
402
            A callable for selecting the best model. Default is None.
403
        select_model_params : dict or None, optional
404
            A dictionary of parameters for selecting the best model. Default is None.
405
        minimize_gain : dict or None, optional
406
            A dictionary specifying the gain for each minimization loss function. Default is None.
407
        num_of_epochs : int or None, optional
408
            The number of epochs to train the model. Default is None.
409
        train_batch_size : int or None, optional
410
            The batch size for training. Default is None.
411
        val_batch_size : int or None, optional
412
            The batch size for validation. Default is None.
413
        test_batch_size : int or None, optional
414
            The batch size for testing. Default is None.
415
        optimizer : Optimizer or None, optional
416
            The optimizer to use for training. Default is None.
417
        lr : float or None, optional
418
            The learning rate. Default is None.
419
        lr_param : dict or None, optional
420
            A dictionary of learning rate parameters. Default is None.
421
        optimizer_params : list or dict or None, optional
422
            A dictionary of optimizer parameters. Default is None.
423
        optimizer_defaults : dict or None, optional
424
            A dictionary of default optimizer settings. Default is None.
425
        training_params : dict or None, optional
426
            A dictionary of training parameters. Default is None.
427
        add_optimizer_params : list or None, optional
428
            Additional optimizer parameters. Default is None.
429
        add_optimizer_defaults : dict or None, optional
430
            Additional default optimizer settings. Default is None.
431

432
        Raises
433
        ------
434
        RuntimeError
435
            If no data is loaded or if there are no modules with learnable parameters.
436
        KeyError
437
            If the sample horizon is not positive.
438
        ValueError
439
            If an input or output variable is not in the model definition.
440

441
        Examples
442
        --------
443
        .. image:: https://colab.research.google.com/assets/colab-badge.svg
444
            :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/training.ipynb
445
            :alt: Open in Colab
446

447
        Example - basic feed-forward training:
448
            >>> x = Input('x')
449
            >>> F = Input('F')
450

451
            >>> xk1 = Output('x[k+1]', Fir()(x.tw(0.2))+Fir()(F.last()))
452

453
            >>> mass_spring_damper = Modely(seed=0)
454
            >>> mass_spring_damper.addModel('xk1',xk1)
455
            >>> mass_spring_damper.neuralizeModel(sample_time = 0.05)
456

457
            >>> data_struct = ['time','x','dx','F']
458
            >>> data_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),'dataset','data')
459
            >>> mass_spring_damper.loadData(name='mass_spring_dataset', source=data_folder, format=data_struct, delimiter=';')
460

461
            >>> params = {'num_of_epochs': 100,'train_batch_size': 128,'lr':0.001}
462
            >>> mass_spring_damper.trainModel(splits=[70,20,10], training_params = params)
463

464
        Example - recurrent training:
465
            >>> x = Input('x')
466
            >>> F = Input('F')
467

468
            >>> xk1 = Output('x[k+1]', Fir()(x.tw(0.2))+Fir()(F.last()))
469

470
            >>> mass_spring_damper = Modely(seed=0)
471
            >>> mass_spring_damper.addModel('xk1',xk1)
472
            >>> mass_spring_damper.addClosedLoop(xk1, x)
473
            >>> mass_spring_damper.neuralizeModel(sample_time = 0.05)
474

475
            >>> data_struct = ['time','x','dx','F']
476
            >>> data_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),'dataset','data')
477
            >>> mass_spring_damper.loadData(name='mass_spring_dataset', source=data_folder, format=data_struct, delimiter=';')
478

479
            >>> params = {'num_of_epochs': 100,'train_batch_size': 128,'lr':0.001}
480
            >>> mass_spring_damper.trainModel(splits=[70,20,10], prediction_samples=10, training_params = params)
481
        """
482
        check(self._data_loaded, RuntimeError, 'There is no _data loaded! The Training will stop.')
1✔
483
        check('Models' in self._model_def.getJson(), RuntimeError,
1✔
484
              'There are no models to train. Load a model using the addModel function.')
485
        check(list(self._model.parameters()), RuntimeError,
1✔
486
              'There are no modules with learnable parameters! The Training will stop.')
487

488
        ## Get running parameter from dict
489
        self.run_training_params = copy.deepcopy(self.__get_train_parameters(training_params))
1✔
490

491
        ## Get connect and closed_loop
492
        prediction_samples = self.__get_parameter(prediction_samples=prediction_samples)
1✔
493
        check(prediction_samples >= 0, KeyError, 'The sample horizon must be positive!')
1✔
494

495
        ## Check close loop and connect
496
        if self.log_internal:
1✔
497
            self.internals = {}
1✔
498
        step = self.__get_parameter(step=step)
1✔
499
        closed_loop = self.__get_parameter(closed_loop=closed_loop)
1✔
500
        connect = self.__get_parameter(connect=connect)
1✔
501
        recurrent_train = True
1✔
502
        if closed_loop:
1✔
503
            for input, output in closed_loop.items():
1✔
504
                check(input in self._model_def['Inputs'], ValueError, f'the tag {input} is not an input variable.')
1✔
505
                check(output in self._model_def['Outputs'], ValueError,
1✔
506
                      f'the tag {output} is not an output of the network')
507
                log.warning(
1✔
508
                    f'Recurrent train: closing the loop between the the input ports {input} and the output ports {output} for {prediction_samples} samples')
509
        elif connect:
1✔
510
            for connect_in, connect_out in connect.items():
1✔
511
                check(connect_in in self._model_def['Inputs'], ValueError,
1✔
512
                      f'the tag {connect_in} is not an input variable.')
513
                check(connect_out in self._model_def['Outputs'], ValueError,
1✔
514
                      f'the tag {connect_out} is not an output of the network')
515
                log.warning(
1✔
516
                    f'Recurrent train: connecting the input ports {connect_in} with output ports {connect_out} for {prediction_samples} samples')
517
        elif self._model_def['States']:  ## if we have state variables we have to do the recurrent train
1✔
518
            log.warning(
1✔
519
                f"Recurrent train: update States variables {list(self._model_def['States'].keys())} for {prediction_samples} samples")
520
        else:
521
            if prediction_samples != 0:
1✔
522
                log.warning(
1✔
523
                    f"The value of the prediction_samples={prediction_samples} is not used in not recursive network.")
524
            recurrent_train = False
1✔
525
        self.run_training_params['recurrent_train'] = recurrent_train
1✔
526

527
        ## Get early stopping
528
        early_stopping = self.__get_parameter(early_stopping=early_stopping)
1✔
529
        if early_stopping:
1✔
NEW
530
            self.run_training_params['early_stopping'] = early_stopping.__name__
×
531
        early_stopping_params = self.__get_parameter(early_stopping_params=early_stopping_params)
1✔
532

533
        ## Get dataset for training
534
        shuffle_data = self.__get_parameter(shuffle_data=shuffle_data)
1✔
535

536
        ## Get the dataset name
537
        train_dataset = self.__get_parameter(train_dataset=train_dataset)
1✔
538
        # TODO manage multiple datasets
539
        if train_dataset is None:  ## If we use all datasets with the splits
1✔
540
            splits = self.__get_parameter(splits=splits)
1✔
541
            check(len(splits) == 3, ValueError,
1✔
542
                  '3 elements must be inserted for the dataset split in training, validation and test')
543
            check(sum(splits) == 100, ValueError, 'Training, Validation and Test splits must sum up to 100.')
1✔
544
            check(splits[0] > 0, ValueError, 'The training split cannot be zero.')
1✔
545

546
            ## Get the dataset name
547
            dataset = list(self._data.keys())[0]  ## take the dataset name
1✔
548
            train_dataset_name = val_dataset_name = test_dataset_name = dataset
1✔
549

550
            ## Collect the split sizes
551
            train_size = splits[0] / 100.0
1✔
552
            val_size = splits[1] / 100.0
1✔
553
            test_size = 1 - (train_size + val_size)
1✔
554
            num_of_samples = self._num_of_samples[dataset]
1✔
555
            n_samples_train = round(num_of_samples * train_size)
1✔
556
            if splits[1] == 0:
1✔
557
                n_samples_test = num_of_samples - n_samples_train
1✔
558
                n_samples_val = 0
1✔
559
            else:
560
                n_samples_test = round(num_of_samples * test_size)
1✔
561
                n_samples_val = num_of_samples - n_samples_train - n_samples_test
1✔
562

563
            ## Split into train, validation and test
564
            XY_train, XY_val, XY_test = {}, {}, {}
1✔
565
            for key, samples in self._data[dataset].items():
1✔
566
                if val_size == 0.0 and test_size == 0.0:  ## we have only training set
1✔
567
                    XY_train[key] = torch.from_numpy(samples).to(TORCH_DTYPE)
1✔
568
                elif val_size == 0.0 and test_size != 0.0:  ## we have only training and test set
1✔
569
                    XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(TORCH_DTYPE)
1✔
570
                    XY_test[key] = torch.from_numpy(samples[n_samples_train:]).to(TORCH_DTYPE)
1✔
571
                elif val_size != 0.0 and test_size == 0.0:  ## we have only training and validation set
1✔
572
                    XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(TORCH_DTYPE)
1✔
573
                    XY_val[key] = torch.from_numpy(samples[n_samples_train:]).to(TORCH_DTYPE)
1✔
574
                else:  ## we have training, validation and test set
575
                    XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(TORCH_DTYPE)
1✔
576
                    XY_val[key] = torch.from_numpy(samples[n_samples_train:-n_samples_test]).to(TORCH_DTYPE)
1✔
577
                    XY_test[key] = torch.from_numpy(samples[n_samples_train + n_samples_val:]).to(TORCH_DTYPE)
1✔
578

579
            ## Set name for resultsAnalysis
580
            train_dataset = self.__get_parameter(train_dataset=f"train_{dataset}_{train_size:0.2f}")
1✔
581
            validation_dataset = self.__get_parameter(validation_dataset=f"validation_{dataset}_{val_size:0.2f}")
1✔
582
            test_dataset = self.__get_parameter(test_dataset=f"test_{dataset}_{test_size:0.2f}")
1✔
583
        else:  ## Multi-Dataset
584
            ## Get the names of the datasets
585
            datasets = list(self._data.keys())
1✔
586
            validation_dataset = self.__get_parameter(validation_dataset=validation_dataset)
1✔
587
            test_dataset = self.__get_parameter(test_dataset=test_dataset)
1✔
588
            train_dataset_name, val_dataset_name, test_dataset_name = train_dataset, validation_dataset, test_dataset
1✔
589

590
            ## Collect the number of samples for each dataset
591
            n_samples_train, n_samples_val, n_samples_test = 0, 0, 0
1✔
592

593
            check(train_dataset in datasets, KeyError, f'{train_dataset} Not Loaded!')
1✔
594
            if validation_dataset is not None and validation_dataset not in datasets:
1✔
NEW
595
                log.warning(
×
596
                    f'Validation Dataset [{validation_dataset}] Not Loaded. The training will continue without validation')
597
            if test_dataset is not None and test_dataset not in datasets:
1✔
NEW
598
                log.warning(f'Test Dataset [{test_dataset}] Not Loaded. The training will continue without test')
×
599

600
            ## Split into train, validation and test
601
            XY_train, XY_val, XY_test = {}, {}, {}
1✔
602
            n_samples_train = self._num_of_samples[train_dataset]
1✔
603
            XY_train = {key: torch.from_numpy(val).to(TORCH_DTYPE) for key, val in self._data[train_dataset].items()}
1✔
604
            if validation_dataset in datasets:
1✔
605
                n_samples_val = self._num_of_samples[validation_dataset]
1✔
606
                XY_val = {key: torch.from_numpy(val).to(TORCH_DTYPE) for key, val in
1✔
607
                          self._data[validation_dataset].items()}
608
            if test_dataset in datasets:
1✔
609
                n_samples_test = self._num_of_samples[test_dataset]
1✔
610
                XY_test = {key: torch.from_numpy(val).to(TORCH_DTYPE) for key, val in self._data[test_dataset].items()}
1✔
611

612
        for key in XY_train.keys():
1✔
613
            assert n_samples_train == XY_train[key].shape[
1✔
614
                0], f'The number of train samples {n_samples_train}!={XY_train[key].shape[0]} not compliant.'
615
            if key in XY_val:
1✔
616
                assert n_samples_val == XY_val[key].shape[
1✔
617
                    0], f'The number of val samples {n_samples_val}!={XY_val[key].shape[0]} not compliant.'
618
            if key in XY_test:
1✔
619
                assert n_samples_test == XY_test[key].shape[
1✔
620
                    0], f'The number of test samples {n_samples_test}!={XY_test[key].shape[0]} not compliant.'
621

622
        assert n_samples_train > 0, f'There are {n_samples_train} samples for training.'
1✔
623
        self.run_training_params['n_samples_train'] = n_samples_train
1✔
624
        self.run_training_params['n_samples_val'] = n_samples_val
1✔
625
        self.run_training_params['n_samples_test'] = n_samples_test
1✔
626
        train_batch_size, val_batch_size, test_batch_size = self.__get_batch_sizes(train_batch_size, val_batch_size,
1✔
627
                                                                                   test_batch_size)
628

629
        ## Define the optimizer
630
        optimizer = self.__inizilize_optimizer(optimizer, optimizer_params, optimizer_defaults, add_optimizer_params,
1✔
631
                                               add_optimizer_defaults, models, lr, lr_param)
632
        self.run_training_params['optimizer'] = optimizer.name
1✔
633
        self.run_training_params['optimizer_params'] = optimizer.optimizer_params
1✔
634
        self.run_training_params['optimizer_defaults'] = optimizer.optimizer_defaults
1✔
635
        self.__optimizer = optimizer.get_torch_optimizer()
1✔
636

637
        ## Get num_of_epochs
638
        num_of_epochs = self.__get_parameter(num_of_epochs=num_of_epochs)
1✔
639

640
        ## Define the loss functions
641
        minimize_gain = self.__get_parameter(minimize_gain=minimize_gain)
1✔
642
        self.run_training_params['minimizers'] = {}
1✔
643
        for name, values in self._model_def['Minimizers'].items():
1✔
644
            self.__loss_functions[name] = CustomLoss(values['loss'])
1✔
645
            self.run_training_params['minimizers'][name] = {}
1✔
646
            self.run_training_params['minimizers'][name]['A'] = values['A']
1✔
647
            self.run_training_params['minimizers'][name]['B'] = values['B']
1✔
648
            self.run_training_params['minimizers'][name]['loss'] = values['loss']
1✔
649
            if name in minimize_gain:
1✔
650
                self.run_training_params['minimizers'][name]['gain'] = minimize_gain[name]
1✔
651

652
        ## Clean the dict of the training parameter
653
        del self.run_training_params['minimize_gain']
1✔
654
        del self.run_training_params['lr']
1✔
655
        del self.run_training_params['lr_param']
1✔
656
        if not recurrent_train:
1✔
657
            del self.run_training_params['connect']
1✔
658
            del self.run_training_params['closed_loop']
1✔
659
            del self.run_training_params['step']
1✔
660
            del self.run_training_params['prediction_samples']
1✔
661
        if early_stopping is None:
1✔
662
            del self.run_training_params['early_stopping']
1✔
663
            del self.run_training_params['early_stopping_params']
1✔
664

665
        ## Create the train, validation and test loss dictionaries
666
        train_losses, val_losses = {}, {}
1✔
667
        for key in self._model_def['Minimizers'].keys():
1✔
668
            train_losses[key] = []
1✔
669
            if n_samples_val > 0:
1✔
670
                val_losses[key] = []
1✔
671

672
        ## Check the needed keys are in the datasets
673
        keys = set(self._model_def['Inputs'].keys())
1✔
674
        keys |= {value['A'] for value in self._model_def['Minimizers'].values()} | {value['B'] for value in
1✔
675
                                                                                   self._model_def[
676
                                                                                       'Minimizers'].values()}
677
        keys -= set(self._model_def['Relations'].keys())
1✔
678
        keys -= set(self._model_def['States'].keys())
1✔
679
        keys -= set(self._model_def['Outputs'].keys())
1✔
680
        if 'connect' in self.run_training_params:
1✔
681
            keys -= set(self.run_training_params['connect'].keys())
1✔
682
        if 'closed_loop' in self.run_training_params:
1✔
683
            keys -= set(self.run_training_params['closed_loop'].keys())
1✔
684
        check(set(keys).issubset(set(XY_train.keys())), KeyError,
1✔
685
              f"Not all the mandatory keys {keys} are present in the training dataset {set(XY_train.keys())}.")
686

687
        # Evaluate the number of update for epochs and the unsued samples
688
        if recurrent_train:
1✔
689
            list_of_batch_indexes = range(0, (n_samples_train - train_batch_size - prediction_samples + 1),
1✔
690
                                          (train_batch_size + step))
691
            check(n_samples_train - train_batch_size - prediction_samples + 1 > 0, ValueError,
1✔
692
                  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}.")
693
            update_per_epochs = (n_samples_train - train_batch_size - prediction_samples + 1) // (
1✔
694
                        train_batch_size + step) + 1
695
            unused_samples = n_samples_train - list_of_batch_indexes[-1] - train_batch_size - prediction_samples
1✔
696

697
            model_inputs = list(self._model_def['Inputs'].keys())
1✔
698
            state_closed_loop = [key for key, value in self._model_def['States'].items() if
1✔
699
                                 'closedLoop' in value.keys()] + list(closed_loop.keys())
700
            state_connect = [key for key, value in self._model_def['States'].items() if
1✔
701
                             'connect' in value.keys()] + list(connect.keys())
702
            non_mandatory_inputs = state_closed_loop + state_connect
1✔
703
            mandatory_inputs = list(set(model_inputs) - set(non_mandatory_inputs))
1✔
704

705
            list_of_batch_indexes_train, train_step = self.__get_batch_indexes(train_dataset_name, n_samples_train,
1✔
706
                                                                               prediction_samples, train_batch_size,
707
                                                                               step, type='train')
708
            if n_samples_val > 0:
1✔
709
                list_of_batch_indexes_val, val_step = self.__get_batch_indexes(val_dataset_name, n_samples_val,
1✔
710
                                                                               prediction_samples, val_batch_size, step,
711
                                                                               type='val')
712
        else:
713
            update_per_epochs = (n_samples_train - train_batch_size) // train_batch_size + 1
1✔
714
            unused_samples = n_samples_train - update_per_epochs * train_batch_size
1✔
715

716
        self.run_training_params['update_per_epochs'] = update_per_epochs
1✔
717
        self.run_training_params['unused_samples'] = unused_samples
1✔
718

719
        ## Set the gradient to true if necessary
720
        json_inputs = self._model_def['Inputs'] | self._model_def['States']
1✔
721
        for key in json_inputs.keys():
1✔
722
            if 'type' in json_inputs[key]:
1✔
723
                if key in XY_train:
1✔
724
                    XY_train[key].requires_grad_(True)
1✔
725
                if key in XY_val:
1✔
726
                    XY_val[key].requires_grad_(True)
1✔
727
                if key in XY_test:
1✔
728
                    XY_test[key].requires_grad_(True)
1✔
729

730
        ## Select the model
731
        select_model = self.__get_parameter(select_model=select_model)
1✔
732
        select_model_params = self.__get_parameter(select_model_params=select_model_params)
1✔
733
        selected_model_def = ModelDef(self._model_def.getJson())
1✔
734

735
        ## Show the training parameters
736
        self.visualizer.showTrainParams()
1✔
737

738
        import time
1✔
739
        ## start the train timer
740
        start = time.time()
1✔
741
        self.visualizer.showStartTraining()
1✔
742

743
        for epoch in range(num_of_epochs):
1✔
744
            ## TRAIN
745
            self._model.train()
1✔
746
            if recurrent_train:
1✔
747
                losses = self.__recurrentTrain(XY_train, list_of_batch_indexes_train, train_batch_size, minimize_gain,
1✔
748
                                               closed_loop, connect, prediction_samples, train_step,
749
                                               non_mandatory_inputs, mandatory_inputs, shuffle=shuffle_data, train=True)
750
            else:
751
                losses = self.__Train(XY_train, n_samples_train, train_batch_size, minimize_gain, shuffle=shuffle_data,
1✔
752
                                      train=True)
753
            ## save the losses
754
            for ind, key in enumerate(self._model_def['Minimizers'].keys()):
1✔
755
                train_losses[key].append(torch.mean(losses[ind]).tolist())
1✔
756

757
            if n_samples_val > 0:
1✔
758
                ## VALIDATION
759
                self._model.eval()
1✔
760
                if recurrent_train:
1✔
761
                    losses = self.__recurrentTrain(XY_val, list_of_batch_indexes_val, val_batch_size, minimize_gain,
1✔
762
                                                   closed_loop, connect, prediction_samples, val_step,
763
                                                   non_mandatory_inputs, mandatory_inputs, shuffle=False, train=False)
764
                else:
765
                    losses = self.__Train(XY_val, n_samples_val, val_batch_size, minimize_gain, shuffle=False,
1✔
766
                                          train=False)
767
                ## save the losses
768
                for ind, key in enumerate(self._model_def['Minimizers'].keys()):
1✔
769
                    val_losses[key].append(torch.mean(losses[ind]).tolist())
1✔
770

771
            ## Early-stopping
772
            if callable(early_stopping):
1✔
NEW
773
                if early_stopping(train_losses, val_losses, early_stopping_params):
×
NEW
774
                    log.info(f'Stopping the training at epoch {epoch} due to early stopping.')
×
NEW
775
                    break
×
776

777
            if callable(select_model):
1✔
NEW
778
                if select_model(train_losses, val_losses, select_model_params):
×
NEW
779
                    best_model_epoch = epoch
×
NEW
780
                    selected_model_def.updateParameters(self._model)
×
781

782
            ## Visualize the training...
783
            self.visualizer.showTraining(epoch, train_losses, val_losses)
1✔
784
            self.visualizer.showWeightsInTrain(epoch=epoch)
1✔
785

786
        ## Save the training time
787
        end = time.time()
1✔
788
        ## Visualize the training time
789
        for key in self._model_def['Minimizers'].keys():
1✔
790
            self._training[key] = {'train': train_losses[key]}
1✔
791
            if n_samples_val > 0:
1✔
792
                self._training[key]['val'] = val_losses[key]
1✔
793
        self.visualizer.showEndTraining(num_of_epochs - 1, train_losses, val_losses)
1✔
794
        self.visualizer.showTrainingTime(end - start)
1✔
795

796
        ## Select the model
797
        if callable(select_model):
1✔
NEW
798
            log.info(f'Selected the model at the epoch {best_model_epoch + 1}.')
×
NEW
799
            self._model = Model(selected_model_def)
×
800
        else:
801
            log.info('The selected model is the LAST model of the training.')
1✔
802

803
        self.resultAnalysis(train_dataset, XY_train, minimize_gain, closed_loop, connect, prediction_samples, step,
1✔
804
                            train_batch_size)
805
        if self.run_training_params['n_samples_val'] > 0:
1✔
806
            self.resultAnalysis(validation_dataset, XY_val, minimize_gain, closed_loop, connect, prediction_samples,
1✔
807
                                step, val_batch_size)
808
        if self.run_training_params['n_samples_test'] > 0:
1✔
809
            self.resultAnalysis(test_dataset, XY_test, minimize_gain, closed_loop, connect, prediction_samples, step,
1✔
810
                                test_batch_size)
811

812
        self.visualizer.showResults()
1✔
813

814
        ## Get trained model from torch and set the model_def
815
        self._model_def.updateParameters(self._model)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc