Spaces:
Running on Zero
Running on Zero
File size: 7,348 Bytes
1159704 | 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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | """Dataset preparation for GRPO training — Sangue e Grafi.
Generates kinship-reasoning scenarios and packages them as a HuggingFace
``datasets.Dataset`` suitable for GRPO training with ``trl.GRPOTrainer``.
Each example contains:
- **prompt**: a kinship reasoning task with narrative, question, and
instructions on the XML tool-calling format
- **gold_answer**: the correct heir's full name
- **will_clause**: the will clause as a JSON string (for reward computation)
- **optimal_hops**: estimated minimum graph traversals needed
Usage::
python -m src.training.prepare_dataset # 50-scenario smoke test
python -c "from src.training.prepare_dataset import prepare_grpo_dataset; \\
ds = prepare_grpo_dataset(500); ds.save_to_disk('data/grpo_train')"
"""
from __future__ import annotations
import json
import logging
from typing import Any
from datasets import Dataset
from src.graph.scenario_generator import generate_batch
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Prompt template
# ---------------------------------------------------------------------------
_PROMPT_TEMPLATE: str = """\
You are a kinship reasoning agent. Navigate the knowledge graph to answer this question.
Narrative:
{narrative}
Question:
{question}
The knowledge graph contains entities of types Person, Asset, and WillClause.
Use <reasoning> tags for your thought process.
Use <action tool="..." entity="..." property="..."/> tags for graph tool calls.
Use <answer>name</answer> when you have the final answer."""
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def prepare_grpo_dataset(
n_scenarios: int = 500,
seed: int = 10000,
save_path: str | None = None,
push_to_hub: bool = False,
hub_repo_id: str | None = None,
) -> Dataset:
"""Generate and return a GRPO-ready HuggingFace dataset.
Args:
n_scenarios: Number of kinship scenarios to generate.
seed: Base seed for reproducible scenario generation.
Default is 10000 (training range). NEVER use benchmark
seeds (42, 99, 137, 256, 500, 777, 1234, 2048, 3333, 4096).
save_path: If provided, save the dataset to this directory via
``Dataset.save_to_disk()``.
push_to_hub: If *True*, push the dataset to HuggingFace Hub.
Requires ``hub_repo_id`` and a valid ``HF_TOKEN`` in the
environment.
hub_repo_id: Hub repository identifier, e.g.
``"your-org/sangue-e-grafi-grpo"``.
Returns:
A :class:`datasets.Dataset` with columns ``prompt``,
``gold_answer``, ``will_clause``, and ``optimal_hops``.
"""
# Guard against benchmark seed contamination
BENCHMARK_SEEDS = {42, 99, 137, 256, 500, 777, 1234, 2048, 3333, 4096}
for i in range(n_scenarios):
s = seed + i
if s in BENCHMARK_SEEDS:
raise ValueError(
f"Seed {s} is a frozen README benchmark seed. "
f"Use seed >= 10000 for training data."
)
logger.info("Generating %d kinship scenarios (seed=%d)…", n_scenarios, seed)
bundles: list[dict[str, Any]] = generate_batch(n_scenarios, seed=seed)
prompts: list[str] = []
gold_answers: list[str] = []
will_clauses: list[str] = []
optimal_hops_list: list[int] = []
for bundle in bundles:
scenario = bundle["scenario"]
# Format the prompt
prompt = _PROMPT_TEMPLATE.format(
narrative=bundle["narrative"],
question=bundle["question"],
)
prompts.append(prompt)
# Gold answer (resolved to full name by generate_batch)
gold_answers.append(bundle["gold_answer"])
# Will clause as JSON for reward-function consumption
will_clause_json = json.dumps(scenario["will"], ensure_ascii=False)
will_clauses.append(will_clause_json)
# Optimal hops
optimal_hops_list.append(scenario.get("optimal_hops", 3))
dataset = Dataset.from_dict({
"prompt": prompts,
"gold_answer": gold_answers,
"will_clause": will_clauses,
"optimal_hops": optimal_hops_list,
})
logger.info(
"Dataset created: %d examples, columns=%s",
len(dataset),
dataset.column_names,
)
# Persist ---------------------------------------------------------------
if save_path:
dataset.save_to_disk(save_path)
logger.info("Dataset saved to %s", save_path)
if push_to_hub:
if not hub_repo_id:
raise ValueError("hub_repo_id is required when push_to_hub=True")
dataset.push_to_hub(hub_repo_id)
logger.info("Dataset pushed to Hub: %s", hub_repo_id)
return dataset
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _print_dataset_stats(dataset: Dataset) -> None:
"""Print summary statistics about a generated dataset."""
import statistics
prompt_lengths = [len(p) for p in dataset["prompt"]]
hop_values = dataset["optimal_hops"]
# Will-type distribution
will_types: dict[str, int] = {}
for wc_json in dataset["will_clause"]:
wc = json.loads(wc_json)
clause = wc.get("clause_text", "")
will_types[clause[:60] + "…"] = will_types.get(clause[:60] + "…", 0) + 1
print("=" * 60)
print(" Sangue e Grafi — GRPO Dataset Statistics")
print("=" * 60)
print(f" Total examples: {len(dataset)}")
print(f" Columns: {dataset.column_names}")
print(f" Prompt length (chars): "
f"min={min(prompt_lengths)}, "
f"max={max(prompt_lengths)}, "
f"mean={statistics.mean(prompt_lengths):.0f}")
print(f" Optimal hops: "
f"min={min(hop_values)}, "
f"max={max(hop_values)}, "
f"mean={statistics.mean(hop_values):.1f}")
print(f" Unique gold answers: {len(set(dataset['gold_answer']))}")
print()
print(" Will clause distribution:")
for clause_snippet, count in sorted(
will_types.items(), key=lambda x: -x[1]
):
print(f" {count:>4d} {clause_snippet}")
print("=" * 60)
# Show one example
print()
print(" Example prompt (first 500 chars):")
print(" " + "-" * 56)
example = dataset[0]["prompt"][:500]
for line in example.split("\n"):
print(f" | {line}")
print(" " + "-" * 56)
print(f" Gold answer: {dataset[0]['gold_answer']}")
print()
# ---------------------------------------------------------------------------
# CLI entry-point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(name)s %(levelname)s %(message)s",
)
# Generate a small smoke-test dataset (using safe training seeds)
ds = prepare_grpo_dataset(
n_scenarios=50,
seed=99990, # Smoke test range — NOT benchmark seeds
save_path="data/grpo_smoke_test",
)
_print_dataset_stats(ds)
|