Spaces:
Sleeping
Sleeping
Upload 31 files
Browse files- evidence_graph_reranker/__init__.py +6 -0
- evidence_graph_reranker/core.py +137 -0
- evidence_graph_reranker/neural.py +61 -0
- examples/passages.json +6 -0
- examples/use.py +15 -0
- reproduce/evaluate_ms_marco_crossencoder.py +125 -0
- reproduce/hotpot_subset_eval.py +136 -0
- reproduce/minilm_cpu_pipeline.py +96 -0
- reproduce/neural_crossencoder_only.py +60 -0
- reproduce/neural_evidence_graph_core.py +52 -0
- reproduce/neural_musique_corrected.py +35 -0
- reproduce/neural_three_benchmarks.py +157 -0
- reproduce/parquet_lite.py +143 -0
- reproduce/parquet_probe.py +38 -0
- reproduce/unified_original_two_rerun.py +60 -0
- reproduce/verify_release_against_1200.py +19 -0
- reproduce/wiki_subset_eval.py +138 -0
- results/hover_neural_only_results.json +72 -0
- results/methodology.md +60 -0
- results/multihop_neural_only_results.json +73 -0
- results/musique_NEURAL_ONLY_results.json +34 -0
- results/pretrained_crossencoder_hotpot_600_scores.npz +3 -0
- results/pretrained_crossencoder_wiki_600_scores.npz +3 -0
- results/qasc_neural_retrieval_neural_only_results.json +72 -0
- results/six_dataset_results.csv +25 -0
- results/six_dataset_results.json +411 -0
- results/unified_hotpot_per_question.json +0 -0
- results/unified_hotpot_rerun.json +65 -0
- results/unified_wiki_per_question.json +0 -0
- results/unified_wiki_rerun.json +66 -0
- tests/test_core.py +50 -0
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)
|