ho22joshua's picture
Claude Opus 5.5
feat: add the ATLAS Open Data GamGam 5-class task and faster training
5b73bcb
Raw History Blame Contribute Delete
6.68 kB
"""Deterministic padded sequence samples for transformer models."""
from __future__ import annotations
from collections.abc import Iterator, Sequence
from dataclasses import dataclass
from typing import Any
import torch
from gnn4colliders.data.events import PackedEvents
from gnn4colliders.data.metadata import BatchMetadata, EventMetadata
from gnn4colliders.data.sample import EventSample
from gnn4colliders.features import build_node_features
@dataclass(frozen=True)
class SequenceSample:
"""One event represented as an ordered sequence of object tokens."""
tokens: torch.Tensor
label: torch.Tensor
global_features: torch.Tensor | None
metadata: EventMetadata
@dataclass(frozen=True)
class SequenceBatch:
"""A padded batch with a boolean mask of valid tokens."""
tokens: torch.Tensor
token_mask: torch.Tensor
labels: torch.Tensor
global_features: torch.Tensor | None
metadata: BatchMetadata
def to(
self, device: torch.device | str, *, non_blocking: bool = False
) -> "SequenceBatch":
return self._map(lambda value: value.to(device, non_blocking=non_blocking))
def pin_memory(self) -> "SequenceBatch":
return self._map(lambda value: value.pin_memory())
def _map(self, move: Any) -> "SequenceBatch":
metadata = BatchMetadata(
fold=move(self.metadata.fold),
weight=move(self.metadata.weight),
sample_id=self.metadata.sample_id,
extra=self.metadata.extra,
)
return SequenceBatch(
tokens=move(self.tokens),
token_mask=move(self.token_mask),
labels=move(self.labels),
global_features=(
None if self.global_features is None else move(self.global_features)
),
metadata=metadata,
)
def sequence_sample_from_event(
event: EventSample,
feature_branches: Sequence[Any],
object_types: Sequence[str],
scales: Sequence[float] | torch.Tensor,
) -> SequenceSample:
"""Build tokens from shared collider features without graph topology."""
tokens, _ = build_node_features(
event.objects, feature_branches, object_types, scales
)
globals_ = torch.as_tensor(event.global_features, dtype=torch.float32)
return SequenceSample(
tokens=tokens,
label=torch.as_tensor(event.label),
global_features=globals_ if globals_.numel() else None,
metadata=event.event_metadata,
)
def collate_sequence_batch(
events: PackedEvents, indices: torch.Tensor
) -> SequenceBatch:
"""Pad the object rows of ``indices`` into a token batch, in order."""
if indices.numel() == 0:
raise ValueError("cannot batch an empty sequence")
rows, counts = events.gather_objects(indices)
max_length = int(counts.max()) if counts.numel() else 0
batch_index = torch.repeat_interleave(
torch.arange(counts.numel()), counts, output_size=rows.shape[0]
)
position = (
torch.arange(rows.shape[0]) - (torch.cumsum(counts, 0) - counts)[batch_index]
)
tokens = rows.new_zeros((counts.numel(), max_length, rows.shape[1]))
tokens[batch_index, position] = rows
token_mask = torch.zeros((counts.numel(), max_length), dtype=torch.bool)
token_mask[batch_index, position] = True
return SequenceBatch(
tokens=tokens,
token_mask=token_mask,
labels=events.labels[indices],
global_features=(
None if events.global_features is None else events.global_features[indices]
),
metadata=events.batch_metadata(indices),
)
def batch_sequence_samples(samples: Sequence[SequenceSample]) -> SequenceBatch:
"""Pad a sequence of variable-length events in deterministic order."""
if not samples:
raise ValueError("cannot batch an empty sequence")
feature_widths = {int(sample.tokens.shape[1]) for sample in samples}
if any(sample.tokens.ndim != 2 for sample in samples):
raise ValueError("sequence tokens must have shape [objects, features]")
if len(feature_widths) != 1:
raise ValueError("all sequence samples must have the same feature width")
max_length = max(int(sample.tokens.shape[0]) for sample in samples)
feature_width = next(iter(feature_widths))
tokens = samples[0].tokens.new_zeros((len(samples), max_length, feature_width))
token_mask = torch.zeros(
(len(samples), max_length), dtype=torch.bool, device=tokens.device
)
for index, sample in enumerate(samples):
length = int(sample.tokens.shape[0])
tokens[index, :length] = sample.tokens
token_mask[index, :length] = True
labels = torch.stack([torch.as_tensor(sample.label) for sample in samples])
globals_ = [sample.global_features for sample in samples]
if any(value is None for value in globals_):
if not all(value is None for value in globals_):
raise ValueError("global_features must be present for every sample or none")
batched_globals = None
else:
batched_globals = torch.stack(
[value for value in globals_ if value is not None]
)
return SequenceBatch(
tokens=tokens,
token_mask=token_mask,
labels=labels,
global_features=batched_globals,
metadata=BatchMetadata.from_events([sample.metadata for sample in samples]),
)
class SequenceDataLoader:
"""Small deterministic loader for variable-length sequence samples."""
def __init__(
self,
dataset: Sequence[SequenceSample],
batch_size: int,
*,
shuffle: bool = False,
seed: int = 12345,
) -> None:
if batch_size < 1:
raise ValueError("batch_size must be positive")
self.dataset = dataset
self.batch_size = batch_size
self.shuffle = shuffle
self.seed = seed
self.epoch = 0
def set_epoch(self, epoch: int) -> None:
self.epoch = epoch
def __len__(self) -> int:
return (len(self.dataset) + self.batch_size - 1) // self.batch_size
def __iter__(self) -> Iterator[SequenceBatch]:
indices = torch.arange(len(self.dataset), dtype=torch.long)
if self.shuffle:
generator = torch.Generator().manual_seed(self.seed + self.epoch)
indices = indices[torch.randperm(len(indices), generator=generator)]
for start in range(0, len(indices), self.batch_size):
selected = [
self.dataset[int(index)]
for index in indices[start : start + self.batch_size]
]
yield batch_sequence_samples(selected)