File size: 8,443 Bytes
c6b1b88 2460459 c6b1b88 2460459 c6b1b88 2460459 c6b1b88 2460459 c6b1b88 2460459 c6b1b88 | 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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | # models/vam · 全模态 (VAM)
`VAM`(原 MiniMindOmni)在 `VLM` 基础上再接入**语音**:既能理解音频/图像输入,也能**生成语音**(Talker 模块)。同样继承 `LMForCausalLM`,复用 `LM` 主干。
## 结构
```
VAM(LMForCausalLM)
├─ model: LM # 复用主干
├─ vision_encoder + vision_proj # 同 VLM(图像理解)
├─ audio_encoder (SenseVoice) + audio_proj # 语音理解
└─ talker: TalkerModule # 语音生成(文本→音频 code)
```
## 双输出(理解 + 生成语音)
`VAM.forward` 同时产出:
- `logits`:文本 token 分布(与 LM 一致);
- `audio_logits`:语音 code 分布(8 层 Mimi 音频 code,维度 `audio_vocab_size=2112`)。
即模型**并行预测文本和语音**,训练时两份交叉熵分别监督。
## 语音(Talker)模块详解
### 音频表示:Mimi Codec
语音不直接用波形,而是用 **Mimi 声码器** 把音频编码为 8 层离散 code:
```
音频波形
└─ Mimi Encoder → 8×T 离散 token(每层 codebook 大小 2048 + 64 special tokens = 2112)
└─ VAM talker 在每一层并行预测
```
- `audio_vocab_size=2112`:2048 个 Mimi code + 64 个特殊 token(含 `<|audio_pad|>`、`<|audio_stop|>`、`<|audio_spk|>`)
- `audio_pad_token=2049` / `audio_stop_token=2050` / `audio_spk_token=2051`
- talker 输出 8 个 `(B, T, 2112)` 的 logits 张量,每层独立计算 CE 损失
- `stop_mask` 对 `<|audio_stop|>` token 做 10× 加权,鼓励模型及时停止
### TalkerModule
在 LLM 某层(`bridge_layer`)之后接一个小型 Transformer 解码器:
```
LM 主干输出 h (B, T, hidden)
└─ TalkerModule:
└─ 第 1 层 cross-attn(以 h 为 query,h 为 key/value)→ 预测第 1 层 code
└─ 第 2 层 cross-attn → 预测第 2 层 code
└─ ...(共 num_talker_hidden_layers 层)
```
- 每层使用独立参数,**并行**预测(非自回归,类似 SoundStorm)
- 训练时 8 层 CE 损失取均值
- Talker 初始化策略:从 `thinker` 的后几层复制权重
### Thinker / Talker 分工
`VAM` 的 LM 主干有 `num_hidden_layers` 层(默认 8),分为:
| 角色 | 层范围 | 功能 |
| --- | --- | --- |
| **Thinker** | `layers[0:bridge_layer]` | 文本理解与推理 |
| **Bridge** | `bridge_layer`(默认 3) | 多模态特征注入点 |
| **Talker** | `layers[bridge_layer+1:]` | 语音生成解码 |
```python
VAM.forward(hidden_states):
# Thinker 层
for layer in self.thinker.layers: h = layer(h)
# 注入多模态特征(vision_proj / audio_proj)
h = inject_multimodal(h)
# Talker 层
for layer in self.talker.layers: h = layer(h)
# 文本 head + 语音 head
logits = self.lm_head(h)
audio_logits = self.talker.decode(h)
```
## 关键字段(`VAMConfig`)
| 字段 | 含义 |
| --- | --- |
| `num_talker_hidden_layers` / `talker_hidden_size` | Talker 子网络规模 |
| `audio_ids` / `audio_special_token` | 音频占位符(如 `<|audio_pad|>`) |
| `audio_hidden_size` / `audio_vocab_size` | 音频特征维 / 音频词表(2048 code + 64 special) |
| `audio_pad/stop/spk_token` | 音频特殊 token id |
| `spk_emb_size` | 说话人 embedding 维(默认 192) |
| `bridge_layer` | 多模态特征注入主干的层(通常 `num_layers // 2 - 1`) |
| `image_token_len` | 单张图像占用的 token 数 |
| `use_moe` | 是否在主干 FFN 使用 MoE |
## 3 阶段 SFT 训练流程
参考 MiniMind-O 的设计,VAM 的 SFT 分为 3 个阶段逐步激活各能力:
### Stage 1:T2A(文本→音频对齐)
```
配置: mode=all, batch_size=4, lr=5e-4, max_samples=2000
数据: sft_t2a_mini.parquet(515k 条,无音频输入,无 spk_emb)
```
- 从预训练 checkpoint(`omni.pth` 或 `omni-v.pth`)初始化
- `mode=all`:所有参数参与训练(113M trainable)
- 数据只有文本(用户问题 + 文本答案 + 音频标签),`question_audios` 列为空
- 目标:让模型学会**生成声学 token**,文本+音频双损失下降
- 损失变化举例:12.79 → 9.80(500 steps)
### Stage 2:A2A audio_proj(音频特征对齐)
```
配置: mode=audio_proj, batch_size=8, lr=5e-4, max_samples=2000
数据: sft_a2a_mini.parquet(77k 条,含 question_audios + spk_emb)
```
- 从 Stage 1 输出初始化
- `mode=audio_proj`:**冻结除 `audio_proj` 外的所有参数**(仅训练 1.0M / 113M 参数)
- 数据包含真实音频输入,需要 `librosa` / `torchaudio` 重采样到 16kHz
- 目标:训练 `audio_proj` 将 SenseVoice 的音频特征映射到 LLM 隐藏空间
- 无视觉数据时 `vision_proj` 梯度为 0,不受影响
### Stage 3:A2A mode=all(全参数微调)
```
配置: mode=all, batch_size=4, lr=2e-5, max_samples=2000
数据: sft_a2a_mini.parquet
```
- 从 Stage 2 输出初始化
- 恢复全参数训练(113M trainable),但**学习率降低至 2e-5**(Stage 1 的 1/25)
- 目标:在已对齐的音频特征基础上,精细调优全部参数
- 损失变化举例:9.72 → 9.27(500 steps)
### 为什么分 3 阶段?
| 阶段 | 解决的问题 | 训练参数 | LR |
| --- | --- | --- | --- |
| 1: T2A mode=all | 冷启动:从头学音频 code 生成 | 全部 | 5e-4 |
| 2: A2A audio_proj | 对齐:让音频特征进入 LLM 空间 | 仅 proj (1%) | 5e-4 |
| 3: A2A mode=all | 精调:全参数协同优化 | 全部 | 2e-5 |
- 若跳过 Stage 1 直接 A2A,模型未见过音频 code 分布,生成质量差
- 若跳过 Stage 2 直接全参数,`audio_proj` 远未收敛,梯度方向主次不分
- Stage 2 用高 LR 只训 proj,是**多模态对齐的标准做法**
## 训练要点
### 损失函数
训练总损失 = 文本 CE + 音频 CE + aux_loss(仅 MoE 时非 0):
```python
# 文本损失(与 LM 一致)
text_loss = CE(logits, labels, ignore_index=-100)
# 音频损失(每层独立 CE,对 stop token 加权)
audio_loss = 0
for i, al in enumerate(audio_logits): # 8 层
layer_loss = CE(al.view(-1, 2112), targets[:, i, :].reshape(-1))
stop_mask = (targets == audio_stop_token).float() # 2050
weighted = layer_loss * valid_mask * (1 + stop_mask * 9)
audio_loss += weighted.sum() / valid_mask.sum()
audio_loss = audio_loss / 8 # 8 层均值
```
### 优化器与梯度
```python
optimizer = AdamW(
filter(lambda p: p.requires_grad, model.parameters()), # 只训 trainable
lr=learning_rate
)
```
- `mode=audio_proj` 时,`model.audio_proj` 约 1.0M 参数,其余 112M 冻结
- `filter(requires_grad)` 避免 optimizer 持有冻结参数的动量(省显存)
### 数据集
| 数据 | 格式 | 行数 | 大小 | 特点 |
| --- | --- | --- | --- | --- |
| T2A | parquet | 515k | 1.5 GB | 无 question_audios,无 spk_emb |
| A2A | parquet | 77k | 841 MB | 含 question_audios + spk_emb + ref_audios |
- 使用 `pyarrow.parquet.iter_batches(batch_size=4096)` 流式读取
- `max_samples` 控制加载行数,适合快速验证
- 音频列(`question_audios`)为二进制 bytes,在 `__getitem__` 时解码
### 说话人嵌入(spk_emb)
- A2A 数据包含预计算的 `spk_emb` 维(192 维,campplus 模型提取)
- T2A 数据无此列 → 回退到 `torch.zeros(192)`
- `spk_emb` 在 forward 中与 hidden states 拼接,协助模型区分说话人
## 检查点管理
训练过程保存两种检查点:
```
save_dir/sft_omni_768.pth # 推理权重(仅 LLM 部分,fp16)
../checkpoints/sft_omni_768.pth # 完整检查点(含 optimizer state 用于续训)
../checkpoints/sft_omni_768_resume.pth # 带 optimizer 的续训文件
```
推理权重过滤掉 `audio_encoder.` 和 `vision_encoder.` 前缀(编码器需单独加载)。
## 要点(面试)
- **统一主干 + 多 head**:理解用共享 `LM` 主干;感知侧挂 encoder+projector,生成侧挂 Talker。新增模态 = 新增 encoder/projector,不改主干。
- **音频 code 多层级**:语音用 8 层离散 code 表示,模型并行预测每一层,贴近 SoundStorm/Mimi 思路。
- **占位 + 投影**范式与 VLM 一致,保证跨模态位置对齐。
- **3 阶段 SFT**:T2A→audio_proj→full,逐步激活文本生成、音频理解、协同优化能力。
- `bridge_layer` 控制多模态信息「插入」主干的深度,是平衡早/晚融合的超参。
|