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
  • FrameTrackPredictionHeadsspatial_modules.py:1664-1720)每个 K=4 query 独立输出 [B, K, T_s, ·] 的 activity / class / direction / distance,多 slot 可在同一帧并发激活
  • _match_frame_tracks_per_clip + compute_frame_track_lossesspatial_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_maskspatial_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 framefirst_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:

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**:

  • TrainSpatialBEATsConfigdump_frame_track_csv: bool = Falseframe_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.csvclass_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_spatiallocal_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(远端跑):
    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 明显改善