File size: 6,132 Bytes
60da8fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
"""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)