supervised.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. from __future__ import annotations
  2. import time
  3. from typing import Any
  4. import torch
  5. from torch.utils.data import DataLoader
  6. from lib.modules import XNet2d
  7. from lib.tools import build_loss, build_optimizer, build_scheduler
  8. from .base import BaseTrainer
  9. class SupervisedSegmentationTrainer(BaseTrainer):
  10. def __init__(self, cfg: dict[str, Any], args: Any | None = None) -> None:
  11. super().__init__(cfg=cfg, args=args)
  12. self.model: XNet2d | None = None
  13. self.optimizer = None
  14. self.scheduler = None
  15. self.loader: DataLoader | None = None
  16. self.val_loader: DataLoader | None = None
  17. self.seg_loss = None
  18. def build(self) -> None:
  19. dataset_cfg = self.cfg["dataset"]
  20. model_cfg = self.cfg["model"]
  21. train_cfg = self.cfg["train"]
  22. self.model = XNet2d(
  23. in_channels=int(model_cfg.get("in_channels", dataset_cfg.get("in_channels", 3))),
  24. num_classes=int(dataset_cfg["num_classes"]),
  25. encoder_channels=tuple(model_cfg.get("encoder_channels", (32, 64, 128, 192))),
  26. encoder_depths=tuple(model_cfg.get("encoder_depths", (2, 2, 2, 2))),
  27. decoder_channels=tuple(model_cfg.get("decoder_channels", (128, 64, 32))),
  28. stem_channels=int(model_cfg.get("stem_channels", 24)),
  29. bottleneck_depth=int(model_cfg.get("bottleneck_depth", 1)),
  30. global_ratio=float(model_cfg.get("global_ratio", 2.0)),
  31. wavelet_type=str(model_cfg.get("wavelet_type", "haar")),
  32. wavelet_level=int(model_cfg.get("wavelet_level", 1)),
  33. use_wavelet_branch=bool(model_cfg.get("use_wavelet_branch", True)),
  34. use_global_branch_stage1=bool(model_cfg.get("use_global_branch_stage1", False)),
  35. ssm_d_state=int(model_cfg.get("ssm_d_state", 16)),
  36. ssm_forward_type=str(model_cfg.get("ssm_forward_type", "v3")),
  37. ssm_backend=str(model_cfg.get("ssm_backend", "auto")),
  38. use_frequency_refine=bool(model_cfg.get("use_frequency_refine", True)),
  39. low_freq_radius_h=float(model_cfg.get("low_freq_radius_h", 0.25)),
  40. low_freq_radius_w=float(model_cfg.get("low_freq_radius_w", 0.25)),
  41. learnable_low_freq_radius=bool(
  42. model_cfg.get("learnable_low_freq_radius", True)
  43. ),
  44. out_channels=model_cfg.get("out_channels"),
  45. ).to(self.device)
  46. self.optimizer = build_optimizer(self.model, self.cfg["optimizer"])
  47. self.scheduler = build_scheduler(self.optimizer, self.cfg.get("scheduler"))
  48. loss_cfg = self.cfg.get("loss")
  49. if loss_cfg is not None:
  50. self.seg_loss = build_loss(loss_cfg)
  51. self.loader = self._build_segmentation_loader(
  52. split=str(dataset_cfg.get("split", "train")),
  53. split_file=dataset_cfg.get("split_file"),
  54. batch_size=self._resolve_batch_size("batch_size", 4),
  55. shuffle=bool(train_cfg.get("shuffle", True)),
  56. augmentation_config=self.cfg.get("augmentation", {}).get("train"),
  57. )
  58. self.val_loader = self._build_val_loader(
  59. batch_size=self._resolve_batch_size(
  60. "val_batch_size",
  61. int(train_cfg.get("batch_size", 4)),
  62. ),
  63. shuffle=False,
  64. )
  65. self._maybe_resume(
  66. module_map={"model": self.model},
  67. optimizer=self.optimizer,
  68. scheduler=self.scheduler,
  69. )
  70. self._init_swanlab()
  71. def _compute_losses(
  72. self,
  73. image: torch.Tensor,
  74. mask: torch.Tensor,
  75. ) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]:
  76. if self.model is None:
  77. raise RuntimeError("Model is not initialized.")
  78. with torch.autocast(device_type=self.device.type, enabled=self._amp_enabled()):
  79. outputs = self.model(image)
  80. seg_logits = outputs["seg_logits"]
  81. if self.seg_loss is None:
  82. seg_loss = torch.nn.functional.binary_cross_entropy_with_logits(seg_logits, mask)
  83. else:
  84. seg_loss = self.seg_loss(seg_logits, mask)
  85. total_loss = seg_loss
  86. losses = {
  87. "total": total_loss,
  88. "seg": seg_loss,
  89. }
  90. return outputs, losses
  91. @staticmethod
  92. def _detach_metrics(losses: dict[str, torch.Tensor]) -> dict[str, float]:
  93. return {key: float(value.detach().cpu()) for key, value in losses.items()}
  94. def _validate(self) -> dict[str, float] | None:
  95. if self.model is None or self.val_loader is None:
  96. return None
  97. self.model.eval()
  98. metrics = self._build_validation_metrics()
  99. total = 0.0
  100. seg = 0.0
  101. steps = 0
  102. with torch.no_grad():
  103. for batch in self.val_loader:
  104. image = batch["image"].to(self.device)
  105. mask = batch["mask"].to(self.device)
  106. outputs, losses = self._compute_losses(image, mask)
  107. total += float(losses["total"].detach().cpu())
  108. seg += float(losses["seg"].detach().cpu())
  109. self._update_validation_metrics(
  110. metrics,
  111. logits=outputs["seg_logits"],
  112. target=mask,
  113. )
  114. steps += 1
  115. if steps == 0:
  116. return None
  117. val_metrics = {
  118. "total": total / steps,
  119. "seg": seg / steps,
  120. }
  121. val_metrics.update(self._compute_validation_metric_values(metrics))
  122. return val_metrics
  123. def train(self) -> None:
  124. if self.model is None or self.loader is None or self.optimizer is None:
  125. raise RuntimeError("Trainer.build() must be called before train().")
  126. epochs = int(self.cfg["train"].get("epochs", 1))
  127. accum_steps = self._accum_steps()
  128. try:
  129. self._print_training_setup(
  130. model_map={"model": self.model},
  131. loader_map={"train": self.loader, "val": self.val_loader},
  132. optimizer=self.optimizer,
  133. scheduler=self.scheduler,
  134. )
  135. for epoch in range(self.start_epoch, epochs):
  136. self.model.train()
  137. self.optimizer.zero_grad()
  138. train_metric_sums = {
  139. "total": 0.0,
  140. "seg": 0.0,
  141. }
  142. train_metrics: dict[str, float] | None = None
  143. end_time = time.perf_counter()
  144. num_steps = len(self.loader)
  145. for step, batch in enumerate(self.loader, start=1):
  146. data_time = time.perf_counter() - end_time
  147. iter_start = time.perf_counter()
  148. image = batch["image"].to(self.device)
  149. mask = batch["mask"].to(self.device)
  150. _, losses = self._compute_losses(image, mask)
  151. scaled_total_loss = losses["total"] / accum_steps
  152. self.grad_scaler.scale(scaled_total_loss).backward()
  153. grad_norm = None
  154. should_step = (step % accum_steps == 0) or (step == num_steps)
  155. if should_step:
  156. if self._grad_clip_enabled():
  157. self.grad_scaler.unscale_(self.optimizer)
  158. grad_norm = self._clip_gradients(self.model)
  159. self.grad_scaler.step(self.optimizer)
  160. self.grad_scaler.update()
  161. self.optimizer.zero_grad()
  162. train_metrics = self._detach_metrics(losses)
  163. if grad_norm is not None:
  164. train_metrics["grad_norm"] = grad_norm
  165. for key, value in train_metrics.items():
  166. train_metric_sums.setdefault(key, 0.0)
  167. train_metric_sums[key] += value
  168. iter_time = time.perf_counter() - iter_start
  169. self._maybe_log_step(
  170. epoch=epoch,
  171. step=step,
  172. num_steps=num_steps,
  173. data_time=data_time,
  174. iter_time=iter_time,
  175. metrics=train_metrics,
  176. prefix="train",
  177. )
  178. end_time = time.perf_counter()
  179. if self.scheduler is not None:
  180. self.scheduler.step()
  181. if train_metrics is None:
  182. raise RuntimeError("Training loader is empty.")
  183. train_metrics = self._average_metric_sums(train_metric_sums, num_steps)
  184. val_metrics = self._validate() if self._should_validate(epoch) else None
  185. summary, should_stop = self._finalize_epoch(
  186. epoch=epoch,
  187. train_metrics=train_metrics,
  188. val_metrics=val_metrics,
  189. checkpoint_state={
  190. "model": self.model.state_dict(),
  191. "optimizer": self.optimizer.state_dict(),
  192. "scheduler": self.scheduler.state_dict() if self.scheduler is not None else None,
  193. },
  194. )
  195. print(summary)
  196. if should_stop:
  197. print({"epoch": epoch, "message": "early stopping triggered"})
  198. break
  199. finally:
  200. self._close_loggers()