Angshul commited on
Commit
d3fd67f
·
verified ·
1 Parent(s): 012b961

Upload 14 files

Browse files
METHOD.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Method
2
+
3
+ For one query with candidate passages $d_1,\dots,d_n$:
4
+
5
+ - $r_j$: cross-encoder logit (InfoMax) or z-score fusion of cross-encoder logit and dense cosine (InfoMax+F).
6
+ - $e_j$: all-MiniLM-L6-v2 embedding, centred on the pool mean and renormalised; $q$ the query embedding, treated the same way.
7
+ - $w_j = \mathrm{minmax}(r)_j^{\gamma}$: relevance weight.
8
+
9
+ **Target.** A latent query-information vector $z \in \mathbb{R}^r$, with prior $z \sim \mathcal N(0,\Lambda)$, on the aspect basis
10
+ $U = [\,q,\ u_2,\dots,u_r\,]$. Here $u_2,\dots,u_r$ are the top right-singular vectors of the relevance-weighted pool embeddings
11
+ after removing the query direction, and $\Lambda = \mathrm{diag}(1, \rho\, s_k^2/s_1^2)$.
12
+
13
+ **Observations.** Each passage is a noisy linear measurement of the target:
14
+
15
+ $$y_j = \sqrt{w_j}\,(U^\top e_j)^\top z + g_j^\top u + \varepsilon_j,\qquad g_j = (I-UU^\top)e_j .$$
16
+
17
+ The shared nuisance $u \sim \mathcal N(0,\beta I)$ correlates the noise of passages with similar off-query content (duplicates,
18
+ same source), and $\varepsilon_j \sim \mathcal N(0,\sigma^2)$.
19
+
20
+ **Selection.** Greedy maximisation of the conditional mutual information
21
+
22
+ $$I(z; y_j \mid y_S) = \tfrac12 \log \frac{\mathrm{Var}(y_j \mid y_S)}{\mathrm{Var}(y_j \mid y_S, z)},$$
23
+
24
+ computed exactly by Schur complements. No probabilities from a language model are needed: in the linear-Gaussian model the
25
+ posterior entropy depends only on which passages are selected.
26
+
27
+ **Anchoring.** The first pick is the top-1 of the base ranking $r$. At each later step, the base ranking's next item $b$ is replaced by
28
+ the max-gain item $j^\star$ only if $\mathrm{gain}(j^\star) > (1+\delta)\,\mathrm{gain}(b)$. With $\delta=\infty$ the selector returns
29
+ the base ranking exactly.
30
+
31
+ **Guarantee.** None is claimed. Correlated noise breaks submodularity of the objective, so the greedy rule has no worst-case
32
+ approximation bound.
README.md CHANGED
@@ -1,15 +1,53 @@
1
  ---
2
- title: Infomax Reranker
3
- emoji: 🏢
4
  colorFrom: indigo
5
- colorTo: green
6
  sdk: gradio
7
  sdk_version: 6.31.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
- license: mit
12
  short_description: Entropy-minimising passage reranker vs MS MARCO CE
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: InfoMax Reranker
3
+ emoji: 🔎
4
  colorFrom: indigo
5
+ colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.31.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: apache-2.0
11
  short_description: Entropy-minimising passage reranker vs MS MARCO CE
12
  ---
13
 
