"""Tolerance-matched runtime comparison: BBQ (EM) vs Crowd-BT (online gradient). Both estimators are stopped by the SAME criterion -- the largest change in the log-scale item score over one full sweep of the data -- so the seconds-vs- minutes comparison of Figure 3 is measured rather than asserted. """ import json, math, time, sys import numpy as np import pandas as pd from bbq_vs_crowdbt import load_clic, load_humaine, encode, ALPHA0, BETA0, MU0, SIGMA_SQ0, KAPPA TOLS = [1e-2, 1e-3, 1e-4, 1e-5, 1e-6] def bbq_timed(i_idx, j_idx, r_idx, K, R, a=5.0, b=0.1, alpha=10.0, beta=2.0, max_iter=4000): lam = np.ones(K); q = np.full(R, 0.5) w = np.ones(i_idx.size) n_r = np.bincount(r_idx, minlength=R).astype(float) hit = {}; t0 = time.perf_counter(); mind = np.inf; ll_prev = None for it in range(max_iter): y = lam[i_idx] / (lam[i_idx] + lam[j_idx]) qc = q[r_idx] g = qc * y / (qc * y + (1.0 - qc) * 0.5) q_new = np.clip((np.bincount(r_idx, weights=g, minlength=R) + (alpha - 1.0)) / (n_r + alpha + beta - 2.0), 1e-12, 1 - 1e-12) wins = np.bincount(i_idx, weights=g, minlength=K) + (a - 1.0) den = np.zeros(K); s = lam[i_idx] + lam[j_idx] np.add.at(den, i_idx, g / s); np.add.at(den, j_idx, g / s) lam_new = wins / (den + b) shift = float(np.max(np.abs(np.log(lam_new) - np.log(lam)))) yi = lam_new[i_idx] / (lam_new[i_idx] + lam_new[j_idx]); qq = q_new[r_idx] ll = float(np.sum(np.log(qq * yi + (1 - qq) * 0.5)) + np.sum((a - 1) * np.log(lam_new) - b * lam_new) + np.sum((alpha - 1) * np.log(q_new) + (beta - 1) * np.log1p(-q_new))) if ll_prev is not None: mind = min(mind, ll - ll_prev) ll_prev = ll lam, q = lam_new, q_new for tol in TOLS: if tol not in hit and shift < tol: hit[tol] = (it + 1, time.perf_counter() - t0) if len(hit) == len(TOLS): break return lam, q, hit, it + 1, time.perf_counter() - t0, mind def crowdbt_timed(i_idx, j_idx, r_idx, K, R, max_epochs=600, seed=0): mu = np.full(K, MU0); ss = np.full(K, SIGMA_SQ0) al = np.full(R, ALPHA0); be = np.full(R, BETA0) rng = np.random.default_rng(seed) order = np.arange(i_idx.size) hit = {}; t0 = time.perf_counter(); traj = [] ii = i_idx.tolist(); jj = j_idx.tolist(); rr = r_idx.tolist() for ep in range(max_epochs): rng.shuffle(order) prev = mu.copy() for t in order: w = ii[t]; l = jj[t]; k = rr[t] mw = mu[w]; ml = mu[l]; sw = ss[w]; sl = ss[l] a = al[k]; b = be[k] m = mw - ml c1 = 1.0 if m > 30 else (0.0 if m < -30 else 1.0 / (1.0 + math.exp(-m))) c2 = 1.0 - c1 ab = a + b dnm = a * c1 + b * c2 if dnm <= 0: continue d = (a - b) * c1 * c2 / dnm mu[w] = mw + sw * d mu[l] = ml - sl * d h = (a - b) * c1 * c2 * (c2 - c1) / dnm - d * d fac = h - d * d ss[w] = sw * max(1.0 + sw * fac, KAPPA) ss[l] = sl * max(1.0 + sl * fac, KAPPA) f = a * c1 / dnm e1 = (f * (a + 1.0) + (1.0 - f) * a) / (ab + 1.0) e2 = (f * (a + 1.0) * (a + 2.0) + (1.0 - f) * a * (a + 1.0)) / ((ab + 1.0) * (ab + 2.0)) v = e2 - e1 * e1 if v > 1e-12 and e1 > e2 and 0.0 < e1 < 1.0: an = e1 * (e1 - e2) / v; bn = (1.0 - e1) * (e1 - e2) / v if an > 0 and bn > 0 and math.isfinite(an) and math.isfinite(bn): al[k] = min(an, 1e6); be[k] = min(bn, 1e6) shift = float(np.max(np.abs(mu - prev))) traj.append(shift) for tol in TOLS: if tol not in hit and shift < tol: hit[tol] = (ep + 1, time.perf_counter() - t0) if len(hit) == len(TOLS): break return mu, al / (al + be), hit, ep + 1, time.perf_counter() - t0, traj def run(name, df, max_epochs): i, j, r, K, R = encode(df) lam, q, bh, bi, bt, mind = bbq_timed(i, j, r, K, R) mu, eta, ch, ce, ct, traj = crowdbt_timed(i, j, r, K, R, max_epochs=max_epochs) from scipy.stats import kendalltau out = dict(dataset=name, comparisons=int(i.size), items=int(K), raters=int(R), bbq_total_iters=bi, bbq_total_secs=round(bt, 4), bbq_min_logpost_delta=mind, bbq_hit={str(k): [v[0], round(v[1], 4)] for k, v in bh.items()}, cbt_total_epochs=ce, cbt_total_secs=round(ct, 3), cbt_hit={str(k): [v[0], round(v[1], 3)] for k, v in ch.items()}, cbt_final_shift=round(traj[-1], 8), kendall_bbq_vs_cbt=round(float(kendalltau(np.log(lam), mu).statistic), 4)) print(json.dumps(out), flush=True) return out if __name__ == "__main__": which = sys.argv[1] out = [] if which == "small": for s in ["screened", "unscreened"]: out.append(run("IHQ-" + s, load_clic(s), 4000)) out.append(run("IHQ-all", pd.concat([load_clic("screened"), load_clic("unscreened")], ignore_index=True), 4000)) elif which == "mtbench": from datasets import load_dataset ds = load_dataset("lmsys/mt_bench_human_judgments") rows = [] for split in ds: for ex in ds[split]: w = ex.get("winner") if w == "model_a": rows.append(dict(rater=str(ex.get("judge")), winner=ex["model_a"], loser=ex["model_b"])) elif w == "model_b": rows.append(dict(rater=str(ex.get("judge")), winner=ex["model_b"], loser=ex["model_a"])) out.append(run("MT-Bench", pd.DataFrame(rows), 4000)) else: h = load_humaine() sub = h.sample(n=105220, random_state=20260802).reset_index(drop=True) out.append(run("HUMAINE-105220", sub, 600)) with open("timing2_%s.json" % which, "w") as f: json.dump(out, f, indent=1)