Spaces:
Sleeping
Sleeping
Download evidence_graph_reranker/core.py from Angshul/Evidence-Graph-Reranker: direct link, hf CLI and curl.
- Browser
- Download file 5.53 kB
-
https://huggingface.co/spaces/Angshul/Evidence-Graph-Reranker/resolve/main/evidence_graph_reranker/core.py
- Command line
-
hf download hf://spaces/Angshul/Evidence-Graph-Reranker/evidence_graph_reranker/core.py
-
curl -L -o core.py https://huggingface.co/spaces/Angshul/Evidence-Graph-Reranker/resolve/main/evidence_graph_reranker/core.py
5.53 kB
| """The *single* fixed graph selector used across all six reported benchmarks. | |
| Only candidate text, its optional title, and a query-specific neural relevance | |
| score are allowed as input. There is no training, gold-label access, or corpus- | |
| specific parameter tuning inside this module. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from typing import Any, Mapping, Sequence | |
| import re | |
| import unicodedata | |
| import numpy as np | |
| def normalize_text(text: Any) -> str: | |
| """Match the exact normalization of the published evaluation scripts.""" | |
| return " ".join( | |
| re.sub(r"[^\w\s]", " ", unicodedata.normalize("NFKD", str(text)).casefold()).split() | |
| ) | |
| class Passage: | |
| title: str | |
| text: str | |
| id: str | None = None | |
| def from_value(cls, value: "Passage | Mapping[str, Any] | str") -> "Passage": | |
| if isinstance(value, Passage): | |
| return value | |
| if isinstance(value, str): | |
| return cls(title="", text=value) | |
| if not isinstance(value, Mapping): | |
| raise TypeError("Each passage must be a string, dict, or Passage.") | |
| text = value.get("text", value.get("body", value.get("paragraph_text", ""))) | |
| return cls(title=str(value.get("title", "") or ""), text=str(text or ""), | |
| id=None if value.get("id") is None else str(value["id"])) | |
| def graph_features(query: str, passages: Sequence[Passage]) -> tuple[np.ndarray, np.ndarray]: | |
| """Return directed source->target article-title edges and query/title matches. | |
| This is intentionally the SAME simple string-link rule as the six-dataset | |
| benchmarks. In particular, it does NOT introduce token overlap, semantic | |
| similarity edges, facet decomposition, or learned relation extraction. | |
| """ | |
| q = normalize_text(query) | |
| titles = [normalize_text(p.title) for p in passages] | |
| bodies = [normalize_text(p.text) for p in passages] | |
| q_words = set(q.split()) | |
| n = len(passages) | |
| link = np.zeros((n, n), np.float32) | |
| match = np.zeros(n, np.float32) | |
| for j, title in enumerate(titles): | |
| if not title: | |
| continue | |
| title_words = set(title.split()) | |
| overlap = len(title_words & q_words) / max(1, len(title_words)) | |
| match[j] = 1.0 if " " + title + " " in " " + q + " " else 0.5 * overlap | |
| for i, body in enumerate(bodies): | |
| if i != j and len(title) >= 4 and " " + title + " " in " " + body + " ": | |
| link[i, j] = 1.0 | |
| return link, match | |
| def minmax_relevance(scores: Sequence[float]) -> np.ndarray: | |
| scores = np.asarray(scores, dtype=np.float32) | |
| if scores.ndim != 1 or not np.isfinite(scores).all(): | |
| raise ValueError("Scores must be a one-dimensional array of finite numbers.") | |
| if not len(scores): | |
| raise ValueError("At least one candidate passage is required.") | |
| lo, hi = float(scores.min()), float(scores.max()) | |
| return (scores - lo) / (hi - lo) if hi > lo + 1e-9 else np.zeros_like(scores) | |
| def select_with_features( | |
| scores: Sequence[float], link: np.ndarray, match: np.ndarray, k: int = 3 | |
| ) -> list[int]: | |
| """Greedy rank with fixed coefficients .4, .5, .2 and deterministic ties. | |
| At step t the score of candidate j is | |
| 0.4 * normalized_relevance_j | |
| + 0.5 * max_{i previously selected} link[i, j] | |
| + 0.2 * query_title_match_j. | |
| """ | |
| if isinstance(k, bool) or not isinstance(k, int) or k < 1: | |
| raise ValueError("k must be a positive integer") | |
| norm_scores = minmax_relevance(scores) | |
| n = len(norm_scores) | |
| link = np.asarray(link, dtype=np.float32) | |
| match = np.asarray(match, dtype=np.float32) | |
| if link.shape != (n, n) or match.shape != (n,): | |
| raise ValueError("Link matrix or match vector dimensions do not match scores") | |
| selected: list[int] = [] | |
| remaining = set(range(n)) | |
| for _ in range(min(k, n)): | |
| nxt = max(remaining, key=lambda j: ( | |
| 0.4 * norm_scores[j] | |
| + 0.5 * max((link[i, j] for i in selected), default=0.0) | |
| + 0.2 * match[j], | |
| -j, | |
| )) | |
| selected.append(nxt) | |
| remaining.remove(nxt) | |
| return selected | |
| def rank_with_scores(query: str, passages: Sequence[Passage | Mapping[str, Any] | str], | |
| scores: Sequence[float], k: int = 3) -> list[dict[str, Any]]: | |
| """Public, network-free inference API if the retriever supplies scores. | |
| Returned `step_score` is the acquisition score at the moment of selection, | |
| not a calibrated relevance probability. | |
| """ | |
| docs = [Passage.from_value(p) for p in passages] | |
| if len(docs) != len(scores): | |
| raise ValueError("Supply exactly one relevance score per candidate passage") | |
| link, match = graph_features(query, docs) | |
| normalized = minmax_relevance(scores) | |
| order = select_with_features(scores, link, match, k) | |
| result = [] | |
| selected = [] | |
| for idx in order: | |
| bonus = max((float(link[i, idx]) for i in selected), default=0.0) | |
| step = 0.4 * float(normalized[idx]) + 0.5 * bonus + 0.2 * float(match[idx]) | |
| item = docs[idx] | |
| result.append({ | |
| "index": idx, "id": item.id, "title": item.title, "text": item.text, | |
| "relevance_score": float(scores[idx]), | |
| "normalized_relevance": float(normalized[idx]), | |
| "directed_link_bonus": bonus, | |
| "query_title_match": float(match[idx]), | |
| "step_score": step, | |
| }) | |
| selected.append(idx) | |
| return result | |