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 层)之后注入多模态特征:
# 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,逐步激活文本生成、音频理解、协同优化能力。