Spaces:
Running on Zero
Running on Zero
File size: 12,417 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 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | """Tasks: what the model sees and answers. A task gives its examples, its slot layout for an arm,
the soft targets, and how a read turns back into a prediction and a score.
agent computer use: action + point (+ swipe end, key, scroll), AGUVIS steps + Wave-UI clicks
box an element's or object's box as centre + log size, Wave-UI elements + RefCOCO referents
seg a referred object's mask as a centre + K ray lengths, RefCOCO
The assistant turn is the slot layout, each slot ending in the placeholder:
action: ___ x: ___ y: ___ x2: ___ y2: ___ key: ___ sdir: ___ samt: ___
and a slot's answer is the next-token distribution at the token before its placeholder.
"""
import math
from . import labels as L
from .data import aguvis, refcoco, waveui
from .geometry import box_iou, mask_iou, poly_mask, radial_decode, radial_encode
from .images import image_of, image_size
from .numbers import LOG_MIN, number_targets, one_hot, read_number, slots_for, unwarp, warp
def coarse_desc(arm, what):
"""How a number's slots are described in a system prompt."""
return f"the tens digit (00-99) and then the fine code of {what}" if arm.coarse else f"the code of {what}"
class Task:
name = ""
numbers = {} # number -> "lin" | "log"
def layout(self, arm):
raise NotImplementedError
def system(self, arm):
raise NotImplementedError
def user(self, ex):
raise NotImplementedError
def number_of(self, slot):
return slot[:-1] if slot.endswith("c") and slot[:-1] in self.numbers else slot
def messages(self, ex, arm, assistant=None):
return [{"role": "system", "content": [{"type": "text", "text": self.system(arm)}]},
{"role": "user", "content": [{"type": "image", "image": image_of(ex)},
{"type": "text", "text": self.user(ex)}]},
{"role": "assistant", "content": [{"type": "text", "text": assistant if assistant is not None
else " ".join(f"{s}: ___" for s in self.layout(arm))}]}]
def labels(self, arm, tok):
out = {}
for s in self.layout(arm):
n = self.number_of(s)
out[s] = L.DIGITS if (n in self.numbers and s.endswith("c") and n != s) else (
arm.fine if n in self.numbers else self.categorical_labels(s, tok))
return out
def categorical_labels(self, slot, tok):
raise KeyError(slot)
def config(self, arm, tok):
"""Everything a reader needs to rebuild this task's prompt and labels (saved in config.json)."""
return dict(slots=self.layout(arm), system=self.system(arm), numbers=self.numbers,
labels=self.labels(arm, tok))
class AgentTask(Task):
"""Computer use. Only the slots an action uses are trained on that example (MASK), so x2 is free
to mean nothing on a click. Text arguments (what to type, the final answer) are not slots: the base
model writes them."""
name = "agent"
numbers = {"x": "lin", "y": "lin", "x2": "lin", "y2": "lin"}
MASK = {"click": ["x", "y"], "select": ["x", "y"], "swipe": ["x", "y", "x2", "y2"],
"scroll": ["sdir", "samt"], "press": ["key"]}
def layout(self, arm):
return (["action"] + slots_for("x", arm) + slots_for("y", arm) + slots_for("x2", arm)
+ slots_for("y2", arm) + ["key", "sdir", "samt"])
def system(self, arm):
return ("You are a GUI agent. Given a screenshot, the task and the previous actions, answer the next step. "
"action: " + ", ".join(aguvis.ACTIONS) + ". "
f"For x, {coarse_desc(arm, 'the horizontal position of the point, from the left edge to the right edge')}; "
f"for y, the same vertically from the top to the bottom. x2 and y2: the end point of a swipe, written "
f"the same way. key: the key to press. sdir and samt: scroll direction and amount.")
def user(self, ex):
return ex["user"]
def categorical_labels(self, slot, tok):
return {"action": L.word_label_ids(tok, "action", aguvis.ACTIONS),
"key": L.word_label_ids(tok, "key", aguvis.KEYS),
"sdir": L.word_label_ids(tok, "sdir", aguvis.SDIR), "samt": L.DIGITS}[slot]
def targets(self, ex, arm):
a, g = ex["action"], ex["args"]
tg = {"action": one_hot(len(aguvis.ACTIONS), aguvis.ACTIONS.index(a))}
for n in self.MASK.get(a, []):
if n in self.numbers:
if g.get(n) is None:
continue
w = g.get("box" if n.startswith("x") else "boy", 0.02) # the target's width / height, if known
tg.update(number_targets(n, g[n], 0.25 * w, arm))
elif n == "key":
tg["key"] = one_hot(len(aguvis.KEYS), aguvis.KEYS.index(g.get("key", "other")))
elif n == "sdir" and g.get("sdir") in aguvis.SDIR:
tg["sdir"] = one_hot(len(aguvis.SDIR), aguvis.SDIR.index(g["sdir"]))
elif n == "samt" and g.get("samt") is not None:
tg["samt"] = one_hot(10, int(g["samt"]))
return tg
def decode(self, lp, arm, how="default"):
out = dict(action=aguvis.ACTIONS[int(lp["action"].argmax())],
x=read_number(lp, "x", arm, how), y=read_number(lp, "y", arm, how))
if "x2" in lp:
out.update(x2=read_number(lp, "x2", arm, how), y2=read_number(lp, "y2", arm, how))
return out
def score(self, pred, ex, thresh=0.14):
"""A step is right when the action is, and (for a click, select or swipe) the point is within
0.14 of the target on each axis."""
ok_a = pred["action"] == ex["action"]
ok_p = True
if ex["action"] in ("click", "select", "swipe"):
ok_p = abs(pred["x"] - ex["args"]["x"]) <= thresh and abs(pred["y"] - ex["args"]["y"]) <= thresh
return dict(action=float(ok_a), point=float(ok_p) if ex["action"] in ("click", "select", "swipe") else None,
step=float(ok_a and ok_p))
def examples(self, n_agent, n_click, n_val):
"""(train, val): AGUVIS steps + Wave-UI clicks; val is the first n_val steps of the same pool
(held out from this run; use aguvis.test_steps() to compare runs)."""
pool = aguvis.steps(n_agent + n_val)
val, train = pool[:n_val], pool[n_val:]
return train + (waveui.clicks(n_click) if n_click else []), val
class BoxTask(Task):
"""An element's or object's box as centre + size; sizes log-warped."""
name = "box"
numbers = {"cx": "lin", "cy": "lin", "w": "log", "h": "log"}
def layout(self, arm):
return slots_for("cx", arm) + slots_for("cy", arm) + slots_for("w", arm) + slots_for("h", arm)
def system(self, arm):
return ("You locate an element or object in an image and give its bounding box as a centre and a size. "
f"For cx, {coarse_desc(arm, 'the horizontal position of the centre, from the left edge to the right edge')}; "
"for cy, the same vertically from the top to the bottom. "
f"For w, {coarse_desc(arm, 'the box width on a scale from tiny to the whole image')}; "
"for h, the same for the height.")
def user(self, ex):
return f"Element: {ex['instruction']}"
def norm_box(self, ex):
W, H = image_size(ex)
x1, y1, x2, y2 = ex["box"]
return [x1 / W, y1 / H, x2 / W, y2 / H] if not ex.get("norm") else list(ex["box"])
def targets(self, ex, arm):
x1, y1, x2, y2 = self.norm_box(ex)
w, h = max(x2 - x1, LOG_MIN), max(y2 - y1, LOG_MIN)
tg = {}
tg.update(number_targets("cx", (x1 + x2) / 2, 0.12 * w, arm))
tg.update(number_targets("cy", (y1 + y2) / 2, 0.12 * h, arm))
tg.update(number_targets("w", warp(w, "log"), 0.02, arm))
tg.update(number_targets("h", warp(h, "log"), 0.02, arm))
return tg
def decode(self, lp, arm, how="default"):
cx, cy = read_number(lp, "cx", arm, how), read_number(lp, "cy", arm, how)
w, h = unwarp(read_number(lp, "w", arm, how), "log"), unwarp(read_number(lp, "h", arm, how), "log")
return [max(0.0, cx - w / 2), max(0.0, cy - h / 2), min(1.0, cx + w / 2), min(1.0, cy + h / 2)]
def score(self, pred, ex):
b = self.norm_box(ex)
i = box_iou(pred, b)
cx, cy = (pred[0] + pred[2]) / 2, (pred[1] + pred[3]) / 2
return dict(iou=i, iou50=float(i >= 0.5), iou75=float(i >= 0.75),
center_in=float(b[0] <= cx <= b[2] and b[1] <= cy <= b[3]))
def examples(self, n_train, n_val, n_photo=0, seed=0):
"""(train, val): Wave-UI element boxes, plus n_photo RefCOCO referent boxes (general objects).
val: Wave-UI validation boxes, and val_photo: RefCOCO val referents (None without photos)."""
train = waveui.boxes(n_train)
val = waveui.val_boxes(n_val)
val_photo = None
if n_photo:
train += refcoco.photo_boxes(n_photo, "train", seed + 100)
val_photo = refcoco.photo_boxes(n_val, "validation", seed + 100)
return train, val, val_photo
class SegTask(Task):
"""A referred object's mask as a centre + K ray lengths; rays are fractions of the image diagonal,
log-warped."""
name = "seg"
def __init__(self, K=24):
self.K = K
self.numbers = {"cx": "lin", "cy": "lin", **{f"r{k}": "log" for k in range(K)}}
def layout(self, arm):
out = slots_for("cx", arm) + slots_for("cy", arm)
for k in range(self.K):
out += slots_for(f"r{k}", arm)
return out
def system(self, arm):
K = self.K
return ("You outline an object in an image. cx and cy: a point well inside the object; "
f"r0 to r{K - 1}: how far the object reaches from that point along {K} directions, r0 pointing right "
f"and the directions turning clockwise in equal steps. For cx, "
f"{coarse_desc(arm, 'the horizontal position of the point, from the left edge to the right edge')}; "
"for cy, the same vertically from the top to the bottom. For each r, "
f"{coarse_desc(arm, 'the distance on a scale from tiny to the whole image diagonal')}.")
def user(self, ex):
return f"Object: {ex['sentence']}"
def encoded(self, ex):
"""(cx, cy, d) in pixels, computed once per example."""
if "radial" not in ex:
W, H = ex["size"]
ex["radial"] = radial_encode(poly_mask(ex["polys"], W, H), self.K)
return ex["radial"]
def targets(self, ex, arm):
W, H = ex["size"]
cx, cy, d = self.encoded(ex)
bx1, by1, bx2, by2 = ex["bbox"]
diag = math.hypot(W, H)
tg = {}
tg.update(number_targets("cx", cx / W, 0.12 * (bx2 - bx1) / W, arm))
tg.update(number_targets("cy", cy / H, 0.12 * (by2 - by1) / H, arm))
for k in range(self.K):
tg.update(number_targets(f"r{k}", warp(d[k] / diag, "log"), 0.03, arm))
return tg
def decode(self, lp, arm, how="default"):
cx, cy = read_number(lp, "cx", arm, how), read_number(lp, "cy", arm, how)
r = [unwarp(read_number(lp, f"r{k}", arm, how), "log") for k in range(self.K)]
return dict(cx=cx, cy=cy, r=r)
def score(self, pred, ex):
W, H = ex["size"]
diag = math.hypot(W, H)
m, pts = radial_decode(pred["cx"] * W, pred["cy"] * H, [r * diag for r in pred["r"]], W, H)
gt = poly_mask(ex["polys"], W, H)
iou, inter, union = mask_iou(m, gt)
xs, ys = [p[0] for p in pts], [p[1] for p in pts]
pb = [max(0, min(xs)), max(0, min(ys)), min(W, max(xs)), min(H, max(ys))]
rec = box_iou([pb[0] / W, pb[1] / H, pb[2] / W, pb[3] / H],
[ex["bbox"][0] / W, ex["bbox"][1] / H, ex["bbox"][2] / W, ex["bbox"][3] / H])
return dict(iou=float(iou), inter=inter, union=union, rec50=float(rec >= 0.5))
def examples(self, n_train, n_val, seed=0):
train = refcoco.examples(refcoco.rows("train"), n_train, seed)
val = refcoco.examples(refcoco.rows("validation"), n_val, seed + 1, one_sentence=False)
return train, val
TASKS = {"agent": AgentTask, "box": BoxTask, "seg": SegTask}
|