Avo-k's picture
Explorateur pass@k · pass+50% · pass^k (concept + SWE-bench Lite)
67131e7 verified
Raw
History Blame Contribute Delete
15 kB
# /// 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()