# 2026-04-22 记录:v7h 基线、v8 融合升级与当前诊断 本文档记录 2026-04-22 这轮关于 `v7h -> v8` 的设计、问答、结果和代码修改。 相关旧文档: - `docs/0422_v7h_v7j.md`:主要记录 `v7h / v7j` 的诊断和尝试 本文聚焦: - 当前最可用版本 `v7h` 到底是什么 - 为什么要做 `v8` - `v8` 的结构改了什么,没改什么 - 目前 `epoch0/epoch1` 的真实现象 - 后续应该继续看什么 ## 1. 当前结论 截至本轮: - 当前最可用的稳定基线仍然是 `v7h` - `v8` 不是坏形态,值得继续训练 - `v8` 当前的主要问题不是 duplicate 崩坏,而是 `ov2/ov3` 上的 class binding 还不稳,尤其 `ov3` 仍有明显 class collapse - `v8` 现在还处于 two-stage 的 stage 1,前 3 个 epoch 本来就不训 `dir/dist`,所以 `F20 / LE_CD / oazi` 在 `epoch0-2` 不适合过早下结论 ## 2. 关键问答与结论 ### 2.1 BEATs 适不适合做逐帧、多源预测 结论: - 原始 `BEATs` 官方下游头更偏 `clip-level audio tagging` - 但 `BEATs` 作为语义 backbone 是适合逐帧任务的 - 真正不够的是“直接拿官方 head 做 strong-label / 多源 SELD” 对当前项目更准确的判断是: - `BEATs` 适合作为语义分支 - `local spatial branch` 适合作为空间分支 - 真正的难点在于: - 如何融合语义/空间表征 - 如何让 query decoder 在同一时刻分辨多个源 - 如何让 per-frame class / DoA 监督不互相拖累 ### 2.2 现在 4 个 track 到底在预测什么 当前 `local_spatial_track` 路线的输出是: - 每帧固定产出 `K=4` 组 candidate tracks - 每条 track 在每帧预测: - `activity` - `class` - `direction` - `distance` 它不是“每帧一定有 4 个真实声源”,而是“每帧最多从 4 个候选槽位里激活若干条”。 结构链路是: ```text fused_embeddings [B, T_s, D] -> SourceQueryDecoder -> track_latents [B, K, D] -> track_time_features [B, K, T_s, D] -> FrameTrackPredictionHeads -> pred_activity -> pred_class_logits -> pred_direction -> pred_distance ``` ### 2.3 `oracle_cls / oracle_azi / oracle_ele` 是什么 这几个量是 `track` 头的 oracle 诊断指标,不是官方 DCASE metric。 定义: - 先在 GT-active 的 frame-source 对上做 matching - matching 时不走 activity threshold,只看 head 本身 - 在 matched pair 上统计: - `oracle_cls`:class 是否对 - `oracle_azi`:azimuth MAE - `oracle_ele`:elevation MAE 用途: - 看 class/DoA head 本身有没有学到 - 不包含最终检测误差,不等于 `F20` ### 2.4 要不要先训练一个独立的 per-frame class-only BEATs 结论:不建议单独另起一个 class-only 模型作为主路线。 原因: - 最终任务的 class 是从 `fused -> source_query_decoder -> track_time_features -> class_head` 这条路径读出来的 - 单独训一个普通 per-frame classifier,学不到 query/track binding 更合理的做法是: - 保留同一套 `track-query` 架构 - 先做 `activity + class` warmup - 再打开 `dir/dist` 这就是当前 `v8` 的 two-stage 设计。 ### 2.5 v7 阶段关于初始化和解冻的结论 已经确认的结论: - `source_query_decoder` 和 `activity_head` 随机初始化是合理的 - `frame_track_prediction_heads.class/direction/distance` 可以从旧的 `local_spatial_prediction_heads` 迁移同语义权重 - 如果 `v7` 只在 `ov1` 上学过,到了 `ov123` 不建议永远冻结 trunk - 当前合理策略是: - 继续从已有可用 checkpoint 热启动 - 解冻 trunk 顶部少量层,例如 top-4 ## 3. 当前最可用基线:v7h `v7h` 继承自 `v7f_ov123_top4`,是当前最可用的稳定基线。 核心训练设定: - `readout_scheme = local_spatial_track` - `enable_clip_aux_head = False` - `ov1:ov2:ov3 = 1:3:3` - 去掉 focal BCE - `frame_activity_pos_weight = 3.0` - class-cost warmup 采用 `1 + 2` epoch - trunk 顶部 4 层解冻 相关配置可参考: - `train_spatial_beats.py:1465` - `docs/0422_v7h_v7j.md` 历史最好结果记录: ```text v7h ep3 val: F20=0.246 LE_CD=34.3° LR_CD=0.504 SELD=0.608 ``` 这也是当前继续做 `v8` 的热启动来源。 ## 4. v8 的设计目标 ### 4.1 为什么要做 v8 对 `v7h` 的判断是: - 前端不要乱改 - `source_query_decoder` 和 `frame_track_prediction_heads` 也先不要乱改 - 真正可能偏弱的是 fused token 的构造方式 `v7h` 的融合只有: ```text fused = LayerNorm(semantic_embeddings + local_update) ``` 这隐含假设: - semantic token 和 spatial token 已经天然对齐 - 只要相加,query decoder 就能自动学会利用空间信息 这个假设偏强,因此设计 `v8`: - 前端完全不改 - 只升级 semantic/spatial 的 fusion ### 4.2 v8 设计原则 - 以 semantic token 为主骨架 - spatial token 不直接硬加,而是通过 cross-attention 注入 - 新模块要能安全从 `v7h` 热启动 因此 `v8` 的 fusion 采用: ```text semantic <- spatial cross-attention (2 layers) + gated direct spatial residual + same final LayerNorm ``` ## 5. v8 新模型架构 ### 5.1 整体链路 `v8` 只改 fused 部分,其他都和 `v7h` 一致: ```text FOA waveform [B, 4, T] -> SpatialBEATsPreprocessor -> patch embedding + BEATs trunk -> frequency_pool -> TemporalResampler (2.5 Hz) -> temporal_readout -> semantic_embeddings [B, T_s, D] -> LocalSpatialEncoder -> local_spatial_resampler -> local_spatial_proj -> local_update [B, T_s, D] -> LocalSpatialCrossFuser (new in v8) semantic <- spatial cross-attn x 2 + gated spatial residual -> local_spatial_fusion_norm -> fused_embeddings [B, T_s, D] -> SourceQueryDecoder -> FrameTrackPredictionHeads ``` ### 5.2 代码落点 新增或修改位置: - `spatial_modules.py:1723-1842` - `LocalSpatialCrossFusionBlock` - `LocalSpatialCrossFuser` - `spatial_beats.py:173-182` - 新增 fusion 配置字段 - `spatial_beats.py:1104-1118` - `build_local_spatial_fusion()` 改为支持 `cross_attn_gated` - `train_spatial_beats.py:1536-1549` - 新增 `make_ov1_local_spatial_v8_ov123_top4_config()` - `run_ov1_v8_ov123_top4.sh:1-68` - 新的启动脚本 ### 5.3 v8 的训练可学习参数 为了保证 `v8` 新融合模块真的训练到,代码里做了两处接线: - `train_spatial_beats.py:2528-2535` - `local_spatial_fuser` 被加入 `always_train_prefixes` - `train_spatial_beats.py:2646-2653` - `local_spatial_fuser.` 被加入 `_SPATIAL_PREFIXES` 也就是说: - 新 fuser 走 `spatial_lr` 组 - 不会被当成 trunk 或 head 漏掉 ## 6. v8 的两阶段训练 ### 6.1 设计原因 直接在新融合结构上同时训练 `class + direction + distance`,早期很容易出现: - class 还没稳定 - DoA 噪声先进入 matching - assignment 被错误的 spatial cost 带偏 所以 `v8` 采用 two-stage: - stage 1:先训 `activity + class` - stage 2:再恢复 `dir/dist` ### 6.2 具体配置 `v8` preset 中: ```text frame_spatial_loss_warmup_epochs = 3 frame_spatial_loss_warmup_scale = 0.0 ``` 对应代码: - `train_spatial_beats.py:1547-1548` - `train_spatial_beats.py:4079-4102` 含义: - `epoch 0-2` - `lambda_frame_direction = 0` - `lambda_frame_distance = 0` - `frame_match_dir_cost_weight = 0` - `frame_match_dist_cost_weight = 0` - `epoch 3+` - 自动恢复完整方向/距离监督 ## 7. v8 热启动方式 默认脚本: ```text ./run_ov1_v8_ov123_top4.sh ``` 默认行为: - 从 `v7h` 的 `best.pt` 热启动 - 只新增 `local_spatial_fuser.*` 参数 - 其余 trunk / local_spatial / source_query_decoder / frame-track heads 继承已有权重 - 训练比例继续保持 `ov1:ov2:ov3 = 1:3:3` 脚本位置: - `run_ov1_v8_ov123_top4.sh:16-37` ## 8. v8 当前结果 ### 8.1 epoch 0 日志 用户记录: ```text [Epoch 0] train: loss=1.3445 activity=0.3099 cls_aux=1.0346 direction=0.4527 dist=0.4894 act↑=0.887 act↓=0.136 sep=0.751 ocls=0.786 oazi=45.2° oele=18.4° [Epoch 0] val: loss=2.1304 activity=0.2310 cls_aux=1.8995 direction=0.4377 dist=0.4337 act↑=0.925 act↓=0.092 sep=0.832 ocls=0.652 oazi=46.9° oele=20.1° ER20=1.047 F20=0.096 LE_CD=51.8° LR_CD=0.486 SELD=0.688 ``` ### 8.2 epoch 0 CSV 诊断 `epoch_0000_csv` 的诊断结论: - 不是 `v7j` 那种 3-4 条轨全亮的崩坏 - `activity` 分离已经很好 - 当前主要瓶颈是 `class + angle`,不是 duplicate 粗统计: ```text TP=207 FP=2147 FN=1844 P=0.088 R=0.101 F1=0.094 FP breakdown: class_wrong = 1211 angle_wrong = 911 duplicate = 25 active_hist: 0 active: 185 1 active: 748 2 active: 683 3 active: 80 no_gt_frames=286 no_gt_with_pred=119 ratio=0.416 mean_maxprob_gt = 0.972 mean_maxprob_nogt = 0.400 ``` 解释: - `act↑/act↓/sep` 很强,说明 activity head 已经把有源/无源分开了 - 但 `class_wrong` 和 `angle_wrong` 很高 - `F20` 低并不意外,因为 stage 1 根本还没训 `dir/dist` ### 8.3 epoch 0 的 ov1 / ov2 / ov3 形态 代表样本观察: `ov1` ```text mean_act_by_track = {0: 0.88, 1: 0.007, 2: 0.001, 3: 0.0} active_tracks_per_frame_hist = {1: 37} top_pred_classes = singing(37) ``` 说明单源样本已经能稳定只亮一条轨。 `ov2` ```text mean_act_by_track = {0: 0.999, 1: 0.745, 2: 0.133, 3: 0.0} active_tracks_per_frame_hist = {1: 5, 2: 45} top_pred_classes = frog(50), bird(37), tool(8) ``` 说明双源样本已经在尝试输出两条轨。 `ov3` ```text mean_act_by_track = {0: 0.977, 1: 0.519, 2: 0.036, 3: 0.0} active_tracks_per_frame_hist = {1: 17, 2: 32} top_pred_classes = tool(81) ``` 说明三源样本里第二条轨开始亮,但有明显 class collapse。 ## 9. v8 的 epoch 1:重点看 ov2 / ov3 第二个 epoch 之后,重点检查了 `ov23`。 ### 9.1 active track 数量变化 在“有预测的帧”上,平均亮起的轨数: ```text epoch0: ov2 = 1.578 ov3 = 1.767 epoch1: ov2 = 1.736 ov3 = 1.942 ``` 这说明: - `ov2/ov3` 上第二条轨更积极了 - 模型更愿意在 overlap 场景输出多轨 这本身不是坏事,但如果 class/DoA 没跟上,就会先表现为 FP 上升。 ### 9.2 代表样本对比 `ov2` 代表样本 `valid__ov2_000000__pred.csv` `epoch0` ```text mean_act_by_track = {0: 0.999, 1: 0.745, 2: 0.133, 3: 0.0} active_hist = {1: 5, 2: 45} top_classes = frog(50), bird(37), tool(8) ``` `epoch1` ```text mean_act_by_track = {0: 0.996, 1: 0.312, 2: 0.625, 3: 0.339} active_hist = {1: 11, 2: 39} top_classes = frog(50), wind(39) ``` 解释: - 第二条活跃轨从 `track1` 转向 `track2` - 类别也从 `bird` 变成了 `wind` - 说明 query 责任在重排,但 class binding 还不稳定 `ov3` 代表样本 `valid__ov3_000004__pred.csv` `epoch0` ```text mean_act_by_track = {0: 0.977, 1: 0.519, 2: 0.036, 3: 0.0} active_hist = {1: 17, 2: 32} top_classes = tool(81) ``` `epoch1` ```text mean_act_by_track = {0: 0.966, 1: 0.665, 2: 0.065, 3: 0.127} active_hist = {1: 11, 2: 39} top_classes = tool(89) ``` 解释: - 第二条轨更稳定地亮了 - 但 class collapse 没缓解,反而更统一地塌成 `tool` 因此当前对 `epoch1` 的判断是: - `ov2`:有变化,但不能算明显变好 - `ov3`:暂时没有变好,仍然是当前最大问题点 ## 10. 当前诊断 截至目前,对 `v8` 的判断是: ### 10.1 已经证明的事 - `v8` 没有走向 `v7j` 那种 duplicate 崩坏 - `activity` 分离做得比担心中要好 - `ov2/ov3` 上的多轨输出能力确实在长出来 ### 10.2 还没解决的事 - `ov2/ov3` 的 class binding 还不稳 - `ov3` 仍然存在明显 class collapse - stage 1 期间不训 `dir/dist`,所以 `F20 / LE_CD / oazi` 现在还不能作为最终判断 ### 10.3 当前最值得关注的信号 - `epoch 3+` 进入 stage 2 后: - `ocls` 是否继续上升 - `oazi` 是否明显下降 - `F20` 是否出现拐点 - `ov3` 的 top predicted classes 是否开始从单一 `tool` 分裂成多个类 - `ov23` 的平均 active tracks 是否继续上涨过快 ## 11. 本轮实际代码修改 本轮已经落地的修改: ### 11.1 新增 v8 融合模块 - `spatial_modules.py:1723-1842` - `LocalSpatialCrossFusionBlock` - `LocalSpatialCrossFuser` ### 11.2 新增 fusion 配置 - `spatial_beats.py:173-182` - `local_spatial_fusion_mode` - `local_spatial_fusion_layers` - `local_spatial_fusion_heads` - `local_spatial_fusion_dropout` - `local_spatial_fusion_gate_bias` - `local_spatial_fusion_direct_gate_bias` ### 11.3 改 fused token 构造 - `spatial_beats.py:1104-1118` - 从 `semantic + local_update` - 改成支持 `local_spatial_fuser(...)` ### 11.4 新增 v8 preset - `train_spatial_beats.py:1536-1549` - 继承 `v7h` - 打开 `cross_attn_gated` - 打开 two-stage spatial warmup - 输出目录改为 `v8_ov123_exp/03_ov123_top4` ### 11.5 训练侧接线 - `train_spatial_beats.py:2528-2535` - `local_spatial_fuser` 加入 `always_train_prefixes` - `train_spatial_beats.py:2646-2653` - `local_spatial_fuser.` 加入 `_SPATIAL_PREFIXES` ### 11.6 新增脚本 - `run_ov1_v8_ov123_top4.sh:1-68` ## 12. 下一步建议 当前建议不改结构,先继续训练 `v8`: 1. 至少跑到 `epoch 3` 之后,确认 stage 2 开启后的趋势 2. 优先看 `ov3` 是否开始摆脱 `tool` collapse 3. 如果 `epoch 3-5` 之后仍然: - `ov3` 继续单类塌缩 - `ocls` 不升 - `oazi` 不降 - `F20` 没有明显抬升 再考虑下一轮结构修改 当前最合理的工作顺序是: - 先把 `v8` 跑穿 stage 1 / stage 2 - 再根据 `ov23` 的 class collapse 是否缓解,决定下一轮改: - query decoder - matching - finer token rate - 或额外的 class-preserving auxiliary