memory-lora-gemma4 / scripts /augment_paraphrases.py
moncefem
Memory-LoRA hypernetwork for Gemma-4-E2B: code, curated data, sixview checkpoints
481fbb6
Raw
History Blame Contribute Delete
4.89 kB
#!/usr/bin/env python3
"""Augment a document's QA pairs with paraphrased variants of the SAME facts.
Why: the direct-LoRA experiment on the Code2LoRA paper doc (runs/direct_paper)
converged to train_loss=0.0 but only hit 33% exact-match on its OWN training
questions at free-generation time, with several wrong answers collapsing to
the same recycled string (e.g. "47.4%" answering four different percentage
questions). With ~1 phrasing per fact and ~18 facts total, the model found a
shortcut (a few frequently-reinforced answers) instead of learning to
discriminate between questions. Multiple paraphrases of the SAME fact force
the model to actually condition on question content rather than pattern-
match a training-set shortcut.
This does NOT introduce new facts -- it takes the existing (question,
answer) pairs for a document and asks the model to rephrase the QUESTION
several different ways while keeping the answer identical.
Usage:
python scripts/augment_paraphrases.py --doc-id code2lora_paper --n-paraphrases 5
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from pathlib import Path
from typing import List
from openai import OpenAI
HERE = Path(__file__).resolve().parent
REPO_ROOT = HERE.parent
sys.path.insert(0, str(REPO_ROOT))
from memory_lora.data_paths import QNA_DIR, CACHE_DIR, ensure_dirs # noqa: E402
from generate_synthetic_dataset import ( # noqa: E402
CachedClient, _load_dotenv, DEFAULT_GEN_MODEL, OPENROUTER_BASE_URL,
)
_load_dotenv(REPO_ROOT / ".env")
PARAPHRASE_SYSTEM = (
"You rewrite a factual question in N different ways while keeping its "
"meaning and the correct short answer completely unchanged. Output a "
"JSON array of N strings, each a differently-phrased question that "
"still has the exact same answer as the original. Vary sentence "
"structure, word choice, and question format (e.g. direct question, "
"'what value/number', fill-in-the-blank style) -- but never change what "
"is being asked. Output ONLY the JSON array, no prose."
)
def gen_paraphrases(client: CachedClient, model: str, question: str, answer: str, n: int) -> List[str]:
prompt = f"Original question: {question}\nCorrect answer: {answer.strip()}\nGenerate {n} paraphrases."
raw = client.chat(
model=model,
messages=[
{"role": "system", "content": PARAPHRASE_SYSTEM},
{"role": "user", "content": prompt},
],
temperature=0.8,
max_tokens=800,
)
match = re.search(r"\[.*\]", raw, re.DOTALL)
if not match:
return []
try:
items = json.loads(match.group(0))
except json.JSONDecodeError:
return []
return [q.strip() for q in items if isinstance(q, str) and q.strip()]
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--doc-id", required=True)
ap.add_argument("--n-paraphrases", type=int, default=5)
ap.add_argument("--gen-model", default=DEFAULT_GEN_MODEL)
ap.add_argument("--held-out-fraction", type=float, default=0.15,
help="Fraction of NEW paraphrases assigned to "
"qna_split=held_out (rest go to train, since the "
"goal here is more train-time exposure per fact).")
args = ap.parse_args()
ensure_dirs()
api_key = os.environ.get("OPENROUTER_API_KEY")
if not api_key:
raise SystemExit("OPENROUTER_API_KEY not set (expected in .env)")
client = CachedClient(OpenAI(base_url=OPENROUTER_BASE_URL, api_key=api_key), cache_dir=CACHE_DIR)
qna_path = QNA_DIR / "qna.jsonl"
rows = [json.loads(l) for l in qna_path.open()]
doc_rows = [r for r in rows if r["doc_id"] == args.doc_id and r["qna_split"] == "train"]
print(f"Found {len(doc_rows)} existing train QA pairs for {args.doc_id}", flush=True)
import random
rng = random.Random(3407)
new_rows = []
for r in doc_rows:
question = r["question"] if r.get("question") else r["prefix"].removeprefix("Q: ").removesuffix("\nA:")
paraphrases = gen_paraphrases(client, args.gen_model, question, r["target"], args.n_paraphrases)
for pq in paraphrases:
qna_split = "held_out" if rng.random() < args.held_out_fraction else "train"
new_rows.append({
"doc_id": r["doc_id"], "doc_version": r["doc_version"], "split": r["split"],
"qna_split": qna_split, "question": pq,
"prefix": f"Q: {pq}\nA:", "target": r["target"],
})
print(f" {question[:60]!r} -> {len(paraphrases)} paraphrases", flush=True)
with qna_path.open("a") as f:
for row in new_rows:
f.write(json.dumps(row) + "\n")
print(f"\nAppended {len(new_rows)} paraphrased QA pairs to {qna_path}", flush=True)
if __name__ == "__main__":
main()