Download scripts/train.py from OneScience-Group/PPNN: direct link, hf CLI and curl.
- Browser
- Download file 5.16 kB
-
https://huggingface.co/OneScience-Group/PPNN/resolve/main/scripts/train.py
- Command line
-
hf download hf://OneScience-Group/PPNN/scripts/train.py
-
curl -L -o train.py https://huggingface.co/OneScience-Group/PPNN/resolve/main/scripts/train.py
5.16 kB
| """Train independent PPNN replicas with Gaussian CRPS and optional rank allocation.""" | |
| import json | |
| import os | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import yaml | |
| from torch.utils.data import DataLoader, TensorDataset | |
| ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(ROOT)) | |
| from model.ppnn import FORMAT_VERSION, PPNN, ensemble_features, gaussian_crps | |
| def load_data(config): | |
| data = np.load(ROOT / config["data"]["file"]) | |
| expected = (len(data["target"]), 50, 18) | |
| if str(data["format_version"]) != FORMAT_VERSION or data["ensemble"].shape != expected: | |
| raise ValueError(f"dataset version/shape mismatch: expected {FORMAT_VERSION} and {expected}") | |
| if len(data["station_id"]) != 537 or int(data["lead_hours"]) != 48: | |
| raise ValueError("dataset must retain 537 stations and 48h lead") | |
| arrays = [data["ensemble"], data["auxiliary"], data["target"]] | |
| if not all(np.isfinite(x).all() for x in arrays): | |
| raise ValueError("dataset contains non-finite values") | |
| return data | |
| def main(): | |
| config = yaml.safe_load((ROOT / "conf/config.yaml").read_text()) | |
| data = load_data(config) | |
| distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1 | |
| if distributed: | |
| torch.distributed.init_process_group("gloo") | |
| rank = torch.distributed.get_rank() if distributed else 0 | |
| world_size = torch.distributed.get_world_size() if distributed else 1 | |
| device_name = config["runtime"]["device"] | |
| local_rank = int(os.environ.get("LOCAL_RANK", "0")) | |
| enough_accelerators = torch.cuda.is_available() and torch.cuda.device_count() >= world_size | |
| device = torch.device("cuda", local_rank) if device_name == "auto" and enough_accelerators else torch.device("cpu") | |
| ensemble = torch.from_numpy(data["ensemble"]) | |
| auxiliary = torch.from_numpy(data["auxiliary"]) | |
| station = torch.from_numpy(data["station_index"]) | |
| target = torch.from_numpy(data["target"]) | |
| continuous_raw = ensemble_features(ensemble, auxiliary) | |
| feature_mean = continuous_raw.mean(0) | |
| feature_std = continuous_raw.std(0, correction=1).clamp_min(1e-6) | |
| target_mean, target_std = target.mean(), target.std(correction=1).clamp_min(1e-6) | |
| continuous = (continuous_raw - feature_mean) / feature_std | |
| target_scaled = (target - target_mean) / target_std | |
| dataset = TensorDataset(continuous, station, target_scaled) | |
| local = [] | |
| replicas = int(config["train"]["replicas"]) | |
| for replica in range(replicas): | |
| if replica % world_size != rank: | |
| continue | |
| seed = int(config["seed"]) + replica | |
| torch.manual_seed(seed) | |
| generator = torch.Generator().manual_seed(seed) | |
| loader = DataLoader(dataset, batch_size=int(config["train"]["batch_size"]), shuffle=True, generator=generator) | |
| model = PPNN(int(config["model"]["hidden_size"]), eps=float(config["model"]["sigma_epsilon"])).to(device) | |
| optimizer = torch.optim.Adam(model.parameters(), lr=float(config["train"]["learning_rate"])) | |
| history = [] | |
| for epoch in range(int(config["train"]["epochs"])): | |
| total = 0.0 | |
| for x, s, y in loader: | |
| mu, sigma = model(x.to(device), s.to(device)) | |
| loss = gaussian_crps(mu, sigma, y.to(device)).mean() | |
| if not torch.isfinite(loss): | |
| raise RuntimeError("non-finite Gaussian CRPS loss") | |
| optimizer.zero_grad(set_to_none=True) | |
| loss.backward() | |
| optimizer.step() | |
| total += float(loss.detach()) * len(y) | |
| history.append(total / len(dataset)) | |
| local.append({"replica": replica, "state_dict": {k: v.cpu() for k, v in model.state_dict().items()}, "loss": history}) | |
| print(f"rank={rank} replica={replica} final_scaled_crps={history[-1]:.6f}") | |
| if distributed: | |
| gathered = [None] * world_size | |
| torch.distributed.all_gather_object(gathered, local) | |
| trained = [item for rank_items in gathered for item in rank_items] | |
| else: | |
| trained = local | |
| if rank == 0: | |
| trained.sort(key=lambda x: x["replica"]) | |
| if [x["replica"] for x in trained] != list(range(replicas)): | |
| raise RuntimeError("distributed ranks did not return every replica") | |
| checkpoint = ROOT / config["paths"]["checkpoint"] | |
| metrics = ROOT / config["paths"]["training_metrics"] | |
| checkpoint.parent.mkdir(parents=True, exist_ok=True) | |
| metrics.parent.mkdir(parents=True, exist_ok=True) | |
| scaling = {"feature_mean": feature_mean, "feature_std": feature_std, "target_mean": target_mean, "target_std": target_std} | |
| states = [x["state_dict"] for x in trained] | |
| torch.save({"format_version": FORMAT_VERSION, "model": states, | |
| "model_config": config["model"], "config": config, "scaling": scaling}, checkpoint) | |
| metrics.write_text(json.dumps({"replicas": [{"id": x["replica"], "loss": x["loss"]} for x in trained], "world_size": world_size}, indent=2) + "\n") | |
| if distributed: | |
| torch.distributed.destroy_process_group() | |
| if __name__ == "__main__": | |
| main() | |