Source code for omniloader.transforms.mix

"""MixUp / CutMix as a collate-time wrapper.

Cross-sample mixing must happen at the batch level, so this is a *collator* that
wraps a base collate function rather than a per-sample :class:`Transform`. It mixes
only the input **feature** tensors and emits the metadata a training loop needs to
compute the mixed loss itself — the paired (shuffled) targets ``<target>_b``, the
mixing coefficient ``mixup_lambda`` and the permutation ``mixup_index``. No loss
logic lives here, keeping the library strictly data-side.

* ``mode="mixup"`` — convex blend ``lam * x + (1 - lam) * x[perm]``.
* ``mode="cutmix"`` — replace a contiguous time span of each sequence with the
  permuted sample's span; ``mixup_lambda`` is the retained fraction.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import numpy as np
import torch

if TYPE_CHECKING:
    from collections.abc import Sequence

    from omniloader.schema.spec import UnifiedSchema


[docs] class MixupCollator: """Wrap a base collator to apply MixUp/CutMix to feature tensors. Args: base_collate: The collator producing the batched dict (e.g. :func:`~omniloader.collate.unified_collate` or :class:`~omniloader.collate.DynamicCollator`). schema: The unified schema, used to pick feature/target keys. alpha: Beta distribution parameter; ``lam ~ Beta(alpha, alpha)``. mode: ``"mixup"`` (blend) or ``"cutmix"`` (span replacement). p: Per-batch probability of applying mixing (otherwise passthrough). seed: Seed for the internal NumPy RNG (deterministic across the run). Raises: ValueError: If ``mode`` is not ``"mixup"`` or ``"cutmix"``. """ def __init__( self, base_collate: Any, schema: UnifiedSchema, alpha: float = 0.2, mode: str = "mixup", p: float = 1.0, seed: int = 0, ) -> None: if mode not in {"mixup", "cutmix"}: raise ValueError(f"mode must be 'mixup' or 'cutmix', got {mode!r}") self.base_collate = base_collate self.feature_keys = list(schema.feature_keys) self.target_keys = list(schema.target_keys) self.seq_features = {spec.name for spec in schema.features if spec.is_sequence} self.alpha = alpha self.mode = mode self.p = p self._rng = np.random.default_rng(seed)
[docs] def __call__(self, batch: Sequence[dict[str, Any]]) -> dict[str, Any]: """Collate then mix; adds ``mixup_lambda``, ``mixup_index`` and ``<t>_b``.""" out = self.base_collate(batch) size = len(batch) if size < 2 or self._rng.random() >= self.p: # Passthrough with identity metadata so downstream code is uniform. out["mixup_lambda"] = 1.0 out["mixup_index"] = torch.arange(size) for key in self.target_keys: if key in out: out[f"{key}_b"] = out[key] return out perm = torch.from_numpy(self._rng.permutation(size)) # One lam for the whole batch keeps the mixed loss consistent across keys. lam = float(self._rng.beta(self.alpha, self.alpha)) rel_start = float(self._rng.random()) # shared relative cut position (cutmix) for key in self.feature_keys: value = out[key] other = value[perm] if self.mode == "mixup" or key not in self.seq_features: out[key] = lam * value + (1.0 - lam) * other # (B, ...) convex blend else: # cutmix: splice a shared time span from the permuted batch self._cut_span(value, other, lam, rel_start) for key in self.target_keys: if key in out: out[f"{key}_b"] = out[key][perm] out["mixup_lambda"] = lam out["mixup_index"] = perm return out
@staticmethod def _cut_span(value: torch.Tensor, other: torch.Tensor, keep: float, rel_start: float) -> None: """Replace a ``(1 - keep)`` time span of ``value`` with ``other`` in place. Args: value: Batched sequence feature ``(B, T, ...)``. other: The permuted batch to splice in. keep: Fraction of ``value`` to retain (the batch's ``mixup_lambda``). rel_start: Relative start of the cut window in ``[0, 1)``. """ length = value.shape[1] cut = round((1.0 - keep) * length) start = int(rel_start * (length - cut)) value[:, start : start + cut] = other[:, start : start + cut]