# multimodal/ · 多模态 把「外部模态」(图像/音频)编码并投影到 LLM 隐藏空间,实现多模态理解与生成。 ## 数据流 ``` 原始输入(图/音) └─ encoder → 模态特征 └─ projector → 投影到 hidden_size └─ 替换 input_ids 中的占位 token 位置的 embedding └─ 进入共享 LM 主干 ``` ## 编码器(Encoders) 把外部模态的原始信号编码为模态特征,冻结预训练权重、不参与 LLM 主干训练。 | 文件 | 类 | 作用 | | --- | --- | --- | | `encoders/vision/siglip.py` | `SiglipVisionEncoder` | 图像 → patch 特征(SigLIP) | | `encoders/audio/sensevoice.py` | `SenseVoiceAudioEncoder` | 语音 → 帧级别 fbank 特征(SenseVoice) | ### SigLIP 视觉编码器 - 基于 `transformers.SiglipVisionModel`(siglip2-base-p32-256) - 图像被分割为 32×32 像素的 patch,输出 patch 特征序列 - `processor` 负责图像预处理(resize、normalize) - 输出通过 `MMVisionProjector` 重采样到 `image_token_len=64` 个视觉 token ### SenseVoice 音频编码器 - 基于 FunASR 的 `SenseVoiceEncoderSmall`(阿里达摩院) - 输入:16kHz 单声道音频 → fbank 特征 - 输出:**帧级别**声学特征(每帧对应约 10ms 音频) - 编码器输出维度:`d_model=512`(`audio_hidden_size`) - 内部包含 frontend(fbank 提取)+ encoder(Conformer 架构) - 使用 `funasr.AutoModel` 加载,`trust_remote_code=True` > 注意:FunASR 的 C 库初始化与 PyArrow 存在冲突,需确保 pyarrow 在 FunASR 模型创建后导入(VAMDataset 使用 lazy import)。 ### 音频特征长度 音频经 SenseVoice 编码后的帧数取决于输入长度。由于帧数与文本 token 数不一致,需用 `audio_proj` 重采样到固定长度匹配占位 token: ``` 音频 3 秒 @ 16kHz = 48000 采样点 → fbank (80 维) × ~300 帧 → audio_proj 重采样到目标帧数(占位 token 数) → 替换 input_ids 中的 <|audio_pad|> 位置 ``` ## 投影器(Projectors) 把 encoder 输出的模态特征投影到 LLM 隐藏维度(`hidden_size`),随后替换 `input_ids` 中的占位 token 位置。 | 文件 | 类 | 作用 | | --- | --- | --- | | `projectors/vision.py` | `MMVisionProjector` | 视觉特征 → LLM `hidden_size` | | `projectors/audio.py` | `MMAudioProjector` | 音频特征 → LLM `hidden_size` | ### MMAudioProjector ``` 输入: (B, T_audio, audio_hidden_size=512) └─ Linear(512 → hidden_size) └─ 2D 位置编码(补偿音频帧与文本 token 的位置差异) └─ 重采样到目标 token 数 输出: (B, target_tokens, hidden_size) ``` ## VAM 双模态架构 VAM 同时处理视觉和音频: ``` VAM(LMForCausalLM) ├─ model: LM # 共享主干 ├─ vision_encoder + proj # 图像理解 ├─ audio_encoder + proj # 语音理解 └─ talker: TalkerModule # 语音生成 ``` ### 多模态特征注入 在 `bridge_layer`(默认第 3 层)之后注入多模态特征: ```python # Forward 中的多模态注入 hidden_states = self.model.embed_tokens(input_ids) # Thinker 层 h = self.thinker(hidden_states) # 注入视觉特征 if pixel_values is not None: vision_features = self.vision_proj(self.vision_encoder(pixel_values)) h = inject_at_positions(h, vision_features, vision_positions) # 注入音频特征 if audio_inputs is not None: audio_features = self.audio_proj(self.audio_encoder(audio_inputs)) h = inject_at_positions(h, audio_features, audio_positions) # Talker 层 h = self.talker(h) # 双 head 输出 logits = self.lm_head(h) audio_logits = self.talker.decode(h) # 8 层 Mimi code ``` ### 模态组合 VAM 支持灵活的模态输入组合: | 输入模态 | 设置 | 用例 | | --- | --- | --- | | 文本 + 音频 | 提供 `audio_inputs`,`pixel_values=None` | 语音对话 | | 文本 + 图像 | 提供 `pixel_values`,`audio_inputs=None` | 图像理解 | | 文本 + 图像 + 音频 | 同时提供两者 | 全模态交互 | | 纯文本 | 两者均为 None | 回退到 LM | ## 设计要点(面试) - **encoder 冻结 + 只训 projector**:大模型主干易灾难性遗忘,冻结预训练视觉/音频 encoder、只训轻量投影层,是高效多模态对齐的常用做法。 - **`encoders/` 与 `projectors/` 分离**:encoder 负责「理解模态」,projector 负责「对齐到 LLM 空间」,二者职责清晰、可独立替换。 - **可扩展性**:加新模态 = 加一对 encoder+projector + 占位 token,主干不动(符合本仓库分层初衷)。 - 文本侧用特殊占位 token(`<|image_pad|>` / `<|audio_pad|>`)预留位置; - `forward` 把投影特征写到这些位置,LLM 对文本/视觉/音频 token 一视同仁。 - **3 阶段 SFT**:T2A→audio_proj→full,逐步激活文本生成、音频理解、协同优化能力。