code wiki / (root) / nx_ir_eval.nx

nx_ir_eval.nx source

↩ module page · 139 lines · 4576 B

1// nx_ir_eval.nx -- sovereign IR evaluation metrics: THE RULER for search SOTA. 2// 3// module: nishi-core.search.ir_eval 4// depends: fx.nx, syscalls.nx 5// capability: CORE_COMPUTE 6// wired_status: LIBRARY 7// 8// genealogy_id: jarvelin_kekalainen_2002_ndcg ; voorhees_trec_mrr ; trec_map 9// 10// WHY: "state of the art" is a MEASURED claim, never an asserted one. Before any 11// fusion/rerank rung may claim a lift it must move a number on a FIXED ruler. 12// This organ is that ruler -- nDCG@k, MRR, Recall@k, Precision@k, and Average 13// Precision -- computed bit-exact in Q16.16 (fx.nx) so a score is reproducible on 14// every target and a faked improvement cannot hide. The rank discount is 15// 1/log2(rank+1) via fx_log2_int (Jarvelin & Kekalainen 2002), the exact use 16// fx.nx's fixed-point log2 was built for. 17// 18// Conventions: 19// gains[i] = graded relevance of the doc at 0-based rank i (>= 0). 20// rels[i] = 1 if the doc at rank i is relevant, else 0. 21// n = number of ranked results; k = cutoff. 22// All ratio outputs are Q16.16 in [0, FX_ONE(=65536)]; 0 means undefined/empty. 23 24import "fx.nx" 25import "syscalls.nx" 26 27// ---- discounted cumulative gain ---------------------------------- 28 29// DCG@k = sum_{i=0}^{min(n,k)-1} gain[i] / log2(i+2). Rank r=i+1, discount 30// log2(r+1)=log2(i+2); the rank-1 discount log2(2)=1.0 is exact. Result Q16.16. 31func ie_dcg_at_k(gains: *i64, n: i64, k: i64) -> i64 { 32 var lim: i64 = n 33 if k < lim { lim = k } 34 var acc: i64 = 0 35 var i: i64 = 0 36 while i < lim { 37 let g: i64 = gains[i] 38 if g > 0 { 39 let disc: i64 = fx_log2_int(i + 2) 40 acc = acc + fx_div(fx_from_int(g), disc) 41 } 42 i = i + 1 43 } 44 return acc 45} 46 47// Copy src -> dst and selection-sort dst into DESCENDING order (the ideal 48// ranking). n small (top-k); deterministic by construction. 49func ie_sort_desc(src: *i64, n: i64, dst: *i64) -> i64 { 50 var i: i64 = 0 51 while i < n { dst[i] = src[i]; i = i + 1 } 52 var a: i64 = 0 53 while a < n { 54 var best: i64 = a 55 var b: i64 = a + 1 56 while b < n { 57 if dst[b] > dst[best] { best = b } 58 b = b + 1 59 } 60 if best != a { 61 let t: i64 = dst[a] 62 dst[a] = dst[best] 63 dst[best] = t 64 } 65 a = a + 1 66 } 67 return 0 68} 69 70// nDCG@k = DCG@k / IDCG@k, Q16.16 in [0, FX_ONE]. IDCG uses gains sorted 71// descending (the ideal ranking). Returns 0 if there are no relevant docs. 72func ie_ndcg_at_k(gains: *i64, n: i64, k: i64) -> i64 { 73 if n <= 0 { return 0 } 74 let ideal: *i64 = sys_mmap(n * 8) as *i64 75 ie_sort_desc(gains, n, ideal) 76 let idcg: i64 = ie_dcg_at_k(ideal, n, k) 77 if idcg <= 0 { return 0 } 78 let dcg: i64 = ie_dcg_at_k(gains, n, k) 79 return fx_div(dcg, idcg) 80} 81 82// ---- reciprocal rank --------------------------------------------- 83 84// MRR for a single query: 1/(1-based rank of the first relevant doc), Q16.16. 85// Returns 0 if no relevant doc appears. 86func ie_mrr(rels: *i64, n: i64) -> i64 { 87 var i: i64 = 0 88 while i < n { 89 if rels[i] != 0 { return fx_from_frac(1, i + 1) } 90 i = i + 1 91 } 92 return 0 93} 94 95// ---- recall / precision ------------------------------------------ 96 97// Count relevant docs within the top-k. 98func ie_hits_at_k(rels: *i64, n: i64, k: i64) -> i64 { 99 var lim: i64 = n 100 if k < lim { lim = k } 101 var h: i64 = 0 102 var i: i64 = 0 103 while i < lim { 104 if rels[i] != 0 { h = h + 1 } 105 i = i + 1 106 } 107 return h 108} 109 110// Recall@k = hits@k / total_relevant, Q16.16. 0 if total_relevant <= 0. 111func ie_recall_at_k(rels: *i64, n: i64, k: i64, total_relevant: i64) -> i64 { 112 if total_relevant <= 0 { return 0 } 113 return fx_from_frac(ie_hits_at_k(rels, n, k), total_relevant) 114} 115 116// Precision@k = hits@k / k, Q16.16. 0 if k <= 0. 117func ie_precision_at_k(rels: *i64, n: i64, k: i64) -> i64 { 118 if k <= 0 { return 0 } 119 return fx_from_frac(ie_hits_at_k(rels, n, k), k) 120} 121 122// ---- average precision ------------------------------------------- 123 124// AP = (sum over relevant ranks r of Precision@r) / total_relevant, Q16.16. 125// The standard TREC single-query AP; mean over queries = MAP (caller averages). 126func ie_ap(rels: *i64, n: i64, total_relevant: i64) -> i64 { 127 if total_relevant <= 0 { return 0 } 128 var acc: i64 = 0 129 var hits: i64 = 0 130 var i: i64 = 0 131 while i < n { 132 if rels[i] != 0 { 133 hits = hits + 1 134 acc = acc + fx_from_frac(hits, i + 1) 135 } 136 i = i + 1 137 } 138 return fx_div(acc, fx_from_int(total_relevant)) 139}