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:827max_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 单位向量 DOApred_distance:softplus 后的距离
颗粒度 ~2.5Hz(fused embedding,约 400ms / 帧)。
监督链路(确实用了 active_time):
- manifest
"active_time":[start,end]→spatial_dataset.py:534读出source_start_time_seconds/source_end_time_seconds build_time_window_mask(spatial_loss.py:129-179)按 clip_duration 比例换算成 T_s 上的window_mask[B, N_gt, T_s]- clip 级 Hungarian 匹配 GT source ↔ K 个 query
activity_target[b,k,t]=1⇔ 存在匹配到 k 的 GT 在 t 帧的 window_mask 内- 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:
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 = 32evaluate_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 入口 |
验证清单
python3 -c "import ast; ast.parse(open('train_spatial_beats.py').read()); ast.parse(open('spatial_loss.py').read())"已通过- Smoke test(远端跑):
关注:GPUS=2 BATCH_SIZE=2 SPATIAL_EPOCHS=1 ./run_ov1_v7f_ov123.shframe_activity_loss > 0且多个 query 都收到非零梯度val_predictions/epoch_0000_csv/下出现 32 对 CSV- 同一
frame_idx下 K=4 个 track 的activity_prob应当呈现差异(而非 4 个全等)
- 退化诊断:训练 1-2 epoch 后,统计同帧 K query 输出的 DOA 两两余弦相似度——若仍接近 1,说明 query 间没差异化,需加 query 多样性正则;若开始分散,说明 ov123 混训奏效。
- 指标对比 vs v7f baseline:
- ov1 子集:cls / DOA / dist 不显著下降
- ov2/ov3 子集:frame_activity F1 与 DOA error 明显改善