Spaces:
Running
Running
| """Reciprocal rank fusion. | |
| WHY NOT JUST ADD THE SCORES | |
| --------------------------- | |
| Because they are not on the same scale, and no amount of tuning fixes that. | |
| SigLIP image-text cosines on a homogeneous corpus occupy roughly 0.01-0.15 | |
| (the modality gap squashes them); caption-LSA cosines spread over most of | |
| [-1, 1]; TF-IDF cosines are mostly 0 with a thin tail near 1. A weighted sum | |
| of those is governed by whichever signal happens to have the widest spread on | |
| that particular query, so the weight you set is not the weight you get. | |
| Standardising per query (z-scores) helps but still lets one ranker's outlier | |
| drag the result: a single candidate three sigma out on appearance outranks a | |
| candidate that every ranker agrees is second-best. | |
| RRF (Cormack, Clarke & Buettcher, SIGIR 2009) throws the magnitudes away and | |
| keeps only the ORDER: | |
| score(d) = sum over rankers r of w_r / (K + rank_r(d)) | |
| That makes fusion scale-free by construction, and it rewards CONSENSUS: a | |
| document ranked 2nd by three rankers beats one ranked 1st by a single ranker | |
| and 200th by the rest. Which is exactly the failure being fixed β "crossing | |
| red car" returned any clip of a person crossing, because one strong signal on | |
| "crossing" was allowed to win alone. Under RRF the answer has to look right to | |
| the appearance index AND the caption index AND the lexical index. | |
| K (default 60, the constant from the paper) damps the top of the curve: it is | |
| the number of rank positions over which differences stop mattering much, so a | |
| ranker cannot dominate purely by being extremely confident about its #1. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| DEFAULT_K = 60.0 | |
| def ranks_from_scores(scores: np.ndarray) -> np.ndarray: | |
| """0-based ranks; highest score gets rank 0. | |
| NaN means ABSTAIN, and an abstention is given the median rank rather than | |
| the last. This matters: the lexical ranker can only score windows that | |
| actually have a caption, and windows whose context vector was estimated by | |
| the tower have none. Sending those to the bottom would let a ranker veto | |
| every item it has no opinion about, which is the opposite of what "no | |
| evidence" should mean. Median rank is the neutral position β it neither | |
| helps nor hurts. | |
| """ | |
| s = np.asarray(scores, dtype=float) | |
| known = ~np.isnan(s) | |
| r = np.empty(len(s), dtype=float) | |
| if not known.any(): | |
| r[:] = 0.0 | |
| return r | |
| sub = s[known] | |
| order = np.argsort(-sub, kind="stable") | |
| rr = np.empty(len(sub), dtype=float) | |
| rr[order] = np.arange(len(sub), dtype=float) | |
| r[known] = rr | |
| r[~known] = float(np.median(rr)) | |
| return r | |
| def rrf(rankings: dict[str, np.ndarray], weights: dict[str, float] | None = None, | |
| k: float = DEFAULT_K) -> np.ndarray: | |
| """Fuse score vectors (all over the SAME candidate set, same order). | |
| Pass raw scores, not ranks β this converts. Returns the fused score, where | |
| higher is better, on an arbitrary but consistent scale. | |
| """ | |
| if not rankings: | |
| raise ValueError("rrf() needs at least one ranking") | |
| n = len(next(iter(rankings.values()))) | |
| out = np.zeros(n, dtype=float) | |
| for name, sc in rankings.items(): | |
| sc = np.asarray(sc, dtype=float) | |
| if len(sc) != n: | |
| raise ValueError( | |
| f"ranker '{name}' has {len(sc)} scores, expected {n} β every " | |
| "ranker must score the same candidate set") | |
| w = 1.0 if weights is None else float(weights.get(name, 1.0)) | |
| if w == 0.0: | |
| continue | |
| out += w / (k + ranks_from_scores(sc) + 1.0) | |
| return out | |
| def variant_max(vs): | |
| """Row-wise max over query-variant score arrays. All-NaN rows | |
| (episodes absent from a channel's table β they abstain, they are | |
| not errors) stay NaN without numpy's All-NaN-slice RuntimeWarning, | |
| which was leaking source lines into bench stdout.""" | |
| m = np.stack(vs) | |
| out = np.full(m.shape[1], np.nan) | |
| fin = np.isfinite(m).any(0) | |
| if fin.any(): | |
| out[fin] = np.nanmax(m[:, fin], 0) | |
| return out | |
| def explain_fusion(rankings, weights=None, k=DEFAULT_K, idx=None, top=5): | |
| """Per-ranker rank of each fused winner. Answers 'why is this here?' β | |
| which is the question a fused score alone can never answer.""" | |
| fused = rrf(rankings, weights, k) | |
| order = np.argsort(-fused)[:top] if idx is None else idx | |
| rk = {n: ranks_from_scores(s) for n, s in rankings.items()} | |
| return [{"i": int(i), "fused": float(fused[i]), | |
| "ranks": {n: int(r[i]) + 1 for n, r in rk.items()}} | |
| for i in order] | |