Spaces:
Sleeping
Sleeping
Download training/trainer.py from AthithyaLogan/SAMSpade: direct link, hf CLI and curl.
- Browser
- Download file 9.12 kB
-
https://huggingface.co/spaces/AthithyaLogan/SAMSpade/resolve/main/training/trainer.py
- Command line
-
hf download hf://spaces/AthithyaLogan/SAMSpade/training/trainer.py
-
curl -L -o trainer.py https://huggingface.co/spaces/AthithyaLogan/SAMSpade/resolve/main/training/trainer.py
9.12 kB
| """EpisodicTrainer — trains AppearanceScorer and ChangeScorer jointly. | |
| The training loop: | |
| • Loads prebuilt corpus prototypes from ``checkpoints/prototypes.pt``. | |
| • Feeds (tile_2022, tile_2024, mask_2022, mask_2024) batches to the detector. | |
| • Computes combined BCE + Dice loss on ``prob_map`` vs ``mask_2024``. | |
| • Validates every epoch; saves best checkpoint by validation IoU. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from pathlib import Path | |
| from typing import Optional | |
| import torch | |
| import torch.nn as nn | |
| from omegaconf import DictConfig | |
| from torch.optim import AdamW | |
| from torch.optim.lr_scheduler import CosineAnnealingLR, LinearLR, SequentialLR | |
| from torch.utils.data import DataLoader | |
| from torch.utils.tensorboard import SummaryWriter | |
| from data.dataset import ConstructionChangeDataset | |
| from evaluation.metrics import SegmentationEvaluator | |
| from losses.segmentation_losses import CombinedSegmentationLoss | |
| from models.detector import ConstructionChangeDetector | |
| logger = logging.getLogger(__name__) | |
| class EpisodicTrainer: | |
| def __init__(self, cfg: DictConfig) -> None: | |
| self.cfg = cfg | |
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| logger.info("Device: %s", self.device) | |
| # --- Model --- | |
| self.model = ConstructionChangeDetector( | |
| k_prototypes=cfg.model.k_prototypes | |
| ).to(self.device) | |
| self.model.load_sam2(cfg.model.sam2_hf_id) | |
| # --- Prototypes --- | |
| proto_path = Path(cfg.training.checkpoint_dir) / "prototypes.pt" | |
| if not proto_path.exists(): | |
| raise FileNotFoundError( | |
| f"Prototypes not found at {proto_path}. " | |
| "Run scripts/build_prototypes.py first." | |
| ) | |
| ckpt = torch.load(proto_path, map_location="cpu") | |
| self.prototypes: torch.Tensor = ckpt["prototypes"].to(self.device) | |
| logger.info("Loaded %d prototypes.", self.prototypes.shape[0]) | |
| # --- Loss --- | |
| self.criterion = CombinedSegmentationLoss( | |
| bce_weight=cfg.training.loss_bce_weight, | |
| dice_weight=cfg.training.loss_dice_weight, | |
| ) | |
| # --- Optimizer (only trainable heads) --- | |
| trainable = list(self.model.appearance_scorer.parameters()) + \ | |
| list(self.model.change_scorer.parameters()) | |
| self.optimizer = AdamW( | |
| trainable, | |
| lr=cfg.training.lr, | |
| weight_decay=cfg.training.weight_decay, | |
| ) | |
| # --- LR schedule: linear warmup → cosine annealing --- | |
| warmup_steps = cfg.training.warmup_epochs | |
| total_steps = cfg.training.epochs | |
| warmup_sched = LinearLR( | |
| self.optimizer, start_factor=0.1, total_iters=warmup_steps | |
| ) | |
| cosine_sched = CosineAnnealingLR( | |
| self.optimizer, T_max=total_steps - warmup_steps, eta_min=1e-6 | |
| ) | |
| self.scheduler = SequentialLR( | |
| self.optimizer, | |
| schedulers=[warmup_sched, cosine_sched], | |
| milestones=[warmup_steps], | |
| ) | |
| # --- AMP --- | |
| self.scaler = torch.cuda.amp.GradScaler(enabled=cfg.training.amp) | |
| # --- Data --- | |
| self.train_loader, self.val_loader = self._build_dataloaders() | |
| # --- Logging --- | |
| log_dir = Path(cfg.training.log_dir) | |
| log_dir.mkdir(parents=True, exist_ok=True) | |
| self.writer = SummaryWriter(str(log_dir)) | |
| self.ckpt_dir = Path(cfg.training.checkpoint_dir) | |
| self.ckpt_dir.mkdir(parents=True, exist_ok=True) | |
| self.best_iou = 0.0 | |
| self.log_every = cfg.training.log_every | |
| # ------------------------------------------------------------------ | |
| def _build_dataloaders(self): | |
| common = dict( | |
| tile_dir=self.cfg.data.tile_dir, | |
| image_size=self.cfg.data.image_size, | |
| min_fg_pixels=self.cfg.data.min_fg_pixels, | |
| val_split=self.cfg.training.val_split, | |
| seed=self.cfg.training.seed, | |
| ) | |
| train_ds = ConstructionChangeDataset(augment=True, split="train", **common) | |
| val_ds = ConstructionChangeDataset(augment=False, split="val", **common) | |
| train_loader = DataLoader( | |
| train_ds, | |
| batch_size=self.cfg.training.batch_size, | |
| shuffle=True, | |
| num_workers=self.cfg.data.num_workers, | |
| pin_memory=True, | |
| drop_last=True, | |
| ) | |
| val_loader = DataLoader( | |
| val_ds, | |
| batch_size=self.cfg.training.batch_size, | |
| shuffle=False, | |
| num_workers=self.cfg.data.num_workers, | |
| pin_memory=True, | |
| ) | |
| return train_loader, val_loader | |
| # ------------------------------------------------------------------ | |
| def train_epoch(self, epoch: int) -> float: | |
| self.model.train() | |
| # Keep encoder in eval mode regardless | |
| if self.model.image_encoder is not None: | |
| self.model.image_encoder.eval() | |
| total_loss = 0.0 | |
| global_step = epoch * len(self.train_loader) | |
| for step, batch in enumerate(self.train_loader): | |
| tile_2022 = batch["tile_2022"].to(self.device) | |
| tile_2024 = batch["tile_2024"].to(self.device) | |
| mask_2022 = batch["mask_2022"].to(self.device) | |
| mask_2024 = batch["mask_2024"].to(self.device) | |
| self.optimizer.zero_grad(set_to_none=True) | |
| with torch.cuda.amp.autocast(enabled=self.cfg.training.amp): | |
| out = self.model(tile_2022, tile_2024, mask_2022, self.prototypes) | |
| loss_dict = self.criterion(out["prob_map"], mask_2024) | |
| self.scaler.scale(loss_dict["loss"]).backward() | |
| self.scaler.unscale_(self.optimizer) | |
| nn.utils.clip_grad_norm_( | |
| list(self.model.appearance_scorer.parameters()) + | |
| list(self.model.change_scorer.parameters()), | |
| max_norm=1.0, | |
| ) | |
| self.scaler.step(self.optimizer) | |
| self.scaler.update() | |
| total_loss += loss_dict["loss"].item() | |
| if step % self.log_every == 0: | |
| gstep = global_step + step | |
| self.writer.add_scalar("train/loss", loss_dict["loss"].item(), gstep) | |
| self.writer.add_scalar("train/bce_loss", loss_dict["bce_loss"].item(), gstep) | |
| self.writer.add_scalar("train/dice_loss", loss_dict["dice_loss"].item(), gstep) | |
| return total_loss / max(len(self.train_loader), 1) | |
| def validate(self, epoch: int) -> dict: | |
| self.model.eval() | |
| evaluator = SegmentationEvaluator(threshold=0.5) | |
| for batch in self.val_loader: | |
| tile_2022 = batch["tile_2022"].to(self.device) | |
| tile_2024 = batch["tile_2024"].to(self.device) | |
| mask_2022 = batch["mask_2022"].to(self.device) | |
| mask_2024 = batch["mask_2024"].to(self.device) | |
| with torch.cuda.amp.autocast(enabled=self.cfg.training.amp): | |
| out = self.model(tile_2022, tile_2024, mask_2022, self.prototypes) | |
| evaluator.update(out["prob_map"].cpu(), mask_2024.cpu()) | |
| metrics = evaluator.compute() | |
| for k, v in metrics.items(): | |
| self.writer.add_scalar(f"val/{k}", v, epoch) | |
| return metrics | |
| def save_checkpoint(self, epoch: int, is_best: bool = False) -> None: | |
| state = { | |
| "epoch": epoch, | |
| "best_iou": self.best_iou, | |
| **self.model.state_dict_trainable(), | |
| "optimizer": self.optimizer.state_dict(), | |
| } | |
| name = "best.pt" if is_best else f"epoch_{epoch:03d}.pt" | |
| torch.save(state, self.ckpt_dir / name) | |
| def load_checkpoint(self, path: str) -> int: | |
| state = torch.load(path, map_location=self.device) | |
| self.model.load_trainable_state_dict(state) | |
| self.best_iou = state.get("best_iou", 0.0) | |
| return state.get("epoch", 0) | |
| # ------------------------------------------------------------------ | |
| def train(self) -> None: | |
| logger.info("Starting training for %d epochs …", self.cfg.training.epochs) | |
| for epoch in range(self.cfg.training.epochs): | |
| train_loss = self.train_epoch(epoch) | |
| metrics = self.validate(epoch) | |
| self.scheduler.step() | |
| logger.info( | |
| "Epoch %03d | loss=%.4f | iou=%.4f | f1=%.4f | " | |
| "precision=%.4f | recall=%.4f", | |
| epoch, train_loss, | |
| metrics["iou"], metrics["f1"], | |
| metrics["precision"], metrics["recall"], | |
| ) | |
| if metrics["iou"] > self.best_iou: | |
| self.best_iou = metrics["iou"] | |
| self.save_checkpoint(epoch, is_best=True) | |
| logger.info(" ↑ New best IoU=%.4f — saved best.pt", self.best_iou) | |
| if (epoch + 1) % self.cfg.training.checkpoint_every == 0: | |
| self.save_checkpoint(epoch, is_best=False) | |
| self.writer.close() | |
| logger.info("Training complete. Best IoU=%.4f", self.best_iou) | |