Source code for neuralfieldmanifold.training.trainer

"""Training routines for DeepLagField."""

from dataclasses import dataclass, field

import numpy as np
import torch

from .losses import LossWeights, compute_losses


[docs] @dataclass(frozen=True) class TrainingConfig: """Epoch count, Adam learning rate, loss weights, and regularization settings. Energy settings control the target prediction power and rolling window. Temperature controls Gumbel order sampling. Validation uses argmax unless stochastic_validation is True. Gradient clipping bounds the parameter gradient norm before each optimizer step. """ epochs: int = 100 learning_rate: float = 1e-3 weights: LossWeights = field(default_factory=LossWeights) target_power: float = 0.5 energy_window_samples: int = 40 temperature: float = 1.0 stochastic_validation: bool = False gradient_clip: float = 1.0
[docs] def train( model, train_loader, *, validation_loader=None, config=None, device="cpu", callback=None, ): """Train DeepLagField and return per-epoch losses and order accuracy. Parameters ---------- model : DeepLagField Model to train. train_loader : iterable Batches of (signal, order), with signals shaped (batch, time) and one AR order per signal. validation_loader : iterable or None Optional validation batches with the same structure. config : TrainingConfig or None Optimization and loss settings. None uses TrainingConfig defaults. device : str or torch.device Device used for model parameters and batches. callback : callable or None Called with the metrics dictionary at the end of each epoch. Returns ------- list of dict Epoch records containing train and optional validation losses, order accuracy, and within-one-order accuracy. Notes ----- Uses Adam, gradient clipping, and ReduceLROnPlateau. Loss records are averages of batch losses. Validation uses deterministic order selection unless config.stochastic_validation is True. """ config = TrainingConfig() if config is None else config if config.epochs < 1 or config.learning_rate <= 0 or config.gradient_clip <= 0: raise ValueError("epochs, learning_rate, and gradient_clip must be positive") model.to(device) optimizer = torch.optim.Adam(model.parameters(), lr=config.learning_rate) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, patience=10, factor=0.5 ) history = [] for epoch in range(config.epochs): record = {"epoch": epoch + 1} for phase, loader in ( ("train", train_loader), ("validation", validation_loader), ): if loader is None: continue training = phase == "train" model.train(training) values, correct, within_one, count = [], 0, 0, 0 with torch.set_grad_enabled(training): for signal, orders in loader: signal, orders = signal.to(device), orders.to(device) optimizer.zero_grad(set_to_none=True) output = model( signal, temperature=config.temperature, sample_order=training or config.stochastic_validation, ) losses = compute_losses( output, signal, orders=model.orders, true_order=orders, weights=config.weights, target_power=config.target_power, energy_window_samples=config.energy_window_samples, ) if training: losses["total"].backward() torch.nn.utils.clip_grad_norm_( model.parameters(), config.gradient_clip ) optimizer.step() values.append( {name: float(value.detach()) for name, value in losses.items()} ) correct += int((output.order == orders).sum()) within_one += int((torch.abs(output.order - orders) <= 1).sum()) count += len(signal) if not values: raise ValueError(f"{phase} loader is empty") record[phase] = { name: float(np.mean([v[name] for v in values])) for name in values[0] } record[phase].update( order_accuracy=correct / count, within_one_accuracy=within_one / count ) scheduler.step(record.get("validation", record["train"])["total"]) history.append(record) if callback is not None: callback(record) model.eval() return history