Add GAMEFORMER_SRA.md (negative result) + standalone GameFormer/C2F/SRA-GameFormer scripts
Browse files- GAMEFORMER_SRA.md +147 -0
- README.md +6 -0
- standalone/c2f_nba_standalone.py +434 -0
- standalone/gameformer_nba_standalone.py +614 -0
- standalone/gameformer_sra_nba_standalone.py +712 -0
GAMEFORMER_SRA.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GameFormer + SRA — a negative result
|
| 2 |
+
|
| 3 |
+
**Question.** SRA improves three *iterative denoisers* (MID, LED, MoFlow). Do its ideas also help
|
| 4 |
+
a **feedforward, non-diffusion** predictor? If so, SRA generalizes beyond denoising.
|
| 5 |
+
|
| 6 |
+
**Answer. No.** Injecting SRA's three components into GameFormer is **neutral-to-slightly-negative**.
|
| 7 |
+
|
| 8 |
+
| Model (NBA, min-ADE₂₀ / min-FDE₂₀ @ 4.0 s) | ADE₄ | FDE₄ | params |
|
| 9 |
+
|---|---|---|---|
|
| 10 |
+
| Vanilla GameFormer (control) | **0.8496** | **0.9557** | 11.85 M |
|
| 11 |
+
| GameFormer **+ SRA** (`GF_SRA=1`) | 0.8554 | 0.9564 | 11.90 M |
|
| 12 |
+
| **Δ (SRA − vanilla)** | **+0.0058 (worse)** | +0.0007 (≈neutral) | +0.05 M |
|
| 13 |
+
|
| 14 |
+
Both peaked at epoch 48 of 50. Same file, same seed (3407), same schedule — **only the three flags differ**.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 1. Experimental design
|
| 19 |
+
|
| 20 |
+
The comparison is a **within-file ablation**, not a cross-codebase one:
|
| 21 |
+
|
| 22 |
+
* One script, `gameformer_sra_nba_standalone.py`, contains both arms.
|
| 23 |
+
* Three env-gated components; **with all flags off the code path is exactly vanilla GameFormer.**
|
| 24 |
+
* Verified: flags-off reports **11.85 M params** — identical to the separate vanilla
|
| 25 |
+
implementation, which scores 0.848/0.951. The refactor perturbs nothing.
|
| 26 |
+
* SRA adds only **+0.05 M params (+0.4 %)**, so any effect is attributable to the *mechanism*,
|
| 27 |
+
not to capacity.
|
| 28 |
+
|
| 29 |
+
Config: level-k = 3, K = 20 modes, 6 encoder layers, AdamW lr 1e-4, wd 1e-4, grad-clip 5,
|
| 30 |
+
MultiStepLR ×0.5, batch 128, 50 epochs, seed 3407. Standalone predictor — history only,
|
| 31 |
+
**no denoiser and no external future signal**.
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
## 2. What was injected
|
| 36 |
+
|
| 37 |
+
All three live inside GameFormer's `InteractionDecoder` (one per level-k stage).
|
| 38 |
+
|
| 39 |
+
### 2.1 Uncertainty-gated message passing — `GF_SRA_SIGMA`
|
| 40 |
+
|
| 41 |
+
GameFormer's GMM head already *emits* a per-step log-σ but never **uses** it for interaction.
|
| 42 |
+
SRA turns it into a message gate: uncertain agents send weaker interaction messages.
|
| 43 |
+
|
| 44 |
+
```python
|
| 45 |
+
sig = torch.exp(last_traj[..., 2:].clamp(0, 5)).mean(-1) # [B,K,A,T] from prev level's GMM
|
| 46 |
+
sig_agent = (sig * w[..., None]).sum(2).mean(-1) # [B,A] score-weighted, mean over T
|
| 47 |
+
...
|
| 48 |
+
futures = futures * self.sigma_gate(sig_agent[..., None]) # MLP(1→32→1)+Sigmoid ⇒ gate ∈ (0,1)
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### 2.2 Sparse RAG neighbor selection — `GF_SRA_TOPN`
|
| 52 |
+
|
| 53 |
+
GameFormer attends **densely** over all agents. SRA restricts each agent to its top-N (=5)
|
| 54 |
+
neighbours, chosen by *future* proximity and biased toward confident agents.
|
| 55 |
+
|
| 56 |
+
```python
|
| 57 |
+
prox = (rep[:, :, None] - rep[:, None]).norm(dim=-1).mean(-1) # [B,A,A] mean future distance
|
| 58 |
+
if sig_agent is not None:
|
| 59 |
+
prox = prox + SIG_SEL_W * sig_agent[:, None, :] # prefer confident neighbours
|
| 60 |
+
prox = prox.masked_fill(eye[None], float('inf')) # exclude self
|
| 61 |
+
nbr_idx = prox.topk(N, largest=False, dim=-1).indices
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
The resulting mask is applied **both** to the interaction self-attention (`attn_mask`) and to the
|
| 65 |
+
cross-attention key mask, so sparsity is enforced everywhere, not just in one place.
|
| 66 |
+
|
| 67 |
+
### 2.3 Explicit relational future features — `GF_SRA_RELFUT`
|
| 68 |
+
|
| 69 |
+
GameFormer's `FutureEncoder` encodes each agent's future **individually**. SRA adds explicit
|
| 70 |
+
**pairwise-relative** future geometry (Δposition, Δvelocity), max-pooled over time.
|
| 71 |
+
|
| 72 |
+
```python
|
| 73 |
+
dpos = rep[:, :, None] - rep[:, None] # [B,A,A,T,2]
|
| 74 |
+
dvel = F.pad(torch.diff(dpos, dim=-2), (0, 0, 1, 0))
|
| 75 |
+
relf = self.rel_mlp(torch.cat([dpos, dvel], -1)).max(dim=-2).values # MLP(4→64→D)
|
| 76 |
+
futures = futures + relf.sum(2) / denom # neighbour-mean
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## 3. Reproducing
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
# control (vanilla GameFormer)
|
| 85 |
+
python gameformer_sra_nba_standalone.py --gpu 0 --exp van --level 3 --epochs 50 --batch_size 128
|
| 86 |
+
|
| 87 |
+
# + SRA (all three components)
|
| 88 |
+
GF_SRA=1 python gameformer_sra_nba_standalone.py --gpu 0 --exp sra --level 3 --epochs 50 --batch_size 128
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
| Variable | Effect |
|
| 92 |
+
|---|---|
|
| 93 |
+
| `GF_SRA=1` | enable all three components |
|
| 94 |
+
| `GF_SRA_SIGMA` / `GF_SRA_TOPN` / `GF_SRA_RELFUT` | enable individually |
|
| 95 |
+
| `GF_TOPN` | neighbour budget (default 5) |
|
| 96 |
+
| `GF_SIG_SEL_W` | weight of σ in neighbour selection (default 0.5) |
|
| 97 |
+
|
| 98 |
+
The script prints its configuration so every run is self-documenting:
|
| 99 |
+
|
| 100 |
+
```
|
| 101 |
+
[SRA-GF] SRA components: neighbor-topN=True(N=5) sigma-gate=True relational-future=True
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
## 4. Why it does not transfer
|
| 107 |
+
|
| 108 |
+
1. **The future-interaction idea is already native to GameFormer.** Its level-k reasoning
|
| 109 |
+
*is* "condition each agent on the others' predicted futures". SRA's relational features
|
| 110 |
+
are therefore largely **redundant** here, adding parameters and noise rather than new signal.
|
| 111 |
+
In MID/LED/MoFlow nothing plays this role, which is exactly why SRA helps there.
|
| 112 |
+
|
| 113 |
+
2. **The uncertainty signal is weaker.** In a denoiser, σ is *principled* — a noise schedule or
|
| 114 |
+
the leapfrog initializer's variance estimate, tied to how much the model should trust its
|
| 115 |
+
current iterate. GameFormer's log-σ is just a learned regression head; gating on it does not
|
| 116 |
+
carry comparable information.
|
| 117 |
+
|
| 118 |
+
3. **Sparsity costs more than it saves at A = 11.** Dense attention over 11 agents is cheap and
|
| 119 |
+
fully informative; discarding all but 5 neighbours removes usable context. SRA's sparse
|
| 120 |
+
selection pays off on 23-agent sport scenes and on hosts where dense interaction is unstable.
|
| 121 |
+
|
| 122 |
+
4. **One-shot vs iterative (the core reason).** SRA is built to *refine an evolving estimate*: it
|
| 123 |
+
is applied at every denoising step, so its gated residual is corrected repeatedly and errors
|
| 124 |
+
are damped. A feedforward decoder gets **one** pass — no iterative correction, no damping.
|
| 125 |
+
The regime SRA was designed for is absent.
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
## 5. How to read this result
|
| 130 |
+
|
| 131 |
+
This is a **scope finding, not a failure**, and it is consistent with the paper's framing:
|
| 132 |
+
SRA is a *denoiser* adapter. Its gains come from repeatedly reshaping an iteratively refined
|
| 133 |
+
future estimate — a mechanism a single-pass predictor does not have.
|
| 134 |
+
|
| 135 |
+
Stated honestly:
|
| 136 |
+
|
| 137 |
+
> SRA's gains are specific to the iterative-denoising setting. Applied to a strong feedforward
|
| 138 |
+
> predictor (GameFormer), it is neutral-to-slightly-negative, which supports rather than
|
| 139 |
+
> contradicts the paper's positioning.
|
| 140 |
+
|
| 141 |
+
It mirrors the MoFlow observation from the other direction: the *host's* update mechanism
|
| 142 |
+
determines what an interaction module can contribute.
|
| 143 |
+
|
| 144 |
+
**Caveats.** Single seed; the ADE gap (0.006, 0.7 %) is small enough that multi-seed runs could
|
| 145 |
+
place it within noise — i.e. "neutral" rather than "worse". It is clearly **not** an improvement
|
| 146 |
+
either way. No per-component ablation was run, so it remains open whether one component alone
|
| 147 |
+
transfers even though the combination does not.
|
README.md
CHANGED
|
@@ -18,6 +18,12 @@ It covers the required directory layout, environment setup, the exact training c
|
|
| 18 |
host × dataset, the E2 ablation settings, environment-variable switches, the adapter contract,
|
| 19 |
and known gotchas.
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
## Contents
|
| 22 |
|
| 23 |
```
|
|
|
|
| 18 |
host × dataset, the E2 ablation settings, environment-variable switches, the adapter contract,
|
| 19 |
and known gotchas.
|
| 20 |
|
| 21 |
+
## Documents
|
| 22 |
+
|
| 23 |
+
- [`RUNNING.md`](RUNNING.md) — how to run every host × dataset
|
| 24 |
+
- [`GAMEFORMER_SRA.md`](GAMEFORMER_SRA.md) — GameFormer+SRA negative result (does SRA generalize to feedforward models?)
|
| 25 |
+
- [`sample_data/README.md`](sample_data/README.md) — bundled 100-scene NBA smoke-test subset
|
| 26 |
+
|
| 27 |
## Contents
|
| 28 |
|
| 29 |
```
|
standalone/c2f_nba_standalone.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
c2f_nba_standalone.py
|
| 5 |
+
================================================================================
|
| 6 |
+
FAITHFUL standalone re-implementation of the **Coarse-to-Fine** trajectory
|
| 7 |
+
predictor of ref [22] ("Towards Capturing the Temporal Dynamics for Trajectory
|
| 8 |
+
Prediction: A Coarse-to-Fine Approach") as a STANDALONE predictor for the NBA
|
| 9 |
+
basketball dataset.
|
| 10 |
+
|
| 11 |
+
It is a STANDALONE predictor: it consumes ONLY agent HISTORY (past trajectories)
|
| 12 |
+
and predicts K multi-modal futures via a native COARSE stage followed by an
|
| 13 |
+
autoregressive temporal FINE-refinement stage. It does NOT consume any external
|
| 14 |
+
denoiser / diffusion prediction (unlike the plug-in `CoarseToFineRefine` module,
|
| 15 |
+
which was handed the host's intermediate future estimate -> that borrowed SRA's
|
| 16 |
+
future-interaction signal and was therefore unfair; this standalone version
|
| 17 |
+
generates its own coarse trajectory, so it is faithful to the native method).
|
| 18 |
+
|
| 19 |
+
--------------------------------------------------------------------------------
|
| 20 |
+
WHAT THE NATIVE METHOD IS (and what we replicate)
|
| 21 |
+
--------------------------------------------------------------------------------
|
| 22 |
+
Coarse-to-fine = two-stage decoding that *captures temporal dynamics* by first
|
| 23 |
+
predicting a rough full trajectory and then refining it step-by-step:
|
| 24 |
+
|
| 25 |
+
STAGE 0 (COARSE): a multimodal decoder emits K rough full-horizon
|
| 26 |
+
trajectories per agent from the social-encoded context (mode + agent query
|
| 27 |
+
cross-attended to all agents' context, then an MLP trajectory head). This is
|
| 28 |
+
the model's OWN coarse prediction -- it is NOT received from a host.
|
| 29 |
+
|
| 30 |
+
STAGE 1..S (FINE): the coarse trajectory is walked TEMPORALLY by a
|
| 31 |
+
unidirectional (autoregressive) GRU -- the mechanism the paper uses to
|
| 32 |
+
capture temporal dynamics -- conditioned on the mode's context, emitting a
|
| 33 |
+
per-timestep residual correction delta_t ; y_fine = y_coarse + delta.
|
| 34 |
+
Repeated S times (progressive coarse -> fine -> finer). delta head is
|
| 35 |
+
zero-initialised so training first fits the coarse stage, then the refiner
|
| 36 |
+
engages (stable).
|
| 37 |
+
|
| 38 |
+
Mode scores: a per-mode classification head; winner-takes-all training.
|
| 39 |
+
|
| 40 |
+
ENCODER (shared, IDENTICAL to the GameFormer standalone so the E4 comparison
|
| 41 |
+
isolates the DECODER mechanism, coarse-to-fine vs level-k, not the backbone):
|
| 42 |
+
* AgentHistoryEncoder = 2-layer LSTM(6->256) over agent history + learned
|
| 43 |
+
player/ball type embedding.
|
| 44 |
+
* FusionEncoder = nn.TransformerEncoder (d=256, heads=8, ff=1024, gelu),
|
| 45 |
+
`encoder_layers` deep -> agent<->agent social self-attention (no map: NBA).
|
| 46 |
+
|
| 47 |
+
LOSS (native coarse-to-fine supervision):
|
| 48 |
+
* coarse WTA-L2 (variety loss over K modes) + fine WTA-L2 + mode
|
| 49 |
+
cross-entropy (label smoothing 0.2) on the fine-stage winning mode.
|
| 50 |
+
* endpoint-emphasised distance (mean_t + checkpoint-step sum) for mode
|
| 51 |
+
selection, marginal PER AGENT (NBA Table-1 metric is marginal min-ADE_20).
|
| 52 |
+
|
| 53 |
+
--------------------------------------------------------------------------------
|
| 54 |
+
NBA I/O + METRIC (matched to MoFlow / Table 1 -- identical to GameFormer standalone)
|
| 55 |
+
--------------------------------------------------------------------------------
|
| 56 |
+
* Data: MoFlow's data/dataloader_nba.py::NBADatasetMinMax, same .npy, split,
|
| 57 |
+
scaling (traj_scale=94/28, traj_mean=[14,7.5]). Past=10, Future=20 (4.0 s).
|
| 58 |
+
* Predict in centered-abs frame (pos/scale - mean); cur_xy = last past step;
|
| 59 |
+
gt_center = fut_traj_original_scale (displacement) + cur_xy.
|
| 60 |
+
* Metric (identical to eval_perscene_moflow.py):
|
| 61 |
+
d = ||pred_disp - gt_disp|| ; ADE4 = d[:,:20].mean_t.min_K ; FDE4 =
|
| 62 |
+
d[:,19].min_K ; mean over 11 agents & scenes (marginal min-of-K=20 @ 4s).
|
| 63 |
+
|
| 64 |
+
Prints: [C2F-NBA] epoch N ADE4=.. FDE4=..
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
import os
|
| 68 |
+
import sys
|
| 69 |
+
import argparse
|
| 70 |
+
import time
|
| 71 |
+
|
| 72 |
+
# ------------------------------------------------------------------ GPU FIRST
|
| 73 |
+
def _early_gpu():
|
| 74 |
+
for i, a in enumerate(sys.argv):
|
| 75 |
+
if a == '--gpu' and i + 1 < len(sys.argv):
|
| 76 |
+
return sys.argv[i + 1]
|
| 77 |
+
if a.startswith('--gpu='):
|
| 78 |
+
return a.split('=', 1)[1]
|
| 79 |
+
return None
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
_g = _early_gpu()
|
| 83 |
+
if _g is not None:
|
| 84 |
+
os.environ['CUDA_VISIBLE_DEVICES'] = str(_g)
|
| 85 |
+
os.environ.setdefault('MPLBACKEND', 'Agg')
|
| 86 |
+
|
| 87 |
+
# ------------------------------------------------ reuse MoFlow's NBA pipeline
|
| 88 |
+
MOFLOW_ROOT = '/mnt/jaewoo4tb/srtp/MoFlow'
|
| 89 |
+
if MOFLOW_ROOT not in sys.path:
|
| 90 |
+
sys.path.insert(0, MOFLOW_ROOT)
|
| 91 |
+
|
| 92 |
+
import types as _types
|
| 93 |
+
import numpy as np
|
| 94 |
+
import torch
|
| 95 |
+
import torch.nn as nn
|
| 96 |
+
import torch.nn.functional as F
|
| 97 |
+
from torch.utils.data import DataLoader
|
| 98 |
+
|
| 99 |
+
try:
|
| 100 |
+
os.chdir(MOFLOW_ROOT)
|
| 101 |
+
except Exception:
|
| 102 |
+
pass
|
| 103 |
+
from data.dataloader_nba import NBADatasetMinMax, seq_collate_nba
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# ============================================================================
|
| 107 |
+
# PRIMITIVES (shared with the GameFormer standalone, verbatim)
|
| 108 |
+
# ============================================================================
|
| 109 |
+
D_MODEL = 256
|
| 110 |
+
N_HEADS = 8
|
| 111 |
+
DROPOUT = 0.1
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class CrossTransformer(nn.Module):
|
| 115 |
+
def __init__(self, dim=D_MODEL, heads=N_HEADS, dropout=DROPOUT):
|
| 116 |
+
super().__init__()
|
| 117 |
+
self.cross_attention = nn.MultiheadAttention(dim, heads, dropout, batch_first=True)
|
| 118 |
+
self.norm_1 = nn.LayerNorm(dim)
|
| 119 |
+
self.norm_2 = nn.LayerNorm(dim)
|
| 120 |
+
self.ffn = nn.Sequential(
|
| 121 |
+
nn.Linear(dim, dim * 4), nn.GELU(), nn.Dropout(dropout),
|
| 122 |
+
nn.Linear(dim * 4, dim), nn.Dropout(dropout))
|
| 123 |
+
|
| 124 |
+
def forward(self, query, key, value, mask=None):
|
| 125 |
+
attn, _ = self.cross_attention(query, key, value, key_padding_mask=mask)
|
| 126 |
+
attn = self.norm_1(attn)
|
| 127 |
+
return self.norm_2(self.ffn(attn) + attn)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class AgentHistoryEncoder(nn.Module):
|
| 131 |
+
"""2-layer LSTM(6->256) over agent history + player/ball type embedding."""
|
| 132 |
+
|
| 133 |
+
def __init__(self, in_dim=6, dim=D_MODEL, n_types=2):
|
| 134 |
+
super().__init__()
|
| 135 |
+
self.motion = nn.LSTM(in_dim, dim, 2, batch_first=True)
|
| 136 |
+
self.type_emb = nn.Embedding(n_types, dim)
|
| 137 |
+
|
| 138 |
+
def forward(self, hist, types):
|
| 139 |
+
B, A, T, C = hist.shape
|
| 140 |
+
traj, _ = self.motion(hist.reshape(B * A, T, C))
|
| 141 |
+
out = traj[:, -1].reshape(B, A, -1)
|
| 142 |
+
out = out + self.type_emb(types)[None]
|
| 143 |
+
return out
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class FusionEncoder(nn.Module):
|
| 147 |
+
"""Agent<->agent social self-attention (no map for NBA)."""
|
| 148 |
+
|
| 149 |
+
def __init__(self, dim=D_MODEL, heads=N_HEADS, layers=6, dropout=DROPOUT):
|
| 150 |
+
super().__init__()
|
| 151 |
+
layer = nn.TransformerEncoderLayer(
|
| 152 |
+
d_model=dim, nhead=heads, dim_feedforward=dim * 4,
|
| 153 |
+
activation=F.gelu, dropout=dropout, batch_first=True)
|
| 154 |
+
self.encoder = nn.TransformerEncoder(layer, layers, enable_nested_tensor=False)
|
| 155 |
+
|
| 156 |
+
def forward(self, tokens, mask=None):
|
| 157 |
+
return self.encoder(tokens, src_key_padding_mask=mask)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
# ============================================================================
|
| 161 |
+
# COARSE-TO-FINE DECODER
|
| 162 |
+
# ============================================================================
|
| 163 |
+
class CoarseDecoder(nn.Module):
|
| 164 |
+
"""Stage-0: K multimodal rough full-horizon trajectories per agent.
|
| 165 |
+
Mode + agent query added to the agent's context token, cross-attended to the
|
| 166 |
+
full agent context (social), then an MLP trajectory head + a mode-score head."""
|
| 167 |
+
|
| 168 |
+
def __init__(self, modalities, n_agents, future_len, dim=D_MODEL):
|
| 169 |
+
super().__init__()
|
| 170 |
+
self.M = modalities
|
| 171 |
+
self.multi_modal_query_embedding = nn.Embedding(modalities, dim)
|
| 172 |
+
self.agent_query_embedding = nn.Embedding(n_agents, dim)
|
| 173 |
+
self.query_encoder = CrossTransformer(dim)
|
| 174 |
+
self.traj_head = nn.Sequential(
|
| 175 |
+
nn.Linear(dim, 512), nn.ELU(), nn.Dropout(0.1),
|
| 176 |
+
nn.Linear(512, future_len * 2))
|
| 177 |
+
self.score_head = nn.Sequential(
|
| 178 |
+
nn.Linear(dim, 64), nn.ELU(), nn.Dropout(0.1), nn.Linear(64, 1))
|
| 179 |
+
self.future_len = future_len
|
| 180 |
+
self.register_buffer('modal', torch.arange(modalities).long())
|
| 181 |
+
self.register_buffer('agent', torch.arange(n_agents).long())
|
| 182 |
+
|
| 183 |
+
def forward(self, encoding, cur_xy, mask=None):
|
| 184 |
+
B, A, D = encoding.shape
|
| 185 |
+
M, T = self.M, self.future_len
|
| 186 |
+
mm = self.multi_modal_query_embedding(self.modal) # [M, D]
|
| 187 |
+
ag = self.agent_query_embedding(self.agent) # [A, D]
|
| 188 |
+
query = encoding[:, :, None, :] + mm[None, None] + ag[None, :, None] # [B,A,M,D]
|
| 189 |
+
|
| 190 |
+
q = query.reshape(B * A, M, D)
|
| 191 |
+
kv = encoding[:, None, :, :].expand(B, A, A, D).reshape(B * A, A, D)
|
| 192 |
+
km = None
|
| 193 |
+
if mask is not None:
|
| 194 |
+
km = mask[:, None, :].expand(B, A, A).reshape(B * A, A)
|
| 195 |
+
content = self.query_encoder(q, kv, kv, km) # [B*A, M, D]
|
| 196 |
+
|
| 197 |
+
coarse = self.traj_head(content).view(B, A, M, T, 2)
|
| 198 |
+
coarse = coarse + cur_xy[:, :, None, None, :] # centered-abs
|
| 199 |
+
score = self.score_head(content).view(B, A, M)
|
| 200 |
+
return content.view(B, A, M, D), coarse, score
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class FineRefiner(nn.Module):
|
| 204 |
+
"""Stage-1..S: autoregressive temporal refinement of the coarse trajectory.
|
| 205 |
+
A unidirectional GRU walks the (centered) coarse trajectory, conditioned on
|
| 206 |
+
the mode context, emitting a per-timestep residual delta_t. Repeated S times."""
|
| 207 |
+
|
| 208 |
+
def __init__(self, dim=D_MODEL, hidden=256, n_stages=2, dropout=DROPOUT):
|
| 209 |
+
super().__init__()
|
| 210 |
+
self.n_stages = n_stages
|
| 211 |
+
self.pos_emb = nn.Linear(2, hidden)
|
| 212 |
+
self.ctx_proj = nn.Linear(dim, hidden)
|
| 213 |
+
self.gru = nn.GRU(hidden, hidden, 2, batch_first=True, dropout=dropout)
|
| 214 |
+
self.delta = nn.Linear(hidden, 2)
|
| 215 |
+
nn.init.zeros_(self.delta.weight) # start as identity: fine == coarse at init
|
| 216 |
+
nn.init.zeros_(self.delta.bias)
|
| 217 |
+
|
| 218 |
+
def forward(self, coarse, content, cur_xy):
|
| 219 |
+
# coarse:[B,A,M,T,2] content:[B,A,M,D] cur_xy:[B,A,2]
|
| 220 |
+
B, A, M, T, _ = coarse.shape
|
| 221 |
+
ctx = self.ctx_proj(content).reshape(B * A * M, 1, -1) # [N,1,H]
|
| 222 |
+
y = coarse
|
| 223 |
+
for _ in range(self.n_stages):
|
| 224 |
+
yc = (y - cur_xy[:, :, None, None, :]).reshape(B * A * M, T, 2) # center
|
| 225 |
+
seq = self.pos_emb(yc) + ctx # broadcast ctx over T
|
| 226 |
+
h, _ = self.gru(seq) # [N,T,H] autoregressive
|
| 227 |
+
d = self.delta(h).view(B, A, M, T, 2)
|
| 228 |
+
y = y + d
|
| 229 |
+
return y
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class CoarseToFineNBA(nn.Module):
|
| 233 |
+
"""Standalone coarse-to-fine predictor (NBA, map dropped)."""
|
| 234 |
+
|
| 235 |
+
def __init__(self, n_agents=11, past_dim=6, future_len=20, modalities=20,
|
| 236 |
+
n_stages=2, dim=D_MODEL, heads=N_HEADS, enc_layers=6, hidden=256,
|
| 237 |
+
n_types=2, ball_idx=10):
|
| 238 |
+
super().__init__()
|
| 239 |
+
self.history_encoder = AgentHistoryEncoder(past_dim, dim, n_types)
|
| 240 |
+
self.fusion_encoder = FusionEncoder(dim, heads, enc_layers)
|
| 241 |
+
self.coarse_decoder = CoarseDecoder(modalities, n_agents, future_len, dim)
|
| 242 |
+
self.fine_refiner = FineRefiner(dim, hidden, n_stages)
|
| 243 |
+
types = torch.zeros(n_agents, dtype=torch.long)
|
| 244 |
+
if 0 <= ball_idx < n_agents and n_types > 1:
|
| 245 |
+
types[ball_idx] = 1
|
| 246 |
+
self.register_buffer('agent_types', types)
|
| 247 |
+
|
| 248 |
+
def forward(self, feats, cur_xy):
|
| 249 |
+
enc = self.history_encoder(feats, self.agent_types) # [B,A,D]
|
| 250 |
+
enc = self.fusion_encoder(enc, None) # [B,A,D] social
|
| 251 |
+
content, coarse, score = self.coarse_decoder(enc, cur_xy, None)
|
| 252 |
+
fine = self.fine_refiner(coarse, content, cur_xy) # [B,A,M,T,2]
|
| 253 |
+
return {'coarse': coarse, 'fine': fine, 'scores': score}
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
# ============================================================================
|
| 257 |
+
# LOSS (coarse WTA + fine WTA + mode CE; marginal per agent)
|
| 258 |
+
# ============================================================================
|
| 259 |
+
def wta_l2(pred, gt, metric_idx):
|
| 260 |
+
"""Winner-takes-all L2 (variety loss). pred:[B,A,M,T,2] gt:[B,A,T,2].
|
| 261 |
+
Mode selection uses endpoint-emphasised distance; returns best mode's mean
|
| 262 |
+
displacement (reconstruction loss) and the winning mode index [B,A]."""
|
| 263 |
+
B, A, M, T, _ = pred.shape
|
| 264 |
+
d = torch.norm(pred - gt[:, :, None], dim=-1) # [B,A,M,T]
|
| 265 |
+
sel = d.mean(-1) + d[..., metric_idx].sum(-1) # [B,A,M]
|
| 266 |
+
best = sel.argmin(-1) # [B,A]
|
| 267 |
+
gi = best[..., None, None, None].expand(B, A, 1, T, 2)
|
| 268 |
+
best_d = torch.norm(torch.gather(pred, 2, gi).squeeze(2) - gt, dim=-1) # [B,A,T]
|
| 269 |
+
reg = best_d.mean(-1) + best_d[..., metric_idx].sum(-1) # [B,A] endpoint emphasis
|
| 270 |
+
return reg.mean(), best
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def c2f_loss(out, gt_center, metric_idx):
|
| 274 |
+
coarse_reg, _ = wta_l2(out['coarse'], gt_center, metric_idx)
|
| 275 |
+
fine_reg, best = wta_l2(out['fine'], gt_center, metric_idx)
|
| 276 |
+
B, A, M = out['scores'].shape
|
| 277 |
+
cls = F.cross_entropy(out['scores'].reshape(B * A, M), best.reshape(B * A),
|
| 278 |
+
label_smoothing=0.2)
|
| 279 |
+
return coarse_reg + fine_reg + 2.0 * cls
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
# ============================================================================
|
| 283 |
+
# DATA (identical to the GameFormer standalone)
|
| 284 |
+
# ============================================================================
|
| 285 |
+
def build_loaders(args):
|
| 286 |
+
cfg = _types.SimpleNamespace(
|
| 287 |
+
traj_mean=[14, 7.5], data_norm='min_max',
|
| 288 |
+
past_frames=args.past_len, future_frames=args.future_len, agents=args.agents)
|
| 289 |
+
train_set = NBADatasetMinMax(
|
| 290 |
+
obs_len=args.past_len, pred_len=args.future_len, training=True,
|
| 291 |
+
num_scenes=args.n_train, cfg=cfg, data_dir=args.data_dir,
|
| 292 |
+
rotate=False, data_norm='min_max')
|
| 293 |
+
test_set = NBADatasetMinMax(
|
| 294 |
+
obs_len=args.past_len, pred_len=args.future_len, training=False,
|
| 295 |
+
test_scenes=args.n_test, cfg=cfg, data_dir=args.data_dir,
|
| 296 |
+
rotate=False, data_norm='min_max')
|
| 297 |
+
train_loader = DataLoader(
|
| 298 |
+
train_set, batch_size=args.batch_size, shuffle=True, num_workers=args.workers,
|
| 299 |
+
collate_fn=seq_collate_nba, pin_memory=True, drop_last=True)
|
| 300 |
+
test_loader = DataLoader(
|
| 301 |
+
test_set, batch_size=args.test_batch, shuffle=False, num_workers=args.workers,
|
| 302 |
+
collate_fn=seq_collate_nba, pin_memory=True)
|
| 303 |
+
return train_loader, test_loader
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def unpack(data, device):
|
| 307 |
+
feats = data['past_traj'].to(device) # [B,A,T,6] normalized
|
| 308 |
+
past_orig = data['past_traj_original_scale'].to(device) # [B,A,T,6] metric
|
| 309 |
+
gt_disp = data['fut_traj_original_scale'].to(device) # [B,A,Tf,2] displacement
|
| 310 |
+
cur_xy = past_orig[:, :, -1, 0:2]
|
| 311 |
+
gt_center = gt_disp + cur_xy[:, :, None, :]
|
| 312 |
+
return feats, cur_xy, gt_disp, gt_center
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
# ============================================================================
|
| 316 |
+
# EVAL (marginal min-of-K=20 at 4.0 s; identical to eval_perscene_moflow.py)
|
| 317 |
+
# ============================================================================
|
| 318 |
+
@torch.no_grad()
|
| 319 |
+
def evaluate(model, loader, device, end):
|
| 320 |
+
model.eval()
|
| 321 |
+
ade_sum = fde_sum = 0.0
|
| 322 |
+
n = 0
|
| 323 |
+
for data in loader:
|
| 324 |
+
feats, cur_xy, gt_disp, _ = unpack(data, device)
|
| 325 |
+
out = model(feats, cur_xy)
|
| 326 |
+
pred = out['fine'][..., :2] # [B,A,M,T,2] fine stage
|
| 327 |
+
pred_disp = pred - cur_xy[:, :, None, None, :]
|
| 328 |
+
d = torch.norm(pred_disp - gt_disp[:, :, None], dim=-1) # [B,A,M,T]
|
| 329 |
+
ade = d[..., :end].mean(-1).min(dim=-1).values # [B,A]
|
| 330 |
+
fde = d[..., end - 1].min(dim=-1).values # [B,A]
|
| 331 |
+
ade_sum += ade.sum().item()
|
| 332 |
+
fde_sum += fde.sum().item()
|
| 333 |
+
n += ade.numel()
|
| 334 |
+
return ade_sum / max(n, 1), fde_sum / max(n, 1)
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
# ============================================================================
|
| 338 |
+
# TRAIN
|
| 339 |
+
# ============================================================================
|
| 340 |
+
def parse_args():
|
| 341 |
+
p = argparse.ArgumentParser('Coarse-to-Fine standalone predictor for NBA')
|
| 342 |
+
p.add_argument('--data_dir', default='/mnt/jaewoo4tb/srtp/MoFlow/data/nba', type=str)
|
| 343 |
+
p.add_argument('--gpu', default='0', type=str)
|
| 344 |
+
p.add_argument('--exp', default='c2f_nba', type=str)
|
| 345 |
+
p.add_argument('--epochs', default=50, type=int)
|
| 346 |
+
p.add_argument('--batch_size', default=128, type=int)
|
| 347 |
+
p.add_argument('--test_batch', default=500, type=int)
|
| 348 |
+
p.add_argument('--workers', default=4, type=int)
|
| 349 |
+
p.add_argument('--seed', default=3407, type=int)
|
| 350 |
+
|
| 351 |
+
p.add_argument('--modalities', default=20, type=int, help='K modes (NBA min-ADE_20)')
|
| 352 |
+
p.add_argument('--stages', default=2, type=int, help='coarse->fine refinement stages')
|
| 353 |
+
p.add_argument('--encoder_layers', default=6, type=int)
|
| 354 |
+
p.add_argument('--hidden', default=256, type=int, help='fine-refiner GRU hidden')
|
| 355 |
+
p.add_argument('--agents', default=11, type=int)
|
| 356 |
+
p.add_argument('--past_len', default=10, type=int)
|
| 357 |
+
p.add_argument('--future_len', default=20, type=int)
|
| 358 |
+
p.add_argument('--ball_idx', default=10, type=int, help='-1 to disable type emb')
|
| 359 |
+
|
| 360 |
+
p.add_argument('--lr', default=1e-4, type=float)
|
| 361 |
+
p.add_argument('--weight_decay', default=1e-4, type=float)
|
| 362 |
+
p.add_argument('--grad_clip', default=5.0, type=float)
|
| 363 |
+
p.add_argument('--n_train', default=32500, type=int)
|
| 364 |
+
p.add_argument('--n_test', default=12500, type=int)
|
| 365 |
+
p.add_argument('--eval_every', default=2, type=int)
|
| 366 |
+
p.add_argument('--save_dir', default=os.path.join(
|
| 367 |
+
os.path.dirname(os.path.abspath(__file__)), 'c2f_nba_ckpt'), type=str)
|
| 368 |
+
return p.parse_args()
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def main():
|
| 372 |
+
args = parse_args()
|
| 373 |
+
torch.manual_seed(args.seed)
|
| 374 |
+
np.random.seed(args.seed)
|
| 375 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 376 |
+
|
| 377 |
+
train_loader, test_loader = build_loaders(args)
|
| 378 |
+
|
| 379 |
+
n_types = 2 if (0 <= args.ball_idx < args.agents) else 1
|
| 380 |
+
model = CoarseToFineNBA(
|
| 381 |
+
n_agents=args.agents, past_dim=6, future_len=args.future_len,
|
| 382 |
+
modalities=args.modalities, n_stages=args.stages, dim=D_MODEL, heads=N_HEADS,
|
| 383 |
+
enc_layers=args.encoder_layers, hidden=args.hidden, n_types=n_types,
|
| 384 |
+
ball_idx=args.ball_idx).to(device)
|
| 385 |
+
n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 386 |
+
print(f'[C2F-NBA] model params: {n_params/1e6:.2f} M | stages={args.stages} '
|
| 387 |
+
f'K={args.modalities} enc_layers={args.encoder_layers} device={device}', flush=True)
|
| 388 |
+
|
| 389 |
+
opt = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
|
| 390 |
+
milestones = sorted(set(int(args.epochs * f) for f in (0.5, 0.65, 0.8, 0.9)))
|
| 391 |
+
milestones = [m for m in milestones if 0 < m < args.epochs]
|
| 392 |
+
sched = torch.optim.lr_scheduler.MultiStepLR(opt, milestones=milestones, gamma=0.5)
|
| 393 |
+
|
| 394 |
+
end = args.future_len
|
| 395 |
+
metric_idx = sorted(set([max(0, args.future_len // 2 - 1), args.future_len - 1]))
|
| 396 |
+
|
| 397 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 398 |
+
best_ade = float('inf')
|
| 399 |
+
|
| 400 |
+
for epoch in range(args.epochs):
|
| 401 |
+
model.train()
|
| 402 |
+
t0 = time.time()
|
| 403 |
+
running = 0.0
|
| 404 |
+
nb = 0
|
| 405 |
+
for data in train_loader:
|
| 406 |
+
feats, cur_xy, _, gt_center = unpack(data, device)
|
| 407 |
+
out = model(feats, cur_xy)
|
| 408 |
+
loss = c2f_loss(out, gt_center, metric_idx)
|
| 409 |
+
opt.zero_grad()
|
| 410 |
+
loss.backward()
|
| 411 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 412 |
+
opt.step()
|
| 413 |
+
running += float(loss.item())
|
| 414 |
+
nb += 1
|
| 415 |
+
sched.step()
|
| 416 |
+
print(f'[C2F-NBA] epoch {epoch+1}/{args.epochs} loss={running/max(nb,1):.4f} '
|
| 417 |
+
f'lr={opt.param_groups[0]["lr"]:.2e} ({time.time()-t0:.1f}s)', flush=True)
|
| 418 |
+
|
| 419 |
+
if (epoch + 1) % args.eval_every == 0 or epoch == args.epochs - 1:
|
| 420 |
+
ade, fde = evaluate(model, test_loader, device, end)
|
| 421 |
+
print(f'[C2F-NBA] epoch {epoch+1} ADE4={ade:.4f} FDE4={fde:.4f}', flush=True)
|
| 422 |
+
if ade < best_ade:
|
| 423 |
+
best_ade = ade
|
| 424 |
+
torch.save({'model': model.state_dict(), 'epoch': epoch + 1,
|
| 425 |
+
'ade4': ade, 'fde4': fde, 'args': vars(args)},
|
| 426 |
+
os.path.join(args.save_dir, f'{args.exp}_best.pt'))
|
| 427 |
+
print(f'[C2F-NBA] saved best (ADE4={ade:.4f}) -> '
|
| 428 |
+
f'{os.path.join(args.save_dir, args.exp + "_best.pt")}', flush=True)
|
| 429 |
+
|
| 430 |
+
print(f'[C2F-NBA] done. best ADE4={best_ade:.4f}', flush=True)
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
if __name__ == '__main__':
|
| 434 |
+
main()
|
standalone/gameformer_nba_standalone.py
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
gameformer_nba_standalone.py
|
| 5 |
+
================================================================================
|
| 6 |
+
FAITHFUL standalone re-implementation of **GameFormer** (Liu et al., ICCV 2023,
|
| 7 |
+
"GameFormer: Game-theoretic Modeling and Learning of Transformer-based
|
| 8 |
+
Interactive Prediction", github.com/MCZhi/GameFormer) as a trajectory predictor
|
| 9 |
+
for the NBA basketball dataset.
|
| 10 |
+
|
| 11 |
+
It is a STANDALONE predictor: it consumes ONLY agent HISTORY (past trajectories)
|
| 12 |
+
and predicts K multi-modal futures via the level-k game-theoretic decoder. It
|
| 13 |
+
does NOT consume any external denoiser / diffusion prediction.
|
| 14 |
+
|
| 15 |
+
--------------------------------------------------------------------------------
|
| 16 |
+
WHAT WAS REPLICATED (evidence = MCZhi/GameFormer @ main)
|
| 17 |
+
--------------------------------------------------------------------------------
|
| 18 |
+
Reference files studied verbatim:
|
| 19 |
+
* model/GameFormer.py (Encoder, Decoder, GameFormer)
|
| 20 |
+
* model/modules.py (AgentEncoder, FutureEncoder, GMMPredictor,
|
| 21 |
+
SelfTransformer, CrossTransformer,
|
| 22 |
+
InitialDecoder, InteractionDecoder)
|
| 23 |
+
* utils/inter_pred_utils.py (level_k_loss, gmm_loss)
|
| 24 |
+
* interaction_prediction/train.py (AdamW 1e-4, MultiStepLR, grad-clip 5,
|
| 25 |
+
level=3, modalities=6, encoder_layers=6)
|
| 26 |
+
|
| 27 |
+
ENCODER (GameFormer.Encoder + modules.AgentEncoder)
|
| 28 |
+
* Per-agent temporal encoder = 2-layer LSTM, hidden 256 (nn.LSTM(8,256,2)).
|
| 29 |
+
We adapt the input feature dim 8 -> 6 (NBA has [abs xy, rel xy, vel xy], no
|
| 30 |
+
heading/size channels). A learned agent-type embedding (player vs. ball) is
|
| 31 |
+
added to the LSTM last-step output, exactly as GameFormer adds `type_emb`.
|
| 32 |
+
* Interaction / self-attention over agents = the GameFormer `fusion_encoder`,
|
| 33 |
+
a nn.TransformerEncoder of TransformerEncoderLayer(d_model=256, nhead=8,
|
| 34 |
+
dim_feedforward=1024, activation=gelu, dropout=0.1, batch_first=True),
|
| 35 |
+
`encoder_layers` deep. In GameFormer this fuses agent tokens WITH per-agent
|
| 36 |
+
map (lane/crosswalk) tokens; NBA has NO map, so the map encoders
|
| 37 |
+
(LaneEncoder / CrosswalkEncoder / segment_map) are DROPPED and the fusion is
|
| 38 |
+
pure agent<->agent self-attention over the 11 tokens (identical for every
|
| 39 |
+
target agent when there is no per-agent map, so it is computed once).
|
| 40 |
+
|
| 41 |
+
LEVEL-k GAME-THEORETIC DECODER (GameFormer.Decoder)
|
| 42 |
+
* Level 0 = InitialDecoder: for each agent, a set of `modalities` (=K) learned
|
| 43 |
+
modal query embeddings + a per-agent query embedding are added to the agent's
|
| 44 |
+
own context token, then cross-attended (CrossTransformer) to the full agent
|
| 45 |
+
context. A GMMPredictor head outputs K trajectories of (mu_x,mu_y,
|
| 46 |
+
log_sig_x,log_sig_y) per step + K mode scores. predictions[...,:2] += the
|
| 47 |
+
agent's current (last observed) position.
|
| 48 |
+
* Levels 1..L = InteractionDecoder (one per level, sharing ONE FutureEncoder):
|
| 49 |
+
- FutureEncoder encodes EVERY agent's K level-(k-1) modal futures into a
|
| 50 |
+
vector (max-pool over time of an MLP on [x,y,heading,vx,vy]); GameFormer
|
| 51 |
+
also appends box size (w,l,h) -> we drop those (no size in NBA), 8->5.
|
| 52 |
+
- The per-mode future encodings are score-softmax-weighted and mean-pooled
|
| 53 |
+
to one vector per agent, then passed through a SelfTransformer to produce
|
| 54 |
+
the interaction encoding over all agents (this is the game-theoretic
|
| 55 |
+
"condition on all other agents' level-(k-1) predictions" step).
|
| 56 |
+
- The interaction tokens are concatenated with the context tokens; each
|
| 57 |
+
agent's OWN future token is masked out; the previous-level content plus
|
| 58 |
+
the agent's own future encoding form the query, cross-attended to that
|
| 59 |
+
combined context, and a fresh GMMPredictor emits the level-k K futures.
|
| 60 |
+
* Output dict: level_0_interactions/scores ... level_L_interactions/scores,
|
| 61 |
+
exactly as GameFormer.Decoder.forward.
|
| 62 |
+
|
| 63 |
+
LOSS (utils.inter_pred_utils.level_k_loss + gmm_loss, gmm=True)
|
| 64 |
+
* Summed over ALL levels k=0..L (level-wise imitation, GameFormer's key term).
|
| 65 |
+
* Per level, GMM winner-takes-all: best mode = argmin over K of
|
| 66 |
+
(mean_t ||traj-gt|| + sum over checkpoint steps), then the Gaussian NLL
|
| 67 |
+
reg = (log_sig_x+log_sig_y) + 0.5*(dx^2/sx^2 + dy^2/sy^2) with log_sig
|
| 68 |
+
clipped to [0,5], reduced as mean_t + sum over checkpoints; plus mode
|
| 69 |
+
classification F.cross_entropy(scores, best_mode, label_smoothing=0.2)
|
| 70 |
+
weighted x2. We MARGINALIZE per agent (GameFormer's interaction_prediction
|
| 71 |
+
task predicts 2 agents JOINTLY via distance.mean(1)/scores.sum(1); the NBA
|
| 72 |
+
Table-1 metric is MARGINAL min-ADE_20, so the best mode / scores are taken
|
| 73 |
+
per agent instead of jointly -- the only loss adaptation).
|
| 74 |
+
|
| 75 |
+
--------------------------------------------------------------------------------
|
| 76 |
+
NBA I/O + METRIC (matched to MoFlow / Table 1)
|
| 77 |
+
--------------------------------------------------------------------------------
|
| 78 |
+
* Data: reuses MoFlow's exact loader data/dataloader_nba.py::NBADatasetMinMax
|
| 79 |
+
(the graph loader dataloader_nba_graph.py merely re-exports it). Same .npy
|
| 80 |
+
files (data/nba/original/nba_{train,test}.npy, 32500/12500 scenes,
|
| 81 |
+
30 frames x 11 agents x 2D), same split (trajs[:n_train] / trajs[:n_test]),
|
| 82 |
+
same coordinate scaling traj_scale_total=94/28 and traj_mean=[14,7.5].
|
| 83 |
+
* Past = 10 frames, Future = 20 frames = 4.0 s @ 5 Hz, 11 agents, 2D.
|
| 84 |
+
* Encoder input = data['past_traj'] (6-dim min-max normalized to [-1,1]).
|
| 85 |
+
* Coordinate frame for the GMM/interaction = data['past_traj_original_scale']
|
| 86 |
+
(centered-abs = pos/scale - traj_mean); cur_xy = its last past step.
|
| 87 |
+
Ground-truth future displacement = data['fut_traj_original_scale']
|
| 88 |
+
(= future - last_observed, in /scale units) -- the SAME tensor the MoFlow
|
| 89 |
+
evaluator (eval_perscene_moflow.py) compares against.
|
| 90 |
+
* Metric (identical to eval_perscene_moflow.py, verified vs Table 1):
|
| 91 |
+
d = ||pred_disp - gt_disp|| # [B,A,K,T]
|
| 92 |
+
ADE4 = d[..., :20].mean(over T).min(over K), mean over 11 agents & scenes
|
| 93 |
+
FDE4 = d[..., 19] .min(over K), mean over 11 agents & scenes
|
| 94 |
+
i.e. marginal min-of-K=20 at the 4.0 s horizon (frame 20). Table-1 baselines
|
| 95 |
+
are ~0.70-1.04 ADE in this space.
|
| 96 |
+
|
| 97 |
+
Prints: [GF-NBA] epoch N ADE4=.. FDE4=..
|
| 98 |
+
|
| 99 |
+
DO NOT run on GPU here (GPUs busy) -- this file is only `python -m py_compile`d.
|
| 100 |
+
"""
|
| 101 |
+
|
| 102 |
+
import os
|
| 103 |
+
import sys
|
| 104 |
+
import argparse
|
| 105 |
+
import time
|
| 106 |
+
|
| 107 |
+
# ------------------------------------------------------------------ GPU FIRST
|
| 108 |
+
# CUDA context is fixed at torch import; select the device BEFORE importing torch
|
| 109 |
+
def _early_gpu():
|
| 110 |
+
for i, a in enumerate(sys.argv):
|
| 111 |
+
if a == '--gpu' and i + 1 < len(sys.argv):
|
| 112 |
+
return sys.argv[i + 1]
|
| 113 |
+
if a.startswith('--gpu='):
|
| 114 |
+
return a.split('=', 1)[1]
|
| 115 |
+
return None
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
_g = _early_gpu()
|
| 119 |
+
if _g is not None:
|
| 120 |
+
os.environ['CUDA_VISIBLE_DEVICES'] = str(_g)
|
| 121 |
+
os.environ.setdefault('MPLBACKEND', 'Agg') # dataloader imports matplotlib
|
| 122 |
+
|
| 123 |
+
# ------------------------------------------------ reuse MoFlow's NBA pipeline
|
| 124 |
+
MOFLOW_ROOT = '/mnt/jaewoo4tb/srtp/MoFlow'
|
| 125 |
+
if MOFLOW_ROOT not in sys.path:
|
| 126 |
+
sys.path.insert(0, MOFLOW_ROOT)
|
| 127 |
+
|
| 128 |
+
import math
|
| 129 |
+
import types as _types
|
| 130 |
+
import numpy as np
|
| 131 |
+
import torch
|
| 132 |
+
import torch.nn as nn
|
| 133 |
+
import torch.nn.functional as F
|
| 134 |
+
from torch.utils.data import DataLoader
|
| 135 |
+
|
| 136 |
+
# so that `from utils.normalization import ...` inside the loader resolves
|
| 137 |
+
try:
|
| 138 |
+
os.chdir(MOFLOW_ROOT)
|
| 139 |
+
except Exception:
|
| 140 |
+
pass
|
| 141 |
+
from data.dataloader_nba import NBADatasetMinMax, seq_collate_nba
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# ============================================================================
|
| 145 |
+
# MODEL (faithful GameFormer, NBA-adapted: no map)
|
| 146 |
+
# ============================================================================
|
| 147 |
+
D_MODEL = 256
|
| 148 |
+
N_HEADS = 8
|
| 149 |
+
DROPOUT = 0.1
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# ---------------------------------------------------------------- primitives
|
| 153 |
+
class SelfTransformer(nn.Module):
|
| 154 |
+
"""modules.py::SelfTransformer (verbatim)."""
|
| 155 |
+
|
| 156 |
+
def __init__(self, dim=D_MODEL, heads=N_HEADS, dropout=DROPOUT):
|
| 157 |
+
super().__init__()
|
| 158 |
+
self.self_attention = nn.MultiheadAttention(dim, heads, dropout, batch_first=True)
|
| 159 |
+
self.norm_1 = nn.LayerNorm(dim)
|
| 160 |
+
self.norm_2 = nn.LayerNorm(dim)
|
| 161 |
+
self.ffn = nn.Sequential(
|
| 162 |
+
nn.Linear(dim, dim * 4), nn.GELU(), nn.Dropout(dropout),
|
| 163 |
+
nn.Linear(dim * 4, dim), nn.Dropout(dropout))
|
| 164 |
+
|
| 165 |
+
def forward(self, inputs, mask=None):
|
| 166 |
+
attn, _ = self.self_attention(inputs, inputs, inputs, key_padding_mask=mask)
|
| 167 |
+
attn = self.norm_1(attn + inputs)
|
| 168 |
+
return self.norm_2(self.ffn(attn) + attn)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
class CrossTransformer(nn.Module):
|
| 172 |
+
"""modules.py::CrossTransformer (verbatim)."""
|
| 173 |
+
|
| 174 |
+
def __init__(self, dim=D_MODEL, heads=N_HEADS, dropout=DROPOUT):
|
| 175 |
+
super().__init__()
|
| 176 |
+
self.cross_attention = nn.MultiheadAttention(dim, heads, dropout, batch_first=True)
|
| 177 |
+
self.norm_1 = nn.LayerNorm(dim)
|
| 178 |
+
self.norm_2 = nn.LayerNorm(dim)
|
| 179 |
+
self.ffn = nn.Sequential(
|
| 180 |
+
nn.Linear(dim, dim * 4), nn.GELU(), nn.Dropout(dropout),
|
| 181 |
+
nn.Linear(dim * 4, dim), nn.Dropout(dropout))
|
| 182 |
+
|
| 183 |
+
def forward(self, query, key, value, mask=None):
|
| 184 |
+
attn, _ = self.cross_attention(query, key, value, key_padding_mask=mask)
|
| 185 |
+
attn = self.norm_1(attn)
|
| 186 |
+
return self.norm_2(self.ffn(attn) + attn)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
class GMMPredictor(nn.Module):
|
| 190 |
+
"""modules.py::GMMPredictor (verbatim). Out: (mu_x,mu_y,log_sig_x,log_sig_y)."""
|
| 191 |
+
|
| 192 |
+
def __init__(self, future_len, dim=D_MODEL):
|
| 193 |
+
super().__init__()
|
| 194 |
+
self._future_len = future_len
|
| 195 |
+
self.gaussian = nn.Sequential(
|
| 196 |
+
nn.Linear(dim, 512), nn.ELU(), nn.Dropout(0.1),
|
| 197 |
+
nn.Linear(512, future_len * 4))
|
| 198 |
+
self.score = nn.Sequential(
|
| 199 |
+
nn.Linear(dim, 64), nn.ELU(), nn.Dropout(0.1), nn.Linear(64, 1))
|
| 200 |
+
|
| 201 |
+
def forward(self, x):
|
| 202 |
+
# x: [B, M, D]
|
| 203 |
+
B, M, _ = x.shape
|
| 204 |
+
res = self.gaussian(x).view(B, M, self._future_len, 4)
|
| 205 |
+
score = self.score(x).squeeze(-1) # [B, M]
|
| 206 |
+
return res, score
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
# ------------------------------------------------------------------ encoder
|
| 210 |
+
class AgentHistoryEncoder(nn.Module):
|
| 211 |
+
"""modules.py::AgentEncoder -- 2-layer LSTM(->256) over agent history + type
|
| 212 |
+
embedding. GameFormer uses nn.LSTM(8,256,2); NBA feature dim is 6."""
|
| 213 |
+
|
| 214 |
+
def __init__(self, in_dim=6, dim=D_MODEL, n_types=2):
|
| 215 |
+
super().__init__()
|
| 216 |
+
self.motion = nn.LSTM(in_dim, dim, 2, batch_first=True)
|
| 217 |
+
self.type_emb = nn.Embedding(n_types, dim)
|
| 218 |
+
|
| 219 |
+
def forward(self, hist, types):
|
| 220 |
+
# hist: [B, A, T, in_dim] ; types: [A] long
|
| 221 |
+
B, A, T, C = hist.shape
|
| 222 |
+
traj, _ = self.motion(hist.reshape(B * A, T, C))
|
| 223 |
+
out = traj[:, -1].reshape(B, A, -1) # [B, A, D]
|
| 224 |
+
out = out + self.type_emb(types)[None] # + per-agent type emb
|
| 225 |
+
return out
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
class FusionEncoder(nn.Module):
|
| 229 |
+
"""GameFormer.Encoder.fusion_encoder -- self-attention over agent tokens.
|
| 230 |
+
(No map tokens for NBA.)"""
|
| 231 |
+
|
| 232 |
+
def __init__(self, dim=D_MODEL, heads=N_HEADS, layers=6, dropout=DROPOUT):
|
| 233 |
+
super().__init__()
|
| 234 |
+
layer = nn.TransformerEncoderLayer(
|
| 235 |
+
d_model=dim, nhead=heads, dim_feedforward=dim * 4,
|
| 236 |
+
activation=F.gelu, dropout=dropout, batch_first=True)
|
| 237 |
+
self.encoder = nn.TransformerEncoder(layer, layers, enable_nested_tensor=False)
|
| 238 |
+
|
| 239 |
+
def forward(self, tokens, mask=None):
|
| 240 |
+
return self.encoder(tokens, src_key_padding_mask=mask)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
# ------------------------------------------------------------- future encoder
|
| 244 |
+
class FutureEncoder(nn.Module):
|
| 245 |
+
"""modules.py::FutureEncoder -- encodes each agent's K modal futures into a
|
| 246 |
+
per-mode vector via max-pool over an MLP on [x,y,heading,vx,vy]. GameFormer
|
| 247 |
+
appends box size (w,l,h) -> dropped for NBA (8 -> 5 input dims)."""
|
| 248 |
+
|
| 249 |
+
def __init__(self, dim=D_MODEL, n_types=2, dt=0.2):
|
| 250 |
+
super().__init__()
|
| 251 |
+
self.dt = dt
|
| 252 |
+
self.mlp = nn.Sequential(nn.Linear(5, 64), nn.ReLU(), nn.Linear(64, dim))
|
| 253 |
+
self.type_emb = nn.Embedding(n_types, dim)
|
| 254 |
+
|
| 255 |
+
def state_process(self, trajs, cur_xy):
|
| 256 |
+
# trajs: [B, A, M, T, 2] ; cur_xy: [B, A, 2]
|
| 257 |
+
B, A, M, T, _ = trajs.shape
|
| 258 |
+
cur = cur_xy[:, :, None, None, :].expand(B, A, M, 1, 2)
|
| 259 |
+
xy = torch.cat([cur, trajs], dim=-2) # [B,A,M,T+1,2]
|
| 260 |
+
dxy = torch.diff(xy, dim=-2) # [B,A,M,T,2]
|
| 261 |
+
v = dxy / self.dt
|
| 262 |
+
theta = torch.atan2(dxy[..., 1], dxy[..., 0].clamp(min=1e-3)).unsqueeze(-1)
|
| 263 |
+
return torch.cat([trajs, theta, v], dim=-1) # [B,A,M,T,5]
|
| 264 |
+
|
| 265 |
+
def forward(self, trajs, cur_xy, types):
|
| 266 |
+
feat = self.state_process(trajs, cur_xy)
|
| 267 |
+
h = self.mlp(feat.detach()) # GameFormer detaches
|
| 268 |
+
out = torch.max(h, dim=-2).values # [B,A,M,D]
|
| 269 |
+
out = out + self.type_emb(types)[None, :, None, :] # + type emb
|
| 270 |
+
return out
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
# ----------------------------------------------------------- level-0 decoder
|
| 274 |
+
class InitialDecoder(nn.Module):
|
| 275 |
+
"""modules.py::InitialDecoder -- vectorized over agents."""
|
| 276 |
+
|
| 277 |
+
def __init__(self, modalities, n_agents, future_len, dim=D_MODEL):
|
| 278 |
+
super().__init__()
|
| 279 |
+
self.M = modalities
|
| 280 |
+
self.A = n_agents
|
| 281 |
+
self.multi_modal_query_embedding = nn.Embedding(modalities, dim)
|
| 282 |
+
self.agent_query_embedding = nn.Embedding(n_agents, dim)
|
| 283 |
+
self.query_encoder = CrossTransformer(dim)
|
| 284 |
+
self.predictor = GMMPredictor(future_len, dim)
|
| 285 |
+
self.register_buffer('modal', torch.arange(modalities).long())
|
| 286 |
+
self.register_buffer('agent', torch.arange(n_agents).long())
|
| 287 |
+
|
| 288 |
+
def forward(self, encoding, cur_xy, mask=None):
|
| 289 |
+
# encoding: [B, A, D] (full agent context, shared as keys)
|
| 290 |
+
B, A, D = encoding.shape
|
| 291 |
+
M = self.M
|
| 292 |
+
multi_modal = self.multi_modal_query_embedding(self.modal) # [M, D]
|
| 293 |
+
agent = self.agent_query_embedding(self.agent) # [A, D]
|
| 294 |
+
mm_agent_query = multi_modal[None, :, :] + agent[:, None, :] # [A, M, D]
|
| 295 |
+
query = encoding[:, :, None, :] + mm_agent_query[None] # [B, A, M, D]
|
| 296 |
+
|
| 297 |
+
q = query.reshape(B * A, M, D)
|
| 298 |
+
kv = encoding[:, None, :, :].expand(B, A, A, D).reshape(B * A, A, D)
|
| 299 |
+
km = None
|
| 300 |
+
if mask is not None:
|
| 301 |
+
km = mask[:, None, :].expand(B, A, A).reshape(B * A, A)
|
| 302 |
+
content = self.query_encoder(q, kv, kv, km) # [B*A, M, D]
|
| 303 |
+
preds, scores = self.predictor(content) # [B*A,M,T,4],[B*A,M]
|
| 304 |
+
|
| 305 |
+
content = content.reshape(B, A, M, D)
|
| 306 |
+
T = preds.shape[-2]
|
| 307 |
+
preds = preds.reshape(B, A, M, T, 4)
|
| 308 |
+
scores = scores.reshape(B, A, M)
|
| 309 |
+
preds = preds.clone()
|
| 310 |
+
preds[..., :2] = preds[..., :2] + cur_xy[:, :, None, None, :]
|
| 311 |
+
return content, preds, scores
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
# ------------------------------------------------------- level-k decoder
|
| 315 |
+
class InteractionDecoder(nn.Module):
|
| 316 |
+
"""modules.py::InteractionDecoder -- one game-theoretic reasoning level,
|
| 317 |
+
vectorized over agents. Conditions each agent on ALL agents' level-(k-1)
|
| 318 |
+
predictions (interaction self-attention), masking the agent's own future."""
|
| 319 |
+
|
| 320 |
+
def __init__(self, future_encoder, future_len, n_agents, dim=D_MODEL):
|
| 321 |
+
super().__init__()
|
| 322 |
+
self.A = n_agents
|
| 323 |
+
self.future_encoder = future_encoder # SHARED across levels
|
| 324 |
+
self.interaction_encoder = SelfTransformer(dim)
|
| 325 |
+
self.query_encoder = CrossTransformer(dim)
|
| 326 |
+
self.decoder = GMMPredictor(future_len, dim)
|
| 327 |
+
|
| 328 |
+
def forward(self, cur_xy, last_traj, last_scores, last_content, encoding, types, mask=None):
|
| 329 |
+
# cur_xy:[B,A,2] last_traj:[B,A,M,T,4] last_scores:[B,A,M]
|
| 330 |
+
# last_content:[B,A,M,D] encoding:[B,A,D]
|
| 331 |
+
B, A, M, T, _ = last_traj.shape
|
| 332 |
+
D = encoding.shape[-1]
|
| 333 |
+
|
| 334 |
+
# encode all agents' level-(k-1) modal futures
|
| 335 |
+
multi_futures = self.future_encoder(last_traj[..., :2], cur_xy, types) # [B,A,M,D]
|
| 336 |
+
futures = (multi_futures * last_scores.softmax(-1).unsqueeze(-1)).mean(dim=2) # [B,A,D]
|
| 337 |
+
|
| 338 |
+
# interaction over agents (self-attention)
|
| 339 |
+
interaction = self.interaction_encoder(futures, None) # [B,A,D]
|
| 340 |
+
|
| 341 |
+
# combined context = [interaction tokens ; context tokens]
|
| 342 |
+
ctx = torch.cat([interaction, encoding], dim=1) # [B, 2A, D]
|
| 343 |
+
# per-agent key mask: mask each agent's OWN future token in the 1st block
|
| 344 |
+
km = torch.zeros(B, A, 2 * A, dtype=torch.bool, device=encoding.device)
|
| 345 |
+
idx = torch.arange(A, device=encoding.device)
|
| 346 |
+
km[:, idx, idx] = True
|
| 347 |
+
if mask is not None:
|
| 348 |
+
# second block inherits context validity mask (all-valid for NBA)
|
| 349 |
+
km[:, :, A:] = km[:, :, A:] | mask[:, None, :]
|
| 350 |
+
km[:, :, :A] = km[:, :, :A] | mask[:, None, :]
|
| 351 |
+
km = km.reshape(B * A, 2 * A)
|
| 352 |
+
kv = ctx[:, None, :, :].expand(B, A, 2 * A, D).reshape(B * A, 2 * A, D)
|
| 353 |
+
|
| 354 |
+
query = (last_content + multi_futures).reshape(B * A, M, D) # prev content + own future
|
| 355 |
+
content = self.query_encoder(query, kv, kv, km) # [B*A, M, D]
|
| 356 |
+
traj, scores = self.decoder(content) # [B*A,M,T,4],[B*A,M]
|
| 357 |
+
|
| 358 |
+
content = content.reshape(B, A, M, D)
|
| 359 |
+
traj = traj.reshape(B, A, M, T, 4)
|
| 360 |
+
scores = scores.reshape(B, A, M)
|
| 361 |
+
traj = traj.clone()
|
| 362 |
+
traj[..., :2] = traj[..., :2] + cur_xy[:, :, None, None, :]
|
| 363 |
+
return content, traj, scores
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
# -------------------------------------------------------------- full decoder
|
| 367 |
+
class GameFormerDecoder(nn.Module):
|
| 368 |
+
"""GameFormer.Decoder -- level 0 + L interaction levels (shared FutureEncoder)."""
|
| 369 |
+
|
| 370 |
+
def __init__(self, modalities, n_agents, future_len, levels, dim=D_MODEL,
|
| 371 |
+
n_types=2, dt=0.2):
|
| 372 |
+
super().__init__()
|
| 373 |
+
self._levels = levels
|
| 374 |
+
self.future_encoder = FutureEncoder(dim, n_types, dt)
|
| 375 |
+
self.initial_stage = InitialDecoder(modalities, n_agents, future_len, dim)
|
| 376 |
+
self.interaction_stage = nn.ModuleList(
|
| 377 |
+
[InteractionDecoder(self.future_encoder, future_len, n_agents, dim)
|
| 378 |
+
for _ in range(levels)])
|
| 379 |
+
|
| 380 |
+
def forward(self, encoding, cur_xy, types, mask=None):
|
| 381 |
+
out = {}
|
| 382 |
+
content, traj, scores = self.initial_stage(encoding, cur_xy, mask)
|
| 383 |
+
out['level_0_interactions'] = traj
|
| 384 |
+
out['level_0_scores'] = scores
|
| 385 |
+
for k in range(1, self._levels + 1):
|
| 386 |
+
content, traj, scores = self.interaction_stage[k - 1](
|
| 387 |
+
cur_xy, traj, scores, content, encoding, types, mask)
|
| 388 |
+
out[f'level_{k}_interactions'] = traj
|
| 389 |
+
out[f'level_{k}_scores'] = scores
|
| 390 |
+
return out
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
# ---------------------------------------------------------------- top model
|
| 394 |
+
class GameFormerNBA(nn.Module):
|
| 395 |
+
"""GameFormer.GameFormer (NBA, map dropped)."""
|
| 396 |
+
|
| 397 |
+
def __init__(self, n_agents=11, past_dim=6, future_len=20, modalities=20,
|
| 398 |
+
levels=2, dim=D_MODEL, heads=N_HEADS, enc_layers=6,
|
| 399 |
+
n_types=2, dt=0.2, ball_idx=10):
|
| 400 |
+
super().__init__()
|
| 401 |
+
self.levels = levels
|
| 402 |
+
self.history_encoder = AgentHistoryEncoder(past_dim, dim, n_types)
|
| 403 |
+
self.fusion_encoder = FusionEncoder(dim, heads, enc_layers)
|
| 404 |
+
self.decoder = GameFormerDecoder(modalities, n_agents, future_len, levels,
|
| 405 |
+
dim, n_types, dt)
|
| 406 |
+
types = torch.zeros(n_agents, dtype=torch.long)
|
| 407 |
+
if 0 <= ball_idx < n_agents and n_types > 1:
|
| 408 |
+
types[ball_idx] = 1
|
| 409 |
+
self.register_buffer('agent_types', types)
|
| 410 |
+
|
| 411 |
+
def forward(self, feats, cur_xy):
|
| 412 |
+
# feats: [B,A,T,past_dim] (encoder input) ; cur_xy: [B,A,2] (metric frame)
|
| 413 |
+
enc = self.history_encoder(feats, self.agent_types) # [B,A,D]
|
| 414 |
+
enc = self.fusion_encoder(enc, None) # [B,A,D]
|
| 415 |
+
return self.decoder(enc, cur_xy, self.agent_types, None)
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
# ============================================================================
|
| 419 |
+
# LOSS (level_k_loss + gmm_loss, gmm=True; MARGINAL per agent for NBA)
|
| 420 |
+
# ============================================================================
|
| 421 |
+
def gmm_loss_marginal(traj, conv, scores, gt, metric_idx):
|
| 422 |
+
"""Per-agent GMM winner-takes-all (marginal version of gmm_loss).
|
| 423 |
+
traj: [B,A,M,T,2] conv: [B,A,M,T,2] scores: [B,A,M] gt: [B,A,T,2]
|
| 424 |
+
"""
|
| 425 |
+
B, A, M, T, _ = traj.shape
|
| 426 |
+
dist = torch.norm(traj - gt[:, :, None], dim=-1) # [B,A,M,T]
|
| 427 |
+
ndist = dist.mean(-1) + dist[..., metric_idx].sum(-1) # [B,A,M]
|
| 428 |
+
best = ndist.argmin(-1) # [B,A]
|
| 429 |
+
|
| 430 |
+
gi = best[..., None, None, None].expand(B, A, 1, T, 2)
|
| 431 |
+
best_traj = torch.gather(traj, 2, gi).squeeze(2) # [B,A,T,2]
|
| 432 |
+
best_conv = torch.gather(conv, 2, gi).squeeze(2) # [B,A,T,2]
|
| 433 |
+
|
| 434 |
+
dx = best_traj[..., 0] - gt[..., 0]
|
| 435 |
+
dy = best_traj[..., 1] - gt[..., 1]
|
| 436 |
+
log_std_x = torch.clip(best_conv[..., 0], 0, 5)
|
| 437 |
+
log_std_y = torch.clip(best_conv[..., 1], 0, 5)
|
| 438 |
+
std_x, std_y = torch.exp(log_std_x), torch.exp(log_std_y)
|
| 439 |
+
|
| 440 |
+
reg = (log_std_x + log_std_y) + 0.5 * ((dx ** 2) / (std_x ** 2) + (dy ** 2) / (std_y ** 2))
|
| 441 |
+
reg = reg.mean(-1) + reg[..., metric_idx].sum(-1) # [B,A]
|
| 442 |
+
|
| 443 |
+
prob_loss = F.cross_entropy(scores.reshape(B * A, M), best.reshape(B * A),
|
| 444 |
+
label_smoothing=0.2)
|
| 445 |
+
return reg.mean() + 2.0 * prob_loss, best
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
def level_k_loss_marginal(outputs, gt_center, levels, metric_idx):
|
| 449 |
+
"""Sum GMM loss across all levels 0..L (GameFormer's level-wise imitation)."""
|
| 450 |
+
total = 0.0
|
| 451 |
+
for k in range(levels + 1):
|
| 452 |
+
pred = outputs[f'level_{k}_interactions'] # [B,A,M,T,4]
|
| 453 |
+
scores = outputs[f'level_{k}_scores'] # [B,A,M]
|
| 454 |
+
traj, conv = pred[..., :2], pred[..., 2:]
|
| 455 |
+
l, _ = gmm_loss_marginal(traj, conv, scores, gt_center, metric_idx)
|
| 456 |
+
total = total + l
|
| 457 |
+
return total
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
# ============================================================================
|
| 461 |
+
# DATA
|
| 462 |
+
# ============================================================================
|
| 463 |
+
def build_loaders(args):
|
| 464 |
+
cfg = _types.SimpleNamespace(
|
| 465 |
+
traj_mean=[14, 7.5], data_norm='min_max',
|
| 466 |
+
past_frames=args.past_len, future_frames=args.future_len, agents=args.agents)
|
| 467 |
+
# Train first so cfg min/max scalars are populated for the test set.
|
| 468 |
+
train_set = NBADatasetMinMax(
|
| 469 |
+
obs_len=args.past_len, pred_len=args.future_len, training=True,
|
| 470 |
+
num_scenes=args.n_train, cfg=cfg, data_dir=args.data_dir,
|
| 471 |
+
rotate=False, data_norm='min_max')
|
| 472 |
+
test_set = NBADatasetMinMax(
|
| 473 |
+
obs_len=args.past_len, pred_len=args.future_len, training=False,
|
| 474 |
+
test_scenes=args.n_test, cfg=cfg, data_dir=args.data_dir,
|
| 475 |
+
rotate=False, data_norm='min_max')
|
| 476 |
+
train_loader = DataLoader(
|
| 477 |
+
train_set, batch_size=args.batch_size, shuffle=True, num_workers=args.workers,
|
| 478 |
+
collate_fn=seq_collate_nba, pin_memory=True, drop_last=True)
|
| 479 |
+
test_loader = DataLoader(
|
| 480 |
+
test_set, batch_size=args.test_batch, shuffle=False, num_workers=args.workers,
|
| 481 |
+
collate_fn=seq_collate_nba, pin_memory=True)
|
| 482 |
+
return train_loader, test_loader
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
def unpack(data, device):
|
| 486 |
+
feats = data['past_traj'].to(device) # [B,A,T,6] normalized
|
| 487 |
+
past_orig = data['past_traj_original_scale'].to(device) # [B,A,T,6] metric
|
| 488 |
+
gt_disp = data['fut_traj_original_scale'].to(device) # [B,A,Tf,2] displacement
|
| 489 |
+
cur_xy = past_orig[:, :, -1, 0:2] # centered-abs last pos
|
| 490 |
+
gt_center = gt_disp + cur_xy[:, :, None, :] # centered-abs future
|
| 491 |
+
return feats, cur_xy, gt_disp, gt_center
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
# ============================================================================
|
| 495 |
+
# EVAL (marginal min-of-K=20 at 4.0 s; identical to eval_perscene_moflow.py)
|
| 496 |
+
# ============================================================================
|
| 497 |
+
@torch.no_grad()
|
| 498 |
+
def evaluate(model, loader, device, levels, end):
|
| 499 |
+
model.eval()
|
| 500 |
+
ade_sum = fde_sum = 0.0
|
| 501 |
+
n = 0
|
| 502 |
+
for data in loader:
|
| 503 |
+
feats, cur_xy, gt_disp, _ = unpack(data, device)
|
| 504 |
+
out = model(feats, cur_xy)
|
| 505 |
+
pred = out[f'level_{levels}_interactions'][..., :2] # [B,A,M,T,2]
|
| 506 |
+
pred_disp = pred - cur_xy[:, :, None, None, :] # -> displacement
|
| 507 |
+
d = torch.norm(pred_disp - gt_disp[:, :, None], dim=-1) # [B,A,M,T]
|
| 508 |
+
ade = d[..., :end].mean(-1).min(dim=-1).values # [B,A] best-of-K
|
| 509 |
+
fde = d[..., end - 1].min(dim=-1).values # [B,A]
|
| 510 |
+
ade_sum += ade.sum().item()
|
| 511 |
+
fde_sum += fde.sum().item()
|
| 512 |
+
n += ade.numel()
|
| 513 |
+
return ade_sum / max(n, 1), fde_sum / max(n, 1)
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
# ============================================================================
|
| 517 |
+
# TRAIN
|
| 518 |
+
# ============================================================================
|
| 519 |
+
def parse_args():
|
| 520 |
+
p = argparse.ArgumentParser('GameFormer standalone predictor for NBA')
|
| 521 |
+
p.add_argument('--data_dir', default='/mnt/jaewoo4tb/srtp/MoFlow/data/nba', type=str)
|
| 522 |
+
p.add_argument('--gpu', default='0', type=str)
|
| 523 |
+
p.add_argument('--exp', default='gf_nba', type=str)
|
| 524 |
+
p.add_argument('--epochs', default=50, type=int)
|
| 525 |
+
p.add_argument('--batch_size', default=128, type=int)
|
| 526 |
+
p.add_argument('--test_batch', default=500, type=int)
|
| 527 |
+
p.add_argument('--workers', default=4, type=int)
|
| 528 |
+
p.add_argument('--seed', default=3407, type=int) # GameFormer default
|
| 529 |
+
|
| 530 |
+
# --- architecture (faithful GameFormer knobs) ---
|
| 531 |
+
p.add_argument('--modalities', default=20, type=int, help='K modes (NBA min-ADE_20)')
|
| 532 |
+
p.add_argument('--level', default=2, type=int, help='game-theoretic levels (paper: 3)')
|
| 533 |
+
p.add_argument('--encoder_layers', default=6, type=int)
|
| 534 |
+
p.add_argument('--agents', default=11, type=int)
|
| 535 |
+
p.add_argument('--past_len', default=10, type=int)
|
| 536 |
+
p.add_argument('--future_len', default=20, type=int) # 4.0 s @ 5 Hz
|
| 537 |
+
p.add_argument('--ball_idx', default=10, type=int, help='-1 to disable type emb')
|
| 538 |
+
|
| 539 |
+
# --- optimization (GameFormer: AdamW 1e-4, MultiStepLR x0.5, clip 5) ---
|
| 540 |
+
p.add_argument('--lr', default=1e-4, type=float)
|
| 541 |
+
p.add_argument('--weight_decay', default=1e-4, type=float)
|
| 542 |
+
p.add_argument('--grad_clip', default=5.0, type=float)
|
| 543 |
+
p.add_argument('--n_train', default=32500, type=int)
|
| 544 |
+
p.add_argument('--n_test', default=12500, type=int)
|
| 545 |
+
p.add_argument('--eval_every', default=2, type=int)
|
| 546 |
+
p.add_argument('--save_dir', default=os.path.join(
|
| 547 |
+
os.path.dirname(os.path.abspath(__file__)), 'gf_nba_ckpt'), type=str)
|
| 548 |
+
return p.parse_args()
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
def main():
|
| 552 |
+
args = parse_args()
|
| 553 |
+
torch.manual_seed(args.seed)
|
| 554 |
+
np.random.seed(args.seed)
|
| 555 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 556 |
+
|
| 557 |
+
train_loader, test_loader = build_loaders(args)
|
| 558 |
+
|
| 559 |
+
n_types = 2 if (0 <= args.ball_idx < args.agents) else 1
|
| 560 |
+
model = GameFormerNBA(
|
| 561 |
+
n_agents=args.agents, past_dim=6, future_len=args.future_len,
|
| 562 |
+
modalities=args.modalities, levels=args.level, dim=D_MODEL, heads=N_HEADS,
|
| 563 |
+
enc_layers=args.encoder_layers, n_types=n_types, dt=0.2,
|
| 564 |
+
ball_idx=args.ball_idx).to(device)
|
| 565 |
+
n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 566 |
+
print(f'[GF-NBA] model params: {n_params/1e6:.2f} M | levels={args.level} '
|
| 567 |
+
f'K={args.modalities} enc_layers={args.encoder_layers} device={device}')
|
| 568 |
+
|
| 569 |
+
opt = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
|
| 570 |
+
milestones = sorted(set(int(args.epochs * f) for f in (0.5, 0.65, 0.8, 0.9)))
|
| 571 |
+
milestones = [m for m in milestones if 0 < m < args.epochs]
|
| 572 |
+
sched = torch.optim.lr_scheduler.MultiStepLR(opt, milestones=milestones, gamma=0.5)
|
| 573 |
+
|
| 574 |
+
end = args.future_len # 4.0 s = frame 20
|
| 575 |
+
metric_idx = sorted(set([max(0, args.future_len // 2 - 1), args.future_len - 1]))
|
| 576 |
+
|
| 577 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 578 |
+
best_ade = float('inf')
|
| 579 |
+
|
| 580 |
+
for epoch in range(args.epochs):
|
| 581 |
+
model.train()
|
| 582 |
+
t0 = time.time()
|
| 583 |
+
running = 0.0
|
| 584 |
+
nb = 0
|
| 585 |
+
for data in train_loader:
|
| 586 |
+
feats, cur_xy, _, gt_center = unpack(data, device)
|
| 587 |
+
out = model(feats, cur_xy)
|
| 588 |
+
loss = level_k_loss_marginal(out, gt_center, args.level, metric_idx)
|
| 589 |
+
opt.zero_grad()
|
| 590 |
+
loss.backward()
|
| 591 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 592 |
+
opt.step()
|
| 593 |
+
running += float(loss.item())
|
| 594 |
+
nb += 1
|
| 595 |
+
sched.step()
|
| 596 |
+
print(f'[GF-NBA] epoch {epoch+1}/{args.epochs} loss={running/max(nb,1):.4f} '
|
| 597 |
+
f'lr={opt.param_groups[0]["lr"]:.2e} ({time.time()-t0:.1f}s)', flush=True)
|
| 598 |
+
|
| 599 |
+
if (epoch + 1) % args.eval_every == 0 or epoch == args.epochs - 1:
|
| 600 |
+
ade, fde = evaluate(model, test_loader, device, args.level, end)
|
| 601 |
+
print(f'[GF-NBA] epoch {epoch+1} ADE4={ade:.4f} FDE4={fde:.4f}', flush=True)
|
| 602 |
+
if ade < best_ade:
|
| 603 |
+
best_ade = ade
|
| 604 |
+
torch.save({'model': model.state_dict(), 'epoch': epoch + 1,
|
| 605 |
+
'ade4': ade, 'fde4': fde, 'args': vars(args)},
|
| 606 |
+
os.path.join(args.save_dir, f'{args.exp}_best.pt'))
|
| 607 |
+
print(f'[GF-NBA] saved best (ADE4={ade:.4f}) -> '
|
| 608 |
+
f'{os.path.join(args.save_dir, args.exp + "_best.pt")}', flush=True)
|
| 609 |
+
|
| 610 |
+
print(f'[GF-NBA] done. best ADE4={best_ade:.4f}', flush=True)
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
if __name__ == '__main__':
|
| 614 |
+
main()
|
standalone/gameformer_sra_nba_standalone.py
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
gameformer_sra_nba_standalone.py
|
| 5 |
+
================================================================================
|
| 6 |
+
SRA-AUGMENTED standalone GameFormer for NBA. Base = the faithful standalone
|
| 7 |
+
GameFormer (Liu et al., ICCV 2023); this file INJECTS SRA's three philosophies
|
| 8 |
+
into GameFormer's level-k InteractionDecoder to test whether the SRA idea
|
| 9 |
+
generalizes to a NON-diffusion (feedforward) predictor.
|
| 10 |
+
|
| 11 |
+
SRA components injected (env-gated; GF_SRA=1 enables all):
|
| 12 |
+
1. NEIGHBOR SELECTION (GF_SRA_TOPN) -- sparse RAG top-N (=GF_TOPN, default 5)
|
| 13 |
+
neighbor selection from future proximity; replaces GameFormer's DENSE
|
| 14 |
+
agent<->agent interaction with a sparse one (self-attn attn_mask + cross-
|
| 15 |
+
attn key mask restricted to each agent's top-N neighbors).
|
| 16 |
+
2. UNCERTAINTY GATING (GF_SRA_SIGMA) -- turns the per-step GMM log-sigma
|
| 17 |
+
(which GameFormer emits but never USES for interaction) into a learned
|
| 18 |
+
message gate in (0,1): uncertain agents send weaker interaction messages
|
| 19 |
+
(uncertainty-weighted message passing). Also biases neighbor selection
|
| 20 |
+
toward confident agents (GF_SIG_SEL_W).
|
| 21 |
+
3. RELATIONAL FUTURE FEATURES (GF_SRA_RELFUT) -- augments GameFormer's
|
| 22 |
+
per-agent future encoding with explicit PAIRWISE-RELATIVE future features
|
| 23 |
+
(MLP over relative position+velocity, max-pooled over time, neighbor-mean).
|
| 24 |
+
|
| 25 |
+
ALL FLAGS OFF == vanilla GameFormer (identical to gameformer_nba_standalone,
|
| 26 |
+
ADE4/FDE4 = 0.848/0.951). So (vanilla) vs (GF_SRA=1) is a clean isolated
|
| 27 |
+
ablation of "does SRA's philosophy help a feedforward predictor too?".
|
| 28 |
+
|
| 29 |
+
It is a STANDALONE predictor: it consumes ONLY agent HISTORY (past trajectories)
|
| 30 |
+
and predicts K multi-modal futures via the level-k game-theoretic decoder. It
|
| 31 |
+
does NOT consume any external denoiser / diffusion prediction.
|
| 32 |
+
|
| 33 |
+
--------------------------------------------------------------------------------
|
| 34 |
+
WHAT WAS REPLICATED (evidence = MCZhi/GameFormer @ main)
|
| 35 |
+
--------------------------------------------------------------------------------
|
| 36 |
+
Reference files studied verbatim:
|
| 37 |
+
* model/GameFormer.py (Encoder, Decoder, GameFormer)
|
| 38 |
+
* model/modules.py (AgentEncoder, FutureEncoder, GMMPredictor,
|
| 39 |
+
SelfTransformer, CrossTransformer,
|
| 40 |
+
InitialDecoder, InteractionDecoder)
|
| 41 |
+
* utils/inter_pred_utils.py (level_k_loss, gmm_loss)
|
| 42 |
+
* interaction_prediction/train.py (AdamW 1e-4, MultiStepLR, grad-clip 5,
|
| 43 |
+
level=3, modalities=6, encoder_layers=6)
|
| 44 |
+
|
| 45 |
+
ENCODER (GameFormer.Encoder + modules.AgentEncoder)
|
| 46 |
+
* Per-agent temporal encoder = 2-layer LSTM, hidden 256 (nn.LSTM(8,256,2)).
|
| 47 |
+
We adapt the input feature dim 8 -> 6 (NBA has [abs xy, rel xy, vel xy], no
|
| 48 |
+
heading/size channels). A learned agent-type embedding (player vs. ball) is
|
| 49 |
+
added to the LSTM last-step output, exactly as GameFormer adds `type_emb`.
|
| 50 |
+
* Interaction / self-attention over agents = the GameFormer `fusion_encoder`,
|
| 51 |
+
a nn.TransformerEncoder of TransformerEncoderLayer(d_model=256, nhead=8,
|
| 52 |
+
dim_feedforward=1024, activation=gelu, dropout=0.1, batch_first=True),
|
| 53 |
+
`encoder_layers` deep. In GameFormer this fuses agent tokens WITH per-agent
|
| 54 |
+
map (lane/crosswalk) tokens; NBA has NO map, so the map encoders
|
| 55 |
+
(LaneEncoder / CrosswalkEncoder / segment_map) are DROPPED and the fusion is
|
| 56 |
+
pure agent<->agent self-attention over the 11 tokens (identical for every
|
| 57 |
+
target agent when there is no per-agent map, so it is computed once).
|
| 58 |
+
|
| 59 |
+
LEVEL-k GAME-THEORETIC DECODER (GameFormer.Decoder)
|
| 60 |
+
* Level 0 = InitialDecoder: for each agent, a set of `modalities` (=K) learned
|
| 61 |
+
modal query embeddings + a per-agent query embedding are added to the agent's
|
| 62 |
+
own context token, then cross-attended (CrossTransformer) to the full agent
|
| 63 |
+
context. A GMMPredictor head outputs K trajectories of (mu_x,mu_y,
|
| 64 |
+
log_sig_x,log_sig_y) per step + K mode scores. predictions[...,:2] += the
|
| 65 |
+
agent's current (last observed) position.
|
| 66 |
+
* Levels 1..L = InteractionDecoder (one per level, sharing ONE FutureEncoder):
|
| 67 |
+
- FutureEncoder encodes EVERY agent's K level-(k-1) modal futures into a
|
| 68 |
+
vector (max-pool over time of an MLP on [x,y,heading,vx,vy]); GameFormer
|
| 69 |
+
also appends box size (w,l,h) -> we drop those (no size in NBA), 8->5.
|
| 70 |
+
- The per-mode future encodings are score-softmax-weighted and mean-pooled
|
| 71 |
+
to one vector per agent, then passed through a SelfTransformer to produce
|
| 72 |
+
the interaction encoding over all agents (this is the game-theoretic
|
| 73 |
+
"condition on all other agents' level-(k-1) predictions" step).
|
| 74 |
+
- The interaction tokens are concatenated with the context tokens; each
|
| 75 |
+
agent's OWN future token is masked out; the previous-level content plus
|
| 76 |
+
the agent's own future encoding form the query, cross-attended to that
|
| 77 |
+
combined context, and a fresh GMMPredictor emits the level-k K futures.
|
| 78 |
+
* Output dict: level_0_interactions/scores ... level_L_interactions/scores,
|
| 79 |
+
exactly as GameFormer.Decoder.forward.
|
| 80 |
+
|
| 81 |
+
LOSS (utils.inter_pred_utils.level_k_loss + gmm_loss, gmm=True)
|
| 82 |
+
* Summed over ALL levels k=0..L (level-wise imitation, GameFormer's key term).
|
| 83 |
+
* Per level, GMM winner-takes-all: best mode = argmin over K of
|
| 84 |
+
(mean_t ||traj-gt|| + sum over checkpoint steps), then the Gaussian NLL
|
| 85 |
+
reg = (log_sig_x+log_sig_y) + 0.5*(dx^2/sx^2 + dy^2/sy^2) with log_sig
|
| 86 |
+
clipped to [0,5], reduced as mean_t + sum over checkpoints; plus mode
|
| 87 |
+
classification F.cross_entropy(scores, best_mode, label_smoothing=0.2)
|
| 88 |
+
weighted x2. We MARGINALIZE per agent (GameFormer's interaction_prediction
|
| 89 |
+
task predicts 2 agents JOINTLY via distance.mean(1)/scores.sum(1); the NBA
|
| 90 |
+
Table-1 metric is MARGINAL min-ADE_20, so the best mode / scores are taken
|
| 91 |
+
per agent instead of jointly -- the only loss adaptation).
|
| 92 |
+
|
| 93 |
+
--------------------------------------------------------------------------------
|
| 94 |
+
NBA I/O + METRIC (matched to MoFlow / Table 1)
|
| 95 |
+
--------------------------------------------------------------------------------
|
| 96 |
+
* Data: reuses MoFlow's exact loader data/dataloader_nba.py::NBADatasetMinMax
|
| 97 |
+
(the graph loader dataloader_nba_graph.py merely re-exports it). Same .npy
|
| 98 |
+
files (data/nba/original/nba_{train,test}.npy, 32500/12500 scenes,
|
| 99 |
+
30 frames x 11 agents x 2D), same split (trajs[:n_train] / trajs[:n_test]),
|
| 100 |
+
same coordinate scaling traj_scale_total=94/28 and traj_mean=[14,7.5].
|
| 101 |
+
* Past = 10 frames, Future = 20 frames = 4.0 s @ 5 Hz, 11 agents, 2D.
|
| 102 |
+
* Encoder input = data['past_traj'] (6-dim min-max normalized to [-1,1]).
|
| 103 |
+
* Coordinate frame for the GMM/interaction = data['past_traj_original_scale']
|
| 104 |
+
(centered-abs = pos/scale - traj_mean); cur_xy = its last past step.
|
| 105 |
+
Ground-truth future displacement = data['fut_traj_original_scale']
|
| 106 |
+
(= future - last_observed, in /scale units) -- the SAME tensor the MoFlow
|
| 107 |
+
evaluator (eval_perscene_moflow.py) compares against.
|
| 108 |
+
* Metric (identical to eval_perscene_moflow.py, verified vs Table 1):
|
| 109 |
+
d = ||pred_disp - gt_disp|| # [B,A,K,T]
|
| 110 |
+
ADE4 = d[..., :20].mean(over T).min(over K), mean over 11 agents & scenes
|
| 111 |
+
FDE4 = d[..., 19] .min(over K), mean over 11 agents & scenes
|
| 112 |
+
i.e. marginal min-of-K=20 at the 4.0 s horizon (frame 20). Table-1 baselines
|
| 113 |
+
are ~0.70-1.04 ADE in this space.
|
| 114 |
+
|
| 115 |
+
Prints: [SRA-GF] epoch N ADE4=.. FDE4=..
|
| 116 |
+
|
| 117 |
+
DO NOT run on GPU here (GPUs busy) -- this file is only `python -m py_compile`d.
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
import os
|
| 121 |
+
import sys
|
| 122 |
+
import argparse
|
| 123 |
+
import time
|
| 124 |
+
|
| 125 |
+
# ------------------------------------------------------------------ GPU FIRST
|
| 126 |
+
# CUDA context is fixed at torch import; select the device BEFORE importing torch
|
| 127 |
+
def _early_gpu():
|
| 128 |
+
for i, a in enumerate(sys.argv):
|
| 129 |
+
if a == '--gpu' and i + 1 < len(sys.argv):
|
| 130 |
+
return sys.argv[i + 1]
|
| 131 |
+
if a.startswith('--gpu='):
|
| 132 |
+
return a.split('=', 1)[1]
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
_g = _early_gpu()
|
| 137 |
+
if _g is not None:
|
| 138 |
+
os.environ['CUDA_VISIBLE_DEVICES'] = str(_g)
|
| 139 |
+
os.environ.setdefault('MPLBACKEND', 'Agg') # dataloader imports matplotlib
|
| 140 |
+
|
| 141 |
+
# ------------------------------------------------ reuse MoFlow's NBA pipeline
|
| 142 |
+
MOFLOW_ROOT = '/mnt/jaewoo4tb/srtp/MoFlow'
|
| 143 |
+
if MOFLOW_ROOT not in sys.path:
|
| 144 |
+
sys.path.insert(0, MOFLOW_ROOT)
|
| 145 |
+
|
| 146 |
+
import math
|
| 147 |
+
import types as _types
|
| 148 |
+
import numpy as np
|
| 149 |
+
import torch
|
| 150 |
+
import torch.nn as nn
|
| 151 |
+
import torch.nn.functional as F
|
| 152 |
+
from torch.utils.data import DataLoader
|
| 153 |
+
|
| 154 |
+
# so that `from utils.normalization import ...` inside the loader resolves
|
| 155 |
+
try:
|
| 156 |
+
os.chdir(MOFLOW_ROOT)
|
| 157 |
+
except Exception:
|
| 158 |
+
pass
|
| 159 |
+
from data.dataloader_nba import NBADatasetMinMax, seq_collate_nba
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
# ============================================================================
|
| 163 |
+
# MODEL (faithful GameFormer, NBA-adapted: no map)
|
| 164 |
+
# ============================================================================
|
| 165 |
+
D_MODEL = 256
|
| 166 |
+
N_HEADS = 8
|
| 167 |
+
DROPOUT = 0.1
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# ---------------------------------------------------------------- SRA toggles
|
| 171 |
+
# Inject SRA's three philosophies into GameFormer's level-k InteractionDecoder.
|
| 172 |
+
# Master flag GF_SRA=1 enables all; or enable individually. All-off == vanilla
|
| 173 |
+
# GameFormer (identical to gameformer_nba_standalone.py, 0.848/0.951).
|
| 174 |
+
def _sra_on(name):
|
| 175 |
+
if os.environ.get('GF_SRA', '') not in ('', '0', 'false', 'False'):
|
| 176 |
+
return True
|
| 177 |
+
return os.environ.get(name, '') not in ('', '0', 'false', 'False')
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
USE_TOPN = _sra_on('GF_SRA_TOPN') # sparse RAG top-N neighbor selection
|
| 181 |
+
USE_SIGMA = _sra_on('GF_SRA_SIGMA') # uncertainty-gated message passing
|
| 182 |
+
USE_RELFUT = _sra_on('GF_SRA_RELFUT') # explicit relational future features
|
| 183 |
+
TOPN_K = int(os.environ.get('GF_TOPN', '5')) # SRA neighbor budget (top_n=5)
|
| 184 |
+
SIG_SEL_W = float(os.environ.get('GF_SIG_SEL_W', '0.5')) # sigma weight in nbr selection
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
# ---------------------------------------------------------------- primitives
|
| 188 |
+
class SelfTransformer(nn.Module):
|
| 189 |
+
"""modules.py::SelfTransformer. `attn_mask` added for SRA sparse-neighbor
|
| 190 |
+
selection (per-query [B*heads,A,A] boolean; True = block non-neighbor)."""
|
| 191 |
+
|
| 192 |
+
def __init__(self, dim=D_MODEL, heads=N_HEADS, dropout=DROPOUT):
|
| 193 |
+
super().__init__()
|
| 194 |
+
self.heads = heads
|
| 195 |
+
self.self_attention = nn.MultiheadAttention(dim, heads, dropout, batch_first=True)
|
| 196 |
+
self.norm_1 = nn.LayerNorm(dim)
|
| 197 |
+
self.norm_2 = nn.LayerNorm(dim)
|
| 198 |
+
self.ffn = nn.Sequential(
|
| 199 |
+
nn.Linear(dim, dim * 4), nn.GELU(), nn.Dropout(dropout),
|
| 200 |
+
nn.Linear(dim * 4, dim), nn.Dropout(dropout))
|
| 201 |
+
|
| 202 |
+
def forward(self, inputs, mask=None, attn_mask=None):
|
| 203 |
+
attn, _ = self.self_attention(inputs, inputs, inputs, key_padding_mask=mask,
|
| 204 |
+
attn_mask=attn_mask)
|
| 205 |
+
attn = self.norm_1(attn + inputs)
|
| 206 |
+
return self.norm_2(self.ffn(attn) + attn)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class CrossTransformer(nn.Module):
|
| 210 |
+
"""modules.py::CrossTransformer (verbatim)."""
|
| 211 |
+
|
| 212 |
+
def __init__(self, dim=D_MODEL, heads=N_HEADS, dropout=DROPOUT):
|
| 213 |
+
super().__init__()
|
| 214 |
+
self.cross_attention = nn.MultiheadAttention(dim, heads, dropout, batch_first=True)
|
| 215 |
+
self.norm_1 = nn.LayerNorm(dim)
|
| 216 |
+
self.norm_2 = nn.LayerNorm(dim)
|
| 217 |
+
self.ffn = nn.Sequential(
|
| 218 |
+
nn.Linear(dim, dim * 4), nn.GELU(), nn.Dropout(dropout),
|
| 219 |
+
nn.Linear(dim * 4, dim), nn.Dropout(dropout))
|
| 220 |
+
|
| 221 |
+
def forward(self, query, key, value, mask=None):
|
| 222 |
+
attn, _ = self.cross_attention(query, key, value, key_padding_mask=mask)
|
| 223 |
+
attn = self.norm_1(attn)
|
| 224 |
+
return self.norm_2(self.ffn(attn) + attn)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
class GMMPredictor(nn.Module):
|
| 228 |
+
"""modules.py::GMMPredictor (verbatim). Out: (mu_x,mu_y,log_sig_x,log_sig_y)."""
|
| 229 |
+
|
| 230 |
+
def __init__(self, future_len, dim=D_MODEL):
|
| 231 |
+
super().__init__()
|
| 232 |
+
self._future_len = future_len
|
| 233 |
+
self.gaussian = nn.Sequential(
|
| 234 |
+
nn.Linear(dim, 512), nn.ELU(), nn.Dropout(0.1),
|
| 235 |
+
nn.Linear(512, future_len * 4))
|
| 236 |
+
self.score = nn.Sequential(
|
| 237 |
+
nn.Linear(dim, 64), nn.ELU(), nn.Dropout(0.1), nn.Linear(64, 1))
|
| 238 |
+
|
| 239 |
+
def forward(self, x):
|
| 240 |
+
# x: [B, M, D]
|
| 241 |
+
B, M, _ = x.shape
|
| 242 |
+
res = self.gaussian(x).view(B, M, self._future_len, 4)
|
| 243 |
+
score = self.score(x).squeeze(-1) # [B, M]
|
| 244 |
+
return res, score
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
# ------------------------------------------------------------------ encoder
|
| 248 |
+
class AgentHistoryEncoder(nn.Module):
|
| 249 |
+
"""modules.py::AgentEncoder -- 2-layer LSTM(->256) over agent history + type
|
| 250 |
+
embedding. GameFormer uses nn.LSTM(8,256,2); NBA feature dim is 6."""
|
| 251 |
+
|
| 252 |
+
def __init__(self, in_dim=6, dim=D_MODEL, n_types=2):
|
| 253 |
+
super().__init__()
|
| 254 |
+
self.motion = nn.LSTM(in_dim, dim, 2, batch_first=True)
|
| 255 |
+
self.type_emb = nn.Embedding(n_types, dim)
|
| 256 |
+
|
| 257 |
+
def forward(self, hist, types):
|
| 258 |
+
# hist: [B, A, T, in_dim] ; types: [A] long
|
| 259 |
+
B, A, T, C = hist.shape
|
| 260 |
+
traj, _ = self.motion(hist.reshape(B * A, T, C))
|
| 261 |
+
out = traj[:, -1].reshape(B, A, -1) # [B, A, D]
|
| 262 |
+
out = out + self.type_emb(types)[None] # + per-agent type emb
|
| 263 |
+
return out
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
class FusionEncoder(nn.Module):
|
| 267 |
+
"""GameFormer.Encoder.fusion_encoder -- self-attention over agent tokens.
|
| 268 |
+
(No map tokens for NBA.)"""
|
| 269 |
+
|
| 270 |
+
def __init__(self, dim=D_MODEL, heads=N_HEADS, layers=6, dropout=DROPOUT):
|
| 271 |
+
super().__init__()
|
| 272 |
+
layer = nn.TransformerEncoderLayer(
|
| 273 |
+
d_model=dim, nhead=heads, dim_feedforward=dim * 4,
|
| 274 |
+
activation=F.gelu, dropout=dropout, batch_first=True)
|
| 275 |
+
self.encoder = nn.TransformerEncoder(layer, layers, enable_nested_tensor=False)
|
| 276 |
+
|
| 277 |
+
def forward(self, tokens, mask=None):
|
| 278 |
+
return self.encoder(tokens, src_key_padding_mask=mask)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
# ------------------------------------------------------------- future encoder
|
| 282 |
+
class FutureEncoder(nn.Module):
|
| 283 |
+
"""modules.py::FutureEncoder -- encodes each agent's K modal futures into a
|
| 284 |
+
per-mode vector via max-pool over an MLP on [x,y,heading,vx,vy]. GameFormer
|
| 285 |
+
appends box size (w,l,h) -> dropped for NBA (8 -> 5 input dims)."""
|
| 286 |
+
|
| 287 |
+
def __init__(self, dim=D_MODEL, n_types=2, dt=0.2):
|
| 288 |
+
super().__init__()
|
| 289 |
+
self.dt = dt
|
| 290 |
+
self.mlp = nn.Sequential(nn.Linear(5, 64), nn.ReLU(), nn.Linear(64, dim))
|
| 291 |
+
self.type_emb = nn.Embedding(n_types, dim)
|
| 292 |
+
|
| 293 |
+
def state_process(self, trajs, cur_xy):
|
| 294 |
+
# trajs: [B, A, M, T, 2] ; cur_xy: [B, A, 2]
|
| 295 |
+
B, A, M, T, _ = trajs.shape
|
| 296 |
+
cur = cur_xy[:, :, None, None, :].expand(B, A, M, 1, 2)
|
| 297 |
+
xy = torch.cat([cur, trajs], dim=-2) # [B,A,M,T+1,2]
|
| 298 |
+
dxy = torch.diff(xy, dim=-2) # [B,A,M,T,2]
|
| 299 |
+
v = dxy / self.dt
|
| 300 |
+
theta = torch.atan2(dxy[..., 1], dxy[..., 0].clamp(min=1e-3)).unsqueeze(-1)
|
| 301 |
+
return torch.cat([trajs, theta, v], dim=-1) # [B,A,M,T,5]
|
| 302 |
+
|
| 303 |
+
def forward(self, trajs, cur_xy, types):
|
| 304 |
+
feat = self.state_process(trajs, cur_xy)
|
| 305 |
+
h = self.mlp(feat.detach()) # GameFormer detaches
|
| 306 |
+
out = torch.max(h, dim=-2).values # [B,A,M,D]
|
| 307 |
+
out = out + self.type_emb(types)[None, :, None, :] # + type emb
|
| 308 |
+
return out
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
# ----------------------------------------------------------- level-0 decoder
|
| 312 |
+
class InitialDecoder(nn.Module):
|
| 313 |
+
"""modules.py::InitialDecoder -- vectorized over agents."""
|
| 314 |
+
|
| 315 |
+
def __init__(self, modalities, n_agents, future_len, dim=D_MODEL):
|
| 316 |
+
super().__init__()
|
| 317 |
+
self.M = modalities
|
| 318 |
+
self.A = n_agents
|
| 319 |
+
self.multi_modal_query_embedding = nn.Embedding(modalities, dim)
|
| 320 |
+
self.agent_query_embedding = nn.Embedding(n_agents, dim)
|
| 321 |
+
self.query_encoder = CrossTransformer(dim)
|
| 322 |
+
self.predictor = GMMPredictor(future_len, dim)
|
| 323 |
+
self.register_buffer('modal', torch.arange(modalities).long())
|
| 324 |
+
self.register_buffer('agent', torch.arange(n_agents).long())
|
| 325 |
+
|
| 326 |
+
def forward(self, encoding, cur_xy, mask=None):
|
| 327 |
+
# encoding: [B, A, D] (full agent context, shared as keys)
|
| 328 |
+
B, A, D = encoding.shape
|
| 329 |
+
M = self.M
|
| 330 |
+
multi_modal = self.multi_modal_query_embedding(self.modal) # [M, D]
|
| 331 |
+
agent = self.agent_query_embedding(self.agent) # [A, D]
|
| 332 |
+
mm_agent_query = multi_modal[None, :, :] + agent[:, None, :] # [A, M, D]
|
| 333 |
+
query = encoding[:, :, None, :] + mm_agent_query[None] # [B, A, M, D]
|
| 334 |
+
|
| 335 |
+
q = query.reshape(B * A, M, D)
|
| 336 |
+
kv = encoding[:, None, :, :].expand(B, A, A, D).reshape(B * A, A, D)
|
| 337 |
+
km = None
|
| 338 |
+
if mask is not None:
|
| 339 |
+
km = mask[:, None, :].expand(B, A, A).reshape(B * A, A)
|
| 340 |
+
content = self.query_encoder(q, kv, kv, km) # [B*A, M, D]
|
| 341 |
+
preds, scores = self.predictor(content) # [B*A,M,T,4],[B*A,M]
|
| 342 |
+
|
| 343 |
+
content = content.reshape(B, A, M, D)
|
| 344 |
+
T = preds.shape[-2]
|
| 345 |
+
preds = preds.reshape(B, A, M, T, 4)
|
| 346 |
+
scores = scores.reshape(B, A, M)
|
| 347 |
+
preds = preds.clone()
|
| 348 |
+
preds[..., :2] = preds[..., :2] + cur_xy[:, :, None, None, :]
|
| 349 |
+
return content, preds, scores
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
# ------------------------------------------------------- level-k decoder
|
| 353 |
+
class InteractionDecoder(nn.Module):
|
| 354 |
+
"""modules.py::InteractionDecoder -- one game-theoretic reasoning level,
|
| 355 |
+
vectorized over agents. Conditions each agent on ALL agents' level-(k-1)
|
| 356 |
+
predictions (interaction self-attention), masking the agent's own future."""
|
| 357 |
+
|
| 358 |
+
def __init__(self, future_encoder, future_len, n_agents, dim=D_MODEL,
|
| 359 |
+
use_topn=None, use_sigma=None, use_relfut=None, top_n=None):
|
| 360 |
+
super().__init__()
|
| 361 |
+
self.A = n_agents
|
| 362 |
+
self.future_encoder = future_encoder # SHARED across levels
|
| 363 |
+
self.interaction_encoder = SelfTransformer(dim)
|
| 364 |
+
self.query_encoder = CrossTransformer(dim)
|
| 365 |
+
self.decoder = GMMPredictor(future_len, dim)
|
| 366 |
+
# --- SRA augmentations (read module-level toggles unless overridden) ---
|
| 367 |
+
self.use_topn = USE_TOPN if use_topn is None else use_topn
|
| 368 |
+
self.use_sigma = USE_SIGMA if use_sigma is None else use_sigma
|
| 369 |
+
self.use_relfut = USE_RELFUT if use_relfut is None else use_relfut
|
| 370 |
+
self.top_n = TOPN_K if top_n is None else top_n
|
| 371 |
+
if self.use_sigma: # uncertainty -> message gate in (0,1)
|
| 372 |
+
self.sigma_gate = nn.Sequential(
|
| 373 |
+
nn.Linear(1, 32), nn.ReLU(), nn.Linear(32, 1), nn.Sigmoid())
|
| 374 |
+
if self.use_relfut: # relational future edge encoder
|
| 375 |
+
self.rel_mlp = nn.Sequential(nn.Linear(4, 64), nn.ReLU(), nn.Linear(64, dim))
|
| 376 |
+
|
| 377 |
+
def forward(self, cur_xy, last_traj, last_scores, last_content, encoding, types, mask=None):
|
| 378 |
+
# cur_xy:[B,A,2] last_traj:[B,A,M,T,4] last_scores:[B,A,M]
|
| 379 |
+
# last_content:[B,A,M,D] encoding:[B,A,D]
|
| 380 |
+
B, A, M, T, _ = last_traj.shape
|
| 381 |
+
D = encoding.shape[-1]
|
| 382 |
+
dev = encoding.device
|
| 383 |
+
w = last_scores.softmax(-1) # [B,A,M] mode weights
|
| 384 |
+
|
| 385 |
+
# encode all agents' level-(k-1) modal futures
|
| 386 |
+
multi_futures = self.future_encoder(last_traj[..., :2], cur_xy, types) # [B,A,M,D]
|
| 387 |
+
futures = (multi_futures * w.unsqueeze(-1)).mean(dim=2) # [B,A,D]
|
| 388 |
+
|
| 389 |
+
# ---- SRA: per-agent uncertainty from prev-level GMM log-sigma ----------
|
| 390 |
+
sig_agent = None
|
| 391 |
+
if self.use_sigma:
|
| 392 |
+
sig = torch.exp(last_traj[..., 2:].clamp(0, 5)).mean(-1) # [B,A,M,T]
|
| 393 |
+
sig_agent = (sig * w[..., None]).sum(2).mean(-1) # [B,A] score-wtd, mean-T
|
| 394 |
+
|
| 395 |
+
# ---- SRA: sparse RAG neighbor selection from future proximity ----------
|
| 396 |
+
nbr_block = None # [B,A,A] True = block
|
| 397 |
+
rep = (last_traj[..., :2] * w[..., None, None]).sum(2) # [B,A,T,2] rep future
|
| 398 |
+
eye = torch.eye(A, dtype=torch.bool, device=dev)
|
| 399 |
+
if self.use_topn and A > 1:
|
| 400 |
+
prox = (rep[:, :, None] - rep[:, None]).norm(dim=-1).mean(-1) # [B,A,A]
|
| 401 |
+
if sig_agent is not None: # prefer confident neighbors
|
| 402 |
+
prox = prox + SIG_SEL_W * sig_agent[:, None, :]
|
| 403 |
+
prox = prox.masked_fill(eye[None], float('inf')) # exclude self from top-N
|
| 404 |
+
N = min(max(self.top_n - 1, 1), A - 1)
|
| 405 |
+
nbr_idx = prox.topk(N, largest=False, dim=-1).indices # [B,A,N] nearest others
|
| 406 |
+
nbr_block = torch.ones(B, A, A, dtype=torch.bool, device=dev)
|
| 407 |
+
nbr_block[:, torch.arange(A, device=dev), torch.arange(A, device=dev)] = False # self ok
|
| 408 |
+
nbr_block.scatter_(2, nbr_idx, False) # neighbors ok
|
| 409 |
+
|
| 410 |
+
# ---- SRA: uncertainty-gated message passing ----------------------------
|
| 411 |
+
if self.use_sigma:
|
| 412 |
+
futures = futures * self.sigma_gate(sig_agent[..., None]) # weaken uncertain senders
|
| 413 |
+
|
| 414 |
+
# ---- SRA: explicit relational future features --------------------------
|
| 415 |
+
if self.use_relfut and A > 1:
|
| 416 |
+
dpos = rep[:, :, None] - rep[:, None] # [B,A,A,T,2]
|
| 417 |
+
dvel = F.pad(torch.diff(dpos, dim=-2), (0, 0, 1, 0)) # [B,A,A,T,2]
|
| 418 |
+
relf = self.rel_mlp(torch.cat([dpos, dvel], dim=-1)).max(dim=-2).values # [B,A,A,D]
|
| 419 |
+
if nbr_block is not None:
|
| 420 |
+
relf = relf.masked_fill(nbr_block[..., None], 0.0)
|
| 421 |
+
denom = (~nbr_block).sum(-1, keepdim=True).clamp(min=1)
|
| 422 |
+
futures = futures + relf.sum(2) / denom # neighbor-mean relational future
|
| 423 |
+
else:
|
| 424 |
+
futures = futures + relf.mean(2)
|
| 425 |
+
|
| 426 |
+
# interaction over agents (self-attention; SRA restricts to neighbors)
|
| 427 |
+
attn_mask = None
|
| 428 |
+
if nbr_block is not None:
|
| 429 |
+
h = self.interaction_encoder.heads
|
| 430 |
+
attn_mask = nbr_block[:, None].expand(B, h, A, A).reshape(B * h, A, A)
|
| 431 |
+
interaction = self.interaction_encoder(futures, None, attn_mask) # [B,A,D]
|
| 432 |
+
|
| 433 |
+
# combined context = [interaction tokens ; context tokens]
|
| 434 |
+
ctx = torch.cat([interaction, encoding], dim=1) # [B, 2A, D]
|
| 435 |
+
# per-agent key mask: mask each agent's OWN future token in the 1st block
|
| 436 |
+
km = torch.zeros(B, A, 2 * A, dtype=torch.bool, device=dev)
|
| 437 |
+
idx = torch.arange(A, device=dev)
|
| 438 |
+
km[:, idx, idx] = True
|
| 439 |
+
if mask is not None:
|
| 440 |
+
# second block inherits context validity mask (all-valid for NBA)
|
| 441 |
+
km[:, :, A:] = km[:, :, A:] | mask[:, None, :]
|
| 442 |
+
km[:, :, :A] = km[:, :, :A] | mask[:, None, :]
|
| 443 |
+
if nbr_block is not None: # SRA sparsity on both blocks
|
| 444 |
+
km[:, :, :A] = km[:, :, :A] | nbr_block # interaction block: neighbors only
|
| 445 |
+
km[:, :, A:] = km[:, :, A:] | nbr_block # context block: neighbors(+self) only
|
| 446 |
+
km = km.reshape(B * A, 2 * A)
|
| 447 |
+
kv = ctx[:, None, :, :].expand(B, A, 2 * A, D).reshape(B * A, 2 * A, D)
|
| 448 |
+
|
| 449 |
+
query = (last_content + multi_futures).reshape(B * A, M, D) # prev content + own future
|
| 450 |
+
content = self.query_encoder(query, kv, kv, km) # [B*A, M, D]
|
| 451 |
+
traj, scores = self.decoder(content) # [B*A,M,T,4],[B*A,M]
|
| 452 |
+
|
| 453 |
+
content = content.reshape(B, A, M, D)
|
| 454 |
+
traj = traj.reshape(B, A, M, T, 4)
|
| 455 |
+
scores = scores.reshape(B, A, M)
|
| 456 |
+
traj = traj.clone()
|
| 457 |
+
traj[..., :2] = traj[..., :2] + cur_xy[:, :, None, None, :]
|
| 458 |
+
return content, traj, scores
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
# -------------------------------------------------------------- full decoder
|
| 462 |
+
class GameFormerDecoder(nn.Module):
|
| 463 |
+
"""GameFormer.Decoder -- level 0 + L interaction levels (shared FutureEncoder)."""
|
| 464 |
+
|
| 465 |
+
def __init__(self, modalities, n_agents, future_len, levels, dim=D_MODEL,
|
| 466 |
+
n_types=2, dt=0.2):
|
| 467 |
+
super().__init__()
|
| 468 |
+
self._levels = levels
|
| 469 |
+
self.future_encoder = FutureEncoder(dim, n_types, dt)
|
| 470 |
+
self.initial_stage = InitialDecoder(modalities, n_agents, future_len, dim)
|
| 471 |
+
self.interaction_stage = nn.ModuleList(
|
| 472 |
+
[InteractionDecoder(self.future_encoder, future_len, n_agents, dim)
|
| 473 |
+
for _ in range(levels)])
|
| 474 |
+
|
| 475 |
+
def forward(self, encoding, cur_xy, types, mask=None):
|
| 476 |
+
out = {}
|
| 477 |
+
content, traj, scores = self.initial_stage(encoding, cur_xy, mask)
|
| 478 |
+
out['level_0_interactions'] = traj
|
| 479 |
+
out['level_0_scores'] = scores
|
| 480 |
+
for k in range(1, self._levels + 1):
|
| 481 |
+
content, traj, scores = self.interaction_stage[k - 1](
|
| 482 |
+
cur_xy, traj, scores, content, encoding, types, mask)
|
| 483 |
+
out[f'level_{k}_interactions'] = traj
|
| 484 |
+
out[f'level_{k}_scores'] = scores
|
| 485 |
+
return out
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
# ---------------------------------------------------------------- top model
|
| 489 |
+
class GameFormerNBA(nn.Module):
|
| 490 |
+
"""GameFormer.GameFormer (NBA, map dropped)."""
|
| 491 |
+
|
| 492 |
+
def __init__(self, n_agents=11, past_dim=6, future_len=20, modalities=20,
|
| 493 |
+
levels=2, dim=D_MODEL, heads=N_HEADS, enc_layers=6,
|
| 494 |
+
n_types=2, dt=0.2, ball_idx=10):
|
| 495 |
+
super().__init__()
|
| 496 |
+
self.levels = levels
|
| 497 |
+
self.history_encoder = AgentHistoryEncoder(past_dim, dim, n_types)
|
| 498 |
+
self.fusion_encoder = FusionEncoder(dim, heads, enc_layers)
|
| 499 |
+
self.decoder = GameFormerDecoder(modalities, n_agents, future_len, levels,
|
| 500 |
+
dim, n_types, dt)
|
| 501 |
+
types = torch.zeros(n_agents, dtype=torch.long)
|
| 502 |
+
if 0 <= ball_idx < n_agents and n_types > 1:
|
| 503 |
+
types[ball_idx] = 1
|
| 504 |
+
self.register_buffer('agent_types', types)
|
| 505 |
+
|
| 506 |
+
def forward(self, feats, cur_xy):
|
| 507 |
+
# feats: [B,A,T,past_dim] (encoder input) ; cur_xy: [B,A,2] (metric frame)
|
| 508 |
+
enc = self.history_encoder(feats, self.agent_types) # [B,A,D]
|
| 509 |
+
enc = self.fusion_encoder(enc, None) # [B,A,D]
|
| 510 |
+
return self.decoder(enc, cur_xy, self.agent_types, None)
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
# ============================================================================
|
| 514 |
+
# LOSS (level_k_loss + gmm_loss, gmm=True; MARGINAL per agent for NBA)
|
| 515 |
+
# ============================================================================
|
| 516 |
+
def gmm_loss_marginal(traj, conv, scores, gt, metric_idx):
|
| 517 |
+
"""Per-agent GMM winner-takes-all (marginal version of gmm_loss).
|
| 518 |
+
traj: [B,A,M,T,2] conv: [B,A,M,T,2] scores: [B,A,M] gt: [B,A,T,2]
|
| 519 |
+
"""
|
| 520 |
+
B, A, M, T, _ = traj.shape
|
| 521 |
+
dist = torch.norm(traj - gt[:, :, None], dim=-1) # [B,A,M,T]
|
| 522 |
+
ndist = dist.mean(-1) + dist[..., metric_idx].sum(-1) # [B,A,M]
|
| 523 |
+
best = ndist.argmin(-1) # [B,A]
|
| 524 |
+
|
| 525 |
+
gi = best[..., None, None, None].expand(B, A, 1, T, 2)
|
| 526 |
+
best_traj = torch.gather(traj, 2, gi).squeeze(2) # [B,A,T,2]
|
| 527 |
+
best_conv = torch.gather(conv, 2, gi).squeeze(2) # [B,A,T,2]
|
| 528 |
+
|
| 529 |
+
dx = best_traj[..., 0] - gt[..., 0]
|
| 530 |
+
dy = best_traj[..., 1] - gt[..., 1]
|
| 531 |
+
log_std_x = torch.clip(best_conv[..., 0], 0, 5)
|
| 532 |
+
log_std_y = torch.clip(best_conv[..., 1], 0, 5)
|
| 533 |
+
std_x, std_y = torch.exp(log_std_x), torch.exp(log_std_y)
|
| 534 |
+
|
| 535 |
+
reg = (log_std_x + log_std_y) + 0.5 * ((dx ** 2) / (std_x ** 2) + (dy ** 2) / (std_y ** 2))
|
| 536 |
+
reg = reg.mean(-1) + reg[..., metric_idx].sum(-1) # [B,A]
|
| 537 |
+
|
| 538 |
+
prob_loss = F.cross_entropy(scores.reshape(B * A, M), best.reshape(B * A),
|
| 539 |
+
label_smoothing=0.2)
|
| 540 |
+
return reg.mean() + 2.0 * prob_loss, best
|
| 541 |
+
|
| 542 |
+
|
| 543 |
+
def level_k_loss_marginal(outputs, gt_center, levels, metric_idx):
|
| 544 |
+
"""Sum GMM loss across all levels 0..L (GameFormer's level-wise imitation)."""
|
| 545 |
+
total = 0.0
|
| 546 |
+
for k in range(levels + 1):
|
| 547 |
+
pred = outputs[f'level_{k}_interactions'] # [B,A,M,T,4]
|
| 548 |
+
scores = outputs[f'level_{k}_scores'] # [B,A,M]
|
| 549 |
+
traj, conv = pred[..., :2], pred[..., 2:]
|
| 550 |
+
l, _ = gmm_loss_marginal(traj, conv, scores, gt_center, metric_idx)
|
| 551 |
+
total = total + l
|
| 552 |
+
return total
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
# ============================================================================
|
| 556 |
+
# DATA
|
| 557 |
+
# ============================================================================
|
| 558 |
+
def build_loaders(args):
|
| 559 |
+
cfg = _types.SimpleNamespace(
|
| 560 |
+
traj_mean=[14, 7.5], data_norm='min_max',
|
| 561 |
+
past_frames=args.past_len, future_frames=args.future_len, agents=args.agents)
|
| 562 |
+
# Train first so cfg min/max scalars are populated for the test set.
|
| 563 |
+
train_set = NBADatasetMinMax(
|
| 564 |
+
obs_len=args.past_len, pred_len=args.future_len, training=True,
|
| 565 |
+
num_scenes=args.n_train, cfg=cfg, data_dir=args.data_dir,
|
| 566 |
+
rotate=False, data_norm='min_max')
|
| 567 |
+
test_set = NBADatasetMinMax(
|
| 568 |
+
obs_len=args.past_len, pred_len=args.future_len, training=False,
|
| 569 |
+
test_scenes=args.n_test, cfg=cfg, data_dir=args.data_dir,
|
| 570 |
+
rotate=False, data_norm='min_max')
|
| 571 |
+
train_loader = DataLoader(
|
| 572 |
+
train_set, batch_size=args.batch_size, shuffle=True, num_workers=args.workers,
|
| 573 |
+
collate_fn=seq_collate_nba, pin_memory=True, drop_last=True)
|
| 574 |
+
test_loader = DataLoader(
|
| 575 |
+
test_set, batch_size=args.test_batch, shuffle=False, num_workers=args.workers,
|
| 576 |
+
collate_fn=seq_collate_nba, pin_memory=True)
|
| 577 |
+
return train_loader, test_loader
|
| 578 |
+
|
| 579 |
+
|
| 580 |
+
def unpack(data, device):
|
| 581 |
+
feats = data['past_traj'].to(device) # [B,A,T,6] normalized
|
| 582 |
+
past_orig = data['past_traj_original_scale'].to(device) # [B,A,T,6] metric
|
| 583 |
+
gt_disp = data['fut_traj_original_scale'].to(device) # [B,A,Tf,2] displacement
|
| 584 |
+
cur_xy = past_orig[:, :, -1, 0:2] # centered-abs last pos
|
| 585 |
+
gt_center = gt_disp + cur_xy[:, :, None, :] # centered-abs future
|
| 586 |
+
return feats, cur_xy, gt_disp, gt_center
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
# ============================================================================
|
| 590 |
+
# EVAL (marginal min-of-K=20 at 4.0 s; identical to eval_perscene_moflow.py)
|
| 591 |
+
# ============================================================================
|
| 592 |
+
@torch.no_grad()
|
| 593 |
+
def evaluate(model, loader, device, levels, end):
|
| 594 |
+
model.eval()
|
| 595 |
+
ade_sum = fde_sum = 0.0
|
| 596 |
+
n = 0
|
| 597 |
+
for data in loader:
|
| 598 |
+
feats, cur_xy, gt_disp, _ = unpack(data, device)
|
| 599 |
+
out = model(feats, cur_xy)
|
| 600 |
+
pred = out[f'level_{levels}_interactions'][..., :2] # [B,A,M,T,2]
|
| 601 |
+
pred_disp = pred - cur_xy[:, :, None, None, :] # -> displacement
|
| 602 |
+
d = torch.norm(pred_disp - gt_disp[:, :, None], dim=-1) # [B,A,M,T]
|
| 603 |
+
ade = d[..., :end].mean(-1).min(dim=-1).values # [B,A] best-of-K
|
| 604 |
+
fde = d[..., end - 1].min(dim=-1).values # [B,A]
|
| 605 |
+
ade_sum += ade.sum().item()
|
| 606 |
+
fde_sum += fde.sum().item()
|
| 607 |
+
n += ade.numel()
|
| 608 |
+
return ade_sum / max(n, 1), fde_sum / max(n, 1)
|
| 609 |
+
|
| 610 |
+
|
| 611 |
+
# ============================================================================
|
| 612 |
+
# TRAIN
|
| 613 |
+
# ============================================================================
|
| 614 |
+
def parse_args():
|
| 615 |
+
p = argparse.ArgumentParser('GameFormer standalone predictor for NBA')
|
| 616 |
+
p.add_argument('--data_dir', default='/mnt/jaewoo4tb/srtp/MoFlow/data/nba', type=str)
|
| 617 |
+
p.add_argument('--gpu', default='0', type=str)
|
| 618 |
+
p.add_argument('--exp', default='sragf_nba', type=str)
|
| 619 |
+
p.add_argument('--epochs', default=50, type=int)
|
| 620 |
+
p.add_argument('--batch_size', default=128, type=int)
|
| 621 |
+
p.add_argument('--test_batch', default=500, type=int)
|
| 622 |
+
p.add_argument('--workers', default=4, type=int)
|
| 623 |
+
p.add_argument('--seed', default=3407, type=int) # GameFormer default
|
| 624 |
+
|
| 625 |
+
# --- architecture (faithful GameFormer knobs) ---
|
| 626 |
+
p.add_argument('--modalities', default=20, type=int, help='K modes (NBA min-ADE_20)')
|
| 627 |
+
p.add_argument('--level', default=2, type=int, help='game-theoretic levels (paper: 3)')
|
| 628 |
+
p.add_argument('--encoder_layers', default=6, type=int)
|
| 629 |
+
p.add_argument('--agents', default=11, type=int)
|
| 630 |
+
p.add_argument('--past_len', default=10, type=int)
|
| 631 |
+
p.add_argument('--future_len', default=20, type=int) # 4.0 s @ 5 Hz
|
| 632 |
+
p.add_argument('--ball_idx', default=10, type=int, help='-1 to disable type emb')
|
| 633 |
+
|
| 634 |
+
# --- optimization (GameFormer: AdamW 1e-4, MultiStepLR x0.5, clip 5) ---
|
| 635 |
+
p.add_argument('--lr', default=1e-4, type=float)
|
| 636 |
+
p.add_argument('--weight_decay', default=1e-4, type=float)
|
| 637 |
+
p.add_argument('--grad_clip', default=5.0, type=float)
|
| 638 |
+
p.add_argument('--n_train', default=32500, type=int)
|
| 639 |
+
p.add_argument('--n_test', default=12500, type=int)
|
| 640 |
+
p.add_argument('--eval_every', default=2, type=int)
|
| 641 |
+
p.add_argument('--save_dir', default=os.path.join(
|
| 642 |
+
os.path.dirname(os.path.abspath(__file__)), 'sragf_nba_ckpt'), type=str)
|
| 643 |
+
return p.parse_args()
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
def main():
|
| 647 |
+
args = parse_args()
|
| 648 |
+
torch.manual_seed(args.seed)
|
| 649 |
+
np.random.seed(args.seed)
|
| 650 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 651 |
+
|
| 652 |
+
train_loader, test_loader = build_loaders(args)
|
| 653 |
+
|
| 654 |
+
n_types = 2 if (0 <= args.ball_idx < args.agents) else 1
|
| 655 |
+
model = GameFormerNBA(
|
| 656 |
+
n_agents=args.agents, past_dim=6, future_len=args.future_len,
|
| 657 |
+
modalities=args.modalities, levels=args.level, dim=D_MODEL, heads=N_HEADS,
|
| 658 |
+
enc_layers=args.encoder_layers, n_types=n_types, dt=0.2,
|
| 659 |
+
ball_idx=args.ball_idx).to(device)
|
| 660 |
+
n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 661 |
+
print(f'[SRA-GF] model params: {n_params/1e6:.2f} M | levels={args.level} '
|
| 662 |
+
f'K={args.modalities} enc_layers={args.encoder_layers} device={device}')
|
| 663 |
+
print(f'[SRA-GF] SRA components: neighbor-topN={USE_TOPN}(N={TOPN_K}) '
|
| 664 |
+
f'sigma-gate={USE_SIGMA} relational-future={USE_RELFUT} '
|
| 665 |
+
f'(all False == vanilla GameFormer)', flush=True)
|
| 666 |
+
|
| 667 |
+
opt = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
|
| 668 |
+
milestones = sorted(set(int(args.epochs * f) for f in (0.5, 0.65, 0.8, 0.9)))
|
| 669 |
+
milestones = [m for m in milestones if 0 < m < args.epochs]
|
| 670 |
+
sched = torch.optim.lr_scheduler.MultiStepLR(opt, milestones=milestones, gamma=0.5)
|
| 671 |
+
|
| 672 |
+
end = args.future_len # 4.0 s = frame 20
|
| 673 |
+
metric_idx = sorted(set([max(0, args.future_len // 2 - 1), args.future_len - 1]))
|
| 674 |
+
|
| 675 |
+
os.makedirs(args.save_dir, exist_ok=True)
|
| 676 |
+
best_ade = float('inf')
|
| 677 |
+
|
| 678 |
+
for epoch in range(args.epochs):
|
| 679 |
+
model.train()
|
| 680 |
+
t0 = time.time()
|
| 681 |
+
running = 0.0
|
| 682 |
+
nb = 0
|
| 683 |
+
for data in train_loader:
|
| 684 |
+
feats, cur_xy, _, gt_center = unpack(data, device)
|
| 685 |
+
out = model(feats, cur_xy)
|
| 686 |
+
loss = level_k_loss_marginal(out, gt_center, args.level, metric_idx)
|
| 687 |
+
opt.zero_grad()
|
| 688 |
+
loss.backward()
|
| 689 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| 690 |
+
opt.step()
|
| 691 |
+
running += float(loss.item())
|
| 692 |
+
nb += 1
|
| 693 |
+
sched.step()
|
| 694 |
+
print(f'[SRA-GF] epoch {epoch+1}/{args.epochs} loss={running/max(nb,1):.4f} '
|
| 695 |
+
f'lr={opt.param_groups[0]["lr"]:.2e} ({time.time()-t0:.1f}s)', flush=True)
|
| 696 |
+
|
| 697 |
+
if (epoch + 1) % args.eval_every == 0 or epoch == args.epochs - 1:
|
| 698 |
+
ade, fde = evaluate(model, test_loader, device, args.level, end)
|
| 699 |
+
print(f'[SRA-GF] epoch {epoch+1} ADE4={ade:.4f} FDE4={fde:.4f}', flush=True)
|
| 700 |
+
if ade < best_ade:
|
| 701 |
+
best_ade = ade
|
| 702 |
+
torch.save({'model': model.state_dict(), 'epoch': epoch + 1,
|
| 703 |
+
'ade4': ade, 'fde4': fde, 'args': vars(args)},
|
| 704 |
+
os.path.join(args.save_dir, f'{args.exp}_best.pt'))
|
| 705 |
+
print(f'[SRA-GF] saved best (ADE4={ade:.4f}) -> '
|
| 706 |
+
f'{os.path.join(args.save_dir, args.exp + "_best.pt")}', flush=True)
|
| 707 |
+
|
| 708 |
+
print(f'[SRA-GF] done. best ADE4={best_ade:.4f}', flush=True)
|
| 709 |
+
|
| 710 |
+
|
| 711 |
+
if __name__ == '__main__':
|
| 712 |
+
main()
|