# B1:局部视觉+近期混合音频,普通池化融合 这是用于判断“原 Omni readout 是否丢掉有用的局部 AV 线索”的基线, 不是已证明的改进,也不是 multi-speaker attention 模型。 保留原 `[T,4,2048]` 特征;不重新提取全局 Thinker,不改标签和 AP 协议。 若要比较**不输入 Thinker 的纯 A / V / AV**,使用 [EasyCom encoder-only 实验](easycom_encoder_only.md)。本页的 `audio/visual/av` 仍包含全局 G,不能将两种设置混为一组。 ## 固定实验定义 | 分支 | 冻结特征 | 更新/池化 | 训练时处理 | |---|---|---|---| | 原全局特征 | Thinker last4,`[T,4,2048]` | 原来的 8s memory、5Hz | 原 concat-MLP,8192→2048 | | 局部视觉 V | 每个人脸裁剪,Qwen vision encoder + merger,2048D | 原视频固定 2FPS;先空间 mean,再逐人等权 mean,再最近 1s 有效帧等权 mean | 可训练 LN→Linear(2048,256)→GELU | | 近期音频 A | Qwen audio encoder + 原生 output projection,2048D | 每 200ms 重编码 `[max(0,t-1),t)` 混合音频;仅池化有效输出 tokens | 同上 | Qwen 3B 的 **2048 是编码器原生输出维度,256 只是新增 head 的可选投影宽度**。 特征文件永远保存原始维度,`--local-projection-dim 0` 可不降维。 正常 AV 组合为 `[2048 global,256 V,256 A,2 validity]`,共 2562D, 接 LN→Linear(2562,1024)→GELU→Dropout→Linear(1024,30),输出仍为 `[10,3]`。 单模态只附加对应一路与一个有效标志;不传 `--local-feature-root` 时保留原 head 参数结构。 局部视觉没有运动编码、ASD 分数、speaker-aware attention、身份 embedding 或图网络。 参与者 ID 仅保存供审计,不输入 head。人数多的帧不会因 token 多而得到更大权重。 没有有效框时视觉为零、valid=False;音频静音仍 valid=True。 缺少音轨、损坏的媒体或缺少应有的标注文件会报错,不会悄悄冒充“空输入”。 时间约定是 `[t-1,t)`:例如 t=1.0s,视觉取 0.0、0.5s,排除 1.0s 的帧。 音频先截断再做 mel 和编码,不对完整视频的音频编码后切片。 这保证相对于 readout 时刻的输入因果性,但不保证编码器在窗口内部逐 token 因果。 实现依据:[Qwen 3B 配置](https://huggingface.co/Qwen/Qwen2.5-Omni-3B/blob/main/config.json)、 [Transformers 4.52.3 编码器 API](https://github.com/huggingface/transformers/blob/v4.52.3/src/transformers/models/qwen2_5_omni/modeling_qwen2_5_omni.py)、 [4.57.6 编码器 API](https://github.com/huggingface/transformers/blob/v4.57.6/src/transformers/models/qwen2_5_omni/modeling_qwen2_5_omni.py)。 脚本加载 Thinker checkpoint 后释放语言解码器,仅将冻结的视觉/音频塔移到 GPU;没有 Thinker forward。 局部提取已适配 **Transformers 4.52.3 和 4.57.6**,现有 L20 / ms-swift 容器若是 4.57.6, 无需降级,也不要为这一步重新安装整个 `requirements.txt`。旧脚本只接受精确的 4.52.3, 因此即使接口存在也会在加载模型前主动报错;更新脚本后重跑原 smoke 命令即可。 两个版本都校验原生 token 输出形状与权重完整性,并显式使用 `use_fast=False` 固定图像预处理路径。 未知版本仍在媒体解码前报错;`--preview-only` 不受编码器版本检查影响。 该兼容范围只针对本局部提取脚本,不代表 Thinker KV replay 或 ROMA 的全部接口可互换。 输出的 `config.transformers` 记录实际运行版本,不再写死 4.52.3。 同一局部特征实验的 train/test sessions 应统一版本、权重和提取配置;加载器会拒绝混用版本。 接口兼容不等于不同版本的浮点特征逐位相同,已有全局 Omni 特征无需重提。 ## 1. EasyCom:先检查框,再跑一个 4s smoke test 在已有项目依赖的 GPU 机器运行,以下路径沿用之前实验;raw 数据根按实际挂载修改。 不需要先重新编码 2FPS 视频,直接读取原 `Main/Video_Compressed`。 ```bash export CUDA_VISIBLE_DEVICES=0 MODEL=/data1/yfzx/wangx/Models/Qwen2.5-Omni-3B DATA_ROOT=/data1/yfzx/wangx/Datasets/easycom/extracted_easycom export FEATURE_ROOT=/data1/yfzx/wangx/Datasets/easycom/omni_features/open_stream_2fps_8s_last4_stack export TARGET_ROOT=/data1/yfzx/wangx/Datasets/easycom/easycom_target_frame export LOCAL_ROOT=/data1/yfzx/wangx/Datasets/easycom/omni_features/local_qwen_1s LOCAL_ARGS=( --dataset easycom --data-root "$DATA_ROOT" --feature-root "$FEATURE_ROOT" --target-root "$TARGET_ROOT" --model-id "$MODEL" --device cuda:0 --dtype bfloat16 --flash-attention-2 --video-fps 2 --visual-window-seconds 1 --audio-window-seconds 1 --box-kind face --box-coordinate-space encoded ) # 不加载模型;输出四张带框图片,请先检查框是否真在人脸上。 python extract_turn_local_features.py "${LOCAL_ARGS[@]}" \ --samples 1 --max-steps 20 --preview-only --output-root features/local_preview # 4s / 20 states;此文件明确标记 partial,不能误拿去训练整段。 python extract_turn_local_features.py "${LOCAL_ARGS[@]}" \ --samples 1 --max-steps 20 --output-root features/local_smoke ``` EasyCom face 标注首帧是 1,head 首帧是 0;按**原始解码帧号**取框。 视频裁去左侧身份 banner,保留右侧 1920 像素;这份 EasyCom 标注的框坐标包含 banner。 EasyCom 默认使用 `encoded`,先减去当前源视频的裁剪起点,再裁框: `x_camera = x_annotation - (source_width - 1920)`。例如已核对的 Session_4 原视频宽度 为 2124,应减去 204;不同视频可能不同,不能统一硬编码。y 也按实际裁剪起点转换。 `camera` 仅用于已经手动换算到裁后图像的标注;Ego4D 无此 banner 裁剪,默认仍为 `camera`。 之前示例中的 `camera` 参数会导致这份原始 EasyCom 标注统一右偏,现已更正。 若当前 shell 的 `LOCAL_ARGS` 还含旧参数,请修改它,或将 `--box-coordinate-space encoded` 放在 `"${LOCAL_ARGS[@]}"` **后面**覆盖旧值。仅更新代码不会覆盖显式传入的旧参数。 预览会打印 source/cropped 尺寸与实际扣除偏移,同时在预览目录生成 `boxes.json`, 记录原框、裁后框与源帧号。必须检查框确实落在人脸上,不能只看最终特征维度。 官方帧号与 banner 说明见 [EasyCom 数据说明](https://github.com/facebookresearch/EasyComDataset#notes-and-known-issues)。 若已经用错误坐标提取局部视觉特征,必须重新从图像提取,不能靠修改 `.pt` 中的框坐标修复。 推荐输出到新目录(例如 `local_qwen_1s_encoded`),并把训练的 `LOCAL_ROOT` 指向新目录。 原全局 Omni 特征和 baseline 无需重算;旧局部 V/AV 训练结果不可与修正后的特征混用。 ```bash python - <<'PY' import torch p = torch.load('features/local_smoke/Session_1.pt', map_location='cpu', weights_only=False) print('partial:', p['partial']) print('transformers:', p['config']['transformers']) print('box coordinates:', p['config']['box_coordinate_space']) for key in ('visual', 'audio'): x = p[key] print(key, tuple(x.shape), x.dtype, 'finite:', bool(torch.isfinite(x).all()), 'valid:', p[key + '_valid'].float().mean().item()) print('person features:', tuple(p['persons']['features'].shape)) print('times:', p['times'][0].item(), p['times'][-1].item()) PY ``` 3B 预期 V/A 各 `[20,2048]`,时间 0.2–4.0s;没有人脸的时刻允许 V 无效。 本机已验证 CPU 对齐/融合/合成数据训练推理,真实 checkpoint + GPU 提取仍需此 smoke test。 另有不下载权重的小型随机 Qwen 编码器 API 和本地 checkpoint 加载测试, 在 4.52.3 / 4.57.6 上均适用;checkpoint 加载测试需要 `accelerate`。 可先检查本机编码器接口,再跑完整回归: ```bash python -m unittest -v test_local_turn_features.TinyQwenEncoderTests python -m unittest -v test_easycom_head_objectives test_local_turn_features ``` ## 2. 提取全部 12 sessions(只做一次) ```bash python extract_turn_local_features.py "${LOCAL_ARGS[@]}" \ --samples 1 2 3 4 5 6 7 8 9 10 11 12 --output-root "$LOCAL_ROOT" ``` 默认前台顺序执行,显示视觉帧进度和音频 state 进度;模型只加载一次。 已有文件默认会报错;恢复时可只列尚未完成的 sessions,或增加 `--reuse-existing` 逐文件验证完整性、时间对齐和提取配置后跳过已完成文件。 `--overwrite` 是明确重算,不会重用不兼容文件。不要覆盖原始 feature root。 `persons` 还保留每帧变长的逐人特征、frame_offsets、框、帧号和时间,供后续 B2 复用。 若在不同挂载路径提取同一 checkpoint,使用相同 `--encoder-id`;不同权重不可共用这个 ID。 ## 3. 四组对照:4 epochs、无 dev、相同种子和 loss | 名称 | 输入 | |---|---| | baseline | 原 Omni last4 | | audio | 原 Omni + 近期音频 | | visual | 原 Omni + 局部视觉 | | av | 原 Omni + 两路局部特征(B1) | ```bash # 先做 seed0 四组;默认前台跑完训练后立即评测 Session 3/2/1。 CUDA_VISIBLE_DEVICES=0 bash run_easycom_local_baselines.sh all 0 # 再补 seed1/2;不会覆盖 seed0。 CUDA_VISIBLE_DEVICES=0 bash run_easycom_local_baselines.sh all 1 2 # 也可把互不重复的组合分配到不同卡,不要复用相同 variant/seed 输出路径。 # CUDA_VISIBLE_DEVICES=1 bash run_easycom_local_baselines.sh audio 0 python summarize_local_baselines.py --protocol causal_valid_only python summarize_local_baselines.py --protocol egospeak_full_timeline ``` 脚本使用训练 Session 4–12、epoch=4、batch=512、lr=7e-5、wd=5e-5、warmup=1.6 epochs、 warmup_factor=0.3、dropout=0.2、LSTR-MCE、不加 transition objective。 `FEATURE_ROOT/TARGET_ROOT/LOCAL_ROOT` 用上面 export 的值,也可自行覆盖。 结果分别在 `checkpoints/easycom_local_b1` 和 `results/easycom_local_b1`。 如要不降维,设置 `LOCAL_PROJECTION_DIM=0`,并换 `CHECKPOINT_ROOT/RESULT_ROOT`。 手动训练只需在原命令增加: ```bash --local-feature-root "$LOCAL_ROOT" --local-modalities av --local-projection-dim 256 ``` 手动推理增加 `--local-feature-root "$LOCAL_ROOT"`;模态组合从 checkpoint 读取。 Loader 会检查 sample、时间、label_indices、维度、valid mask 和 encoder/pooling 配置。 训练与评测必须用同一种 AP 协议作比较;不要混用 full-timeline 与 causal-valid 的表格。 ## 4. F1 Frame-set Attention 已有完整局部 sidecar 的 persons 字段保存了逐帧、逐人的原生 2048D 特征,因此 F1 不需要重新提取图片或音频。新增训练参数为: --local-feature-root "$LOCAL_ROOT" \ --local-modalities av \ --local-fusion frame-set-attention \ --local-projection-dim 256 \ --person-attention-dim 256 \ --person-attention-heads 4 F1 用融合后的 G 和直接局部音频 A 构造一个 query,读取当前最新半开因果帧中的可变人物 集合。人物集合只在 batch 内动态 padding;NULL person 处理没有有效框的时刻。A 仍直接 进入最终 head。第一版不使用 bbox、track ID、跨帧 GRU 或双向 AV attention。 训练输出会打印 source frame 数、person observation 数、max_people 和 attention 配置。 推理只需继续提供 local feature root;融合方式和 attention 参数从 checkpoint 恢复。 正式比较需要重新训练同 split、seed 和优化设置的 pooled AV 对照,不能直接把不同开发 协议的旧 checkpoint 当作唯一基线。 ## 5. F5/F6 screening variants 当前 local sidecar 没有 F3/F4 所需的逐 track 人物历史,预计算的 `states` 也已经位于 Thinker readout 之后。因此下面两项是用于快速筛选的旁路实验,不是路线文档中完整的 F5/F6: - `audio-person-frame-set`:在 F1 前增加一个 audio-only query 对当前人物集合的读取, 通过零初始化标量 gate 加到 G+A floor query;初始函数与 F1 相同。这是 F5 的 current-frame screening,不含 person-conditioned 5 Hz audio/Omni history。 - `gated-person-state-adapter`:F1 先选择当前人物证据,再通过零初始化标量 gate 将 residual 加到最终 frozen Thinker state。它位于 Thinker **之后**,只能叫 F6 screening, 不能叫 Thinker internal adapter 或 end-to-end Omni adaptation。 两项都继续使用当前 `persons`、audio、5 Hz cutoff 和 NULL-person 合约,无需重新提取 局部特征。训练会逐 epoch 打印 `person_gates`,checkpoint/metrics 会记录明确的 `local_fusion_scope`。运行入口: ```bash export FEATURE_ROOT=/data1/yfzx/wangx/Datasets/easycom/omni_features/open_stream_2fps_8s_last4_stack export TARGET_ROOT=/data1/yfzx/wangx/Datasets/easycom/easycom_target_frame export LOCAL_ROOT=/data1/yfzx/wangx/Datasets/easycom/omni_features/local_qwen_1s_encoded_tf4576 # F1、F5 screening、F6 screening;每项 seed 0/1/2,前台顺序执行。 CUDA_VISIBLE_DEVICES=0 bash run_easycom_person_fusion_screening.sh all 0 1 2 # 也可把互不重复的 variant/seed 分配到不同 GPU。 SUMMARIZE=0 CUDA_VISIBLE_DEVICES=1 bash run_easycom_person_fusion_screening.sh f5-screen 0 ``` 脚本固定 Session 4--11 训练、Session 12 开发选 checkpoint,并输出 Session 1--3 的 exploratory dense mAP 与 `pred/valid`。事件协议冻结前,不能按现有 timing proxy 选择这些 模块;完整 F5 仍需 causal person history,完整 F6 仍需进入 Thinker 上层并与 LoRA 控制 比较。 ## Ego4D 接口 完整命令见 [Ego4D 四组局部增强实验](ego4d_local_baselines.md):新增 manifest 批量提取、 续跑校验、`run_ego4d_local_baselines.py` 四组运行入口和匹配 seed 的增量汇总。 提取脚本同样支持 `--dataset ego4d`:输入根为 `/data/wangx/Datasets/ego4d`, 读取 `v1/clips/.mp4`、`v1/annotations/av_train.json` 与 `av_val.json`, `--samples` 传已有 Omni `.pt` 的 clip UID。`--target-root` 指向 `ego4d_target_frame`。 可用 `--ego4d-annotations` 指定其他导出路径。先用 `--preview-only --max-steps 20` 检查框。 读取的是与 clip_uid 对应的可见人物 tracks;不使用 wearer 框、voice_segments、讲话标签, 不对缺失帧用未来框插值。载入大 JSON 需要相应主机内存。 train/infer 使用原 `--dataset ego4d --manifest ...` 加上述 local 参数; EasyCom 的运行 shell 只管理 EasyCom,不能用于 Ego4D 的 split。 ## 如何解读实验 先看每种子 paired 的 0.2s wearer AP 与 10-horizon mean mAP,不能提前承诺提升。 若只有 A 有效,先支持“近期声音补充”的解释;V/AV 的额外收益才支持局部视觉线索有价值。 这几组 head 参数量不同,结果不是纯架构公平性证明;下一步应做同参数量全局头/打乱局部输入控制。 这一版使用提供的框(部分身份标注经人工修正),不是在线 detector 的端到端测试。 论文必须披露这个协议,后续补在线检测框实验;不得把 close-mic、讲话标签或测试集调优引入输入。 提供框读取即使只取当前帧,也不代表框生成器本身是在线因果的。 2FPS 静态脸特征不能代表完整唇动建模;若 V 无提升,不宜直接断言多人视觉无用。