DeepONet / scripts /train.py
yushuang88's picture
Upload folder using huggingface_hub
d46980f verified
Raw
History Blame Contribute Delete
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
@torch.inference_mode()
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()