mirror of
https://github.com/hanzoai/semantic.git
synced 2026-10-30 15:04:19 +00:00
The recall benchmark run twice from these packages over the same 5,882 turns, with the same reader and the dataset's own scorer: turns in a store.Mem retrieved by cosine, and a kg graph of who did what retrieved by looking up the person a question names. An oracle arm holding the annotated evidence bounds what the reader can do at all. The graph scores 0.223 against the flat index's 0.156 overall — level on the 1,540 answerable questions, 0.502 against 0.200 on the 446 adversarial ones — and leads at every matched rate of declining to answer. It gets there by being selectively blind: it reaches 58-79% of the evidence for questions that have an answer and 27% for questions that do not, because a turn about somebody else is not a candidate however close its wording. Where it loses is reported too. The metric is ported from task_eval/evaluation.py, NLTK's Porter stemmer included, and held against the Python by three golden tests and once end to end: score.py runs the official code over the same predictions and reproduces every figure to three decimals. LoCoMo is CC BY-NC and this module is MIT, so neither the dataset nor their scorer is kept here. Both are fetched on first use, the dataset through ingest.Web. Co-authored-by: Hanzo Dev <[email protected]>
130 lines
3.5 KiB
Go
130 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The metric is only worth reporting if it is the same metric. These tests
|
|
// hold it against the Python it was ported from: testdata/stems.tsv is NLTK's
|
|
// answer for every word the corpus contains, testdata/normalise.tsv is the
|
|
// official normalize_answer over every question, answer and turn, and
|
|
// testdata/grade.tsv is eval_question_answering's score for a spread of
|
|
// answers of every category.
|
|
//
|
|
// All three are written by ref/golden.py, which imports the official code
|
|
// unmodified. Two of them quote the dataset, which is CC BY-NC and so is not
|
|
// kept in this repository; `make golden` writes them and those two tests skip
|
|
// until it has. The stem table is a word list and is here.
|
|
|
|
func TestStem(t *testing.T) {
|
|
for word, want := range pairs(t, "testdata/stems.tsv") {
|
|
if got := stem(word); got != want {
|
|
t.Errorf("stem(%q) = %q, NLTK says %q", word, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalise(t *testing.T) {
|
|
for text, want := range pairs(t, "testdata/normalise.tsv") {
|
|
if got := normalise(text); got != want {
|
|
t.Errorf("normalise(%q) = %q, the scorer says %q", text, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGrade(t *testing.T) {
|
|
f, err := open(t, "testdata/grade.tsv")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
lines := bufio.NewScanner(f)
|
|
lines.Buffer(make([]byte, 0, 1<<20), 1<<20)
|
|
n := 0
|
|
for lines.Scan() {
|
|
field := strings.Split(lines.Text(), "\t")
|
|
if len(field) != 4 {
|
|
t.Fatalf("grade.tsv: %d fields in %q", len(field), lines.Text())
|
|
}
|
|
kind := int(field[0][0] - '0')
|
|
want := parse(t, field[3])
|
|
if got := grade(kind, field[1], field[2]); !near(got, want) {
|
|
t.Errorf("grade(%d, %q, %q) = %.6f, the scorer says %.6f",
|
|
kind, field[1], field[2], got, want)
|
|
}
|
|
n++
|
|
}
|
|
if err := lines.Err(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n == 0 {
|
|
t.Fatal("grade.tsv is empty")
|
|
}
|
|
t.Logf("%d scores agree with the official scorer", n)
|
|
}
|
|
|
|
// open reads a golden file, or skips the test when it has not been written.
|
|
// The two that quote the dataset are not in the repository, since the dataset
|
|
// is not ours to redistribute.
|
|
func open(t *testing.T, path string) (*os.File, error) {
|
|
f, err := os.Open(path)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
t.Skipf("%s has not been written; run `make golden` in bench/locomo", path)
|
|
}
|
|
return f, err
|
|
}
|
|
|
|
// near allows for the last bit of a float written out and read back.
|
|
func near(a, b float64) bool {
|
|
d := a - b
|
|
return d < 1e-9 && d > -1e-9
|
|
}
|
|
|
|
func parse(t *testing.T, s string) float64 {
|
|
f, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
t.Fatalf("parse %q: %v", s, err)
|
|
}
|
|
return f
|
|
}
|
|
|
|
// pairs reads a two-column file of input and expected output.
|
|
func pairs(t *testing.T, path string) map[string]string {
|
|
t.Helper()
|
|
f, err := open(t, path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
out := map[string]string{}
|
|
lines := bufio.NewScanner(f)
|
|
lines.Buffer(make([]byte, 0, 1<<20), 1<<20)
|
|
for lines.Scan() {
|
|
before, after, ok := strings.Cut(lines.Text(), "\t")
|
|
if !ok {
|
|
t.Fatalf("%s: no tab in %q", path, lines.Text())
|
|
}
|
|
out[unescape(before)] = unescape(after)
|
|
}
|
|
if err := lines.Err(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(out) == 0 {
|
|
t.Fatalf("%s is empty", path)
|
|
}
|
|
t.Logf("%s: %d cases", path, len(out))
|
|
return out
|
|
}
|
|
|
|
// unescape puts back the tabs and newlines the golden file escaped so that one
|
|
// case stays on one line.
|
|
func unescape(s string) string {
|
|
return strings.NewReplacer(`\t`, "\t", `\n`, "\n", `\\`, `\`).Replace(s)
|
|
}
|