BUCToolkit 1.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
  2. BUCToolkit/BatchGenerate/__init__.py +6 -0
  3. BUCToolkit/BatchGenerate/coords_interp.py +127 -0
  4. BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
  5. BUCToolkit/BatchGenerate/random_replace.py +88 -0
  6. BUCToolkit/BatchMC/MetropolisMC.py +210 -0
  7. BUCToolkit/BatchMC/_BaseMC.py +863 -0
  8. BUCToolkit/BatchMC/__init__.py +8 -0
  9. BUCToolkit/BatchMD/BiasedMD.py +276 -0
  10. BUCToolkit/BatchMD/NVE.py +92 -0
  11. BUCToolkit/BatchMD/NVT.py +510 -0
  12. BUCToolkit/BatchMD/_BaseMD.py +1251 -0
  13. BUCToolkit/BatchMD/__init__.py +18 -0
  14. BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
  15. BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
  16. BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
  17. BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
  18. BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
  19. BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
  20. BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
  21. BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
  22. BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
  23. BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
  24. BUCToolkit/BatchOptim/TS/__init__.py +10 -0
  25. BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
  26. BUCToolkit/BatchOptim/__init__.py +22 -0
  27. BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
  28. BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
  29. BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
  30. BUCToolkit/BatchOptim/frequency.py +496 -0
  31. BUCToolkit/BatchOptim/minimize/CG.py +256 -0
  32. BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
  33. BUCToolkit/BatchOptim/minimize/QN.py +205 -0
  34. BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
  35. BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
  36. BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
  37. BUCToolkit/BatchStructures/__init__.py +15 -0
  38. BUCToolkit/Preprocessing/__init__.py +7 -0
  39. BUCToolkit/Preprocessing/load_files.py +1680 -0
  40. BUCToolkit/Preprocessing/preprocessing.py +923 -0
  41. BUCToolkit/Preprocessing/sampling.py +299 -0
  42. BUCToolkit/Preprocessing/supercells.py +140 -0
  43. BUCToolkit/Preprocessing/write_files.py +524 -0
  44. BUCToolkit/StandardTemplate.py +417 -0
  45. BUCToolkit/__init__.py +33 -0
  46. BUCToolkit/_version.py +7 -0
  47. BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
  48. BUCToolkit/api/DataLoaders.py +321 -0
  49. BUCToolkit/api/Losses.py +170 -0
  50. BUCToolkit/api/Metrics.py +84 -0
  51. BUCToolkit/api/ModelOptims.py +296 -0
  52. BUCToolkit/api/MolecularDynamics.py +282 -0
  53. BUCToolkit/api/MonteCarlo.py +280 -0
  54. BUCToolkit/api/NEB.py +325 -0
  55. BUCToolkit/api/Predictor.py +292 -0
  56. BUCToolkit/api/StructureOptimization.py +484 -0
  57. BUCToolkit/api/Trainer.py +635 -0
  58. BUCToolkit/api/VibrationAnalysis.py +279 -0
  59. BUCToolkit/api/__init__.py +38 -0
  60. BUCToolkit/api/_io.py +1026 -0
  61. BUCToolkit/cli/__init__.py +20 -0
  62. BUCToolkit/cli/cli.py +934 -0
  63. BUCToolkit/cli/input_stub.py +140 -0
  64. BUCToolkit/cli/main.py +368 -0
  65. BUCToolkit/cli/print_logo.py +149 -0
  66. BUCToolkit/cli/template_model.py +6 -0
  67. BUCToolkit/io.py +30 -0
  68. BUCToolkit/utils/AtomSelector.py +64 -0
  69. BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
  70. BUCToolkit/utils/ElemListReduce.py +52 -0
  71. BUCToolkit/utils/Ewald.py +210 -0
  72. BUCToolkit/utils/IrregularTensorReformat.py +59 -0
  73. BUCToolkit/utils/_CheckModules.py +30 -0
  74. BUCToolkit/utils/_Element_info.py +527 -0
  75. BUCToolkit/utils/__init__.py +22 -0
  76. BUCToolkit/utils/_para_flatt_list.py +131 -0
  77. BUCToolkit/utils/_print_formatter.py +59 -0
  78. BUCToolkit/utils/check_structures.py +204 -0
  79. BUCToolkit/utils/function_utils.py +147 -0
  80. BUCToolkit/utils/grad_functions.py +69 -0
  81. BUCToolkit/utils/index_ops.py +432 -0
  82. BUCToolkit/utils/setup_loggers.py +137 -0
  83. buctoolkit-1.0b0.dist-info/METADATA +28 -0
  84. buctoolkit-1.0b0.dist-info/RECORD +88 -0
  85. buctoolkit-1.0b0.dist-info/WHEEL +5 -0
  86. buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
  87. buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
  88. buctoolkit-1.0b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,635 @@