14
+ # InfoMax Reranker
15
+
16
+ A greedy, information-maximising selector over a retrieved candidate pool, compared with the standard
17
+ `cross-encoder/ms-marco-MiniLM-L6-v2` reranker. Neural scoring runs on ZeroGPU; selection runs on CPU.
18
+
19
+ - `app.py`: Gradio app. Paste a query and passages separated by blank lines.
20
+ - `infomax_rerank.py`: the selectors: `infomax`, plus the baselines `mmr`, `fused`/`zfuse` and `by_score`.
21
+ - `METHOD.md`: the model and the selection rule.
22
+ - `results/`: BEIR test results (`beir_results.md`, `beir_results.json`).
23
+ - `benchmark/`: the scripts that produced them (see below).
24
+
25
+ ## Hardware
26
+
27
+ Create the Space with the Gradio SDK and select **ZeroGPU** as hardware. Hosting a ZeroGPU Space needs a PRO or Enterprise
28
+ account. The `@spaces.GPU` decorator has no effect elsewhere, so the app also runs on CPU hardware and locally:
29
+
30
+ ```
31
+ pip install -r requirements.txt gradio
32
+ python app.py
33
+ ```
34
+
35
+ ## Reproducing the BEIR numbers
36
+
37
+ The datasets are the standard BEIR zips (scifact, nfcorpus, fiqa, scidocs, trec-covid), unzipped into `$BEIR_DATA`.
38
+ Outputs go to `$EXP_OUT`. Models are loaded from local folders or Hub ids.
39
+
40
+ ```
41
+ cd benchmark
42
+ export BEIR_DATA=/path/to/beir EXP_OUT=/path/to/out
43
+ python bm25_pools.py # BM25 top-100 pools: test splits + 200-query dev samples
44
+ python dense_stage.py sentence-transformers/all-MiniLM-L6-v2 50
45
+ python ce_stage.py cross-encoder/ms-marco-MiniLM-L6-v2 50
46
+ python build_scores.py 50
47
+ python run_eval.py # tune every method on dev splits only -> best_params.json
48
+ python run_eval.py --test # nDCG / Recall on the test splits + paired bootstrap CIs
49
+ python mrr_recall.py # MRR@10, Recall@3/5/10
50
+ ```
51
+
52
+ As a check on the setup, BM25 nDCG@10 is within about 0.01 of the published BEIR BM25 numbers on all five datasets.
53
+ all-MiniLM-L6-v2 scores 0.648 nDCG@10 on SciFact over the full corpus.
app.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """InfoMax reranker demo (Gradio, ZeroGPU).
2
+
3
+ Neural scoring (MS MARCO MiniLM-L6 cross-encoder + all-MiniLM-L6-v2 embeddings) runs inside the
4
+ @spaces.GPU function; every selector then runs on CPU from those cached scores.
5
+ """
6
+ import spaces # import first on ZeroGPU
7
+
8
+ import os
9
+
10
+ import gradio as gr
11
+ import numpy as np
12
+ import pandas as pd
13
+ import torch
14
+ from sentence_transformers import CrossEncoder, SentenceTransformer
15
+
16
+ from infomax_rerank import by_score, fused, infomax, mmr, zfuse
17
+
18
+ CE_ID = os.environ.get("CE_MODEL", "cross-encoder/ms-marco-MiniLM-L6-v2")
19
+ DENSE_ID = os.environ.get("DENSE_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
20
+ ce = CrossEncoder(CE_ID, max_length=256, device="cpu")
21
+ enc = SentenceTransformer(DENSE_ID, device="cpu")
22
+
23
+ # Settings tuned on dev splits only (SciFact train, NFCorpus dev, FiQA dev); one setting for all datasets.
24
+ TUNED = {
25
+ "InfoMax": dict(rank=1, gamma=4.0, rho=0.3, sigma2=0.03, beta=0.0, delta=0.0),
26
+ "InfoMax+F": dict(rank=4, gamma=8.0, rho=1.0, sigma2=0.03, beta=3.0, delta=0.0, alpha=0.5),
27
+ "MMR": dict(lam=0.95),
28
+ "Fusion": dict(alpha=0.5),
29
+ }
30
+ METHODS = ["CE", "InfoMax", "InfoMax+F", "Dense", "MMR", "Fusion"]
31
+
32
+
33
+ @spaces.GPU(duration=60)
34
+ def neural_scores(query, passages):
35
+ dev = "cuda" if torch.cuda.is_available() else "cpu"
36
+ ce.model.to(dev)
37
+ enc.to(dev)
38
+ logits = np.asarray(ce.predict([(query, p) for p in passages], batch_size=64,
39
+ show_progress_bar=False), np.float32).reshape(-1)
40
+ E = enc.encode([query] + passages, batch_size=64, normalize_embeddings=True,
41
+ convert_to_numpy=True, show_progress_bar=False).astype(np.float32)
42
+ return logits, E[1:], E[0]
43
+
44
+
45
+ def split_passages(text):
46
+ blocks = [b.strip() for b in text.replace("\r\n", "\n").split("\n\n")]
47
+ return [b for b in blocks if b]
48
+
49
+
50
+ def rank_all(query, passages_text, k, rank, gamma, sigma2, beta, delta, alpha):
51
+ query = (query or "").strip()
52
+ passages = split_passages(passages_text or "")
53
+ if not query:
54
+ raise gr.Error("Enter a query.")
55
+ if len(passages) < 2:
56
+ raise gr.Error("Enter at least two passages, separated by a blank line.")
57
+ if len(passages) > 200:
58
+ raise gr.Error("At most 200 passages per request.")
59
+ k = int(min(k, len(passages)))
60
+ r, E, q = neural_scores(query, passages)
61
+ user_f = dict(rank=int(rank), gamma=gamma, rho=1.0, sigma2=sigma2, beta=beta,
62
+ delta=np.inf if delta >= 10 else delta)
63
+ orders, gains = {}, {}
64
+ orders["CE"] = by_score(r)
65
+ orders["Dense"] = by_score(E @ q)
66
+ orders["MMR"] = mmr(r, E, K=k, **TUNED["MMR"])
67
+ orders["Fusion"] = zfuse(r, E, q, **TUNED["Fusion"])
68
+ orders["InfoMax"], gains["InfoMax"] = infomax(r, E, q, K=k, return_gains=True, **TUNED["InfoMax"])
69
+ orders["InfoMax+F"], gains["InfoMax+F"] = infomax(fused(r, E, q, alpha), E, q, K=k, return_gains=True,
70
+ **user_f)
71
+ short = lambda i: f"[{i + 1}] " + (passages[i][:90] + ("…" if len(passages[i]) > 90 else ""))
72
+ table = pd.DataFrame({m: [short(i) for i in orders[m][:k]] for m in METHODS})
73
+ table.insert(0, "Rank", list(range(1, k + 1)))
74
+ detail = pd.DataFrame({
75
+ "Passage": [f"[{i + 1}]" for i in range(len(passages))],
76
+ "CE logit": np.round(r, 3),
77
+ "Dense cosine": np.round(E @ q, 3),
78
+ "InfoMax+F step": [orders["InfoMax+F"][:k].index(i) + 1 if i in orders["InfoMax+F"][:k] else "" for i in
79
+ range(len(passages))],
80
+ })
81
+ g = gains["InfoMax+F"]
82
+ note = ("**InfoMax+F information gain per step (nats):** " +
83
+ ", ".join(f"{t + 1}: {v:.3f}" for t, v in enumerate(g)))
84
+ return table, detail, note
85
+
86
+
87
+ with open(os.path.join(os.path.dirname(__file__), "results", "beir_results.md")) as f:
88
+ RESULTS_MD = f.read()
89
+
90
+ EX_PASSAGES = """Metformin commonly causes gastrointestinal side effects such as nausea, diarrhoea and abdominal discomfort, especially when treatment starts.
91
+
92
+ Gastrointestinal upset, including nausea and diarrhoea, is the most frequent side effect reported by patients starting metformin.
93
+
94
+ Long-term metformin use is associated with reduced vitamin B12 absorption, so periodic B12 monitoring is recommended.
95
+
96
+ Lactic acidosis is a rare but serious adverse effect of metformin, mainly in patients with severe kidney impairment.
97
+
98
+ Metformin lowers hepatic glucose production and improves insulin sensitivity in type 2 diabetes.
99
+
100
+ Taking metformin with meals or using an extended-release formulation can reduce stomach upset.
101
+
102
+ Sulfonylureas such as glipizide can cause hypoglycaemia and weight gain."""
103
+
104
+ with gr.Blocks(title="InfoMax reranker") as demo:
105
+ gr.Markdown("# InfoMax reranker\nGreedy information-maximising selection over a candidate pool, compared with "
106
+ "the standard MS MARCO MiniLM-L6 cross-encoder. Paste a query and passages separated by blank lines.")
107
+ with gr.Tab("Rerank"):
108
+ with gr.Row():
109
+ with gr.Column(scale=1):
110
+ query = gr.Textbox(label="Query", value="What are the side effects of metformin?")
111
+ passages = gr.Textbox(label="Passages (blank line between passages)", lines=16, value=EX_PASSAGES)
112
+ k = gr.Slider(1, 20, value=5, step=1, label="K (passages to select)")
113
+ with gr.Accordion("InfoMax+F settings (defaults = dev-tuned)", open=False):
114
+ rank = gr.Slider(1, 16, value=4, step=1, label="Aspect rank r")
115
+ gamma = gr.Slider(0.5, 16, value=8.0, step=0.5, label="Relevance exponent γ")
116
+ sigma2 = gr.Slider(0.01, 1.0, value=0.03, step=0.01, label="Observation noise σ²")
117
+ beta = gr.Slider(0.0, 10.0, value=3.0, step=0.1, label="Shared nuisance variance β")
118
+ delta = gr.Slider(0.0, 10.0, value=0.0, step=0.1,
119
+ label="Anchor margin δ (10 = never leave the fused ranking)")
120
+ alpha = gr.Slider(0.0, 1.0, value=0.5, step=0.05, label="Dense weight α in relevance fusion")
121
+ btn = gr.Button("Rerank", variant="primary")
122
+ with gr.Column(scale=2):
123
+ out = gr.Dataframe(label="Top-K per method", wrap=True)
124
+ note = gr.Markdown()
125
+ detail = gr.Dataframe(label="Per-passage scores", wrap=True)
126
+ btn.click(rank_all, [query, passages, k, rank, gamma, sigma2, beta, delta, alpha], [out, detail, note])
127
+ with gr.Tab("BEIR results"):
128
+ gr.Markdown(RESULTS_MD)
129
+ with gr.Tab("Method"):
130
+ gr.Markdown(open(os.path.join(os.path.dirname(__file__), "METHOD.md")).read())
131
+
132
+ if __name__ == "__main__":
133
+ demo.queue().launch()
benchmark/bm25_pools.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """First stage: Lucene-style BM25 (k1=0.9, b=0.4, Porter stemming) top-100 pools per test query."""
2
+ import json
3
+ import os
4
+ import re
5
+ import time
6
+
7
+ import numpy as np
8
+ import scipy.sparse as sp
9
+ from nltk.stem import PorterStemmer
10
+ from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS, CountVectorizer
11
+
12
+ from common import DATASETS, OUT, evaluate, load_corpus, load_qrels, load_queries
13
+
14
+ TOK = re.compile(r"[a-z0-9]+")
15
+ _stem = PorterStemmer().stem
16
+ _cache = {}
17
+
18
+
19
+ def analyze(text):
20
+ out = []
21
+ for t in TOK.findall(text.lower()):
22
+ if t in ENGLISH_STOP_WORDS:
23
+ continue
24
+ s = _cache.get(t)
25
+ if s is None:
26
+ s = _cache[t] = _stem(t)
27
+ out.append(s)
28
+ return out
29
+
30
+
31
+ def run(name, split="test", depth=100, k1=0.9, b=0.4, max_q=None, seed=0):
32
+ t0 = time.time()
33
+ ids, titles, texts = load_corpus(name)
34
+ docs = [(ti + " " + tx).strip() for ti, tx in zip(titles, texts)]
35
+ queries = load_queries(name)
36
+ qrels = load_qrels(name, split)
37
+ qids = sorted(q for q in qrels if q in queries)
38
+ if max_q and len(qids) > max_q:
39
+ qids = sorted(np.random.default_rng(seed).choice(qids, max_q, replace=False).tolist())
40
+ cv = CountVectorizer(analyzer=analyze)
41
+ X = cv.fit_transform(docs).tocsc().astype(np.float32) # docs x terms
42
+ N = X.shape[0]
43
+ df = np.diff(X.indptr)
44
+ idf = np.log(1 + (N - df + 0.5) / (df + 0.5)).astype(np.float32)
45
+ X = X.tocsr()
46
+ dl = np.asarray(X.sum(1)).ravel()
47
+ norm = k1 * (1 - b + b * dl / dl.mean())
48
+ X = X.tocoo()
49
+ w = X.data * (k1 + 1) / (X.data + norm[X.row]) * idf[X.col]
50
+ W = sp.csc_matrix((w, (X.row, X.col)), shape=X.shape)
51
+ vocab = cv.vocabulary_
52
+ pools, run_ = {}, {}
53
+ for qid in qids:
54
+ terms = [vocab[t] for t in analyze(queries[qid]) if t in vocab]
55
+ if not terms:
56
+ s = np.zeros(N, dtype=np.float32)
57
+ else:
58
+ u, c = np.unique(terms, return_counts=True)
59
+ s = np.asarray(W[:, u] @ c.astype(np.float32)).ravel()
60
+ top = np.argpartition(-s, depth)[:depth]
61
+ top = top[np.argsort(-s[top], kind="stable")]
62
+ pools[qid] = {"ids": [ids[i] for i in top], "bm25": s[top].tolist()}
63
+ run_[qid] = {ids[i]: float(s[i]) for i in top}
64
+ metrics, _ = evaluate(run_, qrels)
65
+ os.makedirs(OUT, exist_ok=True)
66
+ json.dump(pools, open(os.path.join(OUT, f"{name}.{split}.pools.json"), "w"))
67
+ print(f"{name}/{split}: {N} docs, {len(qids)} queries, {time.time()-t0:.0f}s " +
68
+ " ".join(f"{k}={v:.4f}" for k, v in metrics.items()), flush=True)
69
+
70
+
71
+ if __name__ == "__main__":
72
+ for n in DATASETS:
73
+ run(n, "test")
74
+ for n, sp_ in [("scifact", "train"), ("nfcorpus", "dev"), ("fiqa", "dev")]:
75
+ run(n, sp_, max_q=200)
benchmark/build_scores.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Merge pools + dense embeddings + cross-encoder logits into out/<tag>.scores.npz (format read by run_eval.py)."""
2
+ import json
3
+ import os
4
+ import sys
5
+
6
+ import numpy as np
7
+
8
+ from common import OUT
9
+
10
+ TAGS = ["scifact.test", "nfcorpus.test", "fiqa.test", "scidocs.test", "trec-covid.test",
11
+ "scifact.train", "nfcorpus.dev", "fiqa.dev"]
12
+
13
+
14
+ def main(depth, tags):
15
+ for tag in tags:
16
+ name = tag.split(".")[0]
17
+ pools = json.load(open(os.path.join(OUT, f"{tag}.pools.json")))
18
+ z = np.load(os.path.join(OUT, f"{name}.emb.npz"))
19
+ emb = dict(zip(z["ids"].tolist(), z["emb"]))
20
+ zq = np.load(os.path.join(OUT, f"{name}.qemb.npz"))
21
+ qemb = dict(zip(zq["ids"].tolist(), zq["emb"]))
22
+ logits = np.load(os.path.join(OUT, f"{tag}.ce.npz"))["logits"]
23
+ qids = sorted(pools)
24
+ assert len(logits) == sum(len(pools[q]["ids"][:depth]) for q in qids), tag
25
+ out, p = {"qids": np.array(qids)}, 0
26
+ for q in qids:
27
+ ds = pools[q]["ids"][:depth]
28
+ out[f"{q}|ids"] = np.array(ds)
29
+ out[f"{q}|bm25"] = np.asarray(pools[q]["bm25"][:depth], np.float32)
30
+ out[f"{q}|ce"] = logits[p:p + len(ds)]
31
+ out[f"{q}|emb"] = np.stack([emb[d] for d in ds])
32
+ out[f"{q}|qemb"] = qemb[q]
33
+ p += len(ds)
34
+ np.savez_compressed(os.path.join(OUT, f"{tag}.scores.npz"), **out)
35
+ print(tag, len(qids), "queries")
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main(int(sys.argv[1]), sys.argv[2:] or TAGS)
benchmark/ce_stage.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cross-encoder stage: MS MARCO MiniLM-L6-v2 logits (max_length 256) for every (query, pool doc), top-DEPTH.
2
+ Resumable: out/<tag>.ce.npz holds logits computed so far in pool order.
3
+ Usage: python ce_stage.py CE_DIR DEPTH
4
+ """
5
+ import json
6
+ import os
7
+ import sys
8
+ import time
9
+
10
+ import numpy as np
11
+ import torch
12
+ from sentence_transformers import CrossEncoder
13
+
14
+ from common import OUT, load_corpus, load_queries
15
+
16
+ torch.set_num_threads(os.cpu_count())
17
+ TAGS = ["scifact.test", "nfcorpus.test", "trec-covid.test", "scifact.train", "nfcorpus.dev", "fiqa.dev",
18
+ "fiqa.test", "scidocs.test"]
19
+
20
+
21
+ def main():
22
+ cdir, depth = sys.argv[1], int(sys.argv[2])
23
+ ce = CrossEncoder(cdir, max_length=256, device="cpu")
24
+ cache = {}
25
+ for tag in TAGS:
26
+ name = tag.split(".")[0]
27
+ if name not in cache:
28
+ cache.clear()
29
+ ids, titles, texts = load_corpus(name)
30
+ idx = {d: i for i, d in enumerate(ids)}
31
+ cache[name] = (idx, titles, texts, load_queries(name))
32
+ idx, titles, texts, queries = cache[name]
33
+ pools = json.load(open(os.path.join(OUT, f"{tag}.pools.json")))
34
+ pairs = [(queries[q], (titles[idx[d]] + ". " if titles[idx[d]] else "") + texts[idx[d]])
35
+ for q in sorted(pools) for d in pools[q]["ids"][:depth]]
36
+ path = os.path.join(OUT, f"{tag}.ce.npz")
37
+ done = np.load(path)["logits"] if os.path.exists(path) else np.zeros(0, np.float32)
38
+ if len(done) == len(pairs):
39
+ print(f"{tag}: complete", flush=True)
40
+ continue
41
+ t0, start = time.time(), len(done)
42
+ for s in range(start, len(pairs), 2048):
43
+ chunk = pairs[s:s + 2048]
44
+ o = np.argsort([len(a) + len(b) for a, b in chunk], kind="stable") # length-sort: less padding
45
+ out = np.asarray(ce.predict([chunk[i] for i in o], batch_size=32, show_progress_bar=False),
46
+ np.float32).reshape(-1)
47
+ res = np.empty_like(out)
48
+ res[o] = out
49
+ done = np.concatenate([done, res])
50
+ np.savez(path, logits=done)
51
+ rate = (len(done) - start) / (time.time() - t0)
52
+ print(f"{tag}: {len(done)}/{len(pairs)} pairs, {rate:.0f} pairs/s", flush=True)
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
benchmark/common.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared I/O and evaluation for the BEIR reranking experiments."""
2
+ import csv
3
+ import json
4
+ import os
5
+
6
+ import numpy as np
7
+ import pytrec_eval
8
+
9
+ DATA = os.environ.get("BEIR_DATA", "beir")
10
+ OUT = os.environ.get("EXP_OUT", "out")
11
+ DATASETS = ["scifact", "nfcorpus", "fiqa", "scidocs", "trec-covid"]
12
+
13
+
14
+ def load_corpus(name):
15
+ ids, texts, titles = [], [], []
16
+ with open(os.path.join(DATA, name, "corpus.jsonl")) as f:
17
+ for line in f:
18
+ d = json.loads(line)
19
+ ids.append(d["_id"])
20
+ titles.append(d.get("title") or "")
21
+ texts.append(d.get("text") or "")
22
+ return ids, titles, texts
23
+
24
+
25
+ def load_queries(name):
26
+ q = {}
27
+ with open(os.path.join(DATA, name, "queries.jsonl")) as f:
28
+ for line in f:
29
+ d = json.loads(line)
30
+ q[d["_id"]] = d["text"]
31
+ return q
32
+
33
+
34
+ def load_qrels(name, split="test"):
35
+ qrels = {}
36
+ with open(os.path.join(DATA, name, "qrels", f"{split}.tsv")) as f:
37
+ r = csv.reader(f, delimiter="\t")
38
+ next(r)
39
+ for qid, did, score in r:
40
+ qrels.setdefault(qid, {})[did] = int(score)
41
+ return qrels
42
+
43
+
44
+ def evaluate(run, qrels, ks=(3, 5, 10)):
45
+ """run: {qid: {docid: score}}. Returns mean nDCG@k and Recall@k (BEIR/pytrec_eval conventions)."""
46
+ measures = {f"ndcg_cut.{','.join(map(str, ks))}", f"recall.{','.join(map(str, ks))}"}
47
+ q = {k: v for k, v in qrels.items() if k in run}
48
+ ev = pytrec_eval.RelevanceEvaluator(q, measures)
49
+ res = ev.evaluate({k: run[k] for k in q})
50
+ out = {}
51
+ for k in ks:
52
+ out[f"nDCG@{k}"] = float(np.mean([r[f"ndcg_cut_{k}"] for r in res.values()]))
53
+ out[f"R@{k}"] = float(np.mean([r[f"recall_{k}"] for r in res.values()]))
54
+ return out, res
55
+
56
+
57
+ def order_to_run(order_ids):
58
+ """Selected ordered list -> descending pseudo-scores so pytrec_eval keeps the order."""
59
+ n = len(order_ids)
60
+ return {d: float(n - i) for i, d in enumerate(order_ids)}
benchmark/dense_stage.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dense stage: all-MiniLM-L6-v2 embeddings for every doc in the top-DEPTH pools and every query.
2
+ Resumable: per-dataset store out/<name>.emb.npz updated after each chunk.
3
+ Usage: python dense_stage.py MINILM_DIR DEPTH
4
+ """
5
+ import json
6
+ import os
7
+ import sys
8
+ import time
9
+
10
+ import numpy as np
11
+ import torch
12
+ from sentence_transformers import SentenceTransformer
13
+
14
+ from common import OUT, load_corpus, load_queries
15
+
16
+ torch.set_num_threads(os.cpu_count())
17
+ TAGS = ["scifact.test", "scifact.train", "nfcorpus.test", "nfcorpus.dev", "trec-covid.test",
18
+ "fiqa.test", "fiqa.dev", "scidocs.test"]
19
+
20
+
21
+ def main():
22
+ mdir, depth = sys.argv[1], int(sys.argv[2])
23
+ enc = SentenceTransformer(mdir, device="cpu")
24
+ by_ds = {}
25
+ for t in TAGS:
26
+ by_ds.setdefault(t.split(".")[0], []).append(t)
27
+ for name, tags in by_ds.items():
28
+ store = os.path.join(OUT, f"{name}.emb.npz")
29
+ have = {}
30
+ if os.path.exists(store):
31
+ z = np.load(store)
32
+ have = dict(zip(z["ids"].tolist(), z["emb"]))
33
+ need, qneed = set(), set()
34
+ for t in tags:
35
+ pools = json.load(open(os.path.join(OUT, f"{t}.pools.json")))
36
+ for q, p in pools.items():
37
+ need.update(p["ids"][:depth])
38
+ qneed.add(q)
39
+ ids, titles, texts = load_corpus(name)
40
+ idx = {d: i for i, d in enumerate(ids)}
41
+ queries = load_queries(name)
42
+ todo = sorted(d for d in need if d not in have)
43
+ t0 = time.time()
44
+ for s in range(0, len(todo), 2048):
45
+ chunk = todo[s:s + 2048]
46
+ E = enc.encode([(titles[idx[d]] + ". " if titles[idx[d]] else "") + texts[idx[d]] for d in chunk],
47
+ batch_size=64, normalize_embeddings=True, convert_to_numpy=True, show_progress_bar=False)
48
+ have.update(zip(chunk, E.astype(np.float32)))
49
+ k = sorted(have)
50
+ np.savez(store, ids=np.array(k), emb=np.stack([have[d] for d in k]))
51
+ print(f"{name}: {min(s + 2048, len(todo))}/{len(todo)} docs, {time.time()-t0:.0f}s", flush=True)
52
+ qk = sorted(qneed)
53
+ QE = enc.encode([queries[q] for q in qk], batch_size=64, normalize_embeddings=True,
54
+ convert_to_numpy=True, show_progress_bar=False)
55
+ np.savez(os.path.join(OUT, f"{name}.qemb.npz"), ids=np.array(qk), emb=QE.astype(np.float32))
56
+ print(f"{name}: done ({len(have)} docs, {len(qk)} queries)", flush=True)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
benchmark/mrr_recall.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MRR@10 and Recall@{3,5,10} on the test splits for every method, using the dev-tuned settings in best_params.json.
2
+ Relevance = qrel grade > 0. Paired bootstrap 95% CIs for InfoMax+F minus CE and minus Fusion."""
3
+ import json
4
+ import os
5
+
6
+ import numpy as np
7
+
8
+ from common import OUT
9
+ from run_eval import TEST, boot_ci, load, method_orders
10
+
11
+ METHODS = ["BM25", "Dense", "CE", "MMR", "Fusion", "InfoMax", "InfoMax+F"]
12
+ best = {k: {kk: (np.inf if vv is None else vv) for kk, vv in v.items()}
13
+ for k, v in json.load(open(os.path.join(OUT, "best_params.json"))).items()}
14
+
15
+
16
+ def per_query(Q, qrels, orders):
17
+ out = {"MRR@10": [], "R@3": [], "R@5": [], "R@10": []}
18
+ for x, o in zip(Q, orders):
19
+ rel = {d for d, g in qrels.get(x["qid"], {}).items() if g > 0}
20
+ if not rel:
21
+ continue
22
+ ranked = [x["ids"][j] for j in o]
23
+ hit = [d in rel for d in ranked[:10]]
24
+ out["MRR@10"].append(1.0 / (hit.index(True) + 1) if any(hit) else 0.0)
25
+ for k in (3, 5, 10):
26
+ out[f"R@{k}"].append(sum(hit[:k]) / len(rel))
27
+ return {k: np.array(v) for k, v in out.items()}
28
+
29
+
30
+ res = {}
31
+ for tag in TEST:
32
+ Q, qr = load(tag)
33
+ for m in METHODS:
34
+ o = method_orders(Q, m, best.get(m, {}))
35
+ res[(tag, m)] = per_query(Q, qr, o)
36
+
37
+ for metric in ("MRR@10", "R@3", "R@5", "R@10"):
38
+ print(f"\n{metric}")
39
+ print(f"{'dataset':16s}" + "".join(f"{m:>10s}" for m in METHODS))
40
+ for tag in TEST:
41
+ print(f"{tag:16s}" + "".join(f"{res[(tag, m)][metric].mean():10.4f}" for m in METHODS))
42
+
43
+ print("\nInfoMax+F minus CE / minus Fusion, paired bootstrap 95% CI")
44
+ for metric in ("MRR@10", "R@10"):
45
+ for tag in TEST:
46
+ a = res[(tag, "InfoMax+F")][metric]
47
+ s = []
48
+ for o in ("CE", "Fusion"):
49
+ d, lo, hi = boot_ci(a, res[(tag, o)][metric])
50
+ s.append(f"vs {o}: {d:+.4f} [{lo:+.4f},{hi:+.4f}]")
51
+ print(f"{metric:7s}{tag:16s}" + " ".join(s))
52
+ json.dump({f"{t}|{m}|{k}": float(v.mean()) for (t, m), d in res.items() for k, v in d.items()},
53
+ open(os.path.join(OUT, "mrr_recall.json"), "w"), indent=1)
benchmark/run_eval.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tune every method's knobs on dev splits only (scifact.train, nfcorpus.dev, fiqa.dev; one global
2
+ setting per method), then evaluate on the five BEIR test splits.
3
+
4
+ Usage: python run_eval.py [--quick]
5
+ """
6
+ import itertools
7
+ import json
8
+ import os
9
+ import sys
10
+ import time
11
+
12
+ import numpy as np
13
+
14
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
15
+ from common import OUT, evaluate, load_qrels, order_to_run # noqa: E402
16
+ from infomax_rerank import by_score, fused, infomax, mmr, zfuse # noqa: E402
17
+
18
+ DEV = ["scifact.train", "nfcorpus.dev", "fiqa.dev"]
19
+ TEST = ["scifact.test", "nfcorpus.test", "fiqa.test", "scidocs.test", "trec-covid.test"]
20
+
21
+
22
+ def load(tag):
23
+ name, split = tag.split(".")
24
+ z = np.load(os.path.join(OUT, f"{tag}.scores.npz"))
25
+ qs = []
26
+ for q in z["qids"]:
27
+ qs.append({k: z[f"{q}|{k}"] for k in ("ids", "bm25", "ce", "emb", "qemb")} | {"qid": str(q)})
28
+ return qs, load_qrels(name, split)
29
+
30
+
31
+ def score(Q, qrels, orders):
32
+ run = {x["qid"]: order_to_run([x["ids"][j] for j in o]) for x, o in zip(Q, orders)}
33
+ m, per = evaluate(run, qrels)
34
+ return m, {q: v["ndcg_cut_10"] for q, v in per.items()}
35
+
36
+
37
+ def method_orders(Q, method, p):
38
+ if method == "BM25":
39
+ return [by_score(x["bm25"]) for x in Q]
40
+ if method == "Dense":
41
+ return [by_score(x["emb"] @ x["qemb"]) for x in Q]
42
+ if method == "CE":
43
+ return [by_score(x["ce"]) for x in Q]
44
+ if method == "MMR":
45
+ return [mmr(x["ce"], x["emb"], **p) for x in Q]
46
+ if method == "Fusion":
47
+ return [zfuse(x["ce"], x["emb"], x["qemb"], **p) for x in Q]
48
+ if method == "InfoMax":
49
+ return [infomax(x["ce"], x["emb"], x["qemb"], **p) for x in Q]
50
+ if method == "InfoMax+F": # same selector, relevance/anchor = the tuned CE+dense fusion score
51
+ p = dict(p)
52
+ a = p.pop("alpha")
53
+ return [infomax(fused(x["ce"], x["emb"], x["qemb"], a), x["emb"], x["qemb"], **p) for x in Q]
54
+ raise ValueError(method)
55
+
56
+
57
+ GRIDS = {
58
+ "MMR": [{"lam": l} for l in (0.5, 0.6, 0.7, 0.8, 0.9, 0.95)],
59
+ "Fusion": [{"alpha": a} for a in (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9)],
60
+ "InfoMax": [dict(rank=r, gamma=g, rho=rh, sigma2=s, beta=b, delta=d)
61
+ for r, g, rh, s, b, d in itertools.product(
62
+ (1, 4, 16), (1.0, 2.0, 4.0), (0.3, 1.0), (0.03, 0.3), (0.0, 0.3, 3.0),
63
+ (0.0, 0.5, 2.0, np.inf))],
64
+ "InfoMax+F": [dict(rank=r, gamma=g, rho=1.0, sigma2=s, beta=b, delta=d)
65
+ for r, g, s, b, d in itertools.product(
66
+ (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))],
67
+ }
68
+
69
+
70
+ def boot_ci(a, b, n=10000, seed=0):
71
+ d = np.asarray(a) - np.asarray(b)
72
+ rng = np.random.default_rng(seed)
73
+ m = d[rng.integers(0, len(d), (n, len(d)))].mean(1)
74
+ return float(d.mean()), float(np.percentile(m, 2.5)), float(np.percentile(m, 97.5))
75
+
76
+
77
+ def main():
78
+ quick = "--quick" in sys.argv
79
+ best = {}
80
+ bp = os.path.join(OUT, "best_params.json")
81
+ if "--test" in sys.argv:
82
+ best = {k: {kk: (np.inf if vv is None else vv) for kk, vv in v.items()}
83
+ for k, v in json.load(open(bp)).items()}
84
+ grids = {}
85
+ else:
86
+ dev = {t: load(t) for t in DEV}
87
+ grids = GRIDS
88
+ for meth, grid in grids.items():
89
+ if meth == "InfoMax+F":
90
+ grid = [p | {"alpha": best["Fusion"]["alpha"]} for p in grid]
91
+ if quick:
92
+ grid = grid[:4]
93
+ t0 = time.time()
94
+ res = []
95
+ for p in grid:
96
+ v = np.mean([score(Q, qr, method_orders(Q, meth, p))[0]["nDCG@10"] for Q, qr in dev.values()])
97
+ res.append((v, p))
98
+ res.sort(key=lambda t: -t[0])
99
+ best[meth] = res[0][1]
100
+ ce_dev = np.mean([score(Q, qr, method_orders(Q, "CE", {}))[0]["nDCG@10"] for Q, qr in dev.values()])
101
+ print(f"[tune] {meth}: best dev nDCG@10 {res[0][0]:.4f} (CE {ce_dev:.4f}) with {res[0][1]} "
102
+ f"[{len(grid)} settings, {time.time()-t0:.0f}s]", flush=True)
103
+ if meth.startswith("InfoMax"):
104
+ for v, p in res[:5]:
105
+ print(f" {v:.4f} {p}")
106
+ if "--test" not in sys.argv:
107
+ json.dump({k: {kk: (None if vv == np.inf else vv) for kk, vv in v.items()} for k, v in best.items()},
108
+ open(bp, "w"), indent=1)
109
+ return
110
+
111
+ methods = ["BM25", "Dense", "CE", "MMR", "Fusion", "InfoMax", "InfoMax+F"]
112
+ table, perq = {}, {}
113
+ for tag in TEST:
114
+ Q, qr = load(tag)
115
+ for meth in methods:
116
+ o = method_orders(Q, meth, best.get(meth, {}))
117
+ m, pq = score(Q, qr, o)
118
+ table[(tag, meth)] = m
119
+ perq[(tag, meth)] = pq
120
+ print("\nnDCG@10 on test (BM25 top-50 pools; knobs tuned on dev only)")
121
+ print(f"{'dataset':16s}" + "".join(f"{m:>9s}" for m in methods))
122
+ for tag in TEST:
123
+ print(f"{tag:16s}" + "".join(f"{table[(tag, m)]['nDCG@10']:9.4f}" for m in methods))
124
+ print("\nInfoMax+F minus X, nDCG@10, paired bootstrap 95% CI")
125
+ rows = {}
126
+ for tag in TEST:
127
+ qids = sorted(perq[(tag, "CE")])
128
+ for other in ("CE", "MMR", "Fusion"):
129
+ a = [perq[(tag, "InfoMax+F")][q] for q in qids]
130
+ b = [perq[(tag, other)][q] for q in qids]
131
+ rows[(tag, other)] = boot_ci(a, b)
132
+ print(f"{tag:16s}" + " ".join(f"vs {o}: {rows[(tag, o)][0]:+.4f} [{rows[(tag, o)][1]:+.4f},{rows[(tag, o)][2]:+.4f}]"
133
+ for o in ("CE", "MMR", "Fusion")))
134
+ json.dump({"best": {k: {kk: (None if vv == np.inf else vv) for kk, vv in v.items()} for k, v in best.items()},
135
+ "test": {f"{t}|{m}": v for (t, m), v in table.items()},
136
+ "ci_infomax_minus": {f"{t}|{o}": v for (t, o), v in rows.items()}},
137
+ open(os.path.join(OUT, "results.json"), "w"), indent=1)
138
+
139
+
140
+ if __name__ == "__main__":
141
+ main()
infomax_rerank.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Rerankers over a fixed candidate pool. Each returns a full ordering (indices into the pool).
2
+
3
+ Inputs per query: r = cross-encoder logits (n,), E = unit doc embeddings (n, d), q = unit query embedding (d,).
4
+ """
5
+ import numpy as np
6
+ from scipy.linalg import cho_factor, cho_solve
7
+
8
+
9
+ def minmax(x):
10
+ lo, hi = float(x.min()), float(x.max())
11
+ return (x - lo) / (hi - lo) if hi > lo + 1e-9 else np.zeros_like(x)
12
+
13
+
14
+ def by_score(s):
15
+ return list(np.argsort(-s, kind="stable"))
16
+
17
+
18
+ def complete(order, base):
19
+ seen = set(order)
20
+ return list(order) + [j for j in base if j not in seen]
21
+
22
+
23
+ # ---------------------------------------------------------------- baselines
24
+ def mmr(r, E, lam, K=10):
25
+ rel = minmax(r)
26
+ sim = E @ E.T
27
+ S, rem = [], set(range(len(r)))
28
+ for _ in range(min(K, len(r))):
29
+ if not S:
30
+ j = int(np.argmax(rel))
31
+ else:
32
+ red = sim[:, S].max(1)
33
+ cand = np.array(sorted(rem))
34
+ v = lam * rel[cand] - (1 - lam) * red[cand]
35
+ j = int(cand[np.argmax(v)])
36
+ S.append(j)
37
+ rem.discard(j)
38
+ return complete(S, by_score(r))
39
+
40
+
41
+ def fused(r, E, q, alpha):
42
+ """z-score fusion of cross-encoder logits and dense cosine; alpha = weight on dense."""
43
+ d = E @ q
44
+ z = lambda x: (x - x.mean()) / (x.std() + 1e-9)
45
+ return (1 - alpha) * z(r) + alpha * z(d)
46
+
47
+
48
+ def zfuse(r, E, q, alpha):
49
+ return by_score(fused(r, E, q, alpha))
50
+
51
+
52
+ # ---------------------------------------------------------------- information-max selector
53
+ def _condvar(M, S):
54
+ if not S:
55
+ return np.diag(M).copy()
56
+ c = cho_factor(M[np.ix_(S, S)], lower=True)
57
+ A = cho_solve(c, M[S, :])
58
+ return np.diag(M) - np.einsum("ij,ij->j", M[S, :], A)
59
+
60
+
61
+ def infomax(r, E, q, rank=4, gamma=2.0, rho=1.0, sigma2=0.1, beta=0.0, delta=np.inf, K=10,
62
+ return_gains=False):
63
+ """Greedy I(z; y_j | y_S) with z = query-aspect latent and correlated (nuisance) noise.
64
+
65
+ y_j = s_j <U^T e_j, z> + <g_j, u> + eps_j, z ~ N(0, Lambda), u ~ N(0, beta I), eps ~ N(0, sigma2)
66
+ e_j : pool-centred unit embedding; U: [query direction, top relevance-weighted pool directions]
67
+ s_j^2: minmax(cross-encoder)^gamma ; g_j = (I - U U^T) e_j (off-aspect content -> shared noise)
68
+ Anchored to the cross-encoder order: the CE top remaining item b is replaced by the max-gain
69
+ item only if gain(j*) > (1 + delta) * gain(b). delta = inf reproduces the cross-encoder.
70
+ """
71
+ n = len(r)
72
+ base = by_score(r)
73
+ w = minmax(r) ** gamma
74
+ mu = E.mean(0)
75
+ Ec = E - mu
76
+ Ec /= np.linalg.norm(Ec, axis=1, keepdims=True) + 1e-9
77
+ qc = q - mu
78
+ qc /= np.linalg.norm(qc) + 1e-9
79
+ U = [qc]
80
+ lam = [1.0]
81
+ if rank > 1:
82
+ Wr = np.sqrt(w)[:, None] * (Ec - np.outer(Ec @ qc, qc))
83
+ _, sv, Vt = np.linalg.svd(Wr, full_matrices=False)
84
+ m = min(rank - 1, len(sv))
85
+ U += list(Vt[:m])
86
+ lam += list(rho * (sv[:m] / (sv[0] + 1e-9)) ** 2)
87
+ U = np.array(U).T # d x r
88
+ Lam = np.diag(lam)
89
+ P = Ec @ U # n x r aspect coordinates
90
+ H = np.sqrt(w)[:, None] * P
91
+ G = Ec - P @ U.T # off-aspect residual
92
+ R = beta * (G @ G.T) + sigma2 * np.eye(n)
93
+ C = H @ Lam @ H.T + R
94
+ S, rem, gains_log = [], np.ones(n, bool), []
95
+ for t in range(min(K, n)):
96
+ g = 0.5 * np.log(np.maximum(_condvar(C, S), 1e-12) / np.maximum(_condvar(R, S), 1e-12))
97
+ b = next(j for j in base if rem[j])
98
+ if t == 0:
99
+ j = b
100
+ else:
101
+ gm = np.where(rem, g, -np.inf)
102
+ js = int(np.argmax(gm))
103
+ j = js if gm[js] > (1 + delta) * g[b] + 1e-12 else b
104
+ gains_log.append(float(g[j]))
105
+ S.append(j)
106
+ rem[j] = False
107
+ order = complete(S, base)
108
+ return (order, gains_log) if return_gains else order
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ torch
2
+ sentence-transformers>=3.0
3
+ numpy
4
+ scipy
5
+ pandas
6
+ spaces
results/beir_results.json ADDED
@@ -0,0 +1,528 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tuned_params_dev_only": {
3
+ "MMR": {
4
+ "lam": 0.95
5
+ },
6
+ "Fusion": {
7
+ "alpha": 0.5
8
+ },
9
+ "InfoMax": {
10
+ "rank": 1,
11
+ "gamma": 4.0,
12
+ "rho": 0.3,
13
+ "sigma2": 0.03,
14
+ "beta": 0.0,
15
+ "delta": 0.0
16
+ },
17
+ "InfoMax+F": {
18
+ "rank": 4,
19
+ "gamma": 8.0,
20
+ "rho": 1.0,
21
+ "sigma2": 0.03,
22
+ "beta": 3.0,
23
+ "delta": 0.0,
24
+ "alpha": 0.5
25
+ }
26
+ },
27
+ "ndcg_recall_test": {
28
+ "scifact.test|BM25": {
29
+ "nDCG@3": 0.6431199582422674,
30
+ "R@3": 0.7109444444444445,
31
+ "nDCG@5": 0.6596725941258738,
32
+ "R@5": 0.7526111111111111,
33
+ "nDCG@10": 0.6789590258685955,
34
+ "R@10": 0.8077222222222222
35
+ },
36
+ "scifact.test|Dense": {
37
+ "nDCG@3": 0.6163483357076979,
38
+ "R@3": 0.6764444444444444,
39
+ "nDCG@5": 0.6494822542293248,
40
+ "R@5": 0.7552777777777778,
41
+ "nDCG@10": 0.6775772341008127,
42
+ "R@10": 0.8395555555555556
43
+ },
44
+ "scifact.test|CE": {
45
+ "nDCG@3": 0.6355722307788756,
46
+ "R@3": 0.6832777777777779,
47
+ "nDCG@5": 0.6632119878395338,
48
+ "R@5": 0.7501666666666666,
49
+ "nDCG@10": 0.6762940264571006,
50
+ "R@10": 0.7884444444444444
51
+ },
52
+ "scifact.test|MMR": {
53
+ "nDCG@3": 0.6386774537671235,
54
+ "R@3": 0.6916111111111112,
55
+ "nDCG@5": 0.6626760413062829,
56
+ "R@5": 0.7501666666666666,
57
+ "nDCG@10": 0.6737188913639833,
58
+ "R@10": 0.7817777777777778
59
+ },
60
+ "scifact.test|Fusion": {
61
+ "nDCG@3": 0.6710598597617542,
62
+ "R@3": 0.716,
63
+ "nDCG@5": 0.6884192327495369,
64
+ "R@5": 0.7627777777777777,
65
+ "nDCG@10": 0.7120976163008534,
66
+ "R@10": 0.8312222222222222
67
+ },
68
+ "scifact.test|InfoMax": {
69
+ "nDCG@3": 0.6530626071442321,
70
+ "R@3": 0.7107222222222221,
71
+ "nDCG@5": 0.6737190144089154,
72
+ "R@5": 0.7646111111111111,
73
+ "nDCG@10": 0.6948257751035408,
74
+ "R@10": 0.8252222222222222
75
+ },
76
+ "scifact.test|InfoMax+F": {
77
+ "nDCG@3": 0.6725576912860219,
78
+ "R@3": 0.7196111111111111,
79
+ "nDCG@5": 0.6932716623902917,
80
+ "R@5": 0.7727777777777777,
81
+ "nDCG@10": 0.7155111299403955,
82
+ "R@10": 0.8368888888888889
83
+ },
84
+ "nfcorpus.test|BM25": {
85
+ "nDCG@3": 0.3823382822268318,
86
+ "R@3": 0.10380628945641764,
87
+ "nDCG@5": 0.3584927187341618,
88
+ "R@5": 0.12331434900055137,
89
+ "nDCG@10": 0.3261466393934428,
90
+ "R@10": 0.1564336372752942
91
+ },
92
+ "nfcorpus.test|Dense": {
93
+ "nDCG@3": 0.39224081480653206,
94
+ "R@3": 0.1054475795114919,
95
+ "nDCG@5": 0.3673855429222189,
96
+ "R@5": 0.12987323687278512,
97
+ "nDCG@10": 0.329672569677747,
98
+ "R@10": 0.1573953366222301
99
+ },
100
+ "nfcorpus.test|CE": {
101
+ "nDCG@3": 0.4229396163803222,
102
+ "R@3": 0.11255578971537836,
103
+ "nDCG@5": 0.3853234004565943,
104
+ "R@5": 0.13310119029572776,
105
+ "nDCG@10": 0.3458646508247627,
106
+ "R@10": 0.1623880974154255
107
+ },
108
+ "nfcorpus.test|MMR": {
109
+ "nDCG@3": 0.4203315776968791,
110
+ "R@3": 0.11215193296551876,
111
+ "nDCG@5": 0.3833962466547408,
112
+ "R@5": 0.13296678402481363,
113
+ "nDCG@10": 0.34496455529201764,
114
+ "R@10": 0.16095020847144315
115
+ },
116
+ "nfcorpus.test|Fusion": {
117
+ "nDCG@3": 0.4338920998144631,
118
+ "R@3": 0.11740153719863461,
119
+ "nDCG@5": 0.39540063670679393,
120
+ "R@5": 0.13643880315760448,
121
+ "nDCG@10": 0.35128231451281,
122
+ "R@10": 0.16419803765699678
123
+ },
124
+ "nfcorpus.test|InfoMax": {
125
+ "nDCG@3": 0.4313256861056477,
126
+ "R@3": 0.11718381080918494,
127
+ "nDCG@5": 0.39456391244419725,
128
+ "R@5": 0.13520038656020075,
129
+ "nDCG@10": 0.3459015527645562,
130
+ "R@10": 0.16016548330124453
131
+ },
132
+ "nfcorpus.test|InfoMax+F": {
133
+ "nDCG@3": 0.4288736628251464,
134
+ "R@3": 0.11576876191681756,
135
+ "nDCG@5": 0.3937758274378537,
136
+ "R@5": 0.13596229095231047,
137
+ "nDCG@10": 0.3521629267149766,
138
+ "R@10": 0.1636849294853285
139
+ },
140
+ "fiqa.test|BM25": {
141
+ "nDCG@3": 0.2128203105525299,
142
+ "R@3": 0.19910159285159282,
143
+ "nDCG@5": 0.22466852535393944,
144
+ "R@5": 0.24186197889901592,
145
+ "nDCG@10": 0.24313170715385354,
146
+ "R@10": 0.29682660018308166
147
+ },
148
+ "fiqa.test|Dense": {
149
+ "nDCG@3": 0.3238282228795522,
150
+ "R@3": 0.29630720359887025,
151
+ "nDCG@5": 0.3319682762608849,
152
+ "R@5": 0.3457364780281447,
153
+ "nDCG@10": 0.3454552348536008,
154
+ "R@10": 0.3935277171388282
155
+ },
156
+ "fiqa.test|CE": {
157
+ "nDCG@3": 0.2999096832508026,
158
+ "R@3": 0.28151820316172166,
159
+ "nDCG@5": 0.30844500797059193,
160
+ "R@5": 0.3263454052805905,
161
+ "nDCG@10": 0.3223933548101685,
162
+ "R@10": 0.3740349885025811
163
+ },
164
+ "fiqa.test|MMR": {
165
+ "nDCG@3": 0.29802765556267957,
166
+ "R@3": 0.2794605899929974,
167
+ "nDCG@5": 0.3072794992153618,
168
+ "R@5": 0.3258438620707139,
169
+ "nDCG@10": 0.3208304753270173,
170
+ "R@10": 0.3724917786260379
171
+ },
172
+ "fiqa.test|Fusion": {
173
+ "nDCG@3": 0.33832130422615464,
174
+ "R@3": 0.3066681432422173,
175
+ "nDCG@5": 0.3422595845439949,
176
+ "R@5": 0.3483966779105668,
177
+ "nDCG@10": 0.35845454570372476,
178
+ "R@10": 0.40362110026461884
179
+ },
180
+ "fiqa.test|InfoMax": {
181
+ "nDCG@3": 0.31712114265211333,
182
+ "R@3": 0.29967654516265624,
183
+ "nDCG@5": 0.32570617362477383,
184
+ "R@5": 0.35026567653882473,
185
+ "nDCG@10": 0.33689988451188513,
186
+ "R@10": 0.39294472674102304
187
+ },
188
+ "fiqa.test|InfoMax+F": {
189
+ "nDCG@3": 0.3378956061977511,
190
+ "R@3": 0.3066577326993994,
191
+ "nDCG@5": 0.34026758610658164,
192
+ "R@5": 0.3445141021529911,
193
+ "nDCG@10": 0.35772802740748577,
194
+ "R@10": 0.4016650873132354
195
+ },
196
+ "scidocs.test|BM25": {
197
+ "nDCG@3": 0.13953650967789885,
198
+ "R@3": 0.07848333333333334,
199
+ "nDCG@5": 0.12325782261102586,
200
+ "R@5": 0.10913333333333335,
201
+ "nDCG@10": 0.1489871672519904,
202
+ "R@10": 0.15653333333333336
203
+ },
204
+ "scidocs.test|Dense": {
205
+ "nDCG@3": 0.19532992037642224,
206
+ "R@3": 0.11063333333333332,
207
+ "nDCG@5": 0.17028354531362982,
208
+ "R@5": 0.15068333333333334,
209
+ "nDCG@10": 0.19872779902999924,
210
+ "R@10": 0.20253333333333337
211
+ },
212
+ "scidocs.test|CE": {
213
+ "nDCG@3": 0.1587600182121037,
214
+ "R@3": 0.08903333333333333,
215
+ "nDCG@5": 0.13789532380163394,
216
+ "R@5": 0.12068333333333334,
217
+ "nDCG@10": 0.16390846021956673,
218
+ "R@10": 0.16848333333333335
219
+ },
220
+ "scidocs.test|MMR": {
221
+ "nDCG@3": 0.15687177247920608,
222
+ "R@3": 0.08763333333333334,
223
+ "nDCG@5": 0.13595627703935984,
224
+ "R@5": 0.11843333333333333,
225
+ "nDCG@10": 0.16267991567191226,
226
+ "R@10": 0.16768333333333335
227
+ },
228
+ "scidocs.test|Fusion": {
229
+ "nDCG@3": 0.18745280547233126,
230
+ "R@3": 0.10563333333333334,
231
+ "nDCG@5": 0.16246159640953142,
232
+ "R@5": 0.1428333333333333,
233
+ "nDCG@10": 0.1936768980934276,
234
+ "R@10": 0.19993333333333332
235
+ },
236
+ "scidocs.test|InfoMax": {
237
+ "nDCG@3": 0.177,
238
+ "R@3": 0.10263333333333333,
239
+ "nDCG@5": 0.15042113247744018,
240
+ "R@5": 0.13338333333333333,
241
+ "nDCG@10": 0.1777698710919696,
242
+ "R@10": 0.18303333333333335
243
+ },
244
+ "scidocs.test|InfoMax+F": {
245
+ "nDCG@3": 0.18716785439017192,
246
+ "R@3": 0.10523333333333333,
247
+ "nDCG@5": 0.16395062501621754,
248
+ "R@5": 0.14468333333333333,
249
+ "nDCG@10": 0.19440802729822262,
250
+ "R@10": 0.20011666666666666
251
+ },
252
+ "trec-covid.test|BM25": {
253
+ "nDCG@3": 0.726536063698862,
254
+ "R@3": 0.006129608693906591,
255
+ "nDCG@5": 0.6931832808434869,
256
+ "R@5": 0.009495638499031355,
257
+ "nDCG@10": 0.645142792459132,
258
+ "R@10": 0.017583242371784345
259
+ },
260
+ "trec-covid.test|Dense": {
261
+ "nDCG@3": 0.7977649146579519,
262
+ "R@3": 0.0068480237477636895,
263
+ "nDCG@5": 0.7729841377993071,
264
+ "R@5": 0.011004464313449034,
265
+ "nDCG@10": 0.7502476986052402,
266
+ "R@10": 0.021582899500449337
267
+ },
268
+ "trec-covid.test|CE": {
269
+ "nDCG@3": 0.7835752445892035,
270
+ "R@3": 0.006500822879976922,
271
+ "nDCG@5": 0.7809746080465918,
272
+ "R@5": 0.01090665623000931,
273
+ "nDCG@10": 0.7443051348613305,
274
+ "R@10": 0.020647277435126458
275
+ },
276
+ "trec-covid.test|MMR": {
277
+ "nDCG@3": 0.7872617974664727,
278
+ "R@3": 0.006422345220043002,
279
+ "nDCG@5": 0.7700725718513098,
280
+ "R@5": 0.01065642470462764,
281
+ "nDCG@10": 0.7379188447063662,
282
+ "R@10": 0.020387648913364725
283
+ },
284
+ "trec-covid.test|Fusion": {
285
+ "nDCG@3": 0.8546927872602277,
286
+ "R@3": 0.0070038606633723065,
287
+ "nDCG@5": 0.8204160562167376,
288
+ "R@5": 0.011299291451340282,
289
+ "nDCG@10": 0.7971106002234066,
290
+ "R@10": 0.022138455639211664
291
+ },
292
+ "trec-covid.test|InfoMax": {
293
+ "nDCG@3": 0.7682680318494312,
294
+ "R@3": 0.006316769874605324,
295
+ "nDCG@5": 0.7494877534030594,
296
+ "R@5": 0.010045793623235813,
297
+ "nDCG@10": 0.7130600737191856,
298
+ "R@10": 0.0193403737920575
299
+ },
300
+ "trec-covid.test|InfoMax+F": {
301
+ "nDCG@3": 0.8376536063698862,
302
+ "R@3": 0.007084200669747274,
303
+ "nDCG@5": 0.8241432621592721,
304
+ "R@5": 0.011455199871392525,
305
+ "nDCG@10": 0.7896132829384864,
306
+ "R@10": 0.021971457445884583
307
+ }
308
+ },
309
+ "mrr_recall_test": {
310
+ "scifact.test|BM25|MRR@10": 0.6436679894179895,
311
+ "scifact.test|BM25|R@3": 0.7109444444444445,
312
+ "scifact.test|BM25|R@5": 0.7526111111111111,
313
+ "scifact.test|BM25|R@10": 0.8077222222222222,
314
+ "scifact.test|Dense|MRR@10": 0.6298108465608465,
315
+ "scifact.test|Dense|R@3": 0.6764444444444444,
316
+ "scifact.test|Dense|R@5": 0.7552777777777778,
317
+ "scifact.test|Dense|R@10": 0.8395555555555556,
318
+ "scifact.test|CE|MRR@10": 0.6487526455026454,
319
+ "scifact.test|CE|R@3": 0.6832777777777779,
320
+ "scifact.test|CE|R@5": 0.7501666666666668,
321
+ "scifact.test|CE|R@10": 0.7884444444444444,
322
+ "scifact.test|MMR|MRR@10": 0.6475079365079364,
323
+ "scifact.test|MMR|R@3": 0.6916111111111112,
324
+ "scifact.test|MMR|R@5": 0.7501666666666668,
325
+ "scifact.test|MMR|R@10": 0.7817777777777778,
326
+ "scifact.test|Fusion|MRR@10": 0.6794523809523809,
327
+ "scifact.test|Fusion|R@3": 0.7160000000000001,
328
+ "scifact.test|Fusion|R@5": 0.7627777777777778,
329
+ "scifact.test|Fusion|R@10": 0.8312222222222222,
330
+ "scifact.test|InfoMax|MRR@10": 0.6590330687830689,
331
+ "scifact.test|InfoMax|R@3": 0.7107222222222223,
332
+ "scifact.test|InfoMax|R@5": 0.7646111111111111,
333
+ "scifact.test|InfoMax|R@10": 0.8252222222222222,
334
+ "scifact.test|InfoMax+F|MRR@10": 0.6815767195767196,
335
+ "scifact.test|InfoMax+F|R@3": 0.7196111111111111,
336
+ "scifact.test|InfoMax+F|R@5": 0.7727777777777778,
337
+ "scifact.test|InfoMax+F|R@10": 0.8368888888888889,
338
+ "nfcorpus.test|BM25|MRR@10": 0.5234642979999017,
339
+ "nfcorpus.test|BM25|R@3": 0.10380628945641764,
340
+ "nfcorpus.test|BM25|R@5": 0.12331434900055138,
341
+ "nfcorpus.test|BM25|R@10": 0.15643363727529416,
342
+ "nfcorpus.test|Dense|MRR@10": 0.5332927908005307,
343
+ "nfcorpus.test|Dense|R@3": 0.10544757951149188,
344
+ "nfcorpus.test|Dense|R@5": 0.12987323687278512,
345
+ "nfcorpus.test|Dense|R@10": 0.1573953366222301,
346
+ "nfcorpus.test|CE|MRR@10": 0.577046783625731,
347
+ "nfcorpus.test|CE|R@3": 0.11255578971537837,
348
+ "nfcorpus.test|CE|R@5": 0.13310119029572776,
349
+ "nfcorpus.test|CE|R@10": 0.16238809741542548,
350
+ "nfcorpus.test|MMR|MRR@10": 0.5772875816993465,
351
+ "nfcorpus.test|MMR|R@3": 0.11215193296551876,
352
+ "nfcorpus.test|MMR|R@5": 0.13296678402481366,
353
+ "nfcorpus.test|MMR|R@10": 0.16095020847144315,
354
+ "nfcorpus.test|Fusion|MRR@10": 0.5721055088702147,
355
+ "nfcorpus.test|Fusion|R@3": 0.11740153719863461,
356
+ "nfcorpus.test|Fusion|R@5": 0.13643880315760448,
357
+ "nfcorpus.test|Fusion|R@10": 0.16419803765699678,
358
+ "nfcorpus.test|InfoMax|MRR@10": 0.5800432453683229,
359
+ "nfcorpus.test|InfoMax|R@3": 0.11718381080918494,
360
+ "nfcorpus.test|InfoMax|R@5": 0.13520038656020075,
361
+ "nfcorpus.test|InfoMax|R@10": 0.1601654833012445,
362
+ "nfcorpus.test|InfoMax+F|MRR@10": 0.5687982210428031,
363
+ "nfcorpus.test|InfoMax+F|R@3": 0.11576876191681756,
364
+ "nfcorpus.test|InfoMax+F|R@5": 0.13596229095231044,
365
+ "nfcorpus.test|InfoMax+F|R@10": 0.1636849294853285,
366
+ "fiqa.test|BM25|MRR@10": 0.3025493582206545,
367
+ "fiqa.test|BM25|R@3": 0.19910159285159287,
368
+ "fiqa.test|BM25|R@5": 0.24186197889901592,
369
+ "fiqa.test|BM25|R@10": 0.2968266001830817,
370
+ "fiqa.test|Dense|MRR@10": 0.435741230648638,
371
+ "fiqa.test|Dense|R@3": 0.29630720359887025,
372
+ "fiqa.test|Dense|R@5": 0.34573647802814467,
373
+ "fiqa.test|Dense|R@10": 0.3935277171388283,
374
+ "fiqa.test|CE|MRR@10": 0.4014948314716833,
375
+ "fiqa.test|CE|R@3": 0.28151820316172166,
376
+ "fiqa.test|CE|R@5": 0.3263454052805905,
377
+ "fiqa.test|CE|R@10": 0.3740349885025811,
378
+ "fiqa.test|MMR|MRR@10": 0.4007728297080149,
379
+ "fiqa.test|MMR|R@3": 0.27946058999299744,
380
+ "fiqa.test|MMR|R@5": 0.32584386207071386,
381
+ "fiqa.test|MMR|R@10": 0.3724917786260379,
382
+ "fiqa.test|Fusion|MRR@10": 0.45054992161473634,
383
+ "fiqa.test|Fusion|R@3": 0.3066681432422173,
384
+ "fiqa.test|Fusion|R@5": 0.34839667791056683,
385
+ "fiqa.test|Fusion|R@10": 0.40362110026461884,
386
+ "fiqa.test|InfoMax|MRR@10": 0.41511427101704873,
387
+ "fiqa.test|InfoMax|R@3": 0.29967654516265624,
388
+ "fiqa.test|InfoMax|R@5": 0.35026567653882473,
389
+ "fiqa.test|InfoMax|R@10": 0.39294472674102304,
390
+ "fiqa.test|InfoMax+F|MRR@10": 0.4490440672153635,
391
+ "fiqa.test|InfoMax+F|R@3": 0.30665773269939933,
392
+ "fiqa.test|InfoMax+F|R@5": 0.34451410215299105,
393
+ "fiqa.test|InfoMax+F|R@10": 0.4016650873132354,
394
+ "scidocs.test|BM25|MRR@10": 0.26165238095238097,
395
+ "scidocs.test|BM25|R@3": 0.07848333333333334,
396
+ "scidocs.test|BM25|R@5": 0.10913333333333333,
397
+ "scidocs.test|BM25|R@10": 0.15653333333333336,
398
+ "scidocs.test|Dense|MRR@10": 0.3502642857142857,
399
+ "scidocs.test|Dense|R@3": 0.11063333333333332,
400
+ "scidocs.test|Dense|R@5": 0.15068333333333334,
401
+ "scidocs.test|Dense|R@10": 0.20253333333333337,
402
+ "scidocs.test|CE|MRR@10": 0.29335079365079364,
403
+ "scidocs.test|CE|R@3": 0.08903333333333333,
404
+ "scidocs.test|CE|R@5": 0.12068333333333334,
405
+ "scidocs.test|CE|R@10": 0.16848333333333335,
406
+ "scidocs.test|MMR|MRR@10": 0.29261507936507936,
407
+ "scidocs.test|MMR|R@3": 0.08763333333333333,
408
+ "scidocs.test|MMR|R@5": 0.11843333333333333,
409
+ "scidocs.test|MMR|R@10": 0.16768333333333335,
410
+ "scidocs.test|Fusion|MRR@10": 0.336834126984127,
411
+ "scidocs.test|Fusion|R@3": 0.10563333333333334,
412
+ "scidocs.test|Fusion|R@5": 0.14283333333333334,
413
+ "scidocs.test|Fusion|R@10": 0.19993333333333332,
414
+ "scidocs.test|InfoMax|MRR@10": 0.3088793650793651,
415
+ "scidocs.test|InfoMax|R@3": 0.10263333333333334,
416
+ "scidocs.test|InfoMax|R@5": 0.13338333333333333,
417
+ "scidocs.test|InfoMax|R@10": 0.18303333333333335,
418
+ "scidocs.test|InfoMax+F|MRR@10": 0.33797738095238095,
419
+ "scidocs.test|InfoMax+F|R@3": 0.10523333333333333,
420
+ "scidocs.test|InfoMax+F|R@5": 0.14468333333333333,
421
+ "scidocs.test|InfoMax+F|R@10": 0.20011666666666666,
422
+ "trec-covid.test|BM25|MRR@10": 0.8966666666666666,
423
+ "trec-covid.test|BM25|R@3": 0.006129608693906592,
424
+ "trec-covid.test|BM25|R@5": 0.009495638499031355,
425
+ "trec-covid.test|BM25|R@10": 0.017583242371784342,
426
+ "trec-covid.test|Dense|MRR@10": 0.9066666666666667,
427
+ "trec-covid.test|Dense|R@3": 0.0068480237477636895,
428
+ "trec-covid.test|Dense|R@5": 0.011004464313449036,
429
+ "trec-covid.test|Dense|R@10": 0.021582899500449337,
430
+ "trec-covid.test|CE|MRR@10": 0.8880000000000001,
431
+ "trec-covid.test|CE|R@3": 0.006500822879976923,
432
+ "trec-covid.test|CE|R@5": 0.01090665623000931,
433
+ "trec-covid.test|CE|R@10": 0.02064727743512646,
434
+ "trec-covid.test|MMR|MRR@10": 0.8973333333333333,
435
+ "trec-covid.test|MMR|R@3": 0.006422345220043003,
436
+ "trec-covid.test|MMR|R@5": 0.010656424704627639,
437
+ "trec-covid.test|MMR|R@10": 0.020387648913364732,
438
+ "trec-covid.test|Fusion|MRR@10": 0.95,
439
+ "trec-covid.test|Fusion|R@3": 0.007003860663372309,
440
+ "trec-covid.test|Fusion|R@5": 0.011299291451340282,
441
+ "trec-covid.test|Fusion|R@10": 0.022138455639211668,
442
+ "trec-covid.test|InfoMax|MRR@10": 0.8983333333333333,
443
+ "trec-covid.test|InfoMax|R@3": 0.006316769874605324,
444
+ "trec-covid.test|InfoMax|R@5": 0.010045793623235811,
445
+ "trec-covid.test|InfoMax|R@10": 0.0193403737920575,
446
+ "trec-covid.test|InfoMax+F|MRR@10": 0.9533333333333335,
447
+ "trec-covid.test|InfoMax+F|R@3": 0.007084200669747275,
448
+ "trec-covid.test|InfoMax+F|R@5": 0.011455199871392525,
449
+ "trec-covid.test|InfoMax+F|R@10": 0.02197145744588459
450
+ },
451
+ "ci95_infomaxF_minus_baseline_ndcg10": {
452
+ "scifact.test|CE": [
453
+ 0.039217103483294935,
454
+ 0.022909537675935246,
455
+ 0.055961078282767175
456
+ ],
457
+ "scifact.test|MMR": [
458
+ 0.04179223857641219,
459
+ 0.025205164715028892,
460
+ 0.05874056517241845
461
+ ],
462
+ "scifact.test|Fusion": [
463
+ 0.0034135136395420356,
464
+ -0.0009101887412512667,
465
+ 0.00797676953601148
466
+ ],
467
+ "nfcorpus.test|CE": [
468
+ 0.006298275890213895,
469
+ 0.00014448374247222494,
470
+ 0.012741372907464514
471
+ ],
472
+ "nfcorpus.test|MMR": [
473
+ 0.007198371422958963,
474
+ 0.0009795059674302584,
475
+ 0.013655280658479303
476
+ ],
477
+ "nfcorpus.test|Fusion": [
478
+ 0.0008806122021665923,
479
+ -0.0021193370946726207,
480
+ 0.004199293800506931
481
+ ],
482
+ "fiqa.test|CE": [
483
+ 0.035334672597317214,
484
+ 0.026161183183126945,
485
+ 0.04471607186251431
486
+ ],
487
+ "fiqa.test|MMR": [
488
+ 0.03689755208046848,
489
+ 0.02775011299883495,
490
+ 0.04613706029287447
491
+ ],
492
+ "fiqa.test|Fusion": [
493
+ -0.0007265182962389718,
494
+ -0.0034145826445293446,
495
+ 0.0018706218114601444
496
+ ],
497
+ "scidocs.test|CE": [
498
+ 0.030499567078655924,
499
+ 0.02536860036621575,
500
+ 0.03573024236108956
501
+ ],
502
+ "scidocs.test|MMR": [
503
+ 0.03172811162631037,
504
+ 0.02661644582553447,
505
+ 0.0369284625702656
506
+ ],
507
+ "scidocs.test|Fusion": [
508
+ 0.0007311292047950231,
509
+ -0.0011978684641854693,
510
+ 0.0027101133301523487
511
+ ],
512
+ "trec-covid.test|CE": [
513
+ 0.045308148077155753,
514
+ 0.008178194738087384,
515
+ 0.0853383899434483
516
+ ],
517
+ "trec-covid.test|MMR": [
518
+ 0.05169443823212016,
519
+ 0.014252145682437763,
520
+ 0.09173759710245157
521
+ ],
522
+ "trec-covid.test|Fusion": [
523
+ -0.007497317284920146,
524
+ -0.021556051504264526,
525
+ 0.006579707248007931
526
+ ]
527
+ }
528
+ }
results/beir_results.md ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## BEIR results (test splits)
2
+
3
+ Setup: BM25 (k1=0.9, b=0.4, Porter) top-50 candidates per query; MS MARCO MiniLM-L6-v2 cross-encoder (max length 256);
4
+ all-MiniLM-L6-v2 embeddings. All knobs tuned once on dev splits only (SciFact train, NFCorpus dev, FiQA dev; 200 queries each)
5
+ and applied unchanged to all five test sets.
6
+
7
+ - **CE**: cross-encoder ranking (the standard reranker).
8
+ - **InfoMax**: information-maximising greedy selection, relevance = CE.
9
+ - **InfoMax+F**: same selector, relevance = z-score fusion of CE and dense cosine.
10
+ - **BM25 / Dense**: first-stage scores of the same pool (no reranker). **MMR**: CE relevance, dense redundancy, λ=0.95.
11
+ **Fusion**: z-score average of CE and dense cosine (α=0.5), no set selection.
12
+
13
+ ### nDCG@10
14
+
15
+ | Dataset | BM25 | Dense | CE | MMR | Fusion | InfoMax | InfoMax+F |
16
+ |---|---|---|---|---|---|---|---|
17
+ | SciFact | 0.6790 | 0.6776 | 0.6763 | 0.6737 | 0.7121 | 0.6948 | **0.7155** |
18
+ | NFCorpus | 0.3261 | 0.3297 | 0.3459 | 0.3450 | 0.3513 | 0.3459 | **0.3522** |
19
+ | FiQA | 0.2431 | 0.3455 | 0.3224 | 0.3208 | **0.3585** | 0.3369 | 0.3577 |
20
+ | SciDocs | 0.1490 | **0.1987** | 0.1639 | 0.1627 | 0.1937 | 0.1778 | 0.1944 |
21
+ | TREC-COVID | 0.6451 | 0.7502 | 0.7443 | 0.7379 | **0.7971** | 0.7131 | 0.7896 |
22
+
23
+ ### MRR@10
24
+
25
+ | Dataset | BM25 | Dense | CE | MMR | Fusion | InfoMax | InfoMax+F |
26
+ |---|---|---|---|---|---|---|---|
27
+ | SciFact | 0.6437 | 0.6298 | 0.6488 | 0.6475 | 0.6795 | 0.6590 | **0.6816** |
28
+ | NFCorpus | 0.5235 | 0.5333 | 0.5770 | 0.5773 | 0.5721 | **0.5800** | 0.5688 |
29
+ | FiQA | 0.3025 | 0.4357 | 0.4015 | 0.4008 | **0.4505** | 0.4151 | 0.4490 |
30
+ | SciDocs | 0.2617 | **0.3503** | 0.2934 | 0.2926 | 0.3368 | 0.3089 | 0.3380 |
31
+ | TREC-COVID | 0.8967 | 0.9067 | 0.8880 | 0.8973 | 0.9500 | 0.8983 | **0.9533** |
32
+
33
+ ### Recall@3
34
+
35
+ | Dataset | BM25 | Dense | CE | MMR | Fusion | InfoMax | InfoMax+F |
36
+ |---|---|---|---|---|---|---|---|
37
+ | SciFact | 0.7109 | 0.6764 | 0.6833 | 0.6916 | 0.7160 | 0.7107 | **0.7196** |
38
+ | NFCorpus | 0.1038 | 0.1054 | 0.1126 | 0.1122 | **0.1174** | 0.1172 | 0.1158 |
39
+ | FiQA | 0.1991 | 0.2963 | 0.2815 | 0.2795 | **0.3067** | 0.2997 | 0.3067 |
40
+ | SciDocs | 0.0785 | **0.1106** | 0.0890 | 0.0876 | 0.1056 | 0.1026 | 0.1052 |
41
+ | TREC-COVID | 0.0061 | 0.0068 | 0.0065 | 0.0064 | 0.0070 | 0.0063 | **0.0071** |
42
+
43
+ ### Recall@5
44
+
45
+ | Dataset | BM25 | Dense | CE | MMR | Fusion | InfoMax | InfoMax+F |
46
+ |---|---|---|---|---|---|---|---|
47
+ | SciFact | 0.7526 | 0.7553 | 0.7502 | 0.7502 | 0.7628 | 0.7646 | **0.7728** |
48
+ | NFCorpus | 0.1233 | 0.1299 | 0.1331 | 0.1330 | **0.1364** | 0.1352 | 0.1360 |
49
+ | FiQA | 0.2419 | 0.3457 | 0.3263 | 0.3258 | 0.3484 | **0.3503** | 0.3445 |
50
+ | SciDocs | 0.1091 | **0.1507** | 0.1207 | 0.1184 | 0.1428 | 0.1334 | 0.1447 |
51
+ | TREC-COVID | 0.0095 | 0.0110 | 0.0109 | 0.0107 | 0.0113 | 0.0100 | **0.0115** |
52
+
53
+ ### Recall@10
54
+
55
+ | Dataset | BM25 | Dense | CE | MMR | Fusion | InfoMax | InfoMax+F |
56
+ |---|---|---|---|---|---|---|---|
57
+ | SciFact | 0.8077 | **0.8396** | 0.7884 | 0.7818 | 0.8312 | 0.8252 | 0.8369 |
58
+ | NFCorpus | 0.1564 | 0.1574 | 0.1624 | 0.1610 | **0.1642** | 0.1602 | 0.1637 |
59
+ | FiQA | 0.2968 | 0.3935 | 0.3740 | 0.3725 | **0.4036** | 0.3929 | 0.4017 |
60
+ | SciDocs | 0.1565 | **0.2025** | 0.1685 | 0.1677 | 0.1999 | 0.1830 | 0.2001 |
61
+ | TREC-COVID | 0.0176 | 0.0216 | 0.0206 | 0.0204 | **0.0221** | 0.0193 | 0.0220 |
62
+
63
+ ### InfoMax+F minus baseline, nDCG@10, paired bootstrap 95% CI
64
+
65
+ | Dataset | vs CE | vs MMR | vs Fusion |
66
+ |---|---|---|---|
67
+ | SciFact | +0.0392 [+0.0229, +0.0560] | +0.0418 [+0.0252, +0.0587] | +0.0034 [-0.0009, +0.0080] |
68
+ | NFCorpus | +0.0063 [+0.0001, +0.0127] | +0.0072 [+0.0010, +0.0137] | +0.0009 [-0.0021, +0.0042] |
69
+ | FiQA | +0.0353 [+0.0262, +0.0447] | +0.0369 [+0.0278, +0.0461] | -0.0007 [-0.0034, +0.0019] |
70
+ | SciDocs | +0.0305 [+0.0254, +0.0357] | +0.0317 [+0.0266, +0.0369] | +0.0007 [-0.0012, +0.0027] |
71
+ | TREC-COVID | +0.0453 [+0.0082, +0.0853] | +0.0517 [+0.0143, +0.0917] | -0.0075 [-0.0216, +0.0066] |
72
+
73
+ **Reading.** InfoMax+F beats the cross-encoder on nDCG@10 on all five datasets (intervals above zero). It ties Fusion everywhere:
74
+ on these single-hop benchmarks the gain comes from adding dense relevance, not from set selection. nDCG does not penalise
75
+ redundant relevant passages. TREC-COVID recall is near zero by construction (~494 relevant documents per query).