sparklabuofu's picture
Update the demo
22de727 verified
Raw History Blame Contribute Delete
6.27 kB
"""Train/test SITE split — decided once, before anything is generated, and persisted
(data/datagen/site_split.json, tracked). Every later stage only touches train sites.
Greedy, deterministic (seeded) set cover over a test budget (~20% of sites):
* every slice/target macro gets >= `min_human_test` test sites where humans
recorded it (so transfer is measured on real annotated tasks there);
* every macro supported on >= 4 sites (data/macro_locations.yaml) gets >= 1 test site;
* no pick may leave a macro with fewer than max(3, 60%) of its sites in train.
Leftover budget is filled with sites that have human tasks (evaluable), seeded order.
"""
from __future__ import annotations
from helpers.paths import SITES_DIR
import hashlib
import json
import math
import time
from collections import defaultdict
from miniwebgen import config
def all_sites():
return sorted(p.parent.name for p in SITES_DIR.glob("*/site.json"))
def support_map():
"""{macro: set(site)} from data/macro_locations.yaml (canonical keys)."""
from miniweb.annotation.macro_locations import MACRO_LOCATIONS
from miniweb.annotation.macros import canon
out = defaultdict(set)
for site, macros in MACRO_LOCATIONS.items():
for m in macros or {}:
out[canon(m)].add(site)
return out
def human_map():
"""{macro: set(site)} — sites where a human recording performs the macro. The site is
read from the URLs of the span's actions (multi-site tasks name several sites)."""
import re
from miniweb.annotation.macro_browser import index, task_detail
from miniweb.annotation.macros import canon
out = defaultdict(set)
details = {}
for macro, occs in index()["macros"].items():
for o in occs:
if not o.get("span"):
continue
if o["key"] not in details:
a, t = o["key"].split("/", 1)
details[o["key"]] = task_detail(a, t) or {"actions": []}
lo, hi = o["span"]
for act in details[o["key"]]["actions"][lo - 1:hi]:
m = re.search(r"/sites/([^/?#]+)", act.get("url") or "")
if m:
out[canon(macro)].add(m.group(1))
return out
def _h(seed, s):
return hashlib.sha256(f"{seed}:{s}".encode()).hexdigest()
def build_split(seed=7, test_fraction=0.2, target_macros=config.SLICE_MACROS, min_human_test=3,
sites=None, support=None, human=None):
sites = sorted(sites or all_sites())
support = {m: set(s) & set(sites) for m, s in (support if support is not None else support_map()).items()}
human = {m: set(s) & set(sites) for m, s in (human if human is not None else human_map()).items()}
budget = max(1, round(test_fraction * len(sites)))
need = {} # (kind, macro) -> (required count, candidate sites)
for m in target_macros:
need[("human", m)] = (min_human_test, human.get(m, set()) & support.get(m, set()) or human.get(m, set()))
for m, s in support.items():
if len(s) >= 4:
need[("support", m)] = (1, s)
def train_floor(m):
n = len(support.get(m, ()))
return max(3, math.ceil(0.6 * n)) if n else 0
test = []
def ok(site):
for m, s in support.items():
if site in s and len(s - set(test) - {site}) < min(train_floor(m), len(s)):
return False
return True
def deficit(key):
req, cands = need[key]
return max(0, req - len(set(test) & cands))
order = sorted(sites, key=lambda s: _h(seed, s))
while len(test) < budget:
best, best_score = None, 0.0
for s in order:
if s in test or not ok(s):
continue
score = sum((3.0 if k[0] == "human" else 1.0) for k in need if deficit(k) and s in need[k][1])
if score > best_score:
best, best_score = s, score
if best is None:
break
test.append(best)
humans_any = set().union(*human.values()) if human else set()
for s in sorted(order, key=lambda s: (s not in humans_any, _h(seed, s))):
if len(test) >= budget:
break
if s not in test and ok(s):
test.append(s)
train = [s for s in sites if s not in test]
coverage = {}
for m in sorted(set(support) | set(human)):
coverage[m] = {"train_sites": len(support.get(m, set()) - set(test)),
"test_sites": len(support.get(m, set()) & set(test)),
"test_sites_with_human_tasks": len(human.get(m, set()) & set(test))}
unmet = {f"{k[0]}:{k[1]}": deficit(k) for k in need if deficit(k)}
return {"version": 1, "created": time.strftime("%Y-%m-%d"), "seed": seed, "test_fraction": test_fraction,
"method": __doc__.strip().splitlines()[0], "target_macros": list(target_macros),
"min_human_test": min_human_test, "train": train, "test": sorted(test),
"target_detail": {m: {"train_sites": sorted(support.get(m, set()) - set(test)),
"test_sites": sorted(support.get(m, set()) & set(test)),
"test_sites_with_human_tasks": sorted(human.get(m, set()) & set(test))}
for m in target_macros},
"unmet_requirements": unmet, "coverage": coverage}
def load_split():
if not config.SPLIT_PATH.exists():
raise FileNotFoundError(f"{config.SPLIT_PATH} missing — run `python -m miniwebgen split` first")
return json.loads(config.SPLIT_PATH.read_text())
def ensure_split(force=False, **kw):
"""Load the persisted split; build it only when missing (or --force)."""
if config.SPLIT_PATH.exists() and not force:
return load_split()
split = build_split(**kw)
config.SPLIT_PATH.parent.mkdir(parents=True, exist_ok=True)
config.SPLIT_PATH.write_text(json.dumps(split, indent=1))
return split
def train_sites():
return list(load_split()["train"])
def assert_train(site):
"""Guard used by every stage that touches a site."""
if site in set(load_split()["test"]):
raise ValueError(f"{site} is a TEST site — the pipeline only touches train sites")