""" Pusula — Turkce arama modellerinin gercek kullanici sorgularina dayanikliligi. Veri: leaderboard.json (yalnizca skorlar; benchmark metni icermez). Bozulmalar: pusula_perturb.py. """ import difflib import html import json import gradio as gr from pusula_perturb import PERTURBATIONS, perturb with open("leaderboard.json", encoding="utf-8") as f: DATA = json.load(f) META, MODELS, FIND = DATA["meta"], DATA["models"], DATA.get("findings", {}) PERTS = [p["name"] for p in META["perturbations"]] PLABEL = {p["name"]: p["label"] for p in META["perturbations"]} PDESC = {p["name"]: p["description"] for p in META["perturbations"]} DATASETS = META["datasets"] RANKED = [d for d in DATASETS if d.get("in_ranking", True)] HELD = [d for d in DATASETS if not d.get("in_ranking", True)] # gosterilir, siralamaya girmez HELD_NAMES = {d["name"] for d in HELD} METRIC = META["metric"] METRIC_LABEL = {"ndcg@10": "nDCG@10", "recall@10": "Recall@10", "mrr@10": "MRR@10"}.get(METRIC, METRIC) BY_SHORT = {m["short"]: m for m in MODELS} # Kahraman cumlesi icin bozulmaya gore ifade (sayiya ek gelmeyecek bicimde kurulur) PERT_PHRASE = { "upper": "BÜYÜK HARFLE yazılmış sorgularda", "ascii_full": "Türkçe karakter kullanılmadan yazılmış sorgularda", "ascii_partial": "Türkçe karakterlerin bir kısmı atlanmış sorgularda", "typo": "yazım hatalı sorgularda", "colloquial": "gündelik dille yazılmış sorgularda", "mobile": "telefondan aceleyle yazılmış sorgularda", } VIEWS = {"Mobil sorguda başarı": "mobile", "Temiz sorguda başarı": "clean", "Ortalama koruma": "avg"} ALL = f"Sıralamadaki veri setleri ({len(RANKED)})" if HELD else "Tüm veri setleri" DS_CHOICES = [ALL] + [(f"{d['name']} (sıralama dışı)" if d["name"] in HELD_NAMES else d["name"], d["name"]) for d in DATASETS] def pct(x): return "—" if x is None else f"%{100 * x:.0f}" def fmt_params(p): if not p: return "" return f"{p / 1000:.1f}B" if p >= 1000 else f"{p}M" # ═══ Kahraman alani: veriden turetilen kapak ═══ TR_MTEB_URL = "https://aclanthology.org/2025.findings-emnlp.471/" def _stat(value, label, kind=""): return (f'
{value}
' f'
{label}
') def hero_html(): n_models, n_ds = len(MODELS), len(RANKED) n_q = f"{sum(d['n_queries'] for d in RANKED):,}".replace(",", ".") held_note = (" " + ", ".join(html.escape(d["name"]) for d in HELD) + " tabloda ayrıca gösteriliyor ama eğitim verisiyle örtüştüğü için sıralamaya girmiyor.") if HELD else "" stats = [] wd = FIND.get("worst_drop") if wd: stats.append(_stat(pct(wd["retention"]), f'{html.escape(PLABEL.get(wd["pert"], wd["pert"]))} sorgularda {html.escape(wd["model"])} ' f'modelinin koruma oranı', "pu-stat-neg")) cw = FIND.get("clean_winner") if cw and cw["mobile_rank"] != 1: stats.append(_stat(f'{cw["clean_rank"]}. → {cw["mobile_rank"]}.', f'Temiz sorgularda lider olan {html.escape(cw["model"])}, mobil sorgulardaki sırası')) mw = FIND.get("mobile_winner") if mw: score = BY_SHORT[mw["model"]]["by_pert"]["mobile"]["score"] tied = f', {", ".join(map(html.escape, mw["tied_with"]))} ile aynı grupta' if mw["tied_with"] else "" stats.append(_stat(f'{html.escape(mw["model"])}', f'Mobil sorgularda en iyi model ({METRIC_LABEL} {score:.3f}{tied})')) fe = FIND.get("fold_effect") if fe and fe["before"] is not None: stats.append(_stat(f'{pct(fe["before"])} → {pct(fe["after"])}', "BM25'e tek satırlık ASCII katlama eklenince ASCII sorgulardaki koruma oranı")) return f"""

