torch_em.trainer.default_trainer
1from __future__ import annotations 2 3import os 4import time 5import inspect 6import warnings 7import contextlib 8from tqdm import tqdm 9from functools import partial 10from datetime import datetime 11from collections import OrderedDict 12from importlib import import_module 13from typing import Any, Callable, Dict, Optional, Union, Literal 14 15import numpy as np 16 17import torch 18 19from .wandb_logger import WandbLogger 20from .tensorboard_logger import TensorboardLogger 21from ..util import auto_compile, get_constructor_arguments, is_compiled 22 23 24class DefaultTrainer: 25 """Trainer class for training a segmentation network. 26 27 The trainer class implements the core logic for training a network with pytorch. 28 It implements a training loop to run training and validation, which is started with `fit`. 29 The checkpoints and logs from the training run will be saved in the current working directory, 30 or in the directory specifified by `save_root`. Training can be continued from a checkpoint 31 by passing it's location to the `load_from_checkpoint` argument of `fit`. 32 33 A pre-configured instance of the trainer can be obtained from `torch_em.default_segmentation_trainer`. 34 Alternatively, the trainer class can also be instantiated as in this example: 35 ```python 36 import torch 37 from torch_em.loss import DiceLoss 38 from torch_em.model import UNet2d 39 from torch_em.data.datasets.light_microscopy import get_dsb_loader 40 from torch_em.trainer import DefaultTrainer 41 42 # The training data will be downloaded to this location. 43 data_root = "/path/to/save/the/training/data" 44 patch_shape = (256, 256) 45 46 # Create the model and optimizer. 47 model = UNet2d(in_channels=1, out_channels=1) 48 optimizer = torch.optim.AdamW(model.parameters()) 49 50 trainer = DefaultTrainer( 51 name="unet-training", 52 train_loader=get_dsb_loader(path=data_root, patch_shape=patch_shape, split="train"), 53 val_loader=get_dsb_loader(path=data_root, patch_shape=patch_shape, split="test"), 54 model=model, 55 loss=DiceLoss(), # The loss function. 56 optimizer=optimizer, 57 metric=DiceLoss(), # The metric. The trainer expects smaller values to represent better results. 58 device="cuda", # The device to use for training. 59 ) 60 trainer.fit(iterations=int(2.5e4)) # Train for 25.000 iterations. 61 ``` 62 63 Args: 64 name: The name of the checkpoint that will be created by the trainer. 65 train_loader: The data loader containing the training data. 66 val_loader: The data loader containing the validation data. 67 model: The model to train. 68 loss: The loss function for training. 69 optimizer: The optimizer. 70 metric: The metric for validation. 71 device: The torch device to use for training. If None, will use a GPU if available. 72 lr_scheduler: The learning rate scheduler. 73 log_image_interval: The interval for saving images during logging, in training iterations. 74 mixed_precision: Whether to train with mixed precision. 75 early_stopping: The patience for early stopping in epochs. If None, early stopping will not be used. 76 logger: The logger class. Will be instantiated for logging. 77 By default uses `torch_em.training.tensorboard_logger.TensorboardLogger`. 78 logger_kwargs: The keyword arguments for the logger class. 79 id_: Unique identifier for the trainer. If None then `name` will be used. 80 save_root: The root folder for saving the checkpoint and logs. 81 compile_model: Whether to compile the model before training. 82 rank: Rank argument for distributed training. See `torch_em.multi_gpu_training` for details. 83 mixed_precision_dtype: The dtype for autocast in mixed precision training, 'float16' or 'bfloat16'. 84 The default is 'float16' on the GPU and 'bfloat16' on the CPU. Use 'bfloat16' to avoid overflows. 85 """ 86 def __init__( 87 self, 88 name: Optional[str], 89 train_loader: torch.utils.data.DataLoader, 90 val_loader: torch.utils.data.DataLoader, 91 model: torch.nn.Module, 92 loss: torch.nn.Module, 93 optimizer: torch.optim.Optimizer, 94 metric: Callable, 95 device: Union[str, torch.device], 96 lr_scheduler: Optional[torch.optim.lr_scheduler._LRScheduler] = None, 97 log_image_interval: int = 100, 98 mixed_precision: bool = True, 99 early_stopping: Optional[int] = None, 100 logger=TensorboardLogger, 101 logger_kwargs: Optional[Dict[str, Any]] = None, 102 id_: Optional[str] = None, 103 save_root: Optional[str] = None, 104 compile_model: Optional[Union[bool, str]] = None, 105 rank: Optional[int] = None, 106 mixed_precision_dtype: Optional[str] = None, 107 ): 108 if name is None and not issubclass(logger, WandbLogger): 109 raise TypeError("Name cannot be None if not using the WandbLogger") 110 111 self._generate_name = name is None 112 self.name = name 113 self.id_ = id_ or name 114 self.train_loader = train_loader 115 self.val_loader = val_loader 116 self.model = model 117 self.loss = loss 118 self.optimizer = optimizer 119 self.metric = metric 120 self.device = torch.device(device) 121 self.lr_scheduler = lr_scheduler 122 self.log_image_interval = log_image_interval 123 self.save_root = save_root 124 self.compile_model = compile_model 125 self.rank = rank 126 self._device_type = "cpu" if self.device.type == "cpu" else "cuda" 127 128 self._iteration = 0 129 self._epoch = 0 130 self._best_epoch = 0 131 132 self.mixed_precision = mixed_precision 133 # These are the defaults of torch.autocast for each device type. 134 self.mixed_precision_dtype = mixed_precision_dtype or ("bfloat16" if self._device_type == "cpu" else "float16") 135 self.early_stopping = early_stopping 136 self.train_time = 0.0 137 138 if mixed_precision: 139 # Only float16 needs gradient scaling. bfloat16 has the same range as float32. 140 self.scaler = torch.GradScaler(self._device_type, enabled=self.mixed_precision_dtype == "float16") 141 else: 142 self.scaler = None 143 144 self.logger_class = logger 145 self.logger_kwargs = logger_kwargs 146 self.log_image_interval = log_image_interval 147 148 @property 149 def checkpoint_folder(self): 150 assert self.id_ is not None # Because the logger may generate and set trainer.id on logger.__init__. 151 # Save_root enables saving the checkpoints somewhere else than in the local folder. 152 # This is handy for filesystems with limited space, where saving the checkpoints 153 # and log files can lead to running out of space. 154 save_root = getattr(self, "save_root", None) 155 return os.path.join("./checkpoints", self.id_) if save_root is None else\ 156 os.path.join(save_root, "./checkpoints", self.id_) 157 158 @property 159 def iteration(self): 160 return self._iteration 161 162 @property 163 def epoch(self): 164 return self._epoch 165 166 class Deserializer: 167 """Determines how to deserialize the trainer kwargs from serialized 'init_data'. 168 169 Examples: 170 To extend the initialization process you can inherite from this Deserializer in an inherited Trainer class. 171 Note that `DefaultTrainer.Deserializer.load_generic()` covers most cases already. 172 173 This example adds `the_answer` kwarg, which requires 'calculations' upon initialization: 174 >>> class MyTrainer(DefaultTrainer): 175 >>> def __init__(self, *args, the_answer: int, **kwargs): 176 >>> super().__init__(*args, **kwargs) 177 >>> self.the_answer = the_answer # this allows the default Serializer to save the new kwarg, 178 >>> # see DefaultTrainer.Serializer 179 >>> 180 >>> class Deserializer(DefaultTrainer.Deserializer): 181 >>> def load_the_answer(self): 182 >>> generic_answer = self.init_data["the_answer"] 183 >>> # (device dependent) special deserialization 184 >>> if self.trainer_kwargs["device"].type == "cpu": # accessing previously deserialized kwarg 185 >>> self.trainer_kwargs["the_answer"] = generic_answer + 1 186 >>> else: 187 >>> self.trainer_kwargs["the_answer"] = generic_answer * 2 188 189 Args: 190 init_data: The initialization data of the trainer. 191 save_path: The path where the checkpoint was saved. 192 device: The device. 193 """ 194 195 def __init__(self, init_data: Dict, save_path: str, device: Union[str, torch.device]): 196 self.init_data = init_data 197 self.save_path = save_path 198 # Populate with deserialized trainer kwargs during deserialization; possibly overwrite 'device'. 199 self.trainer_kwargs: Dict[str, Any] = dict( 200 device=torch.device(self.init_data["device"]) if device is None else torch.device(device) 201 ) 202 203 def load(self, kwarg_name: str, optional): 204 """@private 205 """ 206 # `optional` is True if self.trainer.__class__.__init__ specifies a default value for 'kwarg_name' 207 if kwarg_name == "device": 208 pass # deserialized in __init__ 209 elif kwarg_name.endswith("_loader"): 210 self.load_data_loader(kwarg_name, optional) 211 else: 212 load = getattr(self, f"load_{kwarg_name}", self.load_generic) 213 load(kwarg_name, optional=optional) 214 215 def load_data_loader(self, loader_name, optional) -> None: 216 """@private 217 """ 218 ds = self.init_data.get(loader_name.replace("_loader", "_dataset")) 219 if ds is None and optional: 220 return 221 222 loader_kwargs = self.init_data[f"{loader_name}_kwargs"] 223 loader = torch.utils.data.DataLoader(ds, **loader_kwargs) 224 # monkey patch shuffle loader_name to the loader 225 loader.shuffle = loader_kwargs.get("shuffle", False) 226 self.trainer_kwargs[loader_name] = loader 227 228 def load_generic( 229 self, 230 kwarg_name: str, 231 *dynamic_args: Dict, 232 optional: bool, 233 only_class: bool = False, 234 dynamic_kwargs: Optional[Dict[str, Any]] = None, 235 ) -> None: 236 """@private 237 """ 238 if kwarg_name in self.init_data: 239 self.trainer_kwargs[kwarg_name] = self.init_data[kwarg_name] 240 return 241 242 this_cls = self.init_data.get(f"{kwarg_name}_class", None) 243 if this_cls is None: 244 if optional: 245 return 246 else: 247 raise RuntimeError(f"Could not find init data for {kwarg_name} in {self.save_path}") 248 249 assert isinstance(this_cls, str), this_cls 250 assert "." in this_cls, this_cls 251 cls_p, cls_m = this_cls.rsplit(".", 1) 252 this_cls = getattr(import_module(cls_p), cls_m) 253 if only_class: 254 self.trainer_kwargs[kwarg_name] = this_cls 255 else: 256 self.trainer_kwargs[kwarg_name] = this_cls( 257 *dynamic_args, **self.init_data.get(f"{kwarg_name}_kwargs", {}), **(dynamic_kwargs or {}) 258 ) 259 260 def load_name(self, kwarg_name: str, optional: bool): 261 """@private 262 """ 263 self.trainer_kwargs[kwarg_name] = os.path.split(os.path.dirname(self.save_path))[1] 264 265 def load_optimizer(self, kwarg_name: str, optional: bool): 266 """@private 267 """ 268 self.load_generic(kwarg_name, self.trainer_kwargs["model"].parameters(), optional=optional) 269 270 def load_lr_scheduler(self, kwarg_name: str, optional: bool): 271 """@private 272 """ 273 self.load_generic(kwarg_name, self.trainer_kwargs["optimizer"], optional=optional) 274 275 # todo: remove and rename kwarg 'logger' to 'logger_class' 276 def load_logger(self, kwarg_name: str, optional: bool): 277 """@private 278 """ 279 assert kwarg_name == "logger" 280 self.load_generic("logger", optional=optional, only_class=True) 281 282 @staticmethod 283 def _get_save_dict(save_path, device): 284 if not os.path.exists(save_path): 285 raise ValueError(f"Cannot find checkpoint {save_path}") 286 return torch.load(save_path, map_location=device, weights_only=False) 287 288 @classmethod 289 def from_checkpoint( 290 cls, 291 checkpoint_folder: Union[os.PathLike, str], 292 name: Literal["best", "latest"] = "best", 293 device: Optional[Union[str, torch.device]] = None, 294 ): 295 """@private 296 """ 297 save_path = os.path.join(checkpoint_folder, f"{name}.pt") 298 # make sure the correct device is set if we don't have access to CUDA 299 if not torch.cuda.is_available(): 300 device = "cpu" 301 save_dict = cls._get_save_dict(save_path, device) 302 deserializer = cls.Deserializer(save_dict["init"], save_path, device) 303 304 has_kwargs = False 305 deserialized = [] 306 for name, parameter in inspect.signature(cls).parameters.items(): 307 if name == "kwargs": 308 has_kwargs = True 309 continue 310 deserializer.load(name, optional=parameter.default is not inspect.Parameter.empty) 311 deserialized.append(name) 312 313 # to deserialze kwargs we can't rely on inspecting the signature, so we 314 # go through the remaning kwarg names in init data instead 315 if has_kwargs: 316 kwarg_names = list(set(deserializer.init_data.keys()) - set(deserialized)) 317 for name in kwarg_names: 318 if name.endswith("_kwargs"): 319 continue 320 elif name.endswith("_dataset"): 321 deserializer.load(name.replace("dataset", "loader"), optional=False) 322 elif name.endswith("_class"): 323 deserializer.load(name.replace("_class", ""), optional=False) 324 else: 325 deserializer.load(name, optional=False) 326 327 trainer = cls(**deserializer.trainer_kwargs) 328 trainer._initialize(0, save_dict) 329 trainer._is_initialized = True 330 return trainer 331 332 class Serializer: 333 """Implements how to serialize trainer kwargs from a trainer instance. 334 335 Examples: 336 To extend the serialization process you can inherite from this Serializer in a derived Trainer class. 337 Note that the methods `dump_generic_builtin()`, `dump_generic_class()` and `dump_generic_instance()` 338 called by the `dump()` method when appropriate cover most cases already. 339 340 This example adds `the_answer` kwarg, which requires extra steps on dumping only because we don't keep a 341 'the_answer' attribute: 342 >>> class MyTrainer(DefaultTrainer): 343 >>> def __init__(self, *args, the_answer: int, **kwargs): 344 >>> super().__init__(*args, **kwargs) 345 >>> # self.the_answer = the_answer # this would allow the default Serializer to save the new kwarg, 346 >>> # but let's make things more interesting... 347 >>> self.the = the_answer // 10 348 >>> self.answer = the_answer % 10 349 >>> 350 >>> class Serializer(DefaultTrainer.Serializer): 351 >>> trainer: MyTrainer 352 >>> def dump_the_answer(self, kwarg_name: str) -> None: # custom dump method for 'the_answer' kwarg 353 >>> assert kwarg_name == "the_answer" 354 >>> # populate self.init_data with the serialized data required by Deserializer 355 >>> # to restore the trainer kwargs 356 >>> self.init_data["the_answer"] = self.trainer.the * 10 + self.trainer.answer 357 358 This example with both Serializer and Deserializer adds `the_answer` kwarg, 359 while saving it in two separate entries 'the' and 'answer' 360 >>> class MyTrainer(DefaultTrainer): 361 >>> def __init__(self, *args, the_answer: int, **kwargs): 362 >>> super().__init__(*args, **kwargs) 363 >>> self.the_answer = the_answer 364 >>> 365 >>> class Serializer(DefaultTrainer.Serializer): 366 >>> trainer: MyTrainer 367 >>> def dump_the_answer(self, kwarg_name: str): 368 >>> assert kwarg_name == "the_answer" 369 >>> self.init_data.update({ 370 >>> "the": self.trainer.the_answer // 10, 371 >>> "answer": self.trainer.the_answer % 10 372 >>> }) 373 >>> 374 >>> class Deserializer(DefaultTrainer.Deserializer): 375 >>> def load_the_answer(self, kwarg_name: str, optional: bool): 376 >>> assert kwarg_name == "the_answer" 377 >>> # 'optional' is True if MyTrainer.__init__ specifies a default value for 'kwarg_name' 378 >>> self.trainer_kwargs[kwarg_name] = self.init_data["the"] * 10 + self.init_data["answer"] 379 380 Args: 381 trainer: The trainer instance. 382 """ 383 384 def __init__(self, trainer: DefaultTrainer): 385 self.trainer = trainer 386 self.init_data = {} # to be populated during serialization process 387 388 def dump(self, kwarg_name: str) -> None: 389 """@private 390 """ 391 dumper = getattr(self, f"dump_{kwarg_name}", None) 392 if dumper is not None: 393 dumper(kwarg_name) 394 elif kwarg_name.endswith("_loader"): 395 self.dump_data_loader(kwarg_name) 396 elif kwarg_name.endswith("_class"): 397 self.dump_generic_class(kwarg_name) 398 elif not hasattr(self.trainer, kwarg_name): 399 raise AttributeError( 400 f"{self.trainer.__class__} missing attribute '{kwarg_name}' " 401 f"or special dump method {self.trainer.__class__}.Serializer.dump_{kwarg_name}()" 402 ) 403 else: 404 assert hasattr(self.trainer, kwarg_name) 405 obj = getattr(self.trainer, kwarg_name) 406 if obj is None or type(obj) in ( 407 bool, 408 bytearray, 409 bytes, 410 dict, 411 float, 412 frozenset, 413 int, 414 list, 415 set, 416 str, 417 tuple, 418 ): 419 self.dump_generic_builtin(kwarg_name) 420 else: 421 self.dump_generic_instance(kwarg_name) 422 423 def dump_generic_builtin(self, kwarg_name: str) -> None: 424 """@private 425 """ 426 assert hasattr(self.trainer, kwarg_name) 427 self.init_data[kwarg_name] = getattr(self.trainer, kwarg_name) 428 429 def dump_generic_class(self, kwarg_name: str) -> None: 430 """@private 431 """ 432 assert hasattr(self.trainer, kwarg_name) 433 assert kwarg_name.endswith("_class") 434 obj = getattr(self.trainer, kwarg_name) 435 self.init_data[kwarg_name] = None if obj is None else f"{obj.__module__}.{obj.__name__}" 436 437 def dump_generic_instance(self, kwarg_name: str) -> None: 438 """@private 439 """ 440 assert hasattr(self.trainer, kwarg_name) 441 instance = getattr(self.trainer, kwarg_name) 442 self.init_data.update( 443 { 444 f"{kwarg_name}_class": f"{instance.__class__.__module__}.{instance.__class__.__name__}", 445 f"{kwarg_name}_kwargs": get_constructor_arguments(instance), 446 } 447 ) 448 449 def dump_device(self, kwarg_name: str): 450 """@private 451 """ 452 assert hasattr(self.trainer, kwarg_name) 453 self.init_data[kwarg_name] = str(getattr(self.trainer, kwarg_name)) 454 455 def dump_data_loader(self, kwarg_name: str) -> None: 456 """@private 457 """ 458 assert hasattr(self.trainer, kwarg_name) 459 loader = getattr(self.trainer, kwarg_name) 460 if loader is None: 461 return 462 self.init_data.update( 463 { 464 f"{kwarg_name.replace('_loader', '_dataset')}": loader.dataset, 465 f"{kwarg_name}_kwargs": get_constructor_arguments(loader), 466 } 467 ) 468 469 def dump_logger(self, kwarg_name: str): # todo: remove and rename kwarg 'logger' to 'logger_class' 470 """@private 471 """ 472 self.dump_generic_class(f"{kwarg_name}_class") 473 474 def dump_model(self, kwarg_name: str): 475 """@private 476 """ 477 if is_compiled(self.trainer.model): 478 self.init_data.update( 479 {"model_class": self.trainer._model_class, "model_kwargs": self.trainer._model_kwargs} 480 ) 481 else: 482 self.dump_generic_instance("model") 483 484 def _build_init(self) -> Dict[str, Any]: 485 serializer = self.Serializer(self) 486 for name in inspect.signature(self.__class__).parameters: 487 # special rules to serialize kwargs 488 # if a trainer class inherits from DefaultTrainer and has **kwargs 489 # they need to be saved in self._kwargs 490 if name == "kwargs": 491 if not hasattr(self, "_kwargs"): 492 msg = "The trainer class has **kwargs in its signature, but is missing the _kwargs attribute. " +\ 493 "Please add self._kwargs to its __init__ function" 494 raise RuntimeError(msg) 495 kwargs = getattr(self, "_kwargs") 496 for kwarg_name in kwargs: 497 serializer.dump(kwarg_name) 498 continue 499 serializer.dump(name) 500 501 return serializer.init_data 502 503 def _initialize(self, iterations, load_from_checkpoint, epochs=None): 504 assert self.train_loader is not None 505 assert self.val_loader is not None 506 assert self.model is not None 507 assert self.loss is not None 508 assert self.optimizer is not None 509 assert self.metric is not None 510 assert self.device is not None 511 512 if load_from_checkpoint is not None: 513 self.load_checkpoint(load_from_checkpoint) 514 515 if sum((iterations is not None, epochs is not None)) != 1: 516 raise ValueError( 517 "Exactly one of 'iterations' or 'epochs' has to be specified to initialize the trainer." 518 f"You have passed 'iterations'={iterations} and 'epochs'={epochs}" 519 ) 520 521 if epochs is None: 522 epochs = int(np.ceil(float(iterations) / len(self.train_loader))) 523 else: 524 iterations = epochs * len(self.train_loader) 525 526 self.max_iteration = self._iteration + iterations 527 self.max_epoch = self._epoch + epochs 528 529 if not getattr(self, "_is_initialized", False): 530 # check if we compile the model (only supported by pytorch 2) 531 # to enable (de)serialization of compiled models, we keep track of the model class and kwargs 532 if is_compiled(self.model): 533 warnings.warn( 534 "You have passed a compiled model to the trainer." 535 "It will not be possible to (de)serialize the trainer with it." 536 "If you want to be able to do this please pass the normal model." 537 "It can be automatically compiled by setting 'compile_model' to True" 538 ) 539 self._model_class = f"{self.model.__class__.__module__}.{self.model.__class__.__name__}" 540 self._model_kwargs = get_constructor_arguments(self.model) 541 self.model = auto_compile(self.model, self.compile_model) 542 543 self.model.to(self.device) 544 self.loss.to(self.device) 545 546 # this saves all the information that is necessary 547 # to fully load the trainer from the checkpoint 548 self.init_data = self._build_init() 549 550 if self.logger_class is None: 551 self.logger = None 552 else: 553 # may set self.name if self.name is None 554 save_root = getattr(self, "save_root", None) 555 try: 556 self.logger = self.logger_class(self, save_root, **(self.logger_kwargs or {})) 557 except (PermissionError, RuntimeError): 558 warnings.warn( 559 f"The checkpoint folder at {self.checkpoint_folder} could not be created." 560 "The most likely reason for this is that you copied the checkpoint somewhere else," 561 "so we skip this error to enable loading the model from this checkpoint." 562 ) 563 564 try: 565 os.makedirs(self.checkpoint_folder, exist_ok=True) 566 except PermissionError: 567 warnings.warn( 568 f"The checkpoint folder at {self.checkpoint_folder} could not be created." 569 "The most likely reason for this is that you copied the checkpoint somewhere else," 570 "so we skip this error to enable loading the model from this checkpoint." 571 ) 572 pass 573 574 best_metric = np.inf 575 return best_metric 576 577 def save_checkpoint(self, name, current_metric, best_metric, train_time=0.0, **extra_save_dict): 578 """@private 579 """ 580 save_path = os.path.join(self.checkpoint_folder, f"{name}.pt") 581 extra_init_dict = extra_save_dict.pop("init", {}) 582 save_dict = { 583 "iteration": self._iteration, 584 "epoch": self._epoch, 585 "best_epoch": self._best_epoch, 586 "best_metric": best_metric, 587 "current_metric": current_metric, 588 "model_state": self.model.state_dict(), 589 "optimizer_state": self.optimizer.state_dict(), 590 "init": self.init_data | extra_init_dict, 591 "train_time": train_time, 592 "timestamp": datetime.now().strftime("%d-%m-%Y (%H:%M:%S)"), 593 } 594 save_dict.update(**extra_save_dict) 595 if self.scaler is not None: 596 save_dict.update({"scaler_state": self.scaler.state_dict()}) 597 if self.lr_scheduler is not None: 598 save_dict.update({"scheduler_state": self.lr_scheduler.state_dict()}) 599 600 rank = getattr(self, "rank", None) 601 if rank is None or rank == 0: 602 torch.save(save_dict, save_path) 603 604 def load_checkpoint(self, checkpoint="best"): 605 """@private 606 """ 607 if isinstance(checkpoint, str): 608 save_path = os.path.join(self.checkpoint_folder, f"{checkpoint}.pt") 609 if not os.path.exists(save_path): 610 warnings.warn(f"Cannot load checkpoint. {save_path} does not exist.") 611 return 612 save_dict = torch.load(save_path, weights_only=False) 613 elif isinstance(checkpoint, dict): 614 save_dict = checkpoint 615 else: 616 raise RuntimeError 617 618 self._iteration = save_dict["iteration"] 619 self._epoch = save_dict["epoch"] 620 self._best_epoch = save_dict["best_epoch"] 621 self.best_metric = save_dict["best_metric"] 622 self.current_metric = save_dict["current_metric"] 623 self.train_time = save_dict.get("train_time", 0.0) 624 625 model_state = save_dict["model_state"] 626 # to enable loading compiled models 627 compiled_prefix = "_orig_mod." 628 model_state = OrderedDict( 629 [(k[len(compiled_prefix):] if k.startswith(compiled_prefix) else k, v) for k, v in model_state.items()] 630 ) 631 self.model.load_state_dict(model_state) 632 # we need to send the network to the device before loading the optimizer state! 633 self.model.to(self.device) 634 635 self.optimizer.load_state_dict(save_dict["optimizer_state"]) 636 scaler_state = save_dict.get("scaler_state") 637 if self.scaler is not None and scaler_state: 638 self.scaler.load_state_dict(scaler_state) 639 if self.lr_scheduler is not None: 640 self.lr_scheduler.load_state_dict(save_dict["scheduler_state"]) 641 642 return save_dict 643 644 def _verify_if_training_completed(self, checkpoint="latest"): 645 save_path = os.path.join(self.checkpoint_folder, f"{checkpoint}.pt") 646 save_dict = torch.load(save_path, weights_only=False) if os.path.exists(save_path) else None 647 if save_dict and self.max_iteration == save_dict.get("iteration"): 648 return True 649 return False 650 651 def fit( 652 self, 653 iterations: Optional[int] = None, 654 load_from_checkpoint: Optional[Union[os.PathLike, str]] = None, 655 epochs: Optional[int] = None, 656 save_every_kth_epoch: Optional[int] = None, 657 progress=None, 658 overwrite_training: bool = True, 659 ): 660 """Run neural network training. 661 662 Exactly one of 'iterations' or 'epochs' has to be passed. 663 664 Args: 665 iterations: How long to train, specified in iterations. 666 load_from_checkpoint: Path to a checkpoint from where training should be continued . 667 epochs: How long to train, specified in epochs. 668 save_every_kth_epoch: Save checkpoints after every kth epoch in a separate file. 669 The corresponding checkpoints will be saved with the naming scheme 'epoch-{epoch}.pt'. 670 progress: Optional progress bar for integration with external tools. Expected to follow the tqdm interface. 671 overwrite_training: Whether to overwrite existing checkpoints in the save directory. 672 """ 673 best_metric = self._initialize(iterations, load_from_checkpoint, epochs) 674 675 if not overwrite_training: 676 if load_from_checkpoint is not None: 677 raise ValueError( 678 "We do not support 'overwrite_training=False' and 'load_from_checkpoint' at the same time." 679 ) 680 681 if self._verify_if_training_completed(): 682 print( 683 f"The model is trained for {self.max_iteration} iterations / {self.max_epoch} epochs " 684 "and 'overwrite_training' is set to 'False'." 685 ) 686 print(f"The checkpoints are located at '{os.path.abspath(self.checkpoint_folder)}'.") 687 return 688 689 print( 690 "Start fitting for", 691 self.max_iteration - self._iteration, 692 "iterations / ", 693 self.max_epoch - self._epoch, 694 "epochs", 695 ) 696 print("with", len(self.train_loader), "iterations per epoch") 697 698 if self.mixed_precision: 699 train_epoch = self._train_epoch_mixed 700 validate = self._validate_mixed 701 print("Training with mixed precision") 702 else: 703 train_epoch = self._train_epoch 704 validate = self._validate 705 print("Training with single precision") 706 707 total_iterations = epochs * len(self.train_loader) if iterations is None else iterations 708 if progress is None: 709 progress = tqdm(total=total_iterations, desc=f"Epoch {self._epoch}", leave=True) 710 else: 711 progress.total = total_iterations 712 progress.set_description(f"Epoch {self._epoch}") 713 714 msg = "Epoch %i: average [s/it]: %f, current metric: %f, best metric: %f" 715 train_epochs = self.max_epoch - self._epoch 716 t_start = time.time() 717 for epoch in range(train_epochs): 718 719 # Ensure data is shuffled differently at each epoch. 720 try: 721 self.train_loader.sampler.set_epoch(epoch) 722 except AttributeError: 723 pass 724 725 # Run training and validation for this epoch 726 t_per_iter = train_epoch(progress) 727 current_metric = validate() 728 729 # perform all the post-epoch steps: 730 731 # apply the learning rate scheduler 732 if self.lr_scheduler is not None: 733 self.lr_scheduler.step(current_metric) 734 735 # how long did we train in total? 736 total_train_time = (time.time() - t_start) + self.train_time 737 738 # save this checkpoint as the new best checkpoint if 739 # it has the best overall validation metric 740 if current_metric < best_metric: 741 best_metric = current_metric 742 self._best_epoch = self._epoch 743 self.save_checkpoint("best", current_metric, best_metric, train_time=total_train_time) 744 745 # save this checkpoint as the latest checkpoint 746 self.save_checkpoint("latest", current_metric, best_metric, train_time=total_train_time) 747 748 # if we save after every k-th epoch then check if we need to save now 749 if save_every_kth_epoch is not None and (self._epoch + 1) % save_every_kth_epoch == 0: 750 self.save_checkpoint( 751 f"epoch-{self._epoch + 1}", current_metric, best_metric, train_time=total_train_time 752 ) 753 754 # if early stopping has been specified then check if the stopping condition is met 755 if self.early_stopping is not None: 756 epochs_since_best = self._epoch - self._best_epoch 757 if epochs_since_best > self.early_stopping: 758 print("Stopping training because there has been no improvement for", self.early_stopping, "epochs") 759 break 760 761 self._epoch += 1 762 progress.set_description(msg % (self._epoch, t_per_iter, current_metric, best_metric), refresh=True) 763 764 print(f"Finished training after {self._epoch} epochs / {self._iteration} iterations.") 765 print(f"The best epoch is number {self._best_epoch}.") 766 767 if self._generate_name: 768 self.name = None 769 770 # Update the train time 771 self.train_time = total_train_time 772 773 # TODO save the model to wandb if we have the wandb logger 774 if isinstance(self.logger, WandbLogger): 775 self.logger.get_wandb().finish() 776 777 def _backprop(self, loss): 778 loss.backward() 779 self.optimizer.step() 780 781 def _backprop_mixed(self, loss): 782 self.scaler.scale(loss).backward() 783 self.scaler.step(self.optimizer) 784 self.scaler.update() 785 786 def _train_epoch(self, progress): 787 return self._train_epoch_impl(progress, contextlib.nullcontext, self._backprop) 788 789 def _train_epoch_mixed(self, progress): 790 return self._train_epoch_impl( 791 progress, 792 partial(torch.autocast, device_type=self._device_type, dtype=getattr(torch, self.mixed_precision_dtype)), 793 self._backprop_mixed 794 ) 795 796 def _forward_and_loss(self, x, y): 797 pred = self.model(x) 798 if self._iteration % self.log_image_interval == 0: 799 if pred.requires_grad: 800 pred.retain_grad() 801 802 loss = self.loss(pred, y) 803 return pred, loss 804 805 def _train_epoch_impl(self, progress, forward_context, backprop: Callable[[torch.Tensor], None]): 806 self.model.train() 807 808 n_iter = 0 809 t_per_iter = time.time() 810 for x, y in self.train_loader: 811 x, y = x.to(self.device, non_blocking=True), y.to(self.device, non_blocking=True) 812 813 self.optimizer.zero_grad() 814 815 with forward_context(): 816 pred, loss = self._forward_and_loss(x, y) 817 818 backprop(loss) 819 820 lr = [pm["lr"] for pm in self.optimizer.param_groups][0] 821 if self.logger is not None: 822 self.logger.log_train(self._iteration, loss, lr, x, y, pred, log_gradients=True) 823 824 self._iteration += 1 825 n_iter += 1 826 if self._iteration >= self.max_iteration: 827 break 828 progress.update(1) 829 830 t_per_iter = (time.time() - t_per_iter) / n_iter 831 return t_per_iter 832 833 def _validate(self): 834 return self._validate_impl(contextlib.nullcontext) 835 836 def _validate_mixed(self): 837 return self._validate_impl( 838 partial(torch.autocast, device_type=self._device_type, dtype=getattr(torch, self.mixed_precision_dtype)) 839 ) 840 841 def _validate_impl(self, forward_context): 842 self.model.eval() 843 844 metric_val = 0.0 845 loss_val = 0.0 846 847 with torch.no_grad(): 848 for x, y in self.val_loader: 849 x, y = x.to(self.device, non_blocking=True), y.to(self.device, non_blocking=True) 850 with forward_context(): 851 pred, loss = self._forward_and_loss(x, y) 852 metric = self.metric(pred, y) 853 854 loss_val += loss.item() 855 metric_val += metric.item() 856 857 metric_val /= len(self.val_loader) 858 loss_val /= len(self.val_loader) 859 if self.logger is not None: 860 self.logger.log_validation(self._iteration, metric_val, loss_val, x, y, pred) 861 return metric_val
25class DefaultTrainer: 26 """Trainer class for training a segmentation network. 27 28 The trainer class implements the core logic for training a network with pytorch. 29 It implements a training loop to run training and validation, which is started with `fit`. 30 The checkpoints and logs from the training run will be saved in the current working directory, 31 or in the directory specifified by `save_root`. Training can be continued from a checkpoint 32 by passing it's location to the `load_from_checkpoint` argument of `fit`. 33 34 A pre-configured instance of the trainer can be obtained from `torch_em.default_segmentation_trainer`. 35 Alternatively, the trainer class can also be instantiated as in this example: 36 ```python 37 import torch 38 from torch_em.loss import DiceLoss 39 from torch_em.model import UNet2d 40 from torch_em.data.datasets.light_microscopy import get_dsb_loader 41 from torch_em.trainer import DefaultTrainer 42 43 # The training data will be downloaded to this location. 44 data_root = "/path/to/save/the/training/data" 45 patch_shape = (256, 256) 46 47 # Create the model and optimizer. 48 model = UNet2d(in_channels=1, out_channels=1) 49 optimizer = torch.optim.AdamW(model.parameters()) 50 51 trainer = DefaultTrainer( 52 name="unet-training", 53 train_loader=get_dsb_loader(path=data_root, patch_shape=patch_shape, split="train"), 54 val_loader=get_dsb_loader(path=data_root, patch_shape=patch_shape, split="test"), 55 model=model, 56 loss=DiceLoss(), # The loss function. 57 optimizer=optimizer, 58 metric=DiceLoss(), # The metric. The trainer expects smaller values to represent better results. 59 device="cuda", # The device to use for training. 60 ) 61 trainer.fit(iterations=int(2.5e4)) # Train for 25.000 iterations. 62 ``` 63 64 Args: 65 name: The name of the checkpoint that will be created by the trainer. 66 train_loader: The data loader containing the training data. 67 val_loader: The data loader containing the validation data. 68 model: The model to train. 69 loss: The loss function for training. 70 optimizer: The optimizer. 71 metric: The metric for validation. 72 device: The torch device to use for training. If None, will use a GPU if available. 73 lr_scheduler: The learning rate scheduler. 74 log_image_interval: The interval for saving images during logging, in training iterations. 75 mixed_precision: Whether to train with mixed precision. 76 early_stopping: The patience for early stopping in epochs. If None, early stopping will not be used. 77 logger: The logger class. Will be instantiated for logging. 78 By default uses `torch_em.training.tensorboard_logger.TensorboardLogger`. 79 logger_kwargs: The keyword arguments for the logger class. 80 id_: Unique identifier for the trainer. If None then `name` will be used. 81 save_root: The root folder for saving the checkpoint and logs. 82 compile_model: Whether to compile the model before training. 83 rank: Rank argument for distributed training. See `torch_em.multi_gpu_training` for details. 84 mixed_precision_dtype: The dtype for autocast in mixed precision training, 'float16' or 'bfloat16'. 85 The default is 'float16' on the GPU and 'bfloat16' on the CPU. Use 'bfloat16' to avoid overflows. 86 """ 87 def __init__( 88 self, 89 name: Optional[str], 90 train_loader: torch.utils.data.DataLoader, 91 val_loader: torch.utils.data.DataLoader, 92 model: torch.nn.Module, 93 loss: torch.nn.Module, 94 optimizer: torch.optim.Optimizer, 95 metric: Callable, 96 device: Union[str, torch.device], 97 lr_scheduler: Optional[torch.optim.lr_scheduler._LRScheduler] = None, 98 log_image_interval: int = 100, 99 mixed_precision: bool = True, 100 early_stopping: Optional[int] = None, 101 logger=TensorboardLogger, 102 logger_kwargs: Optional[Dict[str, Any]] = None, 103 id_: Optional[str] = None, 104 save_root: Optional[str] = None, 105 compile_model: Optional[Union[bool, str]] = None, 106 rank: Optional[int] = None, 107 mixed_precision_dtype: Optional[str] = None, 108 ): 109 if name is None and not issubclass(logger, WandbLogger): 110 raise TypeError("Name cannot be None if not using the WandbLogger") 111 112 self._generate_name = name is None 113 self.name = name 114 self.id_ = id_ or name 115 self.train_loader = train_loader 116 self.val_loader = val_loader 117 self.model = model 118 self.loss = loss 119 self.optimizer = optimizer 120 self.metric = metric 121 self.device = torch.device(device) 122 self.lr_scheduler = lr_scheduler 123 self.log_image_interval = log_image_interval 124 self.save_root = save_root 125 self.compile_model = compile_model 126 self.rank = rank 127 self._device_type = "cpu" if self.device.type == "cpu" else "cuda" 128 129 self._iteration = 0 130 self._epoch = 0 131 self._best_epoch = 0 132 133 self.mixed_precision = mixed_precision 134 # These are the defaults of torch.autocast for each device type. 135 self.mixed_precision_dtype = mixed_precision_dtype or ("bfloat16" if self._device_type == "cpu" else "float16") 136 self.early_stopping = early_stopping 137 self.train_time = 0.0 138 139 if mixed_precision: 140 # Only float16 needs gradient scaling. bfloat16 has the same range as float32. 141 self.scaler = torch.GradScaler(self._device_type, enabled=self.mixed_precision_dtype == "float16") 142 else: 143 self.scaler = None 144 145 self.logger_class = logger 146 self.logger_kwargs = logger_kwargs 147 self.log_image_interval = log_image_interval 148 149 @property 150 def checkpoint_folder(self): 151 assert self.id_ is not None # Because the logger may generate and set trainer.id on logger.__init__. 152 # Save_root enables saving the checkpoints somewhere else than in the local folder. 153 # This is handy for filesystems with limited space, where saving the checkpoints 154 # and log files can lead to running out of space. 155 save_root = getattr(self, "save_root", None) 156 return os.path.join("./checkpoints", self.id_) if save_root is None else\ 157 os.path.join(save_root, "./checkpoints", self.id_) 158 159 @property 160 def iteration(self): 161 return self._iteration 162 163 @property 164 def epoch(self): 165 return self._epoch 166 167 class Deserializer: 168 """Determines how to deserialize the trainer kwargs from serialized 'init_data'. 169 170 Examples: 171 To extend the initialization process you can inherite from this Deserializer in an inherited Trainer class. 172 Note that `DefaultTrainer.Deserializer.load_generic()` covers most cases already. 173 174 This example adds `the_answer` kwarg, which requires 'calculations' upon initialization: 175 >>> class MyTrainer(DefaultTrainer): 176 >>> def __init__(self, *args, the_answer: int, **kwargs): 177 >>> super().__init__(*args, **kwargs) 178 >>> self.the_answer = the_answer # this allows the default Serializer to save the new kwarg, 179 >>> # see DefaultTrainer.Serializer 180 >>> 181 >>> class Deserializer(DefaultTrainer.Deserializer): 182 >>> def load_the_answer(self): 183 >>> generic_answer = self.init_data["the_answer"] 184 >>> # (device dependent) special deserialization 185 >>> if self.trainer_kwargs["device"].type == "cpu": # accessing previously deserialized kwarg 186 >>> self.trainer_kwargs["the_answer"] = generic_answer + 1 187 >>> else: 188 >>> self.trainer_kwargs["the_answer"] = generic_answer * 2 189 190 Args: 191 init_data: The initialization data of the trainer. 192 save_path: The path where the checkpoint was saved. 193 device: The device. 194 """ 195 196 def __init__(self, init_data: Dict, save_path: str, device: Union[str, torch.device]): 197 self.init_data = init_data 198 self.save_path = save_path 199 # Populate with deserialized trainer kwargs during deserialization; possibly overwrite 'device'. 200 self.trainer_kwargs: Dict[str, Any] = dict( 201 device=torch.device(self.init_data["device"]) if device is None else torch.device(device) 202 ) 203 204 def load(self, kwarg_name: str, optional): 205 """@private 206 """ 207 # `optional` is True if self.trainer.__class__.__init__ specifies a default value for 'kwarg_name' 208 if kwarg_name == "device": 209 pass # deserialized in __init__ 210 elif kwarg_name.endswith("_loader"): 211 self.load_data_loader(kwarg_name, optional) 212 else: 213 load = getattr(self, f"load_{kwarg_name}", self.load_generic) 214 load(kwarg_name, optional=optional) 215 216 def load_data_loader(self, loader_name, optional) -> None: 217 """@private 218 """ 219 ds = self.init_data.get(loader_name.replace("_loader", "_dataset")) 220 if ds is None and optional: 221 return 222 223 loader_kwargs = self.init_data[f"{loader_name}_kwargs"] 224 loader = torch.utils.data.DataLoader(ds, **loader_kwargs) 225 # monkey patch shuffle loader_name to the loader 226 loader.shuffle = loader_kwargs.get("shuffle", False) 227 self.trainer_kwargs[loader_name] = loader 228 229 def load_generic( 230 self, 231 kwarg_name: str, 232 *dynamic_args: Dict, 233 optional: bool, 234 only_class: bool = False, 235 dynamic_kwargs: Optional[Dict[str, Any]] = None, 236 ) -> None: 237 """@private 238 """ 239 if kwarg_name in self.init_data: 240 self.trainer_kwargs[kwarg_name] = self.init_data[kwarg_name] 241 return 242 243 this_cls = self.init_data.get(f"{kwarg_name}_class", None) 244 if this_cls is None: 245 if optional: 246 return 247 else: 248 raise RuntimeError(f"Could not find init data for {kwarg_name} in {self.save_path}") 249 250 assert isinstance(this_cls, str), this_cls 251 assert "." in this_cls, this_cls 252 cls_p, cls_m = this_cls.rsplit(".", 1) 253 this_cls = getattr(import_module(cls_p), cls_m) 254 if only_class: 255 self.trainer_kwargs[kwarg_name] = this_cls 256 else: 257 self.trainer_kwargs[kwarg_name] = this_cls( 258 *dynamic_args, **self.init_data.get(f"{kwarg_name}_kwargs", {}), **(dynamic_kwargs or {}) 259 ) 260 261 def load_name(self, kwarg_name: str, optional: bool): 262 """@private 263 """ 264 self.trainer_kwargs[kwarg_name] = os.path.split(os.path.dirname(self.save_path))[1] 265 266 def load_optimizer(self, kwarg_name: str, optional: bool): 267 """@private 268 """ 269 self.load_generic(kwarg_name, self.trainer_kwargs["model"].parameters(), optional=optional) 270 271 def load_lr_scheduler(self, kwarg_name: str, optional: bool): 272 """@private 273 """ 274 self.load_generic(kwarg_name, self.trainer_kwargs["optimizer"], optional=optional) 275 276 # todo: remove and rename kwarg 'logger' to 'logger_class' 277 def load_logger(self, kwarg_name: str, optional: bool): 278 """@private 279 """ 280 assert kwarg_name == "logger" 281 self.load_generic("logger", optional=optional, only_class=True) 282 283 @staticmethod 284 def _get_save_dict(save_path, device): 285 if not os.path.exists(save_path): 286 raise ValueError(f"Cannot find checkpoint {save_path}") 287 return torch.load(save_path, map_location=device, weights_only=False) 288 289 @classmethod 290 def from_checkpoint( 291 cls, 292 checkpoint_folder: Union[os.PathLike, str], 293 name: Literal["best", "latest"] = "best", 294 device: Optional[Union[str, torch.device]] = None, 295 ): 296 """@private 297 """ 298 save_path = os.path.join(checkpoint_folder, f"{name}.pt") 299 # make sure the correct device is set if we don't have access to CUDA 300 if not torch.cuda.is_available(): 301 device = "cpu" 302 save_dict = cls._get_save_dict(save_path, device) 303 deserializer = cls.Deserializer(save_dict["init"], save_path, device) 304 305 has_kwargs = False 306 deserialized = [] 307 for name, parameter in inspect.signature(cls).parameters.items(): 308 if name == "kwargs": 309 has_kwargs = True 310 continue 311 deserializer.load(name, optional=parameter.default is not inspect.Parameter.empty) 312 deserialized.append(name) 313 314 # to deserialze kwargs we can't rely on inspecting the signature, so we 315 # go through the remaning kwarg names in init data instead 316 if has_kwargs: 317 kwarg_names = list(set(deserializer.init_data.keys()) - set(deserialized)) 318 for name in kwarg_names: 319 if name.endswith("_kwargs"): 320 continue 321 elif name.endswith("_dataset"): 322 deserializer.load(name.replace("dataset", "loader"), optional=False) 323 elif name.endswith("_class"): 324 deserializer.load(name.replace("_class", ""), optional=False) 325 else: 326 deserializer.load(name, optional=False) 327 328 trainer = cls(**deserializer.trainer_kwargs) 329 trainer._initialize(0, save_dict) 330 trainer._is_initialized = True 331 return trainer 332 333 class Serializer: 334 """Implements how to serialize trainer kwargs from a trainer instance. 335 336 Examples: 337 To extend the serialization process you can inherite from this Serializer in a derived Trainer class. 338 Note that the methods `dump_generic_builtin()`, `dump_generic_class()` and `dump_generic_instance()` 339 called by the `dump()` method when appropriate cover most cases already. 340 341 This example adds `the_answer` kwarg, which requires extra steps on dumping only because we don't keep a 342 'the_answer' attribute: 343 >>> class MyTrainer(DefaultTrainer): 344 >>> def __init__(self, *args, the_answer: int, **kwargs): 345 >>> super().__init__(*args, **kwargs) 346 >>> # self.the_answer = the_answer # this would allow the default Serializer to save the new kwarg, 347 >>> # but let's make things more interesting... 348 >>> self.the = the_answer // 10 349 >>> self.answer = the_answer % 10 350 >>> 351 >>> class Serializer(DefaultTrainer.Serializer): 352 >>> trainer: MyTrainer 353 >>> def dump_the_answer(self, kwarg_name: str) -> None: # custom dump method for 'the_answer' kwarg 354 >>> assert kwarg_name == "the_answer" 355 >>> # populate self.init_data with the serialized data required by Deserializer 356 >>> # to restore the trainer kwargs 357 >>> self.init_data["the_answer"] = self.trainer.the * 10 + self.trainer.answer 358 359 This example with both Serializer and Deserializer adds `the_answer` kwarg, 360 while saving it in two separate entries 'the' and 'answer' 361 >>> class MyTrainer(DefaultTrainer): 362 >>> def __init__(self, *args, the_answer: int, **kwargs): 363 >>> super().__init__(*args, **kwargs) 364 >>> self.the_answer = the_answer 365 >>> 366 >>> class Serializer(DefaultTrainer.Serializer): 367 >>> trainer: MyTrainer 368 >>> def dump_the_answer(self, kwarg_name: str): 369 >>> assert kwarg_name == "the_answer" 370 >>> self.init_data.update({ 371 >>> "the": self.trainer.the_answer // 10, 372 >>> "answer": self.trainer.the_answer % 10 373 >>> }) 374 >>> 375 >>> class Deserializer(DefaultTrainer.Deserializer): 376 >>> def load_the_answer(self, kwarg_name: str, optional: bool): 377 >>> assert kwarg_name == "the_answer" 378 >>> # 'optional' is True if MyTrainer.__init__ specifies a default value for 'kwarg_name' 379 >>> self.trainer_kwargs[kwarg_name] = self.init_data["the"] * 10 + self.init_data["answer"] 380 381 Args: 382 trainer: The trainer instance. 383 """ 384 385 def __init__(self, trainer: DefaultTrainer): 386 self.trainer = trainer 387 self.init_data = {} # to be populated during serialization process 388 389 def dump(self, kwarg_name: str) -> None: 390 """@private 391 """ 392 dumper = getattr(self, f"dump_{kwarg_name}", None) 393 if dumper is not None: 394 dumper(kwarg_name) 395 elif kwarg_name.endswith("_loader"): 396 self.dump_data_loader(kwarg_name) 397 elif kwarg_name.endswith("_class"): 398 self.dump_generic_class(kwarg_name) 399 elif not hasattr(self.trainer, kwarg_name): 400 raise AttributeError( 401 f"{self.trainer.__class__} missing attribute '{kwarg_name}' " 402 f"or special dump method {self.trainer.__class__}.Serializer.dump_{kwarg_name}()" 403 ) 404 else: 405 assert hasattr(self.trainer, kwarg_name) 406 obj = getattr(self.trainer, kwarg_name) 407 if obj is None or type(obj) in ( 408 bool, 409 bytearray, 410 bytes, 411 dict, 412 float, 413 frozenset, 414 int, 415 list, 416 set, 417 str, 418 tuple, 419 ): 420 self.dump_generic_builtin(kwarg_name) 421 else: 422 self.dump_generic_instance(kwarg_name) 423 424 def dump_generic_builtin(self, kwarg_name: str) -> None: 425 """@private 426 """ 427 assert hasattr(self.trainer, kwarg_name) 428 self.init_data[kwarg_name] = getattr(self.trainer, kwarg_name) 429 430 def dump_generic_class(self, kwarg_name: str) -> None: 431 """@private 432 """ 433 assert hasattr(self.trainer, kwarg_name) 434 assert kwarg_name.endswith("_class") 435 obj = getattr(self.trainer, kwarg_name) 436 self.init_data[kwarg_name] = None if obj is None else f"{obj.__module__}.{obj.__name__}" 437 438 def dump_generic_instance(self, kwarg_name: str) -> None: 439 """@private 440 """ 441 assert hasattr(self.trainer, kwarg_name) 442 instance = getattr(self.trainer, kwarg_name) 443 self.init_data.update( 444 { 445 f"{kwarg_name}_class": f"{instance.__class__.__module__}.{instance.__class__.__name__}", 446 f"{kwarg_name}_kwargs": get_constructor_arguments(instance), 447 } 448 ) 449 450 def dump_device(self, kwarg_name: str): 451 """@private 452 """ 453 assert hasattr(self.trainer, kwarg_name) 454 self.init_data[kwarg_name] = str(getattr(self.trainer, kwarg_name)) 455 456 def dump_data_loader(self, kwarg_name: str) -> None: 457 """@private 458 """ 459 assert hasattr(self.trainer, kwarg_name) 460 loader = getattr(self.trainer, kwarg_name) 461 if loader is None: 462 return 463 self.init_data.update( 464 { 465 f"{kwarg_name.replace('_loader', '_dataset')}": loader.dataset, 466 f"{kwarg_name}_kwargs": get_constructor_arguments(loader), 467 } 468 ) 469 470 def dump_logger(self, kwarg_name: str): # todo: remove and rename kwarg 'logger' to 'logger_class' 471 """@private 472 """ 473 self.dump_generic_class(f"{kwarg_name}_class") 474 475 def dump_model(self, kwarg_name: str): 476 """@private 477 """ 478 if is_compiled(self.trainer.model): 479 self.init_data.update( 480 {"model_class": self.trainer._model_class, "model_kwargs": self.trainer._model_kwargs} 481 ) 482 else: 483 self.dump_generic_instance("model") 484 485 def _build_init(self) -> Dict[str, Any]: 486 serializer = self.Serializer(self) 487 for name in inspect.signature(self.__class__).parameters: 488 # special rules to serialize kwargs 489 # if a trainer class inherits from DefaultTrainer and has **kwargs 490 # they need to be saved in self._kwargs 491 if name == "kwargs": 492 if not hasattr(self, "_kwargs"): 493 msg = "The trainer class has **kwargs in its signature, but is missing the _kwargs attribute. " +\ 494 "Please add self._kwargs to its __init__ function" 495 raise RuntimeError(msg) 496 kwargs = getattr(self, "_kwargs") 497 for kwarg_name in kwargs: 498 serializer.dump(kwarg_name) 499 continue 500 serializer.dump(name) 501 502 return serializer.init_data 503 504 def _initialize(self, iterations, load_from_checkpoint, epochs=None): 505 assert self.train_loader is not None 506 assert self.val_loader is not None 507 assert self.model is not None 508 assert self.loss is not None 509 assert self.optimizer is not None 510 assert self.metric is not None 511 assert self.device is not None 512 513 if load_from_checkpoint is not None: 514 self.load_checkpoint(load_from_checkpoint) 515 516 if sum((iterations is not None, epochs is not None)) != 1: 517 raise ValueError( 518 "Exactly one of 'iterations' or 'epochs' has to be specified to initialize the trainer." 519 f"You have passed 'iterations'={iterations} and 'epochs'={epochs}" 520 ) 521 522 if epochs is None: 523 epochs = int(np.ceil(float(iterations) / len(self.train_loader))) 524 else: 525 iterations = epochs * len(self.train_loader) 526 527 self.max_iteration = self._iteration + iterations 528 self.max_epoch = self._epoch + epochs 529 530 if not getattr(self, "_is_initialized", False): 531 # check if we compile the model (only supported by pytorch 2) 532 # to enable (de)serialization of compiled models, we keep track of the model class and kwargs 533 if is_compiled(self.model): 534 warnings.warn( 535 "You have passed a compiled model to the trainer." 536 "It will not be possible to (de)serialize the trainer with it." 537 "If you want to be able to do this please pass the normal model." 538 "It can be automatically compiled by setting 'compile_model' to True" 539 ) 540 self._model_class = f"{self.model.__class__.__module__}.{self.model.__class__.__name__}" 541 self._model_kwargs = get_constructor_arguments(self.model) 542 self.model = auto_compile(self.model, self.compile_model) 543 544 self.model.to(self.device) 545 self.loss.to(self.device) 546 547 # this saves all the information that is necessary 548 # to fully load the trainer from the checkpoint 549 self.init_data = self._build_init() 550 551 if self.logger_class is None: 552 self.logger = None 553 else: 554 # may set self.name if self.name is None 555 save_root = getattr(self, "save_root", None) 556 try: 557 self.logger = self.logger_class(self, save_root, **(self.logger_kwargs or {})) 558 except (PermissionError, RuntimeError): 559 warnings.warn( 560 f"The checkpoint folder at {self.checkpoint_folder} could not be created." 561 "The most likely reason for this is that you copied the checkpoint somewhere else," 562 "so we skip this error to enable loading the model from this checkpoint." 563 ) 564 565 try: 566 os.makedirs(self.checkpoint_folder, exist_ok=True) 567 except PermissionError: 568 warnings.warn( 569 f"The checkpoint folder at {self.checkpoint_folder} could not be created." 570 "The most likely reason for this is that you copied the checkpoint somewhere else," 571 "so we skip this error to enable loading the model from this checkpoint." 572 ) 573 pass 574 575 best_metric = np.inf 576 return best_metric 577 578 def save_checkpoint(self, name, current_metric, best_metric, train_time=0.0, **extra_save_dict): 579 """@private 580 """ 581 save_path = os.path.join(self.checkpoint_folder, f"{name}.pt") 582 extra_init_dict = extra_save_dict.pop("init", {}) 583 save_dict = { 584 "iteration": self._iteration, 585 "epoch": self._epoch, 586 "best_epoch": self._best_epoch, 587 "best_metric": best_metric, 588 "current_metric": current_metric, 589 "model_state": self.model.state_dict(), 590 "optimizer_state": self.optimizer.state_dict(), 591 "init": self.init_data | extra_init_dict, 592 "train_time": train_time, 593 "timestamp": datetime.now().strftime("%d-%m-%Y (%H:%M:%S)"), 594 } 595 save_dict.update(**extra_save_dict) 596 if self.scaler is not None: 597 save_dict.update({"scaler_state": self.scaler.state_dict()}) 598 if self.lr_scheduler is not None: 599 save_dict.update({"scheduler_state": self.lr_scheduler.state_dict()}) 600 601 rank = getattr(self, "rank", None) 602 if rank is None or rank == 0: 603 torch.save(save_dict, save_path) 604 605 def load_checkpoint(self, checkpoint="best"): 606 """@private 607 """ 608 if isinstance(checkpoint, str): 609 save_path = os.path.join(self.checkpoint_folder, f"{checkpoint}.pt") 610 if not os.path.exists(save_path): 611 warnings.warn(f"Cannot load checkpoint. {save_path} does not exist.") 612 return 613 save_dict = torch.load(save_path, weights_only=False) 614 elif isinstance(checkpoint, dict): 615 save_dict = checkpoint 616 else: 617 raise RuntimeError 618 619 self._iteration = save_dict["iteration"] 620 self._epoch = save_dict["epoch"] 621 self._best_epoch = save_dict["best_epoch"] 622 self.best_metric = save_dict["best_metric"] 623 self.current_metric = save_dict["current_metric"] 624 self.train_time = save_dict.get("train_time", 0.0) 625 626 model_state = save_dict["model_state"] 627 # to enable loading compiled models 628 compiled_prefix = "_orig_mod." 629 model_state = OrderedDict( 630 [(k[len(compiled_prefix):] if k.startswith(compiled_prefix) else k, v) for k, v in model_state.items()] 631 ) 632 self.model.load_state_dict(model_state) 633 # we need to send the network to the device before loading the optimizer state! 634 self.model.to(self.device) 635 636 self.optimizer.load_state_dict(save_dict["optimizer_state"]) 637 scaler_state = save_dict.get("scaler_state") 638 if self.scaler is not None and scaler_state: 639 self.scaler.load_state_dict(scaler_state) 640 if self.lr_scheduler is not None: 641 self.lr_scheduler.load_state_dict(save_dict["scheduler_state"]) 642 643 return save_dict 644 645 def _verify_if_training_completed(self, checkpoint="latest"): 646 save_path = os.path.join(self.checkpoint_folder, f"{checkpoint}.pt") 647 save_dict = torch.load(save_path, weights_only=False) if os.path.exists(save_path) else None 648 if save_dict and self.max_iteration == save_dict.get("iteration"): 649 return True 650 return False 651 652 def fit( 653 self, 654 iterations: Optional[int] = None, 655 load_from_checkpoint: Optional[Union[os.PathLike, str]] = None, 656 epochs: Optional[int] = None, 657 save_every_kth_epoch: Optional[int] = None, 658 progress=None, 659 overwrite_training: bool = True, 660 ): 661 """Run neural network training. 662 663 Exactly one of 'iterations' or 'epochs' has to be passed. 664 665 Args: 666 iterations: How long to train, specified in iterations. 667 load_from_checkpoint: Path to a checkpoint from where training should be continued . 668 epochs: How long to train, specified in epochs. 669 save_every_kth_epoch: Save checkpoints after every kth epoch in a separate file. 670 The corresponding checkpoints will be saved with the naming scheme 'epoch-{epoch}.pt'. 671 progress: Optional progress bar for integration with external tools. Expected to follow the tqdm interface. 672 overwrite_training: Whether to overwrite existing checkpoints in the save directory. 673 """ 674 best_metric = self._initialize(iterations, load_from_checkpoint, epochs) 675 676 if not overwrite_training: 677 if load_from_checkpoint is not None: 678 raise ValueError( 679 "We do not support 'overwrite_training=False' and 'load_from_checkpoint' at the same time." 680 ) 681 682 if self._verify_if_training_completed(): 683 print( 684 f"The model is trained for {self.max_iteration} iterations / {self.max_epoch} epochs " 685 "and 'overwrite_training' is set to 'False'." 686 ) 687 print(f"The checkpoints are located at '{os.path.abspath(self.checkpoint_folder)}'.") 688 return 689 690 print( 691 "Start fitting for", 692 self.max_iteration - self._iteration, 693 "iterations / ", 694 self.max_epoch - self._epoch, 695 "epochs", 696 ) 697 print("with", len(self.train_loader), "iterations per epoch") 698 699 if self.mixed_precision: 700 train_epoch = self._train_epoch_mixed 701 validate = self._validate_mixed 702 print("Training with mixed precision") 703 else: 704 train_epoch = self._train_epoch 705 validate = self._validate 706 print("Training with single precision") 707 708 total_iterations = epochs * len(self.train_loader) if iterations is None else iterations 709 if progress is None: 710 progress = tqdm(total=total_iterations, desc=f"Epoch {self._epoch}", leave=True) 711 else: 712 progress.total = total_iterations 713 progress.set_description(f"Epoch {self._epoch}") 714 715 msg = "Epoch %i: average [s/it]: %f, current metric: %f, best metric: %f" 716 train_epochs = self.max_epoch - self._epoch 717 t_start = time.time() 718 for epoch in range(train_epochs): 719 720 # Ensure data is shuffled differently at each epoch. 721 try: 722 self.train_loader.sampler.set_epoch(epoch) 723 except AttributeError: 724 pass 725 726 # Run training and validation for this epoch 727 t_per_iter = train_epoch(progress) 728 current_metric = validate() 729 730 # perform all the post-epoch steps: 731 732 # apply the learning rate scheduler 733 if self.lr_scheduler is not None: 734 self.lr_scheduler.step(current_metric) 735 736 # how long did we train in total? 737 total_train_time = (time.time() - t_start) + self.train_time 738 739 # save this checkpoint as the new best checkpoint if 740 # it has the best overall validation metric 741 if current_metric < best_metric: 742 best_metric = current_metric 743 self._best_epoch = self._epoch 744 self.save_checkpoint("best", current_metric, best_metric, train_time=total_train_time) 745 746 # save this checkpoint as the latest checkpoint 747 self.save_checkpoint("latest", current_metric, best_metric, train_time=total_train_time) 748 749 # if we save after every k-th epoch then check if we need to save now 750 if save_every_kth_epoch is not None and (self._epoch + 1) % save_every_kth_epoch == 0: 751 self.save_checkpoint( 752 f"epoch-{self._epoch + 1}", current_metric, best_metric, train_time=total_train_time 753 ) 754 755 # if early stopping has been specified then check if the stopping condition is met 756 if self.early_stopping is not None: 757 epochs_since_best = self._epoch - self._best_epoch 758 if epochs_since_best > self.early_stopping: 759 print("Stopping training because there has been no improvement for", self.early_stopping, "epochs") 760 break 761 762 self._epoch += 1 763 progress.set_description(msg % (self._epoch, t_per_iter, current_metric, best_metric), refresh=True) 764 765 print(f"Finished training after {self._epoch} epochs / {self._iteration} iterations.") 766 print(f"The best epoch is number {self._best_epoch}.") 767 768 if self._generate_name: 769 self.name = None 770 771 # Update the train time 772 self.train_time = total_train_time 773 774 # TODO save the model to wandb if we have the wandb logger 775 if isinstance(self.logger, WandbLogger): 776 self.logger.get_wandb().finish() 777 778 def _backprop(self, loss): 779 loss.backward() 780 self.optimizer.step() 781 782 def _backprop_mixed(self, loss): 783 self.scaler.scale(loss).backward() 784 self.scaler.step(self.optimizer) 785 self.scaler.update() 786 787 def _train_epoch(self, progress): 788 return self._train_epoch_impl(progress, contextlib.nullcontext, self._backprop) 789 790 def _train_epoch_mixed(self, progress): 791 return self._train_epoch_impl( 792 progress, 793 partial(torch.autocast, device_type=self._device_type, dtype=getattr(torch, self.mixed_precision_dtype)), 794 self._backprop_mixed 795 ) 796 797 def _forward_and_loss(self, x, y): 798 pred = self.model(x) 799 if self._iteration % self.log_image_interval == 0: 800 if pred.requires_grad: 801 pred.retain_grad() 802 803 loss = self.loss(pred, y) 804 return pred, loss 805 806 def _train_epoch_impl(self, progress, forward_context, backprop: Callable[[torch.Tensor], None]): 807 self.model.train() 808 809 n_iter = 0 810 t_per_iter = time.time() 811 for x, y in self.train_loader: 812 x, y = x.to(self.device, non_blocking=True), y.to(self.device, non_blocking=True) 813 814 self.optimizer.zero_grad() 815 816 with forward_context(): 817 pred, loss = self._forward_and_loss(x, y) 818 819 backprop(loss) 820 821 lr = [pm["lr"] for pm in self.optimizer.param_groups][0] 822 if self.logger is not None: 823 self.logger.log_train(self._iteration, loss, lr, x, y, pred, log_gradients=True) 824 825 self._iteration += 1 826 n_iter += 1 827 if self._iteration >= self.max_iteration: 828 break 829 progress.update(1) 830 831 t_per_iter = (time.time() - t_per_iter) / n_iter 832 return t_per_iter 833 834 def _validate(self): 835 return self._validate_impl(contextlib.nullcontext) 836 837 def _validate_mixed(self): 838 return self._validate_impl( 839 partial(torch.autocast, device_type=self._device_type, dtype=getattr(torch, self.mixed_precision_dtype)) 840 ) 841 842 def _validate_impl(self, forward_context): 843 self.model.eval() 844 845 metric_val = 0.0 846 loss_val = 0.0 847 848 with torch.no_grad(): 849 for x, y in self.val_loader: 850 x, y = x.to(self.device, non_blocking=True), y.to(self.device, non_blocking=True) 851 with forward_context(): 852 pred, loss = self._forward_and_loss(x, y) 853 metric = self.metric(pred, y) 854 855 loss_val += loss.item() 856 metric_val += metric.item() 857 858 metric_val /= len(self.val_loader) 859 loss_val /= len(self.val_loader) 860 if self.logger is not None: 861 self.logger.log_validation(self._iteration, metric_val, loss_val, x, y, pred) 862 return metric_val
Trainer class for training a segmentation network.
The trainer class implements the core logic for training a network with pytorch.
It implements a training loop to run training and validation, which is started with fit.
The checkpoints and logs from the training run will be saved in the current working directory,
or in the directory specifified by save_root. Training can be continued from a checkpoint
by passing it's location to the load_from_checkpoint argument of fit.
A pre-configured instance of the trainer can be obtained from torch_em.default_segmentation_trainer.
Alternatively, the trainer class can also be instantiated as in this example:
import torch
from torch_em.loss import DiceLoss
from torch_em.model import UNet2d
from torch_em.data.datasets.light_microscopy import get_dsb_loader
from torch_em.trainer import DefaultTrainer
# The training data will be downloaded to this location.
data_root = "/path/to/save/the/training/data"
patch_shape = (256, 256)
# Create the model and optimizer.
model = UNet2d(in_channels=1, out_channels=1)
optimizer = torch.optim.AdamW(model.parameters())
trainer = DefaultTrainer(
name="unet-training",
train_loader=get_dsb_loader(path=data_root, patch_shape=patch_shape, split="train"),
val_loader=get_dsb_loader(path=data_root, patch_shape=patch_shape, split="test"),
model=model,
loss=DiceLoss(), # The loss function.
optimizer=optimizer,
metric=DiceLoss(), # The metric. The trainer expects smaller values to represent better results.
device="cuda", # The device to use for training.
)
trainer.fit(iterations=int(2.5e4)) # Train for 25.000 iterations.
Arguments:
- name: The name of the checkpoint that will be created by the trainer.
- train_loader: The data loader containing the training data.
- val_loader: The data loader containing the validation data.
- model: The model to train.
- loss: The loss function for training.
- optimizer: The optimizer.
- metric: The metric for validation.
- device: The torch device to use for training. If None, will use a GPU if available.
- lr_scheduler: The learning rate scheduler.
- log_image_interval: The interval for saving images during logging, in training iterations.
- mixed_precision: Whether to train with mixed precision.
- early_stopping: The patience for early stopping in epochs. If None, early stopping will not be used.
- logger: The logger class. Will be instantiated for logging.
By default uses
torch_em.training.tensorboard_logger.TensorboardLogger. - logger_kwargs: The keyword arguments for the logger class.
- id_: Unique identifier for the trainer. If None then
namewill be used. - save_root: The root folder for saving the checkpoint and logs.
- compile_model: Whether to compile the model before training.
- rank: Rank argument for distributed training. See
torch_em.multi_gpu_trainingfor details. - mixed_precision_dtype: The dtype for autocast in mixed precision training, 'float16' or 'bfloat16'. The default is 'float16' on the GPU and 'bfloat16' on the CPU. Use 'bfloat16' to avoid overflows.
87 def __init__( 88 self, 89 name: Optional[str], 90 train_loader: torch.utils.data.DataLoader, 91 val_loader: torch.utils.data.DataLoader, 92 model: torch.nn.Module, 93 loss: torch.nn.Module, 94 optimizer: torch.optim.Optimizer, 95 metric: Callable, 96 device: Union[str, torch.device], 97 lr_scheduler: Optional[torch.optim.lr_scheduler._LRScheduler] = None, 98 log_image_interval: int = 100, 99 mixed_precision: bool = True, 100 early_stopping: Optional[int] = None, 101 logger=TensorboardLogger, 102 logger_kwargs: Optional[Dict[str, Any]] = None, 103 id_: Optional[str] = None, 104 save_root: Optional[str] = None, 105 compile_model: Optional[Union[bool, str]] = None, 106 rank: Optional[int] = None, 107 mixed_precision_dtype: Optional[str] = None, 108 ): 109 if name is None and not issubclass(logger, WandbLogger): 110 raise TypeError("Name cannot be None if not using the WandbLogger") 111 112 self._generate_name = name is None 113 self.name = name 114 self.id_ = id_ or name 115 self.train_loader = train_loader 116 self.val_loader = val_loader 117 self.model = model 118 self.loss = loss 119 self.optimizer = optimizer 120 self.metric = metric 121 self.device = torch.device(device) 122 self.lr_scheduler = lr_scheduler 123 self.log_image_interval = log_image_interval 124 self.save_root = save_root 125 self.compile_model = compile_model 126 self.rank = rank 127 self._device_type = "cpu" if self.device.type == "cpu" else "cuda" 128 129 self._iteration = 0 130 self._epoch = 0 131 self._best_epoch = 0 132 133 self.mixed_precision = mixed_precision 134 # These are the defaults of torch.autocast for each device type. 135 self.mixed_precision_dtype = mixed_precision_dtype or ("bfloat16" if self._device_type == "cpu" else "float16") 136 self.early_stopping = early_stopping 137 self.train_time = 0.0 138 139 if mixed_precision: 140 # Only float16 needs gradient scaling. bfloat16 has the same range as float32. 141 self.scaler = torch.GradScaler(self._device_type, enabled=self.mixed_precision_dtype == "float16") 142 else: 143 self.scaler = None 144 145 self.logger_class = logger 146 self.logger_kwargs = logger_kwargs 147 self.log_image_interval = log_image_interval
149 @property 150 def checkpoint_folder(self): 151 assert self.id_ is not None # Because the logger may generate and set trainer.id on logger.__init__. 152 # Save_root enables saving the checkpoints somewhere else than in the local folder. 153 # This is handy for filesystems with limited space, where saving the checkpoints 154 # and log files can lead to running out of space. 155 save_root = getattr(self, "save_root", None) 156 return os.path.join("./checkpoints", self.id_) if save_root is None else\ 157 os.path.join(save_root, "./checkpoints", self.id_)
652 def fit( 653 self, 654 iterations: Optional[int] = None, 655 load_from_checkpoint: Optional[Union[os.PathLike, str]] = None, 656 epochs: Optional[int] = None, 657 save_every_kth_epoch: Optional[int] = None, 658 progress=None, 659 overwrite_training: bool = True, 660 ): 661 """Run neural network training. 662 663 Exactly one of 'iterations' or 'epochs' has to be passed. 664 665 Args: 666 iterations: How long to train, specified in iterations. 667 load_from_checkpoint: Path to a checkpoint from where training should be continued . 668 epochs: How long to train, specified in epochs. 669 save_every_kth_epoch: Save checkpoints after every kth epoch in a separate file. 670 The corresponding checkpoints will be saved with the naming scheme 'epoch-{epoch}.pt'. 671 progress: Optional progress bar for integration with external tools. Expected to follow the tqdm interface. 672 overwrite_training: Whether to overwrite existing checkpoints in the save directory. 673 """ 674 best_metric = self._initialize(iterations, load_from_checkpoint, epochs) 675 676 if not overwrite_training: 677 if load_from_checkpoint is not None: 678 raise ValueError( 679 "We do not support 'overwrite_training=False' and 'load_from_checkpoint' at the same time." 680 ) 681 682 if self._verify_if_training_completed(): 683 print( 684 f"The model is trained for {self.max_iteration} iterations / {self.max_epoch} epochs " 685 "and 'overwrite_training' is set to 'False'." 686 ) 687 print(f"The checkpoints are located at '{os.path.abspath(self.checkpoint_folder)}'.") 688 return 689 690 print( 691 "Start fitting for", 692 self.max_iteration - self._iteration, 693 "iterations / ", 694 self.max_epoch - self._epoch, 695 "epochs", 696 ) 697 print("with", len(self.train_loader), "iterations per epoch") 698 699 if self.mixed_precision: 700 train_epoch = self._train_epoch_mixed 701 validate = self._validate_mixed 702 print("Training with mixed precision") 703 else: 704 train_epoch = self._train_epoch 705 validate = self._validate 706 print("Training with single precision") 707 708 total_iterations = epochs * len(self.train_loader) if iterations is None else iterations 709 if progress is None: 710 progress = tqdm(total=total_iterations, desc=f"Epoch {self._epoch}", leave=True) 711 else: 712 progress.total = total_iterations 713 progress.set_description(f"Epoch {self._epoch}") 714 715 msg = "Epoch %i: average [s/it]: %f, current metric: %f, best metric: %f" 716 train_epochs = self.max_epoch - self._epoch 717 t_start = time.time() 718 for epoch in range(train_epochs): 719 720 # Ensure data is shuffled differently at each epoch. 721 try: 722 self.train_loader.sampler.set_epoch(epoch) 723 except AttributeError: 724 pass 725 726 # Run training and validation for this epoch 727 t_per_iter = train_epoch(progress) 728 current_metric = validate() 729 730 # perform all the post-epoch steps: 731 732 # apply the learning rate scheduler 733 if self.lr_scheduler is not None: 734 self.lr_scheduler.step(current_metric) 735 736 # how long did we train in total? 737 total_train_time = (time.time() - t_start) + self.train_time 738 739 # save this checkpoint as the new best checkpoint if 740 # it has the best overall validation metric 741 if current_metric < best_metric: 742 best_metric = current_metric 743 self._best_epoch = self._epoch 744 self.save_checkpoint("best", current_metric, best_metric, train_time=total_train_time) 745 746 # save this checkpoint as the latest checkpoint 747 self.save_checkpoint("latest", current_metric, best_metric, train_time=total_train_time) 748 749 # if we save after every k-th epoch then check if we need to save now 750 if save_every_kth_epoch is not None and (self._epoch + 1) % save_every_kth_epoch == 0: 751 self.save_checkpoint( 752 f"epoch-{self._epoch + 1}", current_metric, best_metric, train_time=total_train_time 753 ) 754 755 # if early stopping has been specified then check if the stopping condition is met 756 if self.early_stopping is not None: 757 epochs_since_best = self._epoch - self._best_epoch 758 if epochs_since_best > self.early_stopping: 759 print("Stopping training because there has been no improvement for", self.early_stopping, "epochs") 760 break 761 762 self._epoch += 1 763 progress.set_description(msg % (self._epoch, t_per_iter, current_metric, best_metric), refresh=True) 764 765 print(f"Finished training after {self._epoch} epochs / {self._iteration} iterations.") 766 print(f"The best epoch is number {self._best_epoch}.") 767 768 if self._generate_name: 769 self.name = None 770 771 # Update the train time 772 self.train_time = total_train_time 773 774 # TODO save the model to wandb if we have the wandb logger 775 if isinstance(self.logger, WandbLogger): 776 self.logger.get_wandb().finish()
Run neural network training.
Exactly one of 'iterations' or 'epochs' has to be passed.
Arguments:
- iterations: How long to train, specified in iterations.
- load_from_checkpoint: Path to a checkpoint from where training should be continued .
- epochs: How long to train, specified in epochs.
- save_every_kth_epoch: Save checkpoints after every kth epoch in a separate file. The corresponding checkpoints will be saved with the naming scheme 'epoch-{epoch}.pt'.
- progress: Optional progress bar for integration with external tools. Expected to follow the tqdm interface.
- overwrite_training: Whether to overwrite existing checkpoints in the save directory.
167 class Deserializer: 168 """Determines how to deserialize the trainer kwargs from serialized 'init_data'. 169 170 Examples: 171 To extend the initialization process you can inherite from this Deserializer in an inherited Trainer class. 172 Note that `DefaultTrainer.Deserializer.load_generic()` covers most cases already. 173 174 This example adds `the_answer` kwarg, which requires 'calculations' upon initialization: 175 >>> class MyTrainer(DefaultTrainer): 176 >>> def __init__(self, *args, the_answer: int, **kwargs): 177 >>> super().__init__(*args, **kwargs) 178 >>> self.the_answer = the_answer # this allows the default Serializer to save the new kwarg, 179 >>> # see DefaultTrainer.Serializer 180 >>> 181 >>> class Deserializer(DefaultTrainer.Deserializer): 182 >>> def load_the_answer(self): 183 >>> generic_answer = self.init_data["the_answer"] 184 >>> # (device dependent) special deserialization 185 >>> if self.trainer_kwargs["device"].type == "cpu": # accessing previously deserialized kwarg 186 >>> self.trainer_kwargs["the_answer"] = generic_answer + 1 187 >>> else: 188 >>> self.trainer_kwargs["the_answer"] = generic_answer * 2 189 190 Args: 191 init_data: The initialization data of the trainer. 192 save_path: The path where the checkpoint was saved. 193 device: The device. 194 """ 195 196 def __init__(self, init_data: Dict, save_path: str, device: Union[str, torch.device]): 197 self.init_data = init_data 198 self.save_path = save_path 199 # Populate with deserialized trainer kwargs during deserialization; possibly overwrite 'device'. 200 self.trainer_kwargs: Dict[str, Any] = dict( 201 device=torch.device(self.init_data["device"]) if device is None else torch.device(device) 202 ) 203 204 def load(self, kwarg_name: str, optional): 205 """@private 206 """ 207 # `optional` is True if self.trainer.__class__.__init__ specifies a default value for 'kwarg_name' 208 if kwarg_name == "device": 209 pass # deserialized in __init__ 210 elif kwarg_name.endswith("_loader"): 211 self.load_data_loader(kwarg_name, optional) 212 else: 213 load = getattr(self, f"load_{kwarg_name}", self.load_generic) 214 load(kwarg_name, optional=optional) 215 216 def load_data_loader(self, loader_name, optional) -> None: 217 """@private 218 """ 219 ds = self.init_data.get(loader_name.replace("_loader", "_dataset")) 220 if ds is None and optional: 221 return 222 223 loader_kwargs = self.init_data[f"{loader_name}_kwargs"] 224 loader = torch.utils.data.DataLoader(ds, **loader_kwargs) 225 # monkey patch shuffle loader_name to the loader 226 loader.shuffle = loader_kwargs.get("shuffle", False) 227 self.trainer_kwargs[loader_name] = loader 228 229 def load_generic( 230 self, 231 kwarg_name: str, 232 *dynamic_args: Dict, 233 optional: bool, 234 only_class: bool = False, 235 dynamic_kwargs: Optional[Dict[str, Any]] = None, 236 ) -> None: 237 """@private 238 """ 239 if kwarg_name in self.init_data: 240 self.trainer_kwargs[kwarg_name] = self.init_data[kwarg_name] 241 return 242 243 this_cls = self.init_data.get(f"{kwarg_name}_class", None) 244 if this_cls is None: 245 if optional: 246 return 247 else: 248 raise RuntimeError(f"Could not find init data for {kwarg_name} in {self.save_path}") 249 250 assert isinstance(this_cls, str), this_cls 251 assert "." in this_cls, this_cls 252 cls_p, cls_m = this_cls.rsplit(".", 1) 253 this_cls = getattr(import_module(cls_p), cls_m) 254 if only_class: 255 self.trainer_kwargs[kwarg_name] = this_cls 256 else: 257 self.trainer_kwargs[kwarg_name] = this_cls( 258 *dynamic_args, **self.init_data.get(f"{kwarg_name}_kwargs", {}), **(dynamic_kwargs or {}) 259 ) 260 261 def load_name(self, kwarg_name: str, optional: bool): 262 """@private 263 """ 264 self.trainer_kwargs[kwarg_name] = os.path.split(os.path.dirname(self.save_path))[1] 265 266 def load_optimizer(self, kwarg_name: str, optional: bool): 267 """@private 268 """ 269 self.load_generic(kwarg_name, self.trainer_kwargs["model"].parameters(), optional=optional) 270 271 def load_lr_scheduler(self, kwarg_name: str, optional: bool): 272 """@private 273 """ 274 self.load_generic(kwarg_name, self.trainer_kwargs["optimizer"], optional=optional) 275 276 # todo: remove and rename kwarg 'logger' to 'logger_class' 277 def load_logger(self, kwarg_name: str, optional: bool): 278 """@private 279 """ 280 assert kwarg_name == "logger" 281 self.load_generic("logger", optional=optional, only_class=True)
Determines how to deserialize the trainer kwargs from serialized 'init_data'.
Examples:
To extend the initialization process you can inherite from this Deserializer in an inherited Trainer class. Note that
DefaultTrainer.Deserializer.load_generic()covers most cases already.This example adds
the_answerkwarg, which requires 'calculations' upon initialization:>>> class MyTrainer(DefaultTrainer): >>> def __init__(self, *args, the_answer: int, **kwargs): >>> super().__init__(*args, **kwargs) >>> self.the_answer = the_answer # this allows the default Serializer to save the new kwarg, >>> # see DefaultTrainer.Serializer >>> >>> class Deserializer(DefaultTrainer.Deserializer): >>> def load_the_answer(self): >>> generic_answer = self.init_data["the_answer"] >>> # (device dependent) special deserialization >>> if self.trainer_kwargs["device"].type == "cpu": # accessing previously deserialized kwarg >>> self.trainer_kwargs["the_answer"] = generic_answer + 1 >>> else: >>> self.trainer_kwargs["the_answer"] = generic_answer * 2
Arguments:
- init_data: The initialization data of the trainer.
- save_path: The path where the checkpoint was saved.
- device: The device.
196 def __init__(self, init_data: Dict, save_path: str, device: Union[str, torch.device]): 197 self.init_data = init_data 198 self.save_path = save_path 199 # Populate with deserialized trainer kwargs during deserialization; possibly overwrite 'device'. 200 self.trainer_kwargs: Dict[str, Any] = dict( 201 device=torch.device(self.init_data["device"]) if device is None else torch.device(device) 202 )
333 class Serializer: 334 """Implements how to serialize trainer kwargs from a trainer instance. 335 336 Examples: 337 To extend the serialization process you can inherite from this Serializer in a derived Trainer class. 338 Note that the methods `dump_generic_builtin()`, `dump_generic_class()` and `dump_generic_instance()` 339 called by the `dump()` method when appropriate cover most cases already. 340 341 This example adds `the_answer` kwarg, which requires extra steps on dumping only because we don't keep a 342 'the_answer' attribute: 343 >>> class MyTrainer(DefaultTrainer): 344 >>> def __init__(self, *args, the_answer: int, **kwargs): 345 >>> super().__init__(*args, **kwargs) 346 >>> # self.the_answer = the_answer # this would allow the default Serializer to save the new kwarg, 347 >>> # but let's make things more interesting... 348 >>> self.the = the_answer // 10 349 >>> self.answer = the_answer % 10 350 >>> 351 >>> class Serializer(DefaultTrainer.Serializer): 352 >>> trainer: MyTrainer 353 >>> def dump_the_answer(self, kwarg_name: str) -> None: # custom dump method for 'the_answer' kwarg 354 >>> assert kwarg_name == "the_answer" 355 >>> # populate self.init_data with the serialized data required by Deserializer 356 >>> # to restore the trainer kwargs 357 >>> self.init_data["the_answer"] = self.trainer.the * 10 + self.trainer.answer 358 359 This example with both Serializer and Deserializer adds `the_answer` kwarg, 360 while saving it in two separate entries 'the' and 'answer' 361 >>> class MyTrainer(DefaultTrainer): 362 >>> def __init__(self, *args, the_answer: int, **kwargs): 363 >>> super().__init__(*args, **kwargs) 364 >>> self.the_answer = the_answer 365 >>> 366 >>> class Serializer(DefaultTrainer.Serializer): 367 >>> trainer: MyTrainer 368 >>> def dump_the_answer(self, kwarg_name: str): 369 >>> assert kwarg_name == "the_answer" 370 >>> self.init_data.update({ 371 >>> "the": self.trainer.the_answer // 10, 372 >>> "answer": self.trainer.the_answer % 10 373 >>> }) 374 >>> 375 >>> class Deserializer(DefaultTrainer.Deserializer): 376 >>> def load_the_answer(self, kwarg_name: str, optional: bool): 377 >>> assert kwarg_name == "the_answer" 378 >>> # 'optional' is True if MyTrainer.__init__ specifies a default value for 'kwarg_name' 379 >>> self.trainer_kwargs[kwarg_name] = self.init_data["the"] * 10 + self.init_data["answer"] 380 381 Args: 382 trainer: The trainer instance. 383 """ 384 385 def __init__(self, trainer: DefaultTrainer): 386 self.trainer = trainer 387 self.init_data = {} # to be populated during serialization process 388 389 def dump(self, kwarg_name: str) -> None: 390 """@private 391 """ 392 dumper = getattr(self, f"dump_{kwarg_name}", None) 393 if dumper is not None: 394 dumper(kwarg_name) 395 elif kwarg_name.endswith("_loader"): 396 self.dump_data_loader(kwarg_name) 397 elif kwarg_name.endswith("_class"): 398 self.dump_generic_class(kwarg_name) 399 elif not hasattr(self.trainer, kwarg_name): 400 raise AttributeError( 401 f"{self.trainer.__class__} missing attribute '{kwarg_name}' " 402 f"or special dump method {self.trainer.__class__}.Serializer.dump_{kwarg_name}()" 403 ) 404 else: 405 assert hasattr(self.trainer, kwarg_name) 406 obj = getattr(self.trainer, kwarg_name) 407 if obj is None or type(obj) in ( 408 bool, 409 bytearray, 410 bytes, 411 dict, 412 float, 413 frozenset, 414 int, 415 list, 416 set, 417 str, 418 tuple, 419 ): 420 self.dump_generic_builtin(kwarg_name) 421 else: 422 self.dump_generic_instance(kwarg_name) 423 424 def dump_generic_builtin(self, kwarg_name: str) -> None: 425 """@private 426 """ 427 assert hasattr(self.trainer, kwarg_name) 428 self.init_data[kwarg_name] = getattr(self.trainer, kwarg_name) 429 430 def dump_generic_class(self, kwarg_name: str) -> None: 431 """@private 432 """ 433 assert hasattr(self.trainer, kwarg_name) 434 assert kwarg_name.endswith("_class") 435 obj = getattr(self.trainer, kwarg_name) 436 self.init_data[kwarg_name] = None if obj is None else f"{obj.__module__}.{obj.__name__}" 437 438 def dump_generic_instance(self, kwarg_name: str) -> None: 439 """@private 440 """ 441 assert hasattr(self.trainer, kwarg_name) 442 instance = getattr(self.trainer, kwarg_name) 443 self.init_data.update( 444 { 445 f"{kwarg_name}_class": f"{instance.__class__.__module__}.{instance.__class__.__name__}", 446 f"{kwarg_name}_kwargs": get_constructor_arguments(instance), 447 } 448 ) 449 450 def dump_device(self, kwarg_name: str): 451 """@private 452 """ 453 assert hasattr(self.trainer, kwarg_name) 454 self.init_data[kwarg_name] = str(getattr(self.trainer, kwarg_name)) 455 456 def dump_data_loader(self, kwarg_name: str) -> None: 457 """@private 458 """ 459 assert hasattr(self.trainer, kwarg_name) 460 loader = getattr(self.trainer, kwarg_name) 461 if loader is None: 462 return 463 self.init_data.update( 464 { 465 f"{kwarg_name.replace('_loader', '_dataset')}": loader.dataset, 466 f"{kwarg_name}_kwargs": get_constructor_arguments(loader), 467 } 468 ) 469 470 def dump_logger(self, kwarg_name: str): # todo: remove and rename kwarg 'logger' to 'logger_class' 471 """@private 472 """ 473 self.dump_generic_class(f"{kwarg_name}_class") 474 475 def dump_model(self, kwarg_name: str): 476 """@private 477 """ 478 if is_compiled(self.trainer.model): 479 self.init_data.update( 480 {"model_class": self.trainer._model_class, "model_kwargs": self.trainer._model_kwargs} 481 ) 482 else: 483 self.dump_generic_instance("model")
Implements how to serialize trainer kwargs from a trainer instance.
Examples:
To extend the serialization process you can inherite from this Serializer in a derived Trainer class. Note that the methods
dump_generic_builtin(),dump_generic_class()anddump_generic_instance()called by thedump()method when appropriate cover most cases already.This example adds
the_answerkwarg, which requires extra steps on dumping only because we don't keep a 'the_answer' attribute:>>> class MyTrainer(DefaultTrainer): >>> def __init__(self, *args, the_answer: int, **kwargs): >>> super().__init__(*args, **kwargs) >>> # self.the_answer = the_answer # this would allow the default Serializer to save the new kwarg, >>> # but let's make things more interesting... >>> self.the = the_answer // 10 >>> self.answer = the_answer % 10 >>> >>> class Serializer(DefaultTrainer.Serializer): >>> trainer: MyTrainer >>> def dump_the_answer(self, kwarg_name: str) -> None: # custom dump method for 'the_answer' kwarg >>> assert kwarg_name == "the_answer" >>> # populate self.init_data with the serialized data required by Deserializer >>> # to restore the trainer kwargs >>> self.init_data["the_answer"] = self.trainer.the * 10 + self.trainer.answerThis example with both Serializer and Deserializer adds
the_answerkwarg, while saving it in two separate entries 'the' and 'answer'>>> class MyTrainer(DefaultTrainer): >>> def __init__(self, *args, the_answer: int, **kwargs): >>> super().__init__(*args, **kwargs) >>> self.the_answer = the_answer >>> >>> class Serializer(DefaultTrainer.Serializer): >>> trainer: MyTrainer >>> def dump_the_answer(self, kwarg_name: str): >>> assert kwarg_name == "the_answer" >>> self.init_data.update({ >>> "the": self.trainer.the_answer // 10, >>> "answer": self.trainer.the_answer % 10 >>> }) >>> >>> class Deserializer(DefaultTrainer.Deserializer): >>> def load_the_answer(self, kwarg_name: str, optional: bool): >>> assert kwarg_name == "the_answer" >>> # 'optional' is True if MyTrainer.__init__ specifies a default value for 'kwarg_name' >>> self.trainer_kwargs[kwarg_name] = self.init_data["the"] * 10 + self.init_data["answer"]
Arguments:
- trainer: The trainer instance.