0416 Spatial-BEATs 实验记录
本文档记录 2026-04-16 对话期间的所有改动、实验和结果。
1. Bug 修复
1.1 方位角坐标系修复(val_predictions 显示 bug)
问题:val_predictions jsonl 文件中 gt_azimuth_deg 是 DCASE 标准 [-180°, 180°],但 pred_azimuth_deg 被 torch.remainder(..., 360.0) 转成了 [0°, 360°]。导致 GT=-168° 和 Pred=192° 看起来差 360° 但实际是同一个方向。
影响:仅影响 val_predictions 文件的可读性。训练 loss 和 epoch log metrics 不受影响(_circular_distance_deg 对两种坐标系都能正确计算圆周距离)。
修复(spatial_loss.py):
_azi_ele_deg_from_direction_vector:去掉torch.remainder(..., 360.0),atan2直接返回[-180°, 180°]- 新增
_to_dcase_azimuth()工具函数 build_validation_examples、build_pretrunk_ast_validation_examples中 pred_azi 使用_to_dcase_azimuth转换
1.2 Vocabulary 标签修复(65→63 类)
问题 1:female_singing(171例) 和 male_singing(104例) 与 singing 是父子关系,模型在 65 类 softmax 下完全区分不了(test accuracy=0%)。
问题 2:string_instrument 类中有 1644 个样本(22.8%)是 Hi-hat/Crash_cymbal/Cymbal 打击乐,被错误映射。这些应该归入 percussion。
修复(fix_vocabulary_and_manifests.py):
female_singing+male_singing→ 合并到singing(65→63 类)string_instrument中mono_primary_label为 Hi-hat/Crash_cymbal/Cymbal 的样本 → 重标为percussion- 对 ov1/ov2/ov3 manifest 全部原地修复,备份为
.bak_20260416 final_vocabulary.csv重编号为连续的 1~63
受影响文件:
| 文件 | 变更 |
|---|---|
final_vocabulary.csv |
65→63 类,去掉 female_singing/male_singing |
ov1_foa.jsonl |
460 条 singing 合并 + 1644 条 cymbal 修正 |
ov2_foa.jsonl / ov3_foa.jsonl |
无变动(这些类别未出现) |
代码 default 值更新:
| 文件 | 变更 |
|---|---|
spatial_beats.py |
source_num_classes: 65 → 63 |
spatial_dataset.py |
num_classes: 65 → 63 |
spatial_modules.py |
6 处函数参数默认值 65 → 63 |
spatial_atst.py |
source_num_classes: 65 → 63 |
1.3 load_checkpoint strict 改为 non-strict
问题:stage1→stage2 resume 时,如果两阶段 model config 不完全一致(比如 stage1 无 semantic_anchor 但 stage2 有),strict=True 会报错 missing key。
修复(train_spatial_beats.py):load_checkpoint 改用 strict=False,missing/unexpected key 打印 warning 不报错。
2. v2 Test 集完整评测
编写了 eval_spatial_beats.py 评测脚本,在 ov1 完整 test 集(1800 样本) 上评测了 v2 stage2 best.pt。
checkpoint:checkpoints/spatial_beats_ov1_local_spatial_v2_exp/02_spatial/best.pt
| 指标 | 值 |
|---|---|
| class_acc | 43.83% |
| azi_mae | 19.87° |
| ele_mae | 8.66° |
| dist_mae | 0.554 m |
| SELD F1 | 0.3139 |
| SELD LR | 0.6956 |
| SELD LE | 8.64° |
| SELD Score ↓ | 0.6027 |
63 类映射后(post-hoc)
| 指标 | 65类 | 63类映射 |
|---|---|---|
| class_acc | 43.83% | 44.44% (+0.6%) |
| 空间指标 | 不变 | 不变 |
提升来自 singing 子类合并(+11 个正确样本)。
Per-class 分析亮点
- Accuracy=0 的 4 个类:female_singing, male_singing, tape, wood
- Accuracy≥0.7 的 5 个类:bird(70.6%), guitar(70.9%), thunderstorm(76%), knock(77.8%), car(100%)
- 最大混淆来源:父子标签层级冲突(guitar↔keyboard_instrument, wind_instrument→musical_instrument, male_singing→singing)
3. bypass / purify 实验结果
两个新架构实验的 stage1 都全 trunk 解冻(12 层),目标是用更激进的策略达到更高 class_acc。
| 实验 | Stage1 策略 | Stage1 class_acc | Stage2 SELD |
|---|---|---|---|
| bypass | 全解冻 + bypass_local_fusion + 零空间 | ~25% ❌ | 0.743 |
| purify | 全解冻 + freeze_local_spatial + 零空间 | ~25% ❌ | 0.816 |
| v2(对照) | top-2 + semantic_anchor + 空间多任务 | ~56% ✅ | 0.603 |
结论:全 trunk 解冻 + SpatialBEATs 架构 = 崩。top-2 解冻更稳定。
4. 分类能力损失分析
纯 BEATs 分类实验(无空间任务)
| 解冻策略 | val_acc |
|---|---|
| head_only(trunk 全冻) | 62.6% |
| top-8(层 4-11) | 69.1% |
| full(全 12 层) | 70.0% |
SpatialBEATs 分类损失链
纯 BEATs 62.6%(trunk 全冻)
→ SpatialBEATs v2 stage1: 56% (-6.6%: CNN噪声 + 多任务干扰 + 只解冻top-2)
→ SpatialBEATs v2 stage2: 44% (-12%: λ_dir=12 空间梯度冲击语义)
关键发现:v4 的 trunk 起点
v4 config 中 class_finetuned_ckpt 之前指向的是 head_only/best.pt(62.6%),其 trunk 权重和原始 BEATs 完全相同(head_only 全冻 trunk 训练)。已修正为指向 02_full/best.pt(70%),获取 FSD50K 适配过的 trunk。
5. 失败实验
v3 / v3ws(bypass + top-8/top-4)
- v3 epoch1: cls=0.116 ❌
- v3ws epoch1: cls=0.080 ❌
- 原因:bypass 模式 + SpatialBEATs = DDP 不稳定
v3b / v3bws(freeze_local_spatial + top-8/top-4)
- v3b 15 epoch: cls=35.67%,azi=89.66°(空间几乎随机)
- v3bws: 无效果
- 原因:多变量同时改动(top-8 + freeze_local_spatial + ddp_find_unused + 无 anchor),不如 v2 的组合
6. 新增架构:Frame-Level Track Supervision
动机
当前 mono_ast 是 clip-level 预测(attention pool → 1 class + 1 direction),无法:
- 输出 DCASE 格式逐帧检测
- 扩展到 ov2/ov3 多源
- 约束 trunk 逐帧表征质量
实现
在 readout_scheme="local_spatial" 下新增可选 FrameTrack 分支,与 clip-level head 并行运行:
fused_spatial_embeddings [B, T_s, D]
├── attention pool → clip-level mono_ast prediction (已有)
└── SourceQueryDecoder → [B, K, T_s, D] → FrameTrack prediction (新增)
完全复用已有代码:SourceQueryDecoder、FrameTrackPredictionHeads、compute_frame_track_losses 一行都没改。
控制开关:
SpatialBEATsConfig.enable_frame_track: bool = FalseSpatialLossConfig.enable_frame_track_loss: bool = False- 默认关闭,所有现有实验零影响
改动文件:
| 文件 | 改动 |
|---|---|
spatial_beats.py |
Config flag + init 创建 head + forward 产出 |
spatial_loss.py |
Config flag |
train_spatial_beats.py |
run_train_step 追加 loss + validate 追加 examples + preset |
spatial_modules.py |
不改 |
新 preset:ov1_local_spatial_v4f_spatial
7. 当前实验矩阵
正在跑
| 实验 | 状态 | 配置要点 |
|---|---|---|
| v4 stage1 | 运行中 | v2 架构复刻 + 63 类 + 70% trunk init |
等 v4 stage1 结束后
| 实验 | 脚本 | 配置要点 |
|---|---|---|
| v4 stage2 | run_ov1_v4.sh |
v2 复刻(λ_dir=12, anchor=0.5) |
| v4g stage2 | run_ov1_v4g.sh |
温和版(λ_dir=6, λ_cls=2, anchor=1.5) |
| v4f stage2 | run_ov1_v4f.sh |
v4 + 并行 frame-level track head |
预期效果
| 实验 | 预期 class_acc | 预期 azi_mae | 新能力 |
|---|---|---|---|
| v4 stage2 | ~44%(同 v2) | ~20° | baseline |
| v4g stage2 | ~50%+ | ~25-30° | 分类更好,空间稍差 |
| v4f stage2 | ~44% + frame metrics | ~20° | DCASE 逐帧输出 |
8. 新增文件清单
| 文件 | 用途 |
|---|---|
eval_spatial_beats.py |
独立评测脚本,支持所有 preset |
fix_vocabulary_and_manifests.py |
一次性 vocab+manifest 修复脚本 |
run_ov1_v3.sh |
v3 实验(bypass,已失败) |
run_ov1_v3ws.sh |
v3ws 实验(bypass+warmstart,已失败) |
run_ov1_v3b.sh |
v3b 实验(freeze_local_spatial,效果差) |
run_ov1_v3bws.sh |
v3bws 实验(同上+warmstart) |
run_ov1_v4.sh |
v4 实验(v2 复刻 + 63 类 + 70% trunk) |
run_ov1_v4g.sh |
v4g 温和空间版 |
run_ov1_v4f.sh |
v4f frame-level track 版 |
9. 关键经验总结
不要全解冻 trunk:纯 BEATs 全解冻 OK(70%),但 SpatialBEATs 架构下全解冻必崩(25%)。top-2 是当前唯一验证过的安全策略。
bypass_local_fusion 不可用:在 DDP 训练下导致不稳定,即使只是 top-8 解冻也会崩。freeze_local_spatial 稍好但仍差。
Semantic anchor 有效:v2 的 anchor(λ=0.5)让 stage2 class_acc 从 25%(无 anchor 的 kaldi_spatial)保到 44%。
标签质量是分类瓶颈:65 类中父子层级冲突(singing/female_singing、musical_instrument/string_instrument)和标签映射 bug(cymbal→string_instrument)贡献了大量"假错误"。
trunk 初始化很重要:v4 之前一直用的是
head_only/best.pt(trunk ≡ 原始 BEATs),现在改为02_full/best.pt(trunk 已适配 FSD50K,70%),预期 stage1 起点更高。一次只改一个变量:v3b/v3bws 同时改了 5 个变量导致无法诊断,v4 只改了 trunk init 这一项。