Download scripts/train.py from OneScience-Group/SatMAE-PP: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/OneScience-Group/SatMAE-PP/resolve/main/scripts/train.py
- Command line
-
hf download hf://OneScience-Group/SatMAE-PP/scripts/train.py
-
curl -L -o train.py https://huggingface.co/OneScience-Group/SatMAE-PP/resolve/main/scripts/train.py
10.8 kB
| """Pre-train SatMAE++ with masked and native multiscale targets.""" | |
| import argparse | |
| import importlib.util | |
| import json | |
| import math | |
| import os | |
| import random | |
| from contextlib import nullcontext | |
| from functools import partial | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import yaml | |
| from torch import distributed as dist | |
| from torch.nn.parallel import DistributedDataParallel | |
| from torch.utils.data import DataLoader, Dataset, DistributedSampler | |
| ROOT = Path(__file__).resolve().parents[1] | |
| class NPZDataset(Dataset): | |
| def __init__(self, path, scales, channels, image_size): | |
| archive = np.load(path) | |
| self.images = archive["images"] | |
| expected = (channels, image_size, image_size) | |
| if self.images.ndim != 4 or tuple(self.images.shape[1:]) != expected: | |
| raise ValueError(f"images must have shape [N, {channels}, {image_size}, {image_size}]") | |
| self.targets = { | |
| str(scale): archive[f"images_{scale}x"] | |
| for scale in scales if scale != 1 and f"images_{scale}x" in archive | |
| } | |
| missing = [scale for scale in scales if scale != 1 and str(scale) not in self.targets] | |
| if missing: | |
| raise ValueError(f"dataset is missing native high-resolution targets: {missing}") | |
| for scale, values in self.targets.items(): | |
| expected_target = (len(self.images), channels, image_size * int(scale), image_size * int(scale)) | |
| if tuple(values.shape) != expected_target: | |
| raise ValueError(f"images_{scale}x must have shape {expected_target}") | |
| def __len__(self): | |
| return len(self.images) | |
| def __getitem__(self, index): | |
| return torch.from_numpy(self.images[index]), { | |
| scale: torch.from_numpy(values[index]) for scale, values in self.targets.items() | |
| } | |
| def load_model_class(): | |
| spec = importlib.util.spec_from_file_location("satmaepp", ROOT / "model/satmaepp.py") | |
| module = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(module) | |
| return module.SatMAEPP | |
| def model_config(config): | |
| return {key: value for key, value in config["model"].items() | |
| if key not in {"architecture", "runtime_profile"}} | |
| def validate_config(config): | |
| data, model, training = config["data"], config["model"], config["training"] | |
| if data["image_size"] != model["image_size"]: | |
| raise ValueError("data.image_size and model.image_size must match") | |
| if data["channels"] != model["in_channels"] or data["scales"] != model["scales"]: | |
| raise ValueError("data and model channels/scales must match") | |
| if model["mode"] == "rgb" and data["channels"] != 3: | |
| raise ValueError("RGB protocol requires three channels") | |
| if model["mode"] == "multispectral" and data["channels"] != 10: | |
| raise ValueError("Sentinel protocol requires ten channels") | |
| if training["epochs"] <= 0 or training["batch_size"] <= 0 or training["accum_iter"] <= 0: | |
| raise ValueError("epochs, batch_size, and accum_iter must be positive") | |
| def validate_dataset_config(config, archive): | |
| validate_config(config) | |
| data, model = config["data"], config["model"] | |
| if "images" not in archive: | |
| raise ValueError("dataset must contain images") | |
| expected = (data["channels"], data["image_size"], data["image_size"]) | |
| if archive["images"].ndim != 4 or tuple(archive["images"].shape[1:]) != expected: | |
| raise ValueError(f"images must have shape [N, {expected[0]}, {expected[1]}, {expected[2]}]") | |
| def parse_args(): | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--config", type=Path, default=ROOT / "conf/config.yaml") | |
| parser.add_argument("--data", type=Path, default=None) | |
| parser.add_argument("--output", type=Path, default=None) | |
| parser.add_argument("--resume", type=Path, default=None) | |
| parser.add_argument("--epochs", type=int, default=None) | |
| parser.add_argument("--batch-size", type=int, default=None) | |
| parser.add_argument("--device", choices=("auto", "cpu", "cuda"), default=None) | |
| return parser.parse_args() | |
| def learning_rate(progress, config, peak): | |
| warmup = config["warmup_epochs"] | |
| if warmup and progress < warmup: | |
| return peak * progress / warmup | |
| phase = min(max((progress - warmup) / max(config["epochs"] - warmup, 1), 0), 1) | |
| return config["min_learning_rate"] + 0.5 * (peak - config["min_learning_rate"]) * ( | |
| 1 + math.cos(math.pi * phase) | |
| ) | |
| def main(): | |
| args = parse_args() | |
| config = yaml.safe_load(args.config.read_text()) | |
| validate_config(config) | |
| training = config["training"] | |
| if args.epochs is not None: | |
| training["epochs"] = args.epochs | |
| if args.batch_size is not None: | |
| training["batch_size"] = args.batch_size | |
| world = int(os.environ.get("WORLD_SIZE", "1")) | |
| rank = int(os.environ.get("RANK", "0")) | |
| local_rank = int(os.environ.get("LOCAL_RANK", "0")) | |
| requested = args.device or config["runtime"]["device"] | |
| cuda = torch.cuda.is_available() and requested != "cpu" | |
| if requested == "cuda" and not torch.cuda.is_available(): | |
| raise RuntimeError("CUDA was requested but is unavailable") | |
| if world > 1: | |
| dist.init_process_group("nccl" if cuda else "gloo") | |
| device = torch.device(f"cuda:{local_rank}" if cuda else "cpu") | |
| if cuda: | |
| torch.cuda.set_device(local_rank) | |
| seed = config["seed"] + rank | |
| random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) | |
| data_path = args.data or ROOT / config["data"]["root"] / "train.npz" | |
| validate_dataset_config(config, np.load(data_path)) | |
| dataset = NPZDataset(data_path, config["model"]["scales"], config["model"]["in_channels"], config["model"]["image_size"]) | |
| sampler = DistributedSampler(dataset) if world > 1 else None | |
| loader = DataLoader(dataset, batch_size=training["batch_size"], | |
| shuffle=sampler is None, sampler=sampler, | |
| num_workers=training["num_workers"], pin_memory=cuda) | |
| model = load_model_class()(**model_config(config)).to(device) | |
| bare_model = model | |
| if world > 1: | |
| model = DistributedDataParallel(model, device_ids=[local_rank] if cuda else None) | |
| bare_model = model.module | |
| effective_batch = training["batch_size"] * training["accum_iter"] * world | |
| peak_lr = training["learning_rate"] | |
| if peak_lr is None: | |
| peak_lr = training["base_learning_rate"] * effective_batch / 256 | |
| decay, no_decay = [], [] | |
| for name, parameter in bare_model.named_parameters(): | |
| if parameter.requires_grad: | |
| (no_decay if parameter.ndim == 1 or name.endswith("bias") else decay).append(parameter) | |
| optimizer = torch.optim.AdamW([ | |
| {"params": decay, "weight_decay": training["weight_decay"]}, | |
| {"params": no_decay, "weight_decay": 0.0}, | |
| ], lr=peak_lr, betas=(0.9, 0.95)) | |
| amp = bool(config["runtime"].get("amp", True) and cuda) | |
| scaler = torch.amp.GradScaler("cuda", enabled=amp) | |
| start_epoch, history = 0, [] | |
| resume = args.resume or (ROOT / training["resume"] if training.get("resume") else None) | |
| if resume is not None: | |
| checkpoint = torch.load(resume, map_location="cpu", weights_only=False) | |
| bare_model.load_state_dict(checkpoint["model"]) | |
| optimizer.load_state_dict(checkpoint["optimizer"]) | |
| if checkpoint.get("scaler"): | |
| scaler.load_state_dict(checkpoint["scaler"]) | |
| start_epoch, history = checkpoint["epoch"] + 1, checkpoint.get("history", []) | |
| checkpoint_path = args.output or ROOT / config["paths"]["checkpoint"] | |
| metrics_path = ROOT / config["paths"]["training_metrics"] | |
| optimizer.zero_grad(set_to_none=True) | |
| for epoch in range(start_epoch, training["epochs"]): | |
| if sampler: | |
| sampler.set_epoch(epoch) | |
| model.train(); totals = {"loss": 0.0, "reconstruction": 0.0, "multiscale": 0.0} | |
| for step, (images, targets) in enumerate(loader): | |
| lr = learning_rate(epoch + step / max(len(loader), 1), training, peak_lr) | |
| for group in optimizer.param_groups: | |
| group["lr"] = lr | |
| images = images.to(device, non_blocking=cuda) | |
| targets = {scale: value.to(device, non_blocking=cuda) for scale, value in targets.items()} | |
| autocast = partial(torch.amp.autocast, "cuda") if amp else nullcontext | |
| with autocast(): | |
| output = model(images, high_resolution_targets=targets) | |
| loss = output["loss"] / training["accum_iter"] | |
| if not torch.isfinite(loss): | |
| raise ValueError("non-finite training loss") | |
| scaler.scale(loss).backward() | |
| update = (step + 1) % training["accum_iter"] == 0 or step + 1 == len(loader) | |
| if update: | |
| scaler.step(optimizer); scaler.update(); optimizer.zero_grad(set_to_none=True) | |
| totals["loss"] += output["loss"].detach().item() | |
| totals["reconstruction"] += output["reconstruction_loss"].detach().item() | |
| totals["multiscale"] += output["multiscale_loss"].detach().item() | |
| if world > 1: | |
| values = torch.tensor([totals[name] for name in ("loss", "reconstruction", "multiscale")], device=device) | |
| dist.all_reduce(values, op=dist.ReduceOp.SUM) | |
| totals = dict(zip(("loss", "reconstruction", "multiscale"), (value.item() / world for value in values))) | |
| record = {"epoch": epoch + 1, "learning_rate": optimizer.param_groups[0]["lr"]} | |
| record.update({name + "_loss": value / max(len(loader), 1) for name, value in totals.items()}) | |
| history.append(record) | |
| if rank == 0: | |
| print(f"epoch={epoch + 1} loss={record['loss_loss']:.6f} " | |
| f"reconstruction={record['reconstruction_loss']:.6f} multiscale={record['multiscale_loss']:.6f}") | |
| if (epoch + 1) % training["save_every"] == 0 or epoch + 1 == training["epochs"]: | |
| checkpoint_path.parent.mkdir(parents=True, exist_ok=True) | |
| torch.save({"model": bare_model.state_dict(), "optimizer": optimizer.state_dict(), | |
| "scaler": scaler.state_dict() if amp else None, "epoch": epoch, | |
| "history": history, "config": config}, checkpoint_path) | |
| if rank == 0: | |
| metrics_path.parent.mkdir(parents=True, exist_ok=True) | |
| metrics_path.write_text(json.dumps({"history": history, "protocol": config["data"]["protocol"], | |
| "data_source": "synthetic", "effective_batch_size": effective_batch, | |
| "peak_learning_rate": peak_lr}, indent=2) + "\n") | |
| print("checkpoint=", checkpoint_path) | |
| if world > 1: | |
| dist.destroy_process_group() | |
| if __name__ == "__main__": | |
| main() | |