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

tonegas / nnodely / 14319828903

07 Apr 2025 09:27PM UTC coverage: 97.259% (+0.2%) from 97.035%
14319828903

Pull #86

github

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

2275 of 2409 new or added lines in 54 files covered. (94.44%)

1 existing line in 1 file now uncovered.

11637 of 11965 relevant lines covered (97.26%)

0.97 hits per line

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

96.91
/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.operators.memory import Memory
1✔
11
from nnodely.basic.relation import Stream
1✔
12
from nnodely.layers.output import Output
1✔
13

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

155
        return optimizer
1✔
156

157

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

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

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

192
        return batch_indexes, clipped_step
1✔
193

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

233

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

813
        self.visualizer.showResults()
1✔
814

815
        ## Get trained model from torch and set the model_def
816
        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