Text Generation
Transformers
Safetensors
Arabic
llama
arabic
reasoning
chain-of-thought
math
gsm8k
small-language-model
slm
sft
conversational
text-generation-inference
Nawah-Math-Reasoning / code /build_dataset.py
oddadmix's picture
training code: data generation, SFT, eval, GRPO
867d0f3 verified
Raw
History Blame Contribute Delete
4.97 kB
"""
Assemble the Arabic dataset from the translation cache and validate it.
Validation per row (a translation is only kept if it passes):
* both segments translated and non-empty
* the numbers in the Arabic text match the English exactly (order-insensitive multiset)
* no degenerate repetition loop from the translator
* not left largely untranslated (Latin-script residue)
Writes out_gsm/gsm8k_reasoning_ar.parquet plus a rejects file for inspection/retry.
"""
import collections
import json
import re
import sys
from pathlib import Path
import pyarrow as pa
import pyarrow.parquet as pq
sys.path.insert(0, ".")
from gsm_common import build_record
OUT = Path("out_gsm")
AR_DIGITS = str.maketrans("٠١٢٣٤٥٦٧٨٩", "0123456789")
NUM_RE = re.compile(r"\d+(?:\.\d+)?")
LATIN_RE = re.compile(r"[A-Za-z]")
ARABIC_RE = re.compile(r"[؀-ۿ]")
def numbers(text):
return NUM_RE.findall(text.translate(AR_DIGITS).replace(",", ""))
def has_repetition_loop(text, n=6, times=3):
"""Detect the translator getting stuck repeating an n-word window."""
words = text.split()
if len(words) < n * times:
return False
counts = collections.Counter(
" ".join(words[i : i + n]) for i in range(len(words) - n + 1)
)
return counts.most_common(1)[0][1] >= times
VERBALISABLE_MAX = 12 # Arabic writes small quantities as words ("ضعف" for 2, "الستة" for 6)
def check(en, ar, strict=True):
"""
strict=True (reasoning chains): every numeral must survive exactly — the arithmetic depends
on it. strict=False (questions): a small number may be verbalised, but a number the source
never contained is a translation error (observed: '$13751' -> '13571', '4 × 44' -> '4 × 46'),
which silently corrupts the math and is always rejected.
"""
if not ar or not ar.strip():
return "empty"
en_n, ar_n = collections.Counter(numbers(en)), collections.Counter(numbers(ar))
if ar_n - en_n:
return "invented_number"
missing = en_n - ar_n
if missing:
if strict:
return "number_dropped"
if any(float(v) > VERBALISABLE_MAX for v in missing):
return "number_dropped"
if has_repetition_loop(ar):
return "repetition"
if not ARABIC_RE.search(ar):
return "not_arabic"
latin = len(LATIN_RE.findall(ar))
if latin > 0.25 * len(ar.replace(" ", "")):
return "latin_residue"
return None
def main():
trans = {}
with open(OUT / "translations.jsonl", encoding="utf-8") as fh:
for line in fh:
try:
r = json.loads(line)
except json.JSONDecodeError:
continue
trans[r["src"]] = r["tgt"]
print(f"[*] {len(trans)} cached translations")
rows = [json.loads(l) for l in open(OUT / "selected_rows.jsonl", encoding="utf-8")]
print(f"[*] {len(rows)} selected rows")
kept, rejects = [], []
reasons = collections.Counter()
for r in rows:
q_ar, t_ar = trans.get(r["question"]), trans.get(r["thinking"])
if q_ar is None or t_ar is None:
reasons["missing"] += 1
rejects.append({**r, "reason": "missing"})
continue
why = check(r["question"], q_ar, strict=False) or check(r["thinking"], t_ar, strict=True)
if why:
reasons[why] += 1
rejects.append({**r, "question_ar": q_ar, "thinking_ar": t_ar, "reason": why})
continue
kept.append(
{
"text": build_record(q_ar, t_ar, r["answer"]),
"question": q_ar,
"thinking": t_ar,
"answer": r["answer"],
"question_en": r["question"],
"thinking_en": r["thinking"],
"source_index": r["idx"],
}
)
print(f"[*] kept {len(kept)}/{len(rows)} ({len(kept)/len(rows):.2%})")
print(f"[*] rejects: {dict(reasons)}")
table = pa.table({k: [row[k] for row in kept] for k in kept[0]})
pq.write_table(table, OUT / "gsm8k_reasoning_ar.parquet", compression="zstd")
print(f"[+] wrote {OUT / 'gsm8k_reasoning_ar.parquet'} ({table.num_rows} rows)")
with open(OUT / "rejects.jsonl", "w", encoding="utf-8") as fh:
for r in rejects:
fh.write(json.dumps(r, ensure_ascii=False) + "\n")
print(f"[+] wrote {OUT / 'rejects.jsonl'} ({len(rejects)} rows)")
stats = {
"selected": len(rows),
"kept": len(kept),
"kept_pct": 100 * len(kept) / len(rows),
"rejects": dict(reasons),
"unique_translations": len(trans),
}
(OUT / "build_stats.json").write_text(json.dumps(stats, ensure_ascii=False, indent=2), encoding="utf-8")
for row in kept[:3]:
print("-" * 70)
print("EN:", row["question_en"][:120])
print("AR:", row["question"][:120])
print("AR think:", row["thinking"][:160])
if __name__ == "__main__":
main()