Spaces:
Sleeping
Sleeping
| # /// 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}<br>limite i.i.d.=%{y:.3f}<extra></extra>")) | |
| # 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}<br><b>%{y:.3f}</b><extra></extra>")) | |
| 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}<br><b>%{y:.3f}</b><extra></extra>")) | |
| # 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']}<br>" | |
| f"<sup>T = {T} tâches · n = {n} échantillons/tâche</sup>", | |
| 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() | |