fela-tab / train.py
itstheraj's picture
initial commit
c902bdf
Raw
History Blame Contribute Delete
6.04 kB
import argparse
import math
import os
import time
import numpy as np
import torch
import torch.nn.functional as F
from modeling import FelaTab, FelaTabConfig, count_params
from prior import PriorConfig, gen_pool
def collate(tasks, task_type, tmax, rng, maxF, s_frac=(0.5, 0.9)):
B = len(tasks)
used, sup = ([], [])
for t in tasks:
N = t["X"].shape[0]
n = min(N, tmax)
frac = rng.uniform(*s_frac)
s = int(np.clip(int(frac * n), 16, n - 8))
used.append(n)
sup.append(s)
T = max(used)
X = np.zeros((B, T, maxF), np.float32)
y = np.zeros((B, T), np.int64 if task_type == "cls" else np.float32)
lm = np.zeros((B, T), np.float32)
ncl = np.zeros(B, np.int64)
ns = np.zeros(B, np.int64)
for b, t in enumerate(tasks):
N = t["X"].shape[0]
n = used[b]
s = sup[b]
Ft = min(t["X"].shape[1], maxF)
perm = rng.permutation(N)[:n]
X[b, :n, :Ft] = t["X"][perm][:, :Ft]
y[b, :n] = t["y"][perm]
lm[b, s:n] = 1.0
ns[b] = s
ncl[b] = t["n_classes"] if task_type == "cls" else 0
return (
torch.from_numpy(X),
torch.from_numpy(y),
torch.from_numpy(ns),
torch.from_numpy(lm),
torch.from_numpy(ncl),
)
def cls_loss_acc(logits, y, lm, ncl):
B, T, C = logits.shape
cmask = torch.arange(C, device=logits.device).view(1, C) < ncl.view(B, 1)
logits = logits.masked_fill(~cmask.unsqueeze(1), -1000000000.0)
lf = logits.reshape(B * T, C)
yf = y.reshape(B * T).clamp(min=0)
mf = lm.reshape(B * T).bool()
if mf.sum() == 0:
return (logits.sum() * 0.0, 0.0)
loss = F.cross_entropy(lf[mf], yf[mf])
acc = (lf[mf].argmax(-1) == yf[mf]).float().mean().item()
return (loss, acc)
def reg_loss_r2(out, y, lm):
mean = out[..., 0]
logvar = out[..., 1].clamp(-8, 8)
mf = lm.bool()
if mf.sum() == 0:
return (out.sum() * 0.0, 0.0)
m, lv, t = (mean[mf], logvar[mf], y[mf])
nll = 0.5 * (lv + (t - m) ** 2 / torch.exp(lv) + math.log(2 * math.pi))
ss_res = ((t - m) ** 2).sum().item()
ss_tot = ((t - t.mean()) ** 2).sum().item() + 1e-08
return (nll.mean(), 1 - ss_res / ss_tot)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--steps", type=int, default=20000)
ap.add_argument("--bs", type=int, default=16)
ap.add_argument("--tmax", type=int, default=1024)
ap.add_argument("--lr", type=float, default=0.0003)
ap.add_argument("--dim", type=int, default=512)
ap.add_argument("--layers", type=int, default=14)
ap.add_argument("--heads", type=int, default=8)
ap.add_argument("--head_dim", type=int, default=64)
ap.add_argument("--pool", type=int, default=1000)
ap.add_argument("--refresh", type=int, default=400)
ap.add_argument("--ckpt", type=str, default="ckpt/felatab.pt")
ap.add_argument(
"--smoke", action="store_true", help="tiny fast loop to prove it trains"
)
args = ap.parse_args()
if args.smoke:
args.steps, args.dim, args.layers, args.heads = (200, 128, 4, 4)
args.pool, args.bs, args.tmax = (128, 8, 256)
dev = "cuda" if torch.cuda.is_available() else "cpu"
prior_cfg = PriorConfig(max_classes=10, max_rows=6000)
cfg = FelaTabConfig(
dim=args.dim,
n_layers=args.layers,
n_heads=args.heads,
head_dim=args.head_dim,
max_features=prior_cfg.max_features,
max_classes=prior_cfg.max_classes,
)
model = FelaTab(cfg).to(dev)
print(
f"params {count_params(model) / 1000000.0:.2f}M dim={cfg.dim} L={cfg.n_layers} H={cfg.n_heads} dev={dev}"
)
opt = torch.optim.AdamW(
model.parameters(), lr=args.lr, weight_decay=0.01, betas=(0.9, 0.95)
)
sched = torch.optim.lr_scheduler.OneCycleLR(
opt,
max_lr=args.lr,
total_steps=args.steps,
pct_start=0.03,
anneal_strategy="cos",
)
rng = np.random.default_rng(1234)
def refill(seed):
pool = gen_pool(prior_cfg, args.pool, seed=seed, workers=None)
return (
[t for t in pool if t["task_type"] == "cls"],
[t for t in pool if t["task_type"] == "reg"],
)
print("generating initial synthetic pool...")
t0 = time.time()
cls_pool, reg_pool = refill(int(rng.integers(1000000000.0)))
print(
f"pool ready {time.time() - t0:.1f}s (cls {len(cls_pool)} reg {len(reg_pool)})"
)
model.train()
run_loss = run_metric = 0.0
for step in range(1, args.steps + 1):
typ = "cls" if step % 2 == 0 else "reg"
src = cls_pool if typ == "cls" else reg_pool
if not src:
continue
T_target = int(rng.integers(128, args.tmax + 1))
idx = rng.integers(0, len(src), size=args.bs)
batch = [src[i] for i in idx]
X, y, ns, lm, ncl = collate(batch, typ, T_target, rng, cfg.max_features)
X, y, ns, lm, ncl = [z.to(dev) for z in (X, y, ns, lm, ncl)]
out = model(X, y, ns, torch.tensor(0 if typ == "cls" else 1, device=dev))
if typ == "cls":
loss, metric = cls_loss_acc(out.float(), y, lm, ncl)
else:
loss, metric = reg_loss_r2(out.float(), y, lm)
opt.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step()
sched.step()
run_loss += loss.item()
run_metric += metric
if step % 50 == 0:
print(
f"step {step:6d} {typ} loss {run_loss / 50:.4f} metric {run_metric / 50:.4f} lr {sched.get_last_lr()[0]:.2e}",
flush=True,
)
run_loss = run_metric = 0.0
if step % args.refresh == 0:
cls_pool, reg_pool = refill(int(rng.integers(1000000000.0)))
os.makedirs(os.path.dirname(args.ckpt) or ".", exist_ok=True)
torch.save(model.state_dict(), args.ckpt)
print(f"done. saved {args.ckpt}")
if __name__ == "__main__":
main()