smartcore-v1 / code /kod /faz7_prep_rag_sft.py
kdirgul's picture
faz7_prep_rag_sft: --answer_mode sentence (tam-cumle cevap)
6ff4b8b verified
Raw
History Blame Contribute Delete
9.13 kB
"""
Faz 7 ön-hazırlık — RAG-aware SFT verisi (extractive QA → "bağlamdan cevapla").
Amaç: v1-instruct'a "### Girdi:'deki bağlamdan cevabı çıkar (ezberden değil); bağlamda
yoksa 'bilmiyorum' de" davranışını öğret. A/B testinde 177M'in bağlamı yok sayıp
halüsinasyon ürettiği görüldü → bu, prompt'la değil SFT ile kapanır.
Kaynaklar (extractive, cevap bağlamda span):
EN = rajpurkar/squad_v2 (answerable + unanswerable=abstain HAZIR)
TR = ucsahin/TR-Extractive-QA-82K (answer string, context'te span; abstain YOK → sentetik)
Çıktı: {instruction, input, output} JSONL → faz6_sft.py --data'ya verilir.
instruction = faz7_rag.build_rag_prompt'un talimatıyla BİREBİR (train=inference hizası):
"<bağlamı kullan, kısa/doğrudan, yoksa bilmiyorum>\nSoru: {q}"
input = bağlam (context)
output = bağlamdaki cevap span'i | "bilmiyorum"/"I don't know" (abstain)
Çalıştırma (Colab/yerel; datasets + sentencepiece kurulu, HF login):
pip install "datasets>=2.18" sentencepiece
HF_TOKEN=hf_xxx python faz7_prep_rag_sft.py --out rag_sft.jsonl --n_en 15000 --n_tr 15000
"""
import os, sys, json, re, random, argparse
EN_REPO = "rajpurkar/squad_v2"
TR_REPO = "ucsahin/TR-Extractive-QA-82K"
ABSTAIN = {"tr": "bilmiyorum", "en": "I don't know"}
# ───────────── saf-mantık (yerelde gerçek tokenizer'la test edilebilir) ─────────────
# TR tespiti: özel karakter VEYA TR soru-kelimesi (çoğu TR soru "nedir/hangi/neresi" gibi
# kelime taşır ama çğıöşü taşımayabilir — örn "Fotosentez nedir?"). faz7_rag ile AYNI tutulur.
_TR_WORDS = {"nedir", "ne", "hangi", "neresi", "nerede", "nereye", "kim", "kimdir", "neden",
"niçin", "niye", "mıdır", "midir", "mudur", "müdür", "kaç", "kaçtır", "nasıl",
"mı", "mi", "mu", "mü", "için", "ile", "kaçıncı"}
def is_tr(question):
ql = question.lower()
if any(ch in ql for ch in "çğıöşü"):
return True
words = set(ql.replace("?", " ").replace("'", " ").replace(".", " ").split())
return bool(words & _TR_WORDS)
def build_instr(question):
# faz7_rag.build_rag_prompt'un talimat metniyle BİREBİR aynı (dil-duyarlı).
if is_tr(question):
return ("Aşağıdaki bağlamı kullanarak soruyu kısa ve doğrudan yanıtla; adım adım düşünme. "
"Cevap bağlamda yoksa \"bilmiyorum\" de.\nSoru: " + question)
return ("Answer the question using only the context below, briefly and directly; do not think step by step. "
"If the answer is not in the context, say \"I don't know\".\nQuestion: " + question)
def build_prompt(instr, inp=""):
instr = instr.strip(); inp = (inp or "").strip()
if inp:
return f"### Talimat:\n{instr}\n\n### Girdi:\n{inp}\n\n### Yanıt:\n"
return f"### Talimat:\n{instr}\n\n### Yanıt:\n"
def tok_len(sp, instr, out, inp=""):
"""faz6_sft ile aynı tokenizasyon: prompt(+input) + yanıt + eos."""
return len(sp.encode(build_prompt(instr, inp) + out.strip(), out_type=int)) + 1
def sentence_with(context, answer):
"""answer span'inin geçtiği TAM cümleyi döndür (tam-cümle cevap modu). Bulamazsa span'e düşer."""
for s in re.split(r"(?<=[.!?])\s+", context.strip()):
if answer in s:
return s.strip()
return answer
def make_row(sp, question, context, answer, max_len, sentence=False):
"""{instruction,input,output}; max_len aşılırsa None (KESME YOK — faz6 kalıbı).
sentence=True → output = span'in geçtiği tam cümle (extractive cümle-düzeyi)."""
q, ctx, a = question.strip(), context.strip(), answer.strip()
if not (q and ctx and a):
return None
out = sentence_with(ctx, a) if sentence else a
instr = build_instr(q)
if tok_len(sp, instr, out, ctx) > max_len:
return None
return {"instruction": instr, "input": ctx, "output": out}
def synth_tr_abstain(sp, rows, k, max_len, rng):
"""Alakasız soru↔bağlam eşle → 'bilmiyorum' (TR abstain; ucsahin'de doğal abstain yok)."""
out, n = [], len(rows)
if n < 2 or k <= 0:
return out
for _ in range(k * 4): # filtre kaybı için fazladan dene
if len(out) >= k:
break
i, j = rng.randrange(n), rng.randrange(n)
if i == j:
continue
q = rows[i][0]; ctx = rows[j][1]
if rows[i][2] and rows[i][2] in ctx: # cevap tesadüfen bu bağlamda → mismatch değil, atla
continue
r = make_row(sp, q, ctx, ABSTAIN["tr"], max_len)
if r:
out.append(r)
return out[:k]
# ───────────── yükleyiciler (datasets gerekir) ─────────────
def load_tok(token):
import sentencepiece as spm
from huggingface_hub import hf_hub_download
p = hf_hub_download("kdirgul/smartcore-v1", "tokenizer/tokenizer.model", repo_type="model", token=token)
return spm.SentencePieceProcessor(model_file=p)
def gather_en(sp, args):
"""squad_v2: answerable → span; unanswerable (text=[]) → 'I don't know'."""
from datasets import load_dataset
ds = load_dataset(EN_REPO, split="train")
sent = args.answer_mode == "sentence"
pos, neg = [], []
for ex in ds:
texts = (ex.get("answers") or {}).get("text") or []
if texts:
r = make_row(sp, ex.get("question", ""), ex.get("context", ""), texts[0], args.max_len, sentence=sent)
if r:
pos.append(r)
else:
r = make_row(sp, ex.get("question", ""), ex.get("context", ""), ABSTAIN["en"], args.max_len)
if r:
neg.append(r)
print(f"[en] {EN_REPO}: answerable={len(pos)} abstain={len(neg)}", flush=True)
return pos, neg
def gather_tr(sp, args):
"""ucsahin TR-82K: answer span. (abstain doğal yok → sentetik üretilir)"""
from datasets import load_dataset
ds = load_dataset(TR_REPO, split="train")
sent = args.answer_mode == "sentence"
pos, rows = [], []
for ex in ds:
q = (ex.get("question") or "").strip(); ctx = (ex.get("context") or "").strip()
a = (ex.get("answer") or "").strip()
r = make_row(sp, q, ctx, a, args.max_len, sentence=sent)
if r:
pos.append(r); rows.append((q, ctx, a))
print(f"[tr] {TR_REPO}: answerable={len(pos)}", flush=True)
return pos, rows
def stats(sp, rows, name):
if not rows:
print(f"[{name}] 0 örnek", flush=True); return
sample = rows if len(rows) <= 3000 else random.sample(rows, 3000)
ls = sorted(tok_len(sp, r["instruction"], r["output"], r.get("input", "")) for r in sample)
ab = sum(1 for r in rows if r["output"] in ABSTAIN.values())
print(f"[{name}] n={len(rows)} abstain={ab} | token: med={ls[len(ls)//2]} "
f"p90={ls[int(len(ls)*0.9)]} max={ls[-1]}", flush=True)
def take_balanced(pos, neg, n, abstain_frac):
"""n örnek: abstain_frac kadar negatif (abstain), gerisi pozitif."""
n_neg = min(len(neg), int(n * abstain_frac))
n_pos = n - n_neg
return pos[:n_pos] + neg[:n_neg]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", default="rag_sft.jsonl")
ap.add_argument("--max_len", type=int, default=2048)
ap.add_argument("--n_en", type=int, default=15000)
ap.add_argument("--n_tr", type=int, default=15000)
ap.add_argument("--abstain_frac", type=float, default=0.2, help="abstain (bilmiyorum) örnek oranı")
ap.add_argument("--answer_mode", default="span", choices=["span", "sentence"],
help="span=kısa cevap | sentence=span'in geçtiği TAM cümle (daha doğal cevap)")
ap.add_argument("--no_tr_abstain", action="store_true", help="TR sentetik abstain üretme")
ap.add_argument("--seed", type=int, default=42)
args = ap.parse_args()
token = os.environ.get("HF_TOKEN")
try:
from huggingface_hub import get_token
token = token or get_token()
except Exception:
pass
sp = load_tok(token)
rng = random.Random(args.seed)
print("=== EN (squad_v2) ===", flush=True)
en_pos, en_neg = gather_en(sp, args)
rng.shuffle(en_pos); rng.shuffle(en_neg)
en = take_balanced(en_pos, en_neg, args.n_en, args.abstain_frac)
print("=== TR (TR-82K) ===", flush=True)
tr_pos, tr_rows = gather_tr(sp, args)
rng.shuffle(tr_pos); rng.shuffle(tr_rows)
tr_neg = [] if args.no_tr_abstain else synth_tr_abstain(
sp, tr_rows, int(args.n_tr * args.abstain_frac), args.max_len, rng)
tr = take_balanced(tr_pos, tr_neg, args.n_tr, args.abstain_frac)
stats(sp, en, "EN"); stats(sp, tr, "TR")
data = en + tr; rng.shuffle(data)
with open(args.out, "w", encoding="utf-8") as f:
for r in data:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
stats(sp, data, "TOPLAM")
print(f"\n[bitti] {len(data)} örnek (EN {len(en)} + TR {len(tr)}) -> {args.out}", flush=True)
if __name__ == "__main__":
main()