AsianPlayer/TriDrive / code /train /train_future_state.py
AsianPlayer's picture
download
raw
29.5 kB
"""cwm3/train/train_future_state.py — F2′ trainer (R2 WP4 batch 1, 2026-09-16; pre-registration docs/decisions.md 2026-09-16T06:46:35Z): the detached
future-state head (models/future_state_head.py) on the FROZEN FINAL joint model's driver state.
python train/train_future_state.py --seed 0 [--final-run I1_all_minus_road2drv_s0_final] [--name dwell] [--feats dwell|dwell,face] [--epochs 20]
[--extract-only] [--max-anchors N --cpu (smoke, never a result)]
-> results/runs/FS_<name>_s<k>/ ckpt_best.pt config.json history.jsonl feat_stats.npz TRAIN_DONE fs_{regime}_{split}.npz (train / val / test) PRED_DONE run_*.json
Step 1 (cache, results/runs/FS_cache/): zU_<final_run>_{regime}_{split}.npz = h_u f16 [N,128] of the frozen joint model (its fine-tuned U trunk; the FINAL variant
all_minus_road2drv has no interaction INTO the driver branch, so h_u = GRU(Kx) — recomputed here from the U stream under the pack's bf16 autocast and
verified equal to the saved prediction pack within one f16 ulp, 2026-09-16) + the kin / gaze target arrays (S, S0, EM, M, Sg, Sg0, EMg, row_ok, uid, t, dongle,
mode); feats_{regime}_{split}.npz = the label-free dwell features FEATURES (below) of the 8 s history. No gradient ever reaches the joint model.
Step 2 (train): TRAIN anchors of both regimes; AdamW 1e-3 / wd 1e-4, batch 4096, <= --epochs, early stop patience 3 on the VALIDATION occupancy BCE
(parts["occ_bce"], the selection objective); seeds 0 / 1 / 2 each on its own FINAL seed's state.
Step 3 (predict): every split -> fs_{regime}_{split}.npz, row-aligned with pred_layout_{regime}_{split}.npz of the FINAL run (uid / t witness stored):
keys uid t dongle valid[N] (row has a causal box = the FINAL layout's valid_U) state_names[S] off_value[S] z0[N,S] z0valid[N,S]
pi[N,25,S] f16 (P(state = 1 at t + k dt)), pi_off[N,25,S] f16 (off-task occupancy), h_on / h_off[N,25,S] f16, ED_off_s[N,S] f32 (dt * sum_k pi_off),
first_leave_p[N,S,26] f16 (law over the first step differing from z0; bin 25 = beyond 5 s), first_leave_E_s[N,S] (E[min(T, 5.2 s)]),
rem_dwell_p / rem_dwell_E_s (the same law trained on off-task anchors = remaining dwell / recovery time), leave_within_{1,2,3,5}s[N,S],
rem_within_{1,2,3,5}s[N,S], dt, horizon, meta (json: model_id, code sha, inputs digest, feature names, final run, base rates).
FEATURES (dwell set; every quantity is causal, from t-7.9..t):
dw_* (11, train/warning_features_ext.dwell_features: hands_off_share_{1,3,8}s, hands_off_dwell_s, head_away_share_{1,3,8}s, head_away_dwell_s,
head_dev_max_3s, n_hand_leave_8s, hist_valid_share)
hands_state_dwell_s / head_state_dwell_s time in the CURRENT hands / head state (run length of the forward-filled state at the window end, cap 8 s)
n_head_switch_8s head-away switches (either direction) in the window
t_since_any_switch_s time since the last switch of either kin state (cap 8 s)
gz_z0 / gz_dwell0_s the current gaze_off state and its dwell (results/targets_gaze_causal_routes_v2 S0_g / dwell0; own landmark chain)
face_valid_share_8s observation confidence of the face stream (Fh8valid share)
miss_kin / miss_gaze missing flags (no valid kin step in the window / undefined gaze anchor); NaN features -> 0 after standardisation
(+ `face`: the 16 fc_* channels of warning_features_ext.face_block — optional control arm, not the default)
Attribution arms (2026-09-16, same recipe): `--name zonly --feats none` (z_U + mode only) and `--name featonly --no-z` (dwell features + mode only) separate the
information the head adds (8 s dwell / switch history, gaze anchor state) from the head + occupancy-BCE training on the trunk's own information.
Nothing here reads an openpilot DMS field.
"""
from __future__ import annotations
import os
os.environ.setdefault("OMP_NUM_THREADS", "4")
os.environ.setdefault("MKL_NUM_THREADS", "4")
import argparse # noqa: E402
import json # noqa: E402
import sys # noqa: E402
import time # noqa: E402
from pathlib import Path # noqa: E402
import numpy as np # noqa: E402
CWM3 = Path(__file__).resolve().parents[1]
EXPLORE = CWM3.parent
for p in (CWM3, CWM3 / "data", CWM3 / "common", CWM3 / "train", CWM3 / "eval", EXPLORE / "s12_wm"):
if str(p) not in sys.path:
sys.path.insert(0, str(p))
import cwm3_dataset as ds # noqa: E402
import batch as bt # noqa: E402
import warning_features_ext as wfe # noqa: E402
from runmeta import write_run_json, code_hash # noqa: E402
RUNS = CWM3 / "results" / "runs"
CACHE = RUNS / "FS_cache"
REGIMES = ("manual", "engaged")
SPLITS = ("train", "val", "test")
EXTRA_NAMES = ["hands_state_dwell_s", "head_state_dwell_s", "n_head_switch_8s", "t_since_any_switch_s", "gz_z0", "gz_dwell0_s", "face_valid_share_8s", "miss_kin", "miss_gaze"]
FEATURES = list(wfe.DWELL_NAMES) + EXTRA_NAMES
CHUNK = 100_000
FINAL_RUN = "I1_all_minus_road2drv_s{seed}_final"
CODE_FILES = [CWM3 / "train" / "train_future_state.py", CWM3 / "models" / "future_state_head.py", CWM3 / "models" / "heads.py", CWM3 / "train" / "batch.py",
CWM3 / "train" / "warning_features_ext.py", CWM3 / "models" / "cwm3.py", CWM3 / "models" / "branches.py"]
def log(msg, out=None):
line = f"[{time.strftime('%H:%M:%S')}] {msg}"
print(line, flush=True)
if out:
with open(Path(out) / "train.log", "a") as f:
f.write(line + "\n")
# ----------------------------------------------------------------------------- features (CPU, numpy; history only)
def _run_length_end(state, valid, cap=wfe.DWELL_CAP, hz=wfe.HZ):
"""state [n,T] in {0,1} (forward-filled), valid [n,T] -> seconds the CURRENT (last) state has lasted within the window (cap; NaN when no valid step)."""
n, T = state.shape
last = state[:, -1:]
same = (state == last)
# first index from the end where the state differs -> run starts after it
diff_idx = np.where(~same, np.arange(T)[None, :], -1).max(1)
run = (T - 1 - diff_idx) / hz
run = np.minimum(run, cap).astype(np.float32)
run[valid.sum(1) == 0] = np.nan
return run
def extra_features(Kh8, Kh8valid, head_on=None, head_off=None):
"""[n,80,11] history -> the 4 kin extra features (hands_state_dwell_s, head_state_dwell_s, n_head_switch_8s, t_since_any_switch_s)."""
if head_on is None or head_off is None:
head_on, head_off = wfe.head_thresholds()
K = np.asarray(Kh8, np.float32)
V = np.asarray(Kh8valid, bool) & np.isfinite(K[:, :, wfe.IDX_MIN_DIST]) & np.isfinite(K[:, :, wfe.IDX_HEAD_DEV])
hands_on = V & (K[:, :, wfe.IDX_MIN_DIST] < wfe.WHEEL_TH)
dev = np.where(V, np.abs(K[:, :, wfe.IDX_HEAD_DEV]), np.nan)
away = wfe.hysteresis_rows(dev, head_on, head_off)
away_v = np.isfinite(away)
hs = wfe._ffill_state(hands_on.astype(np.float32), V)
hd_ = wfe._ffill_state(np.where(away_v, away, 0.0).astype(np.float32), away_v)
hs0, hd0 = np.nan_to_num(hs, nan=0.0), np.nan_to_num(hd_, nan=0.0)
hands_rl = _run_length_end(hs0, V)
head_rl = _run_length_end(hd0, away_v)
sw_head = ((hd0[:, 1:] != hd0[:, :-1]) & away_v[:, 1:] & np.isfinite(hd_[:, :-1])).astype(np.float32)
sw_hand = ((hs0[:, 1:] != hs0[:, :-1]) & V[:, 1:] & np.isfinite(hs[:, :-1])).astype(np.float32)
n_head = sw_head.sum(1)
n_head[away_v.sum(1) == 0] = np.nan
any_sw = (sw_head + sw_hand) > 0
T = K.shape[1]
idx = np.where(any_sw, np.arange(1, T)[None, :], -1).max(1)
t_since = np.where(idx >= 0, (T - 1 - idx) / wfe.HZ, wfe.DWELL_CAP).astype(np.float32)
t_since[(V.sum(1) == 0) & (away_v.sum(1) == 0)] = np.nan
return np.stack([hands_rl, head_rl, n_head, t_since], 1).astype(np.float32)
def build_features(regime, split, rows=None, with_face=False):
"""-> X float32 [n, D], names. Reads kinhist Kh8_c / Kh8valid (chunked), the gaze anchor state / dwell and the face validity share; `rows` = subset (smoke)."""
ds.set_kin_source("causal_routes", "v2")
kin_dir = ds.kin_dir()
head_on, head_off = wfe.head_thresholds()
with np.load(kin_dir / f"kinhist_causal_{regime}_{split}.npz", allow_pickle=True) as z:
N = len(z["t"])
rows = np.arange(N, dtype=np.int64) if rows is None else np.asarray(rows, np.int64)
row_ok = z["row_ok"][rows].astype(bool) if "row_ok" in z.files else np.ones(len(rows), bool)
parts = []
for i in range(0, len(rows), CHUNK):
r = rows[i:i + CHUNK]
Kh8 = z["Kh8_c"][r]
V = z["Kh8valid"][r] & row_ok[i:i + CHUNK][:, None]
X1 = wfe.dwell_features(Kh8, V, head_on, head_off)
X2 = extra_features(Kh8, V, head_on, head_off)
parts.append(np.concatenate([X1, X2], 1))
Xk = np.concatenate(parts)
with np.load(ds.GAZE / f"gaze_{regime}_{split}.npz", allow_pickle=True) as z:
gok = z["row_ok"][rows].astype(bool)
gz0 = np.where(gok, z["S0_g"][rows].astype(np.float32), np.nan)
gdw = np.where(gok, z["dwell0"][rows].astype(np.float32), np.nan)
with np.load(ds.FACE / f"facehist_causal_{regime}_{split}.npz", allow_pickle=True) as z:
fv = np.concatenate([z["Fh8valid"][rows[i:i + CHUNK]].mean(1) for i in range(0, len(rows), CHUNK)]).astype(np.float32)
if "row_ok" in z.files:
fv = fv * z["row_ok"][rows].astype(np.float32)
miss_kin = (~np.isfinite(Xk[:, wfe.DWELL_NAMES.index("dw_hist_valid_share")]) | (Xk[:, wfe.DWELL_NAMES.index("dw_hist_valid_share")] <= 0)).astype(np.float32)
miss_gaze = (~np.isfinite(gz0)).astype(np.float32)
X = np.concatenate([Xk, gz0[:, None], np.minimum(gdw, 30.0)[:, None], fv[:, None], miss_kin[:, None], miss_gaze[:, None]], 1).astype(np.float32)
names = list(FEATURES)
if with_face:
Xf, nf = wfe.face_block(regime, split, rows)
X, names = np.concatenate([X, Xf], 1), names + nf
assert X.shape[1] == len(names), (X.shape, len(names))
return X, names
# ----------------------------------------------------------------------------- the frozen joint model's driver state
def load_final(final_run, device):
import torch
from models.cwm3 import build_joint
run = RUNS / final_run
assert (run / "TRAIN_DONE").exists() and (run / "PRED_DONE").exists(), f"{run}: not a finished joint run"
cfg = json.load(open(run / "config.json"))["cfg"]
bc = json.load(open(run / "base_ckpts.json"))
ds.set_pack_version(cfg.get("pack_version", "v2"))
ds.set_kin_source(cfg["kin_source"], cfg["kin_version"])
assert "road2drv" not in cfg["interactions"] and not cfg.get("naive"), "h_u would depend on the road stream: extend the extractor to the full joint batch"
model, info = build_joint(int(json.load(open(run / "config.json"))["args"]["seed"]), base={b: tuple(x) for b, x in cfg["base"].items()}, dirs=bc["base_dirs"],
device="cpu", stats_mode=cfg.get("stats_mode", "merged"), interactions=tuple(cfg["interactions"]), decoder=cfg.get("decoder", "direct"),
k_head=True, i_hidden=int(cfg.get("i_hidden", 640)), dropout=float(cfg.get("dropout", 0.1)), tok_source=cfg.get("tok_source", "region"))
best = (run / "BEST").read_text().strip()
if best != "base":
z = torch.load(run / best, map_location="cpu", weights_only=False)
model.load_state_dict(z["model"])
model.eval().to(device)
st = {k: v for k, v in np.load(run / "stats.npz").items()}
pc_u = {"cabin": info["base_cfgs"]["U"].get("cabin", "none"), "recipe": {"u_gaze_chain": True}}
if "kclip" in info["base_cfgs"]["U"]:
pc_u["kclip"] = float(info["base_cfgs"]["U"]["kclip"])
return model, st, pc_u, {"model_id": f"{final_run}:{best}", "base_ckpts": bc["base_ckpts"], "cfg": cfg}
def extract_zU(model, st, pc_u, regime, split, device, max_anchors=None, seed=0, amp=True):
"""-> dict: h_u f16 [N,128] + the target arrays of the (regime, split) pack (kin_row_ok, S, S0, EM, M, Sg, Sg0, EMg, mode, uid, t, dongle, idx)."""
import torch
from models.cwm3 import branch_stats
raw = ds.load_split(regime, split, need={"X", "Kh", "kin", "Y", "gaze"}, max_anchors=max_anchors, seed=seed, verbose=lambda m: log(m))
N = len(raw["t"])
store = bt.GpuStore(raw, bt.KEYS["U"], device)
stu = branch_stats(st, "U")
H = np.zeros((N, model.U.gru.hidden_size), np.float16)
bs = 8192
with torch.no_grad():
for i in range(0, N, bs):
idx = torch.arange(i, min(i + bs, N), device=device)
bU, _ = bt.prep_U(store, idx, stu, pc_u, need_targets=False)
with torch.autocast("cuda", dtype=torch.bfloat16, enabled=(amp and device == "cuda")):
h_u, _ = model.trunk_U(bU)
H[i:i + len(idx)] = h_u.float().cpu().numpy().astype(np.float16)
out = {"h_u": H, "S": raw["S"], "S0": raw["S0"].astype(np.float32), "EM": raw["EM"], "M": raw["M"], "Sg": raw["Sg"], "Sg0": raw["Sg0"].astype(np.float32), "EMg": raw["EMg"],
"kin_row_ok": raw["kin_row_ok"], "mode": raw["mode"].astype(np.int8), "uid": raw["uid"], "t": raw["t"], "dongle": raw["dongle"], "idx": raw["idx"]}
del store, raw
return out
def cache_paths(final_run, regime, split):
return CACHE / f"zU_{final_run}_{regime}_{split}.npz", CACHE / f"feats_{regime}_{split}.npz"
def ensure_cache(final_run, regime, split, device, ctx, max_anchors=None, seed=0, with_face=False):
"""builds (once) and returns (zU dict, feats X, names) for one (regime, split); smoke subsets (max_anchors) are never cached."""
pz, pf = cache_paths(final_run, regime, split)
if max_anchors:
pz, pf = None, None
if pz is not None and pz.exists():
with np.load(pz, allow_pickle=True) as z:
zu = {k: z[k] for k in z.files}
else:
if ctx["model"] is None:
ctx["model"], ctx["st"], ctx["pc_u"], ctx["meta"] = load_final(final_run, device)
t0 = time.time()
zu = extract_zU(ctx["model"], ctx["st"], ctx["pc_u"], regime, split, device, max_anchors, seed)
log(f"z_U {regime}/{split}: {len(zu['t']):,} rows in {time.time() - t0:.0f}s")
if pz is not None:
CACHE.mkdir(parents=True, exist_ok=True)
np.savez(pz, **zu, model_id=np.array(ctx["meta"]["model_id"]))
if pf is not None and pf.exists():
with np.load(pf, allow_pickle=True) as z:
X, names = z["X"], [str(n) for n in z["names"]]
assert np.array_equal(z["uid"], zu["uid"]), "feature cache / z_U cache row mismatch"
else:
t0 = time.time()
X, names = build_features(regime, split, rows=zu["idx"], with_face=with_face)
log(f"features {regime}/{split}: {X.shape} in {time.time() - t0:.0f}s")
if pf is not None:
np.savez(pf, X=X, names=np.array(names), uid=zu["uid"], t=zu["t"])
if with_face and X.shape[1] == len(FEATURES): # cached dwell-only features + the face arm requested -> append the face block
Xf, nf = wfe.face_block(regime, split, zu["idx"])
X, names = np.concatenate([X, Xf], 1), names + nf
return zu, X, names
# ----------------------------------------------------------------------------- targets (train/batch.prep_U conventions, per state)
def make_targets(zu, device):
"""numpy cache arrays -> tensors: z_prev / z / m / Sfut / Sm [N,25,3], z0 / z0valid [N,3] for (hands_on, head_turned, gaze_off)."""
import torch
S = torch.from_numpy(zu["S"].astype(np.float32)).to(device) # [N,25,2] NaN censored
S0 = torch.from_numpy(zu["S0"]).to(device) # [N,2]
Sg = torch.from_numpy(zu["Sg"].astype(np.float32)).to(device)[:, :, None] # [N,25,1]
Sg0 = torch.from_numpy(zu["Sg0"]).to(device)[:, None]
M = torch.from_numpy(zu["M"]).to(device)
EM = torch.from_numpy(zu["EM"]).to(device)
EMg = torch.from_numpy(zu["EMg"]).to(device)
Sall = torch.cat([S, Sg], -1)
S0all = torch.cat([S0, Sg0], -1)
z = torch.nan_to_num(Sall)
z0 = torch.nan_to_num(S0all)
z_prev = torch.cat([z0[:, None], z[:, :-1]], 1)
Sprev = torch.cat([S0all[:, None], Sall[:, :-1]], 1)
onset = torch.stack([EM[:, :, 0], EM[:, :, 2], EMg[:, :, 0]], -1)
m = onset & M[:, :, None] & torch.isfinite(Sall) & torch.isfinite(Sprev)
Sm = torch.isfinite(Sall) & M[:, :, None]
return {"z_prev": z_prev, "z": z, "m": m, "Sfut": z, "Sm": Sm, "z0": z0, "z0valid": torch.isfinite(S0all)}
def standardise(X, stats=None):
"""nan-aware train mean / sd; NaN -> 0 after standardisation; clip +-8."""
if stats is None:
mu = np.nanmean(X, 0)
sd = np.nanstd(X, 0) + 1e-6
stats = {"mu": mu.astype(np.float32), "sd": sd.astype(np.float32)}
Z = (X - stats["mu"]) / stats["sd"]
return np.clip(np.nan_to_num(Z, nan=0.0, posinf=8.0, neginf=-8.0), -8, 8).astype(np.float32), stats
# ----------------------------------------------------------------------------- train / predict
def run_epoch(model, opt, Z, X, mode, tg, idx_all, bs, train, gen=None):
import torch
from models.future_state_head import future_state_loss
tot, n, agg = 0.0, 0, {}
order = idx_all[torch.randperm(len(idx_all), generator=gen, device=idx_all.device)] if train else idx_all
for i in range(0, len(order), bs):
idx = order[i:i + bs]
t = {k: v[idx] for k, v in tg.items()}
out = model(Z[idx].float(), X[idx], mode[idx])
loss, parts = future_state_loss(out, t)
if train:
opt.zero_grad(set_to_none=True)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step()
w = len(idx)
tot += float(loss.detach()) * w
n += w
for k, v in parts.items():
agg[k] = agg.get(k, 0.0) + float(v.detach() if torch.is_tensor(v) else v) * w
return tot / max(n, 1), {k: v / max(n, 1) for k, v in agg.items()}
def predict_split(model, zu, Xs, out_dir, regime, split, device, base_rate, meta, names, bs=16384):
import torch
from models.future_state_head import future_state_outputs, STATES, OFF_VALUE, DT, H, N_BINS, LEAVE_TAUS
N = len(zu["t"])
Z = torch.from_numpy(zu["h_u"]).to(device)
X = torch.from_numpy(Xs).to(device)
mode = torch.from_numpy(zu["mode"].astype(np.int64)).to(device)
S0 = np.concatenate([zu["S0"], zu["Sg0"][:, None]], 1).astype(np.float32)
z0valid = np.isfinite(S0)
z0 = np.where(z0valid, S0, base_rate[None, :]).astype(np.float32)
z0t = torch.from_numpy(z0).to(device)
S = len(STATES)
P = {"pi": np.zeros((N, H, S), np.float16), "pi_off": np.zeros((N, H, S), np.float16), "h_on": np.zeros((N, H, S), np.float16), "h_off": np.zeros((N, H, S), np.float16),
"ED_off_s": np.zeros((N, S), np.float32), "first_leave_p": np.zeros((N, S, N_BINS), np.float16), "rem_dwell_p": np.zeros((N, S, N_BINS), np.float16),
"first_leave_E_s": np.zeros((N, S), np.float32), "rem_dwell_E_s": np.zeros((N, S), np.float32)}
for nm in LEAVE_TAUS:
P[f"leave_within_{nm}"] = np.zeros((N, S), np.float16)
P[f"rem_within_{nm}"] = np.zeros((N, S), np.float16)
model.eval()
with torch.no_grad():
for i in range(0, N, bs):
sl = slice(i, min(i + bs, N))
out = model(Z[sl].float(), X[sl], mode[sl])
res = future_state_outputs(out, z0t[sl])
for k in P:
P[k][sl] = res[k].cpu().numpy().astype(P[k].dtype)
m = dict(meta, regime=regime, split=split, n_rows=int(N), features=names, dt=DT, horizon=H, state_names=list(STATES), off_value=[OFF_VALUE[s] for s in STATES],
base_rate_z0=base_rate.tolist(), written_utc=time.strftime("%Y%m%dT%H%M%SZ", time.gmtime()))
np.savez_compressed(Path(out_dir) / f"fs_{regime}_{split}.npz", uid=zu["uid"], t=zu["t"], dongle=zu["dongle"], valid=zu["kin_row_ok"].astype(bool), z0=z0.astype(np.float16),
z0valid=z0valid, state_names=np.array(list(STATES)), off_value=np.array([OFF_VALUE[s] for s in STATES], np.float32), dt=np.array(DT), horizon=np.array(H),
meta=np.array(json.dumps(m, default=str)), **P)
return N
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--final-run", default="", help="frozen joint run (default I1_all_minus_road2drv_s<seed>_final)")
ap.add_argument("--name", default="dwell", help="run family: results/runs/FS_<name>_s<seed>")
ap.add_argument("--feats", default="dwell", help="dwell | dwell,face | none (attribution arm: a single zero feature -> the head sees z_U + mode only)")
ap.add_argument("--no-z", action="store_true", help="attribution arm: z_U replaced by zeros (the head sees the dwell features + mode only)")
ap.add_argument("--epochs", type=int, default=20)
ap.add_argument("--patience", type=int, default=3)
ap.add_argument("--batch", type=int, default=4096)
ap.add_argument("--lr", type=float, default=1e-3)
ap.add_argument("--d", type=int, default=64)
ap.add_argument("--regimes", default="manual,engaged")
ap.add_argument("--splits", default="train,val,test", help="prediction splits")
ap.add_argument("--extract-only", action="store_true", help="build the z_U / feature caches and stop")
ap.add_argument("--max-anchors", type=int, default=0, help="smoke subset per (regime, split); never cached, never a result")
ap.add_argument("--cpu", action="store_true")
ap.add_argument("--out", default="")
args = ap.parse_args()
import torch
from models.future_state_head import FutureStateHead, n_params, STATES
torch.set_num_threads(4)
device = "cuda" if torch.cuda.is_available() and not args.cpu else "cpu"
final_run = args.final_run or FINAL_RUN.format(seed=args.seed)
out = Path(args.out) if args.out else RUNS / f"FS_{args.name}_s{args.seed}{'_smoke' if args.max_anchors else ''}"
out.mkdir(parents=True, exist_ok=True)
regimes = args.regimes.split(",")
with_face = "face" in args.feats.split(",")
t_start = time.time()
log(f"=== train_future_state seed {args.seed} final_run {final_run} feats {args.feats} out {out} device {device} ===", out)
ctx = {"model": None}
mx = args.max_anchors or None
data = {}
for split in ("train", "val", "test"):
parts = [ensure_cache(final_run, r, split, device, ctx, mx, args.seed, with_face) for r in regimes]
data[split] = parts
meta = ctx.get("meta") or {"model_id": str(np.load(cache_paths(final_run, regimes[0], "train")[0], allow_pickle=True)["model_id"]), "final_run": final_run}
meta["final_run"] = final_run
if ctx["model"] is not None:
del ctx["model"]
torch.cuda.empty_cache() if device == "cuda" else None
if args.extract_only:
log("caches built; --extract-only", out)
return
names = data["train"][0][2]
if args.feats == "none": # attribution arm: no dwell information (one constant zero column)
for split in data:
data[split] = [(zu, np.zeros((len(zu["t"]), 1), np.float32), ["none"]) for (zu, X, nm) in data[split]]
names = ["none"]
if args.no_z: # attribution arm: no trunk state
for split in data:
for zu, _, _ in data[split]:
zu["h_u"] = np.zeros_like(zu["h_u"])
# ---- train / val tensors
def stack(parts, key):
return np.concatenate([p[0][key] for p in parts])
Xtr = np.concatenate([p[1] for p in data["train"]])
Xtr_s, fstats = standardise(Xtr)
Xva_s, _ = standardise(np.concatenate([p[1] for p in data["val"]]), fstats)
np.savez(out / "feat_stats.npz", mu=fstats["mu"], sd=fstats["sd"], names=np.array(names))
tr_zu = {k: stack(data["train"], k) for k in ("h_u", "S", "S0", "EM", "M", "Sg", "Sg0", "EMg", "mode")}
va_zu = {k: stack(data["val"], k) for k in ("h_u", "S", "S0", "EM", "M", "Sg", "Sg0", "EMg", "mode")}
S0tr = np.concatenate([tr_zu["S0"], tr_zu["Sg0"][:, None]], 1)
base_rate = np.array([np.nanmean(S0tr[:, s]) for s in range(S0tr.shape[1])], np.float32)
Ztr, Zva = torch.from_numpy(tr_zu["h_u"]).to(device), torch.from_numpy(va_zu["h_u"]).to(device)
Xtr_t, Xva_t = torch.from_numpy(Xtr_s).to(device), torch.from_numpy(Xva_s).to(device)
mtr, mva = torch.from_numpy(tr_zu["mode"].astype(np.int64)).to(device), torch.from_numpy(va_zu["mode"].astype(np.int64)).to(device)
tgtr, tgva = make_targets(tr_zu, device), make_targets(va_zu, device)
Ntr, Nva = len(Ztr), len(Zva)
log(f"train {Ntr:,} rows, val {Nva:,} rows; features {len(names)}; base rates {base_rate.tolist()}", out)
torch.manual_seed(args.seed)
model = FutureStateHead(d_z=Ztr.shape[1], d_dwell=len(names), n_states=len(STATES), d=args.d).to(device)
npar = n_params(model)
log(f"FutureStateHead params {npar:,}", out)
opt = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=1e-4)
gen = torch.Generator(device=device).manual_seed(args.seed)
idx_tr, idx_va = torch.arange(Ntr, device=device), torch.arange(Nva, device=device)
best, best_ep, bad, hist = float("inf"), -1, 0, []
for ep in range(1, args.epochs + 1):
t0 = time.time()
model.train()
ltr, ptr = run_epoch(model, opt, Ztr, Xtr_t, mtr, tgtr, idx_tr, args.batch, True, gen)
model.eval()
with torch.no_grad():
lva, pva = run_epoch(model, None, Zva, Xva_t, mva, tgva, idx_va, 16384, False)
obj = pva["occ_bce"]
rec = {"epoch": ep, "train_loss": ltr, "val_loss": lva, "val_occ_bce": obj, "val_parts": pva, "train_parts": ptr, "wall_s": round(time.time() - t0, 1)}
hist.append(rec)
with open(out / "history.jsonl", "a") as f:
f.write(json.dumps(rec) + "\n")
improved = obj < best - 1e-6
if improved:
best, best_ep, bad = obj, ep, 0
torch.save({"model": model.state_dict(), "epoch": ep, "val_occ_bce": obj, "feat_names": names, "feat_stats": fstats, "base_rate": base_rate, "args": vars(args),
"final_run": final_run, "d_z": int(Ztr.shape[1]), "n_states": len(STATES), "d": args.d}, out / "ckpt_best.pt")
else:
bad += 1
log(f"ep {ep} train {ltr:.4f} val {lva:.4f} occ_bce {obj:.5f} " + " ".join(f"{k}={v:.4f}" for k, v in pva.items() if k.startswith(("occ_", "trans_", "leave_", "rem_"))) +
f" {'*' if improved else ''} ({rec['wall_s']}s)", out)
if bad >= args.patience:
break
z = torch.load(out / "ckpt_best.pt", map_location=device, weights_only=False)
model.load_state_dict(z["model"])
(out / "BEST").write_text("ckpt_best.pt\n")
(out / "TRAIN_DONE").write_text(json.dumps({"best_epoch": best_ep, "best": best, "epochs_run": len(hist), "params": npar, "selection": "validation occupancy BCE (parts occ_bce)"}))
meta.update(code_sha256=code_hash(CODE_FILES), params=npar, best_epoch=best_ep, val_occ_bce=best, seed=args.seed, name=args.name, feats=args.feats, no_z=bool(args.no_z))
(out / "config.json").write_text(json.dumps({"args": vars(args), "final_run": final_run, "features": names, "params": npar, "meta": meta}, indent=1, default=str))
# ---- predictions
del Ztr, Xtr_t, tgtr
n_pred = {}
for split in args.splits.split(","):
for (zu, X, nm), r in zip(data[split], regimes):
Xs, _ = standardise(X, fstats)
n_pred[f"{r}_{split}"] = predict_split(model, zu, Xs, out, r, split, device, base_rate, meta, names)
log(f"fs_{r}_{split}.npz written ({n_pred[f'{r}_{split}']:,} rows)", out)
inputs = sorted({p for r in regimes for sp in SPLITS for p in (ds.kin_dir() / f"kin_causal_{r}_{sp}.npz", ds.kin_dir() / f"kinhist_causal_{r}_{sp}.npz",
ds.GAZE / f"gaze_{r}_{sp}.npz", ds.FACE / f"facehist_causal_{r}_{sp}.npz") if p.exists()}
| {RUNS / final_run / (RUNS / final_run / "BEST").read_text().strip()})
write_run_json(str(out), f"train_future_state_{args.name}_s{args.seed}", CODE_FILES, [] if args.max_anchors else inputs, seed=args.seed, t_start=t_start,
extra={"final_run": final_run, "features": names, "params": npar, "best_epoch": best_ep, "val_occ_bce": best, "history": hist, "n_pred": n_pred,
"base_rate_z0": base_rate.tolist(), "state_names": list(STATES), "selection": "validation occupancy BCE", "smoke": bool(args.max_anchors), "feats": args.feats, "no_z": bool(args.no_z),
"dms_fields_used": [], "gpu_wall_s": round(time.time() - t_start, 1)})
(out / "PRED_DONE").write_text(json.dumps({"splits": args.splits.split(","), "regimes": regimes, "n_pred": n_pred}))
log(f"DONE in {time.time() - t_start:.0f}s", out)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
29.5 kB
·
Xet hash:
0022ebbaccf46b5e39d6534dc9dd387e1768056122c804e2e327cc3e10761914

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.