File size: 5,884 Bytes
d3fd67f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
137
138
139
140
141
142
"""Tune every method's knobs on dev splits only (scifact.train, nfcorpus.dev, fiqa.dev; one global
setting per method), then evaluate on the five BEIR test splits.

Usage: python run_eval.py [--quick]
"""
import itertools
import json
import os
import sys
import time

import numpy as np

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
from common import OUT, evaluate, load_qrels, order_to_run  # noqa: E402
from infomax_rerank import by_score, fused, infomax, mmr, zfuse  # noqa: E402

DEV = ["scifact.train", "nfcorpus.dev", "fiqa.dev"]
TEST = ["scifact.test", "nfcorpus.test", "fiqa.test", "scidocs.test", "trec-covid.test"]


def load(tag):
    name, split = tag.split(".")
    z = np.load(os.path.join(OUT, f"{tag}.scores.npz"))
    qs = []
    for q in z["qids"]:
        qs.append({k: z[f"{q}|{k}"] for k in ("ids", "bm25", "ce", "emb", "qemb")} | {"qid": str(q)})
    return qs, load_qrels(name, split)


def score(Q, qrels, orders):
    run = {x["qid"]: order_to_run([x["ids"][j] for j in o]) for x, o in zip(Q, orders)}
    m, per = evaluate(run, qrels)
    return m, {q: v["ndcg_cut_10"] for q, v in per.items()}


def method_orders(Q, method, p):
    if method == "BM25":
        return [by_score(x["bm25"]) for x in Q]
    if method == "Dense":
        return [by_score(x["emb"] @ x["qemb"]) for x in Q]
    if method == "CE":
        return [by_score(x["ce"]) for x in Q]
    if method == "MMR":
        return [mmr(x["ce"], x["emb"], **p) for x in Q]
    if method == "Fusion":
        return [zfuse(x["ce"], x["emb"], x["qemb"], **p) for x in Q]
    if method == "InfoMax":
        return [infomax(x["ce"], x["emb"], x["qemb"], **p) for x in Q]
    if method == "InfoMax+F":   # same selector, relevance/anchor = the tuned CE+dense fusion score
        p = dict(p)
        a = p.pop("alpha")
        return [infomax(fused(x["ce"], x["emb"], x["qemb"], a), x["emb"], x["qemb"], **p) for x in Q]
    raise ValueError(method)


GRIDS = {
    "MMR": [{"lam": l} for l in (0.5, 0.6, 0.7, 0.8, 0.9, 0.95)],
    "Fusion": [{"alpha": a} for a in (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9)],
    "InfoMax": [dict(rank=r, gamma=g, rho=rh, sigma2=s, beta=b, delta=d)
                for r, g, rh, s, b, d in itertools.product(
                    (1, 4, 16), (1.0, 2.0, 4.0), (0.3, 1.0), (0.03, 0.3), (0.0, 0.3, 3.0),
                    (0.0, 0.5, 2.0, np.inf))],
    "InfoMax+F": [dict(rank=r, gamma=g, rho=1.0, sigma2=s, beta=b, delta=d)
                  for r, g, s, b, d in itertools.product(
                      (1, 4, 16), (1.0, 2.0, 4.0, 8.0), (0.03, 0.3), (0.0, 0.3, 3.0), (0.0, 0.5, 2.0, np.inf))],
}


def boot_ci(a, b, n=10000, seed=0):
    d = np.asarray(a) - np.asarray(b)
    rng = np.random.default_rng(seed)
    m = d[rng.integers(0, len(d), (n, len(d)))].mean(1)
    return float(d.mean()), float(np.percentile(m, 2.5)), float(np.percentile(m, 97.5))


def main():
    quick = "--quick" in sys.argv
    best = {}
    bp = os.path.join(OUT, "best_params.json")
    if "--test" in sys.argv:
        best = {k: {kk: (np.inf if vv is None else vv) for kk, vv in v.items()}
                for k, v in json.load(open(bp)).items()}
        grids = {}
    else:
        dev = {t: load(t) for t in DEV}
        grids = GRIDS
    for meth, grid in grids.items():
        if meth == "InfoMax+F":
            grid = [p | {"alpha": best["Fusion"]["alpha"]} for p in grid]
        if quick:
            grid = grid[:4]
        t0 = time.time()
        res = []
        for p in grid:
            v = np.mean([score(Q, qr, method_orders(Q, meth, p))[0]["nDCG@10"] for Q, qr in dev.values()])
            res.append((v, p))
        res.sort(key=lambda t: -t[0])
        best[meth] = res[0][1]
        ce_dev = np.mean([score(Q, qr, method_orders(Q, "CE", {}))[0]["nDCG@10"] for Q, qr in dev.values()])
        print(f"[tune] {meth}: best dev nDCG@10 {res[0][0]:.4f} (CE {ce_dev:.4f}) with {res[0][1]}  "
              f"[{len(grid)} settings, {time.time()-t0:.0f}s]", flush=True)
        if meth.startswith("InfoMax"):
            for v, p in res[:5]:
                print(f"        {v:.4f} {p}")
    if "--test" not in sys.argv:
        json.dump({k: {kk: (None if vv == np.inf else vv) for kk, vv in v.items()} for k, v in best.items()},
                  open(bp, "w"), indent=1)
        return

    methods = ["BM25", "Dense", "CE", "MMR", "Fusion", "InfoMax", "InfoMax+F"]
    table, perq = {}, {}
    for tag in TEST:
        Q, qr = load(tag)
        for meth in methods:
            o = method_orders(Q, meth, best.get(meth, {}))
            m, pq = score(Q, qr, o)
            table[(tag, meth)] = m
            perq[(tag, meth)] = pq
    print("\nnDCG@10 on test (BM25 top-50 pools; knobs tuned on dev only)")
    print(f"{'dataset':16s}" + "".join(f"{m:>9s}" for m in methods))
    for tag in TEST:
        print(f"{tag:16s}" + "".join(f"{table[(tag, m)]['nDCG@10']:9.4f}" for m in methods))
    print("\nInfoMax+F minus X, nDCG@10, paired bootstrap 95% CI")
    rows = {}
    for tag in TEST:
        qids = sorted(perq[(tag, "CE")])
        for other in ("CE", "MMR", "Fusion"):
            a = [perq[(tag, "InfoMax+F")][q] for q in qids]
            b = [perq[(tag, other)][q] for q in qids]
            rows[(tag, other)] = boot_ci(a, b)
        print(f"{tag:16s}" + "  ".join(f"vs {o}: {rows[(tag, o)][0]:+.4f} [{rows[(tag, o)][1]:+.4f},{rows[(tag, o)][2]:+.4f}]"
                                       for o in ("CE", "MMR", "Fusion")))
    json.dump({"best": {k: {kk: (None if vv == np.inf else vv) for kk, vv in v.items()} for k, v in best.items()},
               "test": {f"{t}|{m}": v for (t, m), v in table.items()},
               "ci_infomax_minus": {f"{t}|{o}": v for (t, o), v in rows.items()}},
              open(os.path.join(OUT, "results.json"), "w"), indent=1)


if __name__ == "__main__":
    main()