Skip to content

Commit f003ad0

Browse files
committed
Expand benchmarks across classification and generative data
1 parent d2ad280 commit f003ad0

12 files changed

Lines changed: 2365 additions & 106 deletions

benchmarks/README.md

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,103 @@
11
# Reproducible real-dataset benchmark
22

3-
This benchmark downloads fixed stratified slices from `fancyzhx/ag_news` and
4-
`stanfordnlp/imdb`, generates disclosed dirty variants, applies BuffData's local
5-
validation/task-detection/exact-deduplication stages, and trains identical PyTorch
6-
classifiers across three fixed seeds.
3+
## Non-classification matrix
4+
5+
The generative matrix complements the label-classification suites with six large,
6+
real Hugging Face datasets covering instruction/SFT, multi-turn chat, preference/DPO,
7+
extractive QA, summarization, and raw language-modeling text. It compares every
8+
unmodified source slice with BuffData's output, then appends 40% exact duplicates and
9+
10% invalid records and repeats the comparison at 10,000 and 30,000 source rows.
10+
11+
```bash
12+
python benchmarks/benchmark_generative_matrix.py \
13+
--scales 10000 30000 \
14+
--output-dir benchmarks/results-generative
15+
```
16+
17+
The generated `REPORT.md` and `results.json` include rows retained/deleted, source
18+
rows lost or changed, exact duplicates found and removed, invalid rows removed,
19+
defect leakage, cleaning-decision accuracy, deletion precision/recall/F1, exact
20+
output-content accuracy, clean-vs-dirty output parity, character preservation,
21+
per-stage rejections, and throughput. Because these datasets are open-ended rather
22+
than class-labeled, accuracy is defined against the clean optimized control instead
23+
of inventing an inapplicable classifier label. LLM quality scoring and classification
24+
are disabled, and provider token usage must remain zero.
25+
26+
For downstream before → after movement on disjoint held-out rows, run:
27+
28+
```bash
29+
python benchmarks/benchmark_generative_utility.py \
30+
--scales 10000 30000 --eval-rows 250 \
31+
--output-dir benchmarks/results-generative
32+
```
33+
34+
This writes `UTILITY_REPORT.md` and `utility_results.json`. Metrics are task-specific:
35+
response token-F1 for instruction/chat, preference accuracy for DPO, answer exact
36+
match/F1 for QA, ROUGE-L for summarization, and next-token accuracy/perplexity for
37+
raw text. Each report shows clean raw → clean optimized and dirty raw → dirty optimized.
38+
39+
This benchmark downloads fixed stratified slices from **19 Hugging Face datasets**,
40+
generates disclosed dirty variants, applies BuffData's local validation/task-detection/
41+
exact-deduplication stages, and trains identical PyTorch classifiers across three fixed
42+
seeds. The catalog spans sentiment, topic, emotion, hate/irony, question type, and
43+
subjectivity tasks with 2 to 20 classes.
44+
45+
| Family | Datasets |
46+
|---|---|
47+
| Binary | `imdb`, `yelp_polarity`, `amazon_polarity`, `rotten_tomatoes`, `sst2`, `subj`, `tweet_eval_irony`, `tweet_eval_hate`, `cr`, `amazon_counterfactual` |
48+
| Multi-class | `ag_news`, `dbpedia_14`, `emotion`, `tweet_eval_sentiment`, `yahoo_answers_topics`, `tweet_eval_emotion`, `newsgroups_20`, `trec_coarse`, `tweet_sentiment_extraction` |
749

850
```bash
951
cd ~/projects/buffdata
1052
source .venv/bin/activate
1153
python benchmarks/benchmark_buffdata.py
1254
```
1355

56+
The default run is local after the Hugging Face downloads complete; it makes no LLM
57+
API calls. To run a smaller slice while developing:
58+
59+
```bash
60+
python benchmarks/benchmark_buffdata.py \
61+
--datasets ag_news imdb tweet_eval_sentiment \
62+
--train-rows 1000 --test-rows 500 --epochs 2
63+
```
64+
1465
Outputs are written to `benchmarks/results/`, including the exact JSONL inputs,
15-
per-seed metrics in `results.json`, and a concise `REPORT.md`.
66+
per-seed metrics in `results.json`, and a concise `REPORT.md`. The report and JSON now
67+
include both model-quality metrics and explicit data-hygiene accounting:
68+
69+
- input, retained, and deleted row counts plus deletion rate;
70+
- duplicate rows and duplicate groups present in the pipeline input;
71+
- conflicting-label duplicate rows/groups;
72+
- invalid rows deleted by validation and duplicates deleted by exact dedup;
73+
- clean source rows accidentally deleted;
74+
- injected defects removed versus retained, with a per-category/per-stage cross-tab;
75+
- unique-content and empty-text counts, plus exact rejection-reason totals.
76+
77+
`duplicate_rows_in_input` follows BuffData's own exact-dedup equivalence rule: trimmed
78+
classification text, independent of label. `duplicate_rows_deleted` is the observed
79+
dedup-stage result, so the report distinguishes duplicates that exist from rows that
80+
were actually removed.
1681

1782
For the larger Gemini-audited suite (12,000 training records per dataset):
1883

