| """MotionDNA v0 — modelo generativo de movimiento humano entrenado con la pose |
| extraida de music videos. Transformer causal que predice la pose del frame t+1 |
| dada la secuencia 0..t. Genera movimiento nuevo por rollout autoregresivo. |
| Pensado para caber holgado en una L4 (23GB).""" |
| import os, glob, math, time, json |
| import numpy as np |
| import torch, torch.nn as nn |
| from torch.utils.data import Dataset, DataLoader |
|
|
| POSES = os.path.expanduser("~/motiondna/poses") |
| CKPT = os.path.expanduser("~/motiondna/checkpoints") |
| SAMP = os.path.expanduser("~/motiondna/samples") |
| os.makedirs(CKPT, exist_ok=True); os.makedirs(SAMP, exist_ok=True) |
| DEV = "cuda" if torch.cuda.is_available() else "cpu" |
| WIN = 64 |
| DIM = 34 |
| D_MODEL = 256; N_HEAD = 8; N_LAYER = 6; DROP = 0.1 |
| BATCH = 64; EPOCHS = 60; LR = 3e-4 |
|
|
| def normalize(seq): |
| """seq [T,17,2] xy normalizado a frame -> centrado en cadera + escala torso.""" |
| s = seq.copy().astype(np.float32) |
| hip = (s[:,11,:] + s[:,12,:]) / 2.0 |
| s = s - hip[:,None,:] |
| sh = (s[:,5,:] + s[:,6,:]) / 2.0 |
| torso = np.linalg.norm(sh - 0.0, axis=1) |
| scale = np.clip(np.median(torso), 1e-3, None) |
| s = s / scale |
| return s.reshape(s.shape[0], -1) |
|
|
| class MotionDS(Dataset): |
| def __init__(self): |
| self.windows = [] |
| files = sorted(glob.glob(os.path.join(POSES, "*.npy"))) |
| for f in files: |
| seq = np.load(f) |
| if seq.shape[0] < WIN + 1: continue |
| n = normalize(seq) |
| for i in range(0, n.shape[0] - WIN - 1, WIN // 2): |
| self.windows.append(n[i:i+WIN+1]) |
| if self.windows: |
| allp = np.concatenate(self.windows, 0) |
| self.mean = allp.mean(0, keepdims=True); self.std = allp.std(0, keepdims=True) + 1e-6 |
| self.windows = [(w - self.mean) / self.std for w in self.windows] |
| print(f"Ventanas de entrenamiento: {len(self.windows)} | clips: {len(files)}", flush=True) |
| def __len__(self): return len(self.windows) |
| def __getitem__(self, i): |
| w = torch.from_numpy(self.windows[i].astype(np.float32)) |
| return w[:-1], w[1:] |
|
|
| class MotionDNA(nn.Module): |
| def __init__(self): |
| super().__init__() |
| self.inp = nn.Linear(DIM, D_MODEL) |
| self.pos = nn.Parameter(torch.randn(1, WIN, D_MODEL) * 0.02) |
| layer = nn.TransformerEncoderLayer(D_MODEL, N_HEAD, D_MODEL*4, DROP, batch_first=True, activation="gelu") |
| self.enc = nn.TransformerEncoder(layer, N_LAYER) |
| self.out = nn.Linear(D_MODEL, DIM) |
| def forward(self, x): |
| T = x.size(1) |
| h = self.inp(x) + self.pos[:, :T] |
| mask = torch.triu(torch.ones(T, T, device=x.device), 1).bool() |
| h = self.enc(h, mask=mask) |
| return self.out(h) |
|
|
| def main(): |
| ds = MotionDS() |
| if len(ds) == 0: |
| print("ERROR: no hay ventanas. Corre extract_poses.py primero."); return |
| dl = DataLoader(ds, batch_size=BATCH, shuffle=True, num_workers=4, drop_last=True) |
| model = MotionDNA().to(DEV) |
| nparam = sum(p.numel() for p in model.parameters()) |
| print(f"Device: {DEV} | Parametros: {nparam/1e6:.2f}M", flush=True) |
| opt = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-4) |
| sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, EPOCHS) |
| lossf = nn.SmoothL1Loss() |
| best = 1e9; hist = [] |
| for ep in range(1, EPOCHS+1): |
| model.train(); tot = 0; nb = 0 |
| for x, y in dl: |
| x, y = x.to(DEV), y.to(DEV) |
| pred = model(x) |
| loss = lossf(pred, y) |
| opt.zero_grad(); loss.backward() |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) |
| opt.step() |
| tot += loss.item(); nb += 1 |
| sched.step() |
| avg = tot / max(nb,1); hist.append(avg) |
| mem = torch.cuda.max_memory_allocated()/1e9 if DEV=="cuda" else 0 |
| print(f"Epoch {ep:03d}/{EPOCHS} loss={avg:.5f} lr={sched.get_last_lr()[0]:.2e} vram={mem:.2f}GB", flush=True) |
| if avg < best: |
| best = avg |
| torch.save({"model": model.state_dict(), "mean": ds.mean, "std": ds.std, |
| "cfg": {"WIN":WIN,"DIM":DIM,"D_MODEL":D_MODEL,"N_HEAD":N_HEAD,"N_LAYER":N_LAYER}, |
| "epoch": ep, "loss": best}, os.path.join(CKPT, "motiondna_v0_best.pt")) |
| torch.save({"model": model.state_dict(), "mean": ds.mean, "std": ds.std, |
| "cfg": {"WIN":WIN,"DIM":DIM,"D_MODEL":D_MODEL,"N_HEAD":N_HEAD,"N_LAYER":N_LAYER}, |
| "epoch": EPOCHS, "loss": hist[-1]}, os.path.join(CKPT, "motiondna_v0_final.pt")) |
| json.dump(hist, open(os.path.join(CKPT, "loss_history.json"), "w")) |
| |
| model.eval() |
| with torch.no_grad(): |
| seed = ds[0][0][:16].unsqueeze(0).to(DEV) |
| gen = seed.clone() |
| for _ in range(120): |
| nxt = model(gen[:, -WIN:])[:, -1:] |
| gen = torch.cat([gen, nxt], 1) |
| gen = gen[0].cpu().numpy() * ds.std + ds.mean |
| np.save(os.path.join(SAMP, "motiondna_sample.npy"), gen.reshape(-1,17,2)) |
| print(f"LISTO. best_loss={best:.5f} ckpt={CKPT}/motiondna_v0_best.pt sample={SAMP}/motiondna_sample.npy", flush=True) |
|
|
| if __name__ == "__main__": |
| main() |
|
|