Two changes that only mean something together. The graph was never read: emptying it left every number identical, so the subject filter was doing all the work and the structure none. The path arm stands on the nodes a question names, gathers the assertions out of them into relations, and cites the turns behind the one that fits — multi-hop 0.020 to 0.032, adversarial 0.502 to 0.576. Run it with the graph emptied and it goes to nothing at all: recall 0.000, quiet on every question, overall 0.228, which is the score for answering nothing. That is the control the old arms could not fail. The reader was the other half. Sixty percent of multi-hop answers are lists and the reader stopped at two parts, so it discarded the shape of the answer before retrieval had a say. Gather states what the memory returned and the oracle moves 0.068 to 0.119, which is what lets a retrieval gain show underneath it at all. Eighty-eight percent of the distance to one is still what an extractive reader cannot write. What was measured and taken back out: coreference touches three percent of sentences and moves multi-hop by nothing; a second hop reaches more evidence and costs adversarial questions dearly, because reaching the trap turn is how those are lost; scoring whole relations has an arity bias that settles on the loose large one over the exact small one. Folding predicates by stem stays, at 3,472 relations to 2,866. Losses: single hop falls 0.121 to 0.111 walking, and to 0.049 when the reader states everything, because category one is marked part by part and every other answerable category is one bag of tokens where an extra part costs precision. Co-authored-by: Hanzo Dev <[email protected]>
33 KiB
LoCoMo
LoCoMo asks what a system remembers of a conversation held over months. Ten conversations, 272 sessions, 5,882 turns, 1,986 questions, and the questions are typed: single-hop, multi-hop, temporal, open-domain, and adversarial — questions built to look answerable when they are not.
Two memories are built here over the same turns and asked the same questions by the same reader, marked by the dataset's own scorer.
- vector — every turn as a weighted bag of stemmed terms in
store.Mem, retrieved by cosine. This is how the benchmark is usually attempted. - graph — every turn read for who did what, then indexed under the person it
was read as being about. Retrieval is still cosine over
store.Mem; what changes is that the question's named subject filters the candidates first.
That second description is deliberate and was corrected after review. An earlier
version of this file said retrieval here is "a lookup on a node, not a nearest
neighbour." That was false, and the ablation is one line: empty the kg graph
entirely and every number below is byte-identical. The graph is where the read
turns are kept; it is not what answers a question. The measured effect comes
from scoping retrieval by subject, not from graph structure and not from
inference — reason derives 44 facts from 23,942, which is decoration at this
scale.
A third memory does read it. path stands on the nodes a question names,
takes the assertions made out of them, gathers those into the relations they
belong to, and cites the turns behind the relation that fits best. It is the
one arm here whose answers come out of the structure, and the same emptying
that leaves every other column untouched leaves this one silent on all 1,986
questions. It is reported apart, under -arms walk.
Two more columns keep the first two readable. graph+edge is the same graph read differently: the answer comes off the matched edge rather than out of the turn it was read from. oracle is handed the turns the dataset itself names as the evidence — perfect retrieval, everything else unchanged — so the distance between an arm and the oracle is what better remembering is still worth, and the distance from the oracle to 1.0 is what the reader costs everybody equally.
The result
go run ./bench/locomo
k=5 floor=0.30 span=4 band=0.90 parts=2
graph graph+edge oracle vector
category n F1 recall quiet F1 recall quiet F1 recall quiet F1 recall quiet
single hop 841 0.121 0.507 24% 0.056 0.507 21% 0.199 1.000 42% 0.118 0.546 20%
multi hop 282 0.020 0.228 18% 0.025 0.228 17% 0.068 0.973 44% 0.023 0.194 17%
temporal 321 0.338 0.532 23% 0.336 0.532 22% 0.379 0.997 33% 0.357 0.581 16%
open domain 96 0.018 0.238 56% 0.018 0.238 55% 0.032 0.951 79% 0.016 0.258 45%
adversarial 446 0.502 0.135 50% 0.484 0.135 48% 0.426 1.000 43% 0.200 0.511 20%
answerable 1540 0.142 0.444 25% 0.107 0.444 23% 0.202 0.991 43% 0.144 0.471 20%
overall 1986 0.223 0.375 31% 0.191 0.375 29% 0.252 0.993 43% 0.156 0.480 20%
F1 is the official metric. recall is the share of a question's evidence
turns the memory returned, which owes nothing to the reader. quiet is how
often it declined to answer.
Read the overall row as arithmetic, not as a result. A system that replies
"No information available" to all 1,986 questions scores 0.228 on this metric,
above every arm in the table that has to find its own evidence — the best of
those is 0.223 — and within 0.024 of oracle, which is handed the annotated
evidence. A quarter of the questions are adversarial and the scorer rewards
declining them, so any aggregate over the whole set rewards silence. The two
halves have to be read apart, and only the split below means anything.
The two memories are level on the 1,540 answerable questions — 0.142 against
0.144, 95% CI [-0.010, +0.006] — and separate on the 446 adversarial ones,
0.502 against 0.200, CI [+0.247, +0.347]. Both intervals are the percentile
bootstrap over the ten conversations, resampling conversations rather than
questions because questions within one conversation share a memory and are not
independent; there are only 92,378 ways to draw ten from ten with replacement,
so -stats enumerates them all and the interval is exact. The
graph declined on half the adversarial questions and a quarter of the
answerable ones. The vector index declined on a fifth of each. It declines at
the same rate on both because it cannot tell them apart. Abstention here is a
threshold on one number, the confidence in the evidence returned, and how well
that number separates an answerable question from an adversarial one is a
property of the memory that owes nothing to where the threshold sits. It
separates them at AUC 0.514 for vector, which is chance, and 0.698 for
graph. The confidence goes out with the predictions and -stats reads it
back, so this is checkable rather than asserted.
Two limits on how far that separation generalizes. Half of it is the subject filter alone rather than anything read from the turns; the 2x2 below takes that apart. And "level on answerable" is weaker than it sounds: the extractive reader tops out at 0.306 there even with perfect retrieval, so that column is nearly insensitive to retrieval quality and could move once a model reader sits behind both arms.
Which of the two changes carries it
The graph arm changes two things at once against the baseline: it indexes claim spans rather than whole turns, and it answers only out of the pieces of the person a question names rather than out of everything. Two cells cannot tell those apart, so here are the other two.
go run ./bench/locomo -arms factor
graph scoped span vector
category n F1 recall quiet F1 recall quiet F1 recall quiet F1 recall quiet
single hop 841 0.121 0.507 24% 0.129 0.533 21% 0.129 0.541 16% 0.118 0.546 20%
multi hop 282 0.020 0.228 18% 0.021 0.219 18% 0.023 0.209 15% 0.023 0.194 17%
temporal 321 0.338 0.532 23% 0.328 0.559 23% 0.362 0.553 16% 0.357 0.581 16%
open domain 96 0.018 0.238 56% 0.016 0.239 47% 0.016 0.225 43% 0.016 0.258 45%
adversarial 446 0.502 0.135 50% 0.352 0.207 35% 0.170 0.571 17% 0.200 0.511 20%
answerable 1540 0.142 0.444 25% 0.144 0.463 23% 0.151 0.463 17% 0.144 0.471 20%
scoped indexes whole turns and files each under every person it was read as
being about; span is the graph's own index with the subject filter taken off.
Adversarial F1 across the four:
| all reachable | only the subject's | |
|---|---|---|
| whole turn | 0.200 | 0.352 |
| claim span | 0.170 | 0.502 |
Scoping the unchanged turn index is worth +0.152. The claim span on its own is
worth -0.029 — it is worse than the baseline, because a narrower unit with
everything still reachable only makes the trap turn easier to find, and span
does retrieve more adversarial evidence than the baseline does, recall 0.571
against 0.511. The two together are worth +0.303, so +0.179 of the result is in
neither factor but in their product: narrowing the unit is what sharpens the
partition, since a turn belongs to every person spoken of in it and a span
belongs to one. (Differences are computed from the unrounded scores, so they do
not always match the differences of the rounded cells.) On the 1,540 answerable
questions no cell moves — 0.142, 0.144, 0.151, 0.144.
Why the adversarial questions separate them
An adversarial question in LoCoMo is not nonsense. It is a question about the
wrong person. From conv-43:
D20:2 John: Hi Tim! Congrats on your success! … I'm also trying out yoga to get a little extra strength and flexibility.
Q What is Tim trying out to improve his strength and flexibility after recovery from ankle injury?
John does yoga. Tim asked him about it. The question carries no reference
answer — its adversarial_answer field holds the plausible wrong one, "yoga",
and the scorer reads neither — so the reply that scores is one that declines.
What the run produced:
vector : 'yoga get little extra' ctx=[D20:2 D20:4 D7:16 D19:1 D19:6]
graph : 'No information available' ctx=[D7:16 D9:9 D18:9 D6:4 D18:13]
oracle : 'yoga get little extra' ctx=[D20:2]
D20:2 is the nearest turn in the conversation to those words, and the vector index is right about that and wrong about the question. The graph never sees D20:2, because D20:2 is not a turn about Tim; the sentence carrying yoga has John as its subject, so the edge hangs off John. Asked about Tim, the graph finds nothing above the floor and says so.
The oracle answers wrongly for the same reason as the vector index, from better evidence. Perfect retrieval is a liability on an adversarial question: the trap turn is the annotated evidence.
What each index can reach at all
Run with the top-k lifted past the number of turns and the recall column stops measuring ranking and starts measuring reach — what the index is capable of returning, however hard you ask.
go run ./bench/locomo -k 2000 -floor 0
category n graph vector
single hop 841 0.787 0.995
multi hop 282 0.688 0.985
temporal 321 0.785 0.997
open domain 96 0.584 0.966
adversarial 446 0.269 0.964
The flat index reaches essentially everything. The graph reaches 58–79% of the evidence for questions that have an answer, and 27% for questions that do not. It is selectively blind, and blind in the right place. The 21–42% it loses on answerable questions is the cost of a rule-based extractor: a sentence whose subject it fails to resolve produces no edge for that person, and the turn becomes unreachable through them.
The trade, at matched silence
Declining to answer is worth a point on an adversarial question and costs one everywhere else, so any comparison at a single threshold is arguable. The threshold is swept instead.
go run ./bench/locomo -sweep floor
graph graph+edge oracle vector
floor 1-4 adv all quiet 1-4 adv all quiet 1-4 adv all quiet 1-4 adv all quiet
0 0.152 0.047 0.129 5% 0.117 0.016 0.094 2% 0.306 0.045 0.247 3% 0.149 0.092 0.136 8%
0.1 0.152 0.070 0.134 6% 0.117 0.038 0.099 3% 0.270 0.182 0.251 19% 0.149 0.101 0.138 9%
0.2 0.151 0.217 0.166 13% 0.114 0.195 0.133 11% 0.238 0.285 0.248 30% 0.148 0.132 0.145 12%
0.3 0.142 0.502 0.223 31% 0.107 0.484 0.191 29% 0.202 0.426 0.252 43% 0.144 0.200 0.156 20%
0.4 0.126 0.693 0.253 48% 0.094 0.682 0.226 47% 0.164 0.554 0.251 55% 0.131 0.323 0.174 33%
0.5 0.103 0.845 0.270 63% 0.078 0.841 0.249 62% 0.121 0.697 0.251 67% 0.115 0.491 0.199 48%
0.6 0.080 0.948 0.275 75% 0.058 0.946 0.257 74% 0.095 0.791 0.251 75% 0.092 0.668 0.222 62%
0.8 0.038 0.975 0.248 91% 0.030 0.973 0.242 90% 0.042 0.922 0.240 91% 0.043 0.879 0.231 86%
Reading across for equal quiet — each arm at whichever floor makes it as
silent as the other:
| silence | graph 1–4 | graph adv | vector 1–4 | vector adv |
|---|---|---|---|---|
| ~12% | 0.151 | 0.217 | 0.148 | 0.132 |
| ~32% | 0.142 | 0.502 | 0.131 | 0.323 |
| ~48% | 0.126 | 0.693 | 0.115 | 0.491 |
| ~62% | 0.103 | 0.845 | 0.092 | 0.668 |
(graph at floors 0.2, 0.3, 0.4, 0.5 against vector at 0.2, 0.4, 0.5, 0.6.)
At each of those four rates the graph is ahead of the flat index on both halves
of the benchmark. The lead belongs to the middle of the range: matched at 8%
silence — vector declines no less often than that even with the floor at zero
— the adversarial scores are 0.094 against 0.092, and below that rate vector
cannot be matched at all. Silence alone does not buy the result: either arm can
be made to fall silent as often as you like, and over the range where declining
is worth anything only one of them falls silent on the right questions.
Reading the graph instead of ranking text
go run ./bench/locomo -arms walk
graph oracle oracle+set path path+set vector vector+set
category n F1 recall quiet F1 recall quiet F1 recall quiet F1 recall quiet F1 recall quiet F1 recall quiet F1 recall quiet
single hop 841 0.121 0.507 24% 0.199 1.000 42% 0.203 1.000 42% 0.111 0.467 27% 0.049 0.467 24% 0.118 0.546 20% 0.089 0.546 12%
multi hop 282 0.020 0.228 18% 0.068 0.973 44% 0.119 0.973 44% 0.032 0.222 26% 0.071 0.222 25% 0.023 0.194 17% 0.066 0.194 13%
temporal 321 0.338 0.532 23% 0.379 0.997 33% 0.379 0.997 33% 0.353 0.512 21% 0.354 0.512 21% 0.357 0.581 16% 0.356 0.581 15%
open domain 96 0.018 0.238 56% 0.032 0.951 79% 0.036 0.951 78% 0.016 0.213 61% 0.024 0.213 60% 0.016 0.258 45% 0.017 0.258 36%
adversarial 446 0.502 0.135 50% 0.426 1.000 43% 0.426 1.000 43% 0.576 0.130 58% 0.563 0.130 56% 0.200 0.511 20% 0.128 0.511 13%
answerable 1540 0.142 0.444 25% 0.202 0.991 43% 0.214 0.991 43% 0.141 0.415 28% 0.115 0.415 26% 0.144 0.471 20% 0.136 0.471 15%
overall 1986 0.223 0.375 31% 0.252 0.993 43% 0.262 0.993 43% 0.239 0.351 34% 0.216 0.351 33% 0.156 0.480 20% 0.134 0.480 14%
Two changes are crossed here, and they are the two halves of what a multi-hop
question needs. path is the memory: relations instead of nearest turns. +set
is the reader: Gather states every piece of evidence the memory returned
rather than the best one or two, so the width of an answer is decided by the
memory alone. Both are available to every arm, which is what makes the columns
comparable.
The memory. On the multi-hop questions path scores 0.032 against graph's
0.020 and vector's 0.023, with the same reader and near-identical evidence
recall — 0.222 against 0.228. It is not finding more of the evidence. It is
citing a different five turns out of the same reach, and the difference is that
the five best turns by wording are five sayings of one thing while the five best
relations are five things. It is also better at silence: 0.576 adversarial
against 0.502, because an assertion about the wrong person is not reached from
the right person's node at all.
The reader. +set is worth more than the memory and is worth it in exactly
one category. Multi-hop goes 0.023 → 0.066 on vector, 0.032 → 0.071 on path,
and — the number that says what kind of bound this is — 0.068 → 0.119 on
oracle, which is handed the annotated evidence. Perfect retrieval read the
old way scored 0.068; perfect retrieval read this way scores 0.119. The ceiling
moved, so a retrieval result can now show up underneath it, and path's does.
And it is a trade, not a gain. Single-hop falls 0.111 → 0.049 on path and
0.118 → 0.089 on vector; adversarial falls 0.200 → 0.128 on vector. The
metric is asymmetric and that is the whole of the explanation: a category 1
answer is marked part by part, so a reply part that matches nothing costs
nothing, while every other answerable category is marked as one bag of tokens,
where it costs precision. Listing everything you have is free where the answer
is a list and expensive where it is not.
The control
go run ./bench/locomo -arms walk -blind
-blind empties every graph after building it and changes nothing else — same
claims, same pieces, same vectors, same subject resolution. graph, vector,
oracle and both +set columns come back byte-identical. path and path+set
go to 100% quiet, 0.000 evidence recall, 0.004 on the answerable questions and
0.228 overall, which is what constant abstention scores. That is the whole
difference between a report that mentions a graph and a report that reads one,
and it is a flag rather than a paragraph so that anybody can check it.
How far to walk
Two and three hops were built and measured, guided — a hop taken only through a
node the question mentions something of — and unguided. Every arm below is read
with Gather, including graph, so that the only thing varying across the
columns is how far the walk goes; that is why the figures differ from the table
above, where graph and path are read with Reply.
graph path path2 path3 walk2 walk3
multi hop 282 0.064 0.228 17% 0.071 0.222 25% 0.071 0.223 25% 0.071 0.223 25% 0.074 0.229 20% 0.071 0.226 19%
adversarial 446 0.484 0.135 48% 0.563 0.130 56% 0.534 0.150 53% 0.534 0.150 53% 0.305 0.351 30% 0.258 0.404 26%
overall 1986 0.194 0.375 29% 0.216 0.351 33% 0.209 0.357 32% 0.209 0.357 32% 0.159 0.404 25% 0.148 0.420 23%
The second hop moves multi-hop by 0.000 guided and 0.003 unguided, and costs
0.029 and 0.258 on the adversarial questions. The third moves nothing at three
decimals either way. Reach was never the constraint. Over all ten
conversations, one hop from the node a question names reaches 90.1% of the
multi-hop evidence and 84.9–88.0% of the evidence for the other answerable
categories, against 34.0% of the adversarial evidence — and two hops reach
95.4% of the adversarial evidence, which is exactly the trap those questions are
built around. reach_test.go asserts that shape on the first two conversations,
where it is the same and runs in six seconds. Going further out only makes more
to choose from. So the arm walks one hop and the hop setting is gone; this table
is the reason, and the code says it rather than carrying a knob nobody should
turn.
Where the graph does not win
Multi-hop. The expectation going in was that multi-hop favours a graph,
since one person's several answers were said months apart and are one node's
several edges here. Retrieval bears that out — recall 0.228 against 0.194 at
k=5, and 0.419 against 0.388 under -k 20 -parts 4 -band 0.5, the settings
that let an answer be a list. F1 does not move with it: 0.020 against 0.023,
and 0.053 against 0.057. Gathering the turns is not the hard part of a
multi-hop answer; assembling four phrases from four sessions into one list is.
Both halves of that were then taken apart, and the section above has the
result. 169 of the 282 multi-hop answers are comma-separated lists, and Reply
states at most two pieces of evidence, so the reader was throwing the shape of
the answer away before retrieval got a say: handed the annotated evidence and
allowed to state all of it, the oracle goes 0.068 → 0.119. Underneath that
raised ceiling path scores 0.032 against 0.020 by citing relations rather than
turns, and 0.071 with the wider reader. The ceiling is still overwhelmingly the
reader — 0.119 with perfect evidence means 88% of the distance to 1.0 is what an
extractive reader cannot write, and the last column of -arms read is where a
model reader has to answer for that.
Two things that looked like they should help do not. Time: extract.Link
carries From, and every claim here is dated by its session, but only 26 of the
282 multi-hop questions contain a word about time at all, so there is no surface
for the temporal machinery to work on — the strength of the temporal category
(0.338) does not transfer. Coreference: extract.Coref over each session,
so that "she" reaches the name an earlier turn gave it, was wired in and
measured. It reaches 3.1% of sentences and moves temporal by +0.009, adversarial
by -0.006, multi-hop by 0.000 and the overall figure by -0.001. A name already
folds to one node whichever session it was said in, so the cross-session
aggregation those questions need was there before the pronouns were. The code
was taken back out.
Answering off an edge. graph+edge reads the answer from the matched edge —
the object phrase the extractor produced — instead of quoting the turn. It is
worse on single-hop by more than half, 0.056 against 0.121. The extracted object
is a lossy paraphrase of the sentence, and the sentence is what the reference
answer was written from. The graph is a better index than it is a store of
answers, at least when the extractor is rules rather than a model.
Inference. reason runs over the extracted triples with one rule, that what
a thing is called is also what its owner has. Across all ten conversations it
derived 44 facts from 23,942. Conversational memory is dominated by aggregation
over identity — the same person mentioned in two sessions is one key, the folded
name each piece is filed under, which needs no inference at all — and there is
very little left for a reasoner to add. That is a finding, not a defect: it says
where the value in this kind of graph actually sits. Deleting the derived facts
moves nothing by more than two thousandths: single-hop F1 rises 0.121 → 0.122,
single-hop recall falls 0.507 → 0.505, and no F1 on either half moves at three
decimals.
Absolute numbers. Everything here is low. The reader is deterministic and extractive: it picks the sentence covering most of the question, drops the words the question already contained, and keeps the rarest few of what is left. No model is called. The oracle row bounds every arm — 0.306 on answerable questions with perfect retrieval and no abstention — and that bound is the reader's, not the memories'. A model reader would raise every column; it would also make the comparison a comparison of two prompts.
How it is put together
locomo.go the dataset: sessions in order, dates parsed, evidence ids
talk.go reading dialogue: who a sentence is about, and what it claims
graph.go the graph arm: claims -> kg -> reason -> a store filtered by person
vector.go the baseline: one weighted vector per turn
path.go the walk arm: relations off the nodes a question names
oracle.go the arm that cheats, to bound the reader
terms.go one vocabulary, weighed once, shared by everything
read.go the readers, shared by every arm
metric.go the official metric, ported
porter.go the stemmer the official metric runs
factor.go the other two cells: turns scoped, spans unscoped
bench.go the experiment and the table
stats.go what the table cannot show, read back off the predictions
Reading dialogue. The library's extract.Rules knows how to find a company
being founded; a friend saying "I went to a support group yesterday" is a
different grammar. talk.go is its reader, and one observation does most of the
work: in a two-party conversation the subject of a sentence is settled by who is
speaking. I is the speaker, you is the other one, a name is that person, and
a sentence naming nobody is about the speaker. Two rules keep false claims out,
and both are grammar rather than tuning:
- A question asserts nothing. "How's it going with yoga?" does not make the asker someone who does yoga. This is what stops a topic leaking from the person who raised it to the person who answered, and it is exactly what the adversarial category punishes.
- A negation is kept. "I don't eat dairy" is stored with the negation on the predicate, so it is findable and is not mistaken for its opposite.
98.9% of LoCoMo questions (1,964 of 1,986) name one of the two speakers, which is what makes a lookup on a person the right query for them. The resolver takes the longest name it knows that the question contains, speakers and third parties alike, and so resolves 1,975 of them (99.45%); the remaining 11 name nobody it knows and fall back to searching every piece. The run prints both counts.
What is indexed. The baseline indexes turns. The graph indexes pieces — what one person said in one turn — so a turn holds as many pieces as it has people spoken of, and a question reaches only the pieces of the person it names. The claims decide which sentences belong to a piece; the sentences are what it is ranked on. Ranking on the extracted claims alone would measure how well the extractor paraphrases rather than what the graph knows about whom.
Both arms rank by cosine over the same weighted terms through store.Mem, and
the subject restriction is a metadata filter (store.Filter{}.Eq(store.Space, who)) rather than a second store. The comparison is therefore an ablation of
the index, not of two systems.
Confidence is computed once, in Sure, outside the arms: how much of the
question's terms the best turn a memory returned accounts for, weighted by how
rare each term is. Computing it over turns rather than over whatever an arm
happens to index is what keeps the number meaning the same thing in every
column — a memory indexing short pieces would otherwise look less sure of the
same evidence, and the report would be measuring text length.
The reader never sees the question's category and never sees which arm
produced the evidence. It reads a date off the session for a when question,
resolving a month named in the text against it — "back in October" in a May
session is the previous October — and otherwise quotes the sentence. Its three
settings barely matter: swept over all ten conversations, span moves the
graph's overall score from 0.215 to 0.224, band from 0.220 to 0.223 and
parts from 0.220 to 0.223. The two settings that do matter, k and floor,
both trade answering against declining, and both are swept above.
The metric is the official one
metric.go and porter.go port task_eval/evaluation.py and the NLTK stemmer
it calls. That port is held against the Python three ways:
TestStem— NLTK's stem for all 6,597 words in the corpus.TestNormalise— the officialnormalize_answerover all 10,382 distinct questions, answers and turns.TestGrade—eval_question_answering's score for 2,400 replies spanning every category and every branch of the metric.
And then end to end: score.py runs the official Python over the same
predictions file the Go wrote.
make check
n graph graph+edge oracle vector
category F1 recall F1 recall F1 recall F1 recall
single hop 841 0.121 0.507 0.056 0.507 0.199 1.000 0.118 0.546
multi hop 282 0.020 0.228 0.025 0.228 0.068 0.973 0.023 0.194
temporal 321 0.338 0.532 0.336 0.532 0.379 0.997 0.357 0.581
open domain 96 0.018 0.238 0.018 0.238 0.032 0.951 0.016 0.258
adversarial 446 0.502 0.135 0.484 0.135 0.426 1.000 0.200 0.511
answerable 1540 0.142 0.444 0.107 0.444 0.202 0.991 0.144 0.471
overall 1986 0.223 0.375 0.191 0.375 0.252 0.993 0.156 0.480
make check scores both predictions files this way, and every figure matches
the Go to three decimals, across 1,986 questions and all six arms.
One adaptation lives in score.py rather than in the vendored file: the
official recall reads context[0] to tell a session id from a dialogue id, and
an arm that returned nothing has no context[0], so a placeholder matching no
evidence is substituted. Without it the official code awards recall 1.0 to an
arm that returned nothing.
Two details of the official scorer are worth stating, because they shape what
the numbers mean. An adversarial answer is scored by looking for the phrases
no information available or not mentioned in the reply — the reference
answer is not consulted. And a multi-hop answer is scored by splitting both
sides on commas and giving each reference part the best-matching reply part, so
recall is rewarded and a wrong extra part costs nothing. parts is bounded at 2
here for that reason; the bound is ours, not the scorer's.
Running it
go run ./bench/locomo # the table above
go run ./bench/locomo -show 5 # answers, side by side, per category
go run ./bench/locomo -sweep floor # or k, span, band, parts
go run ./bench/locomo -arms factor # index unit against subject scope
go run ./bench/locomo -arms walk # reading the graph against ranking text
go run ./bench/locomo -arms walk -blind # the same, over an emptied graph
go run ./bench/locomo -k 2000 -floor 0 # what each index can reach
go run ./bench/locomo -out bench/locomo/out/answers.json
go run ./bench/locomo -stats bench/locomo/out/answers.json
go test ./bench/locomo/
-stats reads a predictions file back and reports the three things the table
cannot show: what constant abstention scores, how far each arm's confidence
tells an answerable question from an adversarial one, and the paired difference
between every pair of arms with its bootstrap interval. -arms chooses which
memories a run builds and every other flag applies to whichever set it names.
Run from the repository root, and with GOWORK=off if a parent go.work is in
the way. Reading ten conversations into both memories and answering 1,986
questions four ways takes 5.2s of CPU (10.8s wall on a busy laptop), measured with
/usr/bin/time -l.
The dataset is fetched on first use, through the library's own ingest.Web, to
bench/locomo/data/locomo10.json — 2,805,274 bytes, SHA-256
79fa87e90f04081343b8c8debecb80a9a6842b76a7aa537dc9fdf651ea698ff4. Take it from
GitHub and not from HuggingFace: adymaharana/locomo there holds a README and
.gitattributes and no data at all, which is a good way to lose an afternoon.
For the Python cross-check:
cd bench/locomo
make golden # writes the two golden files that quote the dataset
make check # scores both predictions files with the official evaluation.py
make stats # constant abstention, the AUCs, the intervals
Both fetch what they need. LoCoMo is CC BY-NC 4.0 and this repository is MIT, so
neither the dataset nor task_eval/evaluation.py is kept here; .gitignore
lists what is fetched. testdata/stems.tsv is a word list and stays.
What was built, per conversation
conv-26 419 turns 1230 terms 1822 claims 1226 nodes 1682 edges 1 derived
conv-30 369 turns 1010 terms 1482 claims 928 nodes 1301 edges 0 derived
conv-41 663 turns 1389 terms 2714 claims 1650 nodes 2434 edges 3 derived
conv-42 629 turns 1390 terms 2394 claims 1510 nodes 2144 edges 8 derived
conv-43 680 turns 1539 terms 2931 claims 1752 nodes 2529 edges 0 derived
conv-44 675 turns 1345 terms 2964 claims 1636 nodes 2578 edges 7 derived
conv-47 689 turns 1543 terms 2664 claims 1685 nodes 2451 edges 22 derived
conv-48 681 turns 1491 terms 2417 claims 1575 nodes 2182 edges 3 derived
conv-49 509 turns 1336 terms 1899 claims 1310 nodes 1725 edges 0 derived
conv-50 568 turns 1274 terms 2655 claims 1538 nodes 2331 edges 0 derived
Caveats
The vector arm weights terms lexically — inverse document frequency, stemmed, unit length, cosine — rather than with a learned embedding, because nothing here reaches a network. On this data that is the harder baseline, not the easier one: LoCoMo questions are written from the turns they ask about and repeat their words, so lexical overlap is strong. A dense retriever generalises across wording. It does not fix the failure this benchmark turns on, which is that a turn near the question's words is not the same thing as a turn about the person the question names — that is a property of what is indexed, not of how the vectors are made.
No LLM was called, in either arm or in the reader. Absolute F1 is therefore well below published model numbers on this benchmark and is not comparable to them. What is comparable is the two columns against each other, since they differ in exactly one thing.