Qwen2.5-Omni -多模态端到端大模型

这是 Qwen 系列中全新的旗舰级多模态大模型,专为全面的多模式感知设计,可以无缝处理包括文本、图像、音频和视频的各种输入,同时支持流式的文本生成和自然语音合成输出

主要特点

  • 全能创新架构:我们提出了一种全新的Thinker-Talker架构,这是一种端到端的多模态模型,旨在支持文本/图像/音频/视频的跨模态理解,同时以流式方式生成文本和自然语音响应。我们提出了一种新的位置编码技术,称为TMRoPE(Time-aligned Multimodal RoPE),通过时间轴对齐实现视频与音频输入的精准同步。
  • 实时音视频交互:架构旨在支持完全实时交互,支持分块输入和即时输出
  • 自然流畅的语音生成:在语音生成的自然性和稳定性方面超越了许多现有的流式和非流式替代方案。Qwen2.5-Omni 支持修改输出语音的音色类型,目前支持2种音色类型。
  • 全模态性能优势:在同等规模的单模态模型进行基准测试时,表现出卓越的性能。Qwen2.5-Omni在音频能力上优于类似大小的Qwen2-Audio,并与Qwen2.5-VL-7B保持同等水平。
  • 卓越的端到端语音指令跟随能力:Qwen2.5-Omni在端到端语音指令跟随方面表现出与文本输入处理相媲美的效果,在MMLU通用知识理解和GSM8K数学推理等基准测试中表现优异。

摘要:

Qwen2.5-Omni,这是一种端到端的多模态模型,能够感知多种模态信息,包括文本、图像、音频和视频,同时以流式方式生成文本和自然语音响应。为了实现多模态信息输入的流式处理,Qwen2.5-Omni 的音频和视觉编码器采用了 分块处理(block-wise processing) 方法。该策略有效地解耦了长序列多模态数据的处理,将感知任务交由多模态编码器,而将长序列建模任务交由大语言模型(LLM),这种分工机制通过共享注意力机制增强了不同模态的融合。

为同步视频输入的时间戳与音频,我们采用 交错(interleaved) 方式对音视频数据进行序列化,并提出了一种新颖的位置编码方法——TMRoPE(Time-aligned Multimodal RoPE,时间对齐多模态旋转位置编码)ps:关于旋转位置编码

在同时生成文本和语音的过程中,为了避免两种模态之间的相互干扰,我们提出了 Thinker-Talker 架构。在该框架下:

  • Thinker 作为大语言模型,负责文本生成;
  • Talker 是一个 双轨自回归模型(dual-track autoregressive model),它直接利用 Thinker隐藏表示来生成音频标记(audio tokens)作为输出

ThinkerTalker 均以端到端方式进行训练和推理。此外,为了实现流式音频标记解码,我们引入了 滑动窗口 DiT(sliding-window DiT),通过限制感受野来减少初始数据包延迟。

Qwen2.5-Omni 的关键特性可总结如下:

  • Qwen2.5-Omni 是一个 统一多模态模型,能够感知所有模态信息,并以流式方式同时生成文本和自然语音响应。
  • 我们提出了一种新颖的位置编码算法 TMRoPE(Time-aligned Multimodal RoPE),该方法显式融入时间信息,以实现音视频的同步。
  • 我们设计了 Thinker-Talker 架构,以支持 实时理解语音生成
  • 在多模态基准测试中,Qwen2.5-Omni 展示了卓越的性能,相较于类似规模的单模态模型表现更优,尤其在语音指令跟随任务上,其能力可与纯文本输入任务相媲美。
  • 在需要整合多种模态的信息处理任务中(如 OmniBench 评测),Qwen2.5-Omni 达到了 最先进(state-of-the-art) 的性能。
  • 在语音生成方面,Qwen2.5-Omniseed-tts-eval 评测中表现出色,展现出强大的语音生成能力和稳健性。
Thinker 负责 文本生成
Talker 通过直接接收 Thinker 提供的高级表示,专注于 流式语音标记生成

Architecture:

图 2 所示,Qwen2.5-Omni 采用 Thinker-Talker 架构,其中:

  • Thinker 类似于大脑,负责处理和理解 文本、音频、视频 等模态输入,生成高级表示和对应的文本输出。
  • Talker 类似于人类的嘴巴,以流式方式接收 Thinker 生成的高级表示和文本,并顺畅地输出离散语音标记。

架构细节

  • Thinker 采用 Transformer 解码器,并配备 音频编码器图像编码器,以增强信息提取能力。
  • Talker 采用 双轨【同时接收文本token和语音token】自回归 Transformer 解码器 结构(受 Mini-Omni [Xie & Wu, 2024] 启发)。
  • 训练和推理 过程中,Talker 直接接收 Thinker 的高维表示,并共享其全部历史上下文信息,使整个架构作为一个统一模型进行 端到端训练和推理

感知:

Qwen2.5-Omni 通过 Thinker文本、音频、图像和视频(无音频) 进行处理,将它们转化为一系列隐藏表示作为输入。具体步骤如下:

  1. 文本输入
    • 对于文本输入,我们采用 Qwen 的分词器(Byte-level Byte-pair Encoding),词汇表包含 151,643 个常规标记。
  2. 音频输入和视频中的音频
    • 音频输入(包括视频中的音频部分)首先被重采样至 16kHz 的频率,然后将原始波形转化为 128 通道的梅尔频谱图(mel-spectrogram),窗口大小为 25ms,步幅为 10ms
    • 音频编码器采用 Qwen2-Audio 的音频编码器(Chu et al., 2024b),每一帧音频表示大约对应于 原始音频信号的 40ms 时长。
  3. 图像和视频输入
    • 对于图像输入,我们采用 Qwen2.5-VL 的视觉编码器(基于 Vision Transformer(ViT) 模型,约 6.75 亿个参数),能够有效处理图像和视频输入。
    • 视觉编码器使用混合训练方法,结合图像和视频数据,确保其在图像理解和视频理解上的高效表现。
    • 为了最大程度地保留视频信息并适应音频采样率,我们采用 动态帧率(dynamic frame rate)进行视频采样。此外,为保持一致性,每个图像都被视为两个相同的帧。

视频与TMRoPE

TMRoPE

我们提出了一种 音视频时间交错算法(time-interleaving),并引入了新的 位置编码方法 —— TMRoPE(Time-aligned Multimodal RoPE)。如 图 3 所示,TMRoPE 编码了多模态输入的三维位置,采用 多模态旋转位置编码(M-RoPE)ps: Qwen2-VL多模态旋转位置编码 多模位置编码】,并结合绝对时间位置。具体方法是将原始的旋转位置编码分解为三个组件:时间、图像高度和宽度。

  • 文本输入 使用相同的位置信息(位置 ID)来处理各组件,使得 M-RoPE 在文本输入中与 1D-RoPE 等效。
  • 音频输入 也使用相同的位置信息,并引入绝对时间位置编码,每 40ms 对应一个时间 ID
  • 图像输入,每个视觉标记的时间 ID 保持不变,而高度和宽度组件则根据标记在图像中的位置分配不同的 ID。
  • 音视频输入 情况下,音频依然使用相同的时间位置 ID 编码,每帧 40ms。视频则作为一系列图像处理,每帧对应的时间 ID 增量,同时高度和宽度组件的 ID 分配与图像一致。由于视频的帧率不是固定的,我们根据每帧实际时间动态调整时间 ID,以确保每个时间 ID 对应 40ms。

在多模态输入场景下,每种模态的位置信息初始化时会将前一模态的最大位置 ID 增加 1。

视频与音频时间交错方法

为了使模型能够同时接收视觉和听觉信息,我们采用 时间交错方法(time-interleaving) 对带有音频的视频进行特殊设计。具体做法是:

  • 将视频的音频表示按 实际时间2 秒 切分为块。
  • 在每个 2 秒块中,先安排视频的视觉表示,再安排音频表示,从而将视频和音频的表示交错排列。

生成:

文本生成由 Thinker 直接生成,其逻辑与广泛使用的大型语言模型(LLM)相同,文本通过基于词汇表的概率分布进行自回归采样生成。生成过程中可能会采用一些技术,如 重复惩罚(repetition penalty)top-p 采样以提高文本生成的多样性

语音生成中,Talker 接收 Thinker 生成的高维表示和采样的文本标记高维表示和离散采样标记的结合是这个过程中的关键。作为流式算法,语音生成需要在整个文本完全生成之前预测文本的语气和态度。Thinker 提供的高维表示隐含了这些信息,使得语音生成过程更自然。此外,Thinker 的表示主要体现语义相似性而非语音相似性,因此,即使是发音上差异较大的词,其高维表示可能非常相似,这就需要输入离散的采样标记来消除这种不确定性。

我们设计了一个高效的语音编解码器 qwen-tts-tokenizer,它能够高效地表示语音的关键信息,并通过因果音频解码器流式解码成语音。接收到信息后,Talker 开始自回归地生成音频标记和文本标记语音生成过程中不需要与文本进行逐字和逐时间戳的对齐,这大大简化了训练数据的要求和推理过程。

流式设计:

在音频和视频流式交互的背景下,初始包延迟是衡量系统流式性能的关键指标。这个延迟受到多个因素的影响:1)多模态信息输入处理引起的延迟;2)从接收到第一个文本输入到输出第一个语音标记之间的延迟;3)将第一个语音段转换为音频的延迟;4)架构本身的固有延迟,这与模型大小、计算 FLOP 数以及其他因素相关。本文将随后讨论在这四个维度上减少这些延迟的算法和架构改进。

支持预填充(Support Prefilling)

块状预填充(Chunked-prefills) 是现代推理框架中广泛使用的一种机制。为了支持模态交互中的预填充机制,我们修改了音频和视觉编码器,以支持沿时间维度的 块状注意力(block-wise attention)。具体而言,音频编码器从对整个音频的全局注意力改为对每个 2 秒 的音频块进行注意力计算。视觉编码器则使用 Flash Attention 来实现高效的训练和推理,并通过一个简单的 MLP 层 将相邻的 2×2 标记合并为一个标记。补丁大小设置为 14,允许不同分辨率的图像被打包成一个序列。

流式编解码器生成(Streaming Codec Generation)

为了促进音频的流式传输,特别是对于长序列的流式处理,我们提出了一种 滑动窗口块注意力机制(sliding window block attention),该机制限制了当前标记访问的上下文范围。具体来说,我们采用了 Flow-MatchingDiT 模型。输入的code通过 Flow-Matching 转换为 梅尔频谱图(mel-spectrogram),然后通过修改后的 BigVGAN 将生成的梅尔频谱图重建回波形。

DiT中用于编解码到波形生成的滑动窗口块注意力机制的示意图

预训练

Qwen2.5-Omni 由三个训练阶段组成。在第一阶段,我们锁定大型语言模型(LLM)的参数,专注于训练视觉编码器和音频编码器,利用大量的音频-文本和图像-文本对来增强 LLM 的语义理解能力。在第二阶段,我们解冻所有参数,并使用更广泛的多模态数据进行训练,以实现更全面的学习。在最后阶段,我们使用长度为 32k 的数据来提升模型理解复杂长序列数据的能力。

该模型在一个多样化的数据集上进行预训练,数据类型包括图像-文本、视频-文本、视频-音频、音频-文本和文本语料库。我们将层次标签替换为自然语言提示,遵循 Qwen2-Audio(Chu et al., 2024a)的方法,这可以提高模型的泛化能力和指令跟随能力。

在初始预训练阶段,Qwen2.5-Omni 的 LLM 组件使用 Qwen2.5(Yang et al., 2024b)中的参数初始化,视觉编码器与 Qwen2.5-VL 相同,音频编码器则使用 Whisper-large-v3(Radford et al., 2023)初始化。两个编码器分别在固定的 LLM 上进行训练,最初都专注于训练各自的适配器,然后再训练编码器。这个基础训练对装备模型具有坚实的视觉-文本和音频-文本关系和对齐的理解至关重要。

预训练的第二阶段标志着一个重要的进展,它增加了 8000 亿个图像和视频相关的数据标记,3000 亿个音频相关的数据标记,以及 1000 亿个视频带音频相关的数据标记。这一阶段引入了更多的混合多模态数据和更广泛的任务,增强了听觉、视觉和文本信息之间的互动,并加深了理解。加入多模态、多任务数据集对于培养模型同时处理多任务和多模态的能力至关重要,这是一项处理复杂现实世界数据集的关键能力。此外,纯文本数据在保持和提高语言能力方面也起着重要作用。

为了提高训练效率,我们在之前的阶段将最大标记长度限制为 8192 个标记。随后,我们引入了长音频和长视频数据,并将原始文本、音频、图像和视频数据扩展到 32,768 个标记进行训练。实验结果表明,我们的数据在支持长序列数据方面取得了显著的改进。

Post-training

数据格式:

Thinker

在后训练阶段,我们采用 ChatML 格式(OpenAI, 2022)进行指令跟随数据的微调。我们的数据集包括纯文本对话数据、视觉模态对话数据、音频模态对话数据以及混合模态对话数据。

Talker

我们为 Talker 引入了一个三阶段训练过程,使 Qwen2.5-Omni 能够同时生成文本和语音响应。在第一阶段,我们训练 Talker 学习上下文延续。在第二阶段,利用 DPO(Rafailov et al., 2023)增强语音生成的稳定性。在第三阶段,我们应用了多语者指令微调,以提高语音响应的自然性和可控性。

