File size: 4,975 Bytes
f664f3f 2460459 f664f3f 2460459 f664f3f 2460459 f664f3f 2460459 f664f3f 2460459 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | # 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,逐步激活文本生成、音频理解、协同优化能力。
|