NinueNAODD-repro / code /timing2.py
ProCreations's picture
Claims 2 and 6: executed BBQ vs Crowd-BT timing and rater-quality correlation
60da8fb verified
Raw
History Blame Contribute Delete
6.13 kB
"""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)