Pusula

Türkçe retrieval dayanıklılık benchmark'ı

Türkçe arama modelleri, kullanıcıların sorguları gerçekte yazdığı gibi yazıldığında ne kadar başarı kaybediyor? {n_models} model, {n_ds} veri setinden {n_q} sorgu ve {len(PERTS)} tür sorgu bozulmasıyla ölçüldü.{held_note}

{"".join(stats)}
""" # ═══ Siralama tablosu ═══ def _values(m, dataset): if dataset == ALL: return m["clean"], {p: m["by_pert"][p] for p in PERTS} d = m["by_dataset"][dataset] return d["clean"], {p: d[p] for p in PERTS} def _heat(r): if r is None: return '—' alpha = min(0.8, max(0.0, (1 - r) * 0.9)) # %100 renksiz; %90→.09, %75→.23, %50→.45, %15→.77 return (f'{round(100 * r)}') def board_html(view_label, dataset): view = VIEWS[view_label] rows = [] for m in MODELS: clean, perts = _values(m, dataset) rets = [perts[p]["retention"] for p in PERTS if perts[p]["retention"] is not None] rows.append((m, clean, perts, sum(rets) / len(rets) if rets else None)) key = {"mobile": lambda r: -r[2]["mobile"]["score"], "clean": lambda r: -r[1], "avg": lambda r: -(r[3] if r[3] is not None else -1)}[view] rows.sort(key=key) show_groups = view == "mobile" and dataset == ALL head = "".join(f'{html.escape(PLABEL[p])}' for p in PERTS) body = [] for i, (m, clean, perts, avg) in enumerate(rows): g = m["group"] if show_groups else i + 1 band = "pu-band" if show_groups and m["group"] % 2 == 0 else "" same = show_groups and i > 0 and rows[i - 1][0]["group"] == m["group"] rank_cell = f'{"" if same else g}' fam = html.escape(m["family"]) body.append( f'{rank_cell}' f'' f'{html.escape(m["short"])}
{fam}' f'{fmt_params(m.get("params_m"))}
' f'{clean:.3f}{perts["mobile"]["score"]:.3f}' + "".join(_heat(perts[p]["retention"]) for p in PERTS) + f'{"—" if avg is None else round(100 * avg)}') note = ("Aynı numaralı modeller arasındaki fark istatistiksel olarak ayırt edilemez " "(veri seti dengeli eşleştirilmiş bootstrap, %95)." if show_groups else "Gruplama yalnızca sıralamadaki veri setleri ve mobil sıralamada gösterilir.") if dataset in HELD_NAMES: reason = next(d.get("holdout_reason") or "" for d in HELD if d["name"] == dataset) warn = (f'

Sıralama dışı veri seti. {html.escape(reason)} ' f'Bu tablodaki skorlar bazı modeller için şişik olabilir; modelleri karşılaştırmak için kullanmayın.

') else: warn = "" return f"""{warn}
{head}{"".join(body)}
{"Grup" if show_groups else "#"}ModelTemiz
{METRIC_LABEL}
Mobil
{METRIC_LABEL}
Ort.
Koruma oranı (%), yalnızca bozulmanın değiştirdiği sorgularda

{note} Mutlak skorlar alt-örneklenmiş korpus nedeniyle TR-MTEB ile karşılaştırılamaz; benchmark'ın iddiası koruma oranlarıdır.

