Skip to content

Commit e58dbf0

Browse files
committed
fix(data): support symbolic supervised projections
Build inert Map/Project/Select definitions when as_supervised receives a Definition or reference authority, while retaining live Map construction for materialized Dataset inputs.\n\nAccept Mat/Ref assertions without materializing their targets, document the behavior, and cover definition plus persisted-reference construction.\n\nVerified with the focused transform suite, affected model integration tests, and fatal flake8 checks.
1 parent 443ccee commit e58dbf0

3 files changed

Lines changed: 113 additions & 24 deletions

File tree

‎docs/data.md‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -325,11 +325,16 @@ Utility functions help with common supervised-learning structures:
325325

326326
`as_supervised(dataset, inputs, targets)` builds a persisted ordinary Dataset
327327
projection yielding `(inputs, targets)`, rather than asking trainers to select
328-
fields. A scalar path selects one branch (and can retain a nested dictionary);
329-
named/nested selections use dictionaries of `Select.from_path(...)` leaves.
330-
Tuple/list selection trees require explicit `Select.from_path` leaves so path and
331-
output-tree intent cannot be guessed. `input_as_target=True` produces an
332-
autoencoder-style pair without copies or implicit batching.
328+
fields. A live Dataset returns a live `Map`. A soft Definition,
329+
ConcreteDefinition, ObjectRef, StateRef, or explicit `Ref(...)`/`Mat(...)`
330+
assertion returns an inert `Map` Definition without resolving or materializing
331+
the source; an assertion contributes its retained target to that new materializing
332+
Dataset graph. A scalar path selects one branch (and can retain a nested
333+
dictionary); named/nested selections use dictionaries of
334+
`Select.from_path(...)` leaves. Tuple/list selection trees require explicit
335+
`Select.from_path` leaves so path and output-tree intent cannot be guessed.
336+
`input_as_target=True` produces an autoencoder-style pair without copies or
337+
implicit batching.
333338

334339
Native trainers consume this authored Dataset as-is. Put `Batch`, `Shuffle`,
335340
`Take`, and related controls in the Dataset graph before constructing an

‎src/dryml/data/util.py‎

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ def _xy_dataset(dataset, *, x_path=0, y_path=1):
1717

1818

