# Spatial-BEATs 文件级实现清单 ## 1. 文档目的 这份文档把当前确定的 `Spatial-BEATs` 简化版方案,收紧成可以直接指导编码的文件级实现清单。 目标是回答四件事: - 哪些现有文件直接复用 - 哪些新文件需要实现 - 每个模块的职责是什么 - 从 `FOA` 到 `LLM spatial tokens` 的张量形状如何变化 这份清单默认遵循当前主线: - 重点训练 `FOA -> BEATs trunk` 的空间表征 - 后面的模块只承担 `readout / supervision` - 不以复杂 `slot decoder` 为主线 - 最终给 `LLM` 的 token 是固定 `2.5 Hz` - `10 s` 输入默认输出 `25` 个 spatial tokens ## 2. 默认配置 - 输入采样率:`16 kHz` - 输入通道:`4` 个 `FOA` 通道,分别为 `W / X / Y / Z` - 预处理输出通道:`7` - 空间特征:`W_logmel / X_logmel / Y_logmel / Z_logmel / IVx / IVy / IVz` - `mel bins`:`128` - Qwen-like mel 参数: - `sample_rate=16000` - `num_mel_bins=128` - `n_fft=400` - `win_length=400` - `hop_length=160` - `dither=0.0` - source vocabulary:`/apdcephfs_cq12/share_302080740/user/schmittzhu/data/fsd50k/FSD50K.ground_truth/final_vocabulary.csv` - source vocabulary fields:`label_id / final_label` - source class count:`65` - 主干初始化:`BEATs_iter3+ AS2M pre-trained` - `patch embed dim`:`512` - `encoder dim`:`768` - `encoder layers`:`12` - 最终时间 token rate:`2.5 Hz` - 最大同时源数:`4` - 当前监督:弱时间窗口 + 固定位置 - batch 时间组织:每个样本自己的 `T_s_i = round(duration_i * 2.5)`,batch 内 pad 到 `T_s_max` ## 3. 推荐文件布局 建议保留原始 `BEATs` 文件不动,新实现集中放在新增文件里: - 复用:[BEATs.py](/apdcephfs_cq10/share_1603164/user/schmittzhu/code/unilm/beats/BEATs.py) - 复用:[backbone.py](/apdcephfs_cq10/share_1603164/user/schmittzhu/code/unilm/beats/backbone.py) - 新增:`spatial_beats.py` - 新增:`spatial_modules.py` - 新增:`spatial_loss.py` - 新增:`spatial_dataset.py` - 新增:`train_spatial_beats.py` 推荐原则: - 原始 `BEATs.py` 作为参考和权重来源,不直接改 - 原始 `backbone.py` 直接复用,不改 Transformer 内部 - 新的 `Spatial-BEATs` 在新文件里组装 ## 4. 现有文件如何复用 ## 4.1 [BEATs.py](/apdcephfs_cq10/share_1603164/user/schmittzhu/code/unilm/beats/BEATs.py) 当前 `BEATs` 里最重要的可复用部分是: - `BEATsConfig` - `post_extract_proj` - `layer_norm` - `TransformerEncoder` 的使用方式 - 预训练 checkpoint 的参数命名 当前不直接复用的部分是: - `preprocess()` - `patch_embedding = Conv2d(1, embed_dim, ...)` - `predictor` 原因: - `preprocess()` 只支持单通道 `fbank` - `patch_embedding` 只支持 `1` 通道输入 - `predictor` 是 AudioSet 分类头,不适合当前 spatial encoder ## 4.2 [backbone.py](/apdcephfs_cq10/share_1603164/user/schmittzhu/code/unilm/beats/backbone.py) 这里建议直接复用: - `TransformerEncoder` - `TransformerSentenceEncoderLayer` - attention / FFN / pos_conv / layer_norm 当前 `Spatial-BEATs` 不需要改这些内部结构。 ## 5. 新文件实现清单 ## 5.1 `spatial_beats.py` 这个文件放主模型类,负责把所有模块串起来。 建议包含: - `SpatialBEATsConfig` - `SpatialBEATs` - `load_beats_pretrained()` ### 5.1.1 `SpatialBEATsConfig` 职责: - 在 `BEATsConfig` 基础上补充 spatial encoder 需要的超参 建议新增字段: - `foa_feature_channels = 7` - `sample_rate = 16000` - `num_mel_bins = 128` - `target_token_rate = 2.5` - `source_vocab_path` - `source_label_id_field = "label_id"` - `source_label_name_field = "final_label"` - `source_num_classes = 65` - `max_sources = 4` - `readout_layers = 1 or 2` - `slot_hidden_dim = 768` - `distance_head_type = "regression"` - `llm_hidden_dim` ### 5.1.2 `SpatialBEATs.__init__` 职责: - 初始化所有子模块 - 建立 trunk 与 heads - 提供 BEATs checkpoint 兼容结构 建议持有的子模块: - `preprocessor: SpatialBEATsPreprocessor` - `patch_embedding: SpatialPatchEmbedding` - `layer_norm` - `post_extract_proj` - `dropout_input` - `encoder: TransformerEncoder` - `frequency_pool: FrequencyPool` - `temporal_resampler: TemporalResampler` - `temporal_readout: ShallowTemporalReadout` - `slot_readout: FixedSlotReadout` - `prediction_heads: SpatialPredictionHeads` - `projector: SpatialTokenProjector` ### 5.1.3 `SpatialBEATs.forward()` 建议拆成几个清晰阶段: 1. `preprocessor` 2. `patch embedding` 3. `BEATs trunk` 4. `frequency pooling` 5. `temporal resampling` 6. `temporal readout` 7. `slot readout + heads` 8. `projector` 建议输出一个结构化对象或 dict,至少包含: ```python { "foa_feat": foa_feat, "patch_tokens": patch_tokens, "encoder_memory": encoder_memory, "temporal_tokens": temporal_tokens, "spatial_embeddings": spatial_embeddings, "slot_latents": slot_latents, "pred_activity": pred_activity, "pred_azi_logits": pred_azi_logits, "pred_ele_logits": pred_ele_logits, "pred_dist": pred_dist, "pred_class_logits": pred_class_logits, "llm_spatial_tokens": llm_spatial_tokens, "temporal_padding_mask": temporal_padding_mask, "target_num_steps": target_num_steps, } ``` 这样训练和调试都更方便。 ## 5.2 `spatial_modules.py` 这个文件放所有新增模块。 建议包含以下类。 ### 5.2.1 `SpatialBEATsPreprocessor` 职责: - 把 `FOA waveform` 变成 `7` 通道空间特征图 输入: - `waveform: [B, 4, T]` 默认 10 秒示例: - `waveform: [B, 4, 160000]` 内部步骤: 1. 对 `W/X/Y/Z` 做 `STFT` 2. 计算四个通道的 `log-mel` 3. 计算 `IVx / IVy / IVz` 4. 将 `IV` 映射到 `mel` 维 5. 按通道拼接 输出: - `foa_feat: [B, 7, T_f, 128]` 默认示例: - 如果 `frame_shift = 10 ms` - 对 `10 s`,`T_f` 约为 `1000` - 因此输出近似: - `foa_feat: [B, 7, 1000, 128]` 作用: - 用显式 `FOA` 物理特征替代原始 `BEATs` 的单通道 `fbank` ### 5.2.2 `SpatialPatchEmbedding` 职责: - 把 `7` 通道空间特征图变成 patch tokens 建议实现: - `Conv2d(7, 512, kernel_size=patch, stride=patch, bias=cfg.conv_bias)` 输入: - `foa_feat: [B, 7, T_f, 128]` 输出中间张量: - `patch_grid: [B, 512, T_p, F_p]` flatten 后输出: - `patch_tokens: [B, N_p, 512]` 其中: - `T_p = floor((T_f - p_t) / s_t) + 1` - `F_p = floor((128 - p_f) / s_f) + 1` - `N_p = T_p * F_p` 如果第一版保持和当前 `BEATs` 一样的等步长 patch,且 `patch=(16,16)`: - `T_f ≈ 1000 -> T_p ≈ 62` - `128 -> F_p = 8` - `N_p ≈ 496` 则一个典型输出是: - `patch_tokens: [B, 496, 512]` 作用: - 用多通道 spatial features 替换原始单通道 patch stem ### 5.2.3 `FrequencyPool` 职责: - 把 trunk 的二维 patch grid 沿频率维压缩成时间序列 输入: - `encoder_memory: [B, N_p, 768]` - `grid_size = (T_p, F_p)` 先 reshape: - `grid_memory: [B, T_p, F_p, 768]` 再沿 `F_p` 做 pooling: - `temporal_patch_tokens: [B, T_p, 768]` 推荐第一版: - `mean pooling over F_p` 作用: - 把 patch 级二维结构转成时间序列 ### 5.2.4 `TemporalResampler` 职责: - 把 patch 级时间步压到固定 `2.5 Hz` 输入: - `temporal_patch_tokens: [B, T_p, 768]` 输出: - `temporal_tokens: [B, T_s_max, 768]` - `temporal_padding_mask: [B, T_s_max]` 其中: - 每个样本: - `T_s_i = round(duration_i * 2.5)` - batch 内: - `T_s_max = max_i T_s_i` 对 `10 s`: - `T_s_i = 25` 因此单个 `10 s` 样本的有效长度是: - `25` 推荐实现: - 第一版:线性插值 - 备选:轻量 `Conv1d` 下采样 作用: - 明确固定最终时间基准 - 保证最终给 LLM 的 token rate 就是 `2.5 Hz` ### 5.2.5 `ShallowTemporalReadout` 职责: - 把 trunk 输出的时间序列再整理一遍 - 让它更适合作为空间 embedding 建议实现: - `1~2` 层轻量 `TransformerEncoder` - 或最小版 `LayerNorm + MLP` 输入: - `temporal_tokens: [B, T_s_max, 768]` 输出: - `spatial_embeddings: [B, T_s_max, 768]` 作用: - 这就是当前主线里最重要的 LLM 前空间 embedding ### 5.2.6 `FixedSlotReadout` 职责: - 给 encoder-only 训练提供多源监督出口 - 不引入复杂 decoder 输入: - `spatial_embeddings: [B, T_s_max, 768]` 建议最小实现: - `Linear(768, 4 * 768)` - reshape 成: - `slot_latents: [B, T_s_max, 4, 768]` 备选更稳实现: - `Linear(768, 768) -> GELU -> Linear(768, 4 * 768)` - reshape 后: - `slot_latents: [B, T_s_max, 4, 768]` 作用: - 每个时间步提供 `4` 个固定 source 槽位 - 用于承载多源监督 注意: - 这里的 `4` 只是 supervision 容量 - 不影响最终输出给 LLM 的 token 数 ### 5.2.7 `SpatialPredictionHeads` 职责: - 从 `slot_latents` 上读出显式空间监督 输入: - `slot_latents: [B, T_s_max, 4, 768]` 输出: - `pred_activity: [B, T_s_max, 4]` - `pred_azi_logits: [B, T_s_max, 4, 360]` - `pred_ele_logits: [B, T_s_max, 4, 180]` - `pred_dist: [B, T_s_max, 4, 1]` - `pred_class_logits: [B, T_s_max, 4, C_cls]` 作用: - `pred_activity`:弱时间窗口下的源存在性 - `pred_azi_logits`:方位角分类 - `pred_ele_logits`:仰角分类 - `pred_dist`:连续距离回归 - `pred_class_logits`:辅助类别监督 当前默认建议: - `C_cls = 65` - labels 来自 `final_vocabulary.csv` ### 5.2.8 `SpatialTokenProjector` 职责: - 把 `spatial_embeddings` 投影到 LLM hidden size 输入: - `spatial_embeddings: [B, T_s_max, 768]` 输出: - `llm_spatial_tokens: [B, T_s_max, d_llm]` 推荐实现: - `Linear(768, D_mid)` - `GELU` - `LayerNorm` - `Linear(D_mid, d_llm)` 注意: - encoder-only 第一阶段可以不训练这个模块 - 但接口最好先保留 ## 5.3 `spatial_loss.py` 这个文件放 target 组织、matching 和 loss 计算。 建议包含: - `build_time_window_mask()` - `match_fixed_slots()` - `compute_spatial_losses()` ### 5.3.1 `build_time_window_mask()` 输入: - source start / end - clip duration - 每个样本的 `T_s_i` - batch 的 `T_s_max` 输出: - `window_mask: [B, num_gt, T_s_max]` 作用: - 把弱时间标注映射到 `2.5 Hz` 时间轴 ### 5.3.2 `match_fixed_slots()` 输入: - `pred_activity: [B, T_s_max, 4]` - `pred_azi_logits: [B, T_s_max, 4, 360]` - `pred_ele_logits: [B, T_s_max, 4, 180]` - `pred_dist: [B, T_s_max, 4, 1]` - `pred_class_logits: [B, T_s_max, 4, C_cls]` - `gt targets` 输出: - 每个时间步的 slot 分配结果 推荐第一版: - `per-step fixed-slot matching` 作用: - 不依赖复杂 decoder,也能对 4 个固定槽位施加多源监督 ### 5.3.3 `compute_spatial_losses()` 建议输出各项 loss: - `L_activity` - `L_azi` - `L_ele` - `L_dist` - `L_cls_aux` - `L_temp` 总损失: ```text L_total = lambda_act * L_activity + lambda_azi * L_azi + lambda_ele * L_ele + lambda_dist * L_dist + lambda_cls * L_cls_aux + lambda_temp * L_temp ``` 各项作用: - `L_activity`:窗口外负样本,窗口内弱正样本 - `L_azi` / `L_ele`:方向监督 - `L_dist`:距离连续回归 - `L_cls_aux`:帮助保持 source-level 语义区分 - `L_temp`:固定位置场景下的时间平滑 ## 5.4 `spatial_dataset.py` 这个文件负责把数据组织成模型和 loss 可直接使用的格式。 建议一个 batch sample 至少包含: - `waveform: [4, T]` - `source_labels` - `source_azimuth` - `source_elevation` - `source_distance` - `source_start_time` - `source_end_time` - `clip_duration` - `source label id / name` should map through `final_vocabulary.csv` 建议 collate 后的 batch 字段: - `waveform: [B, 4, T]` - `padding_mask: [B, T]` 或 `None` - `gt_sources`: 变长列表,或 pad 成固定 `max_gt` - `clip_duration_seconds: [B]` 建议在 dataset 或 collate 阶段完成: - 时间窗口离散化到每个样本自己的 `T_s_i`,再 pad 到 `T_s_max` - 距离归一化 - 角度离散化成 bin index - label id/name 映射到统一 vocabulary index ## 5.5 `train_spatial_beats.py` 这个文件负责训练阶段组织。 建议职责: - 构建 model - 加载 BEATs checkpoint - 配置参数组和学习率 - 调用 dataset / loss - 执行 encoder-only 训练 建议阶段: ### Stage 1 - 训练: - `preprocessor` - `patch_embedding` - `temporal_resampler` - `temporal_readout` - `slot_readout` - `prediction_heads` - `projector` 默认不训练 - trunk 可以: - 先只解冻高层 - 或全量解冻但用更小 lr ### Stage 2 - 接上 `projector` - 再和 LLM 接口联合训练 ## 6. 预训练权重加载清单 建议在 `spatial_beats.py` 里实现 `load_beats_pretrained()` ## 6.1 直接加载的参数 从 `BEATs` checkpoint 里直接复用: - `layer_norm.*` - `post_extract_proj.*` - `encoder.pos_conv.*` - `encoder.layers.*` - `encoder.layer_norm.*` ## 6.2 部分继承或重新初始化的参数 ### `patch_embedding` 原始 `BEATs`: - `Conv2d(1, 512, ...)` 新的 `Spatial-BEATs`: - `Conv2d(7, 512, ...)` 推荐初始化: - `W_logmel` 通道拷贝原始卷积权重 - 其他 `6` 个通道初始化为: - 零 - 或小随机值 这样可以最大程度保留原始单通道初始化稳定性。 ### 新模块随机初始化 以下模块默认新初始化: - `SpatialBEATsPreprocessor` - `FrequencyPool` - `TemporalResampler` - `ShallowTemporalReadout` - `FixedSlotReadout` - `SpatialPredictionHeads` - `SpatialTokenProjector` ## 7. 10 秒输入的完整张量流 下面给一个默认示例。 假设: - batch size 为 `B` - 输入时长 `10 s` - 采样率 `16 kHz` - `T = 160000` - `mel bins = 128` - patch 为 `16 x 16` ### 7.1 从波形到 trunk 1. 输入波形 - `[B, 4, 160000]` 2. 预处理后 - `[B, 7, 1000, 128]` 3. patch conv 后 - `[B, 512, 62, 8]` 4. flatten + transpose 后 - `[B, 496, 512]` 5. `layer_norm` - `[B, 496, 512]` 6. `post_extract_proj` - `[B, 496, 768]` 7. `TransformerEncoder` - `[B, 496, 768]` ### 7.2 从 trunk 到 spatial embeddings 8. reshape 回 patch grid - `[B, 62, 8, 768]` 9. frequency pooling - `[B, 62, 768]` 10. temporal resample 到 `2.5 Hz` - 单个 `10 s` 样本有效长度为 `25` - mixed-length batch 中张量形状为 `[B, T_s_max, 768]` 11. shallow temporal readout - `[B, T_s_max, 768]` 这一步得到: - `spatial_embeddings: [B, T_s_max, 768]` ### 7.3 从 spatial embeddings 到训练 heads 12. fixed-slot expand - `[B, T_s_max, 4, 768]` 13. activity head - `[B, T_s_max, 4]` 14. azimuth head - `[B, T_s_max, 4, 360]` 15. elevation head - `[B, T_s_max, 4, 180]` 16. distance head - `[B, T_s_max, 4, 1]` 17. class auxiliary head - `[B, T_s_max, 4, C_cls]` ### 7.4 从 spatial embeddings 到 LLM 接口 18. spatial projector - `[B, T_s_max, d_llm]` 这就是最终给 LLM 的 spatial tokens。 关键点: - 单个 `10 s` 样本:`10 s -> 25` 个有效 token - mixed-length batch:张量按 `T_s_max` padding - 不是 `25 * 4` - `4` 只存在于监督头里 ## 8. 推荐实现顺序 建议按下面顺序写代码。 1. `spatial_modules.py` - 先实现 `preprocessor / patch embedding / frequency pool / temporal resampler / readout / heads / projector` 2. `spatial_beats.py` - 把主链路串起来 - 先跑通 forward 和 shape 3. `spatial_dataset.py` - 先把数据组织成 `2.5 Hz` 时间轴上的 supervision,并支持 mixed-length batch padding 4. `spatial_loss.py` - 先实现 `L_activity / L_azi / L_ele / L_dist / L_cls_aux` - 最后再补 `L_temp` 5. `train_spatial_beats.py` - 最后接训练循环 ## 9. 第一版验收标准 在不接 LLM 的前提下,第一版至少应满足: - forward 全链路 shape 正确 - 能成功加载 BEATs trunk 预训练参数 - `10 s` 输入稳定输出: - 对单个 `10 s` 样本有 `25` 个有效 token - batch 张量输出为 `spatial_embeddings [B, T_s_max, 768]` - batch 张量输出为 `llm_spatial_tokens [B, T_s_max, d_llm]` - 训练阶段能算出: - `L_activity` - `L_azi` - `L_ele` - `L_dist` - `L_cls_aux` - loss 能稳定回传到 trunk ## 10. 一句话总结 当前最合理的文件级实现方式是: - 不改原始 `BEATs` 主体文件 - 用新文件组装一个 `FOA -> spatial features -> BEATs trunk -> 2.5Hz spatial embeddings -> fixed-slot supervision heads -> projector` 的 `Spatial-BEATs` - 训练时真正要训好的是前面的 `spatial_embeddings` - 后面的 heads 只是为了把空间监督显式接出来