| import os |
| """Idea B — TriClock-Real: synthetic change events composited onto REAL cam0 |
| footage. Infinite exact labels + real nuisance statistics (the two things the |
| transfer autopsy said were missing, each patched by the other's data source). |
| |
| Episode: query time t in the pre-gold window; memory slots at log wall-clock |
| ages use the REAL frames at those times (so slots contain real person motion, |
| lighting drift, real scene changes = nuisance). Three synthetic objects are |
| drawn on top; each may change once, age log-uniform in [1s, 24h]: |
| lamp: color toggle agent: position swap |
| furniture: appears/disappears (25% never present) |
| Label per object: none / fast(<=60s) / med(<=1h) / slow(<=24h). |
| The model must date the synthetic changes while IGNORING everything real. |
| |
| python3 -m simreal.train_composite --steps 6000 # pretrain + heldout eval |
| python3 -m simreal.train_composite --steps 6000 --finetune-real # + realdata finetune/gold |
| """ |
| import argparse, bisect, json, os, time |
|
|
| import numpy as np |
| import torch |
| import torch.nn.functional as F |
|
|
| from triclock.model import LogLensNet |
|
|
| class ViTEncoder(torch.nn.Module): |
| """~8px patches -> linear embed -> 2-layer transformer -> CLS. Nano ViT.""" |
| def __init__(self, d=64): |
| super().__init__() |
| self.proj = torch.nn.Linear(8 * 8 * 3, d) |
| self.cls = torch.nn.Parameter(torch.zeros(1, 1, d)) |
| self.pos = torch.nn.Parameter(torch.zeros(1, 65, d)) |
| layer = torch.nn.TransformerEncoderLayer(d, 4, d * 4, batch_first=True, |
| dropout=0.0, norm_first=True) |
| self.tr = torch.nn.TransformerEncoder(layer, 2) |
| def forward(self, x): |
| N = x.shape[0] |
| p = x.unfold(2, 8, 8).unfold(3, 8, 8) |
| p = p.permute(0, 2, 3, 1, 4, 5).reshape(N, 64, -1) |
| z = self.proj(p) + self.pos[:, 1:] |
| z = torch.cat([self.cls.expand(N, -1, -1) + self.pos[:, :1], z], 1) |
| return self.tr(z)[:, 0] |
|
|
|
|
| HORIZON_S = 86_400 |
| FAST_S, MED_S = 60, 3_600 |
| BUCKETS = ["none", "fast", "med", "slow"] |
|
|
|
|
| def log_ages(K=12, T=HORIZON_S): |
| b = T ** (1.0 / (K - 1)) |
| return sorted({max(1, int(round(b ** i))) for i in range(K)}) |
|
|
|
|
| def bucket(age_s): |
| if age_s is None or age_s > HORIZON_S: |
| return 0 |
| return 1 if age_s <= FAST_S else (2 if age_s <= MED_S else 3) |
|
|
|
|
| class RealBank: |
| def __init__(self, d="simreal", bank="simreal/frames64.npy"): |
| self.f = np.load(bank, mmap_mode="r") |
| self.ts = np.load(bank.replace("frames64", "ts64")) |
|
|
| def at(self, t_ms): |
| i = bisect.bisect_left(self.ts, t_ms) |
| i = min(max(i, 0), len(self.ts) - 1) |
| return np.asarray(self.f[i], dtype=np.float32) / 255.0 |
|
|
|
|
| def draw(frame, kind, pos, state, color, s=6, contrast=1.0): |
| x, y = pos |
| if contrast < 1.0: |
| import numpy as _np |
| def blend(region, col): |
| return col * contrast + region.mean(axis=(0, 1)) * (1 - contrast) |
| else: |
| blend = None |
| if kind == "lamp": |
| c_ = (color if state == 0 else 1.0 - color) |
| frame[y:y + s, x:x + s] = (blend(frame[y:y + s, x:x + s], c_) if blend else c_) |
| elif kind == "agent": |
| x2, y2 = (x + 22) % 52, (y + 17) % 52 |
| if state == 0: |
| frame[y:y + s - 1, x:x + s - 1] = (blend(frame[y:y + s - 1, x:x + s - 1], color) if blend else color) |
| else: |
| frame[y2:y2 + s - 1, x2:x2 + s - 1] = (blend(frame[y2:y2 + s - 1, x2:x2 + s - 1], color) if blend else color) |
| elif kind == "furniture": |
| if state == 0: |
| frame[y:y + s - 1, x:x + s + 2] = (blend(frame[y:y + s - 1, x:x + s + 2], color) if blend else color) |
| return frame |
|
|
|
|
| def make_episode(bank, t_ms, ages_s, rng, static_bg=False, black_bg=False, objscale=6, contrast=1.0, noise_bg="off", noise_family=0): |
| objs = [] |
| labels = [] |
| for kind in ("lamp", "agent", "furniture"): |
| pos = (int(rng.integers(2, 52)), int(rng.integers(2, 52))) |
| color = rng.uniform(0.15, 0.95, size=3).astype(np.float32) |
| if kind == "furniture" and rng.random() < 0.25: |
| objs.append((kind, pos, None, color)); labels.append(0); continue |
| if rng.random() < 0.2: |
| objs.append((kind, pos, np.inf, color)); labels.append(0); continue |
| age = float(np.exp(rng.uniform(0.0, np.log(HORIZON_S)))) |
| objs.append((kind, pos, age, color)); labels.append(bucket(age)) |
| frames = [] |
| import numpy as _np |
| bg0 = bank.at(int(t_ms)) if static_bg else None |
| for a in [0.0] + list(ages_s): |
| if noise_bg == "static": |
| sd = int(t_ms) % noise_family if noise_family else int(t_ms) % (2**31) |
| fr = _np.clip(_np.random.default_rng(sd).uniform(0, 1, (64, 64, 3)), 0, 1).astype(_np.float32) |
| if os.environ.get("NOISE_BLUR"): |
| import cv2 as _cv2 |
| k = int(os.environ["NOISE_BLUR"]) |
| fr = _cv2.GaussianBlur(fr, (k, k), 0) |
| elif noise_bg == "moving": |
| fr = _np.clip(_np.random.default_rng((int(t_ms) + int(a)) % (2**31)).uniform(0, 1, (64, 64, 3)), 0, 1).astype(_np.float32) |
| elif black_bg: |
| fr = _np.zeros((64, 64, 3), _np.float32) |
| else: |
| fr = (bg0.copy() if static_bg else bank.at(int(t_ms - a * 1000)).copy()) |
| for (kind, pos, chg, color) in objs: |
| if chg is None: |
| if kind != "furniture": |
| fr = draw(fr, kind, pos, 1, color, objscale, contrast) |
| continue |
| state = 0 if a < chg else 1 |
| |
| fr = draw(fr, kind, pos, state, color, objscale, contrast) |
| frames.append(fr) |
| return np.stack(frames), np.array(labels, dtype=np.int64) |
|
|
|
|
| def make_batch(bank, lo, hi, ages, rng, n, static_bg=False, black_bg=False, objscale=6, contrast=1.0, noise_bg="off", noise_family=0): |
| ts = rng.integers(lo, hi, size=n) |
| fs, ys, ag = [], [], [] |
| for t in ts: |
| aj = [max(1.0, a * float(rng.uniform(0.85, 1.15))) for a in ages] |
| f, y = make_episode(bank, int(t), aj, rng, static_bg, black_bg, objscale, contrast, noise_bg, noise_family) |
| fs.append(f); ys.append(y); ag.append(np.array([0.0] + aj, dtype=np.float32)) |
| return np.stack(fs), np.stack(ag), np.stack(ys) |
|
|
|
|
| def to_torch(f, a, y, dev): |
| f = torch.from_numpy(f).permute(0, 1, 4, 2, 3).contiguous().to(dev) |
| return f, torch.from_numpy(a).to(dev), torch.from_numpy(y).to(dev) |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--steps", type=int, default=6000) |
| ap.add_argument("--batch", type=int, default=32) |
| ap.add_argument("--budget", type=int, default=12) |
| ap.add_argument("--seed", type=int, default=0) |
| ap.add_argument("--eval-n", type=int, default=400) |
| ap.add_argument("--finetune-real", action="store_true") |
| ap.add_argument("--static-bg", action="store_true") |
| ap.add_argument("--black-bg", action="store_true") |
| ap.add_argument("--objscale", type=int, default=6) |
| ap.add_argument("--contrast", type=float, default=1.0) |
| ap.add_argument("--bank", default="simreal/frames64.npy") |
| ap.add_argument("--noise-bg", choices=["off","static","moving"], default="off") |
| ap.add_argument("--lr", type=float, default=1e-3) |
| ap.add_argument("--encoder", choices=["cnn","vit"], default="cnn") |
| ap.add_argument("--anneal-banks", default="", help="comma list of banks; switch by step fraction over first 75%") |
| ap.add_argument("--anneal-contrast-from", type=float, default=0, help="start contrast, anneal to --contrast by 75% of steps") |
| ap.add_argument("--anneal-from", type=int, default=0, help="start objscale, anneal to --objscale by 75% of steps") |
| ap.add_argument("--noise-family", type=int, default=0, help="draw static noise from K fixed textures (0=fresh per episode)") |
| ap.add_argument("--out", default="results_simreal") |
| args = ap.parse_args() |
|
|
| dev = "cuda" if torch.cuda.is_available() else "cpu" |
| torch.manual_seed(args.seed) |
| bank = RealBank(bank=args.bank) |
| banks_sched = [RealBank(bank=b) for b in args.anneal_banks.split(",")] if args.anneal_banks else None |
| ages = log_ages(args.budget) |
| span = bank.ts[-1] - bank.ts[0] |
| gold_after = bank.ts[-1] - int(span * 0.25) |
| lo = bank.ts[0] + HORIZON_S * 1000 |
| net = LogLensNet().to(dev) |
| if args.encoder == "vit": |
| net.enc = ViTEncoder(64).to(dev) |
| opt = torch.optim.AdamW(net.parameters(), lr=args.lr) |
| rng = np.random.default_rng(args.seed) |
| t0 = time.time() |
| for step in range(args.steps): |
| if banks_sched: |
| frac = min(1.0, step / (0.75 * args.steps)) |
| bank = banks_sched[min(len(banks_sched) - 1, int(frac * len(banks_sched)))] |
| if args.anneal_from: |
| frac = min(1.0, step / (0.75 * args.steps)) |
| cur = int(round(args.anneal_from + (args.objscale - args.anneal_from) * frac)) |
| else: |
| cur = args.objscale |
| if args.anneal_contrast_from: |
| frac = min(1.0, step / (0.75 * args.steps)) |
| cur_ct = args.anneal_contrast_from + (args.contrast - args.anneal_contrast_from) * frac |
| else: |
| cur_ct = args.contrast |
| f, a, y = to_torch(*make_batch(bank, lo, gold_after, ages, rng, args.batch, args.static_bg, args.black_bg, cur, cur_ct, args.noise_bg, args.noise_family), dev) |
| loss = F.cross_entropy(net(f, a).flatten(0, 1), y.flatten()) |
| opt.zero_grad(); loss.backward(); opt.step() |
| if (step + 1) % 250 == 0: |
| print(f"[simreal] step {step+1}/{args.steps} loss {loss.item():.4f} " |
| f"({time.time()-t0:.0f}s)", flush=True) |
|
|
| |
| |
| net.eval() |
| erng = np.random.default_rng(777) |
| hit = np.zeros(4); tot = np.zeros(4) |
| with torch.no_grad(): |
| for _ in range(args.eval_n // args.batch): |
| f, a, y = to_torch(*make_batch(bank, gold_after, bank.ts[-1], ages, erng, |
| args.batch, args.static_bg, args.black_bg, args.objscale, args.contrast), dev) |
| pred = net(f, a).argmax(-1).cpu().numpy() |
| yy = y.cpu().numpy() |
| for b in range(pred.shape[0]): |
| for o in range(3): |
| tot[yy[b, o]] += 1; hit[yy[b, o]] += pred[b, o] == yy[b, o] |
| acc = {b: round(float(h / t), 4) for b, h, t in zip(BUCKETS, hit, tot) if t} |
| mean = round(float(np.mean([h / t for h, t in zip(hit, tot) if t])), 4) |
| res = {"heldout_composited_acc": acc, "heldout_mean": mean, |
| "steps": args.steps, "seed": args.seed, "device": dev} |
| os.makedirs(args.out, exist_ok=True) |
| torch.save(net.state_dict(), os.path.join(args.out, f"simreal{'_black' if args.black_bg else ('_static' if args.static_bg else '')}_o{args.objscale}_ct{args.contrast:g}_{args.noise_bg}{args.noise_family}_{os.path.basename(args.bank)[:9]}_{args.encoder}_an{args.anneal_from}c{args.anneal_contrast_from:g}{'B' if args.anneal_banks else ''}_s{args.seed}.pt")) |
| json.dump(res, open(os.path.join(args.out, f"simreal{'_black' if args.black_bg else ('_static' if args.static_bg else '')}_o{args.objscale}_ct{args.contrast:g}_{args.noise_bg}{args.noise_family}_{os.path.basename(args.bank)[:9]}_{args.encoder}_an{args.anneal_from}c{args.anneal_contrast_from:g}{'B' if args.anneal_banks else ''}_s{args.seed}.json"), "w"), indent=2) |
| print(json.dumps(res, indent=2), flush=True) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|