Source code for core.samplers.session_batch_sampler
from collections.abc import Iterator
import torch
from torch_brain.datasets import DatasetIndex
[docs]
class SessionBatchSampler(torch.utils.data.Sampler[list[DatasetIndex]]):
"""Group any :class:`DatasetIndex` sampler's output into per-session batches.
All indices in a batch share the same session id. The inner sampler controls
the order of indices within each session; ``shuffle_batches`` controls the
order of batches across sessions.
Args:
sampler: Inner sampler whose ``__iter__`` yields :class:`DatasetIndex` objects.
batch_size: Number of samples per batch.
shuffle_batches: Whether to shuffle the final batch order. Defaults to False.
generator: Generator used when shuffling batches. Defaults to None.
drop_last: Whether to drop the last incomplete batch per session.
Defaults to True.
"""
def __init__(
self,
sampler: torch.utils.data.Sampler,
batch_size: int,
*,
shuffle_batches: bool = False,
generator: torch.Generator | None = None,
drop_last: bool = True,
):
if batch_size <= 0:
raise ValueError("batch_size must be positive.")
self.sampler = sampler
self.batch_size = batch_size
self.shuffle_batches = shuffle_batches
self.generator = generator
self.drop_last = drop_last
self._batches_cache: list[list[DatasetIndex]] | None = None
def _build_batches(self) -> list[list[DatasetIndex]]:
indices_by_session: dict[str, list[DatasetIndex]] = {}
for idx in self.sampler:
session_id = idx.recording_id
if session_id not in indices_by_session:
indices_by_session[session_id] = []
indices_by_session[session_id].append(idx)
batches: list[list[DatasetIndex]] = []
for indices in indices_by_session.values():
max_len = len(indices)
if self.drop_last:
max_len = (max_len // self.batch_size) * self.batch_size
for i in range(0, max_len, self.batch_size):
batch = indices[i : i + self.batch_size]
if len(batch) == self.batch_size or not self.drop_last:
batches.append(batch)
return batches
def _prepare_cache(self) -> None:
if self._batches_cache is None:
self._batches_cache = self._build_batches()
def __len__(self) -> int:
self._prepare_cache()
return len(self._batches_cache)
def __iter__(self) -> Iterator[list[DatasetIndex]]:
self._prepare_cache()
batches = self._batches_cache
self._batches_cache = None # reset so next epoch rebuilds
if self.shuffle_batches and batches:
for idx in torch.randperm(len(batches), generator=self.generator).tolist():
yield batches[idx]
else:
yield from batches
[docs]
def set_epoch(self, epoch: int) -> None:
"""Pass the epoch to the underlying sampler if it supports it."""
if hasattr(self.sampler, "set_epoch"):
self.sampler.set_epoch(epoch)