Spaces:
Running on Zero
Running on Zero
File size: 8,516 Bytes
d0e7c02 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | """Evaluate served slot adapters on the same cases, side by side, with paired statistics.
python -m slotread.evaluate <set> <n> name=run_dir [name=run_dir ...] (n=0: the whole set)
agent_test 1,600 AGUVIS steps no run trained on (rows 6,000+ of every source)
click ScreenSpot clicks in the agent layout (1,272)
box ScreenSpot element boxes (1,272)
box_refcoco RefCOCO val referents as boxes (general objects; 3,811)
box_coco17 COCO val2017 objects by category name (photos RefCOCO never used)
box_fashion Fashionpedia garments (a new domain)
seg_val RefCOCO val masks (also seg_testA, seg_testB); SEG_EXPR=one: one expression per object
SHARD=0/2 python -m slotread.evaluate seg_val 0 ... every 2nd case from 0 (split across servers)
python -m slotread.evaluate merge <out.json> <shard.json> ...
Every case is read by every adapter (agent, click and box: per-slot requests; seg: one request with
prompt log-probabilities), scored under three readouts (default, peak, expected), and the adapters
are compared case by case: a paired bootstrap interval on the difference, and McNemar's exact test for
right/wrong metrics. A difference counts only when its interval excludes zero.
"""
import itertools, json, math, os, sys, time
from concurrent.futures import ThreadPoolExecutor
import numpy as np
from .client import SlotModel, agent_user
from .data import aguvis, evalsets, refcoco
from .images import data_url, image_of
from .numbers import slots_for
OUT = os.environ.get("SLOTREAD_RESULTS", "results")
CASE_WORKERS = int(os.environ.get("CASE_WORKERS", 6))
HOWS = ("default", "peak", "expected")
PRIMARY = {"agent": "step", "click": "hit", "box": "iou", "seg": "iou"}
BOOT = 10000
def mcnemar(a, b):
"""Two-sided exact McNemar p-value from paired booleans; also the discordant counts."""
n01, n10 = int(np.sum(a & ~b)), int(np.sum(~a & b))
n = n01 + n10
if n == 0:
return 1.0, n01, n10
p = 2 * sum(math.comb(n, i) for i in range(min(n01, n10) + 1)) / 2 ** n
return min(1.0, p), n01, n10
def boot(diff, rng):
"""A paired bootstrap 95% interval on the mean difference."""
idx = rng.integers(0, len(diff), (BOOT, len(diff)))
m = diff[idx].mean(1)
return float(np.percentile(m, 2.5)), float(np.percentile(m, 97.5))
def cases(which, n):
if which == "agent_test":
return [dict(kind="agent", ex=ex) for ex in aguvis.test_steps(n)]
if which in ("click", "box"):
return [dict(kind=which, ex=ex) for ex in evalsets.screenspot(n)]
if which == "box_coco17":
return [dict(kind="box", ex=ex) for ex in evalsets.coco17(n)]
if which == "box_fashion":
return [dict(kind="box", ex=ex) for ex in evalsets.fashion(n or 1000)]
if which == "box_refcoco":
return [dict(kind="box", ex=ex) for ex in refcoco.photo_boxes(n, "validation", seed=0)]
split = {"seg_val": "validation", "seg_testA": "test", "seg_testB": "testB"}[which]
one = os.environ.get("SEG_EXPR", "all") == "one"
return [dict(kind="seg", ex=ex) for ex in refcoco.examples(refcoco.rows(split), n, seed=0, one_sentence=one)]
# accuracy runs send screenshots as the post measured them: full-size PNG (EVAL_IMG_FMT / EVAL_IMG_MAX_PX to
# change). Serving and latency use the faster model-sized JPEG (slotread.images); on ScreenSpot clicks one
# run measured 0.825 with it against 0.860 with full-size PNG, while agent steps were unchanged.
EVAL_FMT = os.environ.get("EVAL_IMG_FMT", "png")
EVAL_MAX_PX = int(os.environ.get("EVAL_IMG_MAX_PX", 0))
def evaluate_case(model, case):
kind, ex = case["kind"], case["ex"]
url = data_url(image_of(ex), fmt=EVAL_FMT, max_px=EVAL_MAX_PX)
a, out = model.arm, {}
if kind in ("agent", "click"):
task = model.tasks["agent"]
user = ex["user"] if kind == "agent" else agent_user(ex["instruction"])
lp = model.read("agent", url, user, ["action"] + slots_for("x", a) + slots_for("y", a))
for how in HOWS:
p = task.decode(lp, a, how)
if kind == "agent":
out[how] = task.score(p, ex)
else:
b = ex["box"]
out[how] = dict(hit=float(b[0] <= p["x"] <= b[2] and b[1] <= p["y"] <= b[3]),
action_click=float(p["action"] == "click"))
elif kind == "box":
task = model.tasks["box"]
lp = model.read("box", url, task.user(ex), task.layout(a))
for how in HOWS:
out[how] = task.score(task.decode(lp, a, how), ex)
else:
task = model.tasks["seg"]
lp = model.read_one("seg", url, task.user(ex))
for how in HOWS:
out[how] = task.score(task.decode(lp, a, how), ex)
return out
def summarize(which, rows, names):
kind = rows[0]["kind"]
key = PRIMARY[kind]
cols = [k for k in sorted(rows[0]["arms"][names[0]]["default"]) if k not in ("inter", "union")]
cols += ["ciou"] if kind == "seg" else []
print(f"\n{which}: {len(rows)} cases\n\n| adapter | readout | " + " | ".join(cols) + " |\n|---|---|" + "---|" * len(cols))
summary = {}
for name in names:
for how in HOWS:
vals = [r["arms"][name][how] for r in rows]
m = {}
for k in cols:
if k == "ciou":
m[k] = sum(v["inter"] for v in vals) / max(1, sum(v["union"] for v in vals))
continue
xs = [v[k] for v in vals if v.get(k) is not None]
m[k] = sum(xs) / len(xs) if xs else None
summary[f"{name}/{how}"] = m
print(f"| {name} | {how} | " + " | ".join("" if m[k] is None else f"{m[k]:.3f}" for k in cols) + " |",
flush=True)
rng = np.random.default_rng(0)
print(f"\nPaired, default readouts, on {key}:\n")
for x, y in itertools.combinations(names, 2):
vx = np.array([r["arms"][x]["default"][key] or 0.0 for r in rows], dtype=float)
vy = np.array([r["arms"][y]["default"][key] or 0.0 for r in rows], dtype=float)
lo, hi = boot(vx - vy, rng)
extra = ""
if kind in ("agent", "click"):
p, n01, n10 = mcnemar(vx > 0.5, vy > 0.5)
extra = f" | McNemar p {p:.3f} ({n01} / {n10})"
verdict = "tie" if lo <= 0 <= hi else (f"{x} ahead" if lo > 0 else f"{y} ahead")
print(f"- {x} {vx.mean():.3f} vs {y} {vy.mean():.3f}: diff {vx.mean() - vy.mean():+.3f} "
f"[{lo:+.3f}, {hi:+.3f}]{extra} -> {verdict}", flush=True)
return summary
def merge(out, parts):
"""Rejoin shards of one set (every row carries every adapter, so pairing survives the split)."""
ds = [json.load(open(p)) for p in parts]
rows = [r for d in ds for r in d["rows"]]
summary = summarize(ds[0]["which"], rows, list(ds[0]["arms"]))
json.dump(dict(which=ds[0]["which"], n=len(rows), arms=ds[0]["arms"], summary=summary, rows=rows, shards=parts),
open(out, "w"))
print(f"\nmerged {len(parts)} shards, {len(rows)} cases -> {out}", flush=True)
def main(args=None):
args = args or sys.argv[1:]
if args[0] == "merge":
return merge(args[1], args[2:])
which, n = args[0], int(args[1])
models = [SlotModel(*a.split("=", 1)) for a in args[2:]]
cs = cases(which, n)
shard = os.environ.get("SHARD")
if shard: # "i/k": every k-th case starting at i
i, k = map(int, shard.split("/"))
cs = cs[i::k]
print(f"{which}: {len(cs)} cases{f' (shard {shard})' if shard else ''}, adapters {', '.join(m.name for m in models)}",
flush=True)
def one(ic):
i, c = ic
return dict(id=i, kind=c["kind"], group=list(c["ex"].get("group", [])),
arms={m.name: evaluate_case(m, c) for m in models})
rows, t = [], time.time()
with ThreadPoolExecutor(CASE_WORKERS) as p:
for k, row in enumerate(p.map(one, enumerate(cs))):
rows.append(row)
if (k + 1) % 200 == 0:
print(f" {k + 1}/{len(cs)} ({time.time() - t:.0f}s)", flush=True)
summary = summarize(which, rows, [m.name for m in models])
os.makedirs(OUT, exist_ok=True)
path = f"{OUT}/eval_{which}_{n}{'_s' + shard.replace('/', 'of') if shard else ''}.json"
json.dump(dict(which=which, n=len(rows), arms={m.name: m.path for m in models}, summary=summary, rows=rows),
open(path, "w"))
print(f"\nwrote {path}", flush=True)
if __name__ == "__main__":
main()
|