Source: src/mcss/losses.py · Plain text

src/mcss/losses.py

"""Masked modality losses used by the training engine."""

from __future__ import annotations

from collections.abc import Mapping

import torch
import torch.nn.functional as functional
from torch import Tensor, nn


class MeasurementLoss(nn.Module):
    """Combine only the requested, available measurements with explicit validity masks."""

    def __init__(self, weights: Mapping[str, float], *, epsilon: float = 1e-3) -> None:
        super().__init__()
        if epsilon <= 0:
            raise ValueError("epsilon must be positive")
        self.weights = {
            name: float(weight) for name, weight in weights.items() if float(weight) > 0
        }
        self.epsilon = epsilon

    def forward(
        self, predictions: Mapping[str, Tensor], targets: Mapping[str, Tensor]
    ) -> tuple[Tensor, dict[str, Tensor]]:
        if not predictions:
            raise ValueError("predictions cannot be empty")
        anchor = next(iter(predictions.values()))
        terms: dict[str, Tensor] = {}
        for name, weight in self.weights.items():
            if name not in predictions or name not in targets:
                continue
            terms[name] = _modality_loss(name, predictions, targets, self.epsilon)
            terms[name] = terms[name] * weight
        if not terms:
            return anchor.sum() * 0.0, {}
        total = torch.stack(list(terms.values())).sum()
        return total, terms


def _modality_loss(
    name: str, predictions: Mapping[str, Tensor], targets: Mapping[str, Tensor], epsilon: float
) -> Tensor:
    prediction = predictions[name]
    target = targets[name]
    if name == "rgb":
        valid = torch.isfinite(target).all(dim=-3, keepdim=True)
        valid = _apply_support(valid, targets)
        error = torch.sqrt((prediction - target).square() + epsilon**2)
        return _masked_mean(error, valid)
    if name == "depth":
        valid = torch.isfinite(target) & (target > 0)
        valid = _apply_support(valid, targets)
        relative = (prediction - target).abs() / target.abs().clamp_min(epsilon)
        return _masked_mean(relative, valid)
    if name == "normal":
        prediction = functional.normalize(prediction, dim=-3, eps=epsilon)
        target = functional.normalize(target, dim=-3, eps=epsilon)
        valid = torch.isfinite(target).all(dim=-3, keepdim=True) & (
            target.square().sum(dim=-3, keepdim=True) > epsilon
        )
        valid = _apply_support(valid, targets)
        cosine = (prediction * target).sum(dim=-3, keepdim=True).clamp(-1.0, 1.0)
        return _masked_mean(1.0 - cosine, valid)
    if name == "point":
        valid = torch.isfinite(target).all(dim=-3, keepdim=True)
        if "visibility" in targets:
            valid = valid & (targets["visibility"] > 0.5)
        valid = _apply_support(valid, targets)
        return _masked_mean((prediction - target).abs().mean(dim=-3, keepdim=True), valid)
    if name == "visibility":
        valid = torch.isfinite(target)
        valid = _apply_support(valid, targets)
        with torch.autocast(device_type=prediction.device.type, enabled=False):
            bce = functional.binary_cross_entropy(
                prediction.float().clamp(epsilon, 1 - epsilon),
                target.float(),
                reduction="none",
            )
        return _masked_mean(bce, valid)
    if name == "uncertainty":
        valid = _apply_support(torch.isfinite(target), targets)
        return _masked_mean(torch.sqrt((prediction - target).square() + epsilon**2), valid)
    raise ValueError(f"unsupported modality loss: {name}")


def _masked_mean(value: Tensor, mask: Tensor) -> Tensor:
    mask = torch.broadcast_to(mask, value.shape).to(value.dtype)
    while mask.ndim < value.ndim:
        mask = mask.unsqueeze(-3)
    denominator = mask.sum().clamp_min(1.0)
    return (value * mask).sum() / denominator


def _apply_support(mask: Tensor, targets: Mapping[str, Tensor]) -> Tensor:
    support = targets.get("support")
    if support is None:
        return mask
    return mask & torch.isfinite(support) & (support > 0.5)