Download scripts/train.py from OneScience-Group/DeepONet: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/OneScience-Group/DeepONet/resolve/main/scripts/train.py
- Command line
-
hf download hf://OneScience-Group/DeepONet/scripts/train.py
-
curl -L -o train.py https://huggingface.co/OneScience-Group/DeepONet/resolve/main/scripts/train.py
14 kB
| #!/usr/bin/env python3 | |
| """Train the paper-scale or explicitly reduced DeepONet experiments.""" | |
| from __future__ import annotations | |
| import argparse | |
| import copy | |
| import csv | |
| import os | |
| import random | |
| import sys | |
| import tempfile | |
| from pathlib import Path | |
| from typing import Any, Dict, Iterable, Mapping, Tuple | |
| import numpy as np | |
| import torch | |
| import yaml | |
| from torch import Tensor, nn | |
| from torch.utils.data import DataLoader | |
| PROJECT_ROOT = Path(__file__).resolve().parents[1] | |
| if str(PROJECT_ROOT) not in sys.path: | |
| sys.path.insert(0, str(PROJECT_ROOT)) | |
| from models.DeepONet import build_model, count_parameters # noqa: E402 | |
| from models.dataset import OperatorDataset, build_datasets, resolve_config # noqa: E402 | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--config", type=Path, default=PROJECT_ROOT / "config/config.yaml") | |
| parser.add_argument( | |
| "--experiment", | |
| default="antiderivative", | |
| help="One experiment name or 'all' for all four paper experiments.", | |
| ) | |
| parser.add_argument( | |
| "--variant", | |
| default=None, | |
| help="Variant name, 'all', or omit to use each experiment's paper default.", | |
| ) | |
| parser.add_argument("--smoke-test", action="store_true", help="Run reduced non-paper settings.") | |
| parser.add_argument("--device", default=None, help="cpu, cuda, cuda:N, or auto") | |
| parser.add_argument("--seed", type=int, default=None) | |
| parser.add_argument("--resume", action="store_true") | |
| parser.add_argument("--no-cache", action="store_true") | |
| return parser.parse_args() | |
| def load_config(path: Path, smoke_test: bool) -> Dict[str, Any]: | |
| with path.expanduser().resolve().open("r", encoding="utf-8") as handle: | |
| raw = yaml.safe_load(handle) | |
| if not isinstance(raw, Mapping): | |
| raise ValueError(f"Configuration {path} must contain a mapping") | |
| return resolve_config(raw, smoke_test=smoke_test) | |
| def resolve_path(path_value: str, root: Path = PROJECT_ROOT) -> Path: | |
| path = Path(path_value).expanduser() | |
| return path if path.is_absolute() else root / path | |
| def select_device(requested: str) -> torch.device: | |
| if requested == "auto": | |
| return torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| device = torch.device(requested) | |
| if device.type == "cuda" and not torch.cuda.is_available(): | |
| raise RuntimeError(f"CUDA device {requested!r} requested but CUDA is unavailable") | |
| return device | |
| 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 make_loader( | |
| dataset: OperatorDataset, | |
| batch_size: int | None, | |
| *, | |
| shuffle: bool, | |
| num_workers: int, | |
| pin_memory: bool, | |
| ) -> DataLoader: | |
| effective_batch = len(dataset) if batch_size is None else int(batch_size) | |
| if effective_batch < 1: | |
| raise ValueError("batch size must be positive") | |
| return DataLoader( | |
| dataset, | |
| batch_size=effective_batch, | |
| shuffle=shuffle, | |
| num_workers=num_workers, | |
| pin_memory=pin_memory, | |
| drop_last=False, | |
| ) | |
| def metric_values(prediction: np.ndarray, target: np.ndarray, trim_fraction: float = 0.0) -> Dict[str, float]: | |
| squared_error = np.square(prediction.astype(np.float64) - target.astype(np.float64)).reshape(-1) | |
| mse = float(np.mean(squared_error)) | |
| denominator = float(np.linalg.norm(target.astype(np.float64).reshape(-1))) | |
| relative_l2 = float( | |
| np.linalg.norm(prediction.astype(np.float64).reshape(-1) - target.astype(np.float64).reshape(-1)) | |
| / max(denominator, np.finfo(np.float64).eps) | |
| ) | |
| metrics = {"test_mse": mse, "relative_l2": relative_l2} | |
| if trim_fraction > 0.0: | |
| remove_count = min(len(squared_error) - 1, int(np.ceil(len(squared_error) * trim_fraction))) | |
| kept = np.partition(squared_error, len(squared_error) - remove_count - 1)[ | |
| : len(squared_error) - remove_count | |
| ] | |
| metrics["trimmed_test_mse"] = float(np.mean(kept)) | |
| return metrics | |
| def evaluate( | |
| model: nn.Module, | |
| dataset: OperatorDataset, | |
| device: torch.device, | |
| batch_size: int, | |
| trim_fraction: float, | |
| num_workers: int, | |
| ) -> Dict[str, float]: | |
| loader = make_loader( | |
| dataset, | |
| batch_size, | |
| shuffle=False, | |
| num_workers=num_workers, | |
| pin_memory=device.type == "cuda", | |
| ) | |
| predictions, targets = [], [] | |
| model.eval() | |
| for branch, trunk, target in loader: | |
| output = model(branch.to(device), trunk.to(device)) | |
| predictions.append(output.detach().cpu().numpy()) | |
| targets.append(target.numpy()) | |
| return metric_values(np.concatenate(predictions), np.concatenate(targets), trim_fraction) | |
| def _cpu_copy(value: Any) -> Any: | |
| if isinstance(value, Tensor): | |
| return value.detach().cpu() | |
| if isinstance(value, dict): | |
| return {key: _cpu_copy(item) for key, item in value.items()} | |
| if isinstance(value, list): | |
| return [_cpu_copy(item) for item in value] | |
| if isinstance(value, tuple): | |
| return tuple(_cpu_copy(item) for item in value) | |
| return value | |
| def torch_load(path: Path, map_location: str | torch.device = "cpu") -> Any: | |
| try: | |
| return torch.load(path, map_location=map_location, weights_only=False) | |
| except TypeError: | |
| return torch.load(path, map_location=map_location) | |
| def load_bundle(path: Path) -> Dict[str, Any]: | |
| if not path.exists(): | |
| return {"format_version": 1, "entries": {}} | |
| bundle = torch_load(path) | |
| if not isinstance(bundle, dict) or not isinstance(bundle.get("entries"), dict): | |
| raise ValueError(f"Checkpoint {path} is not a DeepONet indexed bundle") | |
| return bundle | |
| def save_bundle_entry(path: Path, key: str, entry: Mapping[str, Any]) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| bundle = load_bundle(path) | |
| bundle["format_version"] = 1 | |
| bundle["entries"][key] = _cpu_copy(dict(entry)) | |
| with tempfile.NamedTemporaryFile(dir=path.parent, suffix=".pth", delete=False) as handle: | |
| temporary_path = Path(handle.name) | |
| try: | |
| torch.save(bundle, temporary_path) | |
| os.replace(temporary_path, path) | |
| finally: | |
| if temporary_path.exists(): | |
| temporary_path.unlink() | |
| def append_history(path: Path, row: Mapping[str, Any]) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| exists = path.exists() | |
| with path.open("a", newline="", encoding="utf-8") as handle: | |
| writer = csv.DictWriter(handle, fieldnames=list(row.keys())) | |
| if not exists: | |
| writer.writeheader() | |
| writer.writerow(row) | |
| handle.flush() | |
| def variants_for(config: Mapping[str, Any], experiment: str, requested: str | None) -> Iterable[str]: | |
| if requested is None: | |
| return [str(config["experiments"][experiment]["default_variant"])] | |
| if requested == "all": | |
| return list(config["variants"]) | |
| if requested not in config["variants"]: | |
| raise KeyError(f"Unknown variant {requested!r}; choose from {list(config['variants'])}") | |
| return [requested] | |
| def run_training( | |
| config: Dict[str, Any], | |
| experiment: str, | |
| variant: str, | |
| *, | |
| no_cache: bool, | |
| resume: bool, | |
| ) -> None: | |
| experiment_config = config["experiments"][experiment] | |
| training_config = config["training_defaults"] | |
| device = select_device(str(config["runtime"]["device"])) | |
| seed = int(config["runtime"]["seed"]) | |
| set_seed(seed) | |
| if no_cache: | |
| from models.dataset import build_split | |
| train_dataset = build_split(config, experiment, "train", PROJECT_ROOT, use_cache=False) | |
| test_dataset = build_split(config, experiment, "test", PROJECT_ROOT, use_cache=False) | |
| else: | |
| train_dataset, test_dataset = build_datasets(config, experiment, PROJECT_ROOT) | |
| model = build_model(config, experiment, variant).to(device) | |
| optimizer = torch.optim.Adam( | |
| model.parameters(), | |
| lr=float(training_config["learning_rate"]), | |
| weight_decay=float(training_config.get("weight_decay", 0.0)), | |
| ) | |
| criterion = nn.MSELoss(reduction="mean") | |
| checkpoint_path = resolve_path(str(config["paths"]["checkpoint"])) | |
| entry_key = f"{experiment}/{variant}" | |
| start_iteration = 0 | |
| best_metric = float("inf") | |
| if resume and checkpoint_path.exists(): | |
| entry = load_bundle(checkpoint_path)["entries"].get(entry_key) | |
| if entry is None: | |
| raise KeyError(f"Cannot resume: {entry_key!r} is absent from {checkpoint_path}") | |
| model.load_state_dict(entry["model_state"], strict=True) | |
| optimizer.load_state_dict(entry["optimizer_state"]) | |
| start_iteration = int(entry["iteration"]) | |
| best_metric = float(entry["best_metric"]) | |
| batch_size = training_config.get("batch_size") | |
| train_loader = make_loader( | |
| train_dataset, | |
| None if batch_size is None else int(batch_size), | |
| shuffle=True, | |
| num_workers=int(config["runtime"].get("num_workers", 0)), | |
| pin_memory=device.type == "cuda", | |
| ) | |
| train_iterator = iter(train_loader) | |
| iterations = int(experiment_config["iterations"]) | |
| print_every = int(training_config["print_every"]) | |
| evaluate_every = int(training_config["evaluate_every"]) | |
| eval_batch_size = int(training_config["evaluation_batch_size"]) | |
| trim_fraction = float(experiment_config.get("trim_fraction", 0.0)) | |
| history_path = resolve_path(str(config["paths"]["results"])) / experiment / variant / "history.csv" | |
| paper_scale = bool(config["project"]["paper_scale"]) | |
| if not resume and history_path.exists(): | |
| history_path.unlink() | |
| print( | |
| f"START experiment={experiment} variant={variant} device={device} " | |
| f"parameters={count_parameters(model)} train_points={len(train_dataset)} " | |
| f"test_points={len(test_dataset)} iterations={iterations} paper_scale={paper_scale}", | |
| flush=True, | |
| ) | |
| for iteration in range(start_iteration + 1, iterations + 1): | |
| try: | |
| branch, trunk, target = next(train_iterator) | |
| except StopIteration: | |
| train_iterator = iter(train_loader) | |
| branch, trunk, target = next(train_iterator) | |
| model.train() | |
| optimizer.zero_grad(set_to_none=True) | |
| prediction = model(branch.to(device), trunk.to(device)) | |
| loss = criterion(prediction, target.to(device)) | |
| if not torch.isfinite(loss): | |
| raise FloatingPointError(f"non-finite training loss at iteration {iteration}") | |
| loss.backward() | |
| optimizer.step() | |
| train_loss = float(loss.detach().cpu()) | |
| should_evaluate = iteration == 1 or iteration % evaluate_every == 0 or iteration == iterations | |
| should_print = iteration == 1 or iteration % print_every == 0 or should_evaluate | |
| metrics: Dict[str, float] = {} | |
| if should_evaluate: | |
| metrics = evaluate( | |
| model, | |
| test_dataset, | |
| device, | |
| eval_batch_size, | |
| trim_fraction, | |
| int(config["runtime"].get("num_workers", 0)), | |
| ) | |
| row: Dict[str, Any] = { | |
| "iteration": iteration, | |
| "train_loss": train_loss, | |
| "test_mse": metrics["test_mse"], | |
| "relative_l2": metrics["relative_l2"], | |
| "generalization_error": metrics["test_mse"] - train_loss, | |
| "paper_scale": paper_scale, | |
| } | |
| if "trimmed_test_mse" in metrics: | |
| row["trimmed_test_mse"] = metrics["trimmed_test_mse"] | |
| append_history(history_path, row) | |
| if metrics["test_mse"] < best_metric: | |
| best_metric = metrics["test_mse"] | |
| save_bundle_entry( | |
| checkpoint_path, | |
| entry_key, | |
| { | |
| "experiment": experiment, | |
| "variant": variant, | |
| "iteration": iteration, | |
| "best_metric": best_metric, | |
| "metric_name": "test_mse", | |
| "model_state": model.state_dict(), | |
| "optimizer_state": optimizer.state_dict(), | |
| "run_config": copy.deepcopy(config), | |
| "paper_scale": paper_scale, | |
| }, | |
| ) | |
| if should_print: | |
| metric_text = " ".join(f"{name}={value:.8e}" for name, value in metrics.items()) | |
| print( | |
| f"TRAIN experiment={experiment} variant={variant} iteration={iteration}/{iterations} " | |
| f"train_loss={train_loss:.8e} {metric_text}".rstrip(), | |
| flush=True, | |
| ) | |
| print( | |
| f"DONE experiment={experiment} variant={variant} best_test_mse={best_metric:.8e} " | |
| f"checkpoint={checkpoint_path} history={history_path}", | |
| flush=True, | |
| ) | |
| def main() -> None: | |
| args = parse_args() | |
| config = load_config(args.config, args.smoke_test) | |
| if args.device is not None: | |
| config["runtime"]["device"] = args.device | |
| if args.seed is not None: | |
| config["runtime"]["seed"] = args.seed | |
| experiments = list(config["experiments"]) if args.experiment == "all" else [args.experiment] | |
| unknown = [name for name in experiments if name not in config["experiments"]] | |
| if unknown: | |
| raise KeyError(f"Unknown experiments: {unknown}; choose from {list(config['experiments'])}") | |
| for experiment in experiments: | |
| for variant in variants_for(config, experiment, args.variant): | |
| run_training( | |
| config, | |
| experiment, | |
| variant, | |
| no_cache=args.no_cache, | |
| resume=args.resume or bool(config["training_defaults"].get("resume", False)), | |
| ) | |
| if __name__ == "__main__": | |
| main() | |