omniloader 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omniloader/__init__.py +141 -0
- omniloader/cli.py +156 -0
- omniloader/collate.py +123 -0
- omniloader/config.py +457 -0
- omniloader/data/__init__.py +0 -0
- omniloader/data/datasets.py +196 -0
- omniloader/data/factory.py +99 -0
- omniloader/data/npy.py +87 -0
- omniloader/data/splits.py +119 -0
- omniloader/integrations/__init__.py +0 -0
- omniloader/integrations/lightning.py +151 -0
- omniloader/introspection.py +190 -0
- omniloader/loader.py +255 -0
- omniloader/sampling/__init__.py +0 -0
- omniloader/sampling/bucketing.py +120 -0
- omniloader/sampling/sampler.py +112 -0
- omniloader/sampling/strategies.py +394 -0
- omniloader/sampling/subsamplers.py +150 -0
- omniloader/sampling/weights.py +187 -0
- omniloader/schema/__init__.py +0 -0
- omniloader/schema/spec.py +261 -0
- omniloader/schema/unify.py +135 -0
- omniloader/templates/__init__.py +32 -0
- omniloader/templates/config_template.yaml +211 -0
- omniloader/transforms/__init__.py +146 -0
- omniloader/transforms/augment.py +282 -0
- omniloader/transforms/base.py +137 -0
- omniloader/transforms/crop.py +122 -0
- omniloader/transforms/mix.py +112 -0
- omniloader/transforms/normalize.py +235 -0
- omniloader/transforms/stats.py +272 -0
- omniloader/utils/__init__.py +0 -0
- omniloader/utils/padding.py +159 -0
- omniloader/utils/seeding.py +31 -0
- omniloader-1.0.0.dist-info/METADATA +610 -0
- omniloader-1.0.0.dist-info/RECORD +39 -0
- omniloader-1.0.0.dist-info/WHEEL +4 -0
- omniloader-1.0.0.dist-info/entry_points.txt +2 -0
- omniloader-1.0.0.dist-info/licenses/LICENSE +21 -0
omniloader/__init__.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""OmniLoader: a PyTorch meta data loader for disjoint, multi-task datasets.
|
|
2
|
+
|
|
3
|
+
OmniLoader unifies several datasets with heterogeneous annotations into a single
|
|
4
|
+
masked sample scheme so a model can be trained jointly across all of them. It is
|
|
5
|
+
modality- and model-agnostic: it reasons only about vectors and sequences. See
|
|
6
|
+
:class:`~omniloader.loader.OmniLoader` for the entry point.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
12
|
+
|
|
13
|
+
from omniloader.collate import DynamicCollator, unified_collate
|
|
14
|
+
from omniloader.config import OmniConfig, seed_everything
|
|
15
|
+
from omniloader.data.datasets import DictTensorDataset, HDF5Dataset
|
|
16
|
+
from omniloader.data.factory import build_datasets
|
|
17
|
+
from omniloader.data.npy import NpyFolderDataset
|
|
18
|
+
from omniloader.data.splits import load_split_info, save_split_info, split_indices
|
|
19
|
+
from omniloader.introspection import Report, describe, validate
|
|
20
|
+
from omniloader.loader import OmniLoader
|
|
21
|
+
from omniloader.sampling.bucketing import LengthBucketBatchSampler
|
|
22
|
+
from omniloader.sampling.sampler import OmniSampler
|
|
23
|
+
from omniloader.sampling.strategies import (
|
|
24
|
+
AnnealedTemperatureStrategy,
|
|
25
|
+
FixedWeightStrategy,
|
|
26
|
+
MixingStrategy,
|
|
27
|
+
ProportionalStrategy,
|
|
28
|
+
RoundRobinStrategy,
|
|
29
|
+
SubsampleConfig,
|
|
30
|
+
TemperatureStrategy,
|
|
31
|
+
)
|
|
32
|
+
from omniloader.sampling.subsamplers import ExhaustionPolicy, IndexPool
|
|
33
|
+
from omniloader.sampling.weights import (
|
|
34
|
+
class_histogram,
|
|
35
|
+
class_weights_for_loss,
|
|
36
|
+
class_weights_for_sampler,
|
|
37
|
+
)
|
|
38
|
+
from omniloader.schema.spec import (
|
|
39
|
+
DatasetSchema,
|
|
40
|
+
TensorSpec,
|
|
41
|
+
UnifiedSchema,
|
|
42
|
+
)
|
|
43
|
+
from omniloader.schema.unify import SampleUnifier
|
|
44
|
+
from omniloader.templates import config_template_path
|
|
45
|
+
from omniloader.transforms import (
|
|
46
|
+
CenterCrop,
|
|
47
|
+
Compose,
|
|
48
|
+
FeatureDropout,
|
|
49
|
+
FeatureMasking,
|
|
50
|
+
GaussianNoise,
|
|
51
|
+
InstanceNormalize,
|
|
52
|
+
MinMaxNormalize,
|
|
53
|
+
MixupCollator,
|
|
54
|
+
Normalize,
|
|
55
|
+
PerDatasetNormalize,
|
|
56
|
+
RandomCrop,
|
|
57
|
+
RobustNormalize,
|
|
58
|
+
SpanMasking,
|
|
59
|
+
TimeWarp,
|
|
60
|
+
Transform,
|
|
61
|
+
build_transform,
|
|
62
|
+
compute_dataset_stats,
|
|
63
|
+
compute_stats,
|
|
64
|
+
load_stats,
|
|
65
|
+
save_stats,
|
|
66
|
+
)
|
|
67
|
+
from omniloader.transforms.stats import compute_feature_stats
|
|
68
|
+
from omniloader.utils.padding import (
|
|
69
|
+
fill_gaps_with_repeat,
|
|
70
|
+
pad_or_crop_time_dim,
|
|
71
|
+
repeat_pad_time_dim,
|
|
72
|
+
)
|
|
73
|
+
from omniloader.utils.seeding import seed_worker
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
__version__ = version("omniloader")
|
|
77
|
+
except PackageNotFoundError: # pragma: no cover - only during local, uninstalled use
|
|
78
|
+
__version__ = "1.0.0"
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"AnnealedTemperatureStrategy",
|
|
82
|
+
"CenterCrop",
|
|
83
|
+
"Compose",
|
|
84
|
+
"DatasetSchema",
|
|
85
|
+
"DictTensorDataset",
|
|
86
|
+
"DynamicCollator",
|
|
87
|
+
"ExhaustionPolicy",
|
|
88
|
+
"FeatureDropout",
|
|
89
|
+
"FeatureMasking",
|
|
90
|
+
"FixedWeightStrategy",
|
|
91
|
+
"GaussianNoise",
|
|
92
|
+
"HDF5Dataset",
|
|
93
|
+
"IndexPool",
|
|
94
|
+
"InstanceNormalize",
|
|
95
|
+
"LengthBucketBatchSampler",
|
|
96
|
+
"MinMaxNormalize",
|
|
97
|
+
"MixingStrategy",
|
|
98
|
+
"MixupCollator",
|
|
99
|
+
"Normalize",
|
|
100
|
+
"NpyFolderDataset",
|
|
101
|
+
"OmniConfig",
|
|
102
|
+
"OmniLoader",
|
|
103
|
+
"OmniSampler",
|
|
104
|
+
"PerDatasetNormalize",
|
|
105
|
+
"ProportionalStrategy",
|
|
106
|
+
"RandomCrop",
|
|
107
|
+
"Report",
|
|
108
|
+
"RobustNormalize",
|
|
109
|
+
"RoundRobinStrategy",
|
|
110
|
+
"SampleUnifier",
|
|
111
|
+
"SpanMasking",
|
|
112
|
+
"SubsampleConfig",
|
|
113
|
+
"TemperatureStrategy",
|
|
114
|
+
"TensorSpec",
|
|
115
|
+
"TimeWarp",
|
|
116
|
+
"Transform",
|
|
117
|
+
"UnifiedSchema",
|
|
118
|
+
"__version__",
|
|
119
|
+
"build_datasets",
|
|
120
|
+
"build_transform",
|
|
121
|
+
"class_histogram",
|
|
122
|
+
"class_weights_for_loss",
|
|
123
|
+
"class_weights_for_sampler",
|
|
124
|
+
"compute_dataset_stats",
|
|
125
|
+
"compute_feature_stats",
|
|
126
|
+
"compute_stats",
|
|
127
|
+
"config_template_path",
|
|
128
|
+
"describe",
|
|
129
|
+
"fill_gaps_with_repeat",
|
|
130
|
+
"load_split_info",
|
|
131
|
+
"load_stats",
|
|
132
|
+
"pad_or_crop_time_dim",
|
|
133
|
+
"repeat_pad_time_dim",
|
|
134
|
+
"save_split_info",
|
|
135
|
+
"save_stats",
|
|
136
|
+
"seed_everything",
|
|
137
|
+
"seed_worker",
|
|
138
|
+
"split_indices",
|
|
139
|
+
"unified_collate",
|
|
140
|
+
"validate",
|
|
141
|
+
]
|
omniloader/cli.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Command-line interface for inspecting a dataset mix from a config file.
|
|
2
|
+
|
|
3
|
+
Subcommands (all take a JSON/YAML config whose ``datasets`` section declares the
|
|
4
|
+
adapters and schemas — see :mod:`omniloader.data.factory`):
|
|
5
|
+
|
|
6
|
+
* ``omniloader describe CONFIG`` — print the coverage/statistics report.
|
|
7
|
+
* ``omniloader validate CONFIG`` — check datasets against their specs (exit 1 on issues).
|
|
8
|
+
* ``omniloader compute-stats CONFIG -o STATS`` — compute and save normalization stats.
|
|
9
|
+
* ``omniloader class-weights-for-loss CONFIG --target KEY -o WEIGHTS`` — compute and
|
|
10
|
+
save per-class loss weights (and counts) for a categorical target.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from omniloader.config import OmniConfig
|
|
21
|
+
from omniloader.introspection import describe, validate
|
|
22
|
+
from omniloader.loader import OmniLoader
|
|
23
|
+
from omniloader.sampling.weights import class_histogram, class_weights_for_loss
|
|
24
|
+
from omniloader.transforms.stats import compute_dataset_stats, compute_stats, save_stats
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Sequence
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load(config_path: str) -> tuple[list, list]:
|
|
31
|
+
"""Load a config and build its declared datasets and schemas."""
|
|
32
|
+
config = OmniConfig.from_file(config_path)
|
|
33
|
+
datasets, schemas = config.build_datasets()
|
|
34
|
+
if not datasets:
|
|
35
|
+
raise SystemExit(f"Config {config_path!r} declares no datasets")
|
|
36
|
+
return datasets, schemas
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _describe(args: argparse.Namespace) -> int:
|
|
40
|
+
"""Print the dataset report."""
|
|
41
|
+
datasets, schemas = _load(args.config)
|
|
42
|
+
print(describe(datasets, schemas, max_samples=args.max_samples))
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _validate(args: argparse.Namespace) -> int:
|
|
47
|
+
"""Validate datasets against their specs; exit non-zero if issues are found."""
|
|
48
|
+
datasets, schemas = _load(args.config)
|
|
49
|
+
issues = validate(datasets, schemas, num_samples=args.num_samples)
|
|
50
|
+
if issues:
|
|
51
|
+
print("\n".join(issues))
|
|
52
|
+
return 1
|
|
53
|
+
print("OK: all datasets match their declared specs.")
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _compute_stats(args: argparse.Namespace) -> int:
|
|
58
|
+
"""Compute normalization statistics over the loader and save them to JSON.
|
|
59
|
+
|
|
60
|
+
With ``--per-dataset`` the stats are grouped by source dataset (for
|
|
61
|
+
:class:`~omniloader.transforms.normalize.PerDatasetNormalize`); otherwise they
|
|
62
|
+
are pooled over the union.
|
|
63
|
+
"""
|
|
64
|
+
datasets, schemas = _load(args.config)
|
|
65
|
+
loader = OmniLoader(datasets, schemas)
|
|
66
|
+
keys = args.keys or loader.schema.feature_keys
|
|
67
|
+
samples = (loader[i] for i in range(len(loader)))
|
|
68
|
+
if args.per_dataset:
|
|
69
|
+
stats = compute_dataset_stats(samples, keys)
|
|
70
|
+
print(f"Saved per-dataset stats for {sorted(stats)} to {args.output}")
|
|
71
|
+
else:
|
|
72
|
+
stats = compute_stats(samples, keys)
|
|
73
|
+
print(f"Saved stats for {sorted(stats)} to {args.output}")
|
|
74
|
+
save_stats(stats, args.output)
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _class_weights_for_loss(args: argparse.Namespace) -> int:
|
|
79
|
+
"""Compute per-class counts and loss weights for a target and save them to JSON."""
|
|
80
|
+
datasets, schemas = _load(args.config)
|
|
81
|
+
loader = OmniLoader(datasets, schemas)
|
|
82
|
+
samples = [loader[i] for i in range(len(loader))] # single pass, reused for both
|
|
83
|
+
hist = class_histogram(samples, args.target, num_classes=args.num_classes)
|
|
84
|
+
weights = class_weights_for_loss(
|
|
85
|
+
samples, args.target, num_classes=args.num_classes, scheme=args.scheme, beta=args.beta
|
|
86
|
+
)
|
|
87
|
+
payload = {
|
|
88
|
+
"target": args.target,
|
|
89
|
+
"scheme": args.scheme,
|
|
90
|
+
"num_classes": int(weights.numel()),
|
|
91
|
+
"counts": hist.tolist(),
|
|
92
|
+
"weights": weights.tolist(),
|
|
93
|
+
}
|
|
94
|
+
Path(args.output).write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
95
|
+
print(
|
|
96
|
+
f"Saved class weights for {args.target!r} ({payload['num_classes']} classes) "
|
|
97
|
+
f"to {args.output}"
|
|
98
|
+
)
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
103
|
+
"""Construct the argument parser for the ``omniloader`` CLI."""
|
|
104
|
+
parser = argparse.ArgumentParser(prog="omniloader", description=__doc__)
|
|
105
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
106
|
+
|
|
107
|
+
p_desc = sub.add_parser("describe", help="print a dataset coverage/statistics report")
|
|
108
|
+
p_desc.add_argument("config", help="path to a JSON/YAML config")
|
|
109
|
+
p_desc.add_argument("--max-samples", type=int, default=64, dest="max_samples")
|
|
110
|
+
p_desc.set_defaults(func=_describe)
|
|
111
|
+
|
|
112
|
+
p_val = sub.add_parser("validate", help="check datasets against their declared specs")
|
|
113
|
+
p_val.add_argument("config", help="path to a JSON/YAML config")
|
|
114
|
+
p_val.add_argument("--num-samples", type=int, default=4, dest="num_samples")
|
|
115
|
+
p_val.set_defaults(func=_validate)
|
|
116
|
+
|
|
117
|
+
p_stats = sub.add_parser("compute-stats", help="compute and save normalization stats")
|
|
118
|
+
p_stats.add_argument("config", help="path to a JSON/YAML config")
|
|
119
|
+
p_stats.add_argument("-o", "--output", required=True, help="destination stats JSON")
|
|
120
|
+
p_stats.add_argument("--keys", nargs="*", help="feature keys (default: all features)")
|
|
121
|
+
p_stats.add_argument(
|
|
122
|
+
"--per-dataset",
|
|
123
|
+
action="store_true",
|
|
124
|
+
dest="per_dataset",
|
|
125
|
+
help="group stats by source dataset (for PerDatasetNormalize)",
|
|
126
|
+
)
|
|
127
|
+
p_stats.set_defaults(func=_compute_stats)
|
|
128
|
+
|
|
129
|
+
p_cw = sub.add_parser("class-weights-for-loss", help="compute and save per-class loss weights")
|
|
130
|
+
p_cw.add_argument("config", help="path to a JSON/YAML config")
|
|
131
|
+
p_cw.add_argument("--target", required=True, help="categorical target key")
|
|
132
|
+
p_cw.add_argument("-o", "--output", required=True, help="destination weights JSON")
|
|
133
|
+
p_cw.add_argument("--num-classes", type=int, default=None, dest="num_classes")
|
|
134
|
+
p_cw.add_argument("--scheme", choices=["inverse", "effective"], default="inverse")
|
|
135
|
+
p_cw.add_argument("--beta", type=float, default=0.999, help="beta for scheme=effective")
|
|
136
|
+
p_cw.set_defaults(func=_class_weights_for_loss)
|
|
137
|
+
|
|
138
|
+
return parser
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
142
|
+
"""Entry point for the ``omniloader`` console script.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
argv: Optional argument list (defaults to ``sys.argv``).
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The process exit code.
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
args = build_parser().parse_args(argv)
|
|
152
|
+
return int(args.func(args))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__": # pragma: no cover
|
|
156
|
+
raise SystemExit(main())
|
omniloader/collate.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Collate functions for batching unified samples.
|
|
2
|
+
|
|
3
|
+
Two collators are provided:
|
|
4
|
+
|
|
5
|
+
* :func:`unified_collate` — the default: every sample already shares the same
|
|
6
|
+
tensor shapes (the unified schema, with sequences padded to a fixed ``time_dim``),
|
|
7
|
+
so it simply stacks tensors and gathers string metadata into lists.
|
|
8
|
+
* :class:`DynamicCollator` — pads each sequence key only to the batch's longest
|
|
9
|
+
sample instead of a fixed ``time_dim``. Use it with
|
|
10
|
+
``OmniLoader(pad_features=False)`` to avoid wasting memory/compute on padding
|
|
11
|
+
when sequence lengths vary a lot across the batch.
|
|
12
|
+
|
|
13
|
+
Masks vs. cost: a ``<name>_mask`` guarantees *correctness* (padded steps never
|
|
14
|
+
corrupt the model output or the loss), but a padded step still costs compute and
|
|
15
|
+
memory (self-attention is ``O(T²)``). Dynamic padding targets that cost — so it is
|
|
16
|
+
only useful in the native-length regime. Under the default fixed ``time_dim``
|
|
17
|
+
(``pad_features=True``) every batch is already one length and :class:`DynamicCollator`
|
|
18
|
+
is a no-op.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
24
|
+
|
|
25
|
+
import torch
|
|
26
|
+
|
|
27
|
+
from omniloader.utils.padding import pad_or_crop_time_dim
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from collections.abc import Sequence
|
|
31
|
+
|
|
32
|
+
from omniloader.schema.spec import UnifiedSchema
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def unified_collate(batch: Sequence[dict[str, Any]]) -> dict[str, Any]:
|
|
36
|
+
"""Collate a list of unified samples into a batched dict.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
batch: Samples produced by :class:`~omniloader.loader.OmniLoader`.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A dict where each tensor key holds a stacked tensor of shape
|
|
43
|
+
``(B, ...)`` and each non-tensor key holds a list of length ``B``.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If the batch is empty.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
if not batch:
|
|
50
|
+
raise ValueError("Cannot collate an empty batch")
|
|
51
|
+
keys = batch[0].keys()
|
|
52
|
+
out: dict[str, Any] = {}
|
|
53
|
+
for key in keys:
|
|
54
|
+
values = [sample[key] for sample in batch]
|
|
55
|
+
if isinstance(values[0], torch.Tensor):
|
|
56
|
+
out[key] = torch.stack(values, dim=0)
|
|
57
|
+
else:
|
|
58
|
+
out[key] = list(values)
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DynamicCollator:
|
|
63
|
+
"""Collate with per-batch dynamic padding of sequence keys.
|
|
64
|
+
|
|
65
|
+
Sequence values (those whose spec sets ``time_dim``) are padded to the
|
|
66
|
+
longest sample in the batch — ``(B, L_batch, F)`` instead of a fixed
|
|
67
|
+
``(B, time_dim, F)`` — and their ``<name>_mask`` is rebuilt accordingly.
|
|
68
|
+
Vectors, scalars and metadata are batched exactly as
|
|
69
|
+
:func:`unified_collate` does. Pair with ``OmniLoader(pad_features=False)`` so
|
|
70
|
+
samples reach the collator at their native length; under the default fixed
|
|
71
|
+
``time_dim`` every sample already shares one length, so this collator has
|
|
72
|
+
nothing to shrink and behaves like :func:`unified_collate`.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
schema: The unified schema, used to identify sequence keys and their
|
|
76
|
+
placeholder fill values.
|
|
77
|
+
keys: Optional subset of sequence keys to pad dynamically. When ``None``,
|
|
78
|
+
every sequence key in the schema is padded dynamically.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, schema: UnifiedSchema, keys: Sequence[str] | None = None) -> None:
|
|
83
|
+
seq = {spec.name: spec for spec in schema.specs if spec.is_sequence}
|
|
84
|
+
self.seq_specs = seq if keys is None else {k: seq[k] for k in keys if k in seq}
|
|
85
|
+
|
|
86
|
+
def __call__(self, batch: Sequence[dict[str, Any]]) -> dict[str, Any]:
|
|
87
|
+
"""Collate ``batch``, dynamically padding the configured sequence keys.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
batch: Samples to collate.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The batched dict.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValueError: If the batch is empty.
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
if not batch:
|
|
100
|
+
raise ValueError("Cannot collate an empty batch")
|
|
101
|
+
out: dict[str, Any] = {}
|
|
102
|
+
for key in batch[0]:
|
|
103
|
+
# Sequence masks are produced alongside their value key; skip them.
|
|
104
|
+
if key.endswith("_mask") and key[:-5] in self.seq_specs:
|
|
105
|
+
continue
|
|
106
|
+
values = [sample[key] for sample in batch]
|
|
107
|
+
if key in self.seq_specs:
|
|
108
|
+
spec = self.seq_specs[key]
|
|
109
|
+
masks = [sample[f"{key}_mask"] for sample in batch]
|
|
110
|
+
length = max(v.shape[0] for v in values) # batch max sequence length
|
|
111
|
+
# (T_i, ...) -> (L, ...) padded with the spec placeholder value.
|
|
112
|
+
out[key] = torch.stack(
|
|
113
|
+
[pad_or_crop_time_dim(v, length, spec.placeholder)[0] for v in values]
|
|
114
|
+
)
|
|
115
|
+
# (T_i,) bool mask -> (L,) padded with False.
|
|
116
|
+
out[f"{key}_mask"] = torch.stack(
|
|
117
|
+
[pad_or_crop_time_dim(m.to(torch.float32), length)[0].bool() for m in masks]
|
|
118
|
+
)
|
|
119
|
+
elif isinstance(values[0], torch.Tensor):
|
|
120
|
+
out[key] = torch.stack(values, dim=0)
|
|
121
|
+
else:
|
|
122
|
+
out[key] = list(values)
|
|
123
|
+
return out
|