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.
Files changed (39) hide show
  1. omniloader/__init__.py +141 -0
  2. omniloader/cli.py +156 -0
  3. omniloader/collate.py +123 -0
  4. omniloader/config.py +457 -0
  5. omniloader/data/__init__.py +0 -0
  6. omniloader/data/datasets.py +196 -0
  7. omniloader/data/factory.py +99 -0
  8. omniloader/data/npy.py +87 -0
  9. omniloader/data/splits.py +119 -0
  10. omniloader/integrations/__init__.py +0 -0
  11. omniloader/integrations/lightning.py +151 -0
  12. omniloader/introspection.py +190 -0
  13. omniloader/loader.py +255 -0
  14. omniloader/sampling/__init__.py +0 -0
  15. omniloader/sampling/bucketing.py +120 -0
  16. omniloader/sampling/sampler.py +112 -0
  17. omniloader/sampling/strategies.py +394 -0
  18. omniloader/sampling/subsamplers.py +150 -0
  19. omniloader/sampling/weights.py +187 -0
  20. omniloader/schema/__init__.py +0 -0
  21. omniloader/schema/spec.py +261 -0
  22. omniloader/schema/unify.py +135 -0
  23. omniloader/templates/__init__.py +32 -0
  24. omniloader/templates/config_template.yaml +211 -0
  25. omniloader/transforms/__init__.py +146 -0
  26. omniloader/transforms/augment.py +282 -0
  27. omniloader/transforms/base.py +137 -0
  28. omniloader/transforms/crop.py +122 -0
  29. omniloader/transforms/mix.py +112 -0
  30. omniloader/transforms/normalize.py +235 -0
  31. omniloader/transforms/stats.py +272 -0
  32. omniloader/utils/__init__.py +0 -0
  33. omniloader/utils/padding.py +159 -0
  34. omniloader/utils/seeding.py +31 -0
  35. omniloader-1.0.0.dist-info/METADATA +610 -0
  36. omniloader-1.0.0.dist-info/RECORD +39 -0
  37. omniloader-1.0.0.dist-info/WHEEL +4 -0
  38. omniloader-1.0.0.dist-info/entry_points.txt +2 -0
  39. 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