agent-eval-passk / bench.py
Avo-k's picture
Explorateur pass@k · pass+50% · pass^k (concept + SWE-bench Lite)
67131e7 verified
Raw
History Blame Contribute Delete
6.81 kB
"""Vue « benchmark réel » : agrégation des métriques sur T tâches + fiabilité (IC).
Données : SWE-bench Lite Samples (Large Language Monkeys). Pour chaque tâche on a
(n_i ≈ 250, c_i) -> taux estimé p_i = c_i / n_i.
Courbes agrégées (modèle i.i.d. par tâche, moyenne sur les tâches), pour k = 1..K :
pass@k = moyenne_i [ 1 - (1-p_i)^k ] (best-of-k, suppose un oracle)
pass+50% = moyenne_i [ P(Binom(k, p_i) > k/2) ] (vote majoritaire)
pass^k = moyenne_i [ p_i^k ] (fiabilité : k réussites d'affilée)
« Le maximum » = les deux plafonds quand k -> +inf :
couverture (oracle) = fraction de tâches résolubles = moyenne_i [ p_i > 0 ]
plafond vote = fraction de tâches fiables = moyenne_i [ p_i > 0.5 ]
Fiabilité de l'estimation avec un budget (n échantillons/tâche, T tâches) : bootstrap.
On rejoue l'évaluation B fois — T tâches tirées sans remise parmi les ~300, et n
échantillons retirés sans remise des ~250 disponibles (loi hypergéométrique) — puis on
regarde la dispersion des plafonds estimés. Plus n et T grandissent, plus l'intervalle
de confiance se resserre (et le biais du plafond oracle, sous-estimé à petit n car on
rate les réussites rares, s'efface).
"""
from __future__ import annotations
import json
import os
from math import comb
import numpy as np
DATA = os.path.join(os.path.dirname(__file__), "data", "swebench_lite_samples.json")
def load_dataset(path: str = DATA) -> dict:
with open(path) as f:
d = json.load(f)
N = np.array([t["n"] for t in d["tasks"]], dtype=float)
C = np.array([t["c"] for t in d["tasks"]], dtype=float)
return {
"N": N, "C": C, "P": C / N,
"task_ids": [t["task_id"] for t in d["tasks"]],
"n_tasks": int(d["n_tasks"]),
"n_max": int(N.max()), "n_min": int(N.min()),
"model": d["model"], "benchmark": d["benchmark"],
"paper": d.get("paper", ""), "source": d.get("source", ""),
}
def _maj_iid_matrix(P: np.ndarray, K: int) -> np.ndarray:
"""(T, K) : P(Binom(k, p_i) > k/2) pour k = 1..K, vectorisé sur les tâches."""
P = np.asarray(P, float)
out = np.empty((P.shape[0], K))
for ki in range(1, K + 1):
js = np.arange(ki // 2 + 1, ki + 1) # majorité stricte
coeff = np.array([comb(ki, int(j)) for j in js], float)
pj = P[:, None] ** js[None, :]
qj = (1.0 - P)[:, None] ** (ki - js)[None, :]
out[:, ki - 1] = (coeff[None, :] * pj * qj).sum(axis=1)
return out
def aggregate_curves(P: np.ndarray, K: int) -> dict:
"""Courbes moyennes (et écart-type sur les tâches) pour k = 1..K."""
P = np.asarray(P, float)
ks = np.arange(1, K + 1)
mats = {
"at": 1.0 - (1.0 - P)[:, None] ** ks[None, :],
"maj": _maj_iid_matrix(P, K),
"pow": P[:, None] ** ks[None, :],
}
res = {"ks": ks}
for name, M in mats.items():
res[f"{name}_mean"] = M.mean(axis=0)
res[f"{name}_std"] = M.std(axis=0)
return res
def task_ci_halfwidth(std: np.ndarray, T: int, T_full: int, z: float = 1.645) -> np.ndarray:
"""Demi-largeur d'IC due à l'échantillonnage des tâches (CLT + correction pop. finie).
Vaut z·std/sqrt(T) corrigé par sqrt(1 - T/T_full) -> 0 quand on prend toutes les tâches.
"""
fpc = max(0.0, 1.0 - T / T_full)
return z * np.asarray(std) * np.sqrt(fpc / max(T, 1))
def ceilings_true(P: np.ndarray) -> dict:
"""Valeurs « vraies » (toutes les données) : plafonds + score single-shot."""
P = np.asarray(P, float)
return {
"coverage": float((P > 0).mean()), # best-of-inf (oracle)
"voting": float((P > 0.5).mean()), # vote majoritaire à l'infini
"pass1": float(P.mean()), # pass@1 moyen
}
def ceiling_bootstrap(N, C, n: int, T: int, B: int = 400, seed: int = 0) -> dict:
"""IC des deux plafonds pour un budget (n échantillons/tâche, T tâches).
seed fixe -> résultat déterministe (la bande ne « tremble » pas en déplaçant un curseur).
"""
N = np.asarray(N, float)
C = np.asarray(C, float)
T_full = N.shape[0]
n, T, B = int(n), int(min(T, T_full)), int(B)
rng = np.random.default_rng(seed)
idx = np.argsort(rng.random((B, T_full)), axis=1)[:, :T] # T tâches sans remise / rep
Nsub, Csub = N[idx], C[idx]
n_eff = np.minimum(n, Nsub).astype(int) # budget effectif <= n_i
cprime = rng.hypergeometric(Csub.astype(int),
(Nsub - Csub).astype(int), n_eff) # (B, T)
cov = (cprime > 0).mean(axis=1) # couverture estimée par rep
vote = (cprime / n_eff > 0.5).mean(axis=1) # plafond vote estimé par rep
def stat(x):
return {"mean": float(x.mean()),
"lo": float(np.percentile(x, 5)),
"hi": float(np.percentile(x, 95))}
return {"coverage": stat(cov), "voting": stat(vote)}
if __name__ == "__main__":
ds = load_dataset()
P, N, C = ds["P"], ds["N"], ds["C"]
assert ds["n_tasks"] == 300
tr = ceilings_true(P)
print(f"couverture (oracle) = {tr['coverage']:.3f} "
f"plafond vote = {tr['voting']:.3f} pass@1 moyen = {tr['pass1']:.3f}")
assert abs(tr["coverage"] - 168 / 300) < 1e-9
assert abs(tr["voting"] - 38 / 300) < 1e-9
K = ds["n_min"]
cur = aggregate_curves(P, K)
# En k=1, les 3 métriques valent pass@1 moyen.
for m in ("at", "maj", "pow"):
assert abs(cur[f"{m}_mean"][0] - tr["pass1"]) < 1e-9
# Encadrement pass^k <= pass+50% <= pass@k, partout.
assert np.all(cur["pow_mean"] <= cur["maj_mean"] + 1e-9)
assert np.all(cur["maj_mean"] <= cur["at_mean"] + 1e-9)
# Monotonie.
assert np.all(np.diff(cur["at_mean"]) >= -1e-9)
assert np.all(np.diff(cur["pow_mean"]) <= 1e-9)
# pass@k ne dépasse jamais la couverture (best-of-K <= best-of-inf).
assert cur["at_mean"][-1] <= tr["coverage"] + 1e-9
# Budget max -> bootstrap = valeurs vraies, IC nul.
bmax = ceiling_bootstrap(N, C, n=ds["n_max"], T=ds["n_tasks"], B=200)
assert abs(bmax["coverage"]["mean"] - tr["coverage"]) < 1e-9
assert bmax["coverage"]["hi"] - bmax["coverage"]["lo"] < 1e-9
# Petit n -> couverture sous-estimée (on rate les réussites rares).
blow = ceiling_bootstrap(N, C, n=3, T=ds["n_tasks"], B=400)
print(f"couverture estimée à n=3 : {blow['coverage']['mean']:.3f} "
f"[{blow['coverage']['lo']:.3f}, {blow['coverage']['hi']:.3f}]")
assert blow["coverage"]["mean"] < tr["coverage"]
# Peu de tâches -> IC plus large.
w_fewT = ceiling_bootstrap(N, C, n=ds["n_max"], T=20, B=400)["coverage"]
assert (w_fewT["hi"] - w_fewT["lo"]) > 0.02
print("bench.py : tous les tests passent ✔")