MiniMind-O 学习笔记 15:音频如何变成 Token,Mimi 与 8 路 Codebook

理解 MiniMind-O 的 Talker,先要理解一个问题:

模型为什么不是直接生成 wav 波形?
为什么要生成 Mimi codes?

文本模型生成的是 token id。MiniMind-O 的语音输出也走类似思路:不直接生成连续音频,而是先把音频离散化成一串 code id,让 Talker 预测这些 code id,再由 Mimi decoder 还原成声音。

1. 直接生成波形为什么难

音频是连续信号。比如 README 中提到 Mimi 支持 24 kHz 音频,这意味着 1 秒音频有 24000 个采样点。

如果让模型逐点生成波形:

1 秒 -> 24000 个点
10 秒 -> 240000 个点

这个序列太长了。对于 Transformer 来说,训练和推理都会很重。

所以很多语音生成系统会先使用神经音频 codec:

waveform -> codec encoder -> 离散 audio codes
audio codes -> codec decoder -> waveform

在 MiniMind-O 里,这个 codec 是 Mimi。

2. Mimi 可以看成语音版 tokenizer

文本 tokenizer 做的是:

自然语言字符串
-> token ids

Mimi 做的是:

语音 waveform
-> audio code ids

所以可以类比:

文本:
  "你好" -> tokenizer -> [token ids]

音频:
  一段语音 -> Mimi encoder -> 

反过来:

文本 token ids -> tokenizer.decode -> 字符串
audio code ids -> Mimi decoder     -> 声音

MiniMind-O 让 Talker 学的不是 waveform,而是 Mimi 的 audio code ids。

3. 什么是 codebook

codebook 可以理解成一本离散声学字典。

神经 codec 会把连续音频特征映射到字典中的某个条目:

连续声学特征
-> 在 codebook 里找一个最合适的条目
-> 用这个条目的编号表示

如果一个 codebook 有 2048 个条目,那么一个 code 就大致是:

0..2047

这和文本词表很像:

文本词表:
  token id -> 一个文本片段

音频 codebook:
  code id -> 一个声学表示

当然,audio code 对应的不是人能直接读懂的字,而是 codec 学出来的声学向量。

4. 为什么是 8 路 codebook

README 中提到 MiniMind-O 使用 Mimi 的 8 层 codebook。一个音频时间片不是只用一个 code 表示,而是用 8 个 code 一起表示:

frame_t = [
  codebook_0 的 code,
  codebook_1 的 code,
  codebook_2 的 code,
  codebook_3 的 code,
  codebook_4 的 code,
  codebook_5 的 code,
  codebook_6 的 code,
  codebook_7 的 code
]

你可以把它理解成多本字典共同描述同一帧音频。

一种常见直觉是“前面的 codebook 更粗,后面的 codebook 补细节”。这有帮助,但不要把它严格理解成:

codebook 0 = 低频
codebook 7 = 高频

它不是传统信号处理里的分频滤波器组。更准确地说,它们是神经 codec 学出来的多级离散表示,类似多级残差量化:

第 1 本字典表示一部分主要信息
第 2 本字典补充剩余信息
后面的字典继续补充细节

5. MiniMind-O 里的 audio_vocab_size

在 OmniConfig 中有:

self.audio_vocab_size = kwargs.get("audio_vocab_size", 2112)

常规 Mimi code 大致使用:

0..2047

2048 以后留给特殊 token,比如:

2049 -> audio_pad_token
2050 -> audio_stop_token
2051 -> audio_spk_token

这些特殊 token 的作用类似文本里的 pad/eos,只是用在音频 code 序列里。

6. TalkerHead 为什么输出 8 组 logits

Talker 不是输出一个音频 token,而是输出 8 路 codebook 的预测分布:

audio_logits[0] -> codebook 0
audio_logits[1] -> codebook 1
...
audio_logits[7] -> codebook 7

每一路 logits 的最后一维大小都是 audio_vocab_size

训练时:

audio_logits[i] 和 audio_labels[:, i, :] 计算交叉熵

推理时:

从每一路 logits 中采样一个 code
8 个 code 合起来构成一个 audio frame

7. 什么是错位补齐

如果一个音频 frame 需要 8 个 code,那么最直接的办法是同一时刻并行预测 8 个 code。

MiniMind-O 采用了错位方式。可以先看成把 8 路 codebook 斜着排开:

step    cb0     cb1     cb2     cb3     cb4     cb5     cb6     cb7
---------------------------------------------------------------------
1       c0_0    pad     pad     pad     pad     pad     pad     pad
2       c0_1    c1_0    pad     pad     pad     pad     pad     pad
3       c0_2    c1_1    c2_0    pad     pad     pad     pad     pad
4       c0_3    c1_2    c2_1    c3_0    pad     pad     pad     pad
...
8       c0_7    c1_6    c2_5    c3_4    c4_3    c5_2    c6_1    c7_0

到第 8 步时,frame_0 才凑齐:

frame_0 = [c0_0, c1_0, c2_0, c3_0, c4_0, c5_0, c6_0, c7_0]

代码中会按对角线把同一个音频时间片取出来。

为什么这样做?

1. 保持自回归生成形式。
2. 让不同 codebook 之间不是完全独立。
3. 预热几步后可以流式输出 audio frame。

8. 8 路 codebook 在时域上对齐吗

要分两个时间轴:

音频时间轴:
  送进 Mimi decoder 前,8 路 codebook 是对齐的。

生成时间轴:
  Talker 自回归生成时,8 路 codebook 是错位展开的。

所以最终音频 frame 上是对齐的:

frame_0 = [c0_0, c1_0, ..., c7_0]

只是生成时为了自回归和流式,被临时错开了。

9. 本文小结

MiniMind-O 的语音输出链路可以总结成:

Talker 预测 Mimi audio codes
Mimi decoder 把 codes 还原成 waveform

其中:

一个音频 frame = 8 路 codebook 各出一个 code
Talker 输出 = 8 组 codebook logits
错位补齐 = 生成时间轴上的延迟展开,最终再对齐成音频 frame

下一篇我们看这些 audio codes 如何和文本对话一起被放进 OmniDataset,最终变成训练样本。

发表评论