datapruning 2.0.0__tar.gz → 2.1.0__tar.gz
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.
- {datapruning-2.0.0/datapruning.egg-info → datapruning-2.1.0}/PKG-INFO +4 -1
- datapruning-2.1.0/datapruning/multimodal/__init__.py +1 -0
- datapruning-2.1.0/datapruning/multimodal/clip_encoder.py +179 -0
- datapruning-2.1.0/datapruning/sdk.py +289 -0
- {datapruning-2.0.0 → datapruning-2.1.0/datapruning.egg-info}/PKG-INFO +4 -1
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning.egg-info/SOURCES.txt +2 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning.egg-info/requires.txt +4 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/pyproject.toml +7 -1
- datapruning-2.0.0/datapruning/sdk.py +0 -80
- {datapruning-2.0.0 → datapruning-2.1.0}/LICENSE +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/MANIFEST.in +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/README.md +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/build_src/_core.c +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/__init__.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/__init__.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/base.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/el2n_scorer.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/fusion_pipeline.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/kcenter_select.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/margin_scorer.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/registry.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/sliding_window.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/__init__.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/benchmark.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/explainability.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/intelligence.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/optimizer.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/scanner.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/strategy_display.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/strategy_selector.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/pipeline.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/reports/__init__.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/reports/exporter.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/__init__.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/benchmark/__init__.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/benchmark/compare.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/benchmark/evaluation.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/benchmark/synthetic.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/difficulty.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/embeddings/__init__.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/embeddings/torch_utils.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/fusion.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/noise.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/normalize.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/pipeline.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/redundancy.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/scores/__init__.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/scores/aum.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/scores/el2n.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/scores/forgetting.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/scores/grand.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/scores/margin.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/selection.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/selectors/__init__.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/selectors/facility_location.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/selectors/kcenter.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/selectors/out_of_fold.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/strategy.py +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning.egg-info/dependency_links.txt +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/datapruning.egg-info/top_level.txt +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/setup.cfg +0 -0
- {datapruning-2.0.0 → datapruning-2.1.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datapruning
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Intelligent data pruning for ML datasets
|
|
5
5
|
License: Proprietary
|
|
6
6
|
Project-URL: Homepage, https://www.datapruning.com
|
|
@@ -21,6 +21,9 @@ Requires-Dist: numpy>=1.24.0
|
|
|
21
21
|
Requires-Dist: pandas>=2.0.0
|
|
22
22
|
Requires-Dist: scikit-learn>=1.3.0
|
|
23
23
|
Requires-Dist: torch>=2.0.0
|
|
24
|
+
Provides-Extra: clip
|
|
25
|
+
Requires-Dist: transformers>=4.30.0; extra == "clip"
|
|
26
|
+
Requires-Dist: pillow>=9.0.0; extra == "clip"
|
|
24
27
|
Dynamic: license-file
|
|
25
28
|
|
|
26
29
|
# DataPruning
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Multimodal pruning support — encode images and text with CLIP, then prune."""
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""CLIP-based encoder for images and text.
|
|
2
|
+
|
|
3
|
+
Turns images and text into feature vectors that the pruning algorithms
|
|
4
|
+
can work with. Uses HuggingFace transformers for CLIP.
|
|
5
|
+
|
|
6
|
+
Requirements:
|
|
7
|
+
pip install datapruning[clip]
|
|
8
|
+
# or manually: pip install transformers pillow
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
CLIP_MODEL = "openai/clip-vit-base-patch32"
|
|
23
|
+
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"}
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import torch
|
|
27
|
+
from transformers import CLIPModel, CLIPTokenizer, CLIPProcessor
|
|
28
|
+
|
|
29
|
+
HAS_CLIP = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
HAS_CLIP = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _require_clip():
|
|
35
|
+
if not HAS_CLIP:
|
|
36
|
+
raise ImportError(
|
|
37
|
+
"CLIP dependencies not installed. "
|
|
38
|
+
"Install with: pip install datapruning[clip] "
|
|
39
|
+
"or manually: pip install transformers pillow"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _to_tensor(x):
|
|
44
|
+
"""Extract a torch.Tensor from various CLIP output types."""
|
|
45
|
+
if isinstance(x, torch.Tensor):
|
|
46
|
+
return x
|
|
47
|
+
if hasattr(x, "pooler_output") and x.pooler_output is not None:
|
|
48
|
+
return x.pooler_output
|
|
49
|
+
if hasattr(x, "last_hidden_state"):
|
|
50
|
+
return x.last_hidden_state[:, 0, :]
|
|
51
|
+
if hasattr(x, "text_embeds"):
|
|
52
|
+
return x.text_embeds
|
|
53
|
+
if hasattr(x, "image_embeds"):
|
|
54
|
+
return x.image_embeds
|
|
55
|
+
raise TypeError(f"Cannot extract tensor from {type(x).__name__}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CLIPEncoder:
|
|
59
|
+
"""Encode images and text into CLIP feature vectors."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, model_name: str = CLIP_MODEL, device: Optional[str] = None):
|
|
62
|
+
_require_clip()
|
|
63
|
+
self.model_name = model_name
|
|
64
|
+
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
|
65
|
+
self._model = None
|
|
66
|
+
self._processor = None
|
|
67
|
+
self._tokenizer = None
|
|
68
|
+
|
|
69
|
+
def _load(self):
|
|
70
|
+
if self._model is not None:
|
|
71
|
+
return
|
|
72
|
+
logger.info("Loading CLIP model: %s (device=%s)", self.model_name, self.device)
|
|
73
|
+
self._model = CLIPModel.from_pretrained(self.model_name).to(self.device)
|
|
74
|
+
self._model.eval()
|
|
75
|
+
self._processor = CLIPProcessor.from_pretrained(self.model_name)
|
|
76
|
+
self._tokenizer = CLIPTokenizer.from_pretrained(self.model_name)
|
|
77
|
+
logger.info("CLIP model loaded successfully")
|
|
78
|
+
|
|
79
|
+
def encode_images(
|
|
80
|
+
self, image_paths: list[str], batch_size: int = 32
|
|
81
|
+
) -> np.ndarray:
|
|
82
|
+
"""Encode a list of image file paths into CLIP embeddings.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
image_paths : list of str — paths to image files.
|
|
87
|
+
batch_size : int — encoding batch size.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
embeddings : (N, 512) float32 array — CLIP image features.
|
|
92
|
+
"""
|
|
93
|
+
_require_clip()
|
|
94
|
+
self._load()
|
|
95
|
+
|
|
96
|
+
from PIL import Image
|
|
97
|
+
|
|
98
|
+
all_embeddings = []
|
|
99
|
+
for i in range(0, len(image_paths), batch_size):
|
|
100
|
+
batch_paths = image_paths[i : i + batch_size]
|
|
101
|
+
images = []
|
|
102
|
+
for p in batch_paths:
|
|
103
|
+
try:
|
|
104
|
+
img = Image.open(p).convert("RGB")
|
|
105
|
+
images.append(img)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.warning("Failed to load %s: %s", p, e)
|
|
108
|
+
images.append(Image.new("RGB", (224, 224), (0, 0, 0)))
|
|
109
|
+
|
|
110
|
+
inputs = self._processor(images=images, return_tensors="pt", padding=True)
|
|
111
|
+
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
|
112
|
+
|
|
113
|
+
with torch.no_grad():
|
|
114
|
+
features = self._model.get_image_features(**inputs)
|
|
115
|
+
features = _to_tensor(features)
|
|
116
|
+
features = features / features.norm(dim=-1, keepdim=True)
|
|
117
|
+
|
|
118
|
+
all_embeddings.append(features.cpu().numpy().astype(np.float32))
|
|
119
|
+
|
|
120
|
+
return np.concatenate(all_embeddings, axis=0)
|
|
121
|
+
|
|
122
|
+
def encode_texts(
|
|
123
|
+
self, texts: list[str], batch_size: int = 64
|
|
124
|
+
) -> np.ndarray:
|
|
125
|
+
"""Encode a list of text strings into CLIP embeddings.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
texts : list of str — text strings to encode.
|
|
130
|
+
batch_size : int — encoding batch size.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
embeddings : (N, 512) float32 array — CLIP text features.
|
|
135
|
+
"""
|
|
136
|
+
_require_clip()
|
|
137
|
+
self._load()
|
|
138
|
+
|
|
139
|
+
all_embeddings = []
|
|
140
|
+
for i in range(0, len(texts), batch_size):
|
|
141
|
+
batch = texts[i : i + batch_size]
|
|
142
|
+
inputs = self._tokenizer(
|
|
143
|
+
batch, padding=True, truncation=True, return_tensors="pt"
|
|
144
|
+
)
|
|
145
|
+
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
|
146
|
+
|
|
147
|
+
with torch.no_grad():
|
|
148
|
+
features = self._model.get_text_features(**inputs)
|
|
149
|
+
features = _to_tensor(features)
|
|
150
|
+
features = features / features.norm(dim=-1, keepdim=True)
|
|
151
|
+
|
|
152
|
+
all_embeddings.append(features.cpu().numpy().astype(np.float32))
|
|
153
|
+
|
|
154
|
+
return np.concatenate(all_embeddings, axis=0)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def scan_images(folder_path: str) -> list[str]:
|
|
158
|
+
"""Recursively find all image files in a folder.
|
|
159
|
+
|
|
160
|
+
Returns sorted list of absolute paths.
|
|
161
|
+
"""
|
|
162
|
+
folder = Path(folder_path)
|
|
163
|
+
if not folder.exists():
|
|
164
|
+
raise FileNotFoundError(f"Folder not found: {folder_path}")
|
|
165
|
+
if not folder.is_dir():
|
|
166
|
+
raise NotADirectoryError(f"Not a directory: {folder_path}")
|
|
167
|
+
|
|
168
|
+
paths = []
|
|
169
|
+
for ext in IMAGE_EXTENSIONS:
|
|
170
|
+
paths.extend(folder.rglob(f"*{ext}"))
|
|
171
|
+
paths.extend(folder.rglob(f"*{ext.upper()}"))
|
|
172
|
+
|
|
173
|
+
paths = sorted(set(str(p) for p in paths))
|
|
174
|
+
if not paths:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"No image files found in {folder_path}. "
|
|
177
|
+
f"Supported formats: {', '.join(sorted(IMAGE_EXTENSIONS))}"
|
|
178
|
+
)
|
|
179
|
+
return paths
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from datapruning.engine.scanner import scan
|
|
8
|
+
from datapruning.engine.intelligence import analyze_intelligence
|
|
9
|
+
from datapruning.engine.strategy_selector import select_strategy, get_algorithm, list_available_algorithms
|
|
10
|
+
from datapruning.engine.optimizer import run_optimization
|
|
11
|
+
from datapruning.engine.explainability import build_report
|
|
12
|
+
from datapruning.engine.benchmark import benchmark_algorithms, compare_to_full
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DatasetEngine:
|
|
18
|
+
def __init__(self, df: pd.DataFrame, target_col: str = None):
|
|
19
|
+
self.df = df
|
|
20
|
+
self.target_col = target_col
|
|
21
|
+
self._analysis = None
|
|
22
|
+
self._last_result = None
|
|
23
|
+
self._last_optimizer = None
|
|
24
|
+
self._embeddings = None
|
|
25
|
+
self._input_mode = "tabular"
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_images(
|
|
29
|
+
cls,
|
|
30
|
+
folder_path: str,
|
|
31
|
+
labels: list[int],
|
|
32
|
+
keep_ratio: float = 0.5,
|
|
33
|
+
batch_size: int = 32,
|
|
34
|
+
) -> "DatasetEngine":
|
|
35
|
+
"""Create a DatasetEngine from a folder of images.
|
|
36
|
+
|
|
37
|
+
Encodes all images with CLIP into feature vectors, then wraps
|
|
38
|
+
them in a DataFrame so the existing pruning algorithms work
|
|
39
|
+
unchanged.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
folder_path : str — path to folder containing images.
|
|
44
|
+
labels : list of int — integer label for each image (same order).
|
|
45
|
+
keep_ratio : float — default keep ratio for optimize().
|
|
46
|
+
batch_size : int — CLIP encoding batch size.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
DatasetEngine — ready to call analyze(), recommend(), optimize().
|
|
51
|
+
"""
|
|
52
|
+
from datapruning.multimodal.clip_encoder import CLIPEncoder, scan_images
|
|
53
|
+
|
|
54
|
+
image_paths = scan_images(folder_path)
|
|
55
|
+
if len(image_paths) != len(labels):
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Number of images ({len(image_paths)}) != "
|
|
58
|
+
f"number of labels ({len(labels)})"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
encoder = CLIPEncoder()
|
|
62
|
+
embeddings = encoder.encode_images(image_paths, batch_size=batch_size)
|
|
63
|
+
|
|
64
|
+
feat_cols = [f"clip_{i}" for i in range(embeddings.shape[1])]
|
|
65
|
+
df = pd.DataFrame(embeddings, columns=feat_cols)
|
|
66
|
+
df["label"] = labels
|
|
67
|
+
df["filepath"] = image_paths
|
|
68
|
+
|
|
69
|
+
engine = cls(df, target_col="label")
|
|
70
|
+
engine._embeddings = embeddings
|
|
71
|
+
engine._input_mode = "images"
|
|
72
|
+
return engine
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_texts(
|
|
76
|
+
cls,
|
|
77
|
+
texts: list[str],
|
|
78
|
+
labels: list[int],
|
|
79
|
+
batch_size: int = 64,
|
|
80
|
+
) -> "DatasetEngine":
|
|
81
|
+
"""Create a DatasetEngine from a list of text strings.
|
|
82
|
+
|
|
83
|
+
Encodes all texts with CLIP into feature vectors.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
texts : list of str — text strings to encode.
|
|
88
|
+
labels : list of int — integer label for each text.
|
|
89
|
+
batch_size : int — CLIP encoding batch size.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
DatasetEngine — ready to call analyze(), recommend(), optimize().
|
|
94
|
+
"""
|
|
95
|
+
from datapruning.multimodal.clip_encoder import CLIPEncoder
|
|
96
|
+
|
|
97
|
+
if len(texts) != len(labels):
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Number of texts ({len(texts)}) != "
|
|
100
|
+
f"number of labels ({len(labels)})"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
encoder = CLIPEncoder()
|
|
104
|
+
embeddings = encoder.encode_texts(texts, batch_size=batch_size)
|
|
105
|
+
|
|
106
|
+
feat_cols = [f"clip_{i}" for i in range(embeddings.shape[1])]
|
|
107
|
+
df = pd.DataFrame(embeddings, columns=feat_cols)
|
|
108
|
+
df["label"] = labels
|
|
109
|
+
df["text"] = texts
|
|
110
|
+
|
|
111
|
+
engine = cls(df, target_col="label")
|
|
112
|
+
engine._embeddings = embeddings
|
|
113
|
+
engine._input_mode = "texts"
|
|
114
|
+
return engine
|
|
115
|
+
|
|
116
|
+
def _build_multimodal_analysis(self) -> dict:
|
|
117
|
+
"""Build analysis dict from CLIP embeddings for strategy selection."""
|
|
118
|
+
X = self._embeddings
|
|
119
|
+
y = self.df[self.target_col].values
|
|
120
|
+
n_samples, n_features = X.shape
|
|
121
|
+
classes, counts = np.unique(y, return_counts=True)
|
|
122
|
+
imbalance = float(counts.max() / max(counts.min(), 1))
|
|
123
|
+
|
|
124
|
+
scan_result = {
|
|
125
|
+
"rows": n_samples,
|
|
126
|
+
"columns": n_features,
|
|
127
|
+
"memory_mb": round(X.nbytes / 1024 / 1024, 2),
|
|
128
|
+
"missing_pct": 0.0,
|
|
129
|
+
"duplicate_pct": round(
|
|
130
|
+
1.0 - len(np.unique(X, axis=0)) / n_samples, 4
|
|
131
|
+
),
|
|
132
|
+
"numeric_pct": 100.0,
|
|
133
|
+
"categorical_pct": 0.0,
|
|
134
|
+
"estimated_training_cost": "medium",
|
|
135
|
+
"task_type": "binary" if len(classes) == 2 else "multiclass",
|
|
136
|
+
"target_balance": classes.tolist(),
|
|
137
|
+
"imbalance_ratio": imbalance,
|
|
138
|
+
"input_mode": self._input_mode,
|
|
139
|
+
"embedding_dim": n_features,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
intelligence_result = {
|
|
143
|
+
"id_columns": [],
|
|
144
|
+
"date_columns": [],
|
|
145
|
+
"constant_columns": [],
|
|
146
|
+
"near_constant_columns": [],
|
|
147
|
+
"high_cardinality_columns": [],
|
|
148
|
+
"highly_correlated_pairs": [],
|
|
149
|
+
"outlier_summary": [],
|
|
150
|
+
"complexity_score": min(100, int(n_features * 0.1 + n_samples * 0.001)),
|
|
151
|
+
"leakage_risk_columns": [],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {"scan": scan_result, "intelligence": intelligence_result}
|
|
155
|
+
|
|
156
|
+
def analyze(self) -> dict:
|
|
157
|
+
if self._embeddings is not None:
|
|
158
|
+
self._analysis = self._build_multimodal_analysis()
|
|
159
|
+
else:
|
|
160
|
+
scan_result = scan(self.df, target_col=self.target_col)
|
|
161
|
+
intelligence_result = analyze_intelligence(self.df, target_col=self.target_col)
|
|
162
|
+
self._analysis = {"scan": scan_result, "intelligence": intelligence_result}
|
|
163
|
+
return self._analysis
|
|
164
|
+
|
|
165
|
+
def report(self, as_json: bool = False):
|
|
166
|
+
if self._analysis is None:
|
|
167
|
+
self.analyze()
|
|
168
|
+
if as_json:
|
|
169
|
+
return json.dumps(self._analysis, indent=2, default=str)
|
|
170
|
+
s, i = self._analysis["scan"], self._analysis["intelligence"]
|
|
171
|
+
print(f"Rows: {s['rows']:,} | Columns: {s['columns']} | Memory: {s['memory_mb']} MB")
|
|
172
|
+
print(f"Missing: {s['missing_pct']}% | Duplicates: {s['duplicate_pct']}%")
|
|
173
|
+
if "task_type" in s:
|
|
174
|
+
print(f"Task type: {s['task_type']}")
|
|
175
|
+
if "imbalance_ratio" in s:
|
|
176
|
+
print(f"Imbalance ratio: {s['imbalance_ratio']}:1")
|
|
177
|
+
if i["leakage_risk_columns"]:
|
|
178
|
+
print(f"Possible leakage columns: {i['leakage_risk_columns']}")
|
|
179
|
+
print(f"Complexity score: {i['complexity_score']}/100")
|
|
180
|
+
return self._analysis
|
|
181
|
+
|
|
182
|
+
def recommend(self):
|
|
183
|
+
if self._analysis is None:
|
|
184
|
+
self.analyze()
|
|
185
|
+
rec = select_strategy(self._analysis)
|
|
186
|
+
if self._embeddings is not None:
|
|
187
|
+
rec._input_mode = self._input_mode
|
|
188
|
+
rec._embedding_dim = self._embeddings.shape[1]
|
|
189
|
+
return rec
|
|
190
|
+
|
|
191
|
+
def available_algorithms(self) -> list[dict]:
|
|
192
|
+
return list_available_algorithms()
|
|
193
|
+
|
|
194
|
+
def optimize(self, algorithm_key: str = None, keep_ratio: float = 0.5) -> dict:
|
|
195
|
+
if self.target_col is None:
|
|
196
|
+
raise ValueError("target_col is required to optimize.")
|
|
197
|
+
|
|
198
|
+
if self._embeddings is not None:
|
|
199
|
+
return self._optimize_multimodal(algorithm_key, keep_ratio)
|
|
200
|
+
|
|
201
|
+
if algorithm_key is None:
|
|
202
|
+
rec = self.recommend()
|
|
203
|
+
optimizer = rec.algorithm_instance
|
|
204
|
+
else:
|
|
205
|
+
optimizer = get_algorithm(algorithm_key)
|
|
206
|
+
result = run_optimization(self.df, self.target_col, optimizer, keep_ratio=keep_ratio)
|
|
207
|
+
report = build_report(optimizer, result, self.df)
|
|
208
|
+
self._last_result = result
|
|
209
|
+
self._last_optimizer = optimizer
|
|
210
|
+
return {
|
|
211
|
+
"optimized_df": result.optimized_df,
|
|
212
|
+
"explainability": report["explanation"],
|
|
213
|
+
"dataset_score": report["dataset_score"],
|
|
214
|
+
"runtime_seconds": report["runtime_seconds"],
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
def _optimize_multimodal(self, algorithm_key: str = None, keep_ratio: float = 0.5) -> dict:
|
|
218
|
+
"""Optimize using CLIP embeddings directly."""
|
|
219
|
+
import time
|
|
220
|
+
from datapruning.research_pruning.selectors.kcenter import kcenter_greedy
|
|
221
|
+
from datapruning.research_pruning.pipeline import ResearchFusionEngine
|
|
222
|
+
|
|
223
|
+
X = self._embeddings.astype(np.float64)
|
|
224
|
+
y = self.df[self.target_col].values.astype(np.int64)
|
|
225
|
+
|
|
226
|
+
t0 = time.time()
|
|
227
|
+
|
|
228
|
+
if algorithm_key is None:
|
|
229
|
+
rec = self.recommend()
|
|
230
|
+
algorithm_key = rec.algorithm_key
|
|
231
|
+
|
|
232
|
+
n_keep = max(1, int(len(X) * keep_ratio))
|
|
233
|
+
|
|
234
|
+
if algorithm_key == "kcenter":
|
|
235
|
+
selected = kcenter_greedy(X, budget=n_keep)
|
|
236
|
+
elif algorithm_key == "fusion":
|
|
237
|
+
engine = ResearchFusionEngine({"keep_ratio": keep_ratio})
|
|
238
|
+
_, _, report = engine.run(X, y, keep_ratio=keep_ratio, auto_strategy=True)
|
|
239
|
+
selected = np.array(report.get("selected_indices", list(range(n_keep))))
|
|
240
|
+
else:
|
|
241
|
+
from datapruning.research_pruning.scores.el2n import el2n_score
|
|
242
|
+
from datapruning.research_pruning.scores.margin import margin_score
|
|
243
|
+
|
|
244
|
+
scores = np.zeros(len(X))
|
|
245
|
+
try:
|
|
246
|
+
e_scores = el2n_score(X, y)
|
|
247
|
+
scores += e_scores
|
|
248
|
+
except Exception:
|
|
249
|
+
scores += np.linalg.norm(X, axis=1)
|
|
250
|
+
try:
|
|
251
|
+
m_scores = margin_score(X, y)
|
|
252
|
+
scores += m_scores
|
|
253
|
+
except Exception:
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
selected = np.argsort(scores)[::-1][:n_keep]
|
|
257
|
+
|
|
258
|
+
runtime = time.time() - t0
|
|
259
|
+
pruned_df = self.df.iloc[selected].reset_index(drop=True)
|
|
260
|
+
|
|
261
|
+
self._last_result = type("Result", (), {"optimized_df": pruned_df})()
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
"optimized_df": pruned_df,
|
|
265
|
+
"explainability": {
|
|
266
|
+
"algorithm": algorithm_key,
|
|
267
|
+
"input_mode": self._input_mode,
|
|
268
|
+
"embedding_dim": self._embeddings.shape[1],
|
|
269
|
+
"rows_removed": len(self.df) - len(pruned_df),
|
|
270
|
+
"reduction_pct": round((1 - len(pruned_df) / len(self.df)) * 100, 1),
|
|
271
|
+
},
|
|
272
|
+
"dataset_score": {
|
|
273
|
+
"overall_health": 100.0,
|
|
274
|
+
"input_mode": self._input_mode,
|
|
275
|
+
},
|
|
276
|
+
"runtime_seconds": round(runtime, 4),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
def export(self, path: str, result: dict = None):
|
|
280
|
+
result = result or {"optimized_df": self._last_result.optimized_df}
|
|
281
|
+
result["optimized_df"].to_csv(path, index=False)
|
|
282
|
+
print(f"Exported optimized dataset -> {path} ({len(result['optimized_df']):,} rows)")
|
|
283
|
+
return path
|
|
284
|
+
|
|
285
|
+
def benchmark(self, algorithms: list[str] = None, keep_ratio: float = 0.5) -> dict:
|
|
286
|
+
return benchmark_algorithms(self.df, self.target_col, algorithms=algorithms, keep_ratio=keep_ratio)
|
|
287
|
+
|
|
288
|
+
def compare(self, optimized_df: pd.DataFrame) -> dict:
|
|
289
|
+
return compare_to_full(optimized_df, self.df, self.target_col)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datapruning
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Intelligent data pruning for ML datasets
|
|
5
5
|
License: Proprietary
|
|
6
6
|
Project-URL: Homepage, https://www.datapruning.com
|
|
@@ -21,6 +21,9 @@ Requires-Dist: numpy>=1.24.0
|
|
|
21
21
|
Requires-Dist: pandas>=2.0.0
|
|
22
22
|
Requires-Dist: scikit-learn>=1.3.0
|
|
23
23
|
Requires-Dist: torch>=2.0.0
|
|
24
|
+
Provides-Extra: clip
|
|
25
|
+
Requires-Dist: transformers>=4.30.0; extra == "clip"
|
|
26
|
+
Requires-Dist: pillow>=9.0.0; extra == "clip"
|
|
24
27
|
Dynamic: license-file
|
|
25
28
|
|
|
26
29
|
# DataPruning
|
|
@@ -28,6 +28,8 @@ datapruning/engine/optimizer.py
|
|
|
28
28
|
datapruning/engine/scanner.py
|
|
29
29
|
datapruning/engine/strategy_display.py
|
|
30
30
|
datapruning/engine/strategy_selector.py
|
|
31
|
+
datapruning/multimodal/__init__.py
|
|
32
|
+
datapruning/multimodal/clip_encoder.py
|
|
31
33
|
datapruning/reports/__init__.py
|
|
32
34
|
datapruning/reports/exporter.py
|
|
33
35
|
datapruning/research_pruning/__init__.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "datapruning"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.1.0"
|
|
8
8
|
description = "Intelligent data pruning for ML datasets"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -28,6 +28,12 @@ dependencies = [
|
|
|
28
28
|
"torch>=2.0.0",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
clip = [
|
|
33
|
+
"transformers>=4.30.0",
|
|
34
|
+
"pillow>=9.0.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
31
37
|
[project.urls]
|
|
32
38
|
Homepage = "https://www.datapruning.com"
|
|
33
39
|
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
import json
|
|
3
|
-
import pandas as pd
|
|
4
|
-
from datapruning.engine.scanner import scan
|
|
5
|
-
from datapruning.engine.intelligence import analyze_intelligence
|
|
6
|
-
from datapruning.engine.strategy_selector import select_strategy, get_algorithm, list_available_algorithms
|
|
7
|
-
from datapruning.engine.optimizer import run_optimization
|
|
8
|
-
from datapruning.engine.explainability import build_report
|
|
9
|
-
from datapruning.engine.benchmark import benchmark_algorithms, compare_to_full
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class DatasetEngine:
|
|
13
|
-
def __init__(self, df: pd.DataFrame, target_col: str = None):
|
|
14
|
-
self.df = df
|
|
15
|
-
self.target_col = target_col
|
|
16
|
-
self._analysis = None
|
|
17
|
-
self._last_result = None
|
|
18
|
-
self._last_optimizer = None
|
|
19
|
-
|
|
20
|
-
def analyze(self) -> dict:
|
|
21
|
-
scan_result = scan(self.df, target_col=self.target_col)
|
|
22
|
-
intelligence_result = analyze_intelligence(self.df, target_col=self.target_col)
|
|
23
|
-
self._analysis = {"scan": scan_result, "intelligence": intelligence_result}
|
|
24
|
-
return self._analysis
|
|
25
|
-
|
|
26
|
-
def report(self, as_json: bool = False):
|
|
27
|
-
if self._analysis is None:
|
|
28
|
-
self.analyze()
|
|
29
|
-
if as_json:
|
|
30
|
-
return json.dumps(self._analysis, indent=2, default=str)
|
|
31
|
-
s, i = self._analysis["scan"], self._analysis["intelligence"]
|
|
32
|
-
print(f"Rows: {s['rows']:,} | Columns: {s['columns']} | Memory: {s['memory_mb']} MB")
|
|
33
|
-
print(f"Missing: {s['missing_pct']}% | Duplicates: {s['duplicate_pct']}%")
|
|
34
|
-
if "task_type" in s:
|
|
35
|
-
print(f"Task type: {s['task_type']}")
|
|
36
|
-
if "imbalance_ratio" in s:
|
|
37
|
-
print(f"Imbalance ratio: {s['imbalance_ratio']}:1")
|
|
38
|
-
if i["leakage_risk_columns"]:
|
|
39
|
-
print(f"Possible leakage columns: {i['leakage_risk_columns']}")
|
|
40
|
-
print(f"Complexity score: {i['complexity_score']}/100")
|
|
41
|
-
return self._analysis
|
|
42
|
-
|
|
43
|
-
def recommend(self):
|
|
44
|
-
if self._analysis is None:
|
|
45
|
-
self.analyze()
|
|
46
|
-
return select_strategy(self._analysis)
|
|
47
|
-
|
|
48
|
-
def available_algorithms(self) -> list[dict]:
|
|
49
|
-
return list_available_algorithms()
|
|
50
|
-
|
|
51
|
-
def optimize(self, algorithm_key: str = None, keep_ratio: float = 0.5) -> dict:
|
|
52
|
-
if self.target_col is None:
|
|
53
|
-
raise ValueError("target_col is required to optimize.")
|
|
54
|
-
if algorithm_key is None:
|
|
55
|
-
rec = self.recommend()
|
|
56
|
-
optimizer = rec.algorithm_instance
|
|
57
|
-
else:
|
|
58
|
-
optimizer = get_algorithm(algorithm_key)
|
|
59
|
-
result = run_optimization(self.df, self.target_col, optimizer, keep_ratio=keep_ratio)
|
|
60
|
-
report = build_report(optimizer, result, self.df)
|
|
61
|
-
self._last_result = result
|
|
62
|
-
self._last_optimizer = optimizer
|
|
63
|
-
return {
|
|
64
|
-
"optimized_df": result.optimized_df,
|
|
65
|
-
"explainability": report["explanation"],
|
|
66
|
-
"dataset_score": report["dataset_score"],
|
|
67
|
-
"runtime_seconds": report["runtime_seconds"],
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
def export(self, path: str, result: dict = None):
|
|
71
|
-
result = result or {"optimized_df": self._last_result.optimized_df}
|
|
72
|
-
result["optimized_df"].to_csv(path, index=False)
|
|
73
|
-
print(f"Exported optimized dataset -> {path} ({len(result['optimized_df']):,} rows)")
|
|
74
|
-
return path
|
|
75
|
-
|
|
76
|
-
def benchmark(self, algorithms: list[str] = None, keep_ratio: float = 0.5) -> dict:
|
|
77
|
-
return benchmark_algorithms(self.df, self.target_col, algorithms=algorithms, keep_ratio=keep_ratio)
|
|
78
|
-
|
|
79
|
-
def compare(self, optimized_df: pd.DataFrame) -> dict:
|
|
80
|
-
return compare_to_full(optimized_df, self.df, self.target_col)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/benchmark/evaluation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/embeddings/torch_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/selectors/facility_location.py
RENAMED
|
File without changes
|
|
File without changes
|
{datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/selectors/out_of_fold.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|