上下文学习(ICL) 训练阶段,除了像 Thinker 那样使用文本监督外,我们还通过下一标记预测执行语音延续任务,利用包含多模态上下文和语音响应的广泛对话数据集。Talker 学会了从语义表示到语音的单调映射,同时获得了根据上下文生成具有多样化属性(如韵律、情感和口音)的语音的能力。此外,我们还实施了音色解耦技术,以防止模型将特定的声音与不常见的文本模式关联。

为了扩大语者和场景的覆盖范围,预训练数据不可避免地包含标签噪声和发音错误,这可能导致模型产生幻觉。为了解决这个问题,我们引入了强化学习阶段来提高语音生成的稳定性。具体来说,对于每个请求和响应文本与参考语音配对的情况,我们构建了一个数据集 D,其中包含三元组数据 (x, yw, yl),其中 x 是输入序列的输入文本,ywyl 分别是良好和不良生成的语音序列。我们根据这些样本的奖励分数进行排名,奖励分数与 词错误率(WER)标点停顿错误率 相关。

最后,我们对上述基础模型进行了语者微调,使 Talker 能够采用特定的声音并提高其自然性。

Performance  

我们对 Qwen2.5-Omni 进行了全面评估,与类似大小的单模态模型和 Qwen2.5-VL-7B、Qwen2-Audio 和 Gemini-1.5-pro 等闭源模型相比,该模型在所有模态中均表现出色。在需要集成多种模态的任务(例如 OmniBench)中,Qwen2.5-Omni 实现了最佳性能。此外,在单模态任务中,它在语音识别(Common Voice)、翻译(CoVoST2)、音频理解(MMAU)、图像推理(MMMU、MMStar)、视频理解(MVBench)和语音生成(Seed-tts-eval 和主观自然度)等领域表现出色。

DeepSpeed Ulysses: 训练极长序列Transformer模型的系统优化

从生成性AI到科研模型,长序列训练正在变得非常重要。 在生成性AI领域,会话式AI、长文档摘要和视频生成等任务都需要在空间和时间层面对长上下文进行推理。 例如,多模态基础模型,如同时处理语音、图像和波形的模型,需要对具有极长序列的高维输入进行长上下文推理。 同样,章节和书籍级别的摘要(数万甚至数十万字)在会话式AI和摘要任务中也非常重要。

对于科学AI来说,长序列同样至关重要,它为更好地理解结构生物学、医疗保健、气候和天气预测以及大分子模拟打开了大门。 例如,通过在基因序列上训练大型语言模型,我们可以创建可以使用极长序列(人类基因组有64亿个碱基对)学习基因组进化模式的语言模型。在医疗保健领域,以所有的患者护理记录为条件的诊断预测模型需要极长序列的上下文。

尽管对于生成性AI和科学AI来说,长序列长度的重要性逐渐增长,但现有的大型模型训练系统和底层的并行技术(数据、张量、流水线、序列并行)并不能支持高效的长序列训练。现有并行方法存在两个主要挑战。首先,现有的数据、张量和流水线等并行方法无法解决序列维度的扩展问题。其次,由于内存通信效率低下,现有的序列并行方法不够高效。此外,现有方法的易用性不足,需要进行侵入性和复杂易出错的代码重构。

为了解决这些问题,我们很高兴宣布推出DeepSpeed-Ulysses(或称为Ulysses,一个非常长的小说),这是一种简单、易用且高效的方法,用于支持具有极长序列长度的高效可扩展LLM训练。

DeepSpeed-Ulysses将各个样本在序列维度上分割给参与的GPU。然后,在attention计算之前,它对已分割的查询(Q)、键(K)和值(V)执行all-to-all通信操作,以使每个GPU接收完整的序列,但仅用于注意力头的非重叠子集。这使得参与的GPU可以并行计算不同的注意力头。最后,DeepSpeed-Ulysses还使用另一个all-to-all来在注意力头上收集结果,同时重新在序列维度上进行分区。

DeepSpeed-Ulysses及其与此博客一起发布的实现的关键特性如下:

  • 与现有系统相比,序列长度增加了4倍,支持训练超过百万个token的序列。
  • 与现有系统相比,通信减少了超过10倍,导致吞吐量提高了高达2.5倍,并且每个GPU的持续吞吐量超过175 TFlops(超过硬件峰值的54%)。
  • 完全通用的attention:DeepSpeed序列并行支持密集和稀疏的注意力,并可与高效的注意力实现(如FlashAttention v2)一起工作。
  • 支持大规模模型训练:DeepSpeed序列并行不仅支持大序列长度,还可以与ZeRO-3并用支持大模型尺寸。
  • 易于使用和迁移,最小化对现有训练框架的代码更改要求。

在接下来的章节中,我们详细讨论DeepSpeed-Ulysses的核心设计、通信复杂度分析、实验评估以及与现有工作的比较,并展示其可用性和使用指南。

DeepSpeed-Ulysses的核心设计

图1显示了DeepSpeed-Ulysses的核心设计。与已知的Transformer架构一样,设计由N个输入序列在P个可用设备上分区组成。每个本地N/P分区都被投影到查询(Q)、键(K)和值(V)嵌入中。接下来,(QKV) 嵌入通过参与计算设备之间的高度优化的全对全集合(all-to-all collectives)进行全局的 QKV 收集。在全对全集合后,每个头的注意力计算形式为:

注意力计算后,另一个全对全集合将注意力计算的输出上下文张量转换为序列(N/P)并行,用于Transformer模型层的剩余模块中的后续操作(MLP MatMul、层归一化等)。

显著的通信量减少

DeepSpeed-Ulysses与其他现有的长序列方法的区别在于其更小的累积通信量以及随着序列并行度增加而更好的可扩展性,如下所示:

在具有节点内NVSwitch互连和节点间胖树IB拓扑的现代集群上,针对一个聚合消息大小为M的全对全传输,传输到P个GPU上的每个链接的通信量为M/P。 对于隐藏层大小为h、序列长度为N且并行度为P的Transformer模型,DeepSpeed序列并行会在注意计算之前对QKV投影执行聚合消息大小为3Nh的全对全操作,并在注意计算之后对输出上下文投影执行大小为Nh的另一个全对全操作。因此,DeepSpeed序列并行每个链接的聚合通信量为4Nh/P(或O(N/P)复杂度)。值得注意的是,当N和P成比例增加时,这个通信量是恒定的。

相比之下,现有的方法,如Megatron-LM,在N线性增长的情况下会导致通信量线性增加,而与P无关,从而导致O(N)的通信复杂度。例如,Megatron-LM对每个Transformer模型层都执行两个大小为Nhall-gather操作,以及两个大小为Nhreduce-scatter操作。然而,当P >> 1时,大小为M的每个all-gather和reduce-scatter的成本仍然是M,而不是M/P。因此,Megatron-LM序列并行会导致每个链接的通信量为4Nh,这比DeepSpeed序列并行大P倍。这使得DeepSpeed序列并行可以在实现显著更高的训练效率的同时支持极长序列训练。我们的实验评估结果与此理论分析相符。

DeepSpeed-Ulysses的其他亮点

通用的注意力解决方案

DeepSpeed分布式注意力模块的实现足够通用,以支持任何类型的注意力,例如自注意、交叉注意和因果注意,无论是它们的密集还是稀疏版本,以及支持局部注意层级上的长序列的各种优化内核,例如不同版本的FlashAttention。

DeepSpeed-Ulysses的通用性来自其核心设计的模块化性质:一个以注意力为中心的序列并行设计。在注意力计算之前,序列并行性是对N/P分区的,而注意力计算是对每个头的并行性,每个头的注意力全都保留,但头的数量较少,因此注意力计算可以用任何类型的注意力机制替代,例如密集注意力和各种形式的稀疏注意力。

通过ZeRO-3集成实现更大的模型和更长的序列训练

尽管DeepSpeed序列并行在使用更长的序列进行训练时减少了激活内存的使用,但并不影响模型状态的内存占用。因此,为了支持具有大序列长度的大语言模型训练,我们实现了DeepSpeed序列并行与ZeRO-3的集成。

ZeRO Redundancy Optimizer Stage 3 (ZeRO-3) 是一种用于训练大模型的内存优化技术。与传统的神经网络数据并行训练中,模型状态在数据并行等级上进行复制不同,ZeRO-3通过将模型状态在数据并行等级之间进行分区来优化内存使用。然而,使用序列并行时,训练数据可以在批(样本)和序列维度上考虑,相关的并行群组可以组合成一个更大的群组以实现ZeRO并行。

因此,我们将ZeRO-3分区扩展到数据并行和序列并行等级的组合。换句话说,在DeepSpeed序列并行中,ZeRO将模型状态分区在序列和数据并行组之间,并在需要时收集每个等级分区(allgather)。类似地,梯度将在数据并行和序列并行等级之间进行减少,用于参数更新。ZeRO可以在序列和数据维度上实现巨大的内存节省,并且不仅可以扩展到大序列长度,还可以扩展到大模型。

评估

我们在GPT(用于许多NLP任务的基础模型)上使用最多64个A100 GPU(40GB显存)对DeepSpeed-Ulysses进行了评估。我们的评估分为四个方面:i) 序列长度可扩展性,ii) 密集注意力的吞吐量以及与现有系统的比较,iii) 稀疏注意力的吞吐量以及与现有系统的比较,iv) DeepSpeed序列并行的收敛性研究。接下来,我们将对每个类别讨论和展示评估结果。

序列长度可扩展性

第一组实验是在12亿参数的GPT模型上将序列长度扩展到100万token。这个评估的结果如图2所示。DeepSpeed序列并行允许随着GPU数量的增加线性增加序列长度,并且序列长度与GPU数量保持线性比例关系,适当的GPU数量下保持相似的计算吞吐量。

密集注意力评估

接下来,我们在300亿参数的密集注意力模型上对DeepSpeed序列并行进行了评估,并与Megatron序列并行在64个A100 GPU上进行了对比。这些评估的结果如图3所示。

我们将DeepSpeed序列并行与Megatron-LM在不同序列长度下的性能进行了比较。对于我们的评估,我们选择了能使DeepSpeed序列并行和Megatron-LM分别达到最佳性能(通过吞吐量或TFLOPs衡量)的序列长度-批大小组合,我们称之为最佳(批大小-序列长度)配置。对于DeepSpeed序列并行,我们始终使用64的ZeRO并行度。

图3显示,DeepSpeed序列并行在相同序列长度下始终优于Megatron-LM。此外,DeepSpeed序列并行可以运行比Megatron-LM更长的序列。DeepSpeed序列并行的性能优势在于两个方面:(1)DeepSpeed序列并行结合ZeRO-3的内存优化,可以容纳更多的样本,从而提高吞吐量;(2)相对于Megatron-LM序列并行中应用的all-gather通信,DeepSpeed序列并行使用更高效的全对全通信。

图3:DeepSpeed和Megatron LM序列并行在300亿参数模型上的密集注意力评估。

稀疏注意力评估

类似地,我们在300亿参数的稀疏注意力模型上对DeepSpeed序列并行进行了评估,并与Megatron序列并行进行了对比。我们的评估结果如图4所示。稀疏注意力的实验结果与密集注意力实验类似。我们观察到DeepSpeed序列并行的吞吐量性能相对于Megatron-LM提高了2倍以上。通过节省内存,DeepSpeed序列并行结合ZeRO-3可以扩展到比Megatron-LM更长4倍的序列长度。

DeepSpeed序列并行在相同序列长度下始终优于Megatron-LM。事实上,当前的DeepSpeed吞吐量受到本地稀疏注意力实现的瓶颈,因此DeepSpeed吞吐量随着序列长度的增加而降低。我们预计,随着未来局部稀疏注意力实现性能的改善,DeepSpeed与Megatron之间的性能差距将在更大的序列长度下进一步增加。

图4:DeepSpeed和Megatron LM序列并行在300亿参数模型上的稀疏注意力评估。

收敛性研究

最后,图5显示了1.3亿参数GPT模型在32K序列长度下,使用序列并行度设置为4的情况下,在8个A100 GPU上的收敛性。对于DeepSpeed序列并行,我们使用不同的ZeRO阶段进行了收敛性评估。DeepSpeed序列并行是一种纯系统优化技术,用于实现长序列Transformer模型的训练,因此在训练模型质量上没有(负面)影响,并通过实验得到了验证,如图5所示。

图5:使用不同ZeRO内存优化阶段的DeepSpeed序列并行的收敛性评估。

DeepSpeed-Ulysses软件可用性

DeepSpeed-Ulysses只需进行少量简单代码更改来集成到您的代码中。下面是一个启用它的示例:

from deepspeed.sequence.layer import DistributedAttention

# 将原始的自注意(attn)替换为DeepSpeed-Ulysses的自注意

dist_attn = DistributedAttention(attn, get_sequence_parallel_group())

与其他支持序列并行的库(如Megatron-LM)相比,DeepSpeed-Ulysses不需要进行模型重构。 DeepSpeed-Ulysses已经完全与Megatron-DeepSpeed代码库集成并经过测试。这意味着如果您已经 在使用这个代码库来训练大型语言模型,您可以无缝地使用DeepSpeed-Ulysses训练具有极长序列的模型。