1
+ """ Training Methods """
2
+
3
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
4
+ # Authors: Pu Pengxin, Song Xin
5
+ # Version: 0.9a
6
+ # File: Trainer.py
7
+ # Environment: Python 3.12
8
+
9
+ import copy
10
+ import logging
11
+ import os
12
+ import re
13
+ import time
14
+ from math import ceil
15
+ from typing import Dict, Tuple, Literal, List, Any, Callable, Optional
16
+ import traceback
17
+ from inspect import isclass
18
+
19
+ import numpy as np
20
+ import torch as th
21
+ from torch import nn
22
+ from torch.nn import functional as F
23
+ from torch.optim.lr_scheduler import (StepLR, ExponentialLR, ChainedScheduler, ConstantLR, CyclicLR, MultiStepLR,
24
+ LambdaLR, LinearLR, CosineAnnealingWarmRestarts, CosineAnnealingLR)
25
+
26
+ from .Losses import Energy_Force_Loss, Energy_Loss
27
+ from .Metrics import E_MAE, E_R2, F_MAE, F_MaxE, _r2_score, _rmse
28
+ from .ModelOptims import FIRELikeOptimizer, LangevinOptimizer
29
+ from ._io import _CONFIGS, _LoggingEnd, ExpMovingAverage
30
+
31
+
32
+ class Trainer(_CONFIGS):
33
+ r"""
34
+ The model trainer class.
35
+ Users need to set the dataset and dataloader manually.
36
+
37
+ Args:
38
+ config_file: the path of input file
39
+
40
+ Methods:
41
+ train(model: torch.nn.Module), run model training.
42
+ set_device(device: str | torch.device), manually set model and data running device.
43
+ set_loss_fn(loss_fn: Any, loss_config: Optional[Dict] = None), manually set loss function which can be customized.
44
+ set_metrics(metrics_fn: Dict[str, Callable], metrics_fn_config: Dict[str, Dict] | None = None), manually set metrics functions which can be customized.
45
+ set_model_config(model_config: Dict[str, Any] | None = None), manually (re)set the configs (hyperparameters) of model.
46
+ set_lr_scheduler(lr_scheduler: th.optim.lr_scheduler.LRScheduler, lr_scheduler_config: Optional[Dict[str, Any]] = None), set the learning rate scheduler.
47
+ set_model_param(model_state_dict: Dict, is_strict: bool = True, is_assign: bool = False), manually set model parameters by giving model state dict.
48
+
49
+ """
50
+
51
+ def __init__(self, config_file: str, *args) -> None:
52
+ super().__init__(config_file)
53
+
54
+ self.config_file = config_file
55
+ self.reload_config(config_file)
56
+ if self.VERBOSE: self.logger.info('Config File Was Successfully Read.')
57
+ self.param = None
58
+ self._has_load_data = False
59
+ self._data_loader = None
60
+ self._layerwise_opt_configs = None
61
+
62
+ # CONSTANT of training information
63
+ self._OPTIM_DICT = {
64
+ 'Adam': th.optim.Adam, 'SGD': th.optim.SGD, 'AdamW': th.optim.AdamW, 'Adadelta': th.optim.Adadelta,
65
+ 'Adagrad': th.optim.Adagrad, 'ASGD': th.optim.ASGD, 'Adamax': th.optim.Adamax,
66
+ 'FIRE': FIRELikeOptimizer, 'Langevin': LangevinOptimizer,
67
+ 'custom': None
68
+ }
69
+ self._LR_SCHEDULER_DICT = {
70
+ 'StepLR': StepLR, 'ExponentialLR': ExponentialLR, 'ChainedScheduler': ChainedScheduler,
71
+ 'ConstantLR': ConstantLR, 'LambdaLR': LambdaLR, 'LinearLR': LinearLR, 'CosineAnnealingLR': CosineAnnealingLR,
72
+ 'CosineAnnealingWarmRestarts': CosineAnnealingWarmRestarts, 'CyclicLR': CyclicLR, 'MultiStepLR': MultiStepLR,
73
+ 'None': None, 'custom': None
74
+ }
75
+ self._LOSS_DICT = {
76
+ 'MSE': nn.MSELoss, 'MAE': nn.L1Loss, 'Hubber': nn.HuberLoss, 'CrossEntropy': nn.CrossEntropyLoss,
77
+ 'Energy_Force_Loss': Energy_Force_Loss, 'Energy_Loss': Energy_Loss, 'custom': None
78
+ }
79
+ self._METRICS_DICT = {
80
+ 'MSE': F.mse_loss, 'MAE': F.l1_loss, 'R2': _r2_score, 'RMSE': _rmse,
81
+ 'E_MAE': E_MAE, 'E_R2': E_R2, 'F_MAE': F_MAE, 'F_MaxE': F_MaxE, 'custom': None
82
+ }
83
+
84
+ # Section: parse train parameters
85
+ trn_config = self.config.get('TRAIN', None)
86
+ if trn_config is None:
87
+ self.logger.critical('** ERROR: Training task is required without setting args `TRAIN`. Task aborted. BYE!!! **')
88
+ raise RuntimeError('** ERROR: Training task is required without setting args `TRAIN`. Task aborted. BYE!!! **')
89
+
90
+ # epoches & validation set
91
+ self.EPOCH: int = int(trn_config.get('EPOCH', 0))
92
+ self.ACCUMULATE_STEP = trn_config.get('ACCUMULATE_STEP', 1)
93
+ if not (isinstance(self.ACCUMULATE_STEP, int) and self.ACCUMULATE_STEP > 0):
94
+ raise TypeError(f'ACCUMULATE_STEP must be a positive integer, but got {self.ACCUMULATE_STEP}')
95
+ self.VAL_PER_STEP: int = int(trn_config.get('VAL_PER_STEP', 10))
96
+ self.VAL_BATCH_SIZE: int = int(trn_config.get('VAL_BATCH_SIZE', self.BATCH_SIZE))
97
+ self.VAL_IF_TRN_LOSS_BELOW: float = float(trn_config.get('VAL_IF_TRN_LOSS_BELOW', th.inf))
98
+
99
+ # optim info
100
+ optim_name = trn_config.get('OPTIM', None)
101
+ if optim_name is not None: # lazy to check. One may set/reset it by `self.set_optimizer`
102
+ self.OPTIMIZER = self._OPTIM_DICT.get(optim_name, None)
103
+ else:
104
+ self.OPTIMIZER = None
105
+ self.OPTIM_CONFIG = trn_config.get('OPTIM_CONFIG', dict())
106
+ if not isinstance(self.OPTIM_CONFIG, Dict): raise ValueError('OPTIM_CONFIG must be a dictionary.')
107
+ layerwise_optim_conf = trn_config.get('LAYERWISE_OPTIM_CONFIG', None)
108
+ if isinstance(layerwise_optim_conf, dict):
109
+ self.set_layerwise_optim_config(layerwise_optim_conf)
110
+ elif layerwise_optim_conf is not None:
111
+ self.logger.warning(f"Invalid format of `LAYERWISE_OPTIM_CONFIG`: {layerwise_optim_conf}, thus ignoring it.")
112
+
113
+ self.GRAD_CLIP: bool = trn_config.get('GRAD_CLIP', False)
114
+ self.GRAD_CLIP_MAX_NORM: float = float(trn_config.get('GRAD_CLIP_MAX_NORM', 100))
115
+ self.GRAD_CLIP_CONFIG = trn_config.get('GRAD_CLIP_CONFIG', dict())
116
+ if not isinstance(self.GRAD_CLIP, bool): raise TypeError('GRAD_CLIP must be a boolean.')
117
+ if not isinstance(self.GRAD_CLIP_CONFIG, Dict): raise ValueError('GRAD_CLIP_CONFIG must be a dictionary.')
118
+
119
+ lr_scheduler_name = trn_config.get('LR_SCHEDULER', None)
120
+ if lr_scheduler_name is not None:
121
+ self.LR_SCHEDULER = self._LR_SCHEDULER_DICT.get(lr_scheduler_name, None)
122
+ if lr_scheduler_name not in self._LR_SCHEDULER_DICT:
123
+ self.logger.warning(f"Unknown lr scheduler name: {lr_scheduler_name}. Learning rate scheduler will be ignored.")
124
+ else:
125
+ self.LR_SCHEDULER = None
126
+ self.LR_SCHEDULER_CONFIG = trn_config.get('LR_SCHEDULER_CONFIG', dict())
127
+ if not isinstance(self.LR_SCHEDULER_CONFIG, Dict): raise TypeError('LR_SCHEDULER_CONFIG must be a dict.')
128
+
129
+ self.EMA = trn_config.get('EMA', False) # exponential moving average strategy. best_checkpoint saves using ema param, and others do not.
130
+ self.EMA_DECAY = float(trn_config.get('EMA_DECAY', 0.999))
131
+
132
+ # loss & criterion info
133
+ self.loss_name = trn_config.get('LOSS', None)
134
+ if self.loss_name is not None:
135
+ self.LOSS = self._LOSS_DICT.get(self.loss_name, None)
136
+ else:
137
+ self.LOSS = None
138
+ self.LOSS_CONFIG = trn_config.get('LOSS_CONFIG', dict())
139
+ if not isinstance(self.LOSS_CONFIG, Dict): raise TypeError('LOSS_CONFIG must be a dictionary.')
140
+ _metric_name = trn_config.get('METRICS', tuple())
141
+ self.METRICS_CONFIG = trn_config.get('METRICS_CONFIG', dict())
142
+ if not isinstance(_metric_name, (List, Tuple)): raise TypeError(f'METRICS must be a sequence, but occurred {type(_metric_name)}')
143
+ if not isinstance(self.METRICS_CONFIG, Dict): raise TypeError(f'METRICS_CONFIG must be a dict, but occurred {type(self.METRICS_CONFIG)}')
144
+ self.METRICS = dict()
145
+ for __metric in _metric_name:
146
+ if __metric not in self._METRICS_DICT:
147
+ self.METRICS[__metric] = None
148
+ self.METRICS_CONFIG[__metric] = dict()
149
+ elif __metric not in self.METRICS_CONFIG:
150
+ self.METRICS[__metric] = self._METRICS_DICT[__metric]
151
+ self.METRICS_CONFIG[__metric] = dict()
152
+ else:
153
+ self.METRICS[__metric] = self._METRICS_DICT[__metric]
154
+
155
+ def set_loss_fn(self, loss_fn, loss_config: Optional[Dict] = None) -> None:
156
+ """
157
+ Reset loss function, and reset configs of loss function optionally.
158
+ parameters:
159
+ loss_fn: uninstantiated class torch.nn.Module, a user-defind loss function.
160
+ loss_config: Dict[str, Any]|None, the new configs of given loss function. if None, loss_config would not change.
161
+ """
162
+ if loss_config is None:
163
+ pass
164
+ elif not isinstance(loss_config, Dict):
165
+ raise TypeError('loss_config must be a dictionary.')
166
+ else:
167
+ self.LOSS_CONFIG = loss_config
168
+ self.LOSS = loss_fn
169
+
170
+ def set_metrics(self, metrics_fn: Dict[str, Callable], metrics_fn_config: Dict[str, Dict] | None = None):
171
+ """
172
+ Set user-defined metrics function.
173
+ Parameters:
174
+ metrics_fn: Dict[str, Callable], str is the name of metrics function.
175
+ metrics_fn_config: Dict[str, Dict]|None, the configs of metrics function corresponding to the function name str.
176
+ """
177
+ if metrics_fn_config is None: metrics_fn_config = dict()
178
+ for _key in metrics_fn.keys():
179
+ if _key not in metrics_fn_config:
180
+ metrics_fn_config[_key] = dict()
181
+ self.METRICS.update(metrics_fn)
182
+ self.METRICS_CONFIG.update(metrics_fn_config)
183
+
184
+ def set_lr_scheduler(self, lr_scheduler, lr_scheduler_config: Optional[Dict[str, Any]] = None) -> None:
185
+ """
186
+ Set the lr_scheduler that inherit from torch.optim.lr_scheduler.LRScheduler
187
+ """
188
+ self.LR_SCHEDULER = lr_scheduler
189
+ if lr_scheduler_config is not None:
190
+ self.LR_SCHEDULER_CONFIG = lr_scheduler_config
191
+
192
+ def set_optimizer(self, optimizer, optim_config: Optional[Dict] = None) -> None:
193
+ r"""
194
+ Set the optimizer that inherit from torch.optim.Optimizer, and reset optimizer configs optionally.
195
+ parameters:
196
+ optimizer: torch.optim.Optimizer, a user-defind optimizer.
197
+ optim_config: Dict[str, Any]|None, the new configs of given optimizer. if None, optim_config would not change.
198
+ """
199
+ if optim_config is None:
200
+ pass
201
+ elif not isinstance(optim_config, Dict):
202
+ raise TypeError('optim_config must be a dictionary.')
203
+ else:
204
+ self.OPTIM_CONFIG = optim_config
205
+ self.OPTIMIZER = optimizer
206
+
207
+ def set_layerwise_optim_config(self, layer_config_dict: Dict[str, Dict[str, Any]] | None = None):
208
+ """
209
+ The optimizer configs of layers in `layer_config_dict.keys()` would set to the corresponding values,
210
+ and other unspecified layers would use the config of `OPTIM_CONFIG` in the input file.
211
+ The parameters of the layer which lr is set to `None` would be fixed during training without calculating gradients.
212
+ The name of layers can be specified by regular expressions e.g.,
213
+ {"fc1.*": {"lr": 1e-4, "weight_decay": 1e-4}, "fc2.[a-zA-Z]+Norm.*": {"lr": None}}
214
+
215
+ Args:
216
+ layer_config_dict: dict of named layers' learning config: {layer name: {'lr': ...}}.
217
+
218
+ Returns: None
219
+
220
+ """
221
+ if layer_config_dict is not None:
222
+ if not isinstance(layer_config_dict, Dict):
223
+ self.logger.exception(f'ERROR: Expected `layer_lr_dict` is a Dict, but got {type(layer_config_dict)}')
224
+ raise TypeError(f'Expected `layer_lr_dict` is a Dict, but got {type(layer_config_dict)}')
225
+ # fixed_parameter_dict: {name: name length before glob '*'}
226
+ self._layerwise_opt_configs = {re.compile(nam): val for nam, val in layer_config_dict.items()}
227
+ else:
228
+ self._layerwise_opt_configs = None
229
+
230
+ def train(self, model):
231
+ r"""
232
+ Start Training.
233
+
234
+ Herein the input model must be an `uninstantiated` nn.Module class.
235
+ """
236
+ # check logger
237
+ if not self.logger.hasHandlers(): self.logger.addHandler(self.log_handler)
238
+ # check vars
239
+ if not isclass(model):
240
+ raise TypeError('`model` must be a class. You may not instantiate it.')
241
+ _model: nn.Module = model(**self.MODEL_CONFIG)
242
+ if self.START != 'from_scratch' and self.START != 0:
243
+ chk_data = th.load(self.LOAD_CHK_FILE_PATH, weights_only=True)
244
+ if self.param is None:
245
+ _model.load_state_dict(chk_data['model_state_dict'], strict=self.STRICT_LOAD)
246
+ else:
247
+ _model.load_state_dict(self.param, self.is_strict, self.is_assign)
248
+ if self.START == 'resume' or self.START == 1:
249
+ epoch_now = chk_data['epoch']
250
+ val_loss_old = chk_data['val_loss'] if isinstance(chk_data['val_loss'], float) else chk_data['val_loss'][0]
251
+ elif self.START == 'param_only' or self.START == 2:
252
+ epoch_now = 0
253
+ val_loss_old = th.inf
254
+ else:
255
+ self.logger.exception(f'ERROR: Invalid `START` value {self.START}. It should be "from_scratch" / 0 , "resume" / 1 , "param_only" / 2')
256
+ raise ValueError(f'Invalid `START` value {self.START}. It should be "from_scratch" / 0 , "resume" / 1 , "param_only" / 2')
257
+ elif self.START == 'from_scratch' or self.START == 0:
258
+ epoch_now = 0
259
+ chk_data = None
260
+ if self.param is not None:
261
+ _model.load_state_dict(self.param, self.is_strict, self.is_assign)
262
+ val_loss_old = th.inf
263
+ else:
264
+ self.logger.exception(f'ERROR: Invalid parameter of `START`: {self.START}, it must be 0, 1, or 2.')
265
+ raise ValueError(f'Invalid parameter of `START`: {self.START}, it must be 0, 1, or 2.')
266
+
267
+ # model vars
268
+ _model = _model.to(self.DEVICE)
269
+ _model.requires_grad_()
270
+ # loss & eval vars
271
+ if self.LOSS is None:
272
+ self.logger.exception('ERROR: the loss function is None. Please set the loss function.')
273
+ raise RuntimeError('the loss function is None. Please set the loss function.')
274
+ else:
275
+ LOSS = self.LOSS(**self.LOSS_CONFIG)
276
+ LOSS = LOSS.to(self.DEVICE)
277
+ if None in self.METRICS.values():
278
+ self.logger.exception('ERROR: Occurred an unknown metric function. Please use self.set_metrics to specify it.')
279
+ raise RuntimeError('Occurred an unknown metric function. Please use self.set_metrics to specify it.')
280
+
281
+ # check layer-wise optimizer config
282
+ if self._layerwise_opt_configs is not None:
283
+ # classifying
284
+ group_conf_list = list() # List[Dict[opt config]]
285
+ group_id_set = list() # List[Set[layer params id]], has the same order as `group_conf_list`.
286
+ rest_group_ids = set(id(_) for _ in _model.parameters())
287
+ fixed_param_ids = set()
288
+ for parttern, conf in self._layerwise_opt_configs.items():
289
+ _group_id_set = set()
290
+ for para_name, para in _model.named_parameters():
291
+ q_name_len = re.match(parttern, para_name)
292
+ if q_name_len is not None:
293
+ if conf['lr'] is None:
294
+ para.requires_grad_(False) # default self.is_invert_fix_param = False, that to turn off the grad.
295
+ fixed_param_ids.add(id(para))
296
+ else:
297
+ _group_id_set.add(id(para))
298
+ group_id_set.append(_group_id_set)
299
+ group_conf_list.append(conf)
300
+ rest_group_ids.difference_update(_group_id_set)
301
+ rest_group_ids.difference_update(fixed_param_ids)
302
+ del para_name, para, conf, parttern, q_name_len, _group_id_set, fixed_param_ids
303
+ # arrange groups
304
+ parameter_iterator = list()
305
+ for ii, id_set in enumerate(group_id_set):
306
+ par_conf = {'params': [_ for _ in _model.parameters() if id(_) in id_set]}
307
+ par_conf.update(group_conf_list[ii])
308
+ parameter_iterator.append(par_conf)
309
+ par_conf = {'params': filter(lambda p: id(p) in rest_group_ids, _model.parameters())}
310
+ parameter_iterator.append(par_conf)
311
+ del ii, id_set, par_conf
312
+ else:
313
+ parameter_iterator = _model.parameters()
314
+
315
+ # optim vars
316
+ if self.OPTIMIZER is None:
317
+ self.logger.exception('ERROR: the optimizer is None. Please set the optimizer.')
318
+ raise RuntimeError('the optimizer is None. Please set the optimizer.')
319
+ else:
320
+ OPTIMIZER = self.OPTIMIZER(parameter_iterator, **self.OPTIM_CONFIG)
321
+ if self.START == 'resume' or self.START == 1:
322
+ OPTIMIZER.load_state_dict(chk_data['optimizer_state_dict'])
323
+
324
+ if self.LR_SCHEDULER is not None:
325
+ scheduler = self.LR_SCHEDULER(OPTIMIZER, last_epoch=(epoch_now - 1), **self.LR_SCHEDULER_CONFIG)
326
+ if self.START == 'resume' or self.START == 1:
327
+ scheduler.load_state_dict(chk_data['lr_scheduler_state_dict'])
328
+ else:
329
+ scheduler = None
330
+
331
+ if self.EMA:
332
+ ema = ExpMovingAverage(_model, self.EMA_DECAY)
333
+ else:
334
+ ema = None
335
+
336
+ # preprocessing data # TODO
337
+ if self._data_loader is None:
338
+ self.logger.exception('ERROR: Please Set the DataLoader.')
339
+ raise RuntimeError('Please Set the DataLoader.')
340
+ if not self._has_load_data:
341
+ self.logger.exception('ERROR: Please Set the Training Data and Validation Data.')
342
+ raise RuntimeError('Please Set the Training Data and Validation Data')
343
+
344
+ # initialize
345
+ n_trn_samp = len(self.TRAIN_DATA['data']) # sample number
346
+ n_val_samp = len(self.VALID_DATA['data'])
347
+ if n_val_samp < 1:
348
+ self.logger.warning(
349
+ 'WARNING: Validation sample is empty, so all validations would be skipped. I HOPE YOU KNOW WHAT YOU ARE DOING!'
350
+ )
351
+ self.VAL_IF_TRN_LOSS_BELOW = th.nan # set it to nan to avoid validation.
352
+ n_batch = ceil(n_trn_samp / self.BATCH_SIZE) # total batch number per epoch
353
+ history: Dict[Literal['train_loss', 'val_loss'], List[float]] = {'train_loss': list(), 'val_loss': list()}
354
+ OPTIMIZER.zero_grad()
355
+ i = epoch_now
356
+ if not os.path.isdir(self.CHK_SAVE_PATH):
357
+ os.makedirs(self.CHK_SAVE_PATH, )
358
+ if not os.path.isdir(self.CHK_SAVE_PATH):
359
+ os.makedirs(self.CHK_SAVE_PATH, )
360
+
361
+ try:
362
+ # I/O
363
+ if self.VERBOSE > 0:
364
+ self.logout_task_information(_model, 'TRAIN', None, n_trn_samp + n_val_samp)
365
+
366
+ # MAIN LOOP
367
+ if self.DEBUG_MODE:
368
+ th.autograd.set_detect_anomaly(True)
369
+ para_arr_old_list = [0. for _ in _model.named_parameters()]
370
+ time_tol = time.perf_counter()
371
+ __loss = th.inf
372
+ num_update = 0 # number of parameter updating
373
+ _can_valid = False # To avoid stacking, ensuring that validation is always occurred after param. update.
374
+ nan_count = 0 # number of nan occurred.
375
+ n_err = 0 # error number during training.
376
+ for i in range(epoch_now, self.EPOCH):
377
+ time_ep = time.perf_counter()
378
+ real_n_samp = 0
379
+ # load the training set
380
+ trn_set = self._data_loader(self.TRAIN_DATA, self.BATCH_SIZE, self.DEVICE, **self._data_loader_configs) # TODO
381
+
382
+ self.logger.info('-' * 60 + f'EPOCH {i + 1}' + '-' * 60)
383
+ # Training
384
+ num_step = 1
385
+ accum_loss = 0. # the accumulation of loss within gradient accumulation steps.
386
+ accum_step = 0 # the actual steps that accumulated. normally `accum_step` == `self.ACCUMULATE_STEP`, except the last
387
+ time_gp = time.perf_counter()
388
+ for batch_data, batch_label in trn_set:
389
+ try:
390
+ _model.train()
391
+ # to avoid get an empty batch
392
+ if not isinstance(batch_data, (th.Tensor, )):
393
+ len_data = batch_data.batch_size
394
+ else:
395
+ len_data = len(batch_data)
396
+ if len_data <= 0:
397
+ if self.VERBOSE: self.logger.info(f'An empty batch occurred in step {num_step}. Skipped.')
398
+ continue
399
+ # batch device
400
+ batch_data = batch_data.to(self.DEVICE)
401
+ #batch_label = batch_label.to(self.DEVICE)
402
+
403
+ # pred & loss, pred must be a Dict.
404
+ pred_y = _model(batch_data)
405
+ # check nan
406
+ if self.CHECK_NAN:
407
+ for key in pred_y.keys():
408
+ if isinstance(pred_y[key], th.Tensor):
409
+ is_nan = th.isnan(pred_y[key]) + th.isinf(pred_y[key])
410
+ elif isinstance(pred_y[key], list): # for ensemble wrapper model
411
+ is_nan = sum([th.isnan(_p) + th.isinf(_p) for _p in pred_y[key]])
412
+ else:
413
+ break
414
+ if th.any(is_nan):
415
+ nan_count += 1
416
+ pred_y[key] = th.where(th.isnan(pred_y[key]), 0., pred_y[key])
417
+ self.logger.warning(f'NaN occurred in model output, and has been set to 0. Total NaN number: {nan_count}')
418
+ if nan_count > 100:
419
+ raise RuntimeError(f'Too many NaNs occurred in the model output (>= 100).')
420
+ if self.DEBUG_MODE: self.logger.warning(f'batch_data:\n {batch_data}\n\nlabels:\n {batch_label}')
421
+ # backward
422
+ raw_loss:th.Tensor = LOSS(pred_y, batch_label) # loss before grad. accum.
423
+ loss = raw_loss / self.ACCUMULATE_STEP
424
+ loss.backward()
425
+ accum_loss += raw_loss.item()
426
+ accum_step += 1
427
+
428
+ _is_update_params = ((num_step % self.ACCUMULATE_STEP == 0) or (num_step == n_batch)) # reached the accum. step or last step
429
+ # Debug Mode -----------------------------------------------------------------------------------------------------------
430
+ if self.DEBUG_MODE and _is_update_params:
431
+ i__ = 0
432
+ para_arr_old_temp = list()
433
+ self.logger.info(f'\n{"Debug"*24}\nDEBUG IN STEP NUMBER: {num_step}\n{"Debug"*24}')
434
+ for para_name, para in _model.named_parameters():
435
+ para_arr = para.numpy(force=True)
436
+ para_arr_old_temp.append(para_arr)
437
+ self.logger.info('PARAMETER %s : %.3e ~ %.3e' % (para_name, np.min(para_arr), np.max(para_arr)))
438
+ if para.grad is not None:
439
+ para_grad_arr = para.grad.numpy(force=True)
440
+ self.logger.info('PARA_GRAD %s : %.3e ~ %.3e' % (para_name, np.min(para_grad_arr), np.max(para_grad_arr)))
441
+ else:
442
+ self.logger.warning(f'PARAMETER `{para_name}` does not have grad. <<<')
443
+ self.logger.info(f'PARAMETER RMSD: {np.sqrt(np.mean((para_arr - para_arr_old_list[i__]) ** 2)):<4.3e}')
444
+ i__ += 1
445
+ para_arr_old_list = para_arr_old_temp
446
+ if loss != loss:
447
+ self.logger.exception(f'ERROR: NaN occurred in loss.\n\nModel outputs:\n{pred_y}\n\nTraining labels:\n{batch_label}')
448
+ raise Exception('ERROR: NaN occurred in loss.\n\nModel outputs:\n%s\n\nTraining labels:\n%s' % (pred_y, batch_label))
449
+ # Debug Mode END-----------------------------------------------------------------------------------------------------------
450
+
451
+ # update & metrics & output
452
+ real_n_samp += len_data # sample number count
453
+ if _is_update_params:
454
+ # grad clip
455
+ if self.GRAD_CLIP:
456
+ nn.utils.clip_grad_norm_(
457
+ parameters=parameter_iterator,
458
+ max_norm=self.GRAD_CLIP_MAX_NORM,
459
+ **self.GRAD_CLIP_CONFIG
460
+ )
461
+ # update
462
+ OPTIMIZER.step()
463
+ OPTIMIZER.zero_grad()
464
+ if scheduler is not None: scheduler.step()
465
+ if self.EMA: ema.step()
466
+
467
+ # metrics
468
+ with th.no_grad():
469
+ if len(self.METRICS) > 0:
470
+ _metr_list = dict()
471
+ _model.eval()
472
+ for _name, _metr_func in self.METRICS.items():
473
+ _metr = _metr_func(pred_y, batch_label, **self.METRICS_CONFIG[_name])
474
+ _metr_list[_name] = _metr.item()
475
+ __loss = accum_loss / accum_step
476
+ history['train_loss'].append(__loss)
477
+ accum_loss = 0.
478
+ accum_step = 0
479
+ num_update += 1
480
+ _can_valid = True
481
+ # print per step
482
+ if self.VERBOSE:
483
+ with _LoggingEnd(self.log_handler):
484
+ self.logger.info(f'epoch: {i + 1:>6}, ({real_n_samp:>8d}/{n_trn_samp:>8d}), train_loss: {__loss:> 4.4e}')
485
+ if len(self.METRICS) > 0:
486
+ for _name, _metr in _metr_list.items(): # type: ignore
487
+ self.logger.info(f', {_name}: {_metr:> 4.4e}')
488
+ if scheduler is not None: self.logger.info(f', lr: {' '.join([f'{_:< 4.2e}' for _ in scheduler.get_last_lr()])}')
489
+ self.logger.info(f', time: {time.perf_counter() - time_gp:>10.4f}, [UPDATE GRAD]')
490
+
491
+ # validation
492
+ _is_start_val = (__loss < self.VAL_IF_TRN_LOSS_BELOW)
493
+ _is_step_to_val = (num_update % self.VAL_PER_STEP == 0)
494
+ #_is_at_least_val = (num_step < self.VAL_PER_STEP and num_step == n_batch) # if not val. in the whole epoch, val.
495
+ if _is_start_val and _is_step_to_val and _can_valid:
496
+ time_val = time.perf_counter()
497
+ with _LoggingEnd(self.log_handler):
498
+ if self.VERBOSE: self.logger.info('VALIDATION...')
499
+ with th.no_grad():
500
+ if self.EMA: ema.apply()
501
+ _val_loss, _metr_list = self._val(_model, LOSS)
502
+ # print val results
503
+ if _val_loss is not None:
504
+ history['val_loss'].append(_val_loss)
505
+ if self.VERBOSE:
506
+ self.logger.info('Done.')
507
+ with _LoggingEnd(self.log_handler):
508
+ self.logger.info(f'Validation loss: {_val_loss:> 4.4e}')
509
+ for _name, _metr in _metr_list.items():
510
+ self.logger.info(f', {_name}: {_metr:> 4.4e}')
511
+ self.logger.info(f', time: {time.perf_counter() - time_val:<.4f}')
512
+
513
+ if self.SAVE_CHK:
514
+ if _val_loss < val_loss_old:
515
+ with _LoggingEnd(self.log_handler):
516
+ if self.VERBOSE: self.logger.info('Validation loss descent. Saving checkpoint file...')
517
+ val_loss_old = copy.deepcopy(_val_loss)
518
+ states = {
519
+ 'epoch': i,
520
+ 'model_state_dict': _model.state_dict(),
521
+ 'optimizer_state_dict': OPTIMIZER.state_dict(),
522
+ 'val_loss': _val_loss,
523
+ }
524
+ if scheduler is not None: states['lr_scheduler_state_dict'] = scheduler.state_dict()
525
+ th.save(states, os.path.join(self.CHK_SAVE_PATH, f'best_checkpoint{self.CHK_SAVE_POSTFIX}.pt'))
526
+ if self.VERBOSE: self.logger.info('Done.')
527
+ else:
528
+ if self.VERBOSE: self.logger.info(f'Validation loss NOT descent. Minimum loss: {val_loss_old:< 4.4e}.')
529
+ if self.EMA: ema.restore()
530
+ _can_valid = False
531
+
532
+ time_gp = time.perf_counter()
533
+ num_step += 1
534
+
535
+ except Exception as e:
536
+ _can_valid = False
537
+ time_gp = time.perf_counter()
538
+ num_step += 1
539
+ n_err += 1
540
+ if n_err <= 200:
541
+ self.logger.error(
542
+ f'An error occurred in the step {num_step} of epoch {i+1}: {e}; idx: {batch_data.idx}.\n'
543
+ f'Total ERROR NUMBER: {n_err}.\n{traceback.format_exc()}'
544
+ f'Training will continue, but metrics and losses in this step may not be accurate.'
545
+ )
546
+ else:
547
+ self.logger.fatal(
548
+ f'An error occurred in the step {num_step} of epoch {i+1}: {e}. Total ERROR NUMBER: {n_err}.\n'
549
+ f'TOO MANY ERRORS OCCURRED DURING TRAINING. I REFUSE TO CONTINUE SUCH A SICK JOB. BYE!'
550
+ )
551
+ exit(1)
552
+ # print per epoch
553
+ self.logger.info(f'\n*** EPOCH {i + 1:>6} Done. LOOP TIME: {time.perf_counter() - time_ep}')
554
+ self.logger.info('-' * 60 + f'MAIN LOOP DONE' + '-' * 60 + f'\nTOTAL TIME: {time.perf_counter() - time_tol}')
555
+
556
+ # save the last chkpt.
557
+ if len(history['val_loss']) == 0: # avoid empty validation loss.
558
+ if self.VERBOSE: self.logger.info('\nThe model has not yet been validated, skipped saving.')
559
+ elif self.SAVE_CHK:
560
+ with _LoggingEnd(self.log_handler):
561
+ if self.VERBOSE: self.logger.info('\nSaving the last check point file...')
562
+ states = {
563
+ 'epoch': self.EPOCH,
564
+ 'model_state_dict': _model.state_dict(),
565
+ 'optimizer_state_dict': OPTIMIZER.state_dict(),
566
+ 'val_loss': history['val_loss'][-1],
567
+ }
568
+ if scheduler is not None: states['lr_scheduler_state_dict'] = scheduler.state_dict()
569
+ th.save(states, os.path.join(self.CHK_SAVE_PATH, f'checkpoint{self.CHK_SAVE_POSTFIX}.pt'))
570
+ if self.VERBOSE: self.logger.info('Done.')
571
+
572
+ except Exception as e:
573
+ self.logger.exception(f'An ERROR occurred:\n\t{e}\nTraceback:{traceback.format_exc()}\n')
574
+
575
+ finally:
576
+ # If the program stopped, recording checkpoints.
577
+ if self.EMA: ema.restore()
578
+ if i != self.EPOCH - 1:
579
+ self.logger.info('*' * 89)
580
+ self.logger.info(f'*** STOPPED AT {time.strftime("%Y%m%d_%H:%M:%S")} ***')
581
+ if self.SAVE_CHK:
582
+ states = {
583
+ 'epoch': i,
584
+ 'model_state_dict': _model.state_dict(),
585
+ 'optimizer_state_dict': OPTIMIZER.state_dict(),
586
+ 'val_loss': th.inf
587
+ }
588
+ if len(history['val_loss']) != 0:
589
+ states['val_loss'] = history['val_loss'][-1],
590
+ if scheduler is not None: states['lr_scheduler_state_dict'] = scheduler.state_dict()
591
+ th.save(states, os.path.join(self.CHK_SAVE_PATH, f'stop_checkpoint{self.CHK_SAVE_POSTFIX}.pt'))
592
+ self.logger.info(
593
+ f'*** Checkpoint file was saved in {os.path.join(self.CHK_SAVE_PATH, f"stop_checkpoint{self.CHK_SAVE_POSTFIX}.pt")}'
594
+ )
595
+ # close handler
596
+ self.logger.removeHandler(self.log_handler)
597
+ if isinstance(self.log_handler, logging.FileHandler):
598
+ self.log_handler.close()
599
+
600
+ def _val(self, model, LOSS) -> Tuple[float, Dict]:
601
+ """
602
+ the model and LOSS here were instantiated.
603
+ """
604
+ model.eval()
605
+ _val_loss = 0.
606
+ _num_step = 0
607
+ val_set = self._data_loader(self.VALID_DATA, self.VAL_BATCH_SIZE, self.DEVICE, **self._data_loader_configs) # type: ignore # TODO
608
+ _metr_list = {_name: 0. for _name in self.METRICS.keys()}
609
+ with th.no_grad():
610
+ for val_data, val_label in val_set:
611
+ # to avoid get an empty batch
612
+ if not isinstance(val_data, (th.Tensor,)):
613
+ len_data = val_data.batch_size
614
+ else:
615
+ len_data = len(val_data)
616
+ if len_data <= 0:
617
+ if self.VERBOSE: self.logger.info(f'An empty batch occurred in validation. Skipped.')
618
+ continue
619
+ # pred & loss
620
+ pred_y = model(val_data)
621
+ val_loss = LOSS(pred_y, val_label)
622
+ _val_loss += val_loss.item()
623
+ # metrics
624
+ if len(self.METRICS) > 0:
625
+ for _name, _metr_func in self.METRICS.items():
626
+ _metr = _metr_func(pred_y, val_label, **self.METRICS_CONFIG[_name])
627
+ _metr_list[_name] += _metr.item()
628
+ _num_step += 1
629
+ _val_loss = _val_loss / _num_step
630
+ if len(self.METRICS) > 0:
631
+ _metr_list = {_name: _val / _num_step for _name, _val in _metr_list.items()} # note: mean value per batch.
632
+ else:
633
+ _metr_list = dict()
634
+ return _val_loss, _metr_list
635
+