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.
- BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
- BUCToolkit/BatchGenerate/__init__.py +6 -0
- BUCToolkit/BatchGenerate/coords_interp.py +127 -0
- BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
- BUCToolkit/BatchGenerate/random_replace.py +88 -0
- BUCToolkit/BatchMC/MetropolisMC.py +210 -0
- BUCToolkit/BatchMC/_BaseMC.py +863 -0
- BUCToolkit/BatchMC/__init__.py +8 -0
- BUCToolkit/BatchMD/BiasedMD.py +276 -0
- BUCToolkit/BatchMD/NVE.py +92 -0
- BUCToolkit/BatchMD/NVT.py +510 -0
- BUCToolkit/BatchMD/_BaseMD.py +1251 -0
- BUCToolkit/BatchMD/__init__.py +18 -0
- BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
- BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
- BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
- BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
- BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
- BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
- BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
- BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
- BUCToolkit/BatchOptim/TS/__init__.py +10 -0
- BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
- BUCToolkit/BatchOptim/__init__.py +22 -0
- BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
- BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
- BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
- BUCToolkit/BatchOptim/frequency.py +496 -0
- BUCToolkit/BatchOptim/minimize/CG.py +256 -0
- BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
- BUCToolkit/BatchOptim/minimize/QN.py +205 -0
- BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
- BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
- BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
- BUCToolkit/BatchStructures/__init__.py +15 -0
- BUCToolkit/Preprocessing/__init__.py +7 -0
- BUCToolkit/Preprocessing/load_files.py +1680 -0
- BUCToolkit/Preprocessing/preprocessing.py +923 -0
- BUCToolkit/Preprocessing/sampling.py +299 -0
- BUCToolkit/Preprocessing/supercells.py +140 -0
- BUCToolkit/Preprocessing/write_files.py +524 -0
- BUCToolkit/StandardTemplate.py +417 -0
- BUCToolkit/__init__.py +33 -0
- BUCToolkit/_version.py +7 -0
- BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
- BUCToolkit/api/DataLoaders.py +321 -0
- BUCToolkit/api/Losses.py +170 -0
- BUCToolkit/api/Metrics.py +84 -0
- BUCToolkit/api/ModelOptims.py +296 -0
- BUCToolkit/api/MolecularDynamics.py +282 -0
- BUCToolkit/api/MonteCarlo.py +280 -0
- BUCToolkit/api/NEB.py +325 -0
- BUCToolkit/api/Predictor.py +292 -0
- BUCToolkit/api/StructureOptimization.py +484 -0
- BUCToolkit/api/Trainer.py +635 -0
- BUCToolkit/api/VibrationAnalysis.py +279 -0
- BUCToolkit/api/__init__.py +38 -0
- BUCToolkit/api/_io.py +1026 -0
- BUCToolkit/cli/__init__.py +20 -0
- BUCToolkit/cli/cli.py +934 -0
- BUCToolkit/cli/input_stub.py +140 -0
- BUCToolkit/cli/main.py +368 -0
- BUCToolkit/cli/print_logo.py +149 -0
- BUCToolkit/cli/template_model.py +6 -0
- BUCToolkit/io.py +30 -0
- BUCToolkit/utils/AtomSelector.py +64 -0
- BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
- BUCToolkit/utils/ElemListReduce.py +52 -0
- BUCToolkit/utils/Ewald.py +210 -0
- BUCToolkit/utils/IrregularTensorReformat.py +59 -0
- BUCToolkit/utils/_CheckModules.py +30 -0
- BUCToolkit/utils/_Element_info.py +527 -0
- BUCToolkit/utils/__init__.py +22 -0
- BUCToolkit/utils/_para_flatt_list.py +131 -0
- BUCToolkit/utils/_print_formatter.py +59 -0
- BUCToolkit/utils/check_structures.py +204 -0
- BUCToolkit/utils/function_utils.py +147 -0
- BUCToolkit/utils/grad_functions.py +69 -0
- BUCToolkit/utils/index_ops.py +432 -0
- BUCToolkit/utils/setup_loggers.py +137 -0
- buctoolkit-1.0b0.dist-info/METADATA +28 -0
- buctoolkit-1.0b0.dist-info/RECORD +88 -0
- buctoolkit-1.0b0.dist-info/WHEEL +5 -0
- buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
- buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
|