Source code for neuralfieldmanifold.training.losses

"""Reconstruction, order selection, power, and coefficient-smoothness losses."""

from dataclasses import dataclass

import torch
from torch.nn import functional as F


[docs] @dataclass(frozen=True) class LossWeights: """Nonnegative weights for the five components returned by compute_losses.""" reconstruction: float = 1.0 order_classification: float = 10.0 energy: float = 0.1 smoothness: float = 0.05 order_penalty: float = 0.0 def __post_init__(self): if any(not 0 <= v < float("inf") for v in self.__dict__.values()): raise ValueError("Loss weights must be finite and nonnegative")
[docs] def compute_losses( output, signal, *, orders, true_order=None, weights=None, target_power=0.5, energy_window_samples=40, ): """Compute loss components and their weighted sum. Reconstruction is prediction MSE after the maximum-order lag context. Order classification is cross-entropy against the supplied AR orders. Energy penalizes rolling mean-square prediction power relative to target_power, after skipping the first energy_window_samples samples. Smoothness penalizes squared successive differences of coefficients, summed over lags and averaged over batch and time. The optional order penalty is the expected index of the candidate order. Parameters ---------- output : DynamicsOutput Model coefficients, order logits, and signal prediction. signal : torch.Tensor, shape (batch, time) Target sequences. orders : sequence of int Candidate AR orders corresponding to the logit columns. true_order : torch.Tensor, shape (batch,), or None AR order for each sequence. Required when order classification has nonzero weight. weights : LossWeights or None Nonnegative loss weights. None uses LossWeights defaults. target_power : float Target mean-square prediction amplitude. energy_window_samples : int Rolling window length for the energy term. Returns ------- dict of str to torch.Tensor Scalar reconstruction, order_classification, energy, smoothness, order_penalty, and total losses. """ weights = LossWeights() if weights is None else weights p_max = output.coefficients.shape[-1] if ( signal.ndim != 2 or signal.shape != output.prediction.shape or signal.shape[1] <= p_max ): raise ValueError( "Signal shape must match prediction and exceed the maximum order" ) if ( not isinstance(energy_window_samples, int) or energy_window_samples < 1 or target_power < 0 ): raise ValueError("Invalid energy settings") zero = output.prediction.sum() * 0 if true_order is None: if weights.order_classification: raise ValueError( "true_order is required when order_classification has nonzero weight" ) classification = zero else: if true_order.shape != (len(signal),): raise ValueError("true_order must have shape (batch,)") order_values = torch.as_tensor(orders, device=true_order.device) matches = true_order[:, None] == order_values[None, :] if not matches.any(dim=1).all(): raise ValueError("true_order contains an unsupported AR order") classification = F.cross_entropy( output.order_logits, matches.long().argmax(dim=1) ) reconstruction = F.mse_loss(output.prediction[:, p_max:], signal[:, p_max:]) difference = output.coefficients[:, 1:] - output.coefficients[:, :-1] smoothness = torch.mean(torch.sum(difference**2, dim=2)) length, window = signal.shape[1], energy_window_samples if length <= window: energy = zero elif length < 2 * window: if weights.energy: raise ValueError( "Energy loss requires sequence_length >= 2 * energy_window_samples" ) energy = zero else: power = (output.prediction[:, window:].unfold(1, window, 1) ** 2).mean(-1) energy = ((power - target_power) ** 2).mean() indices = torch.arange( len(orders), device=signal.device, dtype=output.order_logits.dtype ) order_penalty = (F.softmax(output.order_logits, dim=-1) * indices).sum(-1).mean() values = dict( reconstruction=reconstruction, order_classification=classification, energy=energy, smoothness=smoothness, order_penalty=order_penalty, ) values["total"] = sum( getattr(weights, name) * value for name, value in values.items() ) return values