data_sampler
Generate a synthetic Q&A dataset from a knowledge base — then throw most of it away.
The premise: a fine-tuning set's quality, not its raw size, drives downstream
accuracy. So data_sampler generates aggressively, judges every row, and keeps only what
is grounded, non-duplicated and leakage-free.
pip install winnow-sampler # the package installs as `data_sampler`
data-sampler-build-techqa --techqa_dir /path/to/TechQA # build the bundled public testbed
data-sampler-run --dataset_key techqa_en --num_new_questions 1500 --num_oos_questions 300
To work on it instead: pip install -e ".[dev]".
Full operating instructions are in USAGE.md, shipped inside the source
distribution (pip download --no-binary :all: winnow-sampler).
The pipeline
KB documents + seed Q&A
│
┌──────▼──────┐ paraphrase · KB question-gen · out-of-scope gen · answer-gen
│ 1. generate │──────────────────────────────► synthetic_qa.jsonl
└──────┬──────┘
│
┌──────▼──────┐ retrieve context, then judge: faithfulness, correctness,
│ 2. score │ completeness, answerability
└──────┬──────┘──────────────────────────────► synthetic_qa_scored.jsonl
│
┌──────▼──────┐ keep faithful AND correct (or verified out-of-scope); dedup
│ 3. curate │──────────────────────────────► synthetic_qa_curated.jsonl
└──────┬──────┘
│
┌──────▼──────┐ leakage-safe, OOS-capped train / held-out split
│ 4. split │──────────────────────────────► sft_train.jsonl + held_out.jsonl
└─────────────┘
Three mechanisms shrink the dataset, all deliberate: the quality threshold in stage 3, question dedup in stage 3, and the out-of-scope ratio cap in stage 4.
What it gets right
Questions come from four layers, not one. Seed questions from the testbed, paraphrases
of those, fresh questions invented from KB passages (single-fact, broad, and multi-hop
spanning two passages), and out-of-scope questions the KB deliberately cannot answer — each
generated with the previous layers' questions in a seen set so layers don't duplicate
each other.
Refusal is trained, not hoped for. Out-of-scope rows get a fixed IDK answer and are
verified by the judge to be genuinely unanswerable from retrieved context before they are
kept, so the model learns when to decline rather than only how.
The split can't leak. A paraphrase follows its parent question: if the parent is held out, so is the paraphrase. Without that, a model evaluated on a held-out question would have trained on a reworded copy of it and scored far too well.
Everything resumes. Every stage writes JSONL and re-reads its predecessor's file, and generation caches each question layer separately, fingerprinted with the knobs that produced it — so an interrupted run picks up where it stopped, and a re-run with different knobs rebuilds what those knobs changed instead of silently reusing the old data. Answer generation is the exception: it is not cached, so a re-run re-answers every in-scope question.
Runs are auditable. Each stage drops a manifest recording its knobs and row counts, so you can see where the data shrank and why — including what it lost: failed LLM calls and dropped rows are counted, and generation fails the run rather than writing a truncated dataset when too many calls die. A cost report covers the generation stage's spend (judging is not yet metered).
Requirements
- Python 3.10+
- An Azure OpenAI resource with a chat deployment (generation and judging) and an
embeddings deployment (the scoring stage retrieves the context it judges against) —
copy
.env.exampleto.envand fill it in. The file is read from the working directory or$DATA_SAMPLER_HOME; real environment variables override it. - A file-backed knowledge base.
techqa_enships configured; build it from the public TechQA release (CDLA-Permissive-1.0) withdata-sampler-build-techqa.
Programmatic API
from data_sampler import DataSampler, GenerationConfig, PipelineConfig, ScoringConfig
config = PipelineConfig(
generation=GenerationConfig(
dataset_key="techqa_en",
paraphrases_per_source_entry=2,
num_new_questions=1500,
num_oos_questions=300,
),
scoring=ScoringConfig(dataset_key="techqa_en"),
)
result = DataSampler(config).run()
print(result["split"]["counts"])
Each stage is also callable on its own — .generate(), .score(), .filter_and_dedup(),
.split() — and falls back to the previous stage's file on disk when given no rows. To
resume: DataSampler(config).run(from_stage="filter").
Adding a knowledge base
- Add an entry to
KB_CONFIGSinsrc/data_sampler/core/data/config.pywithsource="local",lang,domain_descriptionandsystem_prompt. - Write a builder emitting
kb_documents.jsonlandsynthetic_qa_raw.jsonlintoexperiments/shared/<kb>/data/, followingsrc/data_sampler/cli/build_techqa.py.
Nothing else needs editing — the new key appears in every CLI's --dataset_key choices
automatically.
Layout
src/data_sampler/
├── pipeline.py DataSampler — chains the four stages
├── config.py stage configs + output-path resolution
├── env.py workspace .env discovery + credential checks
├── kb.py which KBs are usable
├── caching.py generation-stage resumability (fingerprinted)
├── manifest.py per-stage run manifests
├── generation/ paraphrase / question / answer generation + prompts
├── curation/ judging, filtering, dedup, splitting + judge prompts
├── cli/ one module per command (installed as console scripts)
└── core/ shared building blocks: KB configs, cleaning, chunking,
splitting, LLM clients, local FAISS retrieval, I/O
Tests
pytest
No network, no LLM, no KB required — every external boundary is mocked.
tests/test_end_to_end.py runs all four stages over a fixture KB
(tests/fixtures/mini_kb.py) with only the model and the embedder stubbed, so
the chain itself is covered, not just the stages in isolation.
License
MIT — see LICENSE. The TechQA dataset itself is CDLA-Permissive-1.0 and is not
redistributed here; data-sampler-build-techqa derives the testbed from your own copy.
Release files for winnow-sampler 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| winnow_sampler-0.1.0.tar.gz | 95.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| winnow_sampler-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 180.8 kB
Release files / winnow_sampler-0.1.0.tar.gz
| Download URL | winnow_sampler-0.1.0.tar.gz |
|---|---|
| Size | 95.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
13bb278752894868cec98d408aa56c3092034449ab7418575453745a7b62abad
|
|
BLAKE2b-256 checksum How to use checksums |
999c76aa7ee868f476cea31a887d0cbec45c598d7b6262349c3f77aede8139bf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.7
|
Release files / winnow_sampler-0.1.0-py3-none-any.whl
| Download URL | winnow_sampler-0.1.0-py3-none-any.whl |
|---|---|
| Size | 85.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1f61574a53d0667b529354e232a8d1c6aeb1e96f88cfd5b5d973305a5b6f180a
|
|
BLAKE2b-256 checksum How to use checksums |
b1ae50bf7435ce95be57e92847a2cf1bf2f2dc921fb5e30b6fa21459e18b561e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.7
|