| """ |
| Faz 7 v1.5b — RAG-aware SFT verisi, çok-turlu {messages} chat formatı (kısa-extractive + abstain). |
| |
| v1 (faz7_prep_rag_sft.py = {instruction,input,output}) KORUNUR; bu YENİ üretici faz6_sft.encode_messages |
| ile uyumlu {messages} verir. RAG chat'e doğal oturur: system=direktif, user=bağlam+soru, assistant=kısa cevap. |
| KARAR: RAG-SFT KISA-EXTRACTIVE kalır (sohbet SFT'sinden AYRI; factuality > akıcılık) + abstain ("bilmiyorum"). |
| |
| Kaynaklar (extractive, cevap bağlamda span — şema DOĞRULANDI v1'de): |
| EN = rajpurkar/squad_v2 (answerable + unanswerable=abstain HAZIR) |
| TR = ucsahin/TR-Extractive-QA-82K (answer span; abstain YOK → sentetik alakasız-eşleme) |
| |
| Çıktı: {"messages":[system,user,assistant], "lang", "src"} JSONL → faz6_sft.py --data (encode_messages). |
| Şablon faz6_prep_v15b.render() (### Sistem/### Kullanıcı/### Asistan) — tek-kaynak import. |
| ⚠️ faz7_rag.py inference AYNI SYS/user_msg/is_tr'yi kullanmalı (train=inference hizası) → oradan import edilecek. |
| |
| Çalıştırma (Colab/yerel; datasets+sentencepiece kurulu, HF login): |
| HF_TOKEN=hf_xxx python faz7_prep_rag_v15b.py --out rag_v15b.jsonl --n_en 15000 --n_tr 15000 --answer_mode sentence |
| """ |
| import os, sys, json, re, random, argparse |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) |
| from faz6_prep_v15b import render, valid, load_tok |
|
|
| EN_REPO = "rajpurkar/squad_v2" |
| TR_REPO = "ucsahin/TR-Extractive-QA-82K" |
| ABSTAIN = {"tr": "bilmiyorum", "en": "I don't know"} |
|
|
| |
| _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ı"} |
|
|
| |
| SYS = { |
| "tr": ("Sen bir RAG asistanısın. SADECE aşağıdaki bağlamı kullanarak soruyu KISA ve doğrudan yanıtla; " |
| "ezbere/dışarıdan bilgi ekleme, adım adım düşünme. Cevap bağlamda yoksa \"bilmiyorum\" de."), |
| "en": ("You are a RAG assistant. Answer the question using ONLY the context below, briefly and directly; " |
| "do not add outside knowledge or think step by step. If the answer is not in the context, say \"I don't know\"."), |
| } |
|
|
|
|
| |
| 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 user_msg(question, context): |
| q, ctx = question.strip(), context.strip() |
| if is_tr(question): |
| return f"Bağlam:\n{ctx}\n\nSoru: {q}" |
| return f"Context:\n{ctx}\n\nQuestion: {q}" |
|
|
|
|
| def sentence_with(context, answer): |
| """answer span'inin geçtiği TAM cümleyi döndür (cümle-düzeyi extractive). Bulamazsa span.""" |
| for s in re.split(r"(?<=[.!?])\s+", context.strip()): |
| if answer in s: |
| return s.strip() |
| return answer |
|
|
|
|
| def tok_len(sp, messages): |
| return len(sp.encode(render(messages), out_type=int)) + 1 |
|
|
|
|
| def make_row(sp, question, context, answer, max_len, sentence=False): |
| """{messages:[system,user,assistant]}; max_len aşılırsa None (KESME YOK). Cevap KISA (span/cümle/abstain).""" |
| 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 |
| lang = "tr" if is_tr(q) else "en" |
| messages = [ |
| {"role": "system", "content": SYS[lang]}, |
| {"role": "user", "content": user_msg(q, ctx)}, |
| {"role": "assistant", "content": out}, |
| ] |
| if not valid(messages) or tok_len(sp, messages) > max_len: |
| return None |
| return {"messages": messages, "lang": lang, "src": "rag"} |
|
|
|
|
| def is_abstain(row): |
| return row["messages"][-1]["content"].strip() in ABSTAIN.values() |
|
|
|
|
| def synth_tr_abstain(sp, rows, k, max_len, rng): |
| """Alakasız soru↔bağlam eşle → 'bilmiyorum' (TR abstain; ucsahin'de doğal yok).""" |
| out, n = [], len(rows) |
| if n < 2 or k <= 0: |
| return out |
| for _ in range(k * 4): |
| if len(out) >= k: |
| break |
| i, j = rng.randrange(n), rng.randrange(n) |
| if i == j: |
| continue |
| q, ans_i = rows[i][0], rows[i][2] |
| ctx = rows[j][1] |
| if ans_i and ans_i in ctx: |
| continue |
| r = make_row(sp, q, ctx, ABSTAIN["tr"], max_len) |
| if r: |
| out.append(r) |
| return out[:k] |
|
|
|
|
| |
| def gather_en(sp, args): |
| 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) |
| if args.cap_src and len(pos) >= args.cap_src and len(neg) >= args.cap_src: |
| break |
| print(f"[en] {EN_REPO}: answerable={len(pos)} abstain={len(neg)}", flush=True) |
| return pos, neg |
|
|
|
|
| def gather_tr(sp, args): |
| 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)) |
| if args.cap_src and len(pos) >= args.cap_src: |
| break |
| 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["messages"]) for r in sample) |
| ab = sum(1 for r in rows if is_abstain(r)) |
| 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_neg = min(len(neg), int(n * abstain_frac)) |
| return pos[:n - n_neg] + neg[:n_neg] |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--out", default="rag_v15b.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.15, help="abstain (bilmiyorum) örnek oranı") |
| ap.add_argument("--answer_mode", default="sentence", choices=["span", "sentence"], |
| help="span=kısa | sentence=span'in geçtiği TAM cümle (v1'de sentence 6/6 kazandı)") |
| ap.add_argument("--no_tr_abstain", action="store_true") |
| ap.add_argument("--cap_src", type=int, default=0, help="smoke test: kaynak başına ~N örnek (0=üretim)") |
| 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 if not args.cap_src else min(args.n_en, args.cap_src), 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) |
| k_neg = int((args.n_tr if not args.cap_src else args.cap_src) * args.abstain_frac) |
| tr_neg = [] if args.no_tr_abstain else synth_tr_abstain(sp, tr_rows, k_neg, args.max_len, rng) |
| tr = take_balanced(tr_pos, tr_neg, args.n_tr if not args.cap_src else min(args.n_tr, args.cap_src), 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() |
|
|