1919
def as_supervised(dataset, inputs, targets=None, *, input_as_target: bool = False):
20-
"""Return a persisted Dataset graph yielding canonical ``(inputs, targets)`` pairs.
20+
"""Return a Dataset graph yielding canonical ``(inputs, targets)`` pairs.
2121
2222
Args:
23-
dataset: Source Dataset with a declared element specification.
23+
dataset: Source Dataset with a declared element specification, or a soft
24+
Definition, ConcreteDefinition, ObjectRef, StateRef, or explicit
25+
``Ref(...)``/``Mat(...)`` assertion expected to identify one.
2426
inputs: One scalar path, :class:`Select`, or nonempty tree of explicit
2527
``Select`` leaves describing model inputs.
2628
targets: Matching target selection. It is required unless
@@ -29,56 +31,83 @@ def as_supervised(dataset, inputs, targets=None, *, input_as_target: bool = Fals
2931
autoencoder-style Dataset without copying or backend conversion.
3032
3133
Returns:
32-
An ordinary :class:`~dryml.data.dataset.Map` Dataset whose values are
33-
``(inputs, targets)`` and whose Method tree is persisted with the graph.
34+
An ordinary :class:`~dryml.data.dataset.Map` Dataset for a live source,
35+
or an inert Map Definition for a symbolic source. Its values are
36+
``(inputs, targets)`` and its Method tree is persisted with the graph.
3437
3538
Raises:
36-
TypeError: If ``dataset`` is not a Dataset or a selection tree is malformed.
39+
TypeError: If ``dataset`` is not a Dataset or supported Dataset authority,
40+
or a selection tree is malformed. A symbolic source that does not
41+
materialize a Dataset fails at its normal Definition
42+
binding/materialization boundary.
3743
ValueError: If a selection tree is empty or targets conflict with
3844
``input_as_target``.
3945
4046
Side Effects:
41-
None. This helper does not open the Dataset, copy values, select a backend,
42-
or add batching.
47+
None. This helper does not open or materialize the Dataset, copy values,
48+
select a backend, or add batching.
4349
"""
4450

51+
from dryml.core import ConcreteDefinition, DefLink, Definition, ObjectRef, StateRef
4552
from dryml.data.dataset import Dataset, Map
4653

47-
if not isinstance(dataset, Dataset):
48-
raise TypeError("as_supervised requires a Dataset.")
54+
symbolic_types = (Definition, ConcreteDefinition, ObjectRef, StateRef)
55+
symbolic = isinstance(dataset, (*symbolic_types, DefLink))
56+
if not isinstance(dataset, Dataset) and not symbolic:
57+
raise TypeError("as_supervised requires a Dataset or Dataset reference.")
58+
if isinstance(dataset, DefLink):
59+
dataset = dataset.target
60+
if not isinstance(dataset, (Dataset, *symbolic_types)):
61+
raise TypeError("as_supervised received an unsupported Ref/Mat target.")
4962
if type(input_as_target) is not bool:
5063
raise TypeError("input_as_target must be an exact bool.")
51-
input_method = _selection_method(inputs)
64+
input_method = _selection_method(inputs, symbolic=symbolic)
5265
if input_as_target:
5366
if targets is not None:
5467
raise ValueError("input_as_target does not accept an explicit target selection.")
5568
target_method = input_method
5669
else:
5770
if targets is None:
5871
raise ValueError("as_supervised requires targets unless input_as_target is true.")
59-
target_method = _selection_method(targets)
60-
return Map(dataset, Project(input_method, target_method), preserves_examples=True)
61-
62-
63-
def _selection_method(selection, *, tree_leaf: bool = False):
72+
target_method = _selection_method(targets, symbolic=symbolic)
73+
project = (
74+
Project.defn(input_method, target_method)
75+
if symbolic else Project(input_method, target_method)
76+
)
77+
if symbolic:
78+
return Map.defn(dataset, project, preserves_examples=True)
79+
return Map(dataset, project, preserves_examples=True)
80+
81+
82+
def _selection_method(
83+
selection, *, tree_leaf: bool = False, symbolic: bool = False,
84+
):
6485
"""Build one Project-compatible Method tree while rejecting tuple/list ambiguity."""
6586

6687
if isinstance(selection, Select):
67-
return selection
88+
return Select.defn(selection.idxs) if symbolic else selection
6889
if isinstance(selection, (str, int)):
6990
if tree_leaf:
7091
raise TypeError("Selection trees require Select.from_path leaves.")
71-
return Select.from_path((selection,))
92+
return Select.defn(selection) if symbolic else Select.from_path((selection,))
7293
if isinstance(selection, dict):
7394
if not selection:
7495
raise ValueError("Selection trees must not be empty.")
75-
return Project({key: _selection_method(branch, tree_leaf=True) for key, branch in selection.items()})
96+
branches = {
97+
key: _selection_method(branch, tree_leaf=True, symbolic=symbolic)
98+
for key, branch in selection.items()
99+
}
100+
return Project.defn(branches) if symbolic else Project(branches)
76101
if isinstance(selection, (tuple, list)):
77102
if not selection:
78103
raise ValueError("Selection trees must not be empty.")
79104
if any(not isinstance(branch, Select) for branch in selection):
80105
raise TypeError("Ambiguous tuple/list selections require Select.from_path leaves.")
81-
return Project(type(selection)(_selection_method(branch, tree_leaf=True) for branch in selection))
106+
branches = type(selection)(
107+
_selection_method(branch, tree_leaf=True, symbolic=symbolic)
108+
for branch in selection
109+
)
110+
return Project.defn(branches) if symbolic else Project(branches)
82111
raise TypeError("Selections must be a scalar path, Select, or tree of Select leaves.")
83112

84113

‎tests/data/test_transform_nodes.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import numpy as np
55
import pytest
66

7+
from dryml.core import Definition, Mat, Ref
78
from dryml.core.cardinality import Cardinality
89
from dryml.core.backend import Backend
910
from dryml.core.tensor_spec import Dynamic, TensorSpec
@@ -773,6 +774,31 @@ def test_as_supervised_projects_scalar_whole_tree_named_and_autoencoder_targets(
773774
assert autoencoder_value[0] is autoencoder_value[1]
774775

775776

777+
@pytest.mark.parametrize("authority", ("soft", "concrete", "mat", "ref"))
778+
def test_as_supervised_authors_definition_native_autoencoder_projection(authority):
779+
"""Symbolic Dataset sources remain inert until their Map graph is built."""
780+
781+
source = Definition(
782+
ListDataset,
783+
[{"cart": np.array([1, 2])}],
784+
{"cart": TensorSpec("int64", shape=(2,), backend="numpy")},
785+
)
786+
if authority == "concrete":
787+
source = source.concretize()
788+
elif authority == "mat":
789+
source = Mat(source)
790+
elif authority == "ref":
791+
source = Ref(source)
792+
793+
projected = as_supervised(source, "cart", input_as_target=True)
794+
795+
assert isinstance(projected, Definition)
796+
assert projected.cls is Map
797+
inputs, targets = next(iter(projected.build()))
798+
assert inputs is targets
799+
assert inputs.tolist() == [1, 2]
800+
801+
776802
def test_as_supervised_rejects_empty_or_ambiguous_selection_trees():
777803
source = ListDataset(
778804
[{"x": np.array([1]), "y": np.array([2])}],
@@ -792,6 +818,35 @@ def test_as_supervised_rejects_empty_or_ambiguous_selection_trees():
792818
as_supervised(source, "x")
793819
with pytest.raises(ValueError, match="does not accept"):
794820
as_supervised(source, "x", "y", input_as_target=True)
821+
with pytest.raises(TypeError, match="Dataset or Dataset reference"):
822+
as_supervised(object(), "x", "y")
823+
824+
825+
def test_as_supervised_authors_graph_from_persisted_dataset_references(tmp_path):
826+
"""ObjectRef and StateRef inputs stay inert until the authored graph is built."""
827+
828+
from dryml.core import Repo
829+
from dryml.core.store.dir import DirStore
830+
831+
repo = Repo(DirStore(tmp_path / "store"))
832+
source = ListDataset(
833+
[{"x": np.array([1]), "y": np.array([2])}],
834+
{
835+
"x": TensorSpec("int64", shape=(1,), backend="numpy"),
836+
"y": TensorSpec("int64", shape=(1,), backend="numpy"),
837+
},
838+
)
839+
state_ref = repo.save_object(source, deep_capture=True)
840+
841+
for reference in (state_ref.object, state_ref):
842+
projected = as_supervised(reference, "x", "y")
843+
844+
assert isinstance(projected, Definition)
845+
assert projected.cls is Map
846+
847+
inputs, targets = next(iter(as_supervised(state_ref, "x", "y").build(repo=repo)))
848+
assert inputs.tolist() == [1]
849+
assert targets.tolist() == [2]
795850

796851

797852
def test_as_supervised_nested_and_tuple_selection_trees_survive_save_load(tmp_path):

0 commit comments

Comments
 (0)