dataeval-flow 0.1.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.
- dataeval_flow/__init__.py +93 -0
- dataeval_flow/__main__.py +149 -0
- dataeval_flow/_app/__init__.py +5 -0
- dataeval_flow/_app/_model/__init__.py +5 -0
- dataeval_flow/_app/_model/_coerce.py +126 -0
- dataeval_flow/_app/_model/_discover.py +171 -0
- dataeval_flow/_app/_model/_execution.py +108 -0
- dataeval_flow/_app/_model/_introspect.py +280 -0
- dataeval_flow/_app/_model/_item.py +213 -0
- dataeval_flow/_app/_model/_registry.py +255 -0
- dataeval_flow/_app/_model/_state.py +322 -0
- dataeval_flow/_app/_model/_undo.py +61 -0
- dataeval_flow/_app/_panes/__init__.py +35 -0
- dataeval_flow/_app/_panes/_config_pane.py +173 -0
- dataeval_flow/_app/_panes/_result_pane.py +125 -0
- dataeval_flow/_app/_panes/_task_pane.py +91 -0
- dataeval_flow/_app/_panes/_widgets.py +111 -0
- dataeval_flow/_app/_screens/__init__.py +25 -0
- dataeval_flow/_app/_screens/_base.py +242 -0
- dataeval_flow/_app/_screens/_detail.py +333 -0
- dataeval_flow/_app/_screens/_model.py +102 -0
- dataeval_flow/_app/_screens/_params.py +80 -0
- dataeval_flow/_app/_screens/_pathpicker.py +68 -0
- dataeval_flow/_app/_screens/_section.py +621 -0
- dataeval_flow/_app/_screens/_settings.py +183 -0
- dataeval_flow/_app/_viewmodel/__init__.py +15 -0
- dataeval_flow/_app/_viewmodel/_builder_vm.py +272 -0
- dataeval_flow/_app/_viewmodel/_model_vm.py +70 -0
- dataeval_flow/_app/_viewmodel/_rendering.py +189 -0
- dataeval_flow/_app/_viewmodel/_result_vm.py +210 -0
- dataeval_flow/_app/_viewmodel/_section_vm.py +224 -0
- dataeval_flow/_app/app.py +742 -0
- dataeval_flow/_app/cli.py +592 -0
- dataeval_flow/_logging.py +102 -0
- dataeval_flow/cache.py +1355 -0
- dataeval_flow/config/__init__.py +80 -0
- dataeval_flow/config/_loader.py +79 -0
- dataeval_flow/config/_merge.py +92 -0
- dataeval_flow/config/_models.py +115 -0
- dataeval_flow/config/_paths.py +85 -0
- dataeval_flow/config/schemas/__init__.py +112 -0
- dataeval_flow/config/schemas/_dataset.py +111 -0
- dataeval_flow/config/schemas/_extractor.py +119 -0
- dataeval_flow/config/schemas/_metadata.py +28 -0
- dataeval_flow/config/schemas/_preprocessor.py +18 -0
- dataeval_flow/config/schemas/_selection.py +100 -0
- dataeval_flow/config/schemas/_task.py +89 -0
- dataeval_flow/config/schemas/_workflow.py +135 -0
- dataeval_flow/dataset.py +635 -0
- dataeval_flow/embeddings.py +135 -0
- dataeval_flow/metadata.py +48 -0
- dataeval_flow/preprocessing.py +141 -0
- dataeval_flow/py.typed +0 -0
- dataeval_flow/runner.py +118 -0
- dataeval_flow/selection.py +50 -0
- dataeval_flow/workflow/__init__.py +328 -0
- dataeval_flow/workflow/_text_report.py +511 -0
- dataeval_flow/workflow/base.py +69 -0
- dataeval_flow/workflow/orchestrator.py +454 -0
- dataeval_flow/workflows/__init__.py +1 -0
- dataeval_flow/workflows/analysis/__init__.py +38 -0
- dataeval_flow/workflows/analysis/outputs.py +202 -0
- dataeval_flow/workflows/analysis/params.py +114 -0
- dataeval_flow/workflows/analysis/workflow.py +1313 -0
- dataeval_flow/workflows/cleaning/__init__.py +23 -0
- dataeval_flow/workflows/cleaning/outputs.py +200 -0
- dataeval_flow/workflows/cleaning/params.py +160 -0
- dataeval_flow/workflows/cleaning/report.py +304 -0
- dataeval_flow/workflows/cleaning/workflow.py +794 -0
- dataeval_flow/workflows/drift/__init__.py +1 -0
- dataeval_flow/workflows/drift/outputs.py +144 -0
- dataeval_flow/workflows/drift/params.py +332 -0
- dataeval_flow/workflows/drift/report.py +201 -0
- dataeval_flow/workflows/drift/workflow.py +647 -0
- dataeval_flow/workflows/ood/__init__.py +1 -0
- dataeval_flow/workflows/ood/outputs.py +134 -0
- dataeval_flow/workflows/ood/params.py +161 -0
- dataeval_flow/workflows/ood/report.py +311 -0
- dataeval_flow/workflows/ood/workflow.py +728 -0
- dataeval_flow/workflows/prioritization/__init__.py +1 -0
- dataeval_flow/workflows/prioritization/outputs.py +122 -0
- dataeval_flow/workflows/prioritization/params.py +124 -0
- dataeval_flow/workflows/prioritization/report.py +117 -0
- dataeval_flow/workflows/prioritization/workflow.py +587 -0
- dataeval_flow/workflows/splitting/__init__.py +25 -0
- dataeval_flow/workflows/splitting/outputs.py +101 -0
- dataeval_flow/workflows/splitting/params.py +61 -0
- dataeval_flow/workflows/splitting/report.py +485 -0
- dataeval_flow/workflows/splitting/workflow.py +371 -0
- dataeval_flow-0.1.0.dist-info/METADATA +305 -0
- dataeval_flow-0.1.0.dist-info/RECORD +94 -0
- dataeval_flow-0.1.0.dist-info/WHEEL +4 -0
- dataeval_flow-0.1.0.dist-info/entry_points.txt +2 -0
- dataeval_flow-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Dataset splitting workflow implementation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from dataeval_flow.workflow import WorkflowContext, WorkflowResult
|
|
11
|
+
from dataeval_flow.workflows.splitting.outputs import (
|
|
12
|
+
DataSplittingMetadata,
|
|
13
|
+
DataSplittingOutputs,
|
|
14
|
+
DataSplittingRawOutputs,
|
|
15
|
+
DataSplittingReport,
|
|
16
|
+
SplitInfo,
|
|
17
|
+
)
|
|
18
|
+
from dataeval_flow.workflows.splitting.params import DataSplittingParameters
|
|
19
|
+
from dataeval_flow.workflows.splitting.report import build_findings
|
|
20
|
+
|
|
21
|
+
__all__ = ["DataSplittingWorkflow"]
|
|
22
|
+
|
|
23
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Serialization helpers
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _serialize_label_stats(stats: Any) -> dict[str, Any]:
|
|
32
|
+
"""Convert a LabelStatsResult to a plain dict."""
|
|
33
|
+
if stats is None:
|
|
34
|
+
return {}
|
|
35
|
+
result: dict[str, Any] = {}
|
|
36
|
+
for key in (
|
|
37
|
+
"label_counts_per_class",
|
|
38
|
+
"image_counts_per_class",
|
|
39
|
+
"class_count",
|
|
40
|
+
"label_count",
|
|
41
|
+
"image_count",
|
|
42
|
+
"index2label",
|
|
43
|
+
):
|
|
44
|
+
val = stats.get(key, None) if hasattr(stats, "get") else getattr(stats, key, None)
|
|
45
|
+
if val is not None:
|
|
46
|
+
if hasattr(val, "tolist"):
|
|
47
|
+
val = val.tolist()
|
|
48
|
+
result[key] = val
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _serialize_balance(output: Any) -> dict[str, Any]:
|
|
53
|
+
"""Convert BalanceOutput to a plain dict."""
|
|
54
|
+
result: dict[str, Any] = {}
|
|
55
|
+
for attr in ("balance", "factors", "classwise"):
|
|
56
|
+
df = getattr(output, attr, None)
|
|
57
|
+
if df is not None:
|
|
58
|
+
result[attr] = df.to_dicts() if hasattr(df, "to_dicts") else str(df)
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _serialize_diversity(output: Any) -> dict[str, Any]:
|
|
63
|
+
"""Convert DiversityOutput to a plain dict."""
|
|
64
|
+
result: dict[str, Any] = {}
|
|
65
|
+
for attr in ("factors", "classwise"):
|
|
66
|
+
df = getattr(output, attr, None)
|
|
67
|
+
if df is not None:
|
|
68
|
+
result[attr] = df.to_dicts() if hasattr(df, "to_dicts") else str(df)
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _serialize_coverage(coverage_result: Any) -> dict[str, Any]:
|
|
73
|
+
"""Convert CoverageResult to a plain dict."""
|
|
74
|
+
result: dict[str, Any] = {}
|
|
75
|
+
for key in ("uncovered_indices", "critical_value_radii", "coverage_radius"):
|
|
76
|
+
val = coverage_result.get(key, None) if hasattr(coverage_result, "get") else getattr(coverage_result, key, None)
|
|
77
|
+
if val is not None:
|
|
78
|
+
if hasattr(val, "tolist"):
|
|
79
|
+
val = val.tolist()
|
|
80
|
+
result[key] = val
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Embedding-space coverage helper (step 7)
|
|
86
|
+
# Extracted from _execute to satisfy C901 complexity limit.
|
|
87
|
+
# "Coverage" here refers to dataeval.core.coverage_adaptive(), not test coverage.
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _run_coverage(
|
|
92
|
+
ds_ctx: Any,
|
|
93
|
+
dataset: Any,
|
|
94
|
+
fold_infos: list[SplitInfo],
|
|
95
|
+
test_indices: list[int],
|
|
96
|
+
params: DataSplittingParameters,
|
|
97
|
+
) -> dict[str, Any] | None:
|
|
98
|
+
"""Run per-split coverage assessment if model is provided."""
|
|
99
|
+
if ds_ctx.extractor is None:
|
|
100
|
+
logger.info("Step 7: Skipping coverage (no model provided)")
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
from dataeval.core import coverage_adaptive
|
|
104
|
+
|
|
105
|
+
from dataeval_flow.embeddings import build_embeddings
|
|
106
|
+
|
|
107
|
+
logger.info("Step 7: Per-split coverage assessment")
|
|
108
|
+
embeddings_obj = build_embeddings(
|
|
109
|
+
dataset,
|
|
110
|
+
ds_ctx.extractor,
|
|
111
|
+
transforms=ds_ctx.transforms,
|
|
112
|
+
batch_size=ds_ctx.batch_size,
|
|
113
|
+
)
|
|
114
|
+
all_embeddings = np.array(embeddings_obj)
|
|
115
|
+
|
|
116
|
+
# Normalize to [0, 1]
|
|
117
|
+
emb_min = all_embeddings.min(axis=0, keepdims=True)
|
|
118
|
+
emb_max = all_embeddings.max(axis=0, keepdims=True)
|
|
119
|
+
emb_range = emb_max - emb_min
|
|
120
|
+
emb_range[emb_range == 0] = 1.0
|
|
121
|
+
all_embeddings = (all_embeddings - emb_min) / emb_range
|
|
122
|
+
|
|
123
|
+
for fold_info in fold_infos:
|
|
124
|
+
train_embs = all_embeddings[fold_info.train_indices]
|
|
125
|
+
val_embs = all_embeddings[fold_info.val_indices]
|
|
126
|
+
fold_info.coverage_train = _serialize_coverage(
|
|
127
|
+
coverage_adaptive(train_embs, params.num_observations, params.coverage_percent)
|
|
128
|
+
)
|
|
129
|
+
fold_info.coverage_val = _serialize_coverage(
|
|
130
|
+
coverage_adaptive(val_embs, params.num_observations, params.coverage_percent)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
coverage_test_data: dict[str, Any] | None = None
|
|
134
|
+
if test_indices:
|
|
135
|
+
test_embs = all_embeddings[test_indices]
|
|
136
|
+
coverage_test_data = _serialize_coverage(
|
|
137
|
+
coverage_adaptive(test_embs, params.num_observations, params.coverage_percent)
|
|
138
|
+
)
|
|
139
|
+
return coverage_test_data
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Workflow class
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class DataSplittingWorkflow:
|
|
148
|
+
"""Dataset splitting workflow.
|
|
149
|
+
|
|
150
|
+
Assesses dataset balance/diversity, produces stratified train/val/test
|
|
151
|
+
splits, and optionally rebalances the train split.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def name(self) -> str:
|
|
156
|
+
"""Workflow identifier."""
|
|
157
|
+
return "data-splitting"
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def description(self) -> str:
|
|
161
|
+
"""Human-readable description."""
|
|
162
|
+
return (
|
|
163
|
+
"Assess dataset balance/diversity, produce stratified train/val/test "
|
|
164
|
+
"splits, and optionally rebalance the train split."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def params_schema(self) -> type[BaseModel]:
|
|
169
|
+
"""Pydantic model for workflow parameters."""
|
|
170
|
+
return DataSplittingParameters
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def output_schema(self) -> type[BaseModel]:
|
|
174
|
+
"""Pydantic model for workflow output."""
|
|
175
|
+
return DataSplittingOutputs
|
|
176
|
+
|
|
177
|
+
def execute(
|
|
178
|
+
self,
|
|
179
|
+
context: WorkflowContext,
|
|
180
|
+
params: BaseModel | None = None,
|
|
181
|
+
) -> WorkflowResult[DataSplittingMetadata, DataSplittingOutputs]:
|
|
182
|
+
"""Execute the splitting workflow."""
|
|
183
|
+
if not isinstance(context, WorkflowContext):
|
|
184
|
+
msg = f"Expected WorkflowContext, got {type(context).__name__}"
|
|
185
|
+
raise TypeError(msg)
|
|
186
|
+
|
|
187
|
+
if params is not None and not isinstance(params, DataSplittingParameters):
|
|
188
|
+
msg = f"Expected DataSplittingParameters, got {type(params).__name__}"
|
|
189
|
+
raise TypeError(msg)
|
|
190
|
+
|
|
191
|
+
p = params or DataSplittingParameters()
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
return self._execute(context, p)
|
|
195
|
+
except Exception:
|
|
196
|
+
logger.exception("Splitting workflow failed")
|
|
197
|
+
return WorkflowResult(
|
|
198
|
+
name=self.name,
|
|
199
|
+
success=False,
|
|
200
|
+
data=_empty_outputs(),
|
|
201
|
+
errors=[traceback.format_exc()],
|
|
202
|
+
metadata=DataSplittingMetadata(),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def _execute(
|
|
206
|
+
self,
|
|
207
|
+
context: WorkflowContext,
|
|
208
|
+
params: DataSplittingParameters,
|
|
209
|
+
) -> WorkflowResult[DataSplittingMetadata, DataSplittingOutputs]:
|
|
210
|
+
from dataeval.bias import Balance, Diversity
|
|
211
|
+
from dataeval.core import label_stats
|
|
212
|
+
from dataeval.utils.data import split_dataset
|
|
213
|
+
|
|
214
|
+
from dataeval_flow.metadata import build_metadata
|
|
215
|
+
from dataeval_flow.selection import build_selection
|
|
216
|
+
|
|
217
|
+
# --- Resolve single dataset ---
|
|
218
|
+
if not context.dataset_contexts:
|
|
219
|
+
msg = "No datasets provided"
|
|
220
|
+
raise ValueError(msg)
|
|
221
|
+
|
|
222
|
+
ds_name = next(iter(context.dataset_contexts))
|
|
223
|
+
ds_ctx = context.dataset_contexts[ds_name]
|
|
224
|
+
dataset: Any = ds_ctx.dataset
|
|
225
|
+
|
|
226
|
+
# Apply selection if configured
|
|
227
|
+
if ds_ctx.selection_steps:
|
|
228
|
+
dataset = build_selection(dataset, list(ds_ctx.selection_steps))
|
|
229
|
+
|
|
230
|
+
dataset_size = len(dataset)
|
|
231
|
+
logger.info("Step 1: Building metadata for %s (%d items)", ds_name, dataset_size)
|
|
232
|
+
|
|
233
|
+
# --- Step 1: Build Metadata ---
|
|
234
|
+
metadata = build_metadata(dataset)
|
|
235
|
+
|
|
236
|
+
# --- Step 2: Pre-split bias assessment ---
|
|
237
|
+
logger.info("Step 2: Pre-split bias assessment")
|
|
238
|
+
balance_output = Balance().evaluate(metadata)
|
|
239
|
+
diversity_output = Diversity().evaluate(metadata)
|
|
240
|
+
|
|
241
|
+
pre_split_balance = _serialize_balance(balance_output)
|
|
242
|
+
pre_split_diversity = _serialize_diversity(diversity_output)
|
|
243
|
+
|
|
244
|
+
# --- Step 3: Full-dataset label stats ---
|
|
245
|
+
logger.info("Step 3: Label statistics (full dataset)")
|
|
246
|
+
class_labels = metadata.class_labels
|
|
247
|
+
index2label = metadata.index2label if hasattr(metadata, "index2label") else None
|
|
248
|
+
full_stats = label_stats(class_labels, index2label=index2label)
|
|
249
|
+
label_stats_full = _serialize_label_stats(full_stats)
|
|
250
|
+
|
|
251
|
+
# --- Step 4: Split ---
|
|
252
|
+
logger.info(
|
|
253
|
+
"Step 4: Splitting dataset (num_folds=%d, stratify=%s, test_frac=%s, val_frac=%s)",
|
|
254
|
+
params.num_folds,
|
|
255
|
+
params.stratify,
|
|
256
|
+
params.test_frac,
|
|
257
|
+
params.val_frac,
|
|
258
|
+
)
|
|
259
|
+
# Use metadata if split_on is specified, otherwise use dataset directly
|
|
260
|
+
split_input: Any = metadata if params.split_on else dataset
|
|
261
|
+
# val_frac is only valid for single-fold; multi-fold uses 1/num_folds automatically
|
|
262
|
+
val_frac = params.val_frac if params.num_folds == 1 else 0.0
|
|
263
|
+
splits = split_dataset(
|
|
264
|
+
split_input,
|
|
265
|
+
num_folds=params.num_folds,
|
|
266
|
+
stratify=params.stratify,
|
|
267
|
+
split_on=params.split_on,
|
|
268
|
+
test_frac=params.test_frac,
|
|
269
|
+
val_frac=val_frac,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
test_indices = splits.test.tolist()
|
|
273
|
+
|
|
274
|
+
# --- Step 5: Optional rebalancing ---
|
|
275
|
+
# ClassBalance is a selection step that operates on a Select-wrapped dataset.
|
|
276
|
+
# For now we store the raw indices; rebalancing modifies train indices.
|
|
277
|
+
fold_infos: list[SplitInfo] = []
|
|
278
|
+
for i, fold in enumerate(splits.folds):
|
|
279
|
+
train_idx = fold.train.tolist()
|
|
280
|
+
val_idx = fold.val.tolist()
|
|
281
|
+
|
|
282
|
+
if params.rebalance_method is not None:
|
|
283
|
+
logger.info("Step 5: Rebalancing fold %d train split (method=%s)", i, params.rebalance_method)
|
|
284
|
+
from dataeval.selection import ClassBalance, Indices, Select
|
|
285
|
+
|
|
286
|
+
train_selected = Select(dataset, Indices(train_idx))
|
|
287
|
+
ClassBalance(method=params.rebalance_method)(train_selected)
|
|
288
|
+
train_idx = train_selected.resolve_indices()
|
|
289
|
+
|
|
290
|
+
fold_infos.append(
|
|
291
|
+
SplitInfo(
|
|
292
|
+
fold=i,
|
|
293
|
+
train_indices=train_idx,
|
|
294
|
+
val_indices=val_idx,
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# --- Step 6: Per-split label stats ---
|
|
299
|
+
logger.info("Step 6: Per-split label statistics")
|
|
300
|
+
for fold_info in fold_infos:
|
|
301
|
+
train_labels = class_labels[fold_info.train_indices]
|
|
302
|
+
val_labels = class_labels[fold_info.val_indices]
|
|
303
|
+
fold_info.label_stats_train = _serialize_label_stats(label_stats(train_labels, index2label=index2label))
|
|
304
|
+
fold_info.label_stats_val = _serialize_label_stats(label_stats(val_labels, index2label=index2label))
|
|
305
|
+
|
|
306
|
+
test_labels = class_labels[test_indices] if test_indices else np.array([], dtype=np.intp)
|
|
307
|
+
label_stats_test = (
|
|
308
|
+
_serialize_label_stats(label_stats(test_labels, index2label=index2label)) if len(test_labels) > 0 else {}
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# --- Step 7: Per-split coverage (if model provided) ---
|
|
312
|
+
coverage_test_data = _run_coverage(
|
|
313
|
+
ds_ctx,
|
|
314
|
+
dataset,
|
|
315
|
+
fold_infos,
|
|
316
|
+
test_indices,
|
|
317
|
+
params,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# --- Build raw outputs ---
|
|
321
|
+
raw = DataSplittingRawOutputs(
|
|
322
|
+
dataset_size=dataset_size,
|
|
323
|
+
pre_split_balance=pre_split_balance,
|
|
324
|
+
pre_split_diversity=pre_split_diversity,
|
|
325
|
+
label_stats_full=label_stats_full,
|
|
326
|
+
test_indices=test_indices,
|
|
327
|
+
label_stats_test=label_stats_test,
|
|
328
|
+
coverage_test=coverage_test_data,
|
|
329
|
+
folds=fold_infos,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# --- Build findings ---
|
|
333
|
+
findings = build_findings(raw)
|
|
334
|
+
|
|
335
|
+
# --- Build split sizes for metadata ---
|
|
336
|
+
fold0 = fold_infos[0] if fold_infos else None
|
|
337
|
+
split_sizes = {
|
|
338
|
+
"train": len(fold0.train_indices) if fold0 else 0,
|
|
339
|
+
"val": len(fold0.val_indices) if fold0 else 0,
|
|
340
|
+
"test": len(test_indices),
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
report = DataSplittingReport(
|
|
344
|
+
summary=f"Dataset splitting: {dataset_size} items → {len(fold_infos)} fold(s)",
|
|
345
|
+
findings=findings,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
outputs = DataSplittingOutputs(raw=raw, report=report)
|
|
349
|
+
|
|
350
|
+
metadata = DataSplittingMetadata(
|
|
351
|
+
num_folds=params.num_folds,
|
|
352
|
+
stratified=params.stratify,
|
|
353
|
+
split_on=params.split_on,
|
|
354
|
+
rebalance_method=params.rebalance_method,
|
|
355
|
+
split_sizes=split_sizes,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
return WorkflowResult(
|
|
359
|
+
name=self.name,
|
|
360
|
+
success=True,
|
|
361
|
+
data=outputs,
|
|
362
|
+
metadata=metadata,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _empty_outputs() -> DataSplittingOutputs:
|
|
367
|
+
"""Create empty outputs for error cases."""
|
|
368
|
+
return DataSplittingOutputs(
|
|
369
|
+
raw=DataSplittingRawOutputs(dataset_size=0),
|
|
370
|
+
report=DataSplittingReport(summary="Splitting workflow failed"),
|
|
371
|
+
)
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dataeval-flow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DataEval Workflows container for data evaluation
|
|
5
|
+
Project-URL: Repository, https://gitlab.jatic.net/jatic/aria/dataeval-flow
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: <3.14,>=3.10
|
|
19
|
+
Requires-Dist: dataeval==1.0.4
|
|
20
|
+
Requires-Dist: datasets>=4.0.0
|
|
21
|
+
Requires-Dist: maite-datasets>=0.0.12
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Provides-Extra: all-cpu
|
|
25
|
+
Requires-Dist: onnx>=1.15; extra == 'all-cpu'
|
|
26
|
+
Requires-Dist: onnxruntime>=1.20; extra == 'all-cpu'
|
|
27
|
+
Requires-Dist: opencv-python-headless>=4.8.0; extra == 'all-cpu'
|
|
28
|
+
Requires-Dist: textual>=3.0; extra == 'all-cpu'
|
|
29
|
+
Requires-Dist: torch>=2.2.0; extra == 'all-cpu'
|
|
30
|
+
Requires-Dist: torchvision>=0.17.0; extra == 'all-cpu'
|
|
31
|
+
Provides-Extra: all-cu118
|
|
32
|
+
Requires-Dist: onnx>=1.15; extra == 'all-cu118'
|
|
33
|
+
Requires-Dist: onnxruntime-gpu>=1.20; extra == 'all-cu118'
|
|
34
|
+
Requires-Dist: opencv-python-headless>=4.8.0; extra == 'all-cu118'
|
|
35
|
+
Requires-Dist: textual>=3.0; extra == 'all-cu118'
|
|
36
|
+
Requires-Dist: torch>=2.2.0; extra == 'all-cu118'
|
|
37
|
+
Requires-Dist: torchvision>=0.17.0; extra == 'all-cu118'
|
|
38
|
+
Provides-Extra: all-cu124
|
|
39
|
+
Requires-Dist: onnx>=1.15; extra == 'all-cu124'
|
|
40
|
+
Requires-Dist: onnxruntime-gpu>=1.20; extra == 'all-cu124'
|
|
41
|
+
Requires-Dist: opencv-python-headless>=4.8.0; extra == 'all-cu124'
|
|
42
|
+
Requires-Dist: textual>=3.0; extra == 'all-cu124'
|
|
43
|
+
Requires-Dist: torch>=2.2.0; extra == 'all-cu124'
|
|
44
|
+
Requires-Dist: torchvision>=0.17.0; extra == 'all-cu124'
|
|
45
|
+
Provides-Extra: all-cu128
|
|
46
|
+
Requires-Dist: onnx>=1.15; extra == 'all-cu128'
|
|
47
|
+
Requires-Dist: onnxruntime-gpu>=1.23.2; extra == 'all-cu128'
|
|
48
|
+
Requires-Dist: opencv-python-headless>=4.8.0; extra == 'all-cu128'
|
|
49
|
+
Requires-Dist: textual>=3.0; extra == 'all-cu128'
|
|
50
|
+
Requires-Dist: torch>=2.2.0; extra == 'all-cu128'
|
|
51
|
+
Requires-Dist: torchvision>=0.17.0; extra == 'all-cu128'
|
|
52
|
+
Provides-Extra: app
|
|
53
|
+
Requires-Dist: textual>=3.0; extra == 'app'
|
|
54
|
+
Provides-Extra: cpu
|
|
55
|
+
Requires-Dist: torch>=2.2.0; extra == 'cpu'
|
|
56
|
+
Requires-Dist: torchvision>=0.17.0; extra == 'cpu'
|
|
57
|
+
Provides-Extra: cu118
|
|
58
|
+
Requires-Dist: torch>=2.2.0; extra == 'cu118'
|
|
59
|
+
Requires-Dist: torchvision>=0.17.0; extra == 'cu118'
|
|
60
|
+
Provides-Extra: cu124
|
|
61
|
+
Requires-Dist: torch>=2.2.0; extra == 'cu124'
|
|
62
|
+
Requires-Dist: torchvision>=0.17.0; extra == 'cu124'
|
|
63
|
+
Provides-Extra: cu128
|
|
64
|
+
Requires-Dist: torch>=2.2.0; extra == 'cu128'
|
|
65
|
+
Requires-Dist: torchvision>=0.17.0; extra == 'cu128'
|
|
66
|
+
Provides-Extra: onnx
|
|
67
|
+
Requires-Dist: onnx>=1.15; extra == 'onnx'
|
|
68
|
+
Requires-Dist: onnxruntime>=1.20; extra == 'onnx'
|
|
69
|
+
Provides-Extra: onnx-gpu
|
|
70
|
+
Requires-Dist: onnx>=1.15; extra == 'onnx-gpu'
|
|
71
|
+
Requires-Dist: onnxruntime-gpu>=1.20; extra == 'onnx-gpu'
|
|
72
|
+
Provides-Extra: opencv
|
|
73
|
+
Requires-Dist: opencv-python-headless>=4.8.0; extra == 'opencv'
|
|
74
|
+
Description-Content-Type: text/markdown
|
|
75
|
+
|
|
76
|
+
# DataEval Workflows
|
|
77
|
+
|
|
78
|
+
Workflow orchestration for DataEval with GPU support.
|
|
79
|
+
|
|
80
|
+
## Quick Start
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 1. Build CUDA 11.8 container
|
|
84
|
+
docker build -f docker/Dockerfile.cu118 -t dataeval:cu118 .
|
|
85
|
+
|
|
86
|
+
# 2. Show help
|
|
87
|
+
docker run dataeval:cu118
|
|
88
|
+
|
|
89
|
+
# 3. Run with data and output
|
|
90
|
+
docker run --gpus all \
|
|
91
|
+
--mount type=bind,source=/path/to/data,target=/dataeval,readonly \
|
|
92
|
+
--mount type=bind,source=/path/to/output,target=/output \
|
|
93
|
+
dataeval:cu118
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Requirements
|
|
97
|
+
|
|
98
|
+
| Requirement | Version |
|
|
99
|
+
|-------------|---------|
|
|
100
|
+
| Docker | >= 20.10 |
|
|
101
|
+
| NVIDIA GPU | Any (for GPU mode) |
|
|
102
|
+
| NVIDIA Driver | >= 520 (for GPU mode) |
|
|
103
|
+
| CUDA | 11.8.0 (for GPU mode) |
|
|
104
|
+
|
|
105
|
+
### Verify GPU Access
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
docker run --rm --gpus all nvidia/cuda:11.8.0-base-ubuntu22.04 nvidia-smi
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Volume Mounts
|
|
112
|
+
|
|
113
|
+
| Path | Mode | Purpose |
|
|
114
|
+
|------|------|---------|
|
|
115
|
+
| `/dataeval` | ro | Data directory — datasets, models, configs (required) |
|
|
116
|
+
| `/output` | rw | Results (required) |
|
|
117
|
+
| `/cache` | rw | Computation cache (optional) |
|
|
118
|
+
|
|
119
|
+
### File Permissions
|
|
120
|
+
|
|
121
|
+
The container runs as a non-root user (`dataeval`, UID 1000). Mounted directories for `/output` and `/cache` must be writable by the container process. There are two approaches:
|
|
122
|
+
|
|
123
|
+
#### Option 1: Pass your host UID (recommended)
|
|
124
|
+
|
|
125
|
+
Use `--user` to run the container as your host user, so mounted directories are naturally writable:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
docker run --gpus all \
|
|
129
|
+
--user "$(id -u):$(id -g)" \
|
|
130
|
+
--mount type=bind,source=/path/to/data,target=/dataeval,readonly \
|
|
131
|
+
--mount type=bind,source=/path/to/output,target=/output \
|
|
132
|
+
dataeval:cu118
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Option 2: Open directory permissions
|
|
136
|
+
|
|
137
|
+
Make the output and cache directories world-writable on the host:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
chmod 777 /path/to/output /path/to/cache
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Then run without `--user`. This is simpler but less secure.
|
|
144
|
+
|
|
145
|
+
### Custom Data Root
|
|
146
|
+
|
|
147
|
+
The data root path can be overridden via the `DATAEVAL_DATA` environment variable:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
docker run --gpus all \
|
|
151
|
+
-e DATAEVAL_DATA=/data \
|
|
152
|
+
--mount type=bind,source=/path/to/data,target=/data,readonly \
|
|
153
|
+
--mount type=bind,source=/path/to/output,target=/output \
|
|
154
|
+
dataeval:cu118
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
Config files (YAML or JSON) can be placed anywhere in your data directory. By default, all YAML/JSON files at the root of the data mount are auto-discovered and merged.
|
|
160
|
+
|
|
161
|
+
To specify a config path explicitly:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Config folder within data directory
|
|
165
|
+
docker run --gpus all \
|
|
166
|
+
--mount type=bind,source=/path/to/data,target=/dataeval,readonly \
|
|
167
|
+
--mount type=bind,source=/path/to/output,target=/output \
|
|
168
|
+
dataeval:cu118 --config config/
|
|
169
|
+
|
|
170
|
+
# Single config file
|
|
171
|
+
docker run --gpus all \
|
|
172
|
+
--mount type=bind,source=/path/to/data,target=/dataeval,readonly \
|
|
173
|
+
--mount type=bind,source=/path/to/output,target=/output \
|
|
174
|
+
dataeval:cu118 --config params.yaml
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Dataset and model paths in config files are resolved relative to the data root (`/dataeval` by default).
|
|
178
|
+
|
|
179
|
+
## Dataset Formats
|
|
180
|
+
|
|
181
|
+
Currently supported dataset structures:
|
|
182
|
+
|
|
183
|
+
| Format | Structure | Example |
|
|
184
|
+
|--------|-----------|---------|
|
|
185
|
+
| **Dataset** | Single split, used directly | `cifar10_test/` |
|
|
186
|
+
| **DatasetDict** | Multiple splits (dict), configured via config YAML | `cifar10_full/` |
|
|
187
|
+
|
|
188
|
+
## CPU Fallback
|
|
189
|
+
|
|
190
|
+
For machines without NVIDIA GPU:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
docker build -f docker/Dockerfile.cpu -t dataeval:cpu .
|
|
194
|
+
docker run dataeval:cpu # Shows help
|
|
195
|
+
docker run \
|
|
196
|
+
--mount type=bind,source=/path/to/data,target=/dataeval,readonly \
|
|
197
|
+
--mount type=bind,source=/path/to/output,target=/output \
|
|
198
|
+
dataeval:cpu
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## CLI Modes
|
|
202
|
+
|
|
203
|
+
DataEval Flow has three modes:
|
|
204
|
+
|
|
205
|
+
| Command | Purpose |
|
|
206
|
+
| ---------------------- | ---------------------------------------------------------------- |
|
|
207
|
+
| `dataeval-flow [opts]` | Headless execution — for automation and CI/CD pipelines |
|
|
208
|
+
| `dataeval-flow app` | Interactive TUI dashboard — configure, execute, and view results |
|
|
209
|
+
| `dataeval-flow config` | Simple CLI config builder — create/edit configs without the TUI |
|
|
210
|
+
|
|
211
|
+
### Interactive TUI (`app`)
|
|
212
|
+
|
|
213
|
+
**Installation:**
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
uv sync --extra app # or: pip install dataeval-flow[app]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Usage:**
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
# Launch with a blank config
|
|
223
|
+
python -m dataeval_flow app
|
|
224
|
+
|
|
225
|
+
# Load an existing config for editing
|
|
226
|
+
python -m dataeval_flow app --config /path/to/params.yaml
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The TUI provides a three-pane dashboard for config editing, task execution, and result viewing. It auto-discovers available torchvision transforms, dataeval selection classes, and workflow types, generating dynamic parameter forms from their schemas.
|
|
230
|
+
|
|
231
|
+
### Simple CLI Config Builder (`config`)
|
|
232
|
+
|
|
233
|
+
For environments without the TUI dependency:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
python -m dataeval_flow config
|
|
237
|
+
python -m dataeval_flow config --config /path/to/params.yaml
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Configs can be saved as YAML or JSON.
|
|
241
|
+
|
|
242
|
+
## Dependencies
|
|
243
|
+
|
|
244
|
+
- `dataeval` - Core evaluation library
|
|
245
|
+
- `datasets` - Huggingface library
|
|
246
|
+
- `maite-datasets` - MAITE protocol adapter
|
|
247
|
+
- `maite` - MAITE protocol library
|
|
248
|
+
- `pydantic` - Structural typing and schema validation
|
|
249
|
+
|
|
250
|
+
## Troubleshooting
|
|
251
|
+
|
|
252
|
+
### Build appears stuck at `uv sync`
|
|
253
|
+
|
|
254
|
+
The Docker build may appear frozen during the `uv sync` step:
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
=> [builder 7/7] RUN uv sync --frozen --no-dev --no-install-project 1139.3s
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**This is normal.** The step downloads ~2GB of dependencies (PyTorch, scipy, etc.) with no progress indicator.
|
|
261
|
+
|
|
262
|
+
| Network Speed | Expected Build Time |
|
|
263
|
+
|---------------|---------------------|
|
|
264
|
+
| 100 Mbps | ~10 minutes |
|
|
265
|
+
| 30 Mbps | ~20 minutes |
|
|
266
|
+
| 10 Mbps | ~45 minutes |
|
|
267
|
+
|
|
268
|
+
**Tip:** First build is slow; subsequent builds use Docker cache and complete in seconds.
|
|
269
|
+
|
|
270
|
+
## Running Without Container
|
|
271
|
+
|
|
272
|
+
The `dataeval_flow` package can be used standalone without Docker.
|
|
273
|
+
|
|
274
|
+
**Installation:**
|
|
275
|
+
```bash
|
|
276
|
+
git clone https://gitlab.jatic.net/jatic/aria/dataeval-flow.git
|
|
277
|
+
cd dataeval-flow
|
|
278
|
+
uv sync
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**CLI Usage:**
|
|
282
|
+
```bash
|
|
283
|
+
python -m dataeval_flow --config /path/to/config --output /path/to/output
|
|
284
|
+
python -m dataeval_flow --data /path/to/data --output /path/to/output
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Python API Usage:**
|
|
288
|
+
```python
|
|
289
|
+
from pathlib import Path
|
|
290
|
+
from dataeval_flow import load_config, run_tasks
|
|
291
|
+
|
|
292
|
+
config = load_config(Path("/path/to/data/config.yaml"))
|
|
293
|
+
results = run_tasks(config, data_dir=Path("/path/to/data"))
|
|
294
|
+
print(results[0].report())
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Development:**
|
|
298
|
+
```bash
|
|
299
|
+
uv sync --group dev
|
|
300
|
+
nox
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
MIT
|