Spatial-BEATs / docs /0421.md
dieKarotte's picture
Add files using upload-large-folder tool
86cbd36 verified
|
Raw
History Blame Contribute Delete
9.29 kB
# 2026-04-21 — v7f → v7f_ov123 多源逐帧扩展
## 背景
`run_ov1_v7f.sh` 在 ov1(每条 clip 1 个静止源)上跑出了可接受的 cls 与空间指标,
但用户需要更强的 per-frame 预测能力,并希望加入 ov2/ov3(2-3 源重叠)继续训练。
本次工作回答了 4 个问题,并落地了相应代码改动。
---
## 调研问答
### Q1:现有框架是否原生支持 ov2/ov3 多源训练?
**支持,无需改模型代码。**
- `spatial_dataset.py:827` `max_num_sources` 按 batch 动态算(1–4),并产出 `source_valid_mask`。
- `FrameTrackPredictionHeads``spatial_modules.py:1664-1720`)每个 K=4 query 独立输出 `[B, K, T_s, ·]` 的 activity / class / direction / distance,**多 slot 可在同一帧并发激活**
- `_match_frame_tracks_per_clip` + `compute_frame_track_losses``spatial_loss.py:1583-1730`)做 clip 级 Hungarian,把 GT N 个 source 匹配到 K 个 query,再在每个匹配 source 的 active 窗口内做逐帧监督。
- 多 manifest 接口已就绪:`train_manifest_paths` (`train_spatial_beats.py:177-179`) + CLI `--ov1-manifest --ov2-manifest --ov3-manifest`
### Q2:v7f 在 ov1 上的 perframe 在预测什么?监督怎么算?用了 active_time 吗?
**预测对象**(每帧、每 query 4 个量):
- `pred_activity`:sigmoid 后是"该 query 在该帧是否有事件"
- `pred_class_logits`:是什么事件
- `pred_direction`:3D 单位向量 DOA
- `pred_distance`:softplus 后的距离
颗粒度 ~2.5Hz(fused embedding,约 400ms / 帧)。
**监督链路(确实用了 `active_time`)**:
1. manifest `"active_time":[start,end]` → `spatial_dataset.py:534` 读出 `source_start_time_seconds` / `source_end_time_seconds`
2. `build_time_window_mask`(`spatial_loss.py:129-179`)按 clip_duration 比例换算成 T_s 上的 `window_mask[B, N_gt, T_s]`
3. clip 级 Hungarian 匹配 GT source ↔ K 个 query
4. `activity_target[b,k,t]=1` ⇔ 存在匹配到 k 的 GT 在 t 帧的 window_mask 内
5. activity 在所有 valid 帧 × K 上做 BCE(**真正的逐帧对比**),class/dir/dist 只在匹配的 source 的 active 帧上做 CE / `1-cos` / smooth-L1
### Q3:ov1-only 训练会让 perframe 退化为"重复同一个声源"吗?
**会。** ov1 每条 clip 只有 1 个 source,Hungarian 总把它路由到某个固定 query k\*:
- 1 个 query 学会"复制"输入声源
- 其余 K-1 个 query 的 activity 永远是负样本,class/dir/dist 通路无正梯度
- 在 ov2/ov3 上推理时,多个并发源会被堆到 k\*,表现为"重复预测同一个声源"
解决:在 ov1+ov2+ov3 混训上继续训练,让 K-1 个 query 也吃到正样本梯度。
### Q4:ov1 现在的随机裁剪策略是否需要在 ov123 调整?
**ov1 实际上没在裁剪。** v7 链用 `crop_mode="start"` + `max_clip_duration=20s`,ov1 音频本身 ≤20s,`_maybe_crop_sample` 在长度 ≤ max 时直接 return 原音频。"active_time 接近 [0, clip_dur]"是数据本身的特征:前 200 条 ov1 中 26% 的 source 占 ≥80% 的 clip。
**ov123 也不需要随机裁剪**:ov2/ov3 是 20s 固定长度,start crop 也是 no-op;多源 active_time 错开本身就提供了 perframe 时序变化信号。`_maybe_crop_sample` 的随机起点只锚定 `sources[0]`,对 ov2/ov3 反而可能切掉 source[1]/[2]。
### Q5:v7f 的 val_predictions 为什么是 clip 级的?
**当前 v7f 的设计漏洞。** v7f 设了 `enable_frame_track=True`(并行加 frame-track head),但 `supervision_mode` 仍是 `mono_ast`,example dump 走 `spatial_loss.py:820` 的 `_collect_mono_ast_examples`——它读的是 clip-level mono_ast head 输出,每条 sample 只输出 1 行 `(gt_class_index, pred_class_index, pred_azimuth_deg, ...)`。frame-track 的 collector 在 1850 行但没被触发。
切换 `supervision_mode="local_spatial_track"` 后会走 1850 collector,但**仍然只 dump 每个 (clip, track) 的第 1 个 active frame**(`first_t = nonzero(active_t)[0]`),不是真正的逐帧 dump。
### Q6:per-frame 训练为什么还要保留 clip-level 监督?
**不该保留。** mono_ast head 监督目标只读 `source_class_indices[idx, 0]` 等 source 0 的字段(`spatial_loss.py:820`),完全忽略 source 1/2/3。在 ov2/ov3 上等于明确要求 trunk "压制 source 1/2 的信号、只表征 source 0",与 frame-track 想要的"K 个 query 各自表征不同源"**直接对抗**
### Q7:dump 的 activity 阈值为什么要预设?
**不该预设。** sigmoid 输出本身是连续概率,BCE 训练让"有事件"的帧 → 1、"无事件"→ 0,"有几个源"是 K 个 query 的 prob 自然体现。Dump 时阈值化 = 丢失信息。
**正确做法**:dump 全部 K × T_s 行,每行带 `activity_prob`,事后任意截。
---
## 代码改动
### 1. 新 preset:`ov1_local_spatial_v7f_ov123` (`train_spatial_beats.py`)
继承 `v7f_spatial`,但切换到纯 per-frame track 监督,弃掉 clip-level mono_ast head:
```python
cfg.model.readout_scheme = "local_spatial_track"
cfg.loss.supervision_mode = "local_spatial_track"
cfg.loss.enable_frame_track_loss = False # supervision_mode 直接驱动
cfg.loss.lambda_frame_activity = 1.0
cfg.loss.lambda_frame_class = 1.0
cfg.loss.lambda_frame_direction = 4.0
cfg.loss.lambda_frame_distance = 1.0
cfg.loss.lambda_clip_aux = 0.0
cfg.train_manifest_paths = (ov1, ov2, ov3)
cfg.val_manifest_paths = (ov1, ov2, ov3)
cfg.test_manifest_paths = (ov1, ov2, ov3)
cfg.dump_frame_track_csv = True
cfg.frame_track_csv_max_samples_per_epoch = 32
cfg.output_dir = "checkpoints/spatial_beats_ov1_local_spatial_v7f_ov123_exp/03_ov123"
```
注册到 `--preset` 选项与 dispatcher。
### 2. DCASE 逐帧 CSV dump
**`spatial_loss.py` 新增 `collect_frame_track_csv_rows()`**:对每条 sample 产出
- `gt_rows`:每个有效 source × 在 `window_mask & valid_time` 内的每一帧 → 一行(`activity_prob=1.0`)
- `pred_rows`:**全部 K=4 track × 全部 valid_time 帧 → 一行**,无任何阈值过滤,连续 `activity_prob`
**`train_spatial_beats.py`**
- `TrainSpatialBEATsConfig` 加 `dump_frame_track_csv: bool = False`、`frame_track_csv_max_samples_per_epoch: int = 32`
- `evaluate_one_epoch` 返回值 2-tuple → 3-tuple,多带 `csv_samples`,仅 `local_spatial_track` + dump 开启时累积(capped at 32)
- 新增 `dump_frame_track_csvs()`:用 `final_vocabulary.csv``class_idx → class_name` 映射,每条样本写到
```
<output_dir>/val_predictions/epoch_XXXX_csv/<sample_id>__gt.csv
<output_dir>/val_predictions/epoch_XXXX_csv/<sample_id>__pred.csv
```
- 主循环在 epoch 结束时调用
**CSV schema**(GT/pred 一致,方便 diff / pandas merge):
```
frame_idx, frame_time_s, src_or_track_idx, class_idx, class_name,
azimuth_deg, elevation_deg, distance_m, activity_prob
```
### 3. shell 入口:`run_ov1_v7f_ov123.sh`
- `--preset ov1_local_spatial_v7f_ov123`
- `--resume checkpoints/spatial_beats_ov1_local_spatial_v7_exp/01_classwarmup/best.pt`(v7 stage1,trunk 已收敛 cls=71.6%)
- `--ov1/2/3-manifest` 三个 manifest 都传
- `MASTER_PORT=29548` 避免与 v7f 并跑冲突
- 默认 `GPUS=8 BATCH_SIZE=4 SPATIAL_EPOCHS=20 SPATIAL_LR=1.5e-5`
热启动注意:
- trunk / local_spatial_encoder / local_spatial_prediction_heads 参数名在 `local_spatial` 与 `local_spatial_track` 两个 scheme 下一致,可直接加载
- SourceQueryDecoder / FrameTrackPredictionHeads 是新模块,从 checkpoint 缺失 → 随机初始化(属预期行为)
---
## 关键文件参考
| 文件 | 行 | 用途 |
| --- | --- | --- |
| `spatial_modules.py` | 1664-1720 | `FrameTrackPredictionHeads`,K=4 query × per-frame 输出 |
| `spatial_loss.py` | 129-179 | `build_time_window_mask`,active_time → 帧 mask |
| `spatial_loss.py` | 1583-1636 | `_match_frame_tracks_per_clip`,clip 级 Hungarian |
| `spatial_loss.py` | 1639-1740 | `compute_frame_track_losses`,per-frame BCE/CE/cos/smoothL1 |
| `spatial_loss.py` | (新) | `collect_frame_track_csv_rows`,DCASE dump |
| `spatial_dataset.py` | 534, 827-867 | `active_time` 读取 + 多源 collation |
| `train_spatial_beats.py` | 1262-1300 | `make_ov1_local_spatial_v7f_ov123_config` |
| `train_spatial_beats.py` | (新) | `dump_frame_track_csvs` |
| `run_ov1_v7f_ov123.sh` | — | shell 入口 |
---
## 验证清单
1. `python3 -c "import ast; ast.parse(open('train_spatial_beats.py').read()); ast.parse(open('spatial_loss.py').read())"` 已通过
2. Smoke test(远端跑):
```bash
GPUS=2 BATCH_SIZE=2 SPATIAL_EPOCHS=1 ./run_ov1_v7f_ov123.sh
```
关注:
- `frame_activity_loss > 0` 且多个 query 都收到非零梯度
- `val_predictions/epoch_0000_csv/` 下出现 32 对 CSV
- 同一 `frame_idx` 下 K=4 个 track 的 `activity_prob` 应当呈现差异(而非 4 个全等)
3. **退化诊断**:训练 1-2 epoch 后,统计同帧 K query 输出的 DOA 两两余弦相似度——若仍接近 1,说明 query 间没差异化,需加 query 多样性正则;若开始分散,说明 ov123 混训奏效。
4. **指标对比** vs v7f baseline:
- ov1 子集:cls / DOA / dist 不显著下降
- ov2/ov3 子集:frame_activity F1 与 DOA error 明显改善