# Spatial-BEATs Training And Architecture Overview This document summarizes the current `Spatial-BEATs` implementation in this repository: - model architecture - tensor shape flow - dataset contract - variable-length batching - supervision and losses - stage-1 training setup - current `ov1/ov2/ov3` presets The implementation described here corresponds to: - [spatial_beats.py](/apdcephfs_cq10/share_1603164/user/schmittzhu/code/unilm/beats/spatial_beats.py) - [spatial_modules.py](/apdcephfs_cq10/share_1603164/user/schmittzhu/code/unilm/beats/spatial_modules.py) - [spatial_dataset.py](/apdcephfs_cq10/share_1603164/user/schmittzhu/code/unilm/beats/spatial_dataset.py) - [spatial_loss.py](/apdcephfs_cq10/share_1603164/user/schmittzhu/code/unilm/beats/spatial_loss.py) - [train_spatial_beats.py](/apdcephfs_cq10/share_1603164/user/schmittzhu/code/unilm/beats/train_spatial_beats.py) - [spatial_beats_ov123_stage1_config.py](/apdcephfs_cq10/share_1603164/user/schmittzhu/code/unilm/beats/spatial_beats_ov123_stage1_config.py) ## 1. Goal `Spatial-BEATs` is a separate spatial encoder for FOA audio. It is designed to: - reuse the BEATs backbone and pretrained weights - take full FOA input instead of only the `W` channel - learn spatial structure through explicit supervision - output fixed-rate spatial tokens for an LLM - stay separate from the original audio encoder used for semantic audio understanding The current implementation follows the simplified design: - the main objective is to train the FOA front-end and BEATs trunk to produce spatially informative embeddings - the supervision heads are lightweight readout heads - the final LLM tokens are taken from the encoder-side spatial embeddings, not from the final logits ## 2. High-Level Architecture The end-to-end model path is: ```text FOA waveform -> FOA spatial preprocessor -> multi-channel patch embedding -> BEATs trunk -> frequency pooling -> temporal resampling to 2.5 Hz -> shallow temporal readout -> spatial embeddings -> fixed-slot supervision heads -> projector -> LLM spatial tokens ``` More concretely: ```text [B, 4, T] -> [B, 7, T_f, 128] -> [B, N_p, 512] -> [B, N_p, 768] -> [B, T_p, 768] -> [B, T_s_max, 768] -> [B, T_s_max, 768] -> [B, T_s_max, 4, 768] -> [B, T_s_max, d_llm] ``` Where: - `B`: batch size - `T`: waveform length in samples - `T_f`: acoustic frame count before patching - `N_p`: number of BEATs patches - `T_p`: time-axis patch count after frequency pooling - `T_s_max`: padded token count in the batch after resampling to `2.5 Hz` - `d_llm`: spatial token width sent to the LLM ## 3. Input And Front-End ### 3.1 Input audio The model expects: - FOA waveform - shape `[B, 4, T]` - channel order: `W, X, Y, Z` - sample rate: `16 kHz` ### 3.2 Qwen-like low-level mel setup The current front-end is aligned to the Qwen-2.5-Omni audio tower style low-level parameters: - `sample_rate = 16000` - `num_mel_bins = 128` - `n_fft = 400` - `win_length = 400` - `hop_length = 160` - `dither = 0.0` These parameters are shared between: - `SpatialBEATsConfig` - `SpatialDatasetConfig` This keeps the data pipeline and the model front-end consistent. ### 3.3 FOA feature construction The preprocessor converts FOA waveform into a 7-channel feature map: - `W_logmel` - `X_logmel` - `Y_logmel` - `Z_logmel` - `IVx` - `IVy` - `IVz` Output shape: - `foa_feat: [B, 7, T_f, 128]` This allows the whole FOA structure to enter the backbone instead of relying on only `W`. ## 4. Backbone And Spatial Embedding Path ### 4.1 Spatial patch embedding The model replaces the original single-channel patch stem with a 7-channel patch embedding: - input: `foa_feat [B, 7, T_f, 128]` - output: `patch_tokens [B, N_p, 512]` - also returns `grid_size = (T_p, F_p)` This is the first modified entry point for reusing BEATs on FOA input. ### 4.2 Reused BEATs trunk The trunk reuses BEATs pretrained components: - `layer_norm` - `post_extract_proj` - `encoder.pos_conv` - all transformer layers - `encoder.layer_norm` Flow: - input: `patch_tokens [B, N_p, 512]` - output: `encoder_memory [B, N_p, 768]` ### 4.3 Frequency pooling The patch sequence is reshaped back into a patch grid and pooled over the frequency axis: - input: `encoder_memory [B, N_p, 768]` with `grid_size=(T_p, F_p)` - reshaped internally to `[B, T_p, F_p, 768]` - pooled output: `temporal_patch_tokens [B, T_p, 768]` This produces a time-aligned sequence before the final token-rate conversion. ### 4.4 Temporal resampling The temporal resampler converts the patch-rate sequence into the final spatial token rate: - target token rate: `2.5 Hz` - per-sample target length: ```text T_s_i = round(duration_i * 2.5) ``` Batch handling: - each sample is resampled independently - the batch is padded to `T_s_max = max_i(T_s_i)` - a temporal mask is produced Outputs: - `temporal_tokens: [B, T_s_max, 768]` - `temporal_padding_mask: [B, T_s_max]` Mask convention: - `False`: valid time step - `True`: padded time step ### 4.5 Shallow temporal readout The shallow temporal readout refines the resampled sequence with a lightweight transformer encoder: - input: `temporal_tokens [B, T_s_max, 768]` - output: `spatial_embeddings [B, T_s_max, 768]` This is the main representation used for both: - spatial supervision - final projection to LLM tokens ## 5. Supervision Heads The current stage-1 design does not use a heavy decoder. Instead, it uses a fixed-slot readout for supervision only. ### 5.1 Fixed-slot readout The readout expands each time step into a small number of internal supervision slots: - max slots per step: `K = 4` - input: `spatial_embeddings [B, T_s_max, 768]` - output: `slot_latents [B, T_s_max, 4, 768]` Important: - `K=4` is only a supervision capacity - it does not change the final LLM token count - the final LLM-visible token rate is still `2.5 Hz` ### 5.2 Prediction heads Each supervision slot predicts: - `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]` Where: - `C = 65` - the class vocabulary comes from: - `/apdcephfs_cq12/share_302080740/user/schmittzhu/data/fsd50k/FSD50K.ground_truth/final_vocabulary.csv` These heads are used to supply explicit training loss and push the front-end plus BEATs trunk to learn spatial structure. ## 6. LLM Spatial Tokens The final LLM tokens are not taken from slot logits. They are projected from the encoder-side spatial embeddings: - input: `spatial_embeddings [B, T_s_max, 768]` - output: `llm_spatial_tokens [B, T_s_max, d_llm]` Therefore: - `2.5 Hz` means final LLM-visible tokens arrive at `2.5 tokens/second` - a `20 s` clip produces about `50` spatial tokens - a `10 s` clip produces about `25` spatial tokens This is the externally visible spatial token interface. ## 7. Pretrained Weight Reuse The model initializes from `BEATs_iter3+ AS2M`. Current pretrained loading logic: - selectively load BEATs trunk modules - skip task-specific components that do not match - inflate the old single-channel patch embedding into the new 7-channel stem Patch stem initialization rule: - original BEATs patch weight is copied into channel `0` of the new 7-channel stem - remaining channels start from zero This is a conservative initialization intended to preserve BEATs trunk stability while enabling FOA adaptation. ## 8. Dataset Contract ### 8.1 Supported manifests The dataset loader currently supports: - `ov1_foa.jsonl` - `ov2_foa.jsonl` - `ov3_foa.jsonl` It handles: - single-source top-level manifest style - nested multi-source manifest style with `sources` ### 8.2 Required scene-level data At scene level the dataset expects one FOA path, typically: - `output_foa_path` or compatible fallback names already handled in the parser. ### 8.3 Required source-level data For each source, the loader extracts: - source class - azimuth - elevation - distance - weak time window Internally each source is converted into a `SourceEvent` containing: - `class_index` - `class_label` - `azimuth_deg` - `elevation_deg` - `distance_m` - `start_time_seconds` - `end_time_seconds` ### 8.4 Vocabulary mapping Source labels are mapped to `final_vocabulary.csv`. The loader supports several field aliases, including: - `mono_target_label` - `mono_primary_label` - `final_label` - `source_label` - `label` and several id-style aliases if an integer class index is already present. ## 9. Variable-Length Batching Handling mixed-length FOA clips is a core part of the current implementation. ### 9.1 Waveform padding At batch time: - each waveform is padded to the batch maximum waveform length - the padded tensor has shape `[B, 4, T_max]` - a waveform padding mask is created: ```text waveform_padding_mask: [B, T_max] ``` Mask convention: - `False`: valid waveform sample - `True`: padded sample dui ### 9.2 Temporal token padding After temporal resampling: - each sample has its own `T_s_i = round(duration_i * 2.5)` - the batch is padded to `T_s_max` - the model returns: ```text temporal_padding_mask: [B, T_s_max] target_num_steps: [B] ``` All temporal supervision, matching, and loss computation respect these lengths. ### 9.3 Long clip truncation The current training presets cap clip duration at: - `20.0 seconds` The dataset applies cropping before batching. Preset crop policy: - `crop_mode = "start"` This means: - clips longer than 20 seconds are truncated from the beginning - training and validation follow the same deterministic sequence policy If needed later, the dataset also supports: - `random` - `center` - `none` ## 10. Matching And Losses ### 10.1 Weak temporal supervision The model uses weak source windows: - each source provides `start_time_seconds` and `end_time_seconds` - these define a valid supervision window, not guaranteed frame-level activity The loss code first converts source windows into a time-window mask: ```text window_mask: [B, N_gt, T_s_max] ``` ### 10.2 Per-step fixed-slot matching Matching is performed per time step: - only on valid temporal positions - only within each source's weak time window - between active GT sources and the `K=4` slot predictions The current matcher uses a detached cost built from: - activity - class - azimuth - elevation - distance The output contains the assigned GT target for each valid slot-time pair. ### 10.3 Multi-task loss terms Current loss terms are: - `loss_activity` - `loss_azi` - `loss_ele` - `loss_dist` - `loss_cls_aux` - `loss_temp` Their roles: - `loss_activity` - `BCEWithLogits` on slot activity - computed over valid time steps - `loss_azi` - cross-entropy over 360 azimuth bins - `loss_ele` - cross-entropy over 180 elevation bins - `loss_dist` - `SmoothL1Loss` on continuous distance regression - `loss_cls_aux` - auxiliary source class cross-entropy - `loss_temp` - temporal smoothness regularization over valid consecutive steps The total loss is the weighted sum defined in `SpatialLossConfig`. ## 11. Stage-1 Training Flow The current training entry is stage-1 encoder-focused training. High-level flow per step: ```text batch -> SpatialBEATs.forward() -> match_fixed_slots() -> compute_spatial_losses() -> backward() -> optimizer.step() ``` ### 11.1 Trainable modules in stage 1 By default, stage 1 trains: - `preprocessor` - `patch_embedding` - `frequency_pool` - `temporal_resampler` - `temporal_readout` - `slot_readout` - `prediction_heads` It can also unfreeze the BEATs trunk. The projector is kept frozen by default in stage 1. ### 11.2 Optimizer The current trainer uses: - `AdamW` Default preset values: - `batch_size = 4` - `num_epochs = 20` - `learning_rate = 1e-4` - `weight_decay = 0.05` ## 12. Current Presets ### 12.1 `OV123_STAGE1_CFG` Defined in: - [spatial_beats_ov123_stage1_config.py](/apdcephfs_cq10/share_1603164/user/schmittzhu/code/unilm/beats/spatial_beats_ov123_stage1_config.py) This preset is intended to train on: - `ov1_foa.jsonl` - `ov2_foa.jsonl` - `ov3_foa.jsonl` with split filtering: - train: `("train",)` - val: `("valid",)` - test: `("test",)` and clip truncation: - `max_clip_duration_seconds = 20.0` - `crop_mode = "start"` ### 12.2 `OV23_STAGE1_CFG` This is the safer baseline preset using only: - `ov2_foa.jsonl` - `ov3_foa.jsonl` It uses the same split and truncation policy. ### 12.3 Important note on `ov1` The trainer is already written to use `split` filtering for `ov1`. If the active `ov1` manifest at the configured path does not yet contain `split`, then: - the `OV123` preset will not automatically include those samples in train, valid, or test - the fix is simply to point the preset at the updated `ov1` manifest path The code path itself already supports split-aware loading. ## 13. Current Runtime Status The current implementation has already been checked on: - a real FOA waveform file - mixed-length real manifest samples - full forward pass - fixed-slot matching - multi-task loss computation - BEATs pretrained weight loading The following paths are already operational: - dataset parsing - waveform batching - mixed-length temporal masking - model forward - matching - loss computation - stage-1 optimization loop ## 14. Recommended Launch Pattern Example usage: ```python from spatial_beats_ov123_stage1_config import OV123_STAGE1_CFG from train_spatial_beats import main main(OV123_STAGE1_CFG) ``` If `ov1` still needs a different manifest path, update only: ```python OV123_STAGE1_CFG.train_manifest_paths OV123_STAGE1_CFG.val_manifest_paths OV123_STAGE1_CFG.test_manifest_paths ``` or rebuild the config through: ```python from train_spatial_beats import make_ov123_stage1_config ``` ## 15. Summary The current `Spatial-BEATs` implementation is a FOA-first BEATs-based spatial encoder with: - Qwen-like low-level mel settings - a 7-channel FOA front-end - reused BEATs trunk - fixed-rate `2.5 Hz` spatial token output - fixed-slot supervision heads - variable-length batching - split-aware `ov1/ov2/ov3` training presets The central training idea is: - use explicit spatial supervision to shape the front-end and BEATs trunk - keep the supervision head lightweight - use encoder-side spatial embeddings as the final source of LLM spatial tokens