# MoFlow 최신본 — 이전 버전 대비 무엇이 바뀌었나 비교 기준: HF `po03087/sra-trajectory-code` 커밋 `0d52562` → `37c61d4` 바뀐 MoFlow 파일은 **2개, 총 88줄**이다. 그 중 실제 로직은 5줄이고 나머지는 주석이다. | 파일 | 변경 | 성격 | |---|---|---| | `models/graph_interaction_nba.py` | +23 | **버그 수정** (edge index) | | `models/graph_interaction_nba_v6.py` | +65 | **새 안정화 옵션** 3종 | > **모든 새 동작은 환경변수 토글 뒤에 있고 기본값은 전부 off다.** > 토글을 안 켜면 이전 코드와 비트 단위로 동일하게 동작한다 — 기존 체크포인트와 > 진행 중인 실행이 그대로 재현된다. --- ## 1. `graph_interaction_nba.py` — edge index 씬 혼합 버그 ### 바뀐 코드 ```diff def _make_batched_edge_index(self, num_scenes): single = self._single_edge_index # [2, E0] offsets = torch.arange(num_scenes, ...) * self.num_agents batched = single.unsqueeze(0).expand(num_scenes,-1,-1) + offsets.view(-1,1,1) - return batched.reshape(2, -1) # [2, S*E0] + if os.environ.get('SRA_EDGE_FIX', '') not in ('', '0', 'false', 'False'): + # [S,2,E0] -> [2,S,E0] -> [2,S*E0]: src/dst 축을 먼저 앞으로 옮긴다 + return batched.permute(1, 0, 2).reshape(2, -1) + return batched.reshape(2, -1) # legacy (씬 혼합) ``` ### 왜 틀렸나 `batched` 의 shape 는 `[S, 2, E0]` 이고 메모리 배치는 ``` s0_src s0_dst s1_src s1_dst s2_src s2_dst ... ``` 이다. 여기에 `.reshape(2, -1)` 을 하면 앞 절반이 row 0, 뒤 절반이 row 1 이 되므로 **row 0 = "앞쪽 씬들의 src+dst 전부", row 1 = "뒤쪽 씬들의 src+dst 전부"** 가 된다. src/dst 행이 아니라 **씬 블록**으로 잘린 것이다. ### 측정된 영향 (A=11) | 지표 | 수정 전 | 수정 후 | |---|---|---| | 같은 씬 안에 머무는 edge 비율 | **0 %** | **100 %** | | 들어오는 edge 가 하나도 없는 노드 | **절반** | 0 | | 나머지 노드의 degree | 의도의 **2배** | 정상 | | top-N 이웃이 올바르게 묶이는 비율 | 33 % | 100 % | 즉 "예측된 미래 위에서 씬 내부 top-N 이웃을 고른다"는 SRA 의 설명과 실제 코드가 달랐다. 논문 수치(MID 0.957 / LED 0.778 / MoFlow 0.695)는 전부 이 버그 상태에서 나온 값이다. --- ## 2. `graph_interaction_nba_v6.py` — 안정화 옵션 3종 edge 를 고치자 **MoFlow 만** 새 문제가 생겼다. 학습 loss 는 단조 감소하는데 **샘플링이 발산**한다. 원인은 train/sample mismatch 다. - 학습: 랜덤 timestep 하나에서 그래프를 **한 번** 적용 - 샘플링: 10 스텝 flow 적분에서 **매 스텝** 적용하고 그 출력이 다음 스텝 입력으로 되먹임 버그 상태에서는 노드 절반이 고아라 섭동이 약해서 이 누적이 드러나지 않았다. edge 를 고쳐 모든 노드가 이웃을 받자 섭동이 커지고 10 스텝에 걸쳐 기하급수로 쌓인다. MID·LED 는 iterative denoiser 라 이 현상이 없다. **아래 옵션은 MoFlow 전용 처방이다.** ### 2.1 `SRA_SOFT_START` — 초기 충격 제거 ```python if os.environ.get('SRA_SOFT_START', ...): nn.init.zeros_(self.out_proj.weight); nn.init.zeros_(self.out_proj.bias) nn.init.constant_(gate_proj_linear.bias, SRA_GATE_BIAS) # 기본 -4, sigmoid(-4)≈0.018 ``` MID 가 원래 쓰던 방식(zero-init + warmup gate)을 V6 로 옮긴 것. 둘 다 **학습 가능** 하게 남아 있어서 고정 축소와 달리 그래프가 나중에 제 강도까지 자랄 수 있다. **단독으로는 발산을 막지 못했다.** ### 2.2 `SRA_GATE_SCALE` — 전역 축소 ```python res = SRA_GATE_SCALE * gate * self.out_proj(nodes) # 기본 1.0 = off ``` 가장 단순한 처방이지만 **가장 빨리 발산했다(3회차). 폐기.** ### 2.3 `SRA_RES_CAP` / `SRA_RES_CAP_REL` — residual norm 상한 ```python _cap = float(os.environ.get('SRA_RES_CAP', 0.0) or 0.0) # 절대 상한 if _cap > 0: rn = res.norm(dim=-1, keepdim=True) res = res * torch.where(rn > _cap, _cap / rn.clamp_min(1e-6), torch.ones_like(rn)) _rel = float(os.environ.get('SRA_RES_CAP_REL', 0.0) or 0.0) # 상대 상한 ★ if _rel > 0: lim = _rel * orig.norm(dim=-1, keepdim=True) # 노드마다 자기 임베딩 크기에 비례 rn = res.norm(dim=-1, keepdim=True) res = res * torch.where(rn > lim, lim / rn.clamp_min(1e-6), torch.ones_like(rn)) out = orig + res ``` **절대 cap 은 값을 낮춰도 발산 시점을 미룰 뿐이었다:** | 절대 cap | 결과 | |---|---| | 6.0 | 4회차 발산 | | 3.0 | 5회차 발산 | | 1.0 | 8회차 best 0.9662 → 이후 발산, 최근 1.41 | | 0.5 | 10회차 best 0.8732 → 12회차부터 반등, 최근 1.41 | 이유는 두 가지다. (a) residual norm 은 호스트 임베딩 스케일 위에 있어서 MoFlow 에서 튜닝한 값이 MID/LED 에서는 의미가 달라진다. (b) `x ← x + res` 의 누적을 지배하는 것은 절대 크기가 아니라 **비율 `‖res‖/‖x‖`** 인데 절대 cap 은 이걸 직접 통제하지 못한다. 상대 cap 은 노드마다 `‖res‖ ≤ r·‖orig‖` 로 **비율을 직접** 묶는다. 이것만 살아남았다. ### ⚠️ 같이 고친 것 — 이전 cap 구현의 dead-gradient 버그 처음 쓴 cap 은 `res * (rn.clamp(max=cap) / (rn + 1e-6))` 이었는데 두 가지가 틀렸다. 1. **`res = 0` 에서 gradient 가 정확히 0.** 스케일이 `0/1e-6 = 0` 이고 Jacobian 도 0. `SRA_SOFT_START`(out_proj zero-init)와 같이 켜면 out_proj 가 0 에 영구히 갇혀 **그래프가 전혀 학습되지 않는다.** NaN 이 아니라 조용히 죽는다. | 설정 | out_proj gradient 합 | |---|---| | SOFT_START 만 | 78,751 | | RES_CAP 만 | 765,506 | | **SOFT_START + RES_CAP** | **0.00** ← 학습 불가 | 실제로 그 조합으로 돌린 실행의 체크포인트는 58 epoch 뒤에도 `future_graph.out_proj` 가 정확히 `0.000000e+00` 이었다. 즉 그 실행들은 host 단독(=baseline)을 측정한 것이고, "cap 이 발산을 해결했다"던 결론은 무효였다. 해당 결과 4건은 폐기했다. 2. **cap 미만 값도 부당하게 축소된다.** `rn/(rn+1e-6)` 은 `rn` 이 작을수록 1 에서 멀어진다 — `‖res‖=1e-5` 에서 **0.909 배**. `torch.where` 로 바꾸면 둘 다 해결된다. | 검증 | old | new | |---|---|---| | `res=0` 에서 gradient | 0.0000 | **31.30** | | `‖res‖=1e-5` 일 때 스케일 | 0.909 | **1.000** | | cap 초과 시 상한 | 3.0 | 3.0 (동일) | | 정상 구간 gradient 차이 | — | 상대차 6e-7 | --- ## 3. 토글 요약 | 토글 | 기본 | 역할 | 판정 | |---|---|---|---| | `SRA_EDGE_FIX` | off | edge index 씬 혼합 수정 | **필수** | | `SRA_RES_CAP_REL` | 0 (off) | `‖res‖ ≤ r·‖orig‖` 비율 상한 | **유일한 생존 처방** | | `SRA_RES_CAP` | 0 (off) | 절대 상한 | 두 값 모두 발산, 열등 | | `SRA_SOFT_START` | off | out_proj zero-init + gate bias | 단독 불충분. **RES_CAP 과 병용 금지** (§2.3) | | `SRA_GATE_BIAS` | −4.0 | SOFT_START 의 gate bias | — | | `SRA_GATE_SCALE` | 1.0 (off) | 전역 축소 | 3회차 발산, 폐기 | --- ## 4. 지금 실제로 돌리는 설정 ```bash SRA_EDGE_FIX=1 SRA_RES_CAP_REL=0.03 \ CUDA_VISIBLE_DEVICES=1 python fm_nba_graph_v6.py \ --cfg cfg/nba/cor_fm.yml --exp v3_rel003 \ --batch_size 192 --epochs 150 --fm_in_scaling --tied_noise \ --top_n_neighbors 5 --uncertainty_weight 0.01 --data_dir ./data/nba ``` eval 별 min-ADE₂₀ @4.0s (24회차, 4170/25500 = 16 % 진행): | 설정 | 1–6 | 7–12 | 13–18 | 19–24 | |---|---|---|---|---| | **relcap 0.03** | 1.190 1.010 0.979 1.019 0.971 0.941 | 0.912 0.894 0.894 0.857 0.863 0.863 | 0.840 0.828 0.832 0.818 **0.816** 0.819 | **0.803** 0.823 0.808 0.818 0.807 0.820 | | relcap 0.10 | 1.128 1.017 0.988 0.954 0.950 0.944 | 0.958 0.936 0.880 0.889 0.857 0.862 | 0.876 0.863 0.893 0.873 0.871 0.874 | 0.869 0.915 0.873 0.867 **0.830** 0.894 | | cap 0.5 (절대) | 1.155 1.032 1.018 0.990 0.978 0.907 | 0.937 0.901 0.931 **0.873** 0.875 0.922 | 0.958 → 발산 | 종료 (최근 1.41) | | cap 1.0 (절대) | 1.146 1.013 0.985 1.020 0.978 1.006 | 1.170 **0.966** 1.309 1.378 1.150 1.344 | 발산 | 종료 (최근 1.41) | | cap 없음 | 1.141 1.052 1.048 **1.828** 1.429 1.716 | 발산 | — | — | 현재 best (ADE/FDE 는 **같은 평가 시점**에서 짝지음): | 설정 | best ADE / FDE | @eval | 평가 횟수 | |---|---|---|---| | **relcap 0.03** | **0.8029 / 0.9635** | 19 | 24 (안정) | | relcap 0.10 | 0.8302 / 1.0857 | 23 | 24 (진동) | --- ## 5. 이 변경이 논문 수치에 의미하는 것 — 정직한 상태 **① 논문의 MoFlow+SRA = 0.695 는 버그 상태의 값이다.** 씬을 가로지르는 그래프로 얻은 값이므로, "예측된 미래 위의 씬 내부 sparse graph 덕분"이라는 논문의 설명과 그 수치를 만든 코드가 일치하지 않는다. **② 수정 후 재학습은 아직 0.695 에 도달하지 못했다.** 현재 0.8029 (16 % 진행). 남은 84 % 와 cosine LR 감쇠에서 더 내려가야 하며, **도달하지 못할 가능성은 열려 있다.** **③ `SRA_RES_CAP_REL` 은 논문에 없는 항이고 추론 시에도 적용된다.** 따라서 이건 하이퍼파라미터가 아니라 **메서드 변경**이다. 쓴다면 논문 본문에 기술해야 하고, 안 쓰면 edge 수정본 MoFlow 는 아예 학습되지 않는다 (`SRA_EDGE_FIX=1` 만으로는 4회차에 1.828 로 발산). **④ 다른 호스트는 이 처방을 쓰지 않는다.** MID 는 `graph_gate_init`/`warmup`, LED 는 처방 없이 학습된다. 상대 cap 을 다른 호스트에 적용하려면 그 호스트의 residual/embedding 비율을 먼저 측정해야 한다. **⑤ 비율값 튜닝이 끝나지 않았다.** 0.03 이 0.10 보다 낫다. 더 조인 값(0.01)이 나을지, 0.03 이 이미 과한 제약인지는 미검증이다.