Angshul commited on
Commit
dd990bf
·
verified ·
1 Parent(s): 4b7424b

Upload 31 files

Browse files
evidence_graph_reranker/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Query-conditioned Evidence-Graph Reranker (one fixed algorithm)."""
2
+ from .core import Passage, graph_features, minmax_relevance, select_with_features, rank_with_scores
3
+ from .neural import EvidenceGraphReranker, DENSE_MODEL, CROSS_ENCODER_MODEL
4
+ __all__ = ["Passage", "graph_features", "minmax_relevance", "select_with_features",
5
+ "rank_with_scores", "EvidenceGraphReranker", "DENSE_MODEL", "CROSS_ENCODER_MODEL"]
6
+ __version__ = "0.1.0"
evidence_graph_reranker/core.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The *single* fixed graph selector used across all six reported benchmarks.
2
+
3
+ Only candidate text, its optional title, and a query-specific neural relevance
4
+ score are allowed as input. There is no training, gold-label access, or corpus-
5
+ specific parameter tuning inside this module.
6
+ """
7
+ from __future__ import annotations
8
+ from dataclasses import dataclass
9
+ from typing import Any, Mapping, Sequence
10
+ import re
11
+ import unicodedata
12
+ import numpy as np
13
+
14
+
15
+ def normalize_text(text: Any) -> str:
16
+ """Match the exact normalization of the published evaluation scripts."""
17
+ return " ".join(
18
+ re.sub(r"[^\w\s]", " ", unicodedata.normalize("NFKD", str(text)).casefold()).split()
19
+ )
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Passage:
24
+ title: str
25
+ text: str
26
+ id: str | None = None
27
+
28
+ @classmethod
29
+ def from_value(cls, value: "Passage | Mapping[str, Any] | str") -> "Passage":
30
+ if isinstance(value, Passage):
31
+ return value
32
+ if isinstance(value, str):
33
+ return cls(title="", text=value)
34
+ if not isinstance(value, Mapping):
35
+ raise TypeError("Each passage must be a string, dict, or Passage.")
36
+ text = value.get("text", value.get("body", value.get("paragraph_text", "")))
37
+ return cls(title=str(value.get("title", "") or ""), text=str(text or ""),
38
+ id=None if value.get("id") is None else str(value["id"]))
39
+
40
+
41
+ def graph_features(query: str, passages: Sequence[Passage]) -> tuple[np.ndarray, np.ndarray]:
42
+ """Return directed source->target article-title edges and query/title matches.
43
+
44
+ This is intentionally the SAME simple string-link rule as the six-dataset
45
+ benchmarks. In particular, it does NOT introduce token overlap, semantic
46
+ similarity edges, facet decomposition, or learned relation extraction.
47
+ """
48
+ q = normalize_text(query)
49
+ titles = [normalize_text(p.title) for p in passages]
50
+ bodies = [normalize_text(p.text) for p in passages]
51
+ q_words = set(q.split())
52
+ n = len(passages)
53
+ link = np.zeros((n, n), np.float32)
54
+ match = np.zeros(n, np.float32)
55
+ for j, title in enumerate(titles):
56
+ if not title:
57
+ continue
58
+ title_words = set(title.split())
59
+ overlap = len(title_words & q_words) / max(1, len(title_words))
60
+ match[j] = 1.0 if " " + title + " " in " " + q + " " else 0.5 * overlap
61
+ for i, body in enumerate(bodies):
62
+ if i != j and len(title) >= 4 and " " + title + " " in " " + body + " ":
63
+ link[i, j] = 1.0
64
+ return link, match
65
+
66
+
67
+ def minmax_relevance(scores: Sequence[float]) -> np.ndarray:
68
+ scores = np.asarray(scores, dtype=np.float32)
69
+ if scores.ndim != 1 or not np.isfinite(scores).all():
70
+ raise ValueError("Scores must be a one-dimensional array of finite numbers.")
71
+ if not len(scores):
72
+ raise ValueError("At least one candidate passage is required.")
73
+ lo, hi = float(scores.min()), float(scores.max())
74
+ return (scores - lo) / (hi - lo) if hi > lo + 1e-9 else np.zeros_like(scores)
75
+
76
+
77
+ def select_with_features(
78
+ scores: Sequence[float], link: np.ndarray, match: np.ndarray, k: int = 3
79
+ ) -> list[int]:
80
+ """Greedy rank with fixed coefficients .4, .5, .2 and deterministic ties.
81
+
82
+ At step t the score of candidate j is
83
+ 0.4 * normalized_relevance_j
84
+ + 0.5 * max_{i previously selected} link[i, j]
85
+ + 0.2 * query_title_match_j.
86
+ """
87
+ if isinstance(k, bool) or not isinstance(k, int) or k < 1:
88
+ raise ValueError("k must be a positive integer")
89
+ norm_scores = minmax_relevance(scores)
90
+ n = len(norm_scores)
91
+ link = np.asarray(link, dtype=np.float32)
92
+ match = np.asarray(match, dtype=np.float32)
93
+ if link.shape != (n, n) or match.shape != (n,):
94
+ raise ValueError("Link matrix or match vector dimensions do not match scores")
95
+ selected: list[int] = []
96
+ remaining = set(range(n))
97
+ for _ in range(min(k, n)):
98
+ nxt = max(remaining, key=lambda j: (
99
+ 0.4 * norm_scores[j]
100
+ + 0.5 * max((link[i, j] for i in selected), default=0.0)
101
+ + 0.2 * match[j],
102
+ -j,
103
+ ))
104
+ selected.append(nxt)
105
+ remaining.remove(nxt)
106
+ return selected
107
+
108
+
109
+ def rank_with_scores(query: str, passages: Sequence[Passage | Mapping[str, Any] | str],
110
+ scores: Sequence[float], k: int = 3) -> list[dict[str, Any]]:
111
+ """Public, network-free inference API if the retriever supplies scores.
112
+
113
+ Returned `step_score` is the acquisition score at the moment of selection,
114
+ not a calibrated relevance probability.
115
+ """
116
+ docs = [Passage.from_value(p) for p in passages]
117
+ if len(docs) != len(scores):
118
+ raise ValueError("Supply exactly one relevance score per candidate passage")
119
+ link, match = graph_features(query, docs)
120
+ normalized = minmax_relevance(scores)
121
+ order = select_with_features(scores, link, match, k)
122
+ result = []
123
+ selected = []
124
+ for idx in order:
125
+ bonus = max((float(link[i, idx]) for i in selected), default=0.0)
126
+ step = 0.4 * float(normalized[idx]) + 0.5 * bonus + 0.2 * float(match[idx])
127
+ item = docs[idx]
128
+ result.append({
129
+ "index": idx, "id": item.id, "title": item.title, "text": item.text,
130
+ "relevance_score": float(scores[idx]),
131
+ "normalized_relevance": float(normalized[idx]),
132
+ "directed_link_bonus": bonus,
133
+ "query_title_match": float(match[idx]),
134
+ "step_score": step,
135
+ })
136
+ selected.append(idx)
137
+ return result
evidence_graph_reranker/neural.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Optional pretrained neural scorers. Weights remain with their original authors."""
2
+ from __future__ import annotations
3
+ from typing import Any, Mapping, Sequence
4
+ import numpy as np
5
+ from .core import Passage, rank_with_scores
6
+
7
+ DENSE_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
8
+ CROSS_ENCODER_MODEL = "cross-encoder/ms-marco-MiniLM-L6-v2"
9
+
10
+
11
+ class EvidenceGraphReranker:
12
+ """Hugging Face-friendly neural scoring + the same fixed graph rule.
13
+
14
+ backend='cross-encoder' (the standard MS MARCO reranker) or backend='dense'
15
+ (cosine over normalized all-MiniLM embeddings).
16
+
17
+ Models are downloaded from Hugging Face at first inference. Pass scores
18
+ explicitly to `rank` to avoid downloading or loading a neural model.
19
+ """
20
+
21
+ def __init__(self, backend: str = "cross-encoder", device: str = "cpu", batch_size: int = 16):
22
+ if backend not in {"cross-encoder", "dense"}:
23
+ raise ValueError("backend must be 'cross-encoder' or 'dense'")
24
+ if batch_size < 1:
25
+ raise ValueError("batch_size must be positive")
26
+ self.backend, self.device, self.batch_size = backend, device, batch_size
27
+ self._model = None
28
+
29
+ def _get_model(self):
30
+ if self._model is None:
31
+ try:
32
+ from sentence_transformers import CrossEncoder, SentenceTransformer
33
+ except ImportError as exc:
34
+ raise ImportError("Install the optional dependency: pip install sentence-transformers") from exc
35
+ if self.backend == "cross-encoder":
36
+ self._model = CrossEncoder(CROSS_ENCODER_MODEL, max_length=256, device=self.device)
37
+ else:
38
+ self._model = SentenceTransformer(DENSE_MODEL, device=self.device)
39
+ self._model.max_seq_length = 128
40
+ return self._model
41
+
42
+ def score(self, query: str, passages: Sequence[Passage | Mapping[str, Any] | str]) -> np.ndarray:
43
+ docs = [Passage.from_value(p) for p in passages]
44
+ if not docs:
45
+ raise ValueError("At least one passage is required")
46
+ texts = [(p.title + ". " if p.title else "") + p.text for p in docs]
47
+ model = self._get_model()
48
+ if self.backend == "cross-encoder":
49
+ out = model.predict([(query, text) for text in texts],
50
+ batch_size=self.batch_size, show_progress_bar=False)
51
+ return np.asarray(out, dtype=np.float32).reshape(-1)
52
+ emb = model.encode([query] + texts, batch_size=self.batch_size,
53
+ convert_to_numpy=True, normalize_embeddings=True,
54
+ show_progress_bar=False)
55
+ return (emb[1:] @ emb[0]).astype(np.float32)
56
+
57
+ def rank(self, query: str, passages: Sequence[Passage | Mapping[str, Any] | str],
58
+ k: int = 3, scores: Sequence[float] | None = None) -> list[dict[str, Any]]:
59
+ docs = [Passage.from_value(p) for p in passages]
60
+ scores = self.score(query, docs) if scores is None else scores
61
+ return rank_with_scores(query, docs, scores, k=k)
examples/passages.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ [
2
+ {"id": "p0", "title": "Atlas device", "text": "The Atlas device was launched by Novus Labs in 2025."},
3
+ {"id": "p1", "title": "Novus Labs", "text": "Novus Labs was founded by Mira Chen in 2018."},
4
+ {"id": "p2", "title": "Orion spacecraft", "text": "The Orion spacecraft is designed for deep space missions."},
5
+ {"id": "p3", "title": "Electronics", "text": "Electronics devices are manufactured in many countries."}
6
+ ]
examples/use.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run: python examples/use.py (network needed for first model download)."""
2
+ import json
3
+ from evidence_graph_reranker import EvidenceGraphReranker, rank_with_scores
4
+
5
+ query = "Who founded the company that released the Atlas device?"
6
+ with open("examples/passages.json", encoding="utf-8") as f:
7
+ passages = json.load(f)
8
+
9
+ # Full pretrained MS MARCO -> graph reranking
10
+ reranker = EvidenceGraphReranker(backend="cross-encoder", device="cpu")
11
+ for passage in reranker.rank(query, passages, k=3):
12
+ print(passage["index"], passage["title"], round(passage["step_score"], 4))
13
+
14
+ # If you already have neural relevance scores, the selector needs no checkpoint:
15
+ print([r["index"] for r in rank_with_scores(query, passages, [.91, .45, .20, .15], k=3)])
reproduce/evaluate_ms_marco_crossencoder.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pretrained MS MARCO MiniLM-L6 cross-encoder CPU-only held-out evaluation.
2
+ Inputs are user-uploaded safetensors/tokenizer and existing HotpotQA / 2Wiki candidate pools.
3
+ A fixed 200/600 split (seed 20261030) is reused from previous experiments.
4
+ No gold labels used for the cross-encoder, and none fed to selection.
5
+ """
6
+ import os,time,json,argparse,statistics,unicodedata,re
7
+ os.environ['OPENBLAS_NUM_THREADS']='1';os.environ['OMP_NUM_THREADS']='4'
8
+ from pathlib import Path
9
+ import numpy as np
10
+ import torch
11
+ from torch import nn
12
+ from torch.nn import functional as F
13
+ from safetensors.torch import load_file
14
+ from minilm_cpu_pipeline import BertEmb,Enc,WordPiece
15
+ from hotpot_subset_eval import load_data as hotpot_data
16
+ from wiki_subset_eval import load_data as wiki_data
17
+ from graph_bridge_eval import prep_links,graph_select
18
+
19
+ ROOT=Path('/mnt/data');RNG=20261030
20
+
21
+ class BertCrossEncoder(nn.Module):
22
+ def __init__(self):
23
+ super().__init__()
24
+ self.embeddings=BertEmb();self.encoder=Enc()
25
+ self.pooler=nn.Linear(384,384);self.classifier=nn.Linear(384,1)
26
+ def forward(self,ids,mask,token_types):
27
+ pos=self.embeddings.position_ids[:,:ids.shape[1]]
28
+ h=self.embeddings.LayerNorm(self.embeddings.word_embeddings(ids)+self.embeddings.position_embeddings(pos)+self.embeddings.token_type_embeddings(token_types))
29
+ h=self.encoder(h,mask[:,None,None,:].bool())
30
+ return self.classifier(torch.tanh(self.pooler(h[:,0,:]))).squeeze(-1)
31
+
32
+ def load_crossencoder():
33
+ torch.set_num_threads(min(4,os.cpu_count() or 4)); torch.set_num_interop_threads(1)
34
+ m=BertCrossEncoder(); raw=load_file(str(ROOT/'model.safetensors'),device='cpu')
35
+ clean={}
36
+ for k,v in raw.items():
37
+ if k=='bert.embeddings.position_ids':continue
38
+ if k.startswith('bert.pooler.dense.'):
39
+ k='pooler.'+k[len('bert.pooler.dense.'):]
40
+ elif k.startswith('bert.'):
41
+ k=k[5:]
42
+ clean[k]=v
43
+ missing,unexpected=m.load_state_dict(clean,strict=True)
44
+ assert not missing and not unexpected,(missing,unexpected)
45
+ m.eval();print('Loaded pretrained MS MARCO cross-encoder: 106 tensors, all parameters match',flush=True)
46
+ return m
47
+
48
+ class PairTokenizer(WordPiece):
49
+ def __init__(self,length=256):super().__init__(str(ROOT/'vocab.txt'),length)
50
+ def tokens(self,text):
51
+ text=''.join(ch for ch in unicodedata.normalize('NFD',text.lower()) if unicodedata.category(ch)!='Mn' and (ch.isspace() or unicodedata.category(ch)[0]!='C'))
52
+ out=[]
53
+ for x in re.findall(r'[^\W_]+|_|[^\w\s]',text,flags=re.UNICODE):out.extend(self.wp(x))
54
+ return out
55
+ def pair(self,q,d):
56
+ a=self.tokens(q);b=self.tokens(d);cap=self.max_length-3
57
+ while len(a)+len(b)>cap:
58
+ if len(b)>len(a):b.pop()
59
+ else:a.pop()
60
+ ids=[101]+a+[102]+b+[102]
61
+ tt=[0]*(len(a)+2)+[1]*(len(b)+1)
62
+ return ids,tt
63
+
64
+ @torch.inference_mode()
65
+ def score_pairs(m,tok,pairs,batch=16,limit=None):
66
+ out=np.empty(len(pairs),np.float32); times=[]
67
+ for start in range(0,len(pairs),batch):
68
+ p=pairs[start:start+batch]; inputs=[tok.pair(q,d) for q,d in p]
69
+ L=max(len(x[0]) for x in inputs)
70
+ ids=np.zeros((len(p),L),np.int64);tt=np.zeros_like(ids);mask=np.zeros_like(ids)
71
+ for i,(u,v) in enumerate(inputs):ids[i,:len(u)]=u;tt[i,:len(v)]=v;mask[i,:len(u)]=1
72
+ t=time.perf_counter()
73
+ out[start:start+len(p)]=m(torch.from_numpy(ids),torch.from_numpy(mask),torch.from_numpy(tt)).numpy()
74
+ times.append(time.perf_counter()-t)
75
+ if start and start%(batch*35)==0:print(f' scored {start}/{len(pairs)} pairs',flush=True)
76
+ return out,{'total_forward_s':float(sum(times)),'forward_ms_per_pair':float(sum(times)*1000/max(len(pairs),1)),'batches':len(times)}
77
+
78
+ def coverage(scores,items,k):
79
+ y=np.zeros((len(items),10),np.uint8)
80
+ for i,r in enumerate(items):
81
+ gold={x.casefold().strip() for x,_ in r['supporting_facts']}
82
+ for j,(title,_) in enumerate(r['context']):y[i,j]=int(title.casefold().strip() in gold)
83
+ orders=np.argsort(-scores,axis=1,kind='stable')
84
+ return np.asarray([int(y[i,orders[i,:k]].sum()==y[i].sum() and y[i].sum()>0) for i in range(len(items))]),orders
85
+
86
+ def run(ds,m,tok,batch=16,max_q=None):
87
+ start=time.perf_counter()
88
+ items,_=(hotpot_data() if ds=='hotpot' else wiki_data())
89
+ test=items[200:800];n=len(test)
90
+ if max_q is not None:n=min(n,max_q);test=test[:n]
91
+ pairs=[(r['question'],title+'. '+ ' '.join(sentences)) for r in test for title,sentences in r['context']]
92
+ print(f'{ds}: scoring {n} heldout questions, {len(pairs)} pairs, seq_limit={tok.max_length}',flush=True)
93
+ ts=time.perf_counter();ranks,perf=score_pairs(m,tok,pairs,batch=batch)
94
+ ts=time.perf_counter()-ts; scores=ranks.reshape(n,10)
95
+ emb=np.load(ROOT/('hotpot_subset_out/hotpot_minilm_800.npz' if ds=='hotpot' else 'wiki_subset_out/wiki_minilm_800.npz'))
96
+ Q=emb['q'][200:200+n];D=emb['docs'][200:200+n];Fq=emb['facets'][200:200+n]
97
+ cosine=np.einsum('nd,nkd->nk',Q,D)
98
+ base3,b_order=coverage(cosine,test,3)
99
+ report=json.load(open(ROOT/('hotpot_subset_out/graph_results.json' if ds=='hotpot' else 'wiki_subset_out/graph_results.json')))
100
+ link,match=prep_links(test);docsim=np.einsum('nkd,njd->nkj',D,D); facsim=np.einsum('nmd,nkd->nmk',Fq,D)
101
+ gorders=np.asarray([graph_select(cosine[i],docsim[i],facsim[i],link[i],match[i],k=5,**report['parameters']) for i in range(n)])
102
+ truth=np.zeros((n,10),np.uint8)
103
+ for i,r in enumerate(test):
104
+ gold={x.casefold().strip() for x,_ in r['supporting_facts']}
105
+ for j,(title,_) in enumerate(r['context']):truth[i,j]=int(title.casefold().strip() in gold)
106
+ cross_order=np.argsort(-scores,axis=1,kind='stable')
107
+ def hit(order,k):return np.array([int(truth[i,order[i,:k]].sum()==truth[i].sum() and truth[i].sum()>0) for i in range(n)],np.int8)
108
+ methods={'cosine':b_order,'cross_encoder':cross_order,'evidence_graph':gorders}
109
+ metrics={name:{str(k):float(hit(order,k).mean()) for k in (1,2,3,4,5)} for name,order in methods.items()}
110
+ rng=np.random.default_rng(9027);boots=rng.integers(0,n,size=(3000,n));cis={}
111
+ a=hit(cross_order,3)
112
+ for name,order in methods.items():
113
+ if name=='cross_encoder':continue
114
+ delta=a-hit(order,3)
115
+ cis[name]={'difference':float(delta.mean()),'ci95':np.quantile(delta[boots].mean(axis=1),[.025,.975]).tolist(),'wins':int((delta>0).sum()),'losses':int((delta<0).sum()),'ties':int((delta==0).sum())}
116
+ result={'dataset':ds,'questions':n,'pairs':n*10,'model':'cross-encoder/ms-marco-MiniLM-L6-v2 pretrained original weights','max_wordpieces':tok.max_length,'batch_size':batch,'metrics_complete_gold_doc_coverage':metrics,'cross_encoder_minus':cis,'timing':{**perf,'total_scoring_seconds_with_tokenization':ts,'scoring_ms_per_question':ts*1000/n,'total_run_s':time.perf_counter()-start},'protocol':'Existing seeded held-out questions 200:800 (200 prior tune questions excluded). Same ten candidates. Gold labels used only to measure coverage. Graph selector retains previously tuned parameters.'}
117
+ out=ROOT/f'crossencoder_{ds}_{tok.max_length}_{n}_results.json';out.write_text(json.dumps(result,indent=2))
118
+ np.savez_compressed(ROOT/f'crossencoder_{ds}_{tok.max_length}_{n}_scores.npz',scores=scores,orders=cross_order)
119
+ print('RESULT',json.dumps(result,indent=2),flush=True)
120
+ return result
121
+
122
+ if __name__=='__main__':
123
+ ap=argparse.ArgumentParser();ap.add_argument('--datasets',nargs='+',default=['hotpot','wiki']);ap.add_argument('--max-length',type=int,default=256);ap.add_argument('--batch-size',type=int,default=16);ap.add_argument('--max-queries',type=int,default=None)
124
+ args=ap.parse_args();model=load_crossencoder();tokenizer=PairTokenizer(length=args.max_length)
125
+ for ds in args.datasets:run(ds,model,tokenizer,args.batch_size,args.max_queries)
reproduce/hotpot_subset_eval.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CPU-only evidence-completeness benchmark for HotpotQA distractor setting.
2
+ Uses frozen local MiniLM, no gold evidence in selection. Dataset's supporting facts evaluate chosen titles.
3
+ """
4
+ import os,time,re,json,sys,hashlib
5
+ os.environ['OPENBLAS_NUM_THREADS']='1';os.environ['OMP_NUM_THREADS']='4'
6
+ from pathlib import Path
7
+ import numpy as np
8
+ from minilm_cpu_pipeline import load_model,WordPiece,embed
9
+ DATA=Path('/mnt/data/hotpot_dev_distractor_v1.json');OUT=Path('/mnt/data/hotpot_subset_out');OUT.mkdir(exist_ok=True)
10
+ N_TUNE=200;N_TEST=600;RNG=20261030
11
+
12
+
13
+ def facets(q):
14
+ q=q.strip().rstrip('?')
15
+ # Query-provided pieces only; no gold evidence or topic annotations.
16
+ p=re.split(r'(?i)\b(?:and|or|whose|which|who|that|where|while|whereas)\b|[,;:]\s*',q)
17
+ p=[x.strip() for x in p if len(x.split())>=2]
18
+ if len(p)<2:
19
+ w=q.split();m=max(2,len(w)//2)
20
+ p=[' '.join(w[:m]),' '.join(w[m:])] if len(w)>=5 else [q,q]
21
+ return (p[:3]+[q]*3)[:3]
22
+
23
+ def safe_key(x):return re.sub(r'\s+',' ',x.strip().lower())
24
+
25
+ def load_data():
26
+ data=[r for r in json.load(open(DATA)) if len(r['context'])==10]; rng=np.random.default_rng(RNG)
27
+ # Sample from all subtypes without using labels for feature engineering.
28
+ ids=rng.permutation(len(data))[:N_TUNE+N_TEST]
29
+ items=[data[int(i)] for i in ids]
30
+ counts={}
31
+ for r in items:counts[r['type']]=counts.get(r['type'],0)+1
32
+ print('sample',len(items),'types',counts,flush=True)
33
+ return items,ids
34
+
35
+ def encode_data(items):
36
+ path=OUT/'hotpot_minilm_800.npz'
37
+ identity=hashlib.sha256('|'.join(r['_id'] for r in items).encode()).hexdigest()[:16]
38
+ if path.exists():
39
+ dat=np.load(path)
40
+ if str(dat['identity'])==identity:
41
+ print('loaded cached embeddings',flush=True)
42
+ return dat['q'],dat['docs'],dat['facets']
43
+ model=load_model();tok=WordPiece(length=128)
44
+ queries=[r['question'] for r in items]
45
+ docs=[title+'. '+ ' '.join(sentences) for r in items for title,sentences in r['context']]
46
+ ftexts=[t for r in items for t in facets(r['question'])]
47
+ print('encoding',len(queries),'queries',len(docs),'docs',len(ftexts),'aspects',flush=True)
48
+ st=time.perf_counter();q=embed(queries,model,tok,batch_size=24,callback=lambda i,n,t:print('q',i,n,round(t,1),flush=True) if i>0 else None)
49
+ print('query done',round(time.perf_counter()-st,1),flush=True)
50
+ d=embed(docs,model,tok,batch_size=24,callback=lambda i,n,t:print('docs',i,n,round(t,1),flush=True) if i>0 else None).reshape(len(items),10,384)
51
+ print('documents done',round(time.perf_counter()-st,1),flush=True)
52
+ f=embed(ftexts,model,tok,batch_size=24,callback=lambda i,n,t:print('facets',i,n,round(t,1),flush=True) if i>0 else None).reshape(len(items),3,384)
53
+ print('facets done',round(time.perf_counter()-st,1),flush=True)
54
+ np.savez_compressed(path,identity=identity,q=q,docs=d,facets=f)
55
+ return q,d,f
56
+
57
+ def greedy(rel,dc,fac,method,k=5,alpha=.6,beta=.2,threshold=.35):
58
+ # Single query score vector, 10x10 document similarity, facet-doc sim 3x10.
59
+ K=len(rel);sel=[];remain=set(range(K));
60
+ cfac=np.clip(fac-threshold,0,None)
61
+ mx=np.zeros(len(cfac));mxcluster=np.zeros(K)
62
+ for t in range(k):
63
+ best=(-1e9,None)
64
+ for j in sorted(remain):
65
+ if method=='cosine':v=rel[j]
66
+ elif method=='mmr':v=rel[j] if not sel else alpha*rel[j]-(1-alpha)*max(0,max(dc[j,x] for x in sel))
67
+ elif method=='coverage':v=alpha*rel[j]+(1-alpha)*np.maximum(cfac[:,j]-mx,0).mean()
68
+ elif method=='facility':
69
+ weights=np.clip(rel,0,None)**2
70
+ v=alpha*rel[j]+(1-alpha)*np.dot(weights,np.maximum(np.clip(dc[:,j],0,None)-mxcluster,0))/max(weights.sum(),1e-8)
71
+ elif method=='bridge':
72
+ bridge=max((dc[j,x] for x in sel),default=0)
73
+ v=alpha*rel[j]+(1-alpha)*bridge-beta*max(0,bridge-.8) if sel else rel[j]
74
+ elif method=='combined':
75
+ gain=np.maximum(cfac[:,j]-mx,0).mean()
76
+ # Complementary candidate information, plus query-conditioned facet coverage.
77
+ weights=np.clip(rel,0,None)**2
78
+ facility=np.dot(weights,np.maximum(np.clip(dc[:,j],0,None)-mxcluster,0))/max(weights.sum(),1e-8)
79
+ v=alpha*rel[j]+(1-alpha)*(beta*gain+(1-beta)*facility)
80
+ else:raise ValueError(method)
81
+ if v>best[0]:best=(v,j)
82
+ j=best[1];sel.append(j);remain.remove(j)
83
+ mx=np.maximum(mx,cfac[:,j]);mxcluster=np.maximum(mxcluster,np.clip(dc[:,j],0,None))
84
+ return sel
85
+
86
+ def eval_one(items,scores,docsim,facsim,method,params,inds,ks=(2,3,4,5)):
87
+ out=np.zeros((len(inds),len(ks)),float); selected=[];goldcounts=[];omiss=[]
88
+ for ii,i in enumerate(inds):
89
+ r=items[i]; gold=set(safe_key(t) for t,_ in r['supporting_facts']);titles=[safe_key(x[0]) for x in r['context']]
90
+ goldcounts.append(len(gold));possible=gold.issubset(set(titles));omiss.append(possible)
91
+ order=greedy(scores[i],docsim[i],facsim[i],method,k=max(ks),**params)
92
+ selected.append(order)
93
+ for jj,k in enumerate(ks):out[ii,jj]=int(possible and gold.issubset(set(titles[x] for x in order[:k])))
94
+ return out,np.asarray(selected),np.asarray(goldcounts),np.asarray(omiss)
95
+
96
+ def main():
97
+ items,orig_idx=load_data();q,d,f=encode_data(items)
98
+ sims=np.einsum('nd,nkd->nk',q,d).astype('float32');dcs=np.einsum('nkd,njd->nkj',d,d).astype('float32');fs=np.einsum('nmd,nkd->nmk',f,d).astype('float32')
99
+ tunes=np.arange(N_TUNE);tests=np.arange(N_TUNE,N_TUNE+N_TEST)
100
+ param_grid={
101
+ 'cosine':[{}],
102
+ 'mmr':[{'alpha':a} for a in [.3,.5,.7,.85,.95]],
103
+ 'coverage':[{'alpha':a,'threshold':t} for a in [.1,.3,.5,.7,.9] for t in [.2,.35,.5,.65]],
104
+ 'facility':[{'alpha':a} for a in [.2,.4,.6,.8,.95]],
105
+ 'bridge':[{'alpha':a,'beta':b} for a in [.4,.6,.8] for b in [0,.2,.4]],
106
+ 'combined':[{'alpha':a,'beta':b,'threshold':t} for a in [.2,.4,.6,.8] for b in [.25,.5,.75] for t in [.3,.5]],
107
+ }
108
+ best={};val={}
109
+ for method,grid in param_grid.items():
110
+ st=time.perf_counter();records=[]
111
+ for params in grid:
112
+ arr,_,_,_=eval_one(items,sims,dcs,fs,method,params,tunes)
113
+ records.append((float(arr[:,1].mean()),float(arr[:,2].mean()),params))
114
+ records.sort(key=lambda x:(x[0],x[1]),reverse=True)
115
+ best[method]=records[0][2];val[method]=records[0][0]
116
+ print('TUNED',method,'@3',records[0][0],best[method],'seconds',round(time.perf_counter()-st,1),flush=True)
117
+ results={};test_arrays={};selections={}
118
+ for method,params in best.items():
119
+ st=time.perf_counter();arr,idx,ngold,oracle=eval_one(items,sims,dcs,fs,method,params,tests)
120
+ ms=(time.perf_counter()-st)/len(tests)*1000
121
+ results[method]={'parameters':params,'validation_complete_evidence_at3':val[method],'heldout_complete_evidence_at_k':{str(k):float(arr[:,j].mean()) for j,k in enumerate((2,3,4,5))},'selection_cpu_ms_per_question':ms,'gold_title_count_distribution':{str(x):int((ngold==x).sum()) for x in np.unique(ngold)},'oracle_coverage_rate':float(oracle.mean())}
122
+ test_arrays[method]=arr;selections[method]=idx
123
+ print('TEST',method,results[method]['heldout_complete_evidence_at_k'],'ms',round(ms,3),flush=True)
124
+ base=test_arrays['cosine'];rng=np.random.default_rng(RNG)
125
+ bs=rng.integers(len(tests),size=(2000,len(tests)))
126
+ for method in best:
127
+ delta=test_arrays[method][:,1]-base[:,1]
128
+ ci=np.quantile(delta[bs].mean(axis=1),[.025,.975]).tolist()
129
+ results[method]['delta_at3_vs_cosine']=float(delta.mean());results[method]['delta_at3_ci95']=ci
130
+ results[method]['paired_win_loss_at3']={'wins':int((delta>0).sum()),'losses':int((delta<0).sum()),'ties':int((delta==0).sum())}
131
+ ids=[items[i]['_id'] for i in tests];details=[]
132
+ for j,i in enumerate(tests):
133
+ r=items[i]; details.append({'id':r['_id'],'question':r['question'],'answer':r['answer'],'type':r['type'],'gold_support_titles':sorted(set(t for t,_ in r['supporting_facts'])),'cosine': [r['context'][v][0] for v in selections['cosine'][j]],'coverage':[r['context'][v][0] for v in selections['coverage'][j]],'combined':[r['context'][v][0] for v in selections['combined'][j]]})
134
+ result={'dataset':'HotpotQA dev distractor','source_file':str(DATA),'total_dev':7405,'sample_count':len(items),'tune_count':N_TUNE,'heldout_count':N_TEST,'random_seed':RNG,'protocol':'Each question has own ten provided paragraphs; gold supporting passage titles are evaluation only; frozen MiniLM encoded whole query, each candidate passage, and three heuristic query facets; hyperparameters chosen on 250 dev cases; tested on separate 750 dev cases. No generative LLM was invoked.','results':results}
135
+ (OUT/'hotpot_results.json').write_text(json.dumps(result,indent=2));(OUT/'hotpot_case_studies.json').write_text(json.dumps(details,indent=2));print('DONE',flush=True)
136
+ if __name__=='__main__':main()
reproduce/minilm_cpu_pipeline.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CPU-only MiniLM-L6 embedder from uploaded safetensors; no transformers/tokenizers internet packages.
2
+ Reproduces BERT uncased WordPiece (English corpus) and masked mean pooling + L2 normalization.
3
+ """
4
+ import os,re,unicodedata,json,time,functools, hashlib
5
+ import numpy as np
6
+ import torch
7
+ from torch import nn
8
+ from torch.nn import functional as F
9
+ from safetensors.torch import load_file
10
+
11
+ ROOT='/mnt/data'
12
+ class WordPiece:
13
+ def __init__(self,path=ROOT+'/vocab.txt',length=128):
14
+ self.vocab={s.rstrip('\n'):i for i,s in enumerate(open(path,encoding='utf8'))};self.max_length=length
15
+ @functools.lru_cache(maxsize=200000)
16
+ def wp(self,word):
17
+ if word in self.vocab:return [self.vocab[word]]
18
+ if len(word)>100:return [100]
19
+ a=[];s=0
20
+ while s<len(word):
21
+ e=len(word);cur=None
22
+ while s<e:
23
+ t=word[s:e] if s==0 else '##'+word[s:e]
24
+ if t in self.vocab:cur=self.vocab[t];break
25
+ e-=1
26
+ if cur is None:return [100]
27
+ a.append(cur);s=e
28
+ return a
29
+ def encode(self,text):
30
+ # BertNormalizer: NFD accent stripping, lowercasing, splitting punctuation.
31
+ text=''.join(ch for ch in unicodedata.normalize('NFD',text.lower()) if unicodedata.category(ch)!='Mn' and (ch.isspace() or unicodedata.category(ch)[0]!='C'))
32
+ ret=[101]
33
+ for x in re.findall(r'[^\W_]+|_|[^\w\s]',text,flags=re.UNICODE):
34
+ ret.extend(self.wp(x))
35
+ if len(ret)>=self.max_length-1:break
36
+ return ret[:self.max_length-1]+[102]
37
+
38
+ class BertEmb(nn.Module):
39
+ def __init__(self):
40
+ super().__init__()
41
+ self.word_embeddings=nn.Embedding(30522,384);self.position_embeddings=nn.Embedding(512,384);self.token_type_embeddings=nn.Embedding(2,384);self.LayerNorm=nn.LayerNorm(384,eps=1e-12)
42
+ self.register_buffer('position_ids',torch.arange(512).unsqueeze(0),persistent=False)
43
+ def forward(self,x):return self.LayerNorm(self.word_embeddings(x)+self.position_embeddings(self.position_ids[:,:x.shape[1]])+self.token_type_embeddings(torch.zeros_like(x)))
44
+ class SelfAtt(nn.Module):
45
+ def __init__(self):
46
+ super().__init__();self.query=nn.Linear(384,384);self.key=nn.Linear(384,384);self.value=nn.Linear(384,384)
47
+ def forward(self,x,mask):
48
+ b,s,_=x.shape;q=self.query(x).view(b,s,12,32).transpose(1,2);k=self.key(x).view(b,s,12,32).transpose(1,2);v=self.value(x).view(b,s,12,32).transpose(1,2)
49
+ a=F.scaled_dot_product_attention(q,k,v,attn_mask=mask,dropout_p=0.0)
50
+ return a.transpose(1,2).contiguous().view(b,s,384)
51
+ class AttOutput(nn.Module):
52
+ def __init__(self):
53
+ super().__init__();self.dense=nn.Linear(384,384);self.LayerNorm=nn.LayerNorm(384,eps=1e-12)
54
+ def forward(self,x,res):return self.LayerNorm(self.dense(x)+res)
55
+ class Attention(nn.Module):
56
+ def __init__(self):
57
+ super().__init__();setattr(self,'self',SelfAtt());self.output=AttOutput()
58
+ def forward(self,x,mask):return self.output(getattr(self,'self')(x,mask),x)
59
+ class FFNMid(nn.Module):
60
+ def __init__(self):super().__init__();self.dense=nn.Linear(384,1536)
61
+ def forward(self,x):return F.gelu(self.dense(x),approximate='none')
62
+ class FFNOut(nn.Module):
63
+ def __init__(self):super().__init__();self.dense=nn.Linear(1536,384);self.LayerNorm=nn.LayerNorm(384,eps=1e-12)
64
+ def forward(self,x,res):return self.LayerNorm(self.dense(x)+res)
65
+ class Block(nn.Module):
66
+ def __init__(self):super().__init__();self.attention=Attention();self.intermediate=FFNMid();self.output=FFNOut()
67
+ def forward(self,x,mask):
68
+ a=self.attention(x,mask);return self.output(self.intermediate(a),a)
69
+ class Enc(nn.Module):
70
+ def __init__(self):super().__init__();self.layer=nn.ModuleList([Block() for _ in range(6)])
71
+ def forward(self,x,mask):
72
+ for l in self.layer:x=l(x,mask)
73
+ return x
74
+ class MiniLM(nn.Module):
75
+ def __init__(self):
76
+ super().__init__();self.embeddings=BertEmb();self.encoder=Enc()
77
+ def forward(self,x,m):
78
+ mask=m[:,None,None,:].to(torch.bool);h=self.encoder(self.embeddings(x),mask)
79
+ p=(h*m.unsqueeze(-1)).sum(1)/m.sum(1).unsqueeze(1).clamp(min=1)
80
+ return F.normalize(p,dim=1)
81
+ def load_model():
82
+ torch.set_num_threads(min(5,os.cpu_count() or 5));m=MiniLM()
83
+ sd=load_file(ROOT+'/model.safetensors',device='cpu');del sd['embeddings.position_ids'];del sd['pooler.dense.bias'];del sd['pooler.dense.weight']
84
+ missing,unexpected=m.load_state_dict(sd,strict=True);print('MiniLM verified: missing',missing,'unexpected',unexpected,flush=True);m.eval();return m
85
+ @torch.no_grad()
86
+ def embed(texts,model,tok,batch_size=24,callback=None):
87
+ out=np.empty((len(texts),384),np.float32);t0=time.perf_counter()
88
+ for start in range(0,len(texts),batch_size):
89
+ chunks=[tok.encode(t) for t in texts[start:start+batch_size]]
90
+ L=max(len(c) for c in chunks);batch=np.zeros((len(chunks),L),np.int64);mask=np.zeros((len(chunks),L),np.int64)
91
+ for i,c in enumerate(chunks):batch[i,:len(c)]=c;mask[i,:len(c)]=1
92
+ out[start:start+len(chunks)]=model(torch.from_numpy(batch),torch.from_numpy(mask)).numpy()
93
+ if callback and (start//batch_size)%25==0:callback(start,len(texts),time.perf_counter()-t0)
94
+ return out
95
+ if __name__=='__main__':
96
+ m=load_model();t=WordPiece();x=['The quantum machine learned to retrieve scientific evidence.','I love astronomy and telescope images.'];print(t.encode(x[0]));print(embed(x,m,t));
reproduce/neural_crossencoder_only.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MS MARCO MiniLM cross-encoder, loaded from uploaded weights. CPU, offline."""
2
+ import os,unicodedata,re,time
3
+ import numpy as np
4
+ import torch
5
+ from torch import nn
6
+ from safetensors.torch import load_file
7
+ from minilm_cpu_pipeline import BertEmb,Enc,WordPiece
8
+ from neural_evidence_graph_core import ROOT
9
+
10
+ class BertCrossEncoder(nn.Module):
11
+ def __init__(self):
12
+ super().__init__();self.embeddings=BertEmb();self.encoder=Enc()
13
+ self.pooler=nn.Linear(384,384);self.classifier=nn.Linear(384,1)
14
+ def forward(self,ids,mask,types):
15
+ pos=self.embeddings.position_ids[:,:ids.shape[1]]
16
+ h=self.embeddings.LayerNorm(self.embeddings.word_embeddings(ids)+self.embeddings.position_embeddings(pos)+self.embeddings.token_type_embeddings(types))
17
+ h=self.encoder(h,mask[:,None,None,:].bool())
18
+ return self.classifier(torch.tanh(self.pooler(h[:,0,:]))).squeeze(-1)
19
+
20
+ def load_crossencoder():
21
+ torch.set_num_threads(min(4,os.cpu_count() or 4))
22
+ try:torch.set_num_interop_threads(1)
23
+ except RuntimeError:pass
24
+ m=BertCrossEncoder();raw=load_file(str(ROOT/'model.safetensors'),device='cpu');clean={}
25
+ for k,v in raw.items():
26
+ if k=='bert.embeddings.position_ids':continue
27
+ if k.startswith('bert.pooler.dense.'):k='pooler.'+k[len('bert.pooler.dense.'):]
28
+ elif k.startswith('bert.'):k=k[5:]
29
+ clean[k]=v
30
+ m.load_state_dict(clean,strict=True);m.eval()
31
+ print('Loaded original pretrained MS MARCO model, tensors',len(clean),flush=True)
32
+ return m
33
+
34
+ class PairTokenizer(WordPiece):
35
+ def __init__(self,length=256):super().__init__(str(ROOT/'vocab.txt'),length)
36
+ def tokens(self,text):
37
+ text=''.join(ch for ch in unicodedata.normalize('NFD',text.lower()) if unicodedata.category(ch)!='Mn' and (ch.isspace() or unicodedata.category(ch)[0]!='C'))
38
+ out=[]
39
+ for x in re.findall(r'[^\W_]+|_|[^\w\s]',text,flags=re.UNICODE):out.extend(self.wp(x))
40
+ return out
41
+ def pair(self,q,d):
42
+ a=self.tokens(q);b=self.tokens(d);cap=self.max_length-3
43
+ while len(a)+len(b)>cap:
44
+ if len(b)>len(a):b.pop()
45
+ else:a.pop()
46
+ ids=[101]+a+[102]+b+[102]
47
+ tt=[0]*(len(a)+2)+[1]*(len(b)+1)
48
+ return ids,tt
49
+
50
+ @torch.inference_mode()
51
+ def score_pairs(m,tok,pairs,batch=20):
52
+ out=np.empty(len(pairs),np.float32);times=[]
53
+ for start in range(0,len(pairs),batch):
54
+ p=pairs[start:start+batch];inputs=[tok.pair(q,d) for q,d in p]
55
+ L=max(len(x[0]) for x in inputs)
56
+ ids=np.zeros((len(p),L),np.int64);tt=np.zeros_like(ids);mask=np.zeros_like(ids)
57
+ for i,(u,v) in enumerate(inputs):ids[i,:len(u)]=u;tt[i,:len(v)]=v;mask[i,:len(u)]=1
58
+ t=time.perf_counter();out[start:start+len(p)]=m(torch.from_numpy(ids),torch.from_numpy(mask),torch.from_numpy(tt)).numpy();times.append(time.perf_counter()-t)
59
+ if start and start%(batch*35)==0:print('scored',start,'/',len(pairs),flush=True)
60
+ return out,{'total_forward_s':float(sum(times)),'forward_ms_per_pair':float(sum(times)*1000/max(1,len(pairs))),'batches':len(times)}
reproduce/neural_evidence_graph_core.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Graph selector and benchmark loaders. Contains NO TF-IDF, BM25, or lexical retrieval."""
2
+ import re,unicodedata,json
3
+ from pathlib import Path
4
+ import numpy as np
5
+ ROOT=Path('/mnt/data')
6
+
7
+ def norm(s):
8
+ return ' '.join(re.sub(r'[^\w\s]',' ',unicodedata.normalize('NFKD',str(s)).casefold()).split())
9
+
10
+ def seeded_sample(xs,n,seed=20261030):
11
+ ix=np.random.default_rng(seed).permutation(len(xs))[:n]
12
+ return [xs[int(i)] for i in ix]
13
+
14
+ def doc(title,body,ident=None):return {'title':title,'body':str(body),'id':str(title if ident is None else ident)}
15
+
16
+ def hover(n):
17
+ h={r['_id']:r for r in json.load(open(ROOT/'hotpot_dev_distractor_v1.json'))}
18
+ v=json.load(open(ROOT/'hover_dev_release_v1.1.txt'));xs=[]
19
+ for r in v:
20
+ if r['hpqa_id'] not in h:continue
21
+ hr=h[r['hpqa_id']]
22
+ titles={t.casefold().strip() for t,_ in hr['context']}
23
+ gold={t.casefold().strip() for t,_ in r['supporting_facts']}
24
+ if not gold.issubset(titles):continue
25
+ xs.append({'qid':r['uid'],'query':r['claim'],
26
+ 'docs':[doc(t,' '.join(sentences),t.casefold().strip()) for t,sentences in hr['context']],
27
+ 'gold':gold, 'pool':'10 HotpotQA distractor passages, only HoVer claims with all support titles present',
28
+ 'level':'passage','label':r['label']})
29
+ return seeded_sample(xs,n)
30
+
31
+ def graph_arrays(row):
32
+ q=norm(row['query']);titles=[norm(d['title']) for d in row['docs']]
33
+ bodies=[norm(d['body']) for d in row['docs']];qws=set(q.split());D=len(titles)
34
+ link=np.zeros((D,D),np.float32);match=np.zeros(D,np.float32)
35
+ for j,t in enumerate(titles):
36
+ if not t:continue
37
+ common=len(set(t.split())&qws)/max(1,len(set(t.split())))
38
+ match[j]=1. if ' '+t+' ' in ' '+q+' ' else .5*common
39
+ for i,body in enumerate(bodies):
40
+ if i!=j and len(t)>=4 and ' '+t+' ' in ' '+body+' ':link[i,j]=1
41
+ return link,match
42
+
43
+ def normalize_scores(s):
44
+ s=np.asarray(s,dtype=np.float32);lo=float(s.min());hi=float(s.max())
45
+ return (s-lo)/(hi-lo) if hi>lo+1e-9 else np.zeros_like(s)
46
+
47
+ def graph_select(s,link,match,k=8):
48
+ s=normalize_scores(s);selected=[];remaining=set(range(len(s)))
49
+ for _ in range(min(k,len(s))):
50
+ nxt=max(remaining,key=lambda j:(.4*s[j]+.5*max((link[i,j] for i in selected),default=0.)+.2*match[j],-j))
51
+ selected.append(nxt);remaining.remove(nxt)
52
+ return selected
reproduce/neural_musique_corrected.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ os.environ['OPENBLAS_NUM_THREADS']='1';os.environ['OMP_NUM_THREADS']='4'
3
+ import json,time,sys
4
+ from pathlib import Path
5
+ import numpy as np
6
+ import torch
7
+ from safetensors.torch import load_file
8
+ from minilm_cpu_pipeline import MiniLM,WordPiece,embed
9
+ from neural_evidence_graph_core import graph_arrays,graph_select,normalize_scores,seeded_sample,doc,ROOT
10
+
11
+ def musique(n):
12
+ rows=[json.loads(x) for x in open(ROOT/'musique_ans_v1.0_dev.jsonl.txt') if x.strip()]
13
+ rows=seeded_sample(rows,n)
14
+ return [{'qid':x['id'],'query':x['question'],'docs':[doc(p['title'],p['paragraph_text'],p['idx']) for p in x['paragraphs']],'gold':{str(p['idx']) for p in x['paragraphs'] if p['is_supporting']}} for x in rows]
15
+ P=Path('/mnt/data');n=300
16
+ rows=musique(n)
17
+ model=MiniLM();sd=load_file(str(P/'original_minilm/model.safetensors'));[sd.pop(k) for k in ['embeddings.position_ids','pooler.dense.bias','pooler.dense.weight']];model.load_state_dict(sd,strict=True);model.eval();torch.set_num_threads(4)
18
+ print('Loaded ORIGINAL sentence embedding model, questions',len(rows),flush=True)
19
+ tok=WordPiece(str(P/'vocab.txt'),length=128)
20
+ texts=[r['query'] for r in rows]+[d['title']+'. '+d['body'] for r in rows for d in r['docs']]
21
+ t=time.perf_counter(); embs=embed(texts,model,tok,batch_size=32,callback=lambda i,n,sec:print('Embedded',i,'of',n,'in',round(sec,1),'s',flush=True));tt=time.perf_counter()-t
22
+ Q=embs[:len(rows)]; parts=np.split(embs[len(rows):],np.cumsum([len(r['docs']) for r in rows])[:-1]); D=[x for x in parts]
23
+ sim=[D[i]@Q[i] for i in range(len(rows))]
24
+ cross=np.load(P/'four_benchmark_musique_scores.npz')['crossencoder']
25
+ assert cross.shape[0]==len(rows)
26
+ methods=['original_minilm_cosine','msmarco_crossencoder','original_minilm_graph','msmarco_graph']
27
+ counts={m:{k:[] for k in (2,3,5,8)} for m in methods}
28
+ for i,r in enumerate(rows):
29
+ links,match=graph_arrays(r); gold=r['gold']; docs=r['docs'];
30
+ orders={'original_minilm_cosine':np.argsort(-sim[i],kind='stable').tolist(),'msmarco_crossencoder':np.argsort(-cross[i,:len(docs)],kind='stable').tolist(),'original_minilm_graph':graph_select(sim[i],links,match,8),'msmarco_graph':graph_select(cross[i,:len(docs)],links,match,8)}
31
+ for m,rank in orders.items():
32
+ for k in (2,3,5,8):counts[m][k].append(int(gold.issubset({docs[x]['id'] for x in rank[:k]})))
33
+ report={'dataset':'musique','n':len(rows),'original_embedding_checkpoint':'all-MiniLM-L6-v2, originally user uploaded','cross_encoder_checkpoint':'ms-marco-MiniLM-L6-v2, originally user uploaded','original_embedding_seconds':tt,'protocol':'same 300 seeded MuSiQue queries and 20 candidate passages as previous run. No TF-IDF in either ranking or selection; cross scores reused exactly from pretrained model inference. Fixed graph parameters .4 relevance + .5 directed linkage + .2 query-title match (scores min-max normalized per query). Gold is only used in evaluation.','coverage':{m:{str(k):round(100*np.mean(vals),2) for k,vals in cs.items()} for m,cs in counts.items()}}
34
+ (P/'musique_NEURAL_ONLY_results.json').write_text(json.dumps(report,indent=2))
35
+ print('RESULT',json.dumps(report,indent=2),flush=True)
reproduce/neural_three_benchmarks.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Neural-only evaluation for three user-uploaded RAG/evidence datasets.
2
+ All ranking uses original MiniLM cosine / pretrained MS-MARCO cross encoder.
3
+ Graph is a fixed previously specified directed-link/title selection policy; gold is
4
+ never accessed by scoring/selection. QASC has TWO explicitly separate protocols.
5
+ """
6
+ import os
7
+ os.environ['OPENBLAS_NUM_THREADS']='1'; os.environ['MKL_NUM_THREADS']='1'; os.environ['OMP_NUM_THREADS']='4'
8
+ import json, time, struct, collections, sys
9
+ from pathlib import Path
10
+ import numpy as np
11
+ import torch
12
+ from safetensors.torch import load_file
13
+ from minilm_cpu_pipeline import MiniLM, WordPiece, embed
14
+ from neural_crossencoder_only import load_crossencoder,PairTokenizer,score_pairs
15
+ from neural_evidence_graph_core import (hover,graph_arrays,graph_select,seeded_sample,doc,ROOT)
16
+ from parquet_lite import read_col,reconstruct
17
+ from parquet_probe import parse
18
+
19
+ K=(2,3,5,8)
20
+
21
+ def get_dense():
22
+ m=MiniLM();sd=load_file(str(ROOT/'original_minilm/model.safetensors'))
23
+ for key in ('embeddings.position_ids','pooler.dense.bias','pooler.dense.weight'):sd.pop(key)
24
+ m.load_state_dict(sd,strict=True);m.eval();torch.set_num_threads(4)
25
+ return m,WordPiece(str(ROOT/'vocab.txt'),length=128)
26
+
27
+ def embed_texts(texts,m,tok,batch=32,label=''):
28
+ t=time.perf_counter();out=embed(texts,m,tok,batch_size=batch)
29
+ print('DENSE_EMBED',label,len(texts),'seconds',round(time.perf_counter()-t,3),flush=True)
30
+ return out,time.perf_counter()-t
31
+
32
+ def get_qasc():
33
+ p=ROOT/'train-00000-of-00001.parquet';f=open(p,'rb');f.seek(-8,2)
34
+ size=struct.unpack('<I',f.read(4))[0];f.seek(-8-size,2);meta=parse(f.read(size));rows=[]
35
+ for rg in meta[4]:
36
+ cols={}
37
+ for c in rg[1]:
38
+ key=tuple(c[3][3])
39
+ if key not in [('id',),('question',),('fact1',),('fact2',)]:continue
40
+ cols[key]=reconstruct(*read_col(f,c,{key:(1,0)}),1,0)
41
+ rows.extend([{'id':cols[('id',)][i], 'question':cols[('question',)][i], 'facts':[cols[('fact1',)][i],cols[('fact2',)][i]]} for i in range(rg[3])])
42
+ rows=[r for r in rows if all(x and str(x).strip() for x in r['facts']) and r['facts'][0]!=r['facts'][1]]
43
+ facts=list(dict.fromkeys(z.strip() for r in rows for z in r['facts']))
44
+ return rows,facts
45
+
46
+ def prepare_multihop(n,m,tok):
47
+ t=time.perf_counter();corpus=json.load(open(ROOT/'corpus.json'));allq=json.load(open(ROOT/'MultiHopRAG.json'))
48
+ eligible=[r for r in allq if len({e['url'] for e in r['evidence_list']})>=2]
49
+ questions=seeded_sample(eligible,n)
50
+ texts=[r['title']+'. '+r['body'][:1800] for r in corpus]
51
+ de,tidx=embed_texts(texts,m,tok,label='MultiHop corpus')
52
+ qe,tq=embed_texts([r['query'] for r in questions],m,tok,label='MultiHop queries')
53
+ sim=qe@de.T;inds=np.argsort(-sim,axis=1,kind='stable')[:,:20]
54
+ rows=[];sc=[]
55
+ for i,r in enumerate(questions):
56
+ docs=[doc(corpus[j]['title'],corpus[j]['body'],corpus[j]['url']) for j in inds[i]]
57
+ rows.append({'qid':f'multihop:{i}', 'query':r['query'],'docs':docs,'gold':{e['url'] for e in r['evidence_list']},'pool':'Top-20 neural MiniLM retrieval from full 609-article corpus (no gold insertion)'})
58
+ sc.append(sim[i,inds[i]])
59
+ return rows,sc,{'index_seconds':tidx,'query_embedding_seconds':tq,'total_prep_seconds':time.perf_counter()-t,'index_size':len(corpus),'candidate_pool_size':20}
60
+
61
+ def prepare_hover(n,m,tok):
62
+ t=time.perf_counter();rows=hover(n)
63
+ # Exact 10 candidate passages; labels sourced from corresponding HoVer claims.
64
+ alltexts=[r['query'] for r in rows]+[d['title']+'. '+d['body'] for r in rows for d in r['docs']]
65
+ em,sec=embed_texts(alltexts,m,tok,label='HoVer queries+passages')
66
+ Q=em[:len(rows)]; Ds=np.split(em[len(rows):],np.cumsum([len(r['docs']) for r in rows])[:-1]);sc=[Ds[i]@Q[i] for i in range(len(rows))]
67
+ return rows,sc,{'embedding_seconds':sec,'prep_seconds':time.perf_counter()-t,'candidate_pool_size':10,'eligibility':'HoVer dev claims with HotpotQA dev passages available and all annotated support titles present; conditional subset'}
68
+
69
+ def prepare_qasc(n,m,tok):
70
+ t=time.perf_counter();allrows,facts=get_qasc();qs=seeded_sample(allrows,n)
71
+ fe,tidx=embed_texts(facts,m,tok,label='QASC all distinct supporting fact sentences')
72
+ qe,tq=embed_texts([r['question'] for r in qs],m,tok,label='QASC query')
73
+ sim=qe@fe.T;inds=np.argsort(-sim,axis=1,kind='stable')[:,:60]
74
+ rows_native=[];scores_native=[];rows_ctrl=[];scores_ctrl=[]
75
+ for i,r in enumerate(qs):
76
+ gold={str(x).strip() for x in r['facts']}
77
+ docs=[doc('',facts[int(j)],facts[int(j)]) for j in inds[i,:20]]
78
+ rows_native.append({'qid':r['id'],'query':r['question'],'docs':docs,'gold':gold,'pool':'Top-20 MiniLM from all unique QASC supporting fact sentences, without gold insertion'})
79
+ scores_native.append(sim[i,inds[i,:20]])
80
+ neg=[int(j) for j in inds[i] if facts[int(j)] not in gold][:18]
81
+ if len(neg)!=18:continue
82
+ chosen=list(gold)+[facts[j] for j in neg]
83
+ perm=np.random.default_rng(i+301).permutation(len(chosen))
84
+ chosen=[chosen[int(j)] for j in perm]
85
+ look={f:j for j,f in enumerate(facts)}
86
+ rows_ctrl.append({'qid':r['id'],'query':r['question'],'docs':[doc('',z,z) for z in chosen],'gold':gold,'pool':'CONTROLLED: 2 gold facts deliberately inserted + 18 MiniLM hard negatives (oracle=100%)'})
87
+ scores_ctrl.append(np.array([sim[i,look[z]] for z in chosen],np.float32))
88
+ return (rows_native,scores_native),(rows_ctrl,scores_ctrl),{'index_seconds':tidx,'query_embedding_seconds':tq,'total_prep_seconds':time.perf_counter()-t,'fact_corpus_size':len(facts),'eligible_questions':len(allrows),'candidate_pool_size':20}
89
+
90
+ def assess(name,rows,dense_scores,cross_scores,loadtime,extra):
91
+ tgraph=time.perf_counter();details=[]
92
+ counts={method:{str(k):[] for k in K} for method in ('dense_cosine','crossencoder','dense_graph','crossencoder_graph')}
93
+ oracle={str(k):[] for k in K};av=[];links=[];sizecnt=collections.Counter()
94
+ for i,r in enumerate(rows):
95
+ gold=r['gold'];docs=r['docs'];ss=np.asarray(dense_scores[i],dtype=np.float32);cc=np.asarray(cross_scores[i],dtype=np.float32)
96
+ assert len(ss)==len(cc)==len(docs)
97
+ link,match=graph_arrays(r);links.append(bool(link.any()))
98
+ avail=len(gold&{d['id'] for d in docs});av.append(avail==len(gold));sizecnt[len(gold)]+=1
99
+ orders={
100
+ 'dense_cosine':np.argsort(-ss,kind='stable').tolist(),
101
+ 'crossencoder':np.argsort(-cc,kind='stable').tolist(),
102
+ 'dense_graph':graph_select(ss,link,match,max(K)),
103
+ 'crossencoder_graph':graph_select(cc,link,match,max(K)),
104
+ }
105
+ record={'id':r['qid'],'gold_count':len(gold),'pool_gold_count':avail,'orders':orders,'hits':{}}
106
+ for k in K:
107
+ oracle[str(k)].append(int(avail==len(gold) and len(gold)<=k))
108
+ for method,order in orders.items():
109
+ hit=int(gold.issubset({docs[j]['id'] for j in order[:k]}))
110
+ counts[method][str(k)].append(hit)
111
+ record['hits'].setdefault(method,{})[str(k)]=hit
112
+ details.append(record)
113
+ graphseconds=time.perf_counter()-tgraph
114
+ delta=np.array(counts['crossencoder_graph']['3'],int)-np.array(counts['crossencoder']['3'],int)
115
+ if len(delta):
116
+ gen=np.random.default_rng(31);ci=np.percentile(delta[gen.integers(0,len(delta),(2000,len(delta)))].mean(axis=1)*100,[2.5,97.5])
117
+ else:ci=[float('nan')]*2
118
+ res={'dataset':name,'questions':len(rows),'candidate_count_total':sum(len(r['docs']) for r in rows),'candidate_pool':rows[0]['pool'] if rows else '',
119
+ 'gold_support_count_distribution':dict(sizecnt),'gold_available_fraction':round(float(np.mean(av))*100,2),'gold_available_count':sum(av), 'graph_link_fraction':round(float(np.mean(links))*100,2),
120
+ 'oracle_complete_coverage_pct':{k:round(float(np.mean(v))*100,2) for k,v in oracle.items()},
121
+ 'complete_coverage_pct':{met:{k:round(float(np.mean(val))*100,2) for k,val in d.items()} for met,d in counts.items()},
122
+ 'crossencoder_graph_minus_crossencoder_at3_pp':round(float(delta.mean()*100),2),'ci95_pp':[round(float(z),2) for z in ci],
123
+ 'paired_wins_losses_ties_at3':{'wins':int((delta>0).sum()),'losses':int((delta<0).sum()),'ties':int((delta==0).sum())},
124
+ 'timing_seconds':{**extra,**loadtime,'graph_selection_total_s':round(graphseconds,3),'graph_selection_ms_per_question':round(graphseconds*1000/max(1,len(rows)),2)},
125
+ 'models':{'dense':'user uploaded all-MiniLM-L6-v2', 'crossencoder':'user uploaded cross-encoder/ms-marco-MiniLM-L6-v2'},
126
+ 'protocol':'MiniLM embeddings at max 128 WordPiece for dense. Cross-encoder truncates paired query+passage at 256 WordPiece. Both score exact same candidate pools. Fixed evidence graph .4 minmax relevance + .5 title in already-selected passage + .2 query-title score. No TF-IDF anywhere in candidate construction or scoring. Gold used solely for pool audit and evaluation.'}
127
+ (ROOT/f'{name}_neural_only_results.json').write_text(json.dumps(res,indent=2))
128
+ (ROOT/f'{name}_neural_only_per_question.json').write_text(json.dumps(details))
129
+ print('RESULT',name,json.dumps(res),flush=True)
130
+ return res
131
+
132
+ def evaluate(name,rows,denses,embtimes,ce,tok):
133
+ pairs=[(r['query'],(d['title']+'. ' if d['title'] else '')+d['body']) for r in rows for d in r['docs']]
134
+ print('CROSS_START',name,'n',len(rows),'pairs',len(pairs),flush=True)
135
+ t=time.perf_counter();cross,perf=score_pairs(ce,tok,pairs,batch=20)
136
+ tsec=time.perf_counter()-t
137
+ lengths=np.cumsum([len(r['docs']) for r in rows])[:-1]
138
+ scores=np.split(cross,lengths)
139
+ report=assess(name,rows,denses,scores,{'crossencoder_scoring_seconds':round(tsec,3),'crossencoder_scoring_ms_per_question':round(tsec*1000/len(rows),2),'crossencoder_forward_seconds':round(perf['total_forward_s'],3)},embtimes)
140
+ return report
141
+
142
+ if __name__=='__main__':
143
+ n=int(sys.argv[1]) if len(sys.argv)>1 else 300
144
+ ds=sys.argv[2:] or ['hover','multihop','qasc']
145
+ dense,tok=get_dense();ce=load_crossencoder();ctok=PairTokenizer(length=256)
146
+ for task in ds:
147
+ if task=='hover':
148
+ rows,scores,times=prepare_hover(n,dense,tok)
149
+ evaluate('hover',rows,scores,times,ce,ctok)
150
+ elif task=='multihop':
151
+ rows,scores,times=prepare_multihop(n,dense,tok)
152
+ evaluate('multihop',rows,scores,times,ce,ctok)
153
+ elif task=='qasc':
154
+ (rows,scores),(ctrl,cscores),times=prepare_qasc(n,dense,tok)
155
+ evaluate('qasc_neural_retrieval',rows,scores,times,ce,ctok)
156
+ evaluate('qasc_controlled_pool',ctrl,cscores,{'cached_fact_embeddings_used':True},ce,ctok)
157
+ else:raise ValueError(task)
reproduce/parquet_lite.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal self-contained Parquet reader for this uploaded 2WikiMultiHopQA file.
2
+ Handles Snappy, dictionary/plain encodings, V1 data pages, optional/repeated nested lists.
3
+ Not a general Parquet implementation. Verifies output shape and sample semantic integrity.
4
+ """
5
+ import io,struct,ctypes,numpy as np
6
+ from thrift.transport import TTransport
7
+ from thrift.protocol import TCompactProtocol
8
+ from thrift.Thrift import TType
9
+ from parquet_probe import parse,read_obj
10
+
11
+ LIB=ctypes.CDLL('libsnappy.so.1')
12
+ LIB.snappy_uncompressed_length.argtypes=[ctypes.c_void_p,ctypes.c_size_t,ctypes.POINTER(ctypes.c_size_t)]
13
+ LIB.snappy_uncompressed_length.restype=ctypes.c_int
14
+ LIB.snappy_uncompress.argtypes=[ctypes.c_void_p,ctypes.c_size_t,ctypes.c_void_p,ctypes.POINTER(ctypes.c_size_t)]
15
+ LIB.snappy_uncompress.restype=ctypes.c_int
16
+
17
+ def unsnappy(b):
18
+ inp=ctypes.create_string_buffer(b);n=ctypes.c_size_t();c=LIB.snappy_uncompressed_length(inp,len(b),ctypes.byref(n))
19
+ if c:raise ValueError(('snappy length',c))
20
+ out=ctypes.create_string_buffer(n.value);c=LIB.snappy_uncompress(inp,len(b),out,ctypes.byref(n))
21
+ if c:raise ValueError(('snappy decompress',c))
22
+ return out.raw[:n.value]
23
+
24
+ def varint(b,p):
25
+ x=0;shift=0
26
+ while True:
27
+ v=b[p];p+=1;x|=(v&127)<<shift
28
+ if v<128:return x,p
29
+ shift+=7
30
+
31
+ def decode_rle_hybrid(buf,bitwidth,count):
32
+ if bitwidth==0:return [0]*count
33
+ pos=0;out=[];mask=(1<<bitwidth)-1
34
+ while len(out)<count:
35
+ h,pos=varint(buf,pos)
36
+ if h%2==0:
37
+ n=h>>1;s=(bitwidth+7)//8;x=int.from_bytes(buf[pos:pos+s],'little');pos+=s
38
+ out.extend([x]*min(n,count-len(out)))
39
+ else:
40
+ n=(h>>1)*8;s=(n*bitwidth+7)//8
41
+ bs=buf[pos:pos+s];pos+=s
42
+ # use bit extraction, usually < 40k entries
43
+ acc=int.from_bytes(bs,'little')
44
+ out.extend((acc>>(j*bitwidth))&mask for j in range(min(n,count-len(out))))
45
+ return out[:count]
46
+
47
+ def plain_values(blob,phys,count):
48
+ out=[];pos=0
49
+ for _ in range(count):
50
+ if phys==6:
51
+ n=struct.unpack_from('<I',blob,pos)[0];pos+=4
52
+ out.append(blob[pos:pos+n].decode('utf8','replace'));pos+=n
53
+ elif phys==2:
54
+ out.append(struct.unpack_from('<i',blob,pos)[0]);pos+=4
55
+ elif phys==1:
56
+ out.append(struct.unpack_from('<B',blob,pos)[0]);pos+=1
57
+ else:raise ValueError(('type',phys))
58
+ return out
59
+
60
+ def read_col(f,meta,limits):
61
+ m=meta[3];path=tuple(m[3]);maxdef,maxrep=limits[path]
62
+ start=m.get(11,m[9]);f.seek(start);b=f.read(m[7]);s=io.BytesIO(b)
63
+ proto=TCompactProtocol.TCompactProtocol(TTransport.TFileObjectTransport(s))
64
+ dictvals=[]; reps=[];defs=[];values=[];num=m[5]
65
+ while len(reps)<num:
66
+ h=read_obj(proto,TType.STRUCT);packed=s.read(h[3]);raw=unsnappy(packed) if m[4]==1 else packed
67
+ page_type=h[1]
68
+ if page_type==2:
69
+ dictvals=plain_values(raw,m[1],h[7][1]);continue
70
+ if page_type!=0:raise ValueError(('page type',page_type))
71
+ dh=h[5];N=dh[1];pos=0
72
+ if maxrep:
73
+ n=struct.unpack_from('<I',raw,pos)[0];pos+=4
74
+ rep=decode_rle_hybrid(raw[pos:pos+n],maxrep.bit_length(),N);pos+=n
75
+ else:rep=[0]*N
76
+ if maxdef:
77
+ n=struct.unpack_from('<I',raw,pos)[0];pos+=4
78
+ defs1=decode_rle_hybrid(raw[pos:pos+n],maxdef.bit_length(),N);pos+=n
79
+ else:defs1=[0]*N
80
+ nval=sum(x==maxdef for x in defs1)
81
+ if dh[2] in (8,2):
82
+ bitwidth=raw[pos];pos+=1
83
+ inds=decode_rle_hybrid(raw[pos:],bitwidth,nval)
84
+ value=[dictvals[x] for x in inds]
85
+ elif dh[2]==0:value=plain_values(raw[pos:],m[1],nval)
86
+ else:raise ValueError(('page encoding',dh[2]))
87
+ reps.extend(rep);defs.extend(defs1);values.extend(value)
88
+ return reps,defs,values
89
+
90
+ def reconstruct(reps,defs,vals,maxdef,maxrep):
91
+ rows=[];current=None;ii=0
92
+ if not maxrep:
93
+ for d in defs:
94
+ rows.append(vals[ii] if d==maxdef else None)
95
+ if d==maxdef:ii+=1
96
+ return rows
97
+ if maxrep==1:
98
+ for r,d in zip(reps,defs):
99
+ if r==0:current=[];rows.append(current)
100
+ if d==maxdef:current.append(vals[ii]);ii+=1
101
+ return rows
102
+ if maxrep==2:
103
+ for r,d in zip(reps,defs):
104
+ if r==0:current=[];rows.append(current)
105
+ if r in (0,1) and d>=maxdef-1:current.append([])
106
+ if d==maxdef:
107
+ if not current:current.append([])
108
+ current[-1].append(vals[ii]);ii+=1
109
+ return rows
110
+ raise ValueError(maxrep)
111
+
112
+ LIMITS={
113
+ ('id',):(1,0),('question',):(1,0),('answer',):(1,0),('type',):(1,0),
114
+ ('evidences','list','element','list','element'):(5,2),
115
+ ('supporting_facts','title','list','element'):(4,1),
116
+ ('supporting_facts','sent_id','list','element'):(4,1),
117
+ ('context','title','list','element'):(4,1),
118
+ ('context','sentences','list','element','list','element'):(6,2),
119
+ }
120
+ def load_2wiki(path,nrows=1000):
121
+ f=open(path,'rb');f.seek(-8,2);size=struct.unpack('<I',f.read(4))[0];f.seek(-8-size,2)
122
+ metadata=parse(f.read(size));rowgroups=metadata[4];rows=[]
123
+ for rg in rowgroups:
124
+ cols={}
125
+ for col in rg[1]:
126
+ path=tuple(col[3][3]);rep,defs,vals=read_col(f,col,LIMITS)
127
+ c=reconstruct(rep,defs,vals,*LIMITS[path]);cols[path]=c
128
+ assert len(c)==rg[3],(path,len(c),rg[3])
129
+ for j in range(rg[3]):
130
+ titles=cols[('context','title','list','element')][j]
131
+ sentences=cols[('context','sentences','list','element','list','element')][j]
132
+ supp_titles=cols[('supporting_facts','title','list','element')][j]
133
+ supp_ix=cols[('supporting_facts','sent_id','list','element')][j]
134
+ rows.append({'_id':cols[('id',)][j],'question':cols[('question',)][j],
135
+ 'answer':cols[('answer',)][j],'type':cols[('type',)][j],
136
+ 'context':list(zip(titles,sentences)),
137
+ 'supporting_facts':list(zip(supp_titles,supp_ix)),'evidences':cols[('evidences','list','element','list','element')][j]})
138
+ if len(rows)>=nrows:return rows
139
+ return rows
140
+
141
+ if __name__=='__main__':
142
+ rows=load_2wiki('/mnt/data/validation-00000-of-00001.parquet',10)
143
+ for r in rows[:5]:print(r['_id'],r['question'],'ANSWER',r['answer'],'ctx',len(r['context']),'sf',r['supporting_facts'],'ex',r['context'][:1]);print()
reproduce/parquet_probe.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import struct
2
+ from thrift.transport import TTransport
3
+ from thrift.protocol import TCompactProtocol
4
+ from thrift.Thrift import TType
5
+
6
+ def read_obj(p,t):
7
+ if t==TType.BOOL:return p.readBool()
8
+ if t==TType.BYTE:return p.readByte()
9
+ if t==TType.I16:return p.readI16()
10
+ if t==TType.I32:return p.readI32()
11
+ if t==TType.I64:return p.readI64()
12
+ if t==TType.DOUBLE:return p.readDouble()
13
+ if t==TType.STRING:return p.readBinary().decode('utf8','replace')
14
+ if t==TType.STRUCT:
15
+ p.readStructBegin();out={}
16
+ while True:
17
+ _,tt,i=p.readFieldBegin()
18
+ if tt==TType.STOP:break
19
+ out[i]=read_obj(p,tt);p.readFieldEnd()
20
+ p.readStructEnd();return out
21
+ if t==TType.LIST:
22
+ tt,n=p.readListBegin();arr=[read_obj(p,tt) for _ in range(n)];p.readListEnd();return arr
23
+ if t==TType.SET:
24
+ tt,n=p.readSetBegin();arr=[read_obj(p,tt) for _ in range(n)];p.readSetEnd();return arr
25
+ if t==TType.MAP:
26
+ kt,vt,n=p.readMapBegin();out={read_obj(p,kt):read_obj(p,vt) for _ in range(n)};p.readMapEnd();return out
27
+ raise Exception(('unsupported',t))
28
+
29
+ def parse(b):return read_obj(TCompactProtocol.TCompactProtocol(TTransport.TMemoryBuffer(b)),TType.STRUCT)
30
+
31
+ if __name__=='__main__':
32
+ f=open('/mnt/data/validation-00000-of-00001.parquet','rb');f.seek(-8,2);size=struct.unpack('<I',f.read(4))[0];f.seek(-8-size,2);m=parse(f.read(size))
33
+ print('keys',m.keys(),'version',m[1],'rows',m[3],'rowgroups',len(m[4]))
34
+ for x in m[2]:print('SCHEMA',x)
35
+ for rg in m[4][:2]:
36
+ print('row group nums',rg.get(3),'cols',len(rg[1]))
37
+ for c in rg[1]:
38
+ met=c.get(3,{});print(' col path',met.get(3),'type',met.get(1),'encc',met.get(2),'codec',met.get(4),'numvals',met.get(5),'fileoffset',c.get(2),'pageoffset',met.get(9),'dict',met.get(11),'compressed',met.get(7))
reproduce/unified_original_two_rerun.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Rerun original HotpotQA/2Wiki held-out 600 with *identical* four-dataset graph_select.
2
+ Cached pretrained neural inference outputs are reused, not regenerated. No gold at selection.
3
+ """
4
+ import os
5
+ os.environ['OPENBLAS_NUM_THREADS']='1';os.environ['OMP_NUM_THREADS']='4'
6
+ import time, json, hashlib
7
+ from pathlib import Path
8
+ import numpy as np
9
+ from neural_evidence_graph_core import graph_arrays,graph_select,doc
10
+ from hotpot_subset_eval import load_data as hotpot_data
11
+ from wiki_subset_eval import load_data as wiki_data
12
+ ROOT=Path('/mnt/data'); KS=(2,3,5,8)
13
+ def bootstrap(y,z):
14
+ d=np.asarray(y,int)-np.asarray(z,int); rng=np.random.default_rng(31)
15
+ boots=d[rng.integers(0,len(d),(2000,len(d)))].mean(axis=1)*100
16
+ return {'diff_pp':round(float(d.mean()*100),2),'ci95_pp':[round(float(v),2) for v in np.percentile(boots,[2.5,97.5])], 'wins':int((d>0).sum()),'losses':int((d<0).sum()),'ties':int((d==0).sum())}
17
+ def run(name):
18
+ start=time.perf_counter()
19
+ items,_=(hotpot_data() if name=='hotpot' else wiki_data()); items=items[200:800]; n=len(items)
20
+ emb=np.load(ROOT/('hotpot_subset_out/hotpot_minilm_800.npz' if name=='hotpot' else 'wiki_subset_out/wiki_minilm_800.npz'))
21
+ dense=np.einsum('nd,nkd->nk',emb['q'][200:800],emb['docs'][200:800]);assert dense.shape==(600,10)
22
+ ce=np.load(ROOT/f'crossencoder_{name}_256_600_scores.npz')['scores'];assert ce.shape==(600,10)
23
+ rows=[]
24
+ for r in items:
25
+ rows.append({'qid':r['_id'],'query':r['question'],'docs':[doc(title,' '.join(sentences),title.casefold().strip()) for title,sentences in r['context']],'gold':{t.casefold().strip() for t,_ in r['supporting_facts']}})
26
+ truth=[];orders={k:[] for k in ('dense_cosine','crossencoder','dense_graph','crossencoder_graph')}; gold_counts=[]; links=[]
27
+ tgraph=time.perf_counter()
28
+ for i,row in enumerate(rows):
29
+ L,M=graph_arrays(row); links.append(int(L.any()))
30
+ orders['dense_cosine'].append(np.argsort(-dense[i],kind='stable').tolist())
31
+ orders['crossencoder'].append(np.argsort(-ce[i],kind='stable').tolist())
32
+ orders['dense_graph'].append(graph_select(dense[i],L,M,max(KS)))
33
+ orders['crossencoder_graph'].append(graph_select(ce[i],L,M,max(KS)))
34
+ gold_counts.append(len(row['gold']));truth.append(row['gold'])
35
+ gt=time.perf_counter()-tgraph
36
+ hits={m:{str(k):[] for k in KS} for m in orders};detail=[]
37
+ for i,row in enumerate(rows):
38
+ dd={'id':row['qid'],'gold_count':gold_counts[i],'orders':{m:o[i] for m,o in orders.items()},'hits':{}}
39
+ for m,oo in orders.items():
40
+ dd['hits'][m]={}
41
+ for k in KS:
42
+ pred={row['docs'][j]['id'] for j in oo[i][:k]}
43
+ h=int(bool(row['gold']) and row['gold'].issubset(pred));hits[m][str(k)].append(h);dd['hits'][m][str(k)]=h
44
+ detail.append(dd)
45
+ result={'dataset':name,'questions':n,'candidate_count_per_question':10,'selection_rule':'same neural_evidence_graph_core.graph_arrays and graph_select as four new datasets: 0.4 * per-query minmax relevance + 0.5 * max selected-to-candidate directed title-body link + 0.2 * query-title match; no tuning, no query facets, no specialized presets',
46
+ 'embedding_source':'previously frozen/precomputed actual all-MiniLM-L6-v2 q and passage vectors; cosine recomputed',
47
+ 'crossencoder_source':'previously frozen/precomputed actual pretrained ms-marco-MiniLM-L6-v2 256-wordpiece query-passage logits, same ids',
48
+ 'complete_evidence_coverage_pct':{m:{str(k):round(np.mean(v)*100,2) for k,v in h.items()} for m,h in hits.items()},
49
+ 'crossencoder_graph_vs_crossencoder_at3':bootstrap(hits['crossencoder_graph']['3'],hits['crossencoder']['3']),
50
+ 'dense_graph_vs_dense_at3':bootstrap(hits['dense_graph']['3'],hits['dense_cosine']['3']),
51
+ 'graph_link_present_pct':round(float(np.mean(links)*100),2),
52
+ 'gold_support_count_distribution':{str(k):gold_counts.count(k) for k in set(gold_counts)},
53
+ 'timing':{'graph_feature_extraction_and_four_rankings_ms_per_question':round(gt*1000/n,3),'graph_feature_extraction_and_four_rankings_total_s':round(gt,4),
54
+ 'pretrained_crossencoder_scoring_ms_per_question_from_original_inference_run':round(json.load(open(ROOT/f'crossencoder_{name}_256_600_results.json'))['timing']['scoring_ms_per_question'],2),'cached_embedding_and_crossencoder_scores':True},
55
+ 'note':'This is an actual rerun of ranking/selection and evidence coverage from cached neural scores; scoring step was previously executed, not repeated. Scores are on the same selected 600 held-out question IDs.'}
56
+ p=ROOT/f'unified_{name}_rerun.json';p.write_text(json.dumps(result,indent=2));(ROOT/f'unified_{name}_per_question.json').write_text(json.dumps(detail))
57
+ print(json.dumps(result,indent=2),flush=True)
58
+ return result
59
+ if __name__=='__main__':
60
+ for name in ('hotpot','wiki'):run(name)
reproduce/verify_release_against_1200.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys,json,numpy as np
2
+ from pathlib import Path
3
+ sys.path.insert(0,'/mnt/data')
4
+ sys.path.insert(0,'/mnt/data/evidence_graph_hf_release')
5
+ from evidence_graph_reranker import rank_with_scores
6
+ from hotpot_subset_eval import load_data as hp
7
+ from wiki_subset_eval import load_data as wk
8
+ from neural_evidence_graph_core import graph_select,graph_arrays,doc
9
+ P=Path('/mnt/data');ok=0
10
+ for name,loader in [('hotpot',hp),('wiki',wk)]:
11
+ items,_=loader(); items=items[200:800];ce=np.load(P/f'crossencoder_{name}_256_600_scores.npz')['scores']
12
+ details=json.load(open(P/f'unified_{name}_per_question.json'))
13
+ for i,r in enumerate(items):
14
+ docs=[{'id':t.casefold().strip(),'title':t,'text':' '.join(sent)} for t,sent in r['context']]
15
+ got=rank_with_scores(r['question'],docs,ce[i],k=8)
16
+ assert [x['index'] for x in got]==details[i]['orders']['crossencoder_graph'],(name,i)
17
+ ok+=1
18
+ print(name,'PASS',len(items))
19
+ print('TOTAL_VERIFIED',ok)
reproduce/wiki_subset_eval.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CPU-only evidence-completeness benchmark for HotpotQA distractor setting.
2
+ Uses frozen local MiniLM, no gold evidence in selection. Dataset's supporting facts evaluate chosen titles.
3
+ """
4
+ import os,time,re,json,sys,hashlib
5
+ os.environ['OPENBLAS_NUM_THREADS']='1';os.environ['OMP_NUM_THREADS']='4'
6
+ from pathlib import Path
7
+ import numpy as np
8
+ from minilm_cpu_pipeline import load_model,WordPiece,embed
9
+ from parquet_lite import load_2wiki
10
+ DATA=Path('/mnt/data/validation-00000-of-00001.parquet');OUT=Path('/mnt/data/wiki_subset_out');OUT.mkdir(exist_ok=True)
11
+ N_TUNE=200;N_TEST=600;RNG=20261030
12
+
13
+
14
+ def facets(q):
15
+ q=q.strip().rstrip('?')
16
+ # Query-provided pieces only; no gold evidence or topic annotations.
17
+ p=re.split(r'(?i)\b(?:and|or|whose|which|who|that|where|while|whereas)\b|[,;:]\s*',q)
18
+ p=[x.strip() for x in p if len(x.split())>=2]
19
+ if len(p)<2:
20
+ w=q.split();m=max(2,len(w)//2)
21
+ p=[' '.join(w[:m]),' '.join(w[m:])] if len(w)>=5 else [q,q]
22
+ return (p[:3]+[q]*3)[:3]
23
+
24
+ def safe_key(x):return re.sub(r'\s+',' ',x.strip().lower())
25
+
26
+ def load_data():
27
+ data=[r for r in load_2wiki(str(DATA),12576) if len(r['context'])==10]; rng=np.random.default_rng(RNG)
28
+ print('total 2Wiki valid candidates',len(data),flush=True)
29
+ # Sample from all subtypes without using labels for feature engineering.
30
+ ids=rng.permutation(len(data))[:N_TUNE+N_TEST]
31
+ items=[data[int(i)] for i in ids]
32
+ counts={}
33
+ for r in items:counts[r['type']]=counts.get(r['type'],0)+1
34
+ print('sample',len(items),'types',counts,flush=True)
35
+ return items,ids
36
+
37
+ def encode_data(items):
38
+ path=OUT/'wiki_minilm_800.npz'
39
+ identity=hashlib.sha256('|'.join(r['_id'] for r in items).encode()).hexdigest()[:16]
40
+ if path.exists():
41
+ dat=np.load(path)
42
+ if str(dat['identity'])==identity:
43
+ print('loaded cached embeddings',flush=True)
44
+ return dat['q'],dat['docs'],dat['facets']
45
+ model=load_model();tok=WordPiece(length=128)
46
+ queries=[r['question'] for r in items]
47
+ docs=[title+'. '+ ' '.join(sentences) for r in items for title,sentences in r['context']]
48
+ ftexts=[t for r in items for t in facets(r['question'])]
49
+ print('encoding',len(queries),'queries',len(docs),'docs',len(ftexts),'aspects',flush=True)
50
+ st=time.perf_counter();q=embed(queries,model,tok,batch_size=24,callback=lambda i,n,t:print('q',i,n,round(t,1),flush=True) if i>0 else None)
51
+ print('query done',round(time.perf_counter()-st,1),flush=True)
52
+ d=embed(docs,model,tok,batch_size=24,callback=lambda i,n,t:print('docs',i,n,round(t,1),flush=True) if i>0 else None).reshape(len(items),10,384)
53
+ print('documents done',round(time.perf_counter()-st,1),flush=True)
54
+ f=embed(ftexts,model,tok,batch_size=24,callback=lambda i,n,t:print('facets',i,n,round(t,1),flush=True) if i>0 else None).reshape(len(items),3,384)
55
+ print('facets done',round(time.perf_counter()-st,1),flush=True)
56
+ np.savez_compressed(path,identity=identity,q=q,docs=d,facets=f)
57
+ return q,d,f
58
+
59
+ def greedy(rel,dc,fac,method,k=5,alpha=.6,beta=.2,threshold=.35):
60
+ # Single query score vector, 10x10 document similarity, facet-doc sim 3x10.
61
+ K=len(rel);sel=[];remain=set(range(K));
62
+ cfac=np.clip(fac-threshold,0,None)
63
+ mx=np.zeros(len(cfac));mxcluster=np.zeros(K)
64
+ for t in range(k):
65
+ best=(-1e9,None)
66
+ for j in sorted(remain):
67
+ if method=='cosine':v=rel[j]
68
+ elif method=='mmr':v=rel[j] if not sel else alpha*rel[j]-(1-alpha)*max(0,max(dc[j,x] for x in sel))
69
+ elif method=='coverage':v=alpha*rel[j]+(1-alpha)*np.maximum(cfac[:,j]-mx,0).mean()
70
+ elif method=='facility':
71
+ weights=np.clip(rel,0,None)**2
72
+ v=alpha*rel[j]+(1-alpha)*np.dot(weights,np.maximum(np.clip(dc[:,j],0,None)-mxcluster,0))/max(weights.sum(),1e-8)
73
+ elif method=='bridge':
74
+ bridge=max((dc[j,x] for x in sel),default=0)
75
+ v=alpha*rel[j]+(1-alpha)*bridge-beta*max(0,bridge-.8) if sel else rel[j]
76
+ elif method=='combined':
77
+ gain=np.maximum(cfac[:,j]-mx,0).mean()
78
+ # Complementary candidate information, plus query-conditioned facet coverage.
79
+ weights=np.clip(rel,0,None)**2
80
+ facility=np.dot(weights,np.maximum(np.clip(dc[:,j],0,None)-mxcluster,0))/max(weights.sum(),1e-8)
81
+ v=alpha*rel[j]+(1-alpha)*(beta*gain+(1-beta)*facility)
82
+ else:raise ValueError(method)
83
+ if v>best[0]:best=(v,j)
84
+ j=best[1];sel.append(j);remain.remove(j)
85
+ mx=np.maximum(mx,cfac[:,j]);mxcluster=np.maximum(mxcluster,np.clip(dc[:,j],0,None))
86
+ return sel
87
+
88
+ def eval_one(items,scores,docsim,facsim,method,params,inds,ks=(2,3,4,5)):
89
+ out=np.zeros((len(inds),len(ks)),float); selected=[];goldcounts=[];omiss=[]
90
+ for ii,i in enumerate(inds):
91
+ r=items[i]; gold=set(safe_key(t) for t,_ in r['supporting_facts']);titles=[safe_key(x[0]) for x in r['context']]
92
+ goldcounts.append(len(gold));possible=gold.issubset(set(titles));omiss.append(possible)
93
+ order=greedy(scores[i],docsim[i],facsim[i],method,k=max(ks),**params)
94
+ selected.append(order)
95
+ for jj,k in enumerate(ks):out[ii,jj]=int(possible and gold.issubset(set(titles[x] for x in order[:k])))
96
+ return out,np.asarray(selected),np.asarray(goldcounts),np.asarray(omiss)
97
+
98
+ def main():
99
+ items,orig_idx=load_data();q,d,f=encode_data(items)
100
+ sims=np.einsum('nd,nkd->nk',q,d).astype('float32');dcs=np.einsum('nkd,njd->nkj',d,d).astype('float32');fs=np.einsum('nmd,nkd->nmk',f,d).astype('float32')
101
+ tunes=np.arange(N_TUNE);tests=np.arange(N_TUNE,N_TUNE+N_TEST)
102
+ param_grid={
103
+ 'cosine':[{}],
104
+ 'mmr':[{'alpha':a} for a in [.3,.5,.7,.85,.95]],
105
+ 'coverage':[{'alpha':a,'threshold':t} for a in [.1,.3,.5,.7,.9] for t in [.2,.35,.5,.65]],
106
+ 'facility':[{'alpha':a} for a in [.2,.4,.6,.8,.95]],
107
+ 'bridge':[{'alpha':a,'beta':b} for a in [.4,.6,.8] for b in [0,.2,.4]],
108
+ 'combined':[{'alpha':a,'beta':b,'threshold':t} for a in [.2,.4,.6,.8] for b in [.25,.5,.75] for t in [.3,.5]],
109
+ }
110
+ best={};val={}
111
+ for method,grid in param_grid.items():
112
+ st=time.perf_counter();records=[]
113
+ for params in grid:
114
+ arr,_,_,_=eval_one(items,sims,dcs,fs,method,params,tunes)
115
+ records.append((float(arr[:,1].mean()),float(arr[:,2].mean()),params))
116
+ records.sort(key=lambda x:(x[0],x[1]),reverse=True)
117
+ best[method]=records[0][2];val[method]=records[0][0]
118
+ print('TUNED',method,'@3',records[0][0],best[method],'seconds',round(time.perf_counter()-st,1),flush=True)
119
+ results={};test_arrays={};selections={}
120
+ for method,params in best.items():
121
+ st=time.perf_counter();arr,idx,ngold,oracle=eval_one(items,sims,dcs,fs,method,params,tests)
122
+ ms=(time.perf_counter()-st)/len(tests)*1000
123
+ results[method]={'parameters':params,'validation_complete_evidence_at3':val[method],'heldout_complete_evidence_at_k':{str(k):float(arr[:,j].mean()) for j,k in enumerate((2,3,4,5))},'selection_cpu_ms_per_question':ms,'gold_title_count_distribution':{str(x):int((ngold==x).sum()) for x in np.unique(ngold)},'oracle_coverage_rate':float(oracle.mean())}
124
+ test_arrays[method]=arr;selections[method]=idx
125
+ print('TEST',method,results[method]['heldout_complete_evidence_at_k'],'ms',round(ms,3),flush=True)
126
+ base=test_arrays['cosine'];rng=np.random.default_rng(RNG)
127
+ bs=rng.integers(len(tests),size=(2000,len(tests)))
128
+ for method in best:
129
+ delta=test_arrays[method][:,1]-base[:,1]
130
+ ci=np.quantile(delta[bs].mean(axis=1),[.025,.975]).tolist()
131
+ results[method]['delta_at3_vs_cosine']=float(delta.mean());results[method]['delta_at3_ci95']=ci
132
+ results[method]['paired_win_loss_at3']={'wins':int((delta>0).sum()),'losses':int((delta<0).sum()),'ties':int((delta==0).sum())}
133
+ ids=[items[i]['_id'] for i in tests];details=[]
134
+ for j,i in enumerate(tests):
135
+ r=items[i]; details.append({'id':r['_id'],'question':r['question'],'answer':r['answer'],'type':r['type'],'gold_support_titles':sorted(set(t for t,_ in r['supporting_facts'])),'cosine': [r['context'][v][0] for v in selections['cosine'][j]],'coverage':[r['context'][v][0] for v in selections['coverage'][j]],'combined':[r['context'][v][0] for v in selections['combined'][j]]})
136
+ result={'dataset':'2WikiMultiHopQA validation','source_file':str(DATA),'total_dev':12576,'sample_count':len(items),'tune_count':N_TUNE,'heldout_count':N_TEST,'random_seed':RNG,'protocol':'Each question has own ten provided paragraphs; gold supporting passage titles are evaluation only; frozen MiniLM encoded whole query, each candidate passage, and three heuristic query facets; hyperparameters chosen on 250 dev cases; tested on separate 750 dev cases. No generative LLM was invoked.','results':results}
137
+ (OUT/'wiki_results.json').write_text(json.dumps(result,indent=2));(OUT/'wiki_case_studies.json').write_text(json.dumps(details,indent=2));print('DONE',flush=True)
138
+ if __name__=='__main__':main()
results/hover_neural_only_results.json ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dataset": "hover",
3
+ "questions": 300,
4
+ "candidate_count_total": 3000,
5
+ "candidate_pool": "10 HotpotQA distractor passages, only HoVer claims with all support titles present",
6
+ "gold_support_count_distribution": {
7
+ "2": 152,
8
+ "3": 144,
9
+ "4": 4
10
+ },
11
+ "gold_available_fraction": 100.0,
12
+ "gold_available_count": 300,
13
+ "graph_link_fraction": 94.33,
14
+ "oracle_complete_coverage_pct": {
15
+ "2": 50.67,
16
+ "3": 98.67,
17
+ "5": 100.0,
18
+ "8": 100.0
19
+ },
20
+ "complete_coverage_pct": {
21
+ "dense_cosine": {
22
+ "2": 26.33,
23
+ "3": 47.33,
24
+ "5": 75.67,
25
+ "8": 92.0
26
+ },
27
+ "crossencoder": {
28
+ "2": 33.0,
29
+ "3": 57.0,
30
+ "5": 82.0,
31
+ "8": 92.33
32
+ },
33
+ "dense_graph": {
34
+ "2": 32.33,
35
+ "3": 73.33,
36
+ "5": 89.33,
37
+ "8": 97.33
38
+ },
39
+ "crossencoder_graph": {
40
+ "2": 38.33,
41
+ "3": 72.67,
42
+ "5": 92.33,
43
+ "8": 98.67
44
+ }
45
+ },
46
+ "crossencoder_graph_minus_crossencoder_at3_pp": 15.67,
47
+ "ci95_pp": [
48
+ 9.99,
49
+ 22.0
50
+ ],
51
+ "paired_wins_losses_ties_at3": {
52
+ "wins": 70,
53
+ "losses": 23,
54
+ "ties": 207
55
+ },
56
+ "timing_seconds": {
57
+ "embedding_seconds": 45.51700891600012,
58
+ "prep_seconds": 45.92742176499996,
59
+ "candidate_pool_size": 10,
60
+ "eligibility": "HoVer dev claims with HotpotQA dev passages available and all annotated support titles present; conditional subset",
61
+ "crossencoder_scoring_seconds": 84.113,
62
+ "crossencoder_scoring_ms_per_question": 280.38,
63
+ "crossencoder_forward_seconds": 83.542,
64
+ "graph_selection_total_s": 0.11,
65
+ "graph_selection_ms_per_question": 0.37
66
+ },
67
+ "models": {
68
+ "dense": "user uploaded all-MiniLM-L6-v2",
69
+ "crossencoder": "user uploaded cross-encoder/ms-marco-MiniLM-L6-v2"
70
+ },
71
+ "protocol": "MiniLM embeddings at max 128 WordPiece for dense. Cross-encoder truncates paired query+passage at 256 WordPiece. Both score exact same candidate pools. Fixed evidence graph .4 minmax relevance + .5 title in already-selected passage + .2 query-title score. No TF-IDF anywhere in candidate construction or scoring. Gold used solely for pool audit and evaluation."
72
+ }
results/methodology.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Six-dataset benchmark methodology and measured findings
2
+
3
+ ## Exactly one selector
4
+
5
+ All six results here were scored with the same implementation of `graph_arrays` and `graph_select` from the four-dataset neural-only experiments. Public `evidence_graph_reranker` mirrors that code. There is no per-dataset preset. At inference, query and candidate text generate title/body edges and query/title matches, and every remaining item is scored by `0.4 × minmax(neural_relevance) + 0.5 × selected-to-candidate title reference + 0.2 × query/title match`. The actual neural relevance scorer is either all-MiniLM-L6-v2 cosine or pretrained MS MARCO MiniLM-L6-v2 query–passage logits. The algorithm never reads gold labels.
6
+
7
+ The **relevance backend** is a separate choice, but the selector is identical. Note that the fixed coefficients originated during earlier methodological development; they were not newly tuned on the held-out samples in the unified rerun.
8
+
9
+ ## Complete evidence coverage, percentages
10
+
11
+ At budget k=3 (selected **passages**, not word/token count):
12
+
13
+ | Dataset | n | Dense cosine | MS MARCO | Dense + graph | MS MARCO + graph | Gain vs MS MARCO |
14
+ |---|---:|---:|---:|---:|---:|---:|
15
+ | HotpotQA | 600 | 50.67 | 60.83 | 74.83 | **80.50** | +19.67 pp |
16
+ | 2WikiMultiHopQA | 600 | 34.17 | 44.50 | 68.33 | **70.17** | +25.67 pp |
17
+ | MuSiQue | 300 | 16.00 | 19.33 | 25.00 | **27.00** | +7.67 pp |
18
+ | HoVer | 300 | 47.33 | 57.00 | 73.33 | **72.67** | +15.67 pp |
19
+ | MultiHop-RAG | 300 | 11.67 | 13.67 | 11.67 | **13.67** | +0.00 pp |
20
+ | QASC | 300 | 5.33 | 13.00 | 5.33 | **13.00** | +0.00 pp |
21
+
22
+ Results for k=2,3,5,8 appear in `six_dataset_results.csv`; per-question outcomes for the original two are included.
23
+
24
+ ## CPU timing: separate pipeline stages
25
+
26
+ | Dataset | Crossencoder scorer (ms/q) | Graph feature extraction + selection (ms/q) |
27
+ |---|---:|---:|
28
+ | HotpotQA | 272.18 | 0.406 |
29
+ | 2WikiMultiHopQA | 254.92 | 0.322 |
30
+ | MuSiQue | 538.67 | 0.662 |
31
+ | HoVer | 280.38 | 0.370 |
32
+ | MultiHop-RAG | 635.04 | 7.570 |
33
+ | QASC | 69.12 | 0.340 |
34
+
35
+ **Timing caveat:** the first two graph timings count construction plus four rankings; the other datasets' logs count graph operations within their evaluation loop. CPU hardware and batching are from the original benchmark runs, not normalized across experiments. Neural scorer timings exclude model startup; graph-only timings exclude neural scorer inference. It would be inaccurate to compare 0.4ms to the crossencoder's ~250ms as if the graph pipeline did not require neural relevance scores. The dense+graph pipeline is often much lighter because passage embeddings can be cached, but end-to-end full-pipeline timing requires matched test conditions.
36
+
37
+ ## Original two, unified-rerun integrity
38
+
39
+ - HotpotQA: 600 held-out queries from 800 seeded HotpotQA distractor examples, first 200 excluded; original pool is 10 passages. Graph+crossencoder CEC@3 80.50%; crossencoder alone 60.83%. Paired 95% bootstrap CI for the +19.67 pp gain [16.17, 23.33].
40
+ - 2WikiMultiHopQA: 600 held-out queries from 800 seeded 2Wiki validation examples, first 200 excluded; native candidate pool is 10 passages. Graph+crossencoder CEC@3 70.17%; crossencoder alone 44.50%. Paired 95% bootstrap CI for +25.67 pp [22.17, 29.33].
41
+ - The **graph selection and evaluation were rerun** using exact 0.4/0.5/0.2 rules, with original MiniLM query/document vectors and pretrained MS MARCO query/passage logits loaded from previous completed CPU neural inference. We did not rerun the 12,000 neural pairs. The production package's ranking returned the exact same 1,200 top-8 orders as the experimental implementation in an explicit equality check.
42
+
43
+ ## Four additional datasets
44
+
45
+ - **MuSiQue:** 300 seeded examples, ~20 native candidates per question; complete gold evidence present in pool; question-hop counts vary, so @2 is not always achievable. Frozen pretrained crossencoder logits reused from original neural experiment; dense embeddings recomputed for corrected run.
46
+ - **HoVer:** 300 examples selected from **908** claims whose entire annotated evidence set could be matched to uploaded HotpotQA distractor passages. This is a restricted, gold-available conditional pool, not the full 4,000-claim validation benchmark.
47
+ - **MultiHop-RAG:** 300 questions, MiniLM retrieval top20 from all 609 uploaded articles; no gold insertion. Only 51.67% have all required sources in top20. Graph boost at 3 passages is 0; at 5 it is +3pp. Exact title links were rare (~4%).
48
+ - **QASC:** 300 questions, MiniLM top20 from 6,878 distinct annotated fact sentences (not QASC's full external background corpus), no gold insertion. 31.33% of top20 pools contain both gold facts. Sentences have no titles, so this particular graph rule reduces mathematically to independent neural ranking, causing zero gain.
49
+
50
+ ## Interpretation and limitations
51
+
52
+ - The metric is *complete annotated supporting-document/fact recovery at k*, not answer accuracy, grounded generation, factual precision, or token-budget-normalized retrieval.
53
+ - Gold is used to evaluate, not to score. In HoVer, and in the separate unpublished QASC controlled pool, gold availability matters in candidate pool construction.
54
+ - The graph exploits explicit article title references; results on Wikipedia-derived candidate pools may overstate transfer to unstructured corpora.
55
+ - This greedy rule has **no proved submodular approximation guarantee**.
56
+ - Benchmarks have different candidate pools, n, and retrieval difficulty. Do not pool their percentages as if they were a common IID test set.
57
+
58
+ ## Reproduction pointers
59
+
60
+ Run `pytest -q` to check the public graph selector. The `reproduce/` directory contains the original benchmark code with the same directory conventions as the experiment (`/mnt/data`); it requires the original datasets and user's downloaded pretrained weights separately. To regenerate benchmark neural scores, use the saved original scripts; to repeat the **single-selector** rerun from existing cached scores, invoke `reproduce/unified_original_two_rerun.py` in that original `/mnt/data` layout. The file snapshots here show the measured outputs; the benchmark source files are not silently included or redownloaded.
results/multihop_neural_only_results.json ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dataset": "multihop",
3
+ "questions": 300,
4
+ "candidate_count_total": 6000,
5
+ "candidate_pool": "Top-20 neural MiniLM retrieval from full 609-article corpus (no gold insertion)",
6
+ "gold_support_count_distribution": {
7
+ "3": 96,
8
+ "2": 158,
9
+ "4": 46
10
+ },
11
+ "gold_available_fraction": 51.67,
12
+ "gold_available_count": 155,
13
+ "graph_link_fraction": 4.0,
14
+ "oracle_complete_coverage_pct": {
15
+ "2": 35.0,
16
+ "3": 48.0,
17
+ "5": 51.67,
18
+ "8": 51.67
19
+ },
20
+ "complete_coverage_pct": {
21
+ "dense_cosine": {
22
+ "2": 6.0,
23
+ "3": 11.67,
24
+ "5": 21.67,
25
+ "8": 31.67
26
+ },
27
+ "crossencoder": {
28
+ "2": 7.0,
29
+ "3": 13.67,
30
+ "5": 23.0,
31
+ "8": 36.67
32
+ },
33
+ "dense_graph": {
34
+ "2": 6.0,
35
+ "3": 11.67,
36
+ "5": 22.33,
37
+ "8": 32.33
38
+ },
39
+ "crossencoder_graph": {
40
+ "2": 7.67,
41
+ "3": 13.67,
42
+ "5": 26.0,
43
+ "8": 36.67
44
+ }
45
+ },
46
+ "crossencoder_graph_minus_crossencoder_at3_pp": 0.0,
47
+ "ci95_pp": [
48
+ -1.33,
49
+ 1.33
50
+ ],
51
+ "paired_wins_losses_ties_at3": {
52
+ "wins": 2,
53
+ "losses": 2,
54
+ "ties": 296
55
+ },
56
+ "timing_seconds": {
57
+ "index_seconds": 8.61646041700078,
58
+ "query_embedding_seconds": 2.9061220870007674,
59
+ "total_prep_seconds": 11.600812000000587,
60
+ "index_size": 609,
61
+ "candidate_pool_size": 20,
62
+ "crossencoder_scoring_seconds": 190.511,
63
+ "crossencoder_scoring_ms_per_question": 635.04,
64
+ "crossencoder_forward_seconds": 176.907,
65
+ "graph_selection_total_s": 2.271,
66
+ "graph_selection_ms_per_question": 7.57
67
+ },
68
+ "models": {
69
+ "dense": "user uploaded all-MiniLM-L6-v2",
70
+ "crossencoder": "user uploaded cross-encoder/ms-marco-MiniLM-L6-v2"
71
+ },
72
+ "protocol": "MiniLM embeddings at max 128 WordPiece for dense. Cross-encoder truncates paired query+passage at 256 WordPiece. Both score exact same candidate pools. Fixed evidence graph .4 minmax relevance + .5 title in already-selected passage + .2 query-title score. No TF-IDF anywhere in candidate construction or scoring. Gold used solely for pool audit and evaluation."
73
+ }
results/musique_NEURAL_ONLY_results.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dataset": "musique",
3
+ "n": 300,
4
+ "original_embedding_checkpoint": "all-MiniLM-L6-v2, originally user uploaded",
5
+ "cross_encoder_checkpoint": "ms-marco-MiniLM-L6-v2, originally user uploaded",
6
+ "original_embedding_seconds": 86.82592989300065,
7
+ "protocol": "same 300 seeded MuSiQue queries and 20 candidate passages as previous run. No TF-IDF in either ranking or selection; cross scores reused exactly from pretrained model inference. Fixed graph parameters .4 relevance + .5 directed linkage + .2 query-title match (scores min-max normalized per query). Gold is only used in evaluation.",
8
+ "coverage": {
9
+ "original_minilm_cosine": {
10
+ "2": 9.67,
11
+ "3": 16.0,
12
+ "5": 31.0,
13
+ "8": 44.0
14
+ },
15
+ "msmarco_crossencoder": {
16
+ "2": 11.33,
17
+ "3": 19.33,
18
+ "5": 30.67,
19
+ "8": 44.33
20
+ },
21
+ "original_minilm_graph": {
22
+ "2": 16.33,
23
+ "3": 25.0,
24
+ "5": 43.67,
25
+ "8": 58.67
26
+ },
27
+ "msmarco_graph": {
28
+ "2": 17.67,
29
+ "3": 27.0,
30
+ "5": 42.33,
31
+ "8": 57.67
32
+ }
33
+ }
34
+ }
results/pretrained_crossencoder_hotpot_600_scores.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:74239245e5a900da15ea2e7e2ed3e069a4fea56a2db1460ee287f10696883644
3
+ size 27058
results/pretrained_crossencoder_wiki_600_scores.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:96a5ba0a69bdd84ca396b3733cc06f66d9695b7e9c4a21443e81414a12e5665f
3
+ size 26822
results/qasc_neural_retrieval_neural_only_results.json ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dataset": "qasc_neural_retrieval",
3
+ "questions": 300,
4
+ "candidate_count_total": 6000,
5
+ "candidate_pool": "Top-20 MiniLM from all unique QASC supporting fact sentences, without gold insertion",
6
+ "gold_support_count_distribution": {
7
+ "2": 300
8
+ },
9
+ "gold_available_fraction": 31.33,
10
+ "gold_available_count": 94,
11
+ "graph_link_fraction": 0.0,
12
+ "oracle_complete_coverage_pct": {
13
+ "2": 31.33,
14
+ "3": 31.33,
15
+ "5": 31.33,
16
+ "8": 31.33
17
+ },
18
+ "complete_coverage_pct": {
19
+ "dense_cosine": {
20
+ "2": 3.33,
21
+ "3": 5.33,
22
+ "5": 10.33,
23
+ "8": 16.0
24
+ },
25
+ "crossencoder": {
26
+ "2": 7.33,
27
+ "3": 13.0,
28
+ "5": 17.67,
29
+ "8": 24.33
30
+ },
31
+ "dense_graph": {
32
+ "2": 3.33,
33
+ "3": 5.33,
34
+ "5": 10.33,
35
+ "8": 16.0
36
+ },
37
+ "crossencoder_graph": {
38
+ "2": 7.33,
39
+ "3": 13.0,
40
+ "5": 17.67,
41
+ "8": 24.33
42
+ }
43
+ },
44
+ "crossencoder_graph_minus_crossencoder_at3_pp": 0.0,
45
+ "ci95_pp": [
46
+ 0.0,
47
+ 0.0
48
+ ],
49
+ "paired_wins_losses_ties_at3": {
50
+ "wins": 0,
51
+ "losses": 0,
52
+ "ties": 300
53
+ },
54
+ "timing_seconds": {
55
+ "index_seconds": 17.435790640998675,
56
+ "query_embedding_seconds": 0.6626583439992828,
57
+ "total_prep_seconds": 18.42298589100028,
58
+ "fact_corpus_size": 6878,
59
+ "eligible_questions": 8134,
60
+ "candidate_pool_size": 20,
61
+ "crossencoder_scoring_seconds": 20.737,
62
+ "crossencoder_scoring_ms_per_question": 69.12,
63
+ "crossencoder_forward_seconds": 20.519,
64
+ "graph_selection_total_s": 0.103,
65
+ "graph_selection_ms_per_question": 0.34
66
+ },
67
+ "models": {
68
+ "dense": "user uploaded all-MiniLM-L6-v2",
69
+ "crossencoder": "user uploaded cross-encoder/ms-marco-MiniLM-L6-v2"
70
+ },
71
+ "protocol": "MiniLM embeddings at max 128 WordPiece for dense. Cross-encoder truncates paired query+passage at 256 WordPiece. Both score exact same candidate pools. Fixed evidence graph .4 minmax relevance + .5 title in already-selected passage + .2 query-title score. No TF-IDF anywhere in candidate construction or scoring. Gold used solely for pool audit and evaluation."
72
+ }
results/six_dataset_results.csv ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ dataset,n_questions,budget_k,candidate_pool,dense_cosine_pct,crossencoder_pct,dense_plus_graph_pct,crossencoder_plus_graph_pct,crossencoder_graph_gain_pp,crossencoder_cpu_scoring_ms_per_question,graph_selection_ms_per_question,graph_timing_scope,crossencoder_inference_source
2
+ HotpotQA,600,2,native 10 Wikipedia distractor candidates,33.5,45.83,61.67,67.17,21.34,272.18,0.406,feature extraction + 4 rank orders,previously measured original pretrained forward pass; cached scores reused for unified rerun
3
+ HotpotQA,600,3,native 10 Wikipedia distractor candidates,50.67,60.83,74.83,80.5,19.67,272.18,0.406,feature extraction + 4 rank orders,previously measured original pretrained forward pass; cached scores reused for unified rerun
4
+ HotpotQA,600,5,native 10 Wikipedia distractor candidates,68.0,74.67,84.83,89.83,15.16,272.18,0.406,feature extraction + 4 rank orders,previously measured original pretrained forward pass; cached scores reused for unified rerun
5
+ HotpotQA,600,8,native 10 Wikipedia distractor candidates,85.67,86.83,94.17,95.5,8.67,272.18,0.406,feature extraction + 4 rank orders,previously measured original pretrained forward pass; cached scores reused for unified rerun
6
+ 2WikiMultiHopQA,600,2,native 10 Wikipedia candidates,24.83,33.83,61.5,62.83,29.0,254.92,0.322,feature extraction + 4 rank orders,previously measured original pretrained forward pass; cached scores reused for unified rerun
7
+ 2WikiMultiHopQA,600,3,native 10 Wikipedia candidates,34.17,44.5,68.33,70.17,25.67,254.92,0.322,feature extraction + 4 rank orders,previously measured original pretrained forward pass; cached scores reused for unified rerun
8
+ 2WikiMultiHopQA,600,5,native 10 Wikipedia candidates,52.33,58.67,93.0,93.17,34.5,254.92,0.322,feature extraction + 4 rank orders,previously measured original pretrained forward pass; cached scores reused for unified rerun
9
+ 2WikiMultiHopQA,600,8,native 10 Wikipedia candidates,81.5,79.33,98.33,98.17,18.84,254.92,0.322,feature extraction + 4 rank orders,previously measured original pretrained forward pass; cached scores reused for unified rerun
10
+ MuSiQue,300,2,"native ~20 candidates, all gold included",9.67,11.33,16.33,17.67,6.34,538.67,0.662,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
11
+ MuSiQue,300,3,"native ~20 candidates, all gold included",16.0,19.33,25.0,27.0,7.67,538.67,0.662,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
12
+ MuSiQue,300,5,"native ~20 candidates, all gold included",31.0,30.67,43.67,42.33,11.66,538.67,0.662,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
13
+ MuSiQue,300,8,"native ~20 candidates, all gold included",44.0,44.33,58.67,57.67,13.34,538.67,0.662,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
14
+ HoVer,300,2,10 matched HotpotQA candidates; eligible subset only,26.33,33.0,32.33,38.33,5.33,280.38,0.37,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
15
+ HoVer,300,3,10 matched HotpotQA candidates; eligible subset only,47.33,57.0,73.33,72.67,15.67,280.38,0.37,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
16
+ HoVer,300,5,10 matched HotpotQA candidates; eligible subset only,75.67,82.0,89.33,92.33,10.33,280.38,0.37,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
17
+ HoVer,300,8,10 matched HotpotQA candidates; eligible subset only,92.0,92.33,97.33,98.67,6.34,280.38,0.37,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
18
+ MultiHop-RAG,300,2,"MiniLM top-20 from full 609-article corpus, no gold insertion",6.0,7.0,6.0,7.67,0.67,635.04,7.57,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
19
+ MultiHop-RAG,300,3,"MiniLM top-20 from full 609-article corpus, no gold insertion",11.67,13.67,11.67,13.67,0.0,635.04,7.57,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
20
+ MultiHop-RAG,300,5,"MiniLM top-20 from full 609-article corpus, no gold insertion",21.67,23.0,22.33,26.0,3.0,635.04,7.57,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
21
+ MultiHop-RAG,300,8,"MiniLM top-20 from full 609-article corpus, no gold insertion",31.67,36.67,32.33,36.67,0.0,635.04,7.57,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
22
+ QASC,300,2,"MiniLM top-20 from 6,878 fact-sentence pool, no gold insertion",3.33,7.33,3.33,7.33,0.0,69.12,0.34,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
23
+ QASC,300,3,"MiniLM top-20 from 6,878 fact-sentence pool, no gold insertion",5.33,13.0,5.33,13.0,0.0,69.12,0.34,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
24
+ QASC,300,5,"MiniLM top-20 from 6,878 fact-sentence pool, no gold insertion",10.33,17.67,10.33,17.67,0.0,69.12,0.34,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
25
+ QASC,300,8,"MiniLM top-20 from 6,878 fact-sentence pool, no gold insertion",16.0,24.33,16.0,24.33,0.0,69.12,0.34,reported feature extraction + evaluation/selection operation,previous pretrained neural inference
results/six_dataset_results.json ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "rule": {
3
+ "relevance": 0.4,
4
+ "directed_link": 0.5,
5
+ "query_title_match": 0.2,
6
+ "score_normalization": "per-query min-max",
7
+ "selection": "greedy",
8
+ "gold_used_in_selection": false
9
+ },
10
+ "results": {
11
+ "HotpotQA": {
12
+ "dataset": "hotpot",
13
+ "questions": 600,
14
+ "candidate_count_per_question": 10,
15
+ "selection_rule": "same neural_evidence_graph_core.graph_arrays and graph_select as four new datasets: 0.4 * per-query minmax relevance + 0.5 * max selected-to-candidate directed title-body link + 0.2 * query-title match; no tuning, no query facets, no specialized presets",
16
+ "embedding_source": "previously frozen/precomputed actual all-MiniLM-L6-v2 q and passage vectors; cosine recomputed",
17
+ "crossencoder_source": "previously frozen/precomputed actual pretrained ms-marco-MiniLM-L6-v2 256-wordpiece query-passage logits, same ids",
18
+ "complete_evidence_coverage_pct": {
19
+ "dense_cosine": {
20
+ "2": 33.5,
21
+ "3": 50.67,
22
+ "5": 68.0,
23
+ "8": 85.67
24
+ },
25
+ "crossencoder": {
26
+ "2": 45.83,
27
+ "3": 60.83,
28
+ "5": 74.67,
29
+ "8": 86.83
30
+ },
31
+ "dense_graph": {
32
+ "2": 61.67,
33
+ "3": 74.83,
34
+ "5": 84.83,
35
+ "8": 94.17
36
+ },
37
+ "crossencoder_graph": {
38
+ "2": 67.17,
39
+ "3": 80.5,
40
+ "5": 89.83,
41
+ "8": 95.5
42
+ }
43
+ },
44
+ "crossencoder_graph_vs_crossencoder_at3": {
45
+ "diff_pp": 19.67,
46
+ "ci95_pp": [
47
+ 16.17,
48
+ 23.33
49
+ ],
50
+ "wins": 135,
51
+ "losses": 17,
52
+ "ties": 448
53
+ },
54
+ "dense_graph_vs_dense_at3": {
55
+ "diff_pp": 24.17,
56
+ "ci95_pp": [
57
+ 20.33,
58
+ 28.17
59
+ ],
60
+ "wins": 163,
61
+ "losses": 18,
62
+ "ties": 419
63
+ },
64
+ "graph_link_present_pct": 87.83,
65
+ "gold_support_count_distribution": {
66
+ "2": 600
67
+ },
68
+ "timing": {
69
+ "graph_feature_extraction_and_four_rankings_ms_per_question": 0.406,
70
+ "graph_feature_extraction_and_four_rankings_total_s": 0.2437,
71
+ "pretrained_crossencoder_scoring_ms_per_question_from_original_inference_run": 272.18,
72
+ "cached_embedding_and_crossencoder_scores": true
73
+ },
74
+ "note": "This is an actual rerun of ranking/selection and evidence coverage from cached neural scores; scoring step was previously executed, not repeated. Scores are on the same selected 600 held-out question IDs."
75
+ },
76
+ "2WikiMultiHopQA": {
77
+ "dataset": "wiki",
78
+ "questions": 600,
79
+ "candidate_count_per_question": 10,
80
+ "selection_rule": "same neural_evidence_graph_core.graph_arrays and graph_select as four new datasets: 0.4 * per-query minmax relevance + 0.5 * max selected-to-candidate directed title-body link + 0.2 * query-title match; no tuning, no query facets, no specialized presets",
81
+ "embedding_source": "previously frozen/precomputed actual all-MiniLM-L6-v2 q and passage vectors; cosine recomputed",
82
+ "crossencoder_source": "previously frozen/precomputed actual pretrained ms-marco-MiniLM-L6-v2 256-wordpiece query-passage logits, same ids",
83
+ "complete_evidence_coverage_pct": {
84
+ "dense_cosine": {
85
+ "2": 24.83,
86
+ "3": 34.17,
87
+ "5": 52.33,
88
+ "8": 81.5
89
+ },
90
+ "crossencoder": {
91
+ "2": 33.83,
92
+ "3": 44.5,
93
+ "5": 58.67,
94
+ "8": 79.33
95
+ },
96
+ "dense_graph": {
97
+ "2": 61.5,
98
+ "3": 68.33,
99
+ "5": 93.0,
100
+ "8": 98.33
101
+ },
102
+ "crossencoder_graph": {
103
+ "2": 62.83,
104
+ "3": 70.17,
105
+ "5": 93.17,
106
+ "8": 98.17
107
+ }
108
+ },
109
+ "crossencoder_graph_vs_crossencoder_at3": {
110
+ "diff_pp": 25.67,
111
+ "ci95_pp": [
112
+ 22.17,
113
+ 29.33
114
+ ],
115
+ "wins": 159,
116
+ "losses": 5,
117
+ "ties": 436
118
+ },
119
+ "dense_graph_vs_dense_at3": {
120
+ "diff_pp": 34.17,
121
+ "ci95_pp": [
122
+ 30.0,
123
+ 38.0
124
+ ],
125
+ "wins": 209,
126
+ "losses": 4,
127
+ "ties": 387
128
+ },
129
+ "graph_link_present_pct": 81.67,
130
+ "gold_support_count_distribution": {
131
+ "2": 459,
132
+ "4": 141
133
+ },
134
+ "timing": {
135
+ "graph_feature_extraction_and_four_rankings_ms_per_question": 0.322,
136
+ "graph_feature_extraction_and_four_rankings_total_s": 0.1933,
137
+ "pretrained_crossencoder_scoring_ms_per_question_from_original_inference_run": 254.92,
138
+ "cached_embedding_and_crossencoder_scores": true
139
+ },
140
+ "note": "This is an actual rerun of ranking/selection and evidence coverage from cached neural scores; scoring step was previously executed, not repeated. Scores are on the same selected 600 held-out question IDs."
141
+ },
142
+ "MuSiQue": {
143
+ "dataset": "MuSiQue",
144
+ "questions": 300,
145
+ "gold_available_fraction": 100,
146
+ "oracle_complete_coverage_pct": {
147
+ "2": 51.67,
148
+ "3": 87,
149
+ "5": 100,
150
+ "8": 100
151
+ },
152
+ "complete_coverage_pct": {
153
+ "dense_cosine": {
154
+ "2": 9.67,
155
+ "3": 16.0,
156
+ "5": 31.0,
157
+ "8": 44.0
158
+ },
159
+ "crossencoder": {
160
+ "2": 11.33,
161
+ "3": 19.33,
162
+ "5": 30.67,
163
+ "8": 44.33
164
+ },
165
+ "dense_graph": {
166
+ "2": 16.33,
167
+ "3": 25.0,
168
+ "5": 43.67,
169
+ "8": 58.67
170
+ },
171
+ "crossencoder_graph": {
172
+ "2": 17.67,
173
+ "3": 27.0,
174
+ "5": 42.33,
175
+ "8": 57.67
176
+ }
177
+ },
178
+ "timing_seconds": {
179
+ "embedding_seconds": 86.82592989300065,
180
+ "crossencoder_scoring_seconds": 161.59981333699943,
181
+ "graph_selection_ms_per_question": 0.6617960400035372,
182
+ "crossencoder_scoring_ms_per_question": 538.6660444566648,
183
+ "candidate_pool_size": 20
184
+ },
185
+ "crossencoder_graph_minus_crossencoder_at3_pp": 7.67,
186
+ "ci95_pp": [
187
+ 3.33,
188
+ 12.0
189
+ ],
190
+ "candidate_pool": "Native ~20 annotated MuSiQue passages; all gold included"
191
+ },
192
+ "HoVer": {
193
+ "dataset": "hover",
194
+ "questions": 300,
195
+ "candidate_count_total": 3000,
196
+ "candidate_pool": "10 HotpotQA distractor passages, only HoVer claims with all support titles present",
197
+ "gold_support_count_distribution": {
198
+ "2": 152,
199
+ "3": 144,
200
+ "4": 4
201
+ },
202
+ "gold_available_fraction": 100.0,
203
+ "gold_available_count": 300,
204
+ "graph_link_fraction": 94.33,
205
+ "oracle_complete_coverage_pct": {
206
+ "2": 50.67,
207
+ "3": 98.67,
208
+ "5": 100.0,
209
+ "8": 100.0
210
+ },
211
+ "complete_coverage_pct": {
212
+ "dense_cosine": {
213
+ "2": 26.33,
214
+ "3": 47.33,
215
+ "5": 75.67,
216
+ "8": 92.0
217
+ },
218
+ "crossencoder": {
219
+ "2": 33.0,
220
+ "3": 57.0,
221
+ "5": 82.0,
222
+ "8": 92.33
223
+ },
224
+ "dense_graph": {
225
+ "2": 32.33,
226
+ "3": 73.33,
227
+ "5": 89.33,
228
+ "8": 97.33
229
+ },
230
+ "crossencoder_graph": {
231
+ "2": 38.33,
232
+ "3": 72.67,
233
+ "5": 92.33,
234
+ "8": 98.67
235
+ }
236
+ },
237
+ "crossencoder_graph_minus_crossencoder_at3_pp": 15.67,
238
+ "ci95_pp": [
239
+ 9.99,
240
+ 22.0
241
+ ],
242
+ "paired_wins_losses_ties_at3": {
243
+ "wins": 70,
244
+ "losses": 23,
245
+ "ties": 207
246
+ },
247
+ "timing_seconds": {
248
+ "embedding_seconds": 45.51700891600012,
249
+ "prep_seconds": 45.92742176499996,
250
+ "candidate_pool_size": 10,
251
+ "eligibility": "HoVer dev claims with HotpotQA dev passages available and all annotated support titles present; conditional subset",
252
+ "crossencoder_scoring_seconds": 84.113,
253
+ "crossencoder_scoring_ms_per_question": 280.38,
254
+ "crossencoder_forward_seconds": 83.542,
255
+ "graph_selection_total_s": 0.11,
256
+ "graph_selection_ms_per_question": 0.37
257
+ },
258
+ "models": {
259
+ "dense": "user uploaded all-MiniLM-L6-v2",
260
+ "crossencoder": "user uploaded cross-encoder/ms-marco-MiniLM-L6-v2"
261
+ },
262
+ "protocol": "MiniLM embeddings at max 128 WordPiece for dense. Cross-encoder truncates paired query+passage at 256 WordPiece. Both score exact same candidate pools. Fixed evidence graph .4 minmax relevance + .5 title in already-selected passage + .2 query-title score. No TF-IDF anywhere in candidate construction or scoring. Gold used solely for pool audit and evaluation."
263
+ },
264
+ "MultiHop-RAG": {
265
+ "dataset": "multihop",
266
+ "questions": 300,
267
+ "candidate_count_total": 6000,
268
+ "candidate_pool": "Top-20 neural MiniLM retrieval from full 609-article corpus (no gold insertion)",
269
+ "gold_support_count_distribution": {
270
+ "3": 96,
271
+ "2": 158,
272
+ "4": 46
273
+ },
274
+ "gold_available_fraction": 51.67,
275
+ "gold_available_count": 155,
276
+ "graph_link_fraction": 4.0,
277
+ "oracle_complete_coverage_pct": {
278
+ "2": 35.0,
279
+ "3": 48.0,
280
+ "5": 51.67,
281
+ "8": 51.67
282
+ },
283
+ "complete_coverage_pct": {
284
+ "dense_cosine": {
285
+ "2": 6.0,
286
+ "3": 11.67,
287
+ "5": 21.67,
288
+ "8": 31.67
289
+ },
290
+ "crossencoder": {
291
+ "2": 7.0,
292
+ "3": 13.67,
293
+ "5": 23.0,
294
+ "8": 36.67
295
+ },
296
+ "dense_graph": {
297
+ "2": 6.0,
298
+ "3": 11.67,
299
+ "5": 22.33,
300
+ "8": 32.33
301
+ },
302
+ "crossencoder_graph": {
303
+ "2": 7.67,
304
+ "3": 13.67,
305
+ "5": 26.0,
306
+ "8": 36.67
307
+ }
308
+ },
309
+ "crossencoder_graph_minus_crossencoder_at3_pp": 0.0,
310
+ "ci95_pp": [
311
+ -1.33,
312
+ 1.33
313
+ ],
314
+ "paired_wins_losses_ties_at3": {
315
+ "wins": 2,
316
+ "losses": 2,
317
+ "ties": 296
318
+ },
319
+ "timing_seconds": {
320
+ "index_seconds": 8.61646041700078,
321
+ "query_embedding_seconds": 2.9061220870007674,
322
+ "total_prep_seconds": 11.600812000000587,
323
+ "index_size": 609,
324
+ "candidate_pool_size": 20,
325
+ "crossencoder_scoring_seconds": 190.511,
326
+ "crossencoder_scoring_ms_per_question": 635.04,
327
+ "crossencoder_forward_seconds": 176.907,
328
+ "graph_selection_total_s": 2.271,
329
+ "graph_selection_ms_per_question": 7.57
330
+ },
331
+ "models": {
332
+ "dense": "user uploaded all-MiniLM-L6-v2",
333
+ "crossencoder": "user uploaded cross-encoder/ms-marco-MiniLM-L6-v2"
334
+ },
335
+ "protocol": "MiniLM embeddings at max 128 WordPiece for dense. Cross-encoder truncates paired query+passage at 256 WordPiece. Both score exact same candidate pools. Fixed evidence graph .4 minmax relevance + .5 title in already-selected passage + .2 query-title score. No TF-IDF anywhere in candidate construction or scoring. Gold used solely for pool audit and evaluation."
336
+ },
337
+ "QASC": {
338
+ "dataset": "qasc_neural_retrieval",
339
+ "questions": 300,
340
+ "candidate_count_total": 6000,
341
+ "candidate_pool": "Top-20 MiniLM from all unique QASC supporting fact sentences, without gold insertion",
342
+ "gold_support_count_distribution": {
343
+ "2": 300
344
+ },
345
+ "gold_available_fraction": 31.33,
346
+ "gold_available_count": 94,
347
+ "graph_link_fraction": 0.0,
348
+ "oracle_complete_coverage_pct": {
349
+ "2": 31.33,
350
+ "3": 31.33,
351
+ "5": 31.33,
352
+ "8": 31.33
353
+ },
354
+ "complete_coverage_pct": {
355
+ "dense_cosine": {
356
+ "2": 3.33,
357
+ "3": 5.33,
358
+ "5": 10.33,
359
+ "8": 16.0
360
+ },
361
+ "crossencoder": {
362
+ "2": 7.33,
363
+ "3": 13.0,
364
+ "5": 17.67,
365
+ "8": 24.33
366
+ },
367
+ "dense_graph": {
368
+ "2": 3.33,
369
+ "3": 5.33,
370
+ "5": 10.33,
371
+ "8": 16.0
372
+ },
373
+ "crossencoder_graph": {
374
+ "2": 7.33,
375
+ "3": 13.0,
376
+ "5": 17.67,
377
+ "8": 24.33
378
+ }
379
+ },
380
+ "crossencoder_graph_minus_crossencoder_at3_pp": 0.0,
381
+ "ci95_pp": [
382
+ 0.0,
383
+ 0.0
384
+ ],
385
+ "paired_wins_losses_ties_at3": {
386
+ "wins": 0,
387
+ "losses": 0,
388
+ "ties": 300
389
+ },
390
+ "timing_seconds": {
391
+ "index_seconds": 17.435790640998675,
392
+ "query_embedding_seconds": 0.6626583439992828,
393
+ "total_prep_seconds": 18.42298589100028,
394
+ "fact_corpus_size": 6878,
395
+ "eligible_questions": 8134,
396
+ "candidate_pool_size": 20,
397
+ "crossencoder_scoring_seconds": 20.737,
398
+ "crossencoder_scoring_ms_per_question": 69.12,
399
+ "crossencoder_forward_seconds": 20.519,
400
+ "graph_selection_total_s": 0.103,
401
+ "graph_selection_ms_per_question": 0.34
402
+ },
403
+ "models": {
404
+ "dense": "user uploaded all-MiniLM-L6-v2",
405
+ "crossencoder": "user uploaded cross-encoder/ms-marco-MiniLM-L6-v2"
406
+ },
407
+ "protocol": "MiniLM embeddings at max 128 WordPiece for dense. Cross-encoder truncates paired query+passage at 256 WordPiece. Both score exact same candidate pools. Fixed evidence graph .4 minmax relevance + .5 title in already-selected passage + .2 query-title score. No TF-IDF anywhere in candidate construction or scoring. Gold used solely for pool audit and evaluation."
408
+ }
409
+ },
410
+ "timing_notes": "Crossencoder model scoring excludes query retrieval and answer generation; selection-only numbers exclude neural inference. The first two reused prior neural results on identical held-out samples."
411
+ }
results/unified_hotpot_per_question.json ADDED
The diff for this file is too large to render. See raw diff
 
results/unified_hotpot_rerun.json ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dataset": "hotpot",
3
+ "questions": 600,
4
+ "candidate_count_per_question": 10,
5
+ "selection_rule": "same neural_evidence_graph_core.graph_arrays and graph_select as four new datasets: 0.4 * per-query minmax relevance + 0.5 * max selected-to-candidate directed title-body link + 0.2 * query-title match; no tuning, no query facets, no specialized presets",
6
+ "embedding_source": "previously frozen/precomputed actual all-MiniLM-L6-v2 q and passage vectors; cosine recomputed",
7
+ "crossencoder_source": "previously frozen/precomputed actual pretrained ms-marco-MiniLM-L6-v2 256-wordpiece query-passage logits, same ids",
8
+ "complete_evidence_coverage_pct": {
9
+ "dense_cosine": {
10
+ "2": 33.5,
11
+ "3": 50.67,
12
+ "5": 68.0,
13
+ "8": 85.67
14
+ },
15
+ "crossencoder": {
16
+ "2": 45.83,
17
+ "3": 60.83,
18
+ "5": 74.67,
19
+ "8": 86.83
20
+ },
21
+ "dense_graph": {
22
+ "2": 61.67,
23
+ "3": 74.83,
24
+ "5": 84.83,
25
+ "8": 94.17
26
+ },
27
+ "crossencoder_graph": {
28
+ "2": 67.17,
29
+ "3": 80.5,
30
+ "5": 89.83,
31
+ "8": 95.5
32
+ }
33
+ },
34
+ "crossencoder_graph_vs_crossencoder_at3": {
35
+ "diff_pp": 19.67,
36
+ "ci95_pp": [
37
+ 16.17,
38
+ 23.33
39
+ ],
40
+ "wins": 135,
41
+ "losses": 17,
42
+ "ties": 448
43
+ },
44
+ "dense_graph_vs_dense_at3": {
45
+ "diff_pp": 24.17,
46
+ "ci95_pp": [
47
+ 20.33,
48
+ 28.17
49
+ ],
50
+ "wins": 163,
51
+ "losses": 18,
52
+ "ties": 419
53
+ },
54
+ "graph_link_present_pct": 87.83,
55
+ "gold_support_count_distribution": {
56
+ "2": 600
57
+ },
58
+ "timing": {
59
+ "graph_feature_extraction_and_four_rankings_ms_per_question": 0.406,
60
+ "graph_feature_extraction_and_four_rankings_total_s": 0.2437,
61
+ "pretrained_crossencoder_scoring_ms_per_question_from_original_inference_run": 272.18,
62
+ "cached_embedding_and_crossencoder_scores": true
63
+ },
64
+ "note": "This is an actual rerun of ranking/selection and evidence coverage from cached neural scores; scoring step was previously executed, not repeated. Scores are on the same selected 600 held-out question IDs."
65
+ }
results/unified_wiki_per_question.json ADDED
The diff for this file is too large to render. See raw diff
 
results/unified_wiki_rerun.json ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dataset": "wiki",
3
+ "questions": 600,
4
+ "candidate_count_per_question": 10,
5
+ "selection_rule": "same neural_evidence_graph_core.graph_arrays and graph_select as four new datasets: 0.4 * per-query minmax relevance + 0.5 * max selected-to-candidate directed title-body link + 0.2 * query-title match; no tuning, no query facets, no specialized presets",
6
+ "embedding_source": "previously frozen/precomputed actual all-MiniLM-L6-v2 q and passage vectors; cosine recomputed",
7
+ "crossencoder_source": "previously frozen/precomputed actual pretrained ms-marco-MiniLM-L6-v2 256-wordpiece query-passage logits, same ids",
8
+ "complete_evidence_coverage_pct": {
9
+ "dense_cosine": {
10
+ "2": 24.83,
11
+ "3": 34.17,
12
+ "5": 52.33,
13
+ "8": 81.5
14
+ },
15
+ "crossencoder": {
16
+ "2": 33.83,
17
+ "3": 44.5,
18
+ "5": 58.67,
19
+ "8": 79.33
20
+ },
21
+ "dense_graph": {
22
+ "2": 61.5,
23
+ "3": 68.33,
24
+ "5": 93.0,
25
+ "8": 98.33
26
+ },
27
+ "crossencoder_graph": {
28
+ "2": 62.83,
29
+ "3": 70.17,
30
+ "5": 93.17,
31
+ "8": 98.17
32
+ }
33
+ },
34
+ "crossencoder_graph_vs_crossencoder_at3": {
35
+ "diff_pp": 25.67,
36
+ "ci95_pp": [
37
+ 22.17,
38
+ 29.33
39
+ ],
40
+ "wins": 159,
41
+ "losses": 5,
42
+ "ties": 436
43
+ },
44
+ "dense_graph_vs_dense_at3": {
45
+ "diff_pp": 34.17,
46
+ "ci95_pp": [
47
+ 30.0,
48
+ 38.0
49
+ ],
50
+ "wins": 209,
51
+ "losses": 4,
52
+ "ties": 387
53
+ },
54
+ "graph_link_present_pct": 81.67,
55
+ "gold_support_count_distribution": {
56
+ "2": 459,
57
+ "4": 141
58
+ },
59
+ "timing": {
60
+ "graph_feature_extraction_and_four_rankings_ms_per_question": 0.322,
61
+ "graph_feature_extraction_and_four_rankings_total_s": 0.1933,
62
+ "pretrained_crossencoder_scoring_ms_per_question_from_original_inference_run": 254.92,
63
+ "cached_embedding_and_crossencoder_scores": true
64
+ },
65
+ "note": "This is an actual rerun of ranking/selection and evidence coverage from cached neural scores; scoring step was previously executed, not repeated. Scores are on the same selected 600 held-out question IDs."
66
+ }
tests/test_core.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pytest
3
+ from evidence_graph_reranker import (Passage, graph_features, select_with_features,
4
+ rank_with_scores, EvidenceGraphReranker)
5
+
6
+ def test_no_links_no_titles_preserves_score_order():
7
+ docs=["abc", "xyz", "pqr"]
8
+ result=rank_with_scores("anything",docs,[0.1,0.9,0.5],k=3)
9
+ assert [r['index'] for r in result]==[1,2,0]
10
+
11
+ def test_chain_complementarity():
12
+ docs=[Passage("Atlas device","Atlas device was released by Novus Labs"),
13
+ Passage("Novus Labs","Novus Labs was founded by Mira Chen"),
14
+ Passage("Other company","This is irrelevant")]
15
+ q='Who founded the company that released the Atlas device?'
16
+ link,match=graph_features(q,docs)
17
+ assert link[0,1] == 1.0
18
+ selected=select_with_features([.95,.15,.4],link,match,k=2)
19
+ assert selected==[0,1]
20
+
21
+ def test_matches_exact_reference_on_synthetic_random_rows():
22
+ import sys
23
+ from pathlib import Path
24
+ sys.path.insert(0,'/mnt/data')
25
+ try:
26
+ from neural_evidence_graph_core import graph_select as reference, graph_arrays as ref_features
27
+ except ImportError:
28
+ pytest.skip('Reference experiment script is absent; public package is still standalone')
29
+ rng=np.random.default_rng(11)
30
+ docs=[{'title':'Atlas device','body':'Novus Labs made Atlas device'},
31
+ {'title':'Novus Labs','body':'Novus Labs founded by Mira Chen'},
32
+ {'title':'Mira Chen','body':'A researcher'}]
33
+ row={'query':'Who founded Novus Labs that made Atlas device?', 'docs':docs}
34
+ link,match=ref_features(row)
35
+ L,M=graph_features(row['query'],[Passage(d['title'],d['body']) for d in docs])
36
+ np.testing.assert_array_equal(L,link)
37
+ np.testing.assert_array_equal(M,match)
38
+ for _ in range(100):
39
+ scores=rng.normal(size=3)
40
+ assert reference(scores,link,match,3)==select_with_features(scores,L,M,3)
41
+
42
+ def test_precomputed_scores_do_not_load_models():
43
+ obj=EvidenceGraphReranker()
44
+ chosen=obj.rank('A', ['x', 'y'], scores=[1.0,.3], k=1)
45
+ assert chosen[0]['index']==0 and obj._model is None
46
+
47
+ def test_invalid_inputs():
48
+ with pytest.raises(ValueError): rank_with_scores('q',['a','b'],[1],k=2)
49
+ with pytest.raises(ValueError): rank_with_scores('q',['a'],[float('nan')],k=1)
50
+ with pytest.raises(ValueError): rank_with_scores('q',['a'],[1],k=0)