Qwen2.5-Omni 7B开源,Qwen的第一个端到端的多模态模型,可以文本、图像、音频和视频输入,同时以流式方式生成文本和自然语音回复。
提出了 Thinker-Talker 架构。
PR还没合进去,要注意安装方式
评测的榜似乎画的有点赶,看不出信息量。
提出了一种新的位置嵌入,称为 TMRoPE(时间对齐多模态 RoPE),用于同步视频输入的时戳与音频。
资源占用:理论值如下,实际要在高1.2倍,看起来消耗有点大。
瞄下代码,关于输入,每个模态都有专门的处理组件:
class Qwen2\_5OmniProcessor(ProcessorMixin):
attributes = [
"omni\_processor"
,
"feature\_extractor"
,
"tokenizer"
]
omni\_processor\_class =
"Qwen2VLImageProcessor"
feature\_extractor\_class =
"WhisperFeatureExtractor"
tokenizer\_class = (
"Qwen2Tokenizer"
,
"Qwen2TokenizerFast"
)
视频这里,还计算每个视频时间网格对应的实际秒数,用于TMRoPE中的时间对齐
if
videos is not None:
videos\_inputs = self.omni\_processor(images=None, videos=videos, **output\_kwargs[
"videos\_kwargs"
])
if
fps is None:
fps = [2.0] * len(videos)
videos\_inputs[
"video\_second\_per\_grid"
] = [
fps[i] / self.omni\_processor.temporal\_patch\_size
for
i
in
range(len(fps))
]
模型代码的核心由3块构成,Thinker,Talker,Token2Wav
talker是将文本转成语音编码,Token2Wav是将编码转成波形。
里边实现了几个Token2Wav变体:
- Qwen2_5OmniToken2WavDiTModel:基于扩散模型的波形生成
- Qwen2_5OmniToken2WavBigVGANModel:基于GAN的波形生成
- Qwen2_5OmniToken2WavModel:通用基类
文本输出和语音输出是两条并行的路径:
- 文本输出:输入 → Thinker → 文本输出
- 语音输出:输入 → Thinker → Talker → Token2Wav → 语音输出
只有当需要语音输出时,才会激活Talker模块和Token2Wav模块,将Thinker生成的文本内容转换为语音。
细节可以自行看源码,不贴了。
在看看新的位置编码。
对于纯文本,使用常规的1D位置编码。
对于包含视觉(图像/视频)和文本的混合输入,函数分别计算:
- 视觉部分用3D位置编码
- 文本部分用1D位置编码
比如说,一个有3个时间片、2×2空间分辨率的视频示例:
输入序列: [V V V V V V V V V V V V T T T T T]
视觉时间位置ID: [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]
视觉高度位置ID: [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1]
视觉宽度位置ID: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
文本位置ID从视觉最大位置ID+1开始: [3, 4, 5, 6, 7]
将时间维度映射到位置ID,考虑每秒对应多少个位置单位(position_id_per_seconds)
t\_index = (torch.arange(grid\_t) * second\_per\_grids[video\_idx] * position\_id\_per\_seconds).long()
llm\_pos\_ids = self.get\_llm\_pos\_ids\_for\_vision(
start\_idx, video\_idx, spatial\_merge\_size, t\_index, grid\_hs, grid\_ws
)
针对视频中包含音频的情况,还单独处理了,视频和音频交替编码,按时间块组织,每个时间块包含视频帧和对应的音频段,音频有特殊的开始和结束标记(audio_start_token_id和audio_end_token_id)
# 视频和音频混合处理
t\_index\_split\_chunk = self.split\_list\_into\_ranges(t\_index, t\_ntoken\_per\_chunk)
for
t\_chunk
in
t\_index\_split\_chunk:
vision\_ntoken\_per\_chunk = len(t\_chunk) * grid\_h * grid\_w // (spatial\_merge\_size**2)
new\_src\_item.extend([video\_token\_id] * vision\_ntoken\_per\_chunk)
# 为视频区块分配位置ID
new\_src\_item.extend(min(t\_ntoken\_per\_chunk, pure\_audio\_len - added\_audio\_len) * [audio\_token\_id])
# 为音频区块分配位置ID