Files
zeekay 1b9ddf36dd decision/capability: kai-1 on the cardinality suite, read again in Python
cardinality.json carries kai-1's keys (weights 0834a74f, bench cap on
CUDA in F32, 2026-10-03) beside Laya's and Jev's, unchanged, and
results/cardinality/kai.* its answers and summaries at every size. Every
option scored: 4 options 96.75%, 16 93.50%, 77 90.75%, 150 90.75%,
against Jev's 90.50%, 85.25%, 84.25% and 94.00%.

cardinality.py builds the cases at 4, 16, 77 and 150 options as bench
cap does, asks Kai over POST /v1/decisions, and prints its accuracy beside
the recorded kai-1 and Jev. --check rebuilds the cases and reproduces
both records question by question. Jev is never called.
2026-10-15 10:43:22 -07:00

202 lines
8.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Choice among K options, K = 4, 16, 77 and 150, through POST /v1/decisions.
K = 4, 16, 77 the frozen jev.banking77_full cases (Banking77 test, 400, harness seed 13). At 4
and 16 the candidates are the gold label and its K-1 nearest labels by word overlap
(hard negatives; ties by seed), in seeded order; at 77 the case is the frozen one.
K = 150 CLINC150 test (clinc/clinc_oos plus @155b9c71, in scope), 400 by seed 13.
These are the cases `results/cardinality.json` was measured on (`bench cap`, which builds them with
the same draws); rebuilding them here and scoring kai-1's and Jev's recorded answers against them
gives each record question by question (--check). Accuracy is `accall`: correct over every question, an
unanswered question counted against. K of 1,000 and more (WordNet) and the shortlist variants
(`sl.`, `bi.`) are kept as recorded and are not rebuilt by this file.
python capability/cardinality.py --score # the recorded runs, kai-1 and Jev
uv run --with datasets python capability/cardinality.py --check # rebuilt cases reproduce kai-1's record
export HANZO_API_KEY=...
uv run --with datasets python capability/cardinality.py --url https://api.hanzo.ai --model kai \\
--out runs/kai [--k 16,77] [--limit 20]
A run writes <out>/<model>.k<K>.preds.json.gz ({"ci/qid": [p, ...]}) and prints its accuracy beside
the recorded kai-1 and Jev at each K. Jev is never called: --jev recorded (the only value) reads
results/cardinality/jev.k<K>.sums.json.gz. A K of 4, 16 or 77 needs only the standard library; 150
needs `datasets` for CLINC150.
"""
import argparse
import concurrent.futures as cf
import gzip
import json
import os
import random
import re
import sys
import time
import urllib.error
import urllib.request
HERE = os.path.dirname(os.path.abspath(__file__))
RESULTS = os.path.join(HERE, "results", "cardinality")
STATES = os.path.join(HERE, "..", "results", "states.json.gz")
KS = [4, 16, 77, 150]
SEED = 13
INS = {"clinc": "Which intent does `message` express?"}
RETRY = {429, 500, 502, 503, 504, 529}
def load(path):
with gzip.open(path, "rt") as f:
return json.load(f)
def words(s):
return set(re.findall(r"[a-z]+", s.lower()))
def banking(k):
rows = load(STATES)["jev.banking77_full"]
if k == 77:
return rows
out = []
for ci, (st, qs, g) in enumerate(rows):
labels = list(qs["intent"]["criteria"])
gl = labels[g["intent"]["idx"]]
rng = random.Random("%d/%d/%d" % (SEED, k, ci))
w = words(gl)
rest = sorted((l for l in labels if l != gl),
key=lambda l: (-len(w & words(l)) / len(w | words(l)), rng.random()))
opts = [gl] + rest[:k - 1]
rng.shuffle(opts)
q = dict(qs["intent"], criteria={l: None for l in opts})
out.append([st, {"intent": q}, {"intent": {"idx": opts.index(gl)}}])
return out
def clinc():
from datasets import load_dataset
ds = load_dataset("clinc/clinc_oos", "plus", split="test", revision="155b9c710419136e17307b80d0a13e68cd46b4ec")
names = ds.features["intent"].names
keep = [i for i, n in enumerate(names) if n != "oos"]
labels = [names[i].replace("_", " ") for i in keep]
rows = [r for r in ds if names[r["intent"]] != "oos"]
random.Random(SEED).shuffle(rows)
q = {"type": "choice", "instructions": INS["clinc"], "criteria": {l: None for l in labels}}
return [[{"message": r["text"]}, {"intent": q}, {"intent": {"idx": keep.index(r["intent"])}}] for r in rows[:400]]
def cases(k):
return banking(k) if k <= 77 else clinc()
def argmax(v):
"""The first index of the largest value, as numpy's argmax."""
return max(range(len(v)), key=lambda i: (v[i], -i))
def oks(rows, p):
"""Per question, in case order, whether its answer's first argmax is gold; unanswered is False."""
return [p.get("%d/%s" % (ci, qid)) is not None and argmax(p["%d/%s" % (ci, qid)]) == g[qid]["idx"]
for ci, (_, qs, g) in enumerate(rows) for qid in qs]
def accall(rows, p):
"""(correct, questions): an unanswered question counts against."""
ok = oks(rows, p)
return sum(ok), len(ok)
def recorded(who, k, n=0):
"""(correct, questions) of a recorded run over its first n cases (0: all), from its summaries."""
path = os.path.join(RESULTS, "%s.k%d.sums.json.gz" % (who, k))
if not os.path.exists(path):
return None
s = load(path)["sums"]
s = s[:n] if n else s
return sum(1 for x in s if x and x["ok"]), len(s)
def ask(url, key, body, tries=6, timeout=300):
raw = json.dumps(body).encode()
headers = {"Content-Type": "application/json"}
if key:
headers["Authorization"] = "Bearer " + key
err = "no attempt"
for attempt in range(tries):
try:
with urllib.request.urlopen(urllib.request.Request(url, data=raw, headers=headers), timeout=timeout) as r:
return json.load(r), None
except urllib.error.HTTPError as e:
err = "HTTP %d %s" % (e.code, e.read()[:300].decode("utf-8", "replace"))
if e.code not in RETRY:
return None, err
time.sleep(min(60, 2 ** attempt))
except Exception as e: # transport, timeout
err = "%s: %s" % (type(e).__name__, str(e)[:200])
time.sleep(min(60, 2 ** attempt))
return None, "gave up: " + err
def run(url, key, model, rows, workers):
"""{"ci/qid": vector over the question's options in order} for every case."""
p, errors = {}, []
def one(ci):
st, qs, _ = rows[ci]
return ci, ask(url, key, {"model": model, "state": st, "questions": qs})
with cf.ThreadPoolExecutor(workers) as ex:
for ci, (d, err) in ex.map(one, range(len(rows))):
for qid, q in rows[ci][1].items():
a = None if err else (d.get("answers") or {}).get(qid)
probs = (a or {}).get("probabilities") or {}
p["%d/%s" % (ci, qid)] = [float(probs.get(l, 0.0)) for l in q["criteria"]] if probs else None
if err:
errors.append("%d: %s" % (ci, err))
return p, errors
def cell(x):
return "%16s" % ("–" if x is None else "%d/%d %6.2f%%" % (x[0], x[1], 100.0 * x[0] / x[1]))
def main():
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--k", default=",".join(map(str, KS)))
ap.add_argument("--url", help="a server; /v1/decisions is appended")
ap.add_argument("--model", default="kai")
ap.add_argument("--out", default="runs")
ap.add_argument("--limit", type=int, default=0, help="the first N cases at each K (a sample)")
ap.add_argument("--workers", type=int, default=8)
ap.add_argument("--jev", default="recorded", choices=["recorded"], help="Jev is scored from its recorded answers")
ap.add_argument("--score", action="store_true", help="print the recorded runs and stop")
ap.add_argument("--check", action="store_true", help="rebuild the cases and score kai-1's recorded answers on them")
a = ap.parse_args()
ks = [int(k) for k in a.k.split(",")]
print("%7s %16s %16s %16s" % ("options", "this run", "kai-1", "jev-1.13"))
for k in ks:
mine = None
if a.check or a.url:
rows = cases(k)
rows = rows[:a.limit] if a.limit else rows
if a.check:
for who in ("kai", "jev"):
p = load(os.path.join(RESULTS, "%s.k%d.preds.json.gz" % (who, k)))
want = [bool(x and x["ok"]) for x in load(os.path.join(RESULTS, "%s.k%d.sums.json.gz" % (who, k)))["sums"]]
if oks(rows, p) != want[:len(rows)]:
sys.exit("k%d: %s's recorded answers scored on the rebuilt cases differ from its record" % (k, who))
mine = accall(rows, load(os.path.join(RESULTS, "kai.k%d.preds.json.gz" % k)))
elif a.url:
p, errors = run(a.url.rstrip("/") + "/v1/decisions", os.environ.get("HANZO_API_KEY"), a.model, rows, a.workers)
os.makedirs(a.out, exist_ok=True)
with gzip.open(os.path.join(a.out, "%s.k%d.preds.json.gz" % (a.model.replace("/", "_"), k)), "wt") as f:
json.dump(p, f)
mine = accall(rows, p)
if errors:
print(" k%d: %d cases unanswered, first: %s" % (k, len(errors), errors[0]))
print("%7d %s %s %s" % (k, cell(mine), cell(recorded("kai", k, a.limit)), cell(recorded("jev", k, a.limit))))
if a.check:
print("rebuilt cases reproduce kai-1's and Jev's records question by question at every K")
if __name__ == "__main__":
main()