Source code for personalitylinmult.data.collate

"""Batching for FI samples, producing the model's feature+mask input pairs.

The dataset already fixes every feature's time dimension, so collation is a
stack rather than a pad. What this module owns is the shape of the model call:
:func:`unpack_batch` turns a batch dict into the ``(inputs, masks, names)``
triple LinMulT expects, keeping the mask contract (``True`` = valid) in one
place instead of spread through the training step.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import torch

if TYPE_CHECKING:
    from collections.abc import Sequence


[docs] def collate_fi(samples: list[dict[str, Any]]) -> dict[str, Any]: """Collate FI samples into a batch. Args: samples (list[dict]): Per-clip dicts from :class:`~personalitylinmult.data.dataset.FiHdf5Dataset`. Returns: dict: Every tensor key stacked to a leading batch axis; ``video_id`` collected into a list. Raises: ValueError: If the batch is empty. """ if not samples: raise ValueError("Cannot collate an empty batch.") batch: dict[str, Any] = {"video_id": [s["video_id"] for s in samples]} for key, value in samples[0].items(): if isinstance(value, torch.Tensor): batch[key] = torch.stack([s[key] for s in samples], dim=0) return batch
[docs] def unpack_batch( batch: dict[str, Any], feature_names: Sequence[str] ) -> tuple[list[torch.Tensor], list[torch.Tensor], list[str]]: """Split a batch into the ``(inputs, masks, names)`` LinMulT expects. Every feature is passed with its mask. LinMulT drops all-``True`` masks internally, so a dense feature costs one ``.all()`` check and nothing more — which is why the contract stays uniform rather than special-casing which features "need" a mask. Args: batch (dict): A collated batch. feature_names (Sequence[str]): Features in model input order. Returns: tuple: ``inputs`` of ``(B, T, F)`` (or ``(B, L, T, F)`` for a multi-layer feature), ``masks`` of ``(B, T)`` with ``True`` = valid, and the feature names, which LinMulT needs to route special handling. Raises: KeyError: If a named feature or its mask is absent from the batch. """ inputs: list[torch.Tensor] = [] masks: list[torch.Tensor] = [] for name in feature_names: if name not in batch: raise KeyError(f"Batch has no feature {name!r}. Present: {sorted(batch)}.") mask_key = f"{name}_mask" if mask_key not in batch: raise KeyError( f"Batch has no mask for {name!r}. Every feature must be paired with a mask." ) inputs.append(batch[name]) masks.append(batch[mask_key]) return inputs, masks, list(feature_names)