Repository navigation
Expand file tree
/
Copy pathprofiler.py
More file actions
158 lines (139 loc) · 5.84 KB
/
Copy pathprofiler.py
File metadata and controls
158 lines (139 loc) · 5.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""Dataset profiling and classification applicability detection."""
from __future__ import annotations
from typing import Iterable, List, Optional
from buffdata.engine.client import LLMClient
from buffdata.models.schemas import (
ClassificationTask,
DatasetFormat,
DatasetItem,
DatasetProfile,
)
def representative_sample(items: List[DatasetItem], limit: int = 100) -> List[DatasetItem]:
"""Select deterministic, evenly distributed records instead of only the file head."""
if len(items) <= limit:
return list(items)
if limit == 1:
return [items[len(items) // 2]]
indices = {round(i * (len(items) - 1) / (limit - 1)) for i in range(limit)}
return [items[index] for index in sorted(indices)]
def _flatten_labels(items: Iterable[DatasetItem]) -> tuple[List[str], bool]:
values: set[str] = set()
has_multiple = False
for item in items:
labels = item.labels
if labels is None:
continue
if isinstance(labels, list):
clean = [str(value).strip() for value in labels if str(value).strip()]
has_multiple = has_multiple or len(clean) > 1
values.update(clean)
else:
value = str(labels).strip()
if value:
values.add(value)
return sorted(values), has_multiple
class DatasetProfiler:
def __init__(
self,
client: LLMClient,
model: Optional[str] = None,
sample_size: int = 100,
):
self.client = client
self.model = model or client.default_model
self.sample_size = sample_size
async def profile(self, items: List[DatasetItem]) -> DatasetProfile:
if not items:
return DatasetProfile(
format=DatasetFormat.CUSTOM,
row_count=0,
confidence=1.0,
reasoning="Dataset is empty.",
)
formats = {item.format for item in items}
primary_format = next(iter(formats)) if len(formats) == 1 else DatasetFormat.CUSTOM
sample = representative_sample(items, self.sample_size)
text_fields = self._text_fields(items)
classes, has_multiple = _flatten_labels(items)
label_field = self._label_field(items)
if classes:
if has_multiple:
task = ClassificationTask.MULTI_LABEL
elif len(classes) == 2:
task = ClassificationTask.BINARY
elif len(classes) > 2:
task = ClassificationTask.MULTI_CLASS
else:
task = None
return DatasetProfile(
format=primary_format,
row_count=len(items),
text_fields=text_fields,
existing_label_field=label_field,
classification_applicable=task is not None,
task_type=task,
classes=classes,
confidence=1.0 if task else 0.4,
reasoning=(
"Derived the classification task from existing labels."
if task
else "Only one distinct existing label was found."
),
sampled_ids=[item.id for item in sample],
)
if primary_format in {DatasetFormat.ALPACA, DatasetFormat.CHAT, DatasetFormat.DPO}:
return DatasetProfile(
format=primary_format,
row_count=len(items),
text_fields=text_fields,
classification_applicable=False,
confidence=0.99,
reasoning=f"{primary_format.value} records are generative training data, not a closed-set classification dataset.",
sampled_ids=[item.id for item in sample],
)
sample_texts = [item.get_classification_text() for item in sample]
sample_texts = [text[:4000] for text in sample_texts if text.strip()]
if not sample_texts:
return DatasetProfile(
format=primary_format,
row_count=len(items),
text_fields=text_fields,
confidence=1.0,
reasoning="No classifiable text field was found.",
sampled_ids=[item.id for item in sample],
)
from buffdata.optimizers.classifier import ClassificationSchema
prompt = """Analyze these representative, PII-redacted dataset records.
Decide whether they form a closed-set text classification dataset. Do not force
classification on open-ended pretraining, instruction-response, chat, preference,
or generative data. If applicable, choose binary, multi-class, or multi-label and
infer a concise, stable class list. Confidence must reflect ambiguity.
Records:
""" + "\n---\n".join(sample_texts)
schema = await self.client.generate_structured_async(
prompt=prompt,
response_schema=ClassificationSchema,
model=self.model,
temperature=0.0,
)
return DatasetProfile(
format=primary_format,
row_count=len(items),
text_fields=text_fields,
classification_applicable=schema.applicable,
task_type=schema.task_type,
classes=schema.classes,
confidence=schema.confidence,
reasoning=schema.reasoning,
sampled_ids=[item.id for item in sample],
)
@staticmethod
def _label_field(items: List[DatasetItem]) -> Optional[str]:
for field in ("labels", "label", "category", "target"):
if any(field in item.raw_data for item in items):
return field
return None
@staticmethod
def _text_fields(items: List[DatasetItem]) -> List[str]:
candidates = ("text", "content", "query", "prompt", "instruction", "input", "messages")
return [field for field in candidates if any(field in item.raw_data for item in items)]