Download scripts/train.py from OneScience-Group/FlowBench-FNO-LDC2D-Harmonics: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/OneScience-Group/FlowBench-FNO-LDC2D-Harmonics/resolve/main/scripts/train.py
- Command line
-
hf download hf://OneScience-Group/FlowBench-FNO-LDC2D-Harmonics/scripts/train.py
-
curl -L -o train.py https://huggingface.co/OneScience-Group/FlowBench-FNO-LDC2D-Harmonics/resolve/main/scripts/train.py
12.2 kB
| from __future__ import annotations | |
| import argparse | |
| import csv | |
| import json | |
| import math | |
| import random | |
| import sys | |
| from pathlib import Path | |
| from typing import Any | |
| import numpy as np | |
| import yaml | |
| try: | |
| import torch | |
| from torch import nn | |
| from torch.utils.data import DataLoader, Dataset | |
| except ImportError as exc: # pragma: no cover - exercised in environment preflight | |
| raise SystemExit(f"PyTorch is required to run training: {exc}") from exc | |
| PROJECT_ROOT = Path(__file__).resolve().parents[1] | |
| if str(PROJECT_ROOT) not in sys.path: | |
| sys.path.insert(0, str(PROJECT_ROOT)) | |
| from model.FlowBenchFNO import build_model_from_config | |
| class FlowBenchTensorDataset(Dataset): | |
| def __init__( | |
| self, | |
| x: np.ndarray, | |
| y: np.ndarray, | |
| indices: np.ndarray, | |
| x_mean: np.ndarray | None = None, | |
| x_std: np.ndarray | None = None, | |
| y_mean: np.ndarray | None = None, | |
| y_std: np.ndarray | None = None, | |
| normalize: bool = True, | |
| ) -> None: | |
| self.x = x | |
| self.y = y | |
| self.indices = indices.astype(np.int64) | |
| self.normalize = normalize | |
| self.x_mean = x_mean | |
| self.x_std = x_std | |
| self.y_mean = y_mean | |
| self.y_std = y_std | |
| def __len__(self) -> int: | |
| return int(self.indices.shape[0]) | |
| def __getitem__(self, item: int) -> tuple[torch.Tensor, torch.Tensor]: | |
| idx = self.indices[item] | |
| x = self.x[idx] | |
| y = self.y[idx] | |
| if self.normalize: | |
| x = (x - self.x_mean) / self.x_std | |
| y = (y - self.y_mean) / self.y_std | |
| return torch.from_numpy(x.astype(np.float32)), torch.from_numpy(y.astype(np.float32)) | |
| def load_config(path: str | Path) -> dict[str, Any]: | |
| with open(path, "r", encoding="utf-8") as handle: | |
| return yaml.safe_load(handle) | |
| def resolve_path(path: str | Path, root: Path = PROJECT_ROOT) -> Path: | |
| path = Path(path) | |
| return path if path.is_absolute() else root / path | |
| def set_seed(seed: int) -> None: | |
| random.seed(seed) | |
| np.random.seed(seed) | |
| torch.manual_seed(seed) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed_all(seed) | |
| def select_device(name: str) -> torch.device: | |
| if name == "auto": | |
| return torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| return torch.device(name) | |
| def load_npz_array(path: Path, key: str) -> np.ndarray: | |
| if not path.exists(): | |
| raise FileNotFoundError(f"Missing data file: {path}") | |
| archive = np.load(path) | |
| actual_key = key if key in archive.files else archive.files[0] | |
| array = archive[actual_key] | |
| if array.ndim != 4: | |
| raise ValueError(f"Expected [N,C,H,W] array in {path}, got {array.shape}") | |
| return array.astype(np.float32, copy=False) | |
| def make_splits(n_samples: int, split_fraction: float, shuffle: bool, seed: int) -> tuple[np.ndarray, np.ndarray]: | |
| indices = np.arange(n_samples, dtype=np.int64) | |
| if shuffle: | |
| rng = np.random.default_rng(seed) | |
| rng.shuffle(indices) | |
| n_train = int(math.floor(n_samples * split_fraction)) | |
| return indices[:n_train], indices[n_train:] | |
| def channel_stats(array: np.ndarray, indices: np.ndarray) -> tuple[np.ndarray, np.ndarray]: | |
| subset = array[indices] | |
| mean = subset.mean(axis=(0, 2, 3), keepdims=False).astype(np.float32)[:, None, None] | |
| std = subset.std(axis=(0, 2, 3), keepdims=False).astype(np.float32)[:, None, None] | |
| std = np.maximum(std, np.asarray(1.0e-6, dtype=np.float32)) | |
| return mean, std | |
| def denormalize(tensor: torch.Tensor, mean: torch.Tensor, std: torch.Tensor) -> torch.Tensor: | |
| return tensor * std + mean | |
| def compute_metrics(pred: torch.Tensor, target: torch.Tensor) -> dict[str, float]: | |
| diff = pred - target | |
| mse = torch.mean(diff.square()).item() | |
| mae = torch.mean(torch.abs(diff)).item() | |
| per_sample_num = torch.linalg.vector_norm(diff.flatten(1), dim=1) | |
| per_sample_den = torch.linalg.vector_norm(target.flatten(1), dim=1).clamp_min(1.0e-12) | |
| relative_l2 = torch.mean(per_sample_num / per_sample_den).item() | |
| return {"mse": mse, "relative_l2": relative_l2, "mae": mae} | |
| def evaluate( | |
| model: nn.Module, | |
| loader: DataLoader, | |
| device: torch.device, | |
| y_mean: torch.Tensor, | |
| y_std: torch.Tensor, | |
| max_batches: int | None = None, | |
| ) -> dict[str, float]: | |
| model.eval() | |
| totals = {"mse": 0.0, "relative_l2": 0.0, "mae": 0.0} | |
| count = 0 | |
| for batch_idx, (x, y) in enumerate(loader): | |
| if max_batches is not None and batch_idx >= max_batches: | |
| break | |
| x = x.to(device) | |
| y = y.to(device) | |
| pred = model(x) | |
| pred_phys = denormalize(pred, y_mean, y_std) | |
| y_phys = denormalize(y, y_mean, y_std) | |
| metrics = compute_metrics(pred_phys, y_phys) | |
| batch_size = x.shape[0] | |
| for key, value in metrics.items(): | |
| totals[key] += value * batch_size | |
| count += batch_size | |
| if count == 0: | |
| return {key: float("nan") for key in totals} | |
| return {key: value / count for key, value in totals.items()} | |
| def save_json(path: Path, payload: dict[str, Any]) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(path, "w", encoding="utf-8") as handle: | |
| json.dump(payload, handle, indent=2) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Train FlowBench FNO baseline.") | |
| parser.add_argument("--config", default=str(PROJECT_ROOT / "config/config.yaml")) | |
| parser.add_argument("--max-epochs", type=int, default=None, help="Override config training.num_epochs.") | |
| parser.add_argument("--sample-limit", type=int, default=None, help="Use the first N samples before splitting.") | |
| parser.add_argument("--device", default=None, help="Override experiment.device.") | |
| parser.add_argument("--checkpoint", default=None, help="Override best checkpoint path.") | |
| args = parser.parse_args() | |
| config = load_config(args.config) | |
| seed = int(config["experiment"].get("seed", 42)) | |
| set_seed(seed) | |
| device = select_device(args.device or config["experiment"].get("device", "auto")) | |
| data_cfg = config["data"] | |
| data_root = resolve_path(data_cfg["root_dir"]) | |
| x = load_npz_array(data_root / data_cfg["x_file"], data_cfg.get("x_key", "data")) | |
| y = load_npz_array(data_root / data_cfg["y_file"], data_cfg.get("y_key", "data")) | |
| if x.shape[0] != y.shape[0]: | |
| raise ValueError(f"X/Y sample counts differ: {x.shape[0]} vs {y.shape[0]}") | |
| if args.sample_limit is not None: | |
| x = x[: args.sample_limit] | |
| y = y[: args.sample_limit] | |
| model_cfg = config["model"] | |
| if x.shape[1] != int(model_cfg["in_channels"]): | |
| raise ValueError(f"Config in_channels={model_cfg['in_channels']} but X has {x.shape[1]} channels") | |
| if y.shape[1] != int(model_cfg["out_channels"]): | |
| raise ValueError(f"Config out_channels={model_cfg['out_channels']} but Y has {y.shape[1]} channels") | |
| train_idx, val_idx = make_splits( | |
| x.shape[0], | |
| float(data_cfg.get("split_fraction", 0.8)), | |
| bool(data_cfg.get("shuffle", True)), | |
| seed, | |
| ) | |
| x_mean, x_std = channel_stats(x, train_idx) | |
| y_mean, y_std = channel_stats(y, train_idx) | |
| normalize = bool(data_cfg.get("normalize", True)) | |
| train_ds = FlowBenchTensorDataset(x, y, train_idx, x_mean, x_std, y_mean, y_std, normalize) | |
| val_ds = FlowBenchTensorDataset(x, y, val_idx, x_mean, x_std, y_mean, y_std, normalize) | |
| train_loader = DataLoader( | |
| train_ds, | |
| batch_size=int(config["training"]["batch_size"]), | |
| shuffle=True, | |
| num_workers=int(data_cfg.get("num_workers", 0)), | |
| pin_memory=bool(data_cfg.get("pin_memory", True)) and device.type == "cuda", | |
| ) | |
| val_loader = DataLoader( | |
| val_ds, | |
| batch_size=int(config["training"]["batch_size"]), | |
| shuffle=False, | |
| num_workers=int(data_cfg.get("num_workers", 0)), | |
| pin_memory=bool(data_cfg.get("pin_memory", True)) and device.type == "cuda", | |
| ) | |
| model = build_model_from_config(config).to(device) | |
| optimizer = torch.optim.Adam( | |
| model.parameters(), | |
| lr=float(config["training"]["learning_rate"]), | |
| weight_decay=float(config["training"].get("weight_decay", 0.0)), | |
| ) | |
| criterion = nn.MSELoss() | |
| y_mean_t = torch.from_numpy(y_mean).to(device) | |
| y_std_t = torch.from_numpy(y_std).to(device) | |
| x_mean_t = torch.from_numpy(x_mean) | |
| x_std_t = torch.from_numpy(x_std) | |
| num_epochs = args.max_epochs or int(config["training"]["num_epochs"]) | |
| best_path = resolve_path(args.checkpoint or config["training"]["checkpoint_path"]) | |
| last_path = resolve_path(config["training"]["last_checkpoint_path"]) | |
| metrics_path = resolve_path(config["training"]["metrics_path"]) | |
| history_path = resolve_path(config["training"]["history_path"]) | |
| best_path.parent.mkdir(parents=True, exist_ok=True) | |
| last_path.parent.mkdir(parents=True, exist_ok=True) | |
| history_path.parent.mkdir(parents=True, exist_ok=True) | |
| best_mse = float("inf") | |
| with open(history_path, "w", encoding="utf-8", newline="") as handle: | |
| writer = csv.DictWriter(handle, fieldnames=["epoch", "train_loss", "val_mse", "val_relative_l2", "val_mae"]) | |
| writer.writeheader() | |
| for epoch in range(1, num_epochs + 1): | |
| model.train() | |
| running_loss = 0.0 | |
| seen = 0 | |
| for x_batch, y_batch in train_loader: | |
| x_batch = x_batch.to(device) | |
| y_batch = y_batch.to(device) | |
| optimizer.zero_grad(set_to_none=True) | |
| pred = model(x_batch) | |
| loss = criterion(pred, y_batch) | |
| loss.backward() | |
| clip_norm = config["training"].get("grad_clip_norm") | |
| if clip_norm is not None: | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), float(clip_norm)) | |
| optimizer.step() | |
| batch_size = x_batch.shape[0] | |
| running_loss += loss.item() * batch_size | |
| seen += batch_size | |
| train_loss = running_loss / max(1, seen) | |
| val_metrics = evaluate(model, val_loader, device, y_mean_t, y_std_t) | |
| row = { | |
| "epoch": epoch, | |
| "train_loss": train_loss, | |
| "val_mse": val_metrics["mse"], | |
| "val_relative_l2": val_metrics["relative_l2"], | |
| "val_mae": val_metrics["mae"], | |
| } | |
| writer.writerow(row) | |
| handle.flush() | |
| print( | |
| f"epoch={epoch:04d} train_loss={train_loss:.6e} " | |
| f"val_mse={val_metrics['mse']:.6e} " | |
| f"val_relative_l2={val_metrics['relative_l2']:.6e} " | |
| f"val_mae={val_metrics['mae']:.6e}", | |
| flush=True, | |
| ) | |
| checkpoint = { | |
| "epoch": epoch, | |
| "model_state_dict": model.state_dict(), | |
| "optimizer_state_dict": optimizer.state_dict(), | |
| "best_val_mse": min(best_mse, val_metrics["mse"]), | |
| "config": config, | |
| "normalization": { | |
| "x_mean": x_mean_t.numpy(), | |
| "x_std": x_std_t.numpy(), | |
| "y_mean": torch.from_numpy(y_mean), | |
| "y_std": torch.from_numpy(y_std), | |
| }, | |
| "splits": {"train_idx": train_idx, "val_idx": val_idx}, | |
| "metrics": {"train_loss": train_loss, **val_metrics}, | |
| } | |
| torch.save(checkpoint, last_path) | |
| if val_metrics["mse"] < best_mse: | |
| best_mse = val_metrics["mse"] | |
| torch.save(checkpoint, best_path) | |
| final_payload = { | |
| "best_val_mse": best_mse, | |
| "best_checkpoint": str(best_path), | |
| "last_checkpoint": str(last_path), | |
| "history": str(history_path), | |
| "num_samples": int(x.shape[0]), | |
| "train_samples": int(train_idx.shape[0]), | |
| "val_samples": int(val_idx.shape[0]), | |
| "device": str(device), | |
| } | |
| save_json(metrics_path, final_payload) | |
| print(json.dumps(final_payload, indent=2), flush=True) | |
| if __name__ == "__main__": | |
| main() | |