| """Train DINCAE with random cloud masking and masked Gaussian NLL.""" |
|
|
| import json |
| import os |
| import sys |
| from datetime import date |
| from pathlib import Path |
|
|
| import numpy as np |
| import torch |
| import yaml |
| from torch.nn.parallel import DistributedDataParallel |
| from torch.utils.data import DataLoader, Dataset, DistributedSampler |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(ROOT)) |
|
|
| from model.dincae import DINCAE, build_input, masked_gaussian_nll |
|
|
|
|
| class SSTDataset(Dataset): |
| def __init__(self, arrays, indices, cloud_fraction, seed): |
| self.data, self.indices = arrays, list(indices) |
| self.cloud_fraction, self.seed = float(cloud_fraction), int(seed) |
|
|
| def __len__(self): |
| return len(self.indices) |
|
|
| def __getitem__(self, item): |
| index = self.indices[item] |
| timestamp = date.fromisoformat(str(self.data["timestamps"][index])) |
| observed = self.data["observed_anomaly"].copy() |
| precision = self.data["precision"].copy() |
| rng = np.random.default_rng(self.seed + index) |
| original_valid = np.isfinite(observed[index]) & self.data["ocean_mask"] |
| withheld = original_valid & (rng.random(original_valid.shape) < self.cloud_fraction) |
| observed[index, withheld] = np.nan |
| precision[index, withheld] = 0.0 |
| inputs = build_input(observed, precision, index, self.data["longitude"], |
| self.data["latitude"], timestamp.timetuple().tm_yday) |
| target = self.data["sst_anomaly"][index][None].astype(np.float32) |
| return torch.from_numpy(inputs), torch.from_numpy(target), torch.from_numpy(withheld[None]) |
|
|
|
|
| def main(): |
| config = yaml.safe_load((ROOT / "conf/config.yaml").read_text()) |
| rank = int(os.environ.get("RANK", "0")) |
| world_size = int(os.environ.get("WORLD_SIZE", "1")) |
| local_rank = int(os.environ.get("LOCAL_RANK", "0")) |
| distributed = world_size > 1 |
| if distributed: |
| backend = "nccl" if torch.cuda.is_available() else "gloo" |
| torch.distributed.init_process_group(backend=backend) |
| requested = config["runtime"]["device"] |
| use_cuda = torch.cuda.is_available() and requested != "cpu" |
| device = torch.device(f"cuda:{local_rank}" if use_cuda else "cpu") |
| if use_cuda: |
| torch.cuda.set_device(device) |
| torch.manual_seed(int(config["seed"]) + rank) |
| path = ROOT / config["data"]["root"] / config["data"]["file"] |
| with np.load(path) as loaded: |
| arrays = {key: loaded[key] for key in loaded.files} |
| train_count = min(int(config["data"]["train_samples"]), len(arrays["timestamps"]) - 1) |
| dataset = SSTDataset(arrays, range(train_count), config["data"]["random_cloud_fraction"], config["seed"]) |
| sampler = DistributedSampler(dataset, shuffle=True, seed=int(config["seed"])) if distributed else None |
| loader = DataLoader(dataset, batch_size=int(config["training"]["batch_size"]), |
| shuffle=sampler is None, sampler=sampler, |
| num_workers=int(config["runtime"]["num_workers"])) |
| model = DINCAE(**config["model"]).to(device) |
| if distributed: |
| model = DistributedDataParallel(model, device_ids=[local_rank] if use_cuda else None) |
| optimizer = torch.optim.Adam(model.parameters(), lr=float(config["training"]["learning_rate"])) |
| history = [] |
| for epoch in range(int(config["training"]["epochs"])): |
| if sampler is not None: |
| sampler.set_epoch(epoch) |
| model.train() |
| total, batches = 0.0, 0 |
| for inputs, target, mask in loader: |
| inputs, target, mask = inputs.to(device), target.to(device), mask.to(device) |
| optimizer.zero_grad(set_to_none=True) |
| output = model(inputs) |
| loss = masked_gaussian_nll(output, target, mask, |
| config["training"]["gamma"], config["training"]["delta"]) |
| loss.backward() |
| optimizer.step() |
| total, batches = total + float(loss.detach()), batches + 1 |
| history.append({"epoch": epoch + 1, "masked_gaussian_nll": total / max(batches, 1)}) |
| if rank == 0: |
| print(f"epoch={epoch + 1} loss={history[-1]['masked_gaussian_nll']:.6f}") |
| if rank == 0: |
| raw_model = model.module if distributed else model |
| checkpoint = ROOT / config["paths"]["checkpoint"] |
| checkpoint.parent.mkdir(parents=True, exist_ok=True) |
| torch.save({"model": raw_model.state_dict(), "model_config": config["model"], |
| "gamma": config["training"]["gamma"], "delta": config["training"]["delta"], |
| "format_version": "dincae_checkpoint_v1"}, checkpoint) |
| metrics = ROOT / config["paths"]["training_metrics"] |
| metrics.parent.mkdir(parents=True, exist_ok=True) |
| metrics.write_text(json.dumps({"history": history, "world_size": world_size, |
| "parameter_count": sum(p.numel() for p in raw_model.parameters())}, indent=2)) |
| print(f"checkpoint={checkpoint}") |
| if distributed: |
| torch.distributed.destroy_process_group() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|