""" # ═══ Sorgunu boz ═══ def _diff(a, b): out = [] for op, i1, i2, j1, j2 in difflib.SequenceMatcher(None, a, b, autojunk=False).get_opcodes(): seg = html.escape(b[j1:j2]) if op == "equal": out.append(seg) elif seg: out.append(f"{seg}") else: out.append(f'') return "".join(out) def perturb_html(text, seed): text = (text or "").strip() if not text: return '

Bir sorgu yazın.

' rows = [] for p in PERTS: out = perturb(text, f"demo-{text}", p, seed=int(seed)) changed = out != text fragile, robust = FIND.get("most_fragile", {}).get(p), FIND.get("most_robust", {}).get(p) hint = "" if fragile and robust: fr, rb = BY_SHORT[fragile]["by_pert"][p]["retention"], BY_SHORT[robust]["by_pert"][p]["retention"] hint = (f'En kırılgan: {html.escape(fragile)} {pct(fr)}   ' f'En dayanıklı: {html.escape(robust)} {pct(rb)}') rows.append(f'
{html.escape(PLABEL[p])}' f'{html.escape(PDESC[p])}
' f'

{_diff(text, out) if changed else html.escape(out)}' f'{"" if changed else " (bu sorguyu değiştirmedi)"}

{hint}
') return "".join(rows) def next_seed(seed): return int(seed) + 1 # ═══ Yontem ═══ def method_md(): ds_rows = "\n".join( f"| {d['name']} | {d['domain']} | {'doğal Türkçe' if d.get('native') else 'çeviri'} | {d['n_queries']} | {d['corpus_size']:,} | " .replace(",", ".") + ("Evet" if d.get("in_ranking", True) else "**Hayır**") + " |" for d in DATASETS) held_md = "".join(f"\n- **{d['name']}**: {d.get('holdout_reason') or ''}" for d in HELD) held_md = (f"\n\n**Sıralama dışı veri setleri.** Bu setler tabloda gösterilir ama ortalamaya, sıralamaya, gruplara ve " f"bulgulara girmez:{held_md}") if HELD else "" pinned = "\n".join(f"- **{m['short']}**: `{m['model_id']}` @ `{m['revision'][:7]}`" for m in MODELS if m.get("revision")) pinned_md = f"\n\n### Sabitlenmiş model sürümleri\n{pinned}" if pinned else "" p_rows = "\n".join(f"| {PLABEL[p]} | {PDESC[p]} | {META['example']['outputs'][p]} |" for p in PERTS) failed = "\n".join(f"- **{f['short']}** (`{f['model_id']}`): {f['reason']}" for f in META.get("failed", [])) or "- Yok" return f""" ### Veri setleri | Veri seti | Alan | Kaynak | Sorgu | Korpus | Sıralamada | |---|---|---|---|---|---| {ds_rows}{held_md} Korpus, her sorgunun ilgili dokümanlarını koruyacak şekilde alt-örneklendi. TR-MTEB veri setleri ([Baysan & Güngör, Findings of EMNLP 2025](https://aclanthology.org/2025.findings-emnlp.471/)) ve İletişim Başkanlığı DMM bültenlerinden türetilen doğal Türkçe başlık → analiz görevi kullanıldı. ### Bozulmalar Yalnızca **sorgu** bozulur, dokümanlar temiz kalır. Örnek sorgu: *{META['example']['text']}* | Bozulma | Açıklama | Örnek | |---|---|---| {p_rows} ### Metrik **{METRIC_LABEL}** ve **koruma oranı** = bozuk sorgudaki {METRIC_LABEL} / temiz sorgudaki {METRIC_LABEL}; yalnızca bozulmanın gerçekten değiştirdiği sorgular üzerinden, sıralamadaki veri setleri eşit ağırlıkla. Sıralama mobil sorgulardaki mutlak başarıya göredir. ### Değerlendirilemeyen modeller {failed}{pinned_md} ### Sınırlamalar - Üç veri seti makine çevirisi; sorgular resmî dil kullandığı için gündelik yazım bozulması az sorguyu etkiler. - Mutlak skorlar alt-örneklenmiş korpus nedeniyle TR-MTEB ile karşılaştırılamaz. - Bozulmalar kural tabanlıdır; gerçek kullanıcı hatalarının tüm çeşitliliğini kapsamaz. ### Yeniden üretim Benchmark parmak izi: `{META['fingerprint']}`. Kod ve sorgu bazında skorlar: [{META['dataset_repo']}](https://huggingface.co/datasets/{META['dataset_repo']}). Benchmark metinleri lisans nedeniyle dağıtılmaz; kod, kaynak veri setlerinden birebir aynısını üretir. """ HEAD = ('' '') CSS = """ :root { --pu-ink:#1B2430; --pu-muted:#5F6B7A; --pu-line:rgba(95,107,122,.25); --pu-red:#C8102E; --pu-good:#2F7D6D; --pu-bad:#C8102E; } .gradio-container { max-width: 1280px !important; margin: 0 auto !important; font-family: 'Public Sans', system-ui, -apple-system, 'Segoe UI', sans-serif !important; } footer { display:none !important; } .pu-hero { padding: 30px 4px 10px; } .pu-top { display:flex; justify-content:space-between; align-items:flex-start; gap:20px; flex-wrap:wrap; } .pu-brand { display:flex; gap:14px; align-items:center; color:var(--pu-ink); } .dark .pu-brand { color:#E8ECF1; } .pu-compass { width:50px; height:50px; flex:none; } .pu-brand h1 { margin:0 !important; font:600 2.3rem/1.05 'Source Serif 4', Georgia, serif !important; letter-spacing:-.015em; color:inherit !important; } .pu-kicker { margin:4px 0 0 !important; font-size:.95rem; color:var(--pu-muted) !important; } .pu-links { display:flex; gap:8px; padding-top:6px; } .pu-links a { border:1px solid var(--pu-line); border-radius:6px; padding:6px 12px; font-size:.86rem; font-weight:500; text-decoration:none !important; color:inherit !important; white-space:nowrap; } .pu-links a:hover { border-color: var(--pu-muted); } .pu-lede { max-width:64ch; margin:18px 0 0 !important; font-size:1.02rem; line-height:1.6; color:var(--pu-muted) !important; } .pu-stats { display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:22px; margin-top:28px; } .pu-stat { border-top:2px solid var(--pu-ink); padding-top:14px; } .dark .pu-stat { border-top-color:#E8ECF1; } .pu-stat.pu-stat-neg { border-top-color: var(--pu-red); } .pu-stat-value { font:600 2.25rem/1.1 'Source Serif 4', Georgia, serif; color:var(--pu-ink) !important; font-variant-numeric: tabular-nums; } .dark .pu-stat-value { color:#E8ECF1 !important; } .pu-stat-neg .pu-stat-value { color: var(--pu-red) !important; } .pu-stat-name { font-size:1.45rem; letter-spacing:-.01em; } .pu-stat-label { margin-top:8px; font-size:.88rem; line-height:1.5; color:var(--pu-muted) !important; } .pu-stat-label b { color:inherit !important; font-weight:600; white-space:nowrap; } /* contain:inline-size → kutunun genisligi icerikten bagimsiz; tablo atalari genisletemez, kendi icinde kayar */ .pu-table-wrap { overflow-x:auto; width:100%; contain:inline-size; } .pu-table { width:100%; border-collapse:collapse; font-size:.9rem; border:0 !important; } .pu-table tr { border:0 !important; } /* Gradio her satira 4 yonlu kenarlik veriyor */ .pu-table th, .pu-table td { border-left:0 !important; border-right:0 !important; border-top:0 !important; } .pu-table th { text-align:left; font-weight:600; padding:8px 10px; border-bottom:1px solid var(--pu-line); white-space:nowrap; } .pu-table .pu-subhead th { font-weight:400; font-size:.78rem; color:var(--pu-muted); padding-top:0; text-align:center; } .pu-table td { padding:7px 9px; border-bottom:1px solid var(--pu-line) !important; vertical-align:middle; } .pu-num { text-align:right; font-variant-numeric: tabular-nums; } .pu-strong { font-weight:600; } .pu-rank { width:3em; color:var(--pu-muted); font-variant-numeric: tabular-nums; } .pu-band td:not(.pu-heat) { background: rgba(95,107,122,.06); } .pu-model { white-space:nowrap; } .pu-model a { color:inherit; text-decoration:none; font-weight:500; } .pu-meta { margin-top:2px; display:flex; gap:8px; align-items:center; } .pu-model a:hover { text-decoration:underline; } .pu-fam { font-size:.72rem; padding:1px 6px; border-radius:4px; border:1px solid var(--pu-line); color:var(--pu-muted); } .pu-fam-tür { border-color: rgba(200,16,46,.45); color: var(--pu-red); } .pu-params { font-size:.78rem; color:var(--pu-muted); } .pu-heat { background: rgba(200,16,46,var(--a)) !important; } .pu-na { color:var(--pu-muted); } .pu-note { color:var(--pu-muted); font-size:.85rem; margin-top:10px; } .pu-warn { border-left:3px solid var(--pu-red); padding:8px 12px; margin:0 0 14px !important; font-size:.9rem; background:rgba(200,16,46,.06); } .pu-pert { padding:12px 0; border-bottom:1px solid var(--pu-line); } .pu-pert-head { display:flex; gap:12px; align-items:baseline; flex-wrap:wrap; } .pu-pert-head span { color:var(--pu-muted); font-size:.85rem; } .pu-pert-text { margin:6px 0 4px; font-size:1.05rem; } .pu-pert-text mark { background: rgba(200,16,46,.16); color:inherit; border-radius:2px; padding:0 1px; } .pu-pert-text mark.pu-del { display:inline-block; width:2px; height:1.05em; padding:0; margin:0 1px; vertical-align:text-bottom; background: var(--pu-red); } .pu-pert-text em { color:var(--pu-muted); font-size:.85rem; } .pu-hint { font-size:.82rem; color:var(--pu-muted); } """ EXAMPLES = [["İstanbul'daki kargo şubesi nerede, ne zaman açılıyor?"], ["Kredi kartı borcumu nasıl yapılandırabilirim?"], ["Çocuğum ateşleniyor, ne yapacağım?"], ["Emeklilik fonu getirisi vergiye tabi mi?"], ["Şirketimin hisseleri neden düşüyor?"]] with gr.Blocks(title="Pusula — Türkçe retrieval dayanıklılık benchmark'ı", fill_width=True) as demo: gr.HTML(hero_html()) with gr.Tabs(): with gr.Tab("Sıralama"): with gr.Row(): view = gr.Radio(list(VIEWS), value="Mobil sorguda başarı", label="Sıralama ölçütü") ds = gr.Dropdown(DS_CHOICES, value=ALL, label="Veri seti") board = gr.HTML(board_html("Mobil sorguda başarı", ALL)) view.change(board_html, [view, ds], board) ds.change(board_html, [view, ds], board) with gr.Tab("Sorgunu boz"): seed = gr.State(0) with gr.Row(): q = gr.Textbox(label="Sorgu", value=EXAMPLES[0][0], lines=1, scale=5) again = gr.Button("Yeni rastgele sürüm", scale=1) gr.Examples(EXAMPLES, inputs=q, label="Örnekler") out = gr.HTML(perturb_html(EXAMPLES[0][0], 0)) q.change(perturb_html, [q, seed], out) again.click(next_seed, seed, seed).then(perturb_html, [q, seed], out) with gr.Tab("Yöntem"): gr.Markdown(method_md()) demo.launch(css=CSS, head=HEAD)