# /// script # requires-python = ">=3.10" # dependencies = [ # "gradio>=5", # "plotly>=5.20", # "numpy>=1.24", # ] # /// """Explorateur pass@k / pass+50% / pass^k (Gradio), pensé pour un HuggingFace Space. Deux onglets : 1. Concept — un problème, les trois variables n, c, k, estimateurs exacts (HumanEval). 2. Benchmark réel — vraies données agentiques (SWE-bench Lite, ~250 essais × 300 tâches) : varier le nombre de tâches T et le budget d'échantillons n, et voir avec quelle fiabilité on estime les deux « maximums » (plafond oracle best-of-k et plafond vote). """ from __future__ import annotations import numpy as np import plotly.graph_objects as go import gradio as gr import bench from metrics import curves, pass_at_k, pass_maj_k, pass_pow_k # Couleurs des 3 métriques (mêmes dans les deux onglets) C_AT = "#2563eb" # bleu -> pass@k (best-of-k, croissant) C_MAJ = "#16a34a" # vert -> pass+50% (vote majoritaire, au milieu) C_POW = "#dc2626" # rouge -> pass^k (fiabilité, décroissant) C_GUIDE = "#94a3b8" DS = bench.load_dataset() # chargé une fois T_FULL = DS["n_tasks"] N_MAX = DS["n_max"] def _rgba(hex_color: str, a: float) -> str: h = hex_color.lstrip("#") r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) return f"rgba({r},{g},{b},{a})" def _band(fig, x, lo, hi, hex_color): """Ajoute une bande translucide [lo, hi] sous une courbe.""" fig.add_trace(go.Scatter( x=np.concatenate([x, x[::-1]]), y=np.concatenate([np.clip(hi, 0, 1), np.clip(lo, 0, 1)[::-1]]), fill="toself", fillcolor=_rgba(hex_color, 0.13), line=dict(width=0), hoverinfo="skip", showlegend=False)) # ====================================================================== # ONGLET 1 — CONCEPT (un problème, estimateurs exacts) # ====================================================================== NC0, CC0, KC0 = 20, 6, 5 def fig_concept(n, c, k): ks, at_k, maj_k, pow_k, at_iid, maj_iid, pow_iid, p = curves(n, c) fig = go.Figure() # Limites i.i.d. (pointillés, en arrière-plan) for y, col in [(at_iid, C_AT), (maj_iid, C_MAJ), (pow_iid, C_POW)]: fig.add_trace(go.Scatter(x=ks, y=y, mode="lines", showlegend=False, line=dict(color=col, width=1.4, dash="dot"), opacity=0.45, hovertemplate="k=%{x}
limite i.i.d.=%{y:.3f}")) # Estimateurs HumanEval (traits pleins) for y, col, nm in [(at_k, C_AT, "pass@k (≥1 correct)"), (maj_k, C_MAJ, "pass+50% (vote majoritaire)"), (pow_k, C_POW, "pass^k (tous corrects)")]: fig.add_trace(go.Scatter(x=ks, y=y, mode="lines", name=nm, line=dict(color=col, width=3.2), hovertemplate="k=%{x}
%{y:.3f}")) yv = [pass_at_k(n, c, k), pass_maj_k(n, c, k), pass_pow_k(n, c, k)] fig.add_vline(x=k, line=dict(color=C_GUIDE, width=1.5, dash="dash")) fig.add_trace(go.Scatter(x=[k, k, k], y=yv, mode="markers+text", marker=dict(size=11, color=[C_AT, C_MAJ, C_POW], line=dict(color="white", width=2)), text=[f" {v:.3f}" for v in yv], textposition="middle right", showlegend=False, hoverinfo="skip")) fig.update_layout( template="plotly_white", title=dict(text=f"n = {n} · c = {c} · p = c/n = {p:.3f}", x=0.5, xanchor="center"), xaxis_title="k (taille du tirage)", yaxis_title="probabilité", xaxis=dict(range=[1, max(n, 2)], dtick=max(1, n // 10)), yaxis=dict(range=[-0.03, 1.03]), legend=dict(orientation="h", yanchor="bottom", y=1.04, x=0, font=dict(size=11)), margin=dict(l=60, r=30, t=90, b=55), height=470) return fig def summary_concept(n, c, k): p = c / n if n else 0.0 a, m, w = pass_at_k(n, c, k), pass_maj_k(n, c, k), pass_pow_k(n, c, k) return (f"### Au tirage **k = {k}**\n" f"| métrique | valeur | lecture |\n|---|---|---|\n" f"| **pass@{k}** | **{a:.4f}** | au moins **1** des {k} essais réussit (*best-of-{k}*) |\n" f"| **pass+50%** | **{m:.4f}** | **majorité** (>{k//2}) des {k} corrects → le **vote** gagne |\n" f"| **pass^{k}** | **{w:.4f}** | les **{k}** réussissent **tous** (*fiabilité*) |\n" f"| p = c/n | {p:.4f} | = les trois en k=1 |\n") MODE_PCT = "% de n" # doit correspondre au libellé du gr.Radio def _c_abs(n, c_raw, mode): """c absolu : en mode %, c_raw est un pourcentage de n ; sinon un compte.""" n = max(int(n), 1) if mode == MODE_PCT: return min(max(int(round(c_raw / 100 * n)), 0), n) return min(max(int(c_raw), 0), n) def render_concept(n, c_raw, k, mode): n = max(int(n), 1) c = _c_abs(n, c_raw, mode) k = min(max(int(k), 1), n) return fig_concept(n, c, k), summary_concept(n, c, k) def on_n_concept(n, c_raw, k, lock_p, prev_n, mode): n = max(int(n), 1) if mode == MODE_PCT: c = _c_abs(n, c_raw, mode) # le % reste fixe -> p constant c_update = gr.update() # le curseur (en %) ne bouge pas else: if lock_p and prev_n: c_raw = round((c_raw / prev_n) * n) c = min(max(int(c_raw), 0), n) c_update = gr.update(maximum=n, value=c) k = min(max(int(k), 1), n) return c_update, gr.update(maximum=n, value=k), fig_concept(n, c, k), summary_concept(n, c, k), n def on_mode_concept(new_mode, n, c_raw, k): """Bascule nombre <-> % : reconfigure le curseur c en gardant le c absolu courant.""" n = max(int(n), 1) old_mode = "nombre" if new_mode == MODE_PCT else MODE_PCT # toggle à 2 valeurs c = _c_abs(n, c_raw, old_mode) k = min(max(int(k), 1), n) if new_mode == MODE_PCT: pct = int(round(c / n * 100)) if n else 0 c_update = gr.update(minimum=0, maximum=100, value=pct, label="c — % de réussite (p = c/n)") lock_update = gr.update(visible=False) # le mode % garde p constant tout seul else: c_update = gr.update(minimum=0, maximum=n, value=c, label="c — échantillons corrects") lock_update = gr.update(visible=True) return c_update, lock_update, fig_concept(n, c, k), summary_concept(n, c, k) # ====================================================================== # ONGLET 2 — BENCHMARK RÉEL (agrégation + fiabilité) # ====================================================================== NB0, TB0, KB0 = 50, T_FULL, 10 def fig_bench(n, T, k): n, T, k = int(n), int(T), int(k) cur = bench.aggregate_curves(DS["P"], n) ks = cur["ks"] tr = bench.ceilings_true(DS["P"]) boot = bench.ceiling_bootstrap(DS["N"], DS["C"], n, T) series = [("at", C_AT, "pass@k (best-of-k)"), ("maj", C_MAJ, "pass+50% (vote)"), ("pow", C_POW, "pass^k (fiabilité)")] fig = go.Figure() # Bandes d'incertitude (échantillonnage des T tâches) puis courbes moyennes. for key, col, _ in series: half = bench.task_ci_halfwidth(cur[f"{key}_std"], T, T_FULL) _band(fig, ks, cur[f"{key}_mean"] - half, cur[f"{key}_mean"] + half, col) for key, col, nm in series: fig.add_trace(go.Scatter(x=ks, y=cur[f"{key}_mean"], mode="lines", name=nm, line=dict(color=col, width=3.2), hovertemplate="k=%{x}
%{y:.3f}")) # Les deux plafonds (« le maximum ») : ligne = vrai, bande = IC au budget (n, T). for key, col, label in [("coverage", C_AT, "plafond oracle"), ("voting", C_MAJ, "plafond vote")]: st = boot[key] fig.add_hrect(y0=st["lo"], y1=st["hi"], line_width=0, fillcolor=_rgba(col, 0.10)) fig.add_hline(y=tr[key], line=dict(color=col, width=1.6, dash="dash"), annotation_text=f" {label} (vrai {tr[key]:.0%})", annotation_position="right", annotation_font_color=col) yv = [cur["at_mean"][k - 1], cur["maj_mean"][k - 1], cur["pow_mean"][k - 1]] fig.add_vline(x=k, line=dict(color=C_GUIDE, width=1.5, dash="dash")) fig.add_trace(go.Scatter(x=[k, k, k], y=yv, mode="markers", marker=dict(size=10, color=[C_AT, C_MAJ, C_POW], line=dict(color="white", width=2)), showlegend=False, hoverinfo="skip")) fig.update_layout( template="plotly_white", title=dict(text=f"{DS['model']} · {DS['benchmark']}
" f"T = {T} tâches · n = {n} échantillons/tâche", x=0.5, xanchor="center"), xaxis_title="k (essais utilisés par tâche)", yaxis_title="taux moyen sur les tâches", xaxis=dict(range=[1, max(n, 2)], dtick=max(1, n // 10)), yaxis=dict(range=[-0.03, 1.03]), legend=dict(orientation="h", yanchor="bottom", y=1.04, x=0, font=dict(size=11)), margin=dict(l=60, r=110, t=95, b=55), height=470) return fig def summary_bench(n, T, k): n, T, k = int(n), int(T), int(k) cur = bench.aggregate_curves(DS["P"], n) tr = bench.ceilings_true(DS["P"]) boot = bench.ceiling_bootstrap(DS["N"], DS["C"], n, T) def row(label, st, true_v): return (f"| {label} | **{st['mean']:.1%}** [{st['lo']:.1%}, {st['hi']:.1%}] " f"| {true_v:.1%} |\n") a, m, w = cur["at_mean"][k - 1], cur["maj_mean"][k - 1], cur["pow_mean"][k - 1] return ( f"### « Le maximum » estimé avec **n = {n}**, **T = {T}**\n" f"| plafond (k→∞) | estimé au budget (IC 90 %) | vrai (toutes données) |\n" f"|---|---|---|\n" + row("couverture best-of-k · *oracle*", boot["coverage"], tr["coverage"]) + row("plafond **vote** majoritaire", boot["voting"], tr["voting"]) + f"\nAugmente **n** et **T** → l'IC se resserre vers la vraie valeur " f"(à petit n la couverture est sous-estimée : on rate les réussites rares).\n\n" f"**Au k = {k}** : pass@{k} = {a:.1%} · vote = {m:.1%} · pass^{k} = {w:.2%} " f"— *single-shot (pass@1 moyen) = {tr['pass1']:.1%}*" ) def render_bench(n, T, k): n = max(int(n), 1) T = min(max(int(T), 1), T_FULL) k = min(max(int(k), 1), n) return fig_bench(n, T, k), summary_bench(n, T, k) def on_n_bench(n, T, k): n = max(int(n), 1) k = min(max(int(k), 1), n) f, md = render_bench(n, T, k) return gr.update(maximum=n, value=k), f, md # ====================================================================== # INTERFACE # ====================================================================== LATEX = [{"left": "$$", "right": "$$", "display": True}, {"left": "$", "right": "$", "display": False}] EVT = dict(show_progress="hidden", trigger_mode="always_last") INTRO_CONCEPT = r""" # 📈 pass@k · pass+50% · pass^k Évaluer un agent / LLM : on génère **n** échantillons pour une tâche, **c** sont corrects. Trois questions, trois métriques (estimateurs *sans remise*, cf. [HumanEval](https://github.com/openai/human-eval/blob/master/human_eval/evaluation.py)) : $$\text{pass@}k = 1-\frac{\binom{n-c}{k}}{\binom{n}{k}} \quad\; \text{pass+50\%} = \!\!\sum_{j>k/2}\!\frac{\binom{c}{j}\binom{n-c}{k-j}}{\binom{n}{k}} \quad\; \text{pass}^{k} = \frac{\binom{c}{k}}{\binom{n}{k}}$$ - **pass@k** (bleu) — au moins **1** des k essais réussit (*best-of-k*, suppose qu'on sait choisir le bon). **Croît.** - **pass+50%** (vert) — la **majorité** des k essais réussit → le **vote** gagne, sans vérificateur. Toujours **entre** les deux. - **pass^k** (rouge) — les **k** réussissent **tous** d'affilée (*fiabilité*). **Décroît.** Traits pleins = estimateurs sur n échantillons ; pointillés = limite i.i.d. Coche « garder p constant » et augmente **n** pour voir l'estimateur rejoindre la limite. """ INTRO_BENCH = f""" # 🧪 Sur de vraies données agentiques **{DS['model']}** sur **{DS['benchmark']}** — {T_FULL} tâches, jusqu'à {N_MAX} essais chacune ([{DS['paper']}]({DS['source']})). Les courbes sont la **moyenne sur les tâches**. Fais varier **T** (nombre de tâches) et **n** (budget d'essais par tâche) : les **bandes** autour des courbes et autour des deux **plafonds** montrent *à quel point on estime fiablement* le maximum atteignable — par un **oracle** (best-of-k) ou par **vote** majoritaire. """ with gr.Blocks(title="pass@k · pass+50% · pass^k") as demo: with gr.Tabs(): # ---------------- Onglet Concept ---------------- with gr.Tab("Concept (la formule)"): prev_n = gr.State(NC0) gr.Markdown(INTRO_CONCEPT, latex_delimiters=LATEX) with gr.Row(): with gr.Column(scale=2): n_c = gr.Slider(1, 200, NC0, step=1, label="n — échantillons générés") c_c = gr.Slider(0, NC0, CC0, step=1, label="c — échantillons corrects") mode_c = gr.Radio(["nombre", "% de n"], value="nombre", label="exprimer c en") k_c = gr.Slider(1, NC0, KC0, step=1, label="k — taille du tirage") lock = gr.Checkbox(False, label="Garder p = c/n constant quand n change", info="pour voir l'estimateur converger vers la limite i.i.d.") sum_c = gr.Markdown() with gr.Column(scale=5): plot_c = gr.Plot(label="pass@k · pass+50% · pass^k en fonction de k") in_c = [n_c, c_c, k_c, mode_c] c_c.change(render_concept, in_c, [plot_c, sum_c], **EVT) k_c.change(render_concept, in_c, [plot_c, sum_c], **EVT) n_c.change(on_n_concept, [n_c, c_c, k_c, lock, prev_n, mode_c], [c_c, k_c, plot_c, sum_c, prev_n], **EVT) mode_c.change(on_mode_concept, [mode_c, n_c, c_c, k_c], [c_c, lock, plot_c, sum_c], **EVT) # ---------------- Onglet Benchmark ---------------- with gr.Tab("Benchmark réel (SWE-bench Lite)"): gr.Markdown(INTRO_BENCH) with gr.Row(): with gr.Column(scale=2): T_b = gr.Slider(1, T_FULL, TB0, step=1, label="T — nombre de tâches") n_b = gr.Slider(1, N_MAX, NB0, step=1, label="n — échantillons / tâche (budget)") k_b = gr.Slider(1, N_MAX, KB0, step=1, label="k — essais utilisés / tâche") sum_b = gr.Markdown() with gr.Column(scale=5): plot_b = gr.Plot(label="Métriques agrégées + plafonds (avec IC)") T_b.change(render_bench, [n_b, T_b, k_b], [plot_b, sum_b], **EVT) k_b.change(render_bench, [n_b, T_b, k_b], [plot_b, sum_b], **EVT) n_b.change(on_n_bench, [n_b, T_b, k_b], [k_b, plot_b, sum_b], **EVT) demo.load(render_concept, in_c, [plot_c, sum_c]) demo.load(render_bench, [n_b, T_b, k_b], [plot_b, sum_b]) if __name__ == "__main__": demo.launch()