fdq 0.0.1__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.
- fdq/__about__.py +3 -0
- fdq/__init__.py +0 -0
- fdq/experiment.py +689 -0
- fdq/img_func.py +1159 -0
- fdq/lossFunctions.py +92 -0
- fdq/misc.py +465 -0
- fdq/optimizer.py +81 -0
- fdq/run_experiment.py +110 -0
- fdq/testing.py +258 -0
- fdq/transformers.py +677 -0
- fdq/ui_functions.py +167 -0
- fdq-0.0.1.dist-info/METADATA +735 -0
- fdq-0.0.1.dist-info/RECORD +15 -0
- fdq-0.0.1.dist-info/WHEEL +4 -0
- fdq-0.0.1.dist-info/licenses/LICENSE +674 -0
fdq/__about__.py
ADDED
fdq/__init__.py
ADDED
|
File without changes
|
fdq/experiment.py
ADDED
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import torch
|
|
6
|
+
import wandb
|
|
7
|
+
import shutil
|
|
8
|
+
import argparse
|
|
9
|
+
import importlib
|
|
10
|
+
import funkybob
|
|
11
|
+
from tqdm import tqdm
|
|
12
|
+
from typing import List
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from lossFunctions import createLoss
|
|
15
|
+
from ui_functions import iprint, wprint
|
|
16
|
+
from optimizer import createOptimizer, set_lr_schedule
|
|
17
|
+
from misc import (
|
|
18
|
+
remove_file,
|
|
19
|
+
store_processing_infos,
|
|
20
|
+
FCQmode,
|
|
21
|
+
recursive_dict_update,
|
|
22
|
+
DictToObj,
|
|
23
|
+
replace_tilde_with_abs_path,
|
|
24
|
+
save_train_history,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class fdqExperiment:
|
|
29
|
+
def __init__(self, inargs: argparse.Namespace) -> None:
|
|
30
|
+
self.inargs = inargs
|
|
31
|
+
self.parse_and_clean_args()
|
|
32
|
+
# ------------- GLOBALS ------------------------------
|
|
33
|
+
self.project = self.exp_def.globals.project.replace(" ", "_")
|
|
34
|
+
self.experimentName = self.experiment_file_path.split("/")[-1].split(".json")[0]
|
|
35
|
+
self.funky_name = None
|
|
36
|
+
self.checkpoint_frequency = self.exp_def.store.checkpoint_frequency
|
|
37
|
+
self.mode = FCQmode()
|
|
38
|
+
self.creation_time = datetime.now()
|
|
39
|
+
self.finish_time = None
|
|
40
|
+
self.run_time = None
|
|
41
|
+
self.run_info = {}
|
|
42
|
+
# ------------- Train parameters ------------------------------
|
|
43
|
+
self.gradacc_iter = self.exp_def.train.args.get(
|
|
44
|
+
"accumulate_grad_batches", default=1
|
|
45
|
+
)
|
|
46
|
+
self.useAMP = bool(self.exp_def.train.args.use_AMP)
|
|
47
|
+
self.nb_epochs = self.exp_def.train.args.epochs
|
|
48
|
+
# ------------- Train variables ------------------------------
|
|
49
|
+
self.current_epoch = 0
|
|
50
|
+
self.start_epoch = 0
|
|
51
|
+
self.data = {}
|
|
52
|
+
self.models = {}
|
|
53
|
+
self.inference_model_paths = {}
|
|
54
|
+
self.optimizers = {}
|
|
55
|
+
self.lr_schedulers = {}
|
|
56
|
+
self.losses = {}
|
|
57
|
+
self.last_model_path = {}
|
|
58
|
+
self.best_val_model_path = {}
|
|
59
|
+
self.best_train_model_path = {}
|
|
60
|
+
self.checkpoint_path = None
|
|
61
|
+
self._results_dir = None
|
|
62
|
+
self._test_dir = None
|
|
63
|
+
self._valLoss = float("inf")
|
|
64
|
+
self._trainLoss = float("inf")
|
|
65
|
+
self.bestValLoss = float("inf")
|
|
66
|
+
self.bestTrainLoss = float("inf")
|
|
67
|
+
self.valLoss_per_ep: List[float] = []
|
|
68
|
+
self.trainLoss_per_ep: List[float] = []
|
|
69
|
+
self.new_best_train_loss = False # flag to indicate if a new best epoch was reached according to train loss
|
|
70
|
+
self.new_best_train_loss_ep_id = None
|
|
71
|
+
self.new_best_val_loss = False # flag to indicate if a new best epoch was reached according to val loss
|
|
72
|
+
self.new_best_val_loss_ep_id = None
|
|
73
|
+
self.early_stop_detected = False
|
|
74
|
+
# ------------- MGMT attributes ------------------------------
|
|
75
|
+
self.useTensorboard = self.exp_file.get("store", {}).get("tensorboard", False)
|
|
76
|
+
self.tb_writer = None
|
|
77
|
+
self.useWandb = self.exp_file.get("store", {}).get("use_wandb", False)
|
|
78
|
+
self.wandb_project = self.exp_file.get("store", {}).get("wandb_project", None)
|
|
79
|
+
self.wandb_entity = self.exp_file.get("store", {}).get("wandb_entity", None)
|
|
80
|
+
self.wandb_key = self.exp_file.get("store", {}).get("wandb_key", None)
|
|
81
|
+
self.wandb_initialized = False
|
|
82
|
+
# ------------- SLURM ------------------------------
|
|
83
|
+
slurm_job_id = os.getenv("SLURM_JOB_ID")
|
|
84
|
+
if isinstance(slurm_job_id, str) and slurm_job_id.isdigit():
|
|
85
|
+
self.is_slurm = True
|
|
86
|
+
self.slurm_job_id = slurm_job_id
|
|
87
|
+
else:
|
|
88
|
+
self.is_slurm = False
|
|
89
|
+
self.slurm_job_id = None
|
|
90
|
+
self.previous_slurm_job_id = None
|
|
91
|
+
# ------------- CUDA / CPU -------------------------
|
|
92
|
+
if torch.cuda.is_available() and bool(self.exp_def.train.args.use_GPU):
|
|
93
|
+
torch.cuda.empty_cache()
|
|
94
|
+
self.device = torch.device("cuda")
|
|
95
|
+
self.is_cuda = True
|
|
96
|
+
iprint(
|
|
97
|
+
f"CUDA available: {torch.cuda.is_available()}. NB devices: {torch.cuda.device_count()}"
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
wprint("NO CUDA available - CPU mode")
|
|
101
|
+
self.device = torch.device("cpu")
|
|
102
|
+
self.is_cuda = False
|
|
103
|
+
|
|
104
|
+
def parse_and_clean_args(self):
|
|
105
|
+
self.experiment_file_path = self.inargs.experimentfile
|
|
106
|
+
|
|
107
|
+
with open(self.experiment_file_path, "r", encoding="utf8") as fp:
|
|
108
|
+
try:
|
|
109
|
+
self.exp_file = json.load(fp)
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"Error loading experiment file {self.experiment_file_path} (check syntax?)."
|
|
113
|
+
) from exc
|
|
114
|
+
|
|
115
|
+
self.globals = self.exp_file.get("globals")
|
|
116
|
+
if self.globals is None:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Error: experiment file does not comply - please check template! {self.experiment_file_path}."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
parent = self.globals.get("parent", {})
|
|
122
|
+
# parent must be in same directory or defined with absolute path
|
|
123
|
+
if parent is not None:
|
|
124
|
+
if parent[0] == "/":
|
|
125
|
+
self.parent_file_path = parent
|
|
126
|
+
else:
|
|
127
|
+
self.parent_file_path = os.path.abspath(
|
|
128
|
+
os.path.join(os.path.split(self.experiment_file_path)[0], parent)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if not os.path.exists(self.parent_file_path):
|
|
132
|
+
raise FileNotFoundError(
|
|
133
|
+
f"Error: File {self.parent_file_path} not found."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
with open(self.parent_file_path, "r", encoding="utf8") as fp:
|
|
137
|
+
try:
|
|
138
|
+
parent_expfile = json.load(fp)
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Error loading experiment file {self.parent_file_path} (check syntax?)."
|
|
142
|
+
) from exc
|
|
143
|
+
|
|
144
|
+
self.exp_file = recursive_dict_update(
|
|
145
|
+
d_parent=parent_expfile, d_child=self.exp_file
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
else:
|
|
149
|
+
self.parent_file_path = None
|
|
150
|
+
replace_tilde_with_abs_path(self.exp_file)
|
|
151
|
+
self.exp_def = DictToObj(self.exp_file)
|
|
152
|
+
|
|
153
|
+
def setupData(self):
|
|
154
|
+
for data_name, data_source in self.exp_def.data.items():
|
|
155
|
+
processor_path = data_source.processor
|
|
156
|
+
|
|
157
|
+
if not os.path.exists(processor_path):
|
|
158
|
+
raise FileNotFoundError(f"Processor file not found: {processor_path}")
|
|
159
|
+
|
|
160
|
+
parent_dir = os.path.dirname(processor_path)
|
|
161
|
+
if parent_dir not in sys.path:
|
|
162
|
+
sys.path.append(parent_dir)
|
|
163
|
+
|
|
164
|
+
module_name = os.path.splitext(os.path.basename(processor_path))[0]
|
|
165
|
+
processor = importlib.import_module(module_name)
|
|
166
|
+
self.data[data_name] = DictToObj(processor.createDatasets(self))
|
|
167
|
+
|
|
168
|
+
def runEvaluator(self):
|
|
169
|
+
evaluator_path = self.exp_def.test.evaluator
|
|
170
|
+
|
|
171
|
+
if not os.path.exists(evaluator_path):
|
|
172
|
+
raise FileNotFoundError(f"Evaluator file not found: {evaluator_path}")
|
|
173
|
+
|
|
174
|
+
parent_dir = os.path.dirname(evaluator_path)
|
|
175
|
+
if parent_dir not in sys.path:
|
|
176
|
+
sys.path.append(parent_dir)
|
|
177
|
+
|
|
178
|
+
module_name = os.path.splitext(os.path.basename(evaluator_path))[0]
|
|
179
|
+
currentEvaluator = importlib.import_module(module_name)
|
|
180
|
+
|
|
181
|
+
return currentEvaluator.createEvaluator(self)
|
|
182
|
+
|
|
183
|
+
def createModel(self, instantiate=True):
|
|
184
|
+
for model_name, model_source in self.exp_def.models:
|
|
185
|
+
model_path = model_source.name
|
|
186
|
+
|
|
187
|
+
if not os.path.exists(model_path):
|
|
188
|
+
current_file_path = os.path.abspath(__file__)
|
|
189
|
+
networks_dir = os.path.abspath(
|
|
190
|
+
os.path.join(os.path.dirname(current_file_path), "../networks/")
|
|
191
|
+
)
|
|
192
|
+
model_path = os.path.join(networks_dir, model_path)
|
|
193
|
+
|
|
194
|
+
if not os.path.exists(model_path):
|
|
195
|
+
raise FileNotFoundError(f"Model file not found: {model_path}")
|
|
196
|
+
|
|
197
|
+
parent_dir = os.path.dirname(model_path)
|
|
198
|
+
if parent_dir not in sys.path:
|
|
199
|
+
sys.path.append(parent_dir)
|
|
200
|
+
|
|
201
|
+
module_name = os.path.splitext(os.path.basename(model_path))[0]
|
|
202
|
+
model = importlib.import_module(module_name)
|
|
203
|
+
if instantiate:
|
|
204
|
+
self.models[model_name] = model.createNetwork(self).to(self.device)
|
|
205
|
+
|
|
206
|
+
def copy_data_to_scratch(self):
|
|
207
|
+
"""
|
|
208
|
+
Copy all datasets to scratch dir, and update the paths
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def _mkdir(path):
|
|
212
|
+
if not os.path.exists(path):
|
|
213
|
+
os.makedirs(path)
|
|
214
|
+
|
|
215
|
+
def _cp_files(paths, name):
|
|
216
|
+
if paths is not None:
|
|
217
|
+
if not isinstance(paths, list):
|
|
218
|
+
raise ValueError(f"{name} must be defined as a list!")
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
dst_path = os.path.join(self.clusterDataBasePath, name + "/")
|
|
222
|
+
_mkdir(dst_path)
|
|
223
|
+
|
|
224
|
+
for i, pf in enumerate(tqdm(paths, desc=f"Copying {name} files")):
|
|
225
|
+
new_path = os.path.join(dst_path, os.path.basename(pf))
|
|
226
|
+
os.system(f"rsync -au {pf} {new_path}")
|
|
227
|
+
paths[i] = new_path
|
|
228
|
+
|
|
229
|
+
except Exception as exc:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"Unable to copy {pf} to scratch location!"
|
|
232
|
+
) from exc
|
|
233
|
+
|
|
234
|
+
if self.clusterDataBasePath is None:
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
_mkdir(self.clusterDataBasePath)
|
|
238
|
+
|
|
239
|
+
if self.dataBasePath is not None:
|
|
240
|
+
try:
|
|
241
|
+
dst_path = os.path.join(self.clusterDataBasePath, "base_path/")
|
|
242
|
+
if os.path.exists(dst_path):
|
|
243
|
+
shutil.rmtree(dst_path)
|
|
244
|
+
shutil.copytree(self.dataBasePath, dst_path)
|
|
245
|
+
self.dataBasePath = dst_path
|
|
246
|
+
except Exception as exc:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
f"Unable to copy {self.dataBasePath} to scratch location!"
|
|
249
|
+
) from exc
|
|
250
|
+
|
|
251
|
+
# if self.run_train: TODO
|
|
252
|
+
_cp_files(self.trainFilesPath, "train_files_path")
|
|
253
|
+
_cp_files(self.valFilesPath, "val_files_path")
|
|
254
|
+
# if self.run_test or self.run_test_auto:
|
|
255
|
+
_cp_files(self.testFilesPath, "test_files_path")
|
|
256
|
+
|
|
257
|
+
iprint("----------------------------------------------------")
|
|
258
|
+
iprint("Copy datasets to temporary scratch location... Done!")
|
|
259
|
+
iprint("----------------------------------------------------")
|
|
260
|
+
|
|
261
|
+
def prepareTrainLoop(self):
|
|
262
|
+
train_path = self.exp_def.train.train_loop
|
|
263
|
+
|
|
264
|
+
if not os.path.exists(train_path):
|
|
265
|
+
raise FileNotFoundError(f"Training file not found: {train_path}")
|
|
266
|
+
|
|
267
|
+
parent_dir = os.path.dirname(train_path)
|
|
268
|
+
if parent_dir not in sys.path:
|
|
269
|
+
sys.path.append(parent_dir)
|
|
270
|
+
|
|
271
|
+
module_name = os.path.splitext(os.path.basename(train_path))[0]
|
|
272
|
+
self.trainer = importlib.import_module(module_name)
|
|
273
|
+
|
|
274
|
+
# try:
|
|
275
|
+
# self.train_function = importlib.import_module(
|
|
276
|
+
# f"trainings.{self.training_strategy}"
|
|
277
|
+
# )
|
|
278
|
+
# except Exception as exc:
|
|
279
|
+
# raise ImportError(
|
|
280
|
+
# f"Error loading training strategy {self.training_strategy}."
|
|
281
|
+
# ) from exc
|
|
282
|
+
|
|
283
|
+
# self.copy_data_to_scratch()
|
|
284
|
+
|
|
285
|
+
def prepareTraining(self):
|
|
286
|
+
self.mode.train()
|
|
287
|
+
self.setupData()
|
|
288
|
+
self.prepareTrainLoop()
|
|
289
|
+
self.createModel()
|
|
290
|
+
createOptimizer(self)
|
|
291
|
+
set_lr_schedule(self)
|
|
292
|
+
createLoss(self)
|
|
293
|
+
|
|
294
|
+
if self.useAMP:
|
|
295
|
+
self.scaler = torch.amp.GradScaler(device=self.device, enabled=True)
|
|
296
|
+
|
|
297
|
+
if self.inargs.resume_path is not None:
|
|
298
|
+
iprint(
|
|
299
|
+
"--------------------------------------------------------------------------"
|
|
300
|
+
)
|
|
301
|
+
iprint(f"Loading checkpoint: {self.inargs.resume_pathh}")
|
|
302
|
+
|
|
303
|
+
self.load_checkpoint(self.inargs.resume_path)
|
|
304
|
+
|
|
305
|
+
self.cp_to_res_dir(file_path=self.experiment_file_path)
|
|
306
|
+
|
|
307
|
+
if self.parent_file_path is not None:
|
|
308
|
+
self.cp_to_res_dir(file_path=self.parent_file_path)
|
|
309
|
+
|
|
310
|
+
store_processing_infos(self)
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def results_dir(self):
|
|
314
|
+
if self._results_dir is None:
|
|
315
|
+
dt_string = self.creation_time.strftime("%Y%m%d_%H_%M_%S")
|
|
316
|
+
if self.funky_name is None:
|
|
317
|
+
self.funky_name = next(iter(funkybob.RandomNameGenerator()))
|
|
318
|
+
|
|
319
|
+
folder_name = f"{dt_string}__{self.funky_name}"
|
|
320
|
+
|
|
321
|
+
if self.is_slurm:
|
|
322
|
+
folder_name += f"__{self.slurm_job_id}"
|
|
323
|
+
|
|
324
|
+
if self.is_slurm:
|
|
325
|
+
res_base_path = self.exp_file.get("store", {}).get(
|
|
326
|
+
"cluster_results_path", None
|
|
327
|
+
)
|
|
328
|
+
if res_base_path is None:
|
|
329
|
+
raise ValueError("Error, cluster_results_path was not defined.")
|
|
330
|
+
|
|
331
|
+
else:
|
|
332
|
+
res_base_path = self.exp_file.get("store", {}).get("results_path", None)
|
|
333
|
+
if res_base_path is None:
|
|
334
|
+
raise ValueError("Error, result path was not defined.")
|
|
335
|
+
|
|
336
|
+
if res_base_path[0] == "~":
|
|
337
|
+
res_base_path = os.path.expanduser(res_base_path)
|
|
338
|
+
|
|
339
|
+
self._results_dir = os.path.join(
|
|
340
|
+
res_base_path, self.project, self.experimentName, folder_name
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if not os.path.exists(self._results_dir):
|
|
344
|
+
os.makedirs(self._results_dir)
|
|
345
|
+
|
|
346
|
+
return self._results_dir
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def results_output_dir(self):
|
|
350
|
+
if self._results_output_dir is None:
|
|
351
|
+
self._results_output_dir = os.path.join(
|
|
352
|
+
self.results_dir, "training_outputs"
|
|
353
|
+
)
|
|
354
|
+
if not os.path.exists(self._results_output_dir):
|
|
355
|
+
os.makedirs(self._results_output_dir)
|
|
356
|
+
return self._results_output_dir
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def test_dir(self):
|
|
360
|
+
if self._test_dir is None:
|
|
361
|
+
folder_name = self.creation_time.strftime("%Y%m%d_%H_%M_%S")
|
|
362
|
+
if self.is_slurm:
|
|
363
|
+
folder_name += f"__{self.slurm_job_id}"
|
|
364
|
+
self._test_dir = os.path.join(self.results_dir, "test", folder_name)
|
|
365
|
+
if not os.path.exists(self._test_dir):
|
|
366
|
+
os.makedirs(self._test_dir)
|
|
367
|
+
return self._test_dir
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def valLoss(self):
|
|
371
|
+
return self._valLoss
|
|
372
|
+
|
|
373
|
+
@valLoss.setter
|
|
374
|
+
def valLoss(self, value):
|
|
375
|
+
self._valLoss = value
|
|
376
|
+
self.valLoss_per_ep.append(value)
|
|
377
|
+
if not math.isnan(value):
|
|
378
|
+
self.bestValLoss = min(self.bestValLoss, self._valLoss)
|
|
379
|
+
self.new_best_val_loss = self.bestValLoss == value
|
|
380
|
+
self.new_best_val_loss_ep_id = self.current_epoch
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def trainLoss(self):
|
|
384
|
+
return self._trainLoss
|
|
385
|
+
|
|
386
|
+
@trainLoss.setter
|
|
387
|
+
def trainLoss(self, value):
|
|
388
|
+
self._trainLoss = value
|
|
389
|
+
self.trainLoss_per_ep.append(value)
|
|
390
|
+
if not math.isnan(value):
|
|
391
|
+
self.bestTrainLoss = min(self.bestTrainLoss, self._trainLoss)
|
|
392
|
+
self.new_best_train_loss = self.bestTrainLoss == value
|
|
393
|
+
self.new_best_train_loss_ep_id = self.current_epoch
|
|
394
|
+
|
|
395
|
+
def cp_to_res_dir(self, file_path):
|
|
396
|
+
fn = file_path.split("/")[-1]
|
|
397
|
+
iprint(f"Saving {fn} to {self.results_dir}...")
|
|
398
|
+
shutil.copyfile(file_path, f"{self.results_dir}/{fn}")
|
|
399
|
+
|
|
400
|
+
def copy_files_to_test_dir(self, file_path):
|
|
401
|
+
fn = file_path.split("/")[-1]
|
|
402
|
+
iprint(f"Saving {fn} to {self.test_dir}...")
|
|
403
|
+
shutil.copyfile(file_path, f"{self.test_dir}/{fn}")
|
|
404
|
+
|
|
405
|
+
def load_checkpoint(self, path):
|
|
406
|
+
"""
|
|
407
|
+
Load checkpoint to resume training.
|
|
408
|
+
"""
|
|
409
|
+
if not os.path.exists(path):
|
|
410
|
+
raise FileNotFoundError(f"Error, checkpoint file {path} not found.")
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
checkpoint = torch.load(path)
|
|
414
|
+
self.start_epoch = checkpoint["epoch"]
|
|
415
|
+
self.trainLoss = checkpoint["train_loss"]
|
|
416
|
+
self.valLoss = checkpoint["val_loss"]
|
|
417
|
+
self.funky_name = checkpoint["funky_name"]
|
|
418
|
+
self.previous_slurm_job_id = checkpoint.get("slurm_job_id")
|
|
419
|
+
except Exception as exc:
|
|
420
|
+
raise ValueError(f"Error loading checkpoint {path}.") from exc
|
|
421
|
+
|
|
422
|
+
iprint(
|
|
423
|
+
f"Loaded checkpoint {self.start_epoch}. Train loss: {self.trainLoss:.4f}, val loss: {self.valLoss:.4f}"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if self.start_epoch >= self.nb_epochs - 1:
|
|
427
|
+
raise ValueError(
|
|
428
|
+
f"Error, checkpoint epoch {self.start_epoch + 1} already reached defined nb epochs ({self.nb_epochs})."
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
self.model.load_state_dict(checkpoint["model_state_dict"])
|
|
432
|
+
self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
|
|
433
|
+
|
|
434
|
+
def save_checkpoint(self):
|
|
435
|
+
if self.checkpoint_frequency is None or self.checkpoint_frequency == 0:
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
if self.current_epoch % self.checkpoint_frequency != 0:
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
remove_file(self.checkpoint_path)
|
|
442
|
+
self.checkpoint_path = os.path.join(
|
|
443
|
+
self.results_dir, f"checkpoint_e{self.current_epoch}.fdqcpt"
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
iprint(f"Saving checkpoint to {self.checkpoint_path}")
|
|
447
|
+
|
|
448
|
+
if self.optimizers == {}:
|
|
449
|
+
optimizer_state = "No optimizers used"
|
|
450
|
+
else:
|
|
451
|
+
optimizer_state = {
|
|
452
|
+
optim_name: optim.state_dict()
|
|
453
|
+
for optim_name, optim in self.optimizers.items()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
model_state = {
|
|
457
|
+
model_name: model.state_dict() for model_name, model in self.models.items()
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
checkpoint = {
|
|
461
|
+
"epoch": self.current_epoch,
|
|
462
|
+
"model_state_dict": model_state,
|
|
463
|
+
"optimizer": optimizer_state,
|
|
464
|
+
"train_loss": self.trainLoss_per_ep[-1],
|
|
465
|
+
"val_loss": self.valLoss_per_ep[-1],
|
|
466
|
+
"funky_name": self.funky_name,
|
|
467
|
+
"slurm_job_id": self.slurm_job_id,
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
torch.save(checkpoint, self.checkpoint_path)
|
|
471
|
+
|
|
472
|
+
def save_current_model(self):
|
|
473
|
+
"""
|
|
474
|
+
Store model including weights.
|
|
475
|
+
This is run at the end of every epoch.
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
for model_name, model in self.models.items():
|
|
479
|
+
if self.exp_def.store.get("save_last_model", False):
|
|
480
|
+
remove_file(self.last_model_path.get(model_name))
|
|
481
|
+
self.last_model_path[model_name] = os.path.join(
|
|
482
|
+
self.results_dir,
|
|
483
|
+
f"last_{model_name}_e{self.current_epoch}.fdqm",
|
|
484
|
+
)
|
|
485
|
+
torch.save(model, self.last_model_path[model_name])
|
|
486
|
+
|
|
487
|
+
# new best val loss (default!)
|
|
488
|
+
best_model_path = os.path.join(
|
|
489
|
+
self.results_dir,
|
|
490
|
+
f"best_val_{model_name}_e{self.current_epoch}.fdqm",
|
|
491
|
+
)
|
|
492
|
+
if (
|
|
493
|
+
self.current_epoch == self.start_epoch
|
|
494
|
+
or self.exp_def.store.get("save_best_val_model", False)
|
|
495
|
+
and self.new_best_val_loss
|
|
496
|
+
):
|
|
497
|
+
remove_file(self.best_val_model_path.get(model_name))
|
|
498
|
+
self.best_val_model_path[model_name] = best_model_path
|
|
499
|
+
torch.save(model, best_model_path)
|
|
500
|
+
|
|
501
|
+
# save best model according to train loss
|
|
502
|
+
# this might be useful if we use dummy validation losses like in diffusion
|
|
503
|
+
best_train_model_path = os.path.join(
|
|
504
|
+
self.results_dir,
|
|
505
|
+
f"best_train_{model_name}_e{self.current_epoch}.fdqm",
|
|
506
|
+
)
|
|
507
|
+
if (
|
|
508
|
+
self.current_epoch == self.start_epoch
|
|
509
|
+
or self.exp_def.store.get("save_best_train_model", False)
|
|
510
|
+
and self.new_best_train_loss
|
|
511
|
+
):
|
|
512
|
+
remove_file(self.best_train_model_path.get(model_name))
|
|
513
|
+
self.best_train_model_path[model_name] = best_train_model_path
|
|
514
|
+
torch.save(model, best_train_model_path)
|
|
515
|
+
|
|
516
|
+
def load_models(self):
|
|
517
|
+
self.createModel(instantiate=False)
|
|
518
|
+
for model_name, _ in self.exp_def.models:
|
|
519
|
+
path = self.inference_model_paths[model_name]
|
|
520
|
+
iprint(f"Loading model {model_name} from {path}")
|
|
521
|
+
self.models[model_name] = torch.load(path, weights_only=False).to(
|
|
522
|
+
self.device
|
|
523
|
+
)
|
|
524
|
+
self.models[model_name].eval()
|
|
525
|
+
|
|
526
|
+
def dump_model(self, res_folder=None):
|
|
527
|
+
# https://pytorch.org/tutorials/advanced/cpp_export.html
|
|
528
|
+
iprint("Start model dumping")
|
|
529
|
+
|
|
530
|
+
example = torch.rand(
|
|
531
|
+
1, self.nb_in_channels, self.net_input_size[0], self.net_input_size[1]
|
|
532
|
+
).to(self.device)
|
|
533
|
+
|
|
534
|
+
# jit tracer to serialize model using example
|
|
535
|
+
# this only works if there is no flow control applied in the model.
|
|
536
|
+
# otherwise, the model has to be annotated and the torch script compiler applied.
|
|
537
|
+
traced_script_module = torch.jit.trace(self.model, example)
|
|
538
|
+
|
|
539
|
+
# test network
|
|
540
|
+
# test_out = traced_script_module(example)
|
|
541
|
+
# print(test_out)
|
|
542
|
+
|
|
543
|
+
iprint(f"Storing model to {os.path.join(res_folder, 'serialized_model.fdqpt')}")
|
|
544
|
+
traced_script_module.save(os.path.join(res_folder, "serialized_model.fdqpt"))
|
|
545
|
+
|
|
546
|
+
def get_next_export_fn(self, name=None, file_ending="jpg"):
|
|
547
|
+
if self.mode.is_test():
|
|
548
|
+
if name is None:
|
|
549
|
+
path = os.path.join(
|
|
550
|
+
self.test_dir, f"test_image_{self.test_output_id:02}.{file_ending}"
|
|
551
|
+
)
|
|
552
|
+
else:
|
|
553
|
+
path = os.path.join(
|
|
554
|
+
self.test_dir,
|
|
555
|
+
f"test_image_{self.test_output_id:02}__{name}.{file_ending}",
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
self.test_output_id += 1
|
|
559
|
+
|
|
560
|
+
else:
|
|
561
|
+
if name is None:
|
|
562
|
+
path = os.path.join(
|
|
563
|
+
self.results_output_dir,
|
|
564
|
+
f"out_e{self.current_epoch:02}_{self.train_output_id:02}.{file_ending}",
|
|
565
|
+
)
|
|
566
|
+
else:
|
|
567
|
+
path = os.path.join(
|
|
568
|
+
self.results_output_dir,
|
|
569
|
+
f"out_e{self.current_epoch:02}_{self.train_output_id:02}__{name}.{file_ending}",
|
|
570
|
+
)
|
|
571
|
+
self.train_output_id += 1
|
|
572
|
+
|
|
573
|
+
return path
|
|
574
|
+
|
|
575
|
+
def print_dataset_infos(self):
|
|
576
|
+
iprint("-------------------------------------------")
|
|
577
|
+
if self.valset_is_train_subset:
|
|
578
|
+
iprint("Validation set is a subset of the training set.")
|
|
579
|
+
iprint(f"Validation subset ratio: {self.val_from_train_ratio}")
|
|
580
|
+
iprint(f"Nb samples train: {self.trainset_size}")
|
|
581
|
+
iprint(f"Train subset: {self.train_subset}")
|
|
582
|
+
iprint(f"Nb samples val: {self.valset_size}")
|
|
583
|
+
iprint(f"Validation subset: {self.val_subset}")
|
|
584
|
+
iprint(f"Nb samples test: {self.testset_size}")
|
|
585
|
+
iprint(f"Test subset: {self.test_subset}")
|
|
586
|
+
iprint("-------------------------------------------")
|
|
587
|
+
|
|
588
|
+
def clean_up(self):
|
|
589
|
+
iprint("-------------------------------------------")
|
|
590
|
+
iprint("Training done!\nCleaning up..")
|
|
591
|
+
iprint("-------------------------------------------")
|
|
592
|
+
if self.useTensorboard:
|
|
593
|
+
self.tb_writer.close()
|
|
594
|
+
|
|
595
|
+
if self.wandb_initialized:
|
|
596
|
+
wandb.finish()
|
|
597
|
+
|
|
598
|
+
store_processing_infos(self)
|
|
599
|
+
|
|
600
|
+
def check_early_stop(self):
|
|
601
|
+
"""
|
|
602
|
+
1) Stop training if the validation los over last last N epochs did not further decrease.
|
|
603
|
+
We want at least N epochs in each training start, also if its a resume from checkpoint training.
|
|
604
|
+
(--> Therefore, (cur_epoch - self.start_epoch) > self.early_stop_val_loss)
|
|
605
|
+
|
|
606
|
+
2) Stop training if the loss is NaN for N epochs.
|
|
607
|
+
"""
|
|
608
|
+
e_stop_nan = self.exp_def.train.args.early_stop_nan
|
|
609
|
+
e_stop_val = self.exp_def.train.args.early_stop_val_loss
|
|
610
|
+
e_stop_train = self.exp_def.train.args.early_stop_train_loss
|
|
611
|
+
|
|
612
|
+
# early stop NaN ?
|
|
613
|
+
if e_stop_nan is not None:
|
|
614
|
+
if all(math.isnan(x) for x in self.trainLoss_per_ep[-e_stop_nan:]):
|
|
615
|
+
self.early_stop_nan_detected = "NaN detected"
|
|
616
|
+
wprint(
|
|
617
|
+
"\n###############################\n"
|
|
618
|
+
f"!! Early Stop NaN EP {self.current_epoch} !!\n"
|
|
619
|
+
"###############################\n"
|
|
620
|
+
)
|
|
621
|
+
return True
|
|
622
|
+
|
|
623
|
+
# early stop val loss?
|
|
624
|
+
# did we have a new best val loss within the last N epochs?
|
|
625
|
+
# we want at least N losses
|
|
626
|
+
if e_stop_val is not None and len(self.valLoss_per_ep) >= e_stop_val:
|
|
627
|
+
# was there a new best val loss within the last N epochs?
|
|
628
|
+
if min(self.valLoss_per_ep[-e_stop_val:]) != self.bestValLoss:
|
|
629
|
+
self.early_stop_nan_detected = "ValLoss_stagnated"
|
|
630
|
+
wprint(
|
|
631
|
+
"\n###############################\n"
|
|
632
|
+
f"!! Early Stop Val Loss EP {self.current_epoch} !!\n"
|
|
633
|
+
"###############################\n"
|
|
634
|
+
)
|
|
635
|
+
return True
|
|
636
|
+
|
|
637
|
+
# early stop train loss?
|
|
638
|
+
elif e_stop_train is not None and len(self.trainLoss_per_ep) >= e_stop_train:
|
|
639
|
+
if min(self.trainLoss_per_ep[-e_stop_train:]) != self.bestTrainLoss:
|
|
640
|
+
wprint(
|
|
641
|
+
"\n###############################\n"
|
|
642
|
+
f"!! Early Stop Train Loss EP {self.current_epoch} !!\n"
|
|
643
|
+
"###############################\n"
|
|
644
|
+
)
|
|
645
|
+
self.early_stop_nan_detected = "TrainLoss_stagnated"
|
|
646
|
+
return True
|
|
647
|
+
|
|
648
|
+
return False
|
|
649
|
+
|
|
650
|
+
def update_gradients(self, b_idx, loader_name, model_name):
|
|
651
|
+
length_loader = self.data[loader_name].n_train_batches
|
|
652
|
+
|
|
653
|
+
if ((b_idx + 1) % self.gradacc_iter == 0) or (b_idx + 1 == length_loader):
|
|
654
|
+
if self.useAMP:
|
|
655
|
+
self.scaler.step(self.optimizers[model_name])
|
|
656
|
+
self.scaler.update()
|
|
657
|
+
else:
|
|
658
|
+
self.optimizers[model_name].step()
|
|
659
|
+
|
|
660
|
+
self.optimizers[model_name].zero_grad()
|
|
661
|
+
|
|
662
|
+
def finalize_epoch(self):
|
|
663
|
+
# update learning rate
|
|
664
|
+
for model_name in self.models:
|
|
665
|
+
scheduler = self.lr_schedulers[model_name]
|
|
666
|
+
if scheduler is not None:
|
|
667
|
+
current_LR = scheduler.get_last_lr()
|
|
668
|
+
scheduler.step()
|
|
669
|
+
new_LR = scheduler.get_last_lr()
|
|
670
|
+
if current_LR != new_LR:
|
|
671
|
+
iprint(f"Updating LR of {model_name} from {current_LR} to {new_LR}")
|
|
672
|
+
|
|
673
|
+
# end of last epoch
|
|
674
|
+
if self.current_epoch == self.nb_epochs - 1:
|
|
675
|
+
self.finish_time = datetime.now()
|
|
676
|
+
store_processing_infos(self)
|
|
677
|
+
|
|
678
|
+
try:
|
|
679
|
+
self.run_time = datetime.now() - self.creation_time
|
|
680
|
+
td = self.run_time
|
|
681
|
+
run_t_string = f"days: {td.days}, hours: {td.seconds // 3600}, minutes: {td.seconds % 3600 / 60.0:.0f}"
|
|
682
|
+
iprint(f"Current run time: {run_t_string}")
|
|
683
|
+
store_processing_infos(self)
|
|
684
|
+
except Exception:
|
|
685
|
+
pass
|
|
686
|
+
|
|
687
|
+
save_train_history(self)
|
|
688
|
+
self.save_checkpoint()
|
|
689
|
+
self.save_current_model()
|