a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
152 lines
3.9 KiB
Go
152 lines
3.9 KiB
Go
package stdbench
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// DefaultKs are the rank cutoffs Recall@K / Precision@K report.
|
|
var DefaultKs = []int{1, 5, 10, 20}
|
|
|
|
// Retriever ranks corpus Doc IDs for a query, best first, capped at k.
|
|
type Retriever func(query string, k int) []string
|
|
|
|
// Metrics is the aggregate score of a retriever over a Dataset.
|
|
type Metrics struct {
|
|
Dataset string `json:"dataset"`
|
|
Queries int `json:"queries"`
|
|
Scored int `json:"scored"` // queries with a relevance judgement
|
|
RecallAtK map[int]float64 `json:"recall_at_k"`
|
|
PrecAtK map[int]float64 `json:"precision_at_k"`
|
|
NDCGAt10 float64 `json:"ndcg_at_10"`
|
|
MRR float64 `json:"mrr"`
|
|
}
|
|
|
|
// Evaluate runs retrieve against every query in ds and aggregates the
|
|
// standard retrieval metrics. Queries with no relevance judgement are
|
|
// counted in Queries but excluded from the metric averages. ks is the
|
|
// Recall@K / Precision@K cutoff set; pass nil for DefaultKs.
|
|
func Evaluate(ds Dataset, retrieve Retriever, ks []int) Metrics {
|
|
if len(ks) == 0 {
|
|
ks = DefaultKs
|
|
}
|
|
maxK := 10 // NDCG@10 always needs at least the top 10.
|
|
for _, k := range ks {
|
|
if k > maxK {
|
|
maxK = k
|
|
}
|
|
}
|
|
|
|
m := Metrics{
|
|
Dataset: ds.Name,
|
|
Queries: len(ds.Queries),
|
|
RecallAtK: make(map[int]float64, len(ks)),
|
|
PrecAtK: make(map[int]float64, len(ks)),
|
|
}
|
|
recallSum := make(map[int]float64, len(ks))
|
|
precSum := make(map[int]float64, len(ks))
|
|
var ndcgSum, rrSum float64
|
|
|
|
for _, q := range ds.Queries {
|
|
if len(q.Relevant) == 0 {
|
|
continue
|
|
}
|
|
m.Scored++
|
|
ranked := retrieve(q.Text, maxK)
|
|
for _, k := range ks {
|
|
hit := 0
|
|
for i, id := range ranked {
|
|
if i >= k {
|
|
break
|
|
}
|
|
if q.Relevant[id] > 0 {
|
|
hit++
|
|
}
|
|
}
|
|
recallSum[k] += float64(hit) / float64(len(q.Relevant))
|
|
precSum[k] += float64(hit) / float64(k)
|
|
}
|
|
ndcgSum += ndcg(ranked, q.Relevant, 10)
|
|
rrSum += reciprocalRank(ranked, q.Relevant)
|
|
}
|
|
|
|
if m.Scored > 0 {
|
|
for _, k := range ks {
|
|
m.RecallAtK[k] = recallSum[k] / float64(m.Scored)
|
|
m.PrecAtK[k] = precSum[k] / float64(m.Scored)
|
|
}
|
|
m.NDCGAt10 = ndcgSum / float64(m.Scored)
|
|
m.MRR = rrSum / float64(m.Scored)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// ndcg computes normalized discounted cumulative gain at cutoff k using
|
|
// the graded relevance labels in rel. Returns 0 when no relevant doc
|
|
// exists (ideal DCG would be zero).
|
|
func ndcg(ranked []string, rel map[string]int, k int) float64 {
|
|
dcg := 0.0
|
|
for i, id := range ranked {
|
|
if i >= k {
|
|
break
|
|
}
|
|
if g := rel[id]; g > 0 {
|
|
dcg += float64(g) / math.Log2(float64(i+2))
|
|
}
|
|
}
|
|
grades := make([]int, 0, len(rel))
|
|
for _, g := range rel {
|
|
if g > 0 {
|
|
grades = append(grades, g)
|
|
}
|
|
}
|
|
sort.Sort(sort.Reverse(sort.IntSlice(grades)))
|
|
idcg := 0.0
|
|
for i, g := range grades {
|
|
if i >= k {
|
|
break
|
|
}
|
|
idcg += float64(g) / math.Log2(float64(i+2))
|
|
}
|
|
if idcg == 0 {
|
|
return 0
|
|
}
|
|
return dcg / idcg
|
|
}
|
|
|
|
// reciprocalRank returns 1/rank of the first relevant hit, or 0 when no
|
|
// relevant doc appears in the ranked list.
|
|
func reciprocalRank(ranked []string, rel map[string]int) float64 {
|
|
for i, id := range ranked {
|
|
if rel[id] > 0 {
|
|
return 1.0 / float64(i+1)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Markdown renders the metrics as a Markdown section.
|
|
func (m Metrics) Markdown() string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "### %s\n\n", m.Dataset)
|
|
fmt.Fprintf(&b, "_%d queries · %d scored against relevance judgements_\n\n", m.Queries, m.Scored)
|
|
b.WriteString("| metric | value |\n|--------|-------|\n")
|
|
|
|
ks := make([]int, 0, len(m.RecallAtK))
|
|
for k := range m.RecallAtK {
|
|
ks = append(ks, k)
|
|
}
|
|
sort.Ints(ks)
|
|
for _, k := range ks {
|
|
fmt.Fprintf(&b, "| Recall@%d | %.3f |\n", k, m.RecallAtK[k])
|
|
}
|
|
for _, k := range ks {
|
|
fmt.Fprintf(&b, "| Precision@%d | %.3f |\n", k, m.PrecAtK[k])
|
|
}
|
|
fmt.Fprintf(&b, "| NDCG@10 | %.3f |\n", m.NDCGAt10)
|
|
fmt.Fprintf(&b, "| MRR | %.3f |\n", m.MRR)
|
|
return b.String()
|
|
}
|