Paraformer-时间戳模型

参考论文:

  • FunASR: A Fundamental End-to-End Speech Recognition Toolkit   
  • 如图2(a)所示。Paraformer是一个单步非自回归(NAR)模型,结合了基于语言模型的快速采样模块,以增强NAR解码器捕捉标记之间依赖关系的能力。

    Paraformer由两个核心模块组成:预测器和采样器。预测器模块用于生成声学嵌入,捕捉输入语音信号中的信息。在训练过程中,采样器模块通过随机替换标记到声学嵌入中,结合目标嵌入生成语义嵌入。这种方法使得模型能够捕捉不同标记之间的相互依赖关系,并提高模型的整体性能。然而,在推理过程中,采样器模块处于非激活状态,声学嵌入仅通过单次传递输出最终预测结果。这种方法确保了更快的推理时间和更低的延迟。

    Timestamp Predictor:

    准确的时间戳预测是 ASR 系统的关键功能。然而,传统的工业 ASR 系统需要额外的混合模型来进行力对齐 (FA) 以实现时间戳预测 (TP),从而增加计算量和时间成本。FunASR 提供​​了一个端到端的 ASR 模型,通过重新设计 Paraformer 预测器的结构来实现准确的时间戳预测,如图2 (b) 所示。我们引入了一个转置卷积层和 LSTM 层来对编码器输出进行上采样,并通过后处理 CIF 权重 α2 来生成时间戳。我们将两个fireplaces 之间的帧视为前一个标记的持续时间,并根据α₂标出静音部分。此外,FunASR还发布了一个类似强制对齐的模型TP-Aligner,该模型包括一个较小的编码器和一个时间戳预测器。它接受语音和相应的转录作为输入,以生成时间戳。

    我们在AISHELL和60,000小时工业数据上进行了实验,以评估时间戳预测的质量。用于衡量时间戳质量的评估指标是累积平均偏移(AAS)。我们使用了一个包含5,549个手动标记时间戳的测试集,将提供的模型与使用Kaldi训练的FA系统进行时间戳预测性能比较。结果显示,Paraformer-TP在AISHELL上优于FA系统。在工业实验中,我们发现提出的时间戳预测方法在时间戳准确性方面与混合FA系统相当(差距小于10毫秒)。此外,这种单次解决方案对于商业使用非常有价值,因为它有助于减少计算和时间开销。

    DPO为什么会让大语言模型输出变长

    摘自:https://zhuanlan.zhihu.com/p/5830338806

    总的来说,DPO让模型输出变长主要可以分为以下几个原因:

    1. RM和模型评测的长度偏好。不管是Reward Model还是当前用与评测的模型(即便是GPT4)都会存在比较明显的长度偏好,即倾向于给更长的回答一个更高的分数。这一点已经有非常多工作给出过分析了。
    2. 训练数据本身长度分布不均衡。实战过程中往往就是用RM进行排序构造训练数据,RM的长度偏好就是会导致训练数据中容易出现chosen比rejected更长的情况。训练数据的长度差异(chosen比rejected长)就会导致训练后模型输出变长。
    3. 数据长度差异导致的reward被高估或低估。《Eliminating Biased Length Reliance of Direct Preference Optimization via Down-Sampled KL Divergence》中发现,DPO的算法本身也存在对response长度的依赖,chosen和rejected之间的长度差异可能会导致reward被高估/低估(overestimated or underestimated rewards)。即,当chosen过短时,reward会被低估,而当chosen过长时,reward会被高估
    4. DPO算法本身的长度敏感性。《Length Desensitization in Direct Preference Optimization》中提到,response长度会影响到似然概率的大小,并且进一步影响到训练优化方向:当chosen更长时,DPO会往chosen的方向进行优化(增大chosen概率),从而使输出变长;而rejected更长时,DPO会往远离rejected的方向优化(降低rejected概率),但却未必会让输出变短。

    如何解决:

    1. RM的优化:前面讲的都是对DPO进行长度控制的工作,但对RM本身的长度偏好进行优化的工作没有看到太多,如果大家有看到相关的也可以在评论区提供一下。如果将RM本身的长度偏好问题解决的话,那就可以极大程度上解决训练数据的长度分布均衡问题了。
    2. 数据的优化:有些工作会在数据构造时对长度进行综合考虑,如对RM打分进行长度归一后再排序、采样多个答案进行排序时根据均值方差限制chosen的长度等,通过这些方式可以减少长度差距过大的情况。如果数据本身的长度分布均衡了,也能一定程度上减缓这种问题。
    3. 训练算法上的优化:如果从LD-DPO的分析上看,即便数据分布比较均衡,只要存在长度差异,DPO本身的长度敏感性就是会导致模型输出变长,因此可能还是需要一些算法层面的优化,比如在DPO阶段加入SFTloss就是一种简单有效的方法,在很多公开的大模型技术报告中也都有用到该方法。另外R-DPO、SamPO和LD-DPO的长度控制效果都算是比较好的方法。

    DPO面临的一个问题(准确来讲是一种现象)就是会让大模型的输出变长,且多轮DPO的话会让模型输出越来越长。本篇文章我们将结合搜集到的一些相关工作,探讨一下业界对该现象的一些分析,探究这一现象产生的根本原因,以及如何有效地解决。

    首先我们需要思考一个问题,模型输出变长到底是不是一件坏事?一般来说,输出变长可能会使内容更加详细,信息量更丰富,回复质量更高,用户体验更好。但如果过度长,输出了很多冗余信息,回复质量没有明显改善,反而带来了推理成本的增加,回复变得啰嗦,用户体验反而变差了。

    因此,无论是从用户体验的角度还是多轮DPO能否run下去的角度,做好长度控制都是有必要的。

    相关工作

    先简要介绍一些相关工作,然后后面详细总结。

    1.《Disentangling Length from Quality in Direct Preference Optimization》(简称R-DPO)

    在这之前的一些RL的工作也有分析过长度爆炸问题,但该文章可能是第一个提出DPO的长度爆炸问题的。

    文章中发现,无论是RL训练中使用的Reward Model还是用来评测模型效果的打分模型(如GPT-4)都表现出明显的长度偏好,即会给更长的答案一个更高的分数(如下图)。且在一些公开的DPO训练数据集中,chosen的长度往往会比rejected更长,而这可能就是DPO后的模型输出长度明显比SFT模型更长的原因

    为了解决这个问题,该文章提出了一种长度正则化的策略,即在计算loss的时候添加一个长度正则项,从而避免模型对长度的过度拟合,公式如下:

    其中 |yw| 表示chosen的长度, |yl| 表示rejected的长度,从公式中可以看出,当chosen与rejected的长度差距越大,正则项的值越大,从而实现对长度的“惩罚”效果。

    从文章中的实验结果可以看出,该方法确实可以在尽可能减少性能损失的前提下有效解决长度增长问题。(有时还是会损失一定的性能。)

    2.《SimPO: Simple Preference Optimization with a Reference-Free Reward》(简称SimPO)

    陈丹琦团队的工作,直接去掉了reference model,用长度归一的方式实现长度控制。其loss如下:

    文章中提到了很多输出长度相关的内容,但核心贡献并不是做长度控制,而是用一种更简单高效的方法实现偏好训练。从公式上看,和原始DPOloss相比主要有两处不同,一个是分母从reference model的logp替换成了长度,另外就是增加了一个 γ ,类似一个offset的作用。不过其中对chosen和rejected的reward做长度归一的部分,直觉上看起来应该是能起到一定的长度控制效果的。

    不过从论文中的实验结果看,该方法的效果还是比较好的(当时声称训出最强8B大模型),但与标准DPO相比似乎并没有实现长度控制的效果。

    3.《Eliminating Biased Length Reliance of Direct Preference Optimization via Down-Sampled KL Divergence》(简称SamPO

    这篇论文对DPO后长度变长的问题进行了一定的分析,提出的一个核心观点是:DPO的算法本身也存在对response长度的依赖,chosen和rejected之间的长度差异可能会导致reward被高估/低估(overestimated or underestimated rewards)。即,当chosen过短时,reward会被低估,而当chosen过长时,reward会被高估。

    这篇工作中提出的一种方式就是在token级别下采样的概率特征,以计算正则化的KL散度,从而减少因pair长度不同而导致的奖励偏差。其loss的计算如下:

    从公式可以看出,该方法的核心就是在计算reward的时候不再是全部token的条件概率的累乘(取log后就是累加),而是随机采样公共数量的token进行累乘。这样即便chosen和rejected长度不同,参与reward计算的token数是一样的。也就是说,在SamPO训练过程中,魔都看到的chosen和rejected相当于是完全等长的。

    从文章中的实验结果看,该方法确实能有效控制模型输出长度的增长,甚至在多轮DPO依然能有效控制长度。但是在性能上看依然做不到碾压标准DPO的效果。

    但该方法有两个风险便是:

    1. 本身DPO就存在一定的波动,随机下采样可能会导致训练的稳定性不强;
    2. 随机采样必然会导致一些信息缺失,如果采样时舍弃掉了一些非常重要的token可能会影响到训练效果。

    4.《Length Desensitization in Direct Preference Optimization》(简称LD-DPO)

    该论文可能是第一个从理论层面分析DPO后模型输出变长的原因的,其核心分析主要包括两方面:

    1. DPO的梯度优化方向和chosen/rejected的似然概率成反比。
    2. Response长度对似然概率的影响极大,因此长度会直接影响reward的计算,并影响到DPO的优化方向。

    上图是一个对训练数据的统计热力图,图中,横坐标为chosen的长度,纵坐标为rejected的长度,颜色深度表示 log⁡πθ(yl|x)−log⁡πθ(yw|x) 值的大小。第一张图(a)是标准DPO,可以看出长度差距越大时,颜色越深,也就说明长度差距可能会导致reward计算产生bias,且长度差距越大这种bias越大。而这种bias会进一步影响到DPO的优化方向,使其往输出更长的方向进行优化。

    该文章提出的解决方案是在计算似然概率时对长度进行解耦,将更长的答案拆成“公共长度部分”和“额外部分”,并进一步将后者拆分为真实偏好和冗余偏好,并对其中的冗余部分进行降权操作,通过一系列推导后将 πθ(y|x) 转化为如下的形式(可近似理解为完整似然概率部分与公共长度部分似然概率的加权和):

    从公式上看,这种方式可以让长度更长的那个response(不管是chosen还是rejected)实现一定的缩放(),减少长度带来的似然概率的断崖式下滑,使其与另一个短response(不受影响)之间更具可比性,同时又不会像SamPO那样完全舍弃掉额外部分的信息。

    从论文中的实验结果看,这种方法能够实现比较好的长度控制,且模型性能还能有一定提升,并且可以通过调整参数 α 可以实现不同程度的控制效果。另外文章还提出一个比较有意思的发现,就是过度冗余的回答可能反而会损害模型的推理能力,他们通过这种方法控制长度后,模型的推理能力也有明显提升。

    其他工作

    除此之外,还有一些工作直接在数据上做文章,通过控制chosen和rejected的长度差距来实现长度控制,如《Following Length Constraints in Instructions》(简称LIFT-DPO)。以及在一些开源模型的技术报告中我们也能看到一些相关的长度控制方法,如在利用RM打分排序时就综合考虑长度问题等,这些数据工作就不再详细展开了。

    如何实现有效的长度控制?

    1. RM的优化:前面讲的都是对DPO进行长度控制的工作,但对RM本身的长度偏好进行优化的工作没有看到太多,如果大家有看到相关的也可以在评论区提供一下。如果将RM本身的长度偏好问题解决的话,那就可以极大程度上解决训练数据的长度分布均衡问题了。
    2. 数据的优化:有些工作会在数据构造时对长度进行综合考虑,如对RM打分进行长度归一后再排序、采样多个答案进行排序时根据均值方差限制chosen的长度等,通过这些方式可以减少长度差距过大的情况。如果数据本身的长度分布均衡了,也能一定程度上减缓这种问题。
    3. 训练算法上的优化:如果从LD-DPO的分析上看,即便数据分布比较均衡,只要存在长度差异,DPO本身的长度敏感性就是会导致模型输出变长,因此可能还是需要一些算法层面的优化,比如在DPO阶段加入SFTloss就是一种简单有效的方法,在很多公开的大模型技术报告中也都有用到该方法。另外R-DPO、SamPO和LD-DPO的长度控制效果都算是比较好的方法。

    最后结合我自己的一些尝试来直接对比一下上面的四种方法:

    1. R-DPO是通过加正则项的方式实现长度控制,说是正则项,但其实只是一个常数,其原理相当于是对每条数据加上一个权重(文章中也提到了这点),即当chosen和rejected长度差距大时降低该数据的权重。也就是说,该方法其实是让模型减少对长度差距大的数据的学习权重。这种方法确实可以实现一定的长度控制效果,但必然会减少一些数据的利用率,这可能也是训练效果会有一定损失的原因。我自己尝试了一下该方案,实验下来确实可以做到长度控制效果,但大部分情况下性能都会比标准DPO差一些。
    2. SimPO是用长度归一来替换Reference Model的KL约束,理论上和长度控制其实没有太大关系,更多的是简化训练和提升性能。实验结果确实也体现了并不会比标准DPO更短。(该方法热度很高,但网络上褒贬不一,很多人表示无法复现结果。)根据我自己实验经验来看,跑出好的结果需要仔细调参,论文推荐的超参不一定适合所有情况。
    3. SamPO是直接用下采样的方式,强行将模型视角下的长答案变得和短答案一样长,该方法给人的直观感受就是长度控制效果肯定很好,但是很可能会有性能损失。但我自己实验下来,长度控制效果和R-DPO差不多,但是性能也比较不稳定,更换随机种子就会导致性能产生波动。我也尝试过将随机下采样改为top-k采样,即保留概率最大的top-k个token,但效果并不会比随机更好(这么直觉的方法可能论文作者也尝试过了)。
    4. LD-DPO的方法是只对答案过长的部分做了解耦和降权处理,通过降低过长部分的权重来实现整个条件概率的缩放,看起来是四种方法中实现最优雅的一种,既降低了长度差异带来的reward bias问题,又不会丢弃信息,相当于是用极小的代价实现了概率缩放目的。从论文中贴出的结果看,确实也是性能最强的一个,长度控制效果也是最好的。但论文代码没有开源,所以没有实验验证。但从公式上看复现难度应该不是很大,有能力的可以尝试复现一下看看效果。

    transformers 的 generate() 方法实现多样化文本生成:参数含义和算法原理解读

    这个类对外提供的方法是 generate(),通过调参能完成以下事情:

    • greedy decoding:当 num_beams=1 而且 do_sample=False 时,调用 greedy_search()方法,每个step生成条件概率最高的词,因此生成单条文本。
    • multinomial sampling:当 num_beams=1 且 do_sample=True 时,调用 sample() 方法,对词表做一个采样,而不是选条件概率最高的词,增加多样性。
    • beam-search decoding:当 num_beams>1 且 do_sample=False 时,调用 beam_search() 方法,做一个 num_beams 的柱搜索,每次都是贪婪选择top N个柱。
    • beam-search multinomial sampling:当 num_beams>1 且 do_sample=True 时,调用 beam_sample() 方法,相当于每次不再是贪婪选择top N个柱,而是加了一些采样。
    • diverse beam-search decoding:当 num_beams>1 且 num_beam_groups>1 时,调用 group_beam_search() 方法。
    • constrained beam-search decoding:当 constraints!=None 或者 force_words_ids!=None,实现可控文本生成。

    参数列表

    核心代码详见:generate()入口函数定义, GenerationConfig类

    1.控制生成长度的参数

    参数类型缺省值含义
    max_lengthint20表示 prompt + max_new_tokens 累加的最大长度,如果max_new_tokens也设置了,会覆盖这个参数
    max_new_tokensint生成部分的tokens的最大长度 (忽略prompt部分的长度)
    min_length0表示 prompt + min_new_tokens 累加的最小长度,如果min_new_tokens也设置了,会覆盖这个参数
    min_new_tokensint生成部分的tokens的最小长度 (忽略prompt部分的长度)
    early_stoppingbool, strFalse对于beam search方法的控制终止的配置。
    False: 当有’num_beams’个候选生成,则终止
    True: 应用一些启发式规则判断不能找到更好的生成候选,来提前终止生成
    “never”: 当判断没有更好的可生成的candidate, beam search 过程终止
    max_timefloat执行生成的最大时间(s秒数)
    stop_stringsstr, array[str]配置模型生成的终止字符串,当模型生成参数配置的字符串,则终止生成。

    2. 控制生成策略的参数

    参数类型缺省值含义
    do_sampleboolFalseTrue: 生成过程使用采样逻辑
    False: 使用greedy做生成
    num_beamsint1设置beam search 束的数量。如果是1不做beam search 搜索
    num_beam_groupsint1为了保证生成的多样性,将num_beams 设置成多组。参考文献: https://arxiv.org/pdf/1610.02424.pdf
    penalty_alphafloatcontrastive search decoding的配置项,用于平衡生成置信度和衰减的惩罚
    dola_layersstr, List[int]str :
    “None”: 不使用dola
    “low” : 较低的一半layers, 最多20层使用dola
    “high”: 较高的一半layers, 最多20层使用dola
    List[int] : 通过指定一个index数组,指定dola 层
    “low”: 提升长答案的task,
    “high”:提升短答案的task

    3.cache配置参数

    参数类型缺省值含义
    use_cacheboolTrue是否使用KV cache 加速推理速度
    cache_implementationstr指定cache实现的name,在调用generate()时,实例化cache。
    ”static”: [StaticCache]
    “offloaded_static”: [OffloadedStaticCache]
    ”sliding_window”: [SlidingWindowCache]
    “hybrid”: [HybridCache]
    “mamba”: [MambaCache]
    ”quantized”:[QuantizedCache]
    cache_configCacheConfig , dictNonecache类使用的参数
    return_legacy_cacheboolTrue当DynamicCache 被使用时,是否返回历史的和新格式的cache

    4.操作模型输出logit的配置参数

    参数类型缺省值含义
    temperaturefloat1.0这个值用于建模下一个token的概率, 这个值被设置在generation_config.json文件中
    top_kint50筛选最高概率的top k个词, 这个值被设置在generation_config.json文件中
    top_pfloat1.0当设置<1时,筛选概率最高的token,累加概率不超过top_p的token
    min_pfloat配置筛选概率最低的一批token, 累加概率不超过min_p,裁剪掉,该配置相当于top_p的反向操作
    typical_pfloat1.0测量两个分布的相似性: 预测下一个目标token的概率 and 预测下一个随机Token的条件概率期望。如果设置<1,则筛选最典型的token。
    epsilon_cutofffloat0.0按设置的值,卡掉低概率值的token,一般设置为:3e-4 to 9e-4
    eta_cutofffloat0.0混合局部典型性采样和epsilon采样方法
    diversity_penaltyfloat0.0只对group beam search方法生效,如果在某个特定时间生成的token与任何beam 组生成的token一致,则beam的score减去这个值
    repetition_penaltyfloat1.01.0 默认不惩罚
    encoder_repetition_penaltyfloat1.0对于不在原始输入的token,指数级的惩罚
    length_penaltyfloat1.0对于beam 类的生成方法的长度惩罚,由于序列score是 log likelihood , > 0 倾向于更长的 <0 倾向于更短的
    no_repeat_ngram_sizeint0如果大于0, 则对应的size的ngram只能出现1次
    bad_words_idsList[List[int]]列出不允许生成的tokens_id
    force_words_idsList[List[int]] or List[List[List[int]]]必须被生成的words_ids。 如果配置List[List[List[int]]] 设置对于每个token的约束
    renormalize_logitsboolFalse对于所有的logits做后处理后,是否要再做下normalize
    constraintsList[Constraint]通过定义一个List[Constraint] 对象数组,来确保输出是在某些限制的场景下。一般用于安全的场景
    forced_bos_token_idintmodel.config.forced_bos_token_id强制跟在decoder_start_token_id之后的第一个token,对多语言模型是有用的
    forced_eos_token_idint or List[int]model.config.forced_eos_token_id当生成的token达到max_length上限时,最后一位输出的token
    remove_invalid_valuesboolmodel.config.remove_invalid_values是否移出可能生成的nan and inf 值,配置这个会减慢生成速度
    exponential_decay_length_penaltytuple(int, float)指数级增加长度的惩罚,tuple(start_index, decay_factor) start index 指示惩罚的开始i,decay_factor 指数衰减的惩罚因子
    suppress_tokensList[int]通过设置禁止的token的logit为-inf,来禁止token被sample
    begin_suppress_tokensList[int]通过设置首位禁止的token的logit为-inf,来禁止首位这部分token被采样到,进而导致被生成
    forced_decoder_idsList[List[int]]一个整数pair的数组,格式[生成index, token_index]指示固定位置强制生成某个token,例如[[1, 123]] 第二个位置总是生成token 123
    sequence_biasDict[Tuple[int], float]token list -> bias的映射,正的bias提升几率,负的bias降低几率
    token_healingboolFalse对prompt尾部的token做相似替换,以提升生成质量
    guidance_scalefloat是一个缩放因子,当>1时,这个因子越高,越鼓励模型生成与prompt接近的samples 。
    watermarking_configBaseWatermarkingConfig or dict对输出结果增加水印

    5.输出结果配置参数

    参数类型缺省值含义
    num_return_sequencesint1对于batch中的每个元素,设置独立计算的返回的sequence的数量
    output_attentionsboolFalse是否返回所有的attention的向量
    output_hidden_statesboolFalse是否返回所有网络层的隐层状态
    output_scoresboolFalse是否返回prediction scores
    output_logitsbool是否返回未处理过的的logit score
    return_dict_in_generateboolFalse除了返回生成序列,是否还返回a [`~utils.ModelOutput`]

    6.生成时使用的特殊token的配置参数

    参数类型缺省值含义
    pad_token_idintpadding token ID
    bos_token_idintbeginning -of – sequence token ID
    eos_token_idUnion[int, List[int]]end-of-sequence token ID

    6.辅助生成的配置参数(投机采样)

    参数类型缺省值含义
    is_assistantboolFalse指定是否模型是一个assistant(draft) model
    num_assistant_tokensint20投机采样过程,每次迭代 assistant model 要输出多少个token,给到目标模型做check。配置更高的值,如果assistant model 效果好 能带来更好的加速比
    num_assistant_tokens_schedulestrconstant“heuristic” : 当所有投机采样的token都正确时,将num_assistant_tokens增加2,否则减少1。
    “constant”: num_assistant_tokens 保持固定不变
    “heuristic_transient”: 类似于启发式方法,每次生成调用,都置成初始化的num_assistant_tokens值
    assistant_confidence_thresholdfloat0.4当assistant model预估当前token的置信度 小于 阈值时,提前终止assistant model的生成
    prompt_lookup_num_tokensint作为候选token 要输出的token的数量
    max_matching_ngram_sizeint2match prompt的最大ngram的数量
    assistant_early_exitint
    assistant_lookbehindint10如果设置为正整数,则重新编码过程将额外考虑最后的assistant_lookbehind个辅助标记,以正确对齐标记。此设置仅可在推测解码中使用不同的分词器时使用。
    target_lookbehindint10如果设置为正整数,则重新编码过程将额外考虑最后的target_lookbehind个辅助标记,以正确对齐标记。此设置仅可在推测解码中使用不同的分词器时使用。


    如有整理错误,欢迎指正~

    语音理解模型—OSUM

    OSUM: Advancing Open Speech Understanding Models with Limited Resources in Academia

    大型语言模型(LLMs)在各种下游任务中取得了显著进展,启发了业界对语音理解语言模型(speech understanding language models, SULMs)的研发,以期实现基于语音情感、性别等副语言的高表现力交互。然而,大多数先进的SULM是由行业头部公司开发的,消耗大规模的数据和计算资源。而这些资源在学术界并不容易获得。此外,虽然训练好的模型和推理代码被开源了,但训练框架和数据处理流程依然缺乏透明度,这也为进一步研究产生了障碍。在本研究中,我们提出了OSUM,一个开放的语音理解模型,旨在探索在有限的学术资源下训练SLUM的潜力。OSUM模型将Whisper编码器与Qwen2 LLM相结合,支持广泛的语音任务,包括语音识别(ASR)、带时间戳的语音识别(SRWT)、语音事件检测(VED)、语音情感识别(SER)、说话风格识别(SSR)、说话者性别分类(SGC)、说话者年龄预测(SAP)和语音转文本聊天(STTC)。通过采用ASR+X训练策略,OSUM通过同时优化模态对齐和目标任务,实现了高效稳定的多任务训练。除了提供强大的性能,OSUM还强调透明度,提供公开可用的代码,并详细介绍了数据处理流程,以期为学术界提供有价值的参考,旨在加速先进SULM技术的研究和创新。

    方案设计 

    OSUM模型将Whisper编码器与Qwen2 LLM相结合,支持广泛的语音任务,包括语音识别(ASR)、带时间戳的语音识别(SRWT)、语音事件检测(VED)、语音情感识别(SER)、说话风格识别(SSR)、说话者性别分类(SGC)、说话者年龄预测(SAP)和语音转文本聊天(STTC)。通过采用ASR+X训练策略,OSUM通过同时优化模态对齐和目标任务,实现了高效稳定的多任务训练。

    模型结构

    模型的输入包括语音和自然语言提示。不同于 Whisper 和Qwen-Audio 依靠指令标签,Osum采用描述性文本,将所有八个支持任务转换为图2所示。当前,我们的模型仅支持基于文本的响应,但是音频输出功能正在积极开发。

    如图2所示,OSUM模型由一个Speech Encoder、一个Adaptor和一个LLM组成。在训练过程中,Speech Encoder和Adaptor中的所有参数都会更新,而大语言模型则使用LoRA方法进行微调。各部分具体配置如下:

    • Speech Encoder: Whisper-Medium (769M);
    • Adaptor: Conv1D * 3 + Transformer * 4,4倍下采样;
    • LLM: Qwen2-7B-Instruct带LoRA。LoRA hyperparameters-α, rank, and dropout ratio are set to 32, 8, and 0.1,

    多任务监督训练

    训练过程包括两个阶段:

    首先,在没有LLM的情况下,对原始的Whisper模型进行多任务监督微调,多任务数据微调了 Whisper ,以确保OSUM模型的更快收敛。此外,此阶段使我们能够验证多任务数据的可靠性。具体来说,我们扩展了Whisper的指示标签,以适应更多的任务,每个前向推理仅执行一个任务。

    其次,将微调后的Whisper编码器与Qwen2大语言模型相结合,构建出完整的OSUM系统,然后使用更大的数据集进行进一步的监督训练。

    OSUM模型的输入包括一段语音和一个自然语言描述的prompt,而输出在现阶段仅支持文本回复,音频输出功能正在开发中。为节省计算资源,OSUM的多任务训练引入了一种“ASR+X”范式,即同时训练ASR任务和一个附加任务X。这在加速训练的同时,允许执行X任务时参考文本和声学两种特征,从而提升性能和训练稳定性。“ASR+X”范式是在LLM的自回归框架内通过调整预测标签来实现的,无需对模型架构或损失函数进行修改。执行不同的X任务是通过给LLM不同的自然语言prompt来实现的,每个任务有5个候选prompt,训练时随机选择一个。prompt的示例如表1所示。

    训练数据

    OSUM旨在使用多样化的语音数据集进行多任务训练,目标是构建一个能够在对话场景中全面理解输入语音的统一模型。多任务训练过程使各个任务能够从共享学习中获益,从而提升模型的整体性能。有关用于训练的数据集的详细信息见表2所示,本版本模型的训练数据规模大约为5万小时。

    技术性能

    总览

    如图2所示,OSUM 模型和Qwen2-Audio 相比,在大多数任务中,尽管 OSUM 使用的计算资源和训练数据明显更少,但它的表现优于Qwen2-Audio。

    图2 OSUM与Qwen2-Audio各项任务性能对比的雷达图。雷达图中每个模型各项任务的值是基于公开测试集和内部测试集的平均结果得出的

    各项指标与性能演示

    ASR(语音识别):如表4所示,OSUM在中文ASR上表现优越,具体地,在WenetSpeech test meeting、3个AISHELL-2子测试集以及4个内部使用的SpeechIO测试集上优于其他模型。OSUM在英语测试集上性能也可与SenseVoice-S相媲美。值得注意的是,这些结果是在使用少得多的训练数据的情况下取得的。此外,我们发现,即使在训练过程中未纳入中英混语料数据集,OSUM在识别中英混语音方面也展现出了令人惊讶的出色能力。

    表4公开测试集和内部测试集上ASR任务的评估结果。加粗字体表示同一测试集中的最佳结果。所有内部测试结果均由我们自行推理得出

    表45公开测试集和内部测试集上多任务的评估结果。每个测试集的最佳结果都用粗体突出显示。蓝色字体显示的结果以及内部测试集的结果,均是我们使用原始发布的模型自行推理得出的

    SRWT(带时间戳的语音识别):如表5所示,OSUM模型在SRWT任务上的性能显著优于Whisper-Large-v3,相对优势达到了36.70%,并且也超过了Qwen-Audio。此外,OSUM的表现甚至略微超过了GMM-HMM模型,而后者在时间戳预测任务被广泛使用。另外,此功能不仅使得OSUM能够以端到端的方式预测时间戳,更重要的是,它引导OSUM模型理解了“时间”这一概念。在将来,我们将会利用这一能力继续开发更灵活的应用,例如判断音频中何时出现了语音事件,何时出现了说话人转换等。

    VED(语音事件检测):我们首先在公开测试集ESC-50和VocalSound上评估OSUM的性能。ESC-50包含大量的非人声音频事件,我们将它们归类为“其他”。表45示的实验结果表明,OSUM可以成功地将这些非人声音频事件归类为“其他”。此外,在VocalSound数据集上的结果显示,OSUM与Qwen2-audio相比虽然存在一定差距,但也取得了超过80%的准确率。值得注意的是,为更加符合真实使用场景,我们的训练数据是语音和音频事件拼接而成,但公开测试集只有孤立的音频事件而没有说话语音。即便存在这一不匹配的情况,OSUM模型的在公开测试集上的结果也证明了其有效性和泛化性。与公开测试集不同,我们人工录制了同时包含语音和声学事件的内部测试集。表45结果表明,PANNs由于其仅为孤立音频事件检测而设计,在我们内部测试集中基本处于不可用状态。Qwen2-audio的表现相对较好,但也出现了性能下降。相比之下,OSUM模型在公开测试集和内部测试集上都取得了较为均衡的结果,展现出了更强的泛化能力。

    SER(语音情感识别):如表45示,对于SER任务,使用公开数据集的实验中,OSUM在MER2023测试集上展现出了卓越的性能,超过了一些近期的公开基准模型。在MELD数据集上,OSUM的性能略低于SenseVoice-L模型,这很可能是因为后者在更大规模的语音情感数据集上进行了训练。此外,OSUM在内部测试集上的结果与EmoBox模型相当,显著优于其他对比方法。但是,我们也观察到,厌恶和恐惧这两种情感尤其难以识别,其归因于这两种情感的训练数据更加稀缺,也容易和其他情感混淆。

    SSR(说话风格识别):表5中实验表明,OSUM所采用的声学-文本双模态风格分类方法的表现显著优于GLM-4-9B-Chat所采用的单文本模态方法,这充分证明了“ASR+X”策略的价值。现阶段OSUM能够区分八种风格:“新闻科普”,“恐怖故事”,“童话故事”,“客服”,“诗歌散文”,“有声书”,“日常口语”以及“其他”。我们详细分析了测试集上各类别的准确率,发现OSUM在对“新闻科普”、“有声书”、“童话故事”以及“客服”风格类别上表现出色;然而,在“诗歌散文”、“恐怖故事”类别上仍有提升空间。有趣的是,我们发现从实际测试的主观体验上来说,OSUM风格分类正确率是超过测试集的,总体来说可以让人满意。

    SGC(说话者性别分类):在SGC公开测试集上的结果表明,OSUM在AISHELL-1测试集上达到了100%的准确率。这一结果在一定程度上表明该任务上存在说话人过拟合现象。此外,在Kaggle测试集上,我们的方法略优于Qwen2-Audio。但在我们的内部测试集上,OSUM的性能略低于Qwen2-Audio,但依然超过了95%。总之,OSUM在SGC任务上展现出了不错的性能,而且实测效果很少出现性别判断错误的情况。

    SAP(说话者年龄预测):在SAP任务上,由于我们发现青少年和成年人的声学相似度非常高,这使得有效区分他们变得很复杂。因此,我们将年龄分为三类:儿童、成年人和老年人。尽管我们努力调试了prompt,但Qwen2-Audio在Kaggle测试集和我们的内部测试集上,年龄分类准确率都较低。这可能是因为这些模型对年龄的分类过于细致,从而影响了Qwen2-Audio模型的最终效果。表4中结果显示,OSUM在Kaggle测试集上显著优于Qwen2-Audio,达到了76.52%的准确率。在我们的内部测试集上OSUM分类准确率虽然略有下降,但仍然超过了Qwen2-Audio。这表明OSUM在不同的数据上表现出了很强的泛化能力。

    STTC(语音转文本聊天):如表5所示,在STTC任务中,我们在所有测试集上都遵循了AirBench的评估协议。这包括提供音频查询的文本以及两个不同答案的文本,让基于文本的大语言模型(LLM)给出1到10的主观评分。这两个答案一个是真实回复,另一个是语音大语言模型(SULM)生成的答案。测试结果表明,在AirBench的官方speech子测试集上,OSUM的得分虽然低于Qwen2-Audio,但也处于一个合理范围。这主要是因为我们没有使用英语对话数据进行训练,目前的得分完全依赖于大语言模型自身的表现。反之,在我们内部的中文对话测试集上,OSUM的表现优于Qwen2-Audio,这充分证明了OSUM在中文对话任务上性能是不错的。总体而言,我们的OSUM模型在对话能力方面与Qwen2-Audio相当。

    更多功能

    OSUM理解大模型在将来会提供更多的功能,可作为通用语音打标工具使用。此外,我们正在开发的功能包括:

    1. 同时支持ASR+X和单X任务模式,在执行单X任务打标时推理速度更快。
    2. 同时输出ASR+X1+X2+..Xn的多任务打标模式,一次性提供几乎全部所需标签。
    3. 增加更多的理解任务。

    Step-Audio:产品级开源实时语音对话模型

    阶跃星辰:Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤),方言(如 粤语,四川话),可控制语速及韵律风格,支持RAP和哼唱等。其核心技术突破体现在以下四大技术亮点:

    • 1300亿多模态模型: 单模型能实现理解生成一体化完成语音识别、语义理解、对话、语音克隆、语音生成等功能,开源千亿参数多模态模型 Step-Audio-Chat
    • 高效数据生成链路: 基于130B 突破传统 TTS 对人工采集数据的依赖,生成高质量的合成音频数据,并同步开源首个基于大规模合成数据训练,支持 RAP 和哼唱的指令加强版语音合成模型 Step-Audio-TTS-3B ,该模型具有增强的指令遵循功能以控制语音综合的能力。
    • 精细语音控制: 支持多种情绪(如生气,高兴,悲伤)、方言(包括粤语、四川话等)和唱歌(包括 RAP、干声哼唱)的精准调控,满足用户对多样化语音生成的需求。
    • 扩展工具调用: 通过 ToolCall 机制和角色扮演增强,进一步提升其在 Agents 和复杂任务中的表现。
    端到端语音相互作用的人类评估。

    模型组成

    图2 采用了AQTA(音频输入,文本输出) + TTS框架 进行实时语音对话

    Step-Audio的体系结构。 Step-Adio主要由三个组成部分组成:语音令牌,LLM和语音解码器。语音令牌器负责将输入语音离散到令牌中。LLM模型接收文本和语音令牌,输出文本,而语音解码器生成波形输出。

    传统的语音对话系统通常采用包括ASR的级联建筑,LLM和TTS模块。但是,我们提出的模型在训练阶段进行了全面的多模式培训以及对文本和音频的一致性,已经具有端到端的语音对话功能。尽管对替代设计进行了广泛的探索,但我们最终采用了AQTA(音频输入,文本输出) + TTS框架 进行实时语音对话,如图2所示,这是由以下考虑的驱动的:

    • 高质量的纯净对话数据的稀缺性:纯净对话数据的可用性有限,再加上其受限的场景,限制了端到端语音对话模型的训练效率。
    • 输出语音的可控性和自定义:通过引入TTS模块,我们可以灵活地控制语音参数,例如音色和音调,以满足用户的个性化需求,同时不断增强模型的表现力能力。

    在Step-Audio系统中,音频流采用Linguistic tokenizer【语义】(码率16.7Hz,码本大小1024)与Semantice tokenizer【声学】(码率25Hz,码本大小4096)并行的双码本编码器方案,双码本在排列上使用了2:3时序交错策略。通过音频语境化持续预训练和任务定向微调强化了130B参数量的基础模型(Step-1),最终构建了强大的跨模态语音理解能力。为了实现实时音频生成,系统采用了混合语音解码器,结合流匹配(flow matching)与神经声码技术。此外,采用语音活动检测(VAD)模块提取声段。

    Tokenizer

    我们通过token级交错方法实现Linguistic token与Semantic token的有效整合。Linguistic tokenizer的码本大小是1024,码率16.7Hz;而Semantic tokenizer则使用4096的大容量码本来捕捉更精细的声学细节,码率25Hz。鉴于两者的码率差异,我们建立了2:3的时间对齐比例——每两个Linguistic token对应三个Linguistic token形成时序配对

    语言模型

    为了提升Step-Audio有效处理语音信息的能力,并实现精准的语音-文本对齐,我们在Step-1(一个拥有1300亿参数的基于文本的大型语言模型LLM)的基础上进行了音频持续预训练。

    在多轮对话系统中音频令牌和文本令牌之间的长度差异需要有效的处理策略。为了解决这个问题,历史信息最初是在系统输入之前使用ASR模型转录为文本格式的,从而优化了计算效率。但是,应注意的是,模型体系结构在需要时保持处理和使用音频令牌作为历史上下文的能力。

    语音解码器

    Step-Audio语音解码器主要是将包含语义和声学信息的离散标记信息转换成连续的语音信号。该解码器架构结合了一个30亿参数的语言模型、流匹配模型(flow matching model)和梅尔频谱到波形的声码器(mel-to-wave vocoder)。为优化合成语音的清晰度(intelligibility)和自然度(naturalness),语音解码器采用双码交错训练方法(dual-code interleaving),确保生成过程中语义与声学特征的无缝融合

    实时推理管线

    为了实现实时的语音交互,我们对推理管线进行了一系列优化。其中最核心的是控制模块(Controller),该模块负责管理状态转换、协调响应生成,并确保关键子系统间的无缝协同。这些子系统包括:

    • 语音活动检测(VAD):实时检测用户语音起止
    • 流式音频分词器(Streaming Audio Tokenizer):实时音频流处理。输入音频流是通过两个平行的令牌管道处理的,每个管道都采用固定持续分段。将所得令牌无缝合并为2:3交织比的单个序列。没有流音频令牌,根据音频输入的长度,推理时间将明显较慢。
    • Step-Audio语言模型与语音解码器:多模态回复生成
    • 上下文管理器(Context Manager):动态维护对话历史与状态。我们的系统利用文本转录而不是原始的音频令牌来实现历史上下文,因为它提供了更紧凑的表示(平均文本审计代币比率为1:14),提高性能,并启用更长的对话,对质量的影响最小的影响很小。 ASR异步将用户语音转录为文本,并保持准确,最新的对话历史记录。

    后训练细节

    在后训练阶段,我们针对自动语音识别(ASR)与文本转语音(TTS)任务进行了专项监督微调(Supervised Fine-Tuning, SFT)。对于音频输入-文本输出(Audio Question Text Answer, AQTA)任务,我们采用多样化高质量数据集进行SFT,并采用了基于人类反馈的强化学习(RLHF)以提升响应质量,从而实现对情感表达、语速、方言及韵律的细粒度控制。

    TTS模型:

    解决TTS任务中高质量语音数据的稀缺性

    Training Detail

    与传统的语音合成(TTS)系统注重对说话人特征、情感表达、语言特征和风格元素的精细控制不同,我们的方法采用了基于聊天的范式和大型语言模型(LLMs)的训练方法。这一战略对齐显著增强了系统的灵活性,同时建立了一个可扩展的框架,以支持未来模型和数据的扩展,从而解决了语音合成系统在可扩展性方面的关键挑战。

    监督的微调格式:

    SFT格式包括三个基本组成部分:系统提示、人类输入和助手回复,采用两轮对话结构。在这种格式中,系统提示作为指定说话人属性和定义支持的指令标签的基础元素。人类输入和助手回复部分则专门用于处理文本内容和双词典表示。第一轮的文本和音频标记可以用来保持领域内说话人的音色和风格一致性,同时也支持领域外的零样本克隆。

    指令标签

    指令标签分为两种不同的类别:描述性标签和比较性标签。描述性标签用于控制语言、方言、声音和风格等方面,而比较性标签则用于情感和语速控制的层次化区分。描述性标签的数据是通过Step-Audio模型克隆生成的,支持包括日语、韩语、粤语、四川方言、可爱声音、说唱和唱歌等语言和风格。比较性标签的数据则是通过Audio Edit模型生成的,支持诸如快乐、愤怒、悲伤等情感,以及快慢等语速变化,每种变化都被分为五个层级。

    我们使用第5.1.1节中概述的SFT数据,并采用一个具有30亿参数的模型,训练一个周期,初始学习率为 2×10−5。学习率采用余弦衰减策略进行调整,最低值设置为 2×10−6。

    AQTA:

    我们为AQTA任务应用了基于人类反馈的强化学习(RLHF),从而创建了Step-Audio-Chat模型,如图6所示。

    说明:

    用了AQTA(音频输入,文本输出) + TTS框架 情况下是如何实现多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤),方言(如 粤语,四川话),可控制语速及韵律风格,支持RAP和哼唱

    通过TTS【cosyvoice】代码可知,LLM的文本输出中会包含 {语言}【情感】 [语速] 这样的文本输出,然后TTS用于合成对应的音频: 使用[{}]的声音,根据这些情感标签的指示,调整你的情感、语气、语调和哼唱节奏

        self.sys_prompt_dict = {
            "sys_prompt_for_rap": "请参考对话历史里的音色,用RAP方式将文本内容大声说唱出来。",
            "sys_prompt_for_vocal": "请参考对话历史里的音色,用哼唱的方式将文本内容大声唱出来。",
            "sys_prompt_wo_spk": '作为一名卓越的声优演员,你的任务是根据文本中()或()括号内标注的情感、语种或方言、音乐哼唱、语音调整等标签,以丰富细腻的情感和自然顺畅的语调来朗读文本。\n# 情感标签涵盖了多种情绪状态,包括但不限于:\n- "高兴1"\n- "高兴2"\n- "生气1"\n- "生气2"\n- "悲伤1"\n- "撒娇1"\n\n# 语种或方言标签包含多种语言或方言,包括但不限于:\n- "中文"\n- "英文"\n- "韩语"\n- "日语"\n- "四川话"\n- "粤语"\n- "广东话"\n\n# 音乐哼唱标签包含多种类型歌曲哼唱,包括但不限于:\n- "RAP"\n- "哼唱"\n\n# 语音调整标签,包括但不限于:\n- "慢速1"\n- "慢速2"\n- "快速1"\n- "快速2"\n\n请在朗读时,根据这些情感标签的指示,调整你的情感、语气、语调和哼唱节奏,以确保文本的情感和意义得到准确而生动的传达,如果没有()或()括号,则根据文本语义内容自由演绎。',
            "sys_prompt_with_spk": '作为一名卓越的声优演员,你的任务是根据文本中()或()括号内标注的情感、语种或方言、音乐哼唱、语音调整等标签,以丰富细腻的情感和自然顺畅的语调来朗读文本。\n# 情感标签涵盖了多种情绪状态,包括但不限于:\n- "高兴1"\n- "高兴2"\n- "生气1"\n- "生气2"\n- "悲伤1"\n- "撒娇1"\n\n# 语种或方言标签包含多种语言或方言,包括但不限于:\n- "中文"\n- "英文"\n- "韩语"\n- "日语"\n- "四川话"\n- "粤语"\n- "广东话"\n\n# 音乐哼唱标签包含多种类型歌曲哼唱,包括但不限于:\n- "RAP"\n- "哼唱"\n\n# 语音调整标签,包括但不限于:\n- "慢速1"\n- "慢速2"\n- "快速1"\n- "快速2"\n\n请在朗读时,使用[{}]的声音,根据这些情感标签的指示,调整你的情感、语气、语调和哼唱节奏,以确保文本的情感和意义得到准确而生动的传达,如果没有()或()括号,则根据文本语义内容自由演绎。',
        }

    VITA-1.5:GPT-4o级别的实时视觉和语音交互模型

    [📖 VITA-1.5 Paper] [🤖 Basic Demo] [🍎 VITA-1.0]

    [📽 VITA-1.5 Demo Show! Here We Go! 🔥]

    引言

    近年来,多模态大语言模型(MLLMs)在视觉和文本的结合上取得了显著进展。然而,随着人机交互需求的增加,语音在多模态对话系统中的作用变得愈发重要。语音不仅是信息传递的关键媒介,还能显著提升交互的自然性和便捷性。因此,如何将视觉和语音模态高效整合,实现高性能的多模态交互,成为了当前研究的重点。

    VITA-1.5的提出正是为了解决这一挑战。通过精心设计的多阶段训练方法,VITA-1.5逐步训练大语言模型(LLM)理解视觉和语音信息,最终实现了流畅的视觉和语音交互。与现有模型相比,VITA-1.5不仅保留了强大的视觉-语言能力,还实现了高效的语音对话能力,显著加速了多模态端到端的响应速度。

    VITA-1.5

    模型架构

    图 2:VITA-1.5 的整体架构。输入端由视觉和音频编码器及其连接到 LLM 的适配器组成。输出端有一个端到端的语音生成模块,而不是像初始 VITA-1.0 版本那样直接使用外部 TTS 模型。

    VITA-1.5的整体架构如图2所示。输入侧与VITA-1.0版本相同,采用“多模态编码器-适配器-LLM”的配置。它将视觉/音频Transformer和多层连接器与LLM结合进行联合训练,旨在增强对视觉、语言和音频的统一理解。在输出侧,VITA-1.5拥有自己的端到端语音模块,而不是像原始VITA-1.0版本那样使用外部TTS模型。

    视觉模态

    视觉编码器:VITA-1.5采用InternViT-300M作为视觉编码器,输入图像大小为448×448像素,每张图像生成256个视觉标记。对于高分辨率图像,VITA-1.5采用动态分块策略捕捉局部细节,提高图像理解的准确性。

    视频处理:视频被视为一种特殊的多图像输入。如果视频长度短于4秒,则均匀采样4帧;对于4到16秒的视频,每秒采样一帧;对于超过16秒的视频,均匀采样16帧。视频帧不应用动态分块,以避免过多的视觉标记影响处理效率。

    视觉适配器:使用两层MLP将视觉特征映射到适合LLM理解的视觉标记。

    音频模态

    语音编码器:类似于[56],我们的音频编码模块由多个下采样卷积层(4倍下采样)和24个Transformer块(隐藏大小为1024)组成。下采样层有助于降低音频特征的帧率,提高LLM的处理速度。音频编码器约有350M参数,输出帧率为12.5Hz。使用Mel滤波器组特征作为音频编码器的输入,窗口大小为25ms,偏移为10ms。

    语音适配器:由多个2倍下采样的卷积层组成。

    语音解码器:使用TiCodec作为我们的编解码模型,定制了一个大小为1024的单码本。这种单码本设计简化了推理阶段的解码过程。编解码模型负责将连续语音信号编码为离散语音标记,频率为40Hz,同时能够将这些标记解码回采样率为24,000Hz的语音信号。

    当前的LLM只能输出文本标记,语音生成能力要求LLM能够输出语音标记。为此,我们在文本标记后添加了两个语音解码器:1)非自回归(NAR)语音解码器,全局处理文本标记并建模语义特征,旨在生成语音标记的初始分布;2)自回归(AR)语音解码器,基于NAR解码器生成的语音信息逐步生成更高质量的语音标记。最终的语音标记序列通过编解码模型的语音解码器解码为连续语音信号流(波形)。我们为NAR和AR语音解码器采用了4个LLaMA解码层,隐藏大小为896,参数大小约为120M。

    训练数据

    如表1所示,多模态指令微调的训练数据涵盖了广泛的类别,如描述数据和问答数据,包括中文和英文。在不同的训练阶段,从整体数据集中选择性地采样子集以服务于不同的目标。具体来说,数据集分类如下:

    • 图像描述数据:使用ShareGPT4V、ALLaVA-Caption、SharedGPT4o-Image和合成数据等数据集训练模型生成图像的描述性语言。
    • 图像问答数据:使用LLaVA-150K、LLaVA-Mixture-sample、LVIS-Instruct、ScienceQA、ChatQA和从LLaVA-OV采样的子集(如通用图像问答和数学推理数据集)等数据集训练模型回答基于图像的问题和执行视觉推理任务。
    • OCR和图表数据:支持模型理解OCR和图表内容,使用Anyword-3M、ICDAR2019-LSVT、UReader、SynDOG、ICDAR2019-LSVT-QA和从LLaVA-OV采样的相应数据等数据集。
    • 视频数据:使用ShareGemini和合成数据等数据集训练模型处理视频输入并执行诸如描述和基于视频的问答等任务。
    • 纯文本数据:增强模型理解和生成语言的能力,促进基于文本的问答任务。

    除了表1中列出的图像和视频数据外,还纳入了110,000小时的内部语音-转录配对ASR数据,涵盖中文和英文,用于训练音频编码器并将音频编码器与LLM对齐。此外,使用TTS系统生成的3,000小时文本-语音配对数据用于训练语音解码器。

    三阶段训练策略

    为了确保VITA-1.5在涉及视觉、语言和音频的任务中表现良好,我们必须面对一个关键挑战,即不同模态之间的训练冲突。例如,添加语音数据可能会对视觉数据的理解产生负面影响,因为语音的特征与视觉的特征显著不同,导致学习过程中的干扰。为了解决这一挑战,我们设计了一个三阶段训练策略,如图3所示。核心思想是逐步将不同模态引入模型,使其在增加新模态能力的同时保持现有模态的能力。

    VITA-1.5的训练管道。训练过程分为三个阶段,以逐步将视觉和音频纳入LLM同时缓解了形态冲突。第一阶段的重点是视觉训练,包括视觉对齐(阶段1.1,使用表1中的20%字幕数据),视觉理解(阶段1.2,使用100%的字幕数据)以及用于Visual QA的指令调整(阶段1.3,使用20%字幕数据和100%QA数据)。阶段2引入音频输入调整,并具有音频对齐(阶段2.1,使用11,000小时的语音转录对)和语音质量检查的指令调整(阶段2.2,采样4%字幕数据和20%的QA数据)。最后,第3阶段的重点是音频输出调整,包括对编解码器模型的训练(使用3,000个小时的文本语音数据)和语音解码器培训(阶段3.2)。图像中显示的百分比对应于表1中指定的数据采样率。

    阶段1:视觉训练

    阶段1.1 视觉对齐:在此阶段,我们的目标是弥合视觉和语言之间的差距。前者的特征从预训练的视觉编码器InternViT-300M中提取,后者通过LLM引入。我们使用表1中20%的描述性描述数据进行训练,其中只有视觉适配器是可训练的,而其他模块是冻结的。这种方法允许LLM初步对齐视觉模态。

    阶段1.2 视觉理解:在此阶段,我们的目标是教会LLM转录图像内容。为此,我们使用表1中所有的描述性描述数据。在此过程中,视觉模块的编码器和适配器以及LLM都是可训练的。重点是使模型通过学习关于图像的描述性文本,建立视觉和语言之间的强连接,使其能够通过生成自然语言描述来理解图像内容。

    阶段1.3 视觉SFT:在阶段1.2之后,模型已经获得了对图像和视频的基本理解。然而,指令跟随能力仍然有限,难以应对视觉问答任务。为了实现这一点,我们使用表1中所有的问答数据,同时保留20%的描述性描述数据以增加数据集的多样性和任务的复杂性。

    在训练过程中,视觉模块的编码器和适配器以及LLM都是可训练的。此阶段的关键目标是使模型不仅能够理解视觉内容,还能够根据指令回答问题。

    阶段2:音频输入微调

    阶段2.1 音频对齐:在完成阶段1的训练后,模型已经建立了强大的图像和视频理解基础。在此阶段,我们的目标是基于阶段1减少音频和语言之间的差异,使LLM能够理解音频输入。训练数据包括11,000小时的语音-转录对。我们采用两步方法:(a)语音编码器训练:我们采用常见语音识别系统中使用的训练框架,使用连接时序分类(CTC)损失函数[18]训练语音编码器。目的是使编码器从语音输入中预测转录文本。此步骤确保音频编码器能够提取语音特征并将其映射到文本表示空间。(b)语音适配器训练:在训练语音编码器后,我们将其与LLM集成,使用音频适配器将音频特征引入LLM的输入层。此阶段的训练目标是使LLM能够输出语音数据的转录文本。

    此外,在步骤(b)中,我们引入了特殊的可训练输入标记来指导语音理解过程。这些标记提供了额外的上下文信息,指导用于问答任务的LLM执行ASR任务。

    阶段2.2 音频SFT:此阶段的重点是引入语音问题和文本答案的问答功能。为此,我们从表1中采样4%的描述数据和20%的问答数据。在数据处理方面,大约一半的基于文本的问题被随机替换为其对应的语音版本,使用TTS系统生成。

    在此阶段,视觉编码器和适配器、音频编码器和适配器以及LLM都是可训练的,旨在提高模型对多模态输入的适应性。此外,我们在LLM的输出中添加了一个分类头。该头用于区分输入是来自语音还是文本。结果,模型可以更准确地解释语音输入,并高效灵活地处理不同模态。

    阶段3:音频输出微调

    在前两个训练阶段,VITA-1.5模型已经有效地发展了其多模态理解能力。然而,一个关键的能力,即语音输出,仍然缺失,这对于其作为交互助手的角色至关重要。为了在不影响模型基本能力的情况下引入语音输出功能,我们借鉴了[56]的策略,使用3,000小时的文本-语音数据,并采用两步训练方法(见图3)。

    阶段3.1 编解码训练:此步骤的目标是使用语音数据训练具有单码本的编解码模型。编解码模型的编码器能够将语音映射到离散标记,而解码器可以将离散标记映射回语音流。在VITA-1.5的推理阶段,仅使用解码器。

    阶段3.2 NAR + AR解码器训练:此阶段的训练使用文本-语音配对数据,其中文本被输入到LLM的分词器和嵌入层以获得其嵌入向量,语音被输入到编解码模型的编码器以获得其语音标记。文本嵌入向量被发送到NAR语音解码器以获得全局语义特征,然后将这些特征发送到AR语音解码器,预测相应的语音标记。请注意,在此阶段LLM是冻结的,因此多模态性能不受影响。

    评估

    视觉-语言评估

    基线:我们比较了一系列开源MLLMs,包括VILA-1.5、LLaVA-Next、CogVLM2、InternLM-XComposer2.5、Cambrian-1、MiniCPM-V-2.6、Ovis1.5、InternVL-Chat-1.5、InternVL-2、LLaVA-OV和Video-LLaVA、SilME和LongVA,以及5个闭源MLLMs,包括GPT-4V、GPT-4o、GPT-4o-mini、Gemini 1.5 Pro和Claude 3.5 Sonnet。

    评估基准:为了评估VITA-1.5的图像感知和理解能力,我们使用了多个评估基准,包括MME、MMBench、MMStar、MMMU、MathVista、HallusionBench、AI2D、OCRBench和MMVet。这些基准涵盖了广泛的方面,包括通用多模态能力(如MME、MMBench和MMMU)、数学推理(MathVista)、幻觉检测(HallusionBench)、图表(AI2D)和OCR(OCRBench)理解,提供了全面的评估结果。对于视频理解,我们使用了代表性的评估基准,包括Video-MME、MVBench和TempCompass。

    视觉-语言能力:表2展示了VITA-1.5的图像理解性能比较。在三个阶段的训练后,VITA-1.5的表现与最先进的开源模型相当,甚至超过了一些闭源模型,如GPT-4V和GPT-4o-mini。这一结果突显了VITA-1.5在图像-语言任务中的强大能力。如表3所示,VITA-1.5在视频理解评估中表现出与顶级开源模型相当的性能。与专有模型的显著差距表明,VITA-1.5在视频理解方面仍有显著的改进空间和潜力。请注意,在阶段2(音频输入微调)和阶段3(音频输出微调)的训练后,VITA-1.5几乎保留了其在阶段1(视觉-语言训练)中的原始视觉-语言能力。

    语音评估

    基线:以下三个基线模型用于比较:Wav2vec2-base、Mini-Omini2、Freeze-Omini和VITA-1.0。

    评估基准普通话评估集包括三个数据集:aishell-1、test net和test meeting。这些数据集用于评估模型在普通话语音上的表现。评估指标是字符错误率(CER)。英语评估集包括四个数据集:dev-clean、dev-other、test-clean和test-other,用于评估模型在英语语音上的表现。评估指标是词错误率(WER)。

    ASR性能:表4中的评估结果表明,VITA-1.5在普通话和英语ASR任务中均取得了领先的准确性。这表明VITA-1.5已成功集成了先进的语音能力,以支持多模态交互。

    结论

    本文介绍了VITA-1.5,这是一个通过精心设计的三阶段训练策略整合视觉和语音的多模态LLM。通过缓解模态之间的固有冲突,VITA-1.5在视觉和语音理解方面实现了强大的能力,无需依赖单独的ASR或TTS模块即可实现高效的语音到语音交互。广泛的评估表明,VITA-1.5在多模态基准测试中表现出色。我们希望VITA-1.5能够接过VITA-1.0的旗帜,继续推动开源模型在实时多模态交互领域的进步。

    ASR语音识别指标计算

    #coding=utf-8
    import os
    import sys
    import re
    from typing import List, Union
    import jiwer
    import pdb
    
    
    def cal_wer(path_ref, path_hyp, metric_type, output_detail, path_output):
    
        ref_text, hyp_text, ref_key = _read_file(path_ref, path_hyp, metric_type)
        
        cal_wer_from_list(ref_text, hyp_text, ref_key, metric_type, output_detail, path_output)
    
    
    def cal_wer_from_list(
        reference: Union[str, List[str]], 
        hypothesis: Union[str, List[str]], 
        key: Union[str, List[str]], 
        metric_type: str, 
        output_detail: bool, 
        path_output: str
    ):
        if isinstance(reference, str):
            reference = [reference]
        if isinstance(hypothesis, str):
            hypothesis = [hypothesis]
        if isinstance(key, str):
            key = [key]
    
        # 根据ref是否为空, 先分别计算wer指标再汇总
        ref_normal, hyp_normal, key_normal = [], [], []
        ref_empty, hyp_empty, key_empty = [], [], []
        for i in range(len(reference)):
            if len(reference[i]) != 0:
                ref_normal.append(reference[i])
                hyp_normal.append(hypothesis[i])
                key_normal.append(key[i])
            else:
                ref_empty.append(reference[i])
                hyp_empty.append(hypothesis[i])
                key_empty.append(key[i])
    
        res_normal, out_normal = _cal_wer_normal(ref_normal, hyp_normal, metric_type)
        res_empty, out_empty = _cal_wer_empty(hyp_empty, metric_type)
        _summary(ref_normal, hyp_normal, res_normal, out_normal.alignments, key_normal, 
                 hyp_empty, res_empty, out_empty, key_empty, 
                 metric_type, output_detail, path_output)
    
    
    def _read_file(path_ref, path_hyp, metric_type):
        ref_key, ref_text = _preprocess(path_ref, '\t', metric_type)
        hyp_key, hyp_text = _preprocess(path_hyp, '\t', metric_type)
    
        tmp_dict = {}
        tmp_text = []
        for i in range(len(hyp_key)):
            if hyp_key[i] not in tmp_dict.keys():
                tmp_dict[hyp_key[i]] = hyp_text[i]
            else:
                print ("repeated key")
        for i in range(len(ref_key)):
            if ref_key[i] in tmp_dict.keys():
                tmp_text.append(tmp_dict[ref_key[i]])
            else:
                tmp_text.append("")
    
        return ref_text, tmp_text, ref_key
    
    
    def _preprocess(path_in, sep, metric_type):
        res_key, res_text = [], []
    
        with open(path_in, "r", encoding="utf-8") as f_in:
            lines = f_in.readlines()
            for line in lines:
                line = line.strip().split(sep, 1)
                if len(line) == 2:
                    key, text = line
                    text = re.sub("<s>", "", text)
                    text = re.sub("</s>", "", text)
                    text = re.sub("<unk>", "", text)
                    text = re.sub("@@ ", "", text)
                    text = re.sub("@ ", "", text)
                    text = re.sub("@@", "", text)
                    text = re.sub("@", "", text)
                    #text = re.sub(" ", "", text)
                    text = text.lower()
                else:
                    key = line[0]
                    text = ""
    
                text = [x for x in text]
                text_tmp = ""
                if metric_type == "wer":
                    for ch in text:
                        if '\u4e00' <= ch <= '\u9fff':
                            text_tmp += " " + ch + " "
                        else:
                            text_tmp += ch
                    text = text_tmp.strip().replace("  ", " ")
                elif metric_type == "cer":
                    text_tmp = "".join(text)
                    text = text_tmp.strip().replace(" ", "")
                else:
                    assert False
    
                res_key.append(key)
                res_text.append(text)
    
        return res_key, res_text
    
    
    def _cal_wer_normal(reference, hypothesis, metric_type):
        if metric_type == "wer":
            out = jiwer.process_words(reference=reference, hypothesis=hypothesis)
            ERR = out.wer
        elif metric_type == "cer":
            out = jiwer.process_characters(reference=reference, hypothesis=hypothesis)
            ERR = out.cer
        else:
            assert False
    
        H = out.hits
        S = out.substitutions
        D = out.deletions
        I = out.insertions
        N = H + S + D
    
        res = [ERR, N, S, D, I]
    
        return res, out
    
    
    def _cal_wer_empty(hypothesis, metric_type):
        out = []
    
        I = 0
        for hyp in hypothesis:
            if hyp == "":
                i = 0
            else:
                if metric_type == "wer":
                    i = len(hyp.split(" "))
                elif metric_type == "cer":
                    i = len(hyp)
                else:
                    assert False
            I += i
            out.append(i)
    
        res = [0, 0, 0, 0, I]
    
        return res, out
    
    
    def _summary(ref_normal, hyp_normal, res_normal, out_normal, key_normal,
                 hyp_empty, res_empty, out_empty, key_empty, 
                 metric_type, output_detail, path_output):
        # wer/cer计算
        _, N, S, D, I = res_normal
        I += res_empty[-1]
        if N != 0:
            ERR = (S + D + I) / N
            SUB = S / N
            DEL = D / N
            INS = I / N
            N_WORD = N
        else:
            if I == 0:
                ERR = 0
            else:
                ERR = 1
            SUB, DEL, INS, N_WORD = 0, 0, I, 0
    
        # 句准计算 + 详细错误指标 + 详细错误统计
        utt_normal, alignments_normal, statistics_normal = _analyse_normal(
            ref_normal, hyp_normal, out_normal, key_normal, metric_type)
        utt_empty, alignments_empty, statistics_empty = _analyse_empty(
            hyp_empty, out_empty, key_empty, metric_type)
    
        utt = utt_normal + utt_empty
        alignments = alignments_normal + alignments_empty
        for key in statistics_empty['insert'].keys():
            if key not in statistics_normal['insert'].keys():
                statistics_normal['insert'][key] = statistics_empty['insert'][key]
            else:
                statistics_normal['insert'][key] += statistics_empty['insert'][key]
        N_SENT = len(out_normal) + len(out_empty)
        ACC_UTT = utt / N_SENT
        res = [ERR, SUB, DEL, INS, N_WORD, ACC_UTT, N_SENT]
    
        _format_output(res, alignments, statistics_normal, metric_type, output_detail, path_output)
    
    
    def _analyse_normal(ref_normal, hyp_normal, out_normal, key_normal, metric_type):
        utt_normal = 0
        alignments_normal = []
        statistics_normal = {'substitute' : {}, 'delete' : {}, 'insert' : {}}
    
        for i, alignment in enumerate(out_normal):
            err, n_hit, n_sub, n_del, n_ins = 0, 0, 0, 0, 0
            ref_align, hyp_align = "", ""
            sub_align, del_align, ins_align = "", "", ""
            for j, chunk in enumerate(alignment):
                if (metric_type == "wer" and (ref_align != "" or hyp_align != "")):
                    ref_align += " "
                    hyp_align += " "
                if chunk.type == 'equal':
                    n_hit += chunk.ref_end_idx - chunk.ref_start_idx
                    ref_align += _extract_string(ref_normal[i], chunk.ref_start_idx, chunk.ref_end_idx, metric_type)
                    hyp_align += _extract_string(hyp_normal[i], chunk.hyp_start_idx, chunk.hyp_end_idx, metric_type)
    
                elif chunk.type == 'substitute':
                    err += 1
                    n_sub += chunk.ref_end_idx - chunk.ref_start_idx
    
                    ref_sub = _extract_string(ref_normal[i], chunk.ref_start_idx, chunk.ref_end_idx, metric_type)
                    hyp_sub = _extract_string(hyp_normal[i], chunk.hyp_start_idx, chunk.hyp_end_idx, metric_type)
    
                    ref_align += ref_sub
                    hyp_align += hyp_sub
    
                    key_sub = "(" + ref_sub + ") --> (" + hyp_sub + ")"
    
                    sub_align += key_sub + "\t"
    
                    if key_sub not in statistics_normal['substitute'].keys():
                        statistics_normal['substitute'][key_sub] = 1
                    else:
                        statistics_normal['substitute'][key_sub] += 1
    
                elif chunk.type == 'delete':
                    err += 1
                    n_del += chunk.ref_end_idx - chunk.ref_start_idx
    
                    ref_del = _extract_string(ref_normal[i], chunk.ref_start_idx, chunk.ref_end_idx, metric_type)
                    hyp_del = "*"
    
                    ref_align += ref_del
                    hyp_align += hyp_del
    
                    key_del = ref_del
    
                    del_align += key_del + "\t"
    
                    if key_del not in statistics_normal['delete'].keys():
                        statistics_normal['delete'][key_del] = 1
                    else:
                        statistics_normal['delete'][key_del] += 1
    
                elif chunk.type == 'insert':
                    err += 1
                    n_ins += chunk.hyp_end_idx - chunk.hyp_start_idx
    
                    ref_ins = "*"
                    hyp_ins = _extract_string(hyp_normal[i], chunk.hyp_start_idx, chunk.hyp_end_idx, metric_type)
    
                    ref_align += ref_ins
                    hyp_align += hyp_ins
    
                    key_ins = hyp_ins
    
                    ins_align += key_ins + "\t"
    
                    if key_ins not in statistics_normal['insert'].keys():
                        statistics_normal['insert'][key_ins] = 1
                    else:
                        statistics_normal['insert'][key_ins] += 1
    
                else:
                    assert False
    
            if err == 0:
                utt_normal += 1
            alignments_normal.append((key_normal[i], ref_align, hyp_align, 
                                      sub_align, del_align, ins_align, 
                                      n_hit, n_sub, n_del, n_ins))
    
        return utt_normal, alignments_normal, statistics_normal
    
    
    def _analyse_empty(hyp_empty, out_empty, key_empty, metric_type):
        utt_empty = 0
        alignments_empty = []
        statistics_empty = {'insert' : {}}
    
        for i, ins in enumerate(out_empty):
            ref_align, hyp_align = "", ""
            sub_align, del_align, ins_align = "", "", ""
    
            if ins == 0:
                utt_empty += 1
            else:
                ref_ins = "*"
                hyp_ins = _extract_string(hyp_empty[i], 0, len(hyp_empty[i]), metric_type)
    
                ref_align += ref_ins
                hyp_align += hyp_ins
    
                key_ins = hyp_ins
    
                ins_align += key_ins + "\t"
    
                if key_ins not in statistics_empty['insert'].keys():
                    statistics_empty['insert'][key_ins] = 1
                else:
                    statistics_empty['insert'][key_ins] += 1
            alignments_empty.append((key_empty[i], ref_align, hyp_align, 
                                    sub_align, del_align, ins_align, 
                                    0, 0, 0, ins))
    
        return utt_empty, alignments_empty, statistics_empty
    
    
    def _extract_string(s, begin, end, metric_type):
        res = ""
        if metric_type == 'wer':
            res = ' '.join(s.split(' ')[begin:end])
        elif metric_type == 'cer':
            res = s[begin:end]
        else:
            assert False
        return res
    
    
    def _format_output(res, alignments, statistics, metric_type, output_detail, path_output):
        with open(path_output, "w", encoding="utf-8") as f_out:
            if output_detail == True:
                f_out.write("-"*100 + "\n")
                for i, sample in enumerate(alignments):
                    key, ref, hyp = sample[0:3]
                    sub_align, del_align, ins_align = sample[3:6]
                    n_hit, n_sub, n_del, n_ins = sample[6:]
    
                    f_out.write("KEY: " + key + "\n")
                    f_out.write("REF: " + ref + "\n")
                    f_out.write("HYP: " + hyp + "\n")
                    f_out.write("CNT: " + "H(" + str(n_hit) + ") " + \
                                          "S(" + str(n_sub) + ") " + \
                                          "D(" + str(n_del) + ") " + \
                                          "I(" + str(n_ins) + ")\n")
                    f_out.write("SUB: " + sub_align + "\n")
                    f_out.write("DEL: " + del_align + "\n")
                    f_out.write("INS: " + ins_align + "\n\n")
                f_out.write("-"*100 + "\n")
    
                f_out.write("-"*100 + "\n")
                lst_sub = list(sorted(statistics['substitute'].items(), key = lambda x : x[1], reverse=True))
                lst_del = list(sorted(statistics['delete'].items(), key = lambda x : x[1], reverse=True))
                lst_ins = list(sorted(statistics['insert'].items(), key = lambda x : x[1], reverse=True))
                f_out.write("\n替换错误统计: \n")
                for x in lst_sub:
                    f_out.write("\t" + x[0] + "(" + str(x[1]) + ")" + "\n")
                f_out.write("\n删除错误统计: \n")
                for x in lst_del:
                    f_out.write("\t" + x[0] + "(" + str(x[1]) + ")" + "\n")
                f_out.write("\n插入错误统计: \n")
                for x in lst_ins:
                    f_out.write("\t" + x[0] + "(" + str(x[1]) + ")" + "\n")
                f_out.write("-"*100 + "\n")
    
            f_out.write("-"*100 + "\n")
            f_out.write(metric_type.upper() + ": " + str(round(res[0] * 100.0, 2)) + '%\n')
            f_out.write("WORDS: " + str(res[4]) + "\t")
            f_out.write("SUB: " + str(round(res[1] * 100.0, 2)) + "%\t")
            f_out.write("DEL: " + str(round(res[2] * 100.0, 2)) + "%\t")
            f_out.write("INS: " + str(round(res[3] * 100.0, 2)) + "%\n")
            f_out.write("ACC_UTT: " + str(round(res[5] * 100.0, 2)) + '%\t')
            f_out.write("SENTS: " + str(res[6]) + '\n')
            f_out.write("-"*100 + "\n")
        
        print (metric_type + " calculation done")
        print ("saved to " + path_output)
    
    
    if __name__ == '__main__':
    
        '''
        # example of function cal_wer_from_list
        ref = ["今 天 天 气", "hello 我 ok 的", ""]
        hyp = ["今 天 天", "halo 我 ok 的 呀", "噪 声"]
        key = ["000", "001", "002"]
        path_output = "./example.wer"
        cal_wer(ref, hyp, key, "wer", True, path_output)
    
        ref = ["今天天气", "hello我ok的", ""]
        hyp = ["今天天", "halo我ok的呀", "噪声"]
        key = ["000", "001", "002"]
        path_output = "./example.cer"
        cal_wer_from_list(ref, hyp, key, "cer", True, path_output)
        '''
    

    InspireMusic–阿里通义开源音乐生成框架

    InspireMusic是由通义实验室开源的音乐生成技术,旨在打造一款集音乐生成、歌曲生成、音频生成能力为一体的开源AIGC工具包。

    为研究者和开发者提供音乐/歌曲/音频生成模型的训练和调优工具及模型,方便优化生成效果;同时为音乐爱好者提供一个易于使用的文本生成音乐/歌曲/音频创作工具,可通过文字描述或音频提示来控制生成内容。

    目前,InspireMusic已开源了音乐生成的训练和推理代码,支持通过简单的文字描述或音频提示,快速生成多种风格的音乐作品。

    InspireMusic的文生音乐创作模式涵盖了多种曲风、情感表达和复杂的音乐结构控制,提供了极大的创作自由度和灵活性。未来计划进一步开放歌唱生成和音频生成的基础模型,欢迎研究者、开发者及用户积极参与体验和研发。该开源工具包为社区开发者提供了丰富的技术资源,支持从学术研究到产品开发的广泛应用。

    🎶 主要特点

    • 统一的音频生成框架:基于音频大模型技术,InspireMusic支持音乐、歌曲及音频的生成,为用户提供多样化选择;
    • 灵活可控生成:基于文本提示和音乐特征描述,用户可精准控制生成音乐的风格和结构;
    • 简单易用:简便的模型微调和推理工具,为用户提供高效的训练与调优工具。

    🌟代码仓库

    核心模型

    InspireMusic由音频tokenizer、自回归Transformer模型、基于常微分方程的扩散模型即Conditional Flow Matching (CFM)模型、Vocoder所组成,可支持文本生成音乐、音乐续写等任务。通过具有高压缩比的单码本WavTokenizer将输入的连续音频特征转换成离散音频token,然后利用基于Qwen模型初始化的自回归Transformer模型预测音频token,再由CFM扩散模型重建音频的潜层特征,最终通过Vocoder输出高质量的音频波形。两种推理模式的设计:fast模型和高音质模型,为不同需求的用户提供了灵活的选择。

    工具包安装使用指南

    第一步:下载代码库

    git clone --recursive https://github.com/FunAudioLLM/InspireMusic.git
    # If you failed to clone submodule due to network failures, please run the following command until success
    cd InspireMusic
    git submodule update --init --recursive

    第二步:安装代码库

    conda create -n inspiremusic python=3.8
    conda activate inspiremusic
    cd InspireMusic
    # pynini is required by WeTextProcessing, use conda to install it as it can be executed on all platforms.
    conda install -y -c conda-forge pynini==2.1.5
    pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host=mirrors.aliyun.com
    # install flash attention to speedup training, support version 2.6.3
    pip install flash-attn --no-build-isolation

    第三步:下载模型

    InspireMusic-Base模型(https://www.modelscope.cn/iic/InspireMusic)
    # git模型下载,请确保已安装git lfs
    mkdir -p pretrained_models
    git clone https://www.modelscope.cn/iic/InspireMusic.git pretrained_models/InspireMusic-Base

    第四步:基本用法说明快速开始

    cd InspireMusic/examples/music_generation/
    bash run.sh

    训练LLM和flow matching模型样例脚本。

    torchrun --nnodes=1 --nproc_per_node=8 \
        --rdzv_id=1024 --rdzv_backend="c10d" --rdzv_endpoint="localhost:0" \
        inspiremusic/bin/train.py \
        --train_engine "torch_ddp" \
        --config conf/inspiremusic.yaml \
        --train_data data/train.data.list \
        --cv_data data/dev.data.list \
        --model llm \
        --model_dir `pwd`/exp/music_generation/llm/ \
        --tensorboard_dir `pwd`/tensorboard/music_generation/llm/ \
        --ddp.dist_backend "nccl" \
        --num_workers 8 \
        --prefetch 100 \
        --pin_memory \
        --deepspeed_config ./conf/ds_stage2.json \
        --deepspeed.save_states model+optimizer \
        --fp16
    
    torchrun --nnodes=1 --nproc_per_node=8 \
        --rdzv_id=1024 --rdzv_backend="c10d" --rdzv_endpoint="localhost:0" \
        inspiremusic/bin/train.py \
        --train_engine "torch_ddp" \
        --config conf/inspiremusic.yaml \
        --train_data data/train.data.list \
        --cv_data data/dev.data.list \
        --model flow \
        --model_dir `pwd`/exp/music_generation/flow/ \
        --tensorboard_dir `pwd`/tensorboard/music_generation/flow/ \
        --ddp.dist_backend "nccl" \
        --num_workers 8 \
        --prefetch 100 \
        --pin_memory \
        --deepspeed_config ./conf/ds_stage2.json \
        --deepspeed.save_states model+optimizer

    推理脚本

    cd InspireMusic/examples/music_generation/
    bash infer.sh

    带有CFM的推理模式

    pretrained_model_dir = "pretrained_models/InspireMusic/"
    for task in 'text-to-music' 'continuation'; do
      python inspiremusic/bin/inference.py --task $task \
          --gpu 0 \
          --config conf/inspiremusic.yaml \
          --prompt_data data/test/parquet/data.list \
          --flow_model $pretrained_model_dir/flow.pt \
          --llm_model $pretrained_model_dir/llm.pt \
          --music_tokenizer $pretrained_model_dir/music_tokenizer \
          --wavtokenizer $pretrained_model_dir/wavtokenizer \
          --result_dir `pwd`/exp/inspiremusic/${task}_test \
          --chorus verse \
          --min_generate_audio_seconds 8 \
          --max_generate_audio_seconds 30 
    done

    不带CFM的fast推理模式

    pretrained_model_dir = "pretrained_models/InspireMusic/"
    for task in 'text-to-music' 'continuation'; do
      python inspiremusic/bin/inference.py --task $task \
          --gpu 0 \
          --config conf/inspiremusic.yaml \
          --prompt_data data/test/parquet/data.list \
          --flow_model $pretrained_model_dir/flow.pt \
          --llm_model $pretrained_model_dir/llm.pt \
          --music_tokenizer $pretrained_model_dir/music_tokenizer \
          --wavtokenizer $pretrained_model_dir/wavtokenizer \
          --result_dir `pwd`/exp/inspiremusic/${task}_test \
          --chorus verse \
          --fast \
          --min_generate_audio_seconds 8 \
          --max_generate_audio_seconds 30 
    done