1984
```bash
2085
python benchmarks/benchmark_buffdata.py \
2186
--datasets dbpedia_14 yelp_polarity emotion \
2287
--train-rows 12000 --test-rows 2000 --epochs 6 \
23-
--gemini-audit-rows 20 --output-dir benchmarks/results-large
88+
--gemini-audit-rows 20 --audit-provider gemini \
89+
--output-dir benchmarks/results-large
2490
```
2591

2692
`--gemini-audit-rows` uses BuffData's scalable batched-sample quality mode. Validation,
2793
deduplication, and task detection still process every row; Gemini judges only a
2894
deterministic representative sample (20 rows per structured request) and its token
2995
usage is recorded in the report.
3096

31-
The local benchmark does not require an API key. After `GEMINI_API_KEY` is set,
32-
the provider-backed optimizer can be tested separately on generative datasets;
33-
it is deliberately not mixed into this classification benchmark because LLM
34-
scoring/refinement and deterministic validation/deduplication test different claims.
97+
The local benchmark does not require an API key. Setting `--gemini-audit-rows` above
98+
zero enables the optional provider-backed quality audit and requires the corresponding
99+
provider credential. Audit scoring is informational: deterministic validation and
100+
deduplication remain the only stages deciding the row-retention numbers.
35101

36102
## Accuracy regression gate
37103

benchmarks/benchmark_accuracy_regression.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@
2020
from buffdata.engine.pipeline import OptimizationPipeline
2121
from buffdata.models.schemas import DatasetItem, PipelineConfig
2222
from benchmark_buffdata import (
23+
DEFECT_CLEAN,
2324
build_vocab,
2425
make_dirty,
2526
stratified_rows,
2627
summarize,
28+
summarize_data_hygiene,
29+
summarize_defect_breakdown,
2730
train_once,
2831
write_jsonl,
2932
)
@@ -65,7 +68,13 @@ async def optimize(
6568
{"id": item.id, "text": item.get_classification_text(), "label": int(item.labels)}
6669
for item in result.accepted
6770
]
68-
return generated, result.metrics
71+
metrics = dict(result.metrics)
72+
# Real per-category, per-stage accept/reject cross-tab from the pipeline's own
73+
# decisions (not the injection ratios) -- how many of each injected defect type
74+
# were actually caught vs. slipped through, and which stage caught them.
75+
metrics["defect_breakdown"] = summarize_defect_breakdown(result.accepted, result.rejected)
76+
metrics["hygiene"] = summarize_data_hygiene(rows, result.accepted, result.rejected)
77+
return generated, metrics
6978

7079

7180
def evaluate(
@@ -146,6 +155,40 @@ def report(payload: dict[str, Any]) -> str:
146155
"",
147156
f"Generated optimized data improves accuracy by **{gain['accuracy'] * 100:+.2f} points** "
148157
f"and macro-F1 by **{gain['macro_f1'] * 100:+.2f} points**.",
158+
"",
159+
"### Data hygiene: how many rows were actually retained vs. lost",
160+
"",
161+
"Cross-tabulated against the pipeline's real per-row decisions (`item.metadata` after the "
162+
"run), not the injection ratios -- 'Lost' is a row BuffData actually rejected, broken down "
163+
"by the exact stage that caught it.",
164+
"",
165+
"| Category | Injected | Retained | Lost | Lost via validate | Lost via dedup |",
166+
"|---|---:|---:|---:|---:|---:|",
167+
])
168+
breakdown = value.get("defect_breakdown", {})
169+
category_label = {
170+
DEFECT_CLEAN: "clean (uncontaminated source)",
171+
"conflicting_duplicate": "conflicting-label duplicate",
172+
"class_skew_duplicate": "class-skew duplicate",
173+
"empty": "empty row",
174+
}
175+
totals = {"input": 0, "retained": 0, "lost": 0}
176+
stage_totals: dict[str, int] = {}
177+
for category, label in category_label.items():
178+
entry = breakdown.get(category, {"input": 0, "retained": 0, "lost": 0, "lost_by_stage": {}})
179+
for key in ("input", "retained", "lost"):
180+
totals[key] += entry[key]
181+
for stage, count in entry["lost_by_stage"].items():
182+
stage_totals[stage] = stage_totals.get(stage, 0) + count
183+
lines.append(
184+
f"| {label} | {entry['input']:,} | {entry['retained']:,} | {entry['lost']:,} | "
185+
f"{entry['lost_by_stage'].get('validate', 0):,} | {entry['lost_by_stage'].get('dedup', 0):,} |"
186+
)
187+
lines.append(
188+
f"| **Total** | **{totals['input']:,}** | **{totals['retained']:,}** | **{totals['lost']:,}** | "
189+
f"**{stage_totals.get('validate', 0):,}** | **{stage_totals.get('dedup', 0):,}** |"
190+
)
191+
lines.extend([
149192
"",
150193
"## Gates",
151194
"",
@@ -264,6 +307,7 @@ async def main(args: argparse.Namespace) -> None:
264307
"conditions": value_conditions,
265308
"generated_minus_original": value_gain,
266309
"defects": defects,
310+
"defect_breakdown": generated_metrics.get("defect_breakdown", {}),
267311
"pipeline_metrics": generated_metrics,
268312
},
269313
"gates": {

0 commit comments

Comments
 (0)