FireRedASR -小红书语音识别大模型

小红书 FireRed 团队正式发布并开源了基于大模型的语音识别模型 ——FireRedASR,在语音识别领域带来新突破。在业界广泛采用的中文普通话公开测试集上,FireRedASR 凭借卓越的性能取得了新 SOTA!FireRedASR 在字错误率(CER)这一核心技术指标上,对比此前的 SOTA Seed-ASR,错误率相对降低 8.4%,充分体现了团队在语音识别技术领域的创新能力与技术突破。

FireredAsr,旨在满足各种应用程序中出色的性能和最佳效率的各种要求。 fireredasr包括两个变体:

FireRedASR-LLM

采用Encoder-Adapter-LLM,结合了文本预训练 LLM 的能力,为极致的 ASR 准确率而生,适用于对准确率要求极高的应用场景。在公共普通话基准上,fireredasr-LLM (8.3b参数)达到3.05%的平均字符错误率(CER),超过了3.33%的最新SOTA,相对CER(CERR)8.4%。它显示出优于工业级基线的卓越概括能力,在多源普通话ASR方案(例如视频,现场和智能助理)中,达到24%-40%的CERR。

FireRedASR-AED

基于经典的 Attention-based Encoder-Decoder 架构,FireRedASR-AED 通过扩展参数至 1.1B,成功平衡了 ASR 语音识别的高准确率与推理效率。适用于资源受限的应用程序。

主要贡献

  • High-Accuracy Models with Efficiency: ASR识别准确率优于Seed-ASR[字节跳动],模型在保持效率的同时达到卓越精度的能力。
  • Robust Real-World Performance: 在各种实用的场景中,包括简短的视频,直播,字幕生成,语音输入和智能助手,我们的模型表现出了出色的功能,与相比的相对减少(CERR)相比实现了24%-40%流行的开源基线和领先的商业解决方案。
  • 多功能识别能力:支持方言/中文/英文/歌曲识别。而且在歌词识别中表现出色

模型结构:

FireRedASR-AED是基于注意的编码器-解码器 ASR模型。训练数据:包括大约70,000小时的音频数据,主要是高质量的普通话语音。与Whisper中使用的弱标记数据集不同,我们的大多数数据都是由专业注释者手动转录的,从而确保了高转录精度和可靠性。该数据集还包含大约11,000小时的英语语音数据,以增强英语ASR功能。

Input Features: 输入25ms窗口的80-dimensional  log Mel filterbank (Fbank),10ms frame shifts,然后是全局均值和方差归一化。

Encoder Structure:编码器由两个主要组件组成:一个下采样模块和Conformer  blocks堆叠。

Decoder Structure:解码器遵循Transformer 体系结构。

Tokenization:BPE编码英文文本, 1,000 English BPE tokens, 6,827 Chinese characters, and 5 special tokens.

FireRedASR-LLM: Encoder-Adapter-LLM 架构。

Input Features and Encoder: 训练数据和处理、encoder跟FireredAsr-AED相同。

Adapter Structure:一个简单但有效的线性RELU线性网络组成,该网络投射了编码器的输出维度,以匹配输入LLM。在适配器的开头合并了一个额外的框架剪接操作。此操作进一步将时间分辨率从40ms降低到每个帧的80ms,从而降低了序列长度并提高了计算效率LLM。

LLM初始化和处理:LLM用QWEN2-7B-INSTRUCT的预训练的重量初始化。训练数据格式:(prompt, speech, transcript)

Training Strategy编码器和适配器是完全训练的,LLM采用lora微调,保证LLM的文本能力。此策略可确保编码器和适配器经过充分训练,以将语音特征映射到LLM的语义空间中,同时保留其预训练能力。训练目标基于交叉熵损失,损失仅在输入的转录部分上计算,忽略提示和语音嵌入。

Evaluation

缩放定律的观察

LLMs 方面的最新研究表明,模型性能通常会随着模型尺寸的增加而提高,这称为缩放定律 。如表3所示,我们研究了具有不同模型大小的模型的缩放行为。对于 FireRedASR-AED,我们将模型大小逐步从 140M、413M、732M 扩展到 1.1B 参数。随着模型尺寸的增加,性能持续提高,从 XS 扩展到 S、从 S 扩展到 M 以及从 M 扩展到 L 配置时分别实现 6.1%、5.3% 和 5.6% 的 CERR。对于 FireRedASR-LLM,专注于扩展编码器,同时保持 LLM 主干不变。编码器大小从 86M 增加到 710M 参数,适配器参数的变化很小(17M 到 22M)。这表现出相似的扩展模式并带来一致的性能改进,从 XS(3.29%)到 L(3.05%)配置的总体 CERR 为 7.3%。这些结果证明了我们的扩展策略的有效性,并表明通过更大的模型容量可以进一步改进。

下图是 FireRedASR 和其他 ASR 大模型的对比,在业界常用的中文普通话公开测试集上,FireRedASR-LLM(8.3B 参数量)取得了最优 CER 3.05%、成为新 SOTA!FireRedASR-AED (1.1B 参数量)紧随其后取得 3.18%,两者均比 Seed-ASR(12+B 参数量)的 3.33% 低、并且参数量更小。FireRedASR 也比 Qwen-Audio、SenseVoice、Whisper、Paraformer 取得了更优的 CER。

FireRedASR 不仅在公开测试集上表现优异,在多种日常场景下,也展现了卓越的语音识别效果。
如下图所示,在由短视频、直播、语音输入和智能助手等多种来源组成的 Speech 测试集上,与业内领先的 ASR 服务提供商(ProviderA)和 Paraformer-Large 相比, FireRedASR-LLM 的 CER 相对降低 23.7%~40.0%,优势十分明显。
值得一提的是,在需要歌词识别能力的场景中,FireRedASR-LLM 也表现出极强的适配能力,CER 实现了 50.2%~66.7% 的相对降低,这一成果进一步拓宽了 FireRedASR 的应用范围,使其不仅能胜任传统语音识别需求,还能在创新性的多媒体场景中大放异彩。

值得一提的是,FireRedASR 在中文方言和英语场景中同样表现不俗。在 KeSpeech(中文方言)和 LibriSpeech(英语)测试集上,FireRedASR 的 CER 显著优于此前的开源 SOTA 模型,使其在支持好普通话 ASR 的前提下,在中文方言和英语上也足够通用,进一步凸显了其鲁棒的语言适配能力。

Discussion:

FireredAsr模型优于竞争模型的原因:

高质量和多样化的训练数据:语料库主要由从现实世界情景中收集的专业转录音频组成,该音频比在受控环境中提供的传统阅读式录音相比,它提供的训练信号明显更高。该数据集包括声音条件,扬声器,重音和内容域的广泛差异,总计数万小时。这种多样性和规模使我们的模型能够学习强大的语音表征和语言模式。

实证研究表明,一千小时的高质量,人工标注的数据比一万小时的弱标记数据(例如,来自视频标题,OCR结果或其他ASR模型的输出)更好的结果,这解释了我们比Whisper的优势 。此外,在我们的语料库中包含唱歌数据为处理音乐内容时的基线模型的显着改进做出了贡献。

优化的训练策略:将FireredAsr-A的扩展为140m到1.1b参数时,我们将正则化和学习率确定为影响模型收敛的关键因素。我们制定了一种渐进式正则化训练策略:最初没有正则化技术以实现快速收敛,然后逐渐引入更强的正则化,因为出现了过度拟合的趋势。此外,较大的模型需要降低学习率,这对于调整此参数的最佳性能至关重要。

高效的ASR框架

总结:提出了fireredasr-LLM和FireredAsr-AED,两种针对普通话优化的高性能ASR模型。通过全面的评估,我们证明了他们的体系结构,培训策略和高质量的数据集可以在保持计算效率的同时达到最先进的性能。

DeepSeek-R1 技术报告

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

Github: https://github.com/deepseek-ai/DeepSeek-R1

DeepSeek-R1:通过强化学习提升LLM的推理能力

R1训练流程:

•冷启动 •基于推理的强化学习 •Rejection Sampling •SFT •全场景强化学习

DeepSeek-R1-Zero 采用大规模强化学习(RL)进行训练,无需预先进行监督微调(SFT),表现出显著的推理能力。在强化学习过程中,DeepSeek-R1-Zero 展现出多种卓越且新颖的推理特性。但该模型仍面临可读性不足语言混杂等问题。

为解决这些问题并进一步增强推理性能,研究团队开发了 DeepSeek-R1,该模型在进行强化学习前引入了多阶段训练和冷启动数据。

DeepSeek-R1 在推理任务上实现了与 OpenAI-o1-1217 相当的性能水平

为促进学术研究发展,研究团队开源了 DeepSeek-R1-Zero、DeepSeek-R1,以及基于 Qwen 和 Llama 架构从 DeepSeek-R1 知识蒸馏获得的六个稠密模型(1.5B、7B、8B、14B、32B、70B)。

引言

近年来,LLM技术发展迅速,不断缩小与AGI的差距。后训练技术已成为完整训练流程中的关键环节,证实能够提升推理任务准确率,实现社会价值观对齐,适应用户偏好,同时相较于预训练所需计算资源较少。在推理能力方面,OpenAI的o1系列模型首次通过延长Chain-of-Thought(CoT)推理过程引入了推理时扩展机制,在数学、编程和科学推理等多个推理任务中取得显著进展。

然而,如何实现有效的测试时扩展仍是学术界面临的重要课题。前期研究探索了多种方法,包括过程型奖励模型、强化学习以及蒙特卡洛树搜索和束搜索等算法。但这些方法均未能达到与OpenAI的o1系列模型相当的通用推理水平。

本研究采用纯RL方法提升语言模型的推理能力。研究旨在探索LLM在无监督数据条件下通过纯RL过程实现自我进化的推理能力潜力。

具体而言,研究选用DeepSeek-V3-Base作为基础模型,采用群组相对策略优化(GRPO)作为RL框架提升模型推理性能。在训练过程中,DeepSeek-R1-Zero自然形成了多种高效且创新的推理特征。经过数千轮RL迭代,DeepSeek-R1-Zero在推理基准测试中展现出优异性能。例如,在AIME 2024测试中,pass@1得分从15.6%提升至71.0%,采用majority voting机制后,得分进一步提高到86.7%,达到OpenAI-o1-0912的性能水平。

然而,DeepSeek-R1-Zero仍面临可读性不足、语言混杂等挑战。

为解决这些问题并进一步提升推理性能,研究团队开发了DeepSeek-R1模型,该模型整合了初始训练数据和多阶段训练流程。具体实施步骤包括:首先收集数千条初始训练数据用于DeepSeek-V3-Base模型的微调;随后进行推理强化学习训练;在RL过程接近收敛时,通过拒绝采样(rejection sampling)方法从RL检查点生成新的SFT数据,并结合DeepSeek-V3在写作、事实QA和自我认知等领域的监督数据重新训练DeepSeek-V3-Base模型;最后,使用新数据完成微调后的检查点进行额外的RL训练,综合考虑各类场景的提示词。

经过上述步骤,最终获得的DeepSeek-R1模型达到了与OpenAI-o1-1217相当的性能水平。

研究进一步探索了从DeepSeek-R1到较小dense模型的知识蒸馏。以Qwen2.5 32B为基础模型,直接从DeepSeek-R1进行知识蒸馏的效果优于直接应用RL训练,表明大型基础模型所发现的推理模式对提升推理能力具有关键作用。研究团队已开源蒸馏后的Qwen和Llama系列模型。

值得注意的是,14B蒸馏模型的性能显著超越了当前最先进的开源模型QwQ-32B-Preview,而32B和70B蒸馏模型则在稠密模型推理基准测试中创造了新的记录

主要贡献

后训练:基础模型的大规模强化学习应用

  • 本研究直接将RL应用于基础模型,无需将SFT作为前置步骤。这种方法使模型能够通过CoT探索复杂问题的解决方案,最终开发出DeepSeek-R1-Zero模型。DeepSeek-R1-Zero具备自我验证、反思和生成长CoT等能力,为学术界提供了重要研究成果。这是首个验证LLM推理能力可纯粹通过RL提升而无需SFT的开放研究,为该领域未来发展奠定基础
  • 研究提出了DeepSeek-R1的开发流程,包含两个RL阶段用于优化推理模式和人类偏好对齐,以及两个SFT阶段用于构建模型的推理和非推理基础能力。该流程将有助于行业开发更高性能的模型。

知识蒸馏:小型模型的性能提升

  • 研究表明大型模型的推理模式可通过知识蒸馏迁移至小型模型,其效果优于直接对小型模型进行RL训练。开源的DeepSeek-R1及其API将支持学术界开发更优秀的小型模型
  • 利用DeepSeek-R1生成的推理数据,研究团队对学术界广泛使用的多个稠密模型进行了微调。评估结果显示,经过知识蒸馏的小型dense模型在基准测试中表现优异。DeepSeek-R1-Distill-Qwen-7B在AIME 2024上达到55.5%的性能,超越QwQ-32B-Preview。DeepSeek-R1-Distill-Qwen-32B在AIME 2024、MATH-500和LiveCodeBench上分别达到72.6%、94.3%和57.2%的成绩,显著优于现有开源模型,达到与o1-mini相当的水平。研究团队已向学术界开源基于Qwen2.5和Llama3系列的1.5B、7B、8B、14B、32B和70B蒸馏检查点

研究方法

概述

传统研究主要依赖大规模监督数据提升模型性能。本研究证实,即使在无需监督微调(SFT)作为初始训练的情况下,通过大规模强化学习(RL)也能显著提升推理能力。此外,引入适量初始训练数据可进一步优化性能。后续章节将介绍:(1)DeepSeek-R1-Zero:直接对基础模型应用RL,无需任何SFT数据;(2)DeepSeek-R1基于经数千个长CoT样例微调的检查点进行RL训练;(3)将DeepSeek-R1的推理能力通过知识蒸馏迁移至小型稠密模型

DeepSeek-R1-Zero:基础模型的强化学习应用

前期相关研究表明强化学习在推理任务中具有显著效果。然而,这些研究高度依赖耗时的监督数据采集。本节探索LLM在无监督数据条件下通过纯强化学习实现推理能力自我进化的潜力。研究首先概述强化学习算法,随后展示实验结果,以期为学术界提供研究参考。

强化学习算法

群组相对策略优化(GRPO): 为优化RL训练成本,研究采用GRPO算法,摒弃了通常与策略模型规模相当的评论家模型,转而通过群组评分估计基线。具体而言,对每个问题 q ,GRPO从旧策略 πθold 采样输出组{ o1,o2,…,oG },通过最大化以下目标优化策略模型 πθ :

其中 ε 和 β 是超参数, Ai 是优势函数,使用组内每个输出对应的奖励组{ r1,r2,…,rG }计算得到:

奖励建模

奖励机制作为训练信号来源,决定RL的优化方向。DeepSeek-R1-Zero采用基于规则的双重奖励系统

  • 准确性奖励:评估响应正确性。如对确定性数学问题,要求模型以特定格式(如方框内)提供最终答案,实现基于规则的可靠验证。对LeetCode问题,则通过编译器基于预设测试用例生成反馈。
  • 格式奖励:要求模型将推理过程置于指定标签对内。研究未采用结果或过程神经奖励模型,原因在于神经奖励模型可能在大规模RL过程中产生奖励欺骗问题,且重训奖励模型需额外资源,增加训练流程复杂度。

训练模板

DeepSeek-R1-Zero的训练始于简洁指令模板的设计。

如表1所示,模板要求模型首先生成推理过程,随后给出最终答案。研究刻意将约束限定于结构格式,避免引入内容偏见(如强制反思推理或特定问题解决策略),以准确观测模型在RL过程中的自然演化。

DeepSeek-R1-Zero的性能分析、演化过程及关键突破

性能分析 图2记录了DeepSeek-R1-Zero在RL训练过程中AIME 2024基准测试的性能变化轨迹。

图2 | DeepSeek-R1-Zero训练过程中的AIME准确率变化。为确保评估稳定性,对每个问题采样16个响应并计算总体平均准确率。

数据显示,随着RL训练的深入,模型性能呈现稳定上升趋势。在AIME 2024测试中,平均pass@1得分从初始的15.6%显著提升至71.0%,达到OpenAI-o1-0912的性能水平,充分证实了RL算法在模型性能优化方面的有效性。

表2 | DeepSeek-R1-Zero与OpenAI o1模型在推理相关基准测试上的性能对比。

表2详细对比了DeepSeek-R1-Zero与OpenAI o1-0912模型在各类推理基准测试上的表现。结果表明,纯RL训练使DeepSeek-R1-Zero获得了出色的推理能力,无需借助监督微调数据,这证实了模型通过单一RL机制实现有效学习和泛化的能力。通过引入majority voting机制,模型性能得到进一步提升。例如,在AIME基准测试中,采用majority voting后性能从71.0%提升至86.7%,超越OpenAI-o1-0912。这种优异表现凸显了模型的基础能力和推理潜力。

演化过程分析 DeepSeek-R1-Zero的演化过程展示了RL在推理能力自主优化方面的显著效果。通过直接对基础模型实施RL训练,研究得以在无监督微调影响下观测模型进展。

图3 | 展示DeepSeek-R1-Zero在RL训练过程中训练集的平均响应长度变化,反映模型自主习得延长推理时间的能力。

如图3所示,模型的推理时长在训练过程中持续优化,这种进展源于模型的内生发展而非外部干预。DeepSeek-R1-Zero通过扩展测试计算时间,自然形成了解决复杂推理任务的能力。其计算规模从数百到数千个推理token不等,实现了深度的思维探索和优化。随着测试计算时间的延长,模型展现出复杂的行为特征,包括反思机制(重新评估先前推理步骤)和多元问题解决策略的探索。这些行为模式并非预设,而是源于模型与RL环境的交互作用,显著增强了其处理高难度任务的效率和准确性。

关键突破与局限性 研究过程中观察到模型出现重要突破,如表3所示,体现在中期版本中。

表3:记录DeepSeek-R1-Zero中期版本的重要突破,展示模型获得自主思考复核能力的过程,体现RL在模型能力提升方面的有效性。

此阶段,DeepSeek-R1-Zero习得了重新评估初始方法并延长思考时间的能力。这一进展不仅体现了模型推理能力的提升,也展示了RL在实现复杂学习成果方面的潜力。这种现象验证了RL的核心优势:通过适当的激励机制,促使模型自主发展高级问题解决策略。

然而,DeepSeek-R1-Zero仍存在若干局限性。尽管具备强大的推理能力和创新的推理行为,但在可读性和语言一致性方面仍面临挑战。为提高推理过程的可读性并促进开放社区交流,研究团队开发了DeepSeek-R1模型,该模型结合了RL和用户友好的初始训练数据。

DeepSeek-R1:基于冷启动的强化学习方法

基于DeepSeek-R1-Zero的成功实践,研究聚焦两个核心问题:

  1. 通过引入少量高质量数据作为冷启动,是否能够进一步提升推理性能或加速收敛?
  2. 如何开发既能生成清晰连贯的CoT,又具备强大通用能力的用户友好型模型?

为解决上述问题,研究团队设计了四阶段训练流程

冷启动机制

区别于DeepSeek-R1-Zero,DeepSeek-R1采用少量长CoT数据对模型进行预微调作为初始RL策略网络,以避免基础模型RL训练早期的不稳定性。数据收集采用多种方法:

  • 基于长CoT示例的少样本提示
  • 直接提示生成包含反思验证的详细答案
  • 整理DeepSeek-R1-Zero的规范化输出
  • 人工标注后处理优化

研究收集数千条冷启动数据用于DeepSeek-V3-Base的预训练。相较于DeepSeek-R1-Zero,冷启动数据具有以下优势:

  • 可读性增强:克服了DeepSeek-R1-Zero输出内容可读性差的局限。通过设计标准化输出模式,包括响应末尾的总结性内容,并筛除不符合阅读友好性要求的输出。输出采用|special_token|<reasoning_process>|special_token|<summary>格式,包含查询的推理过程和结果摘要。
  • 性能提升:基于人类认知模式优化的冷启动数据设计,展现出优于DeepSeek-R1-Zero的性能表现,验证了迭代训练对推理模型的优越性。

推理强化学习优化

完成冷启动数据预训练后,采用与DeepSeek-R1-Zero类似的大规模RL训练流程,重点提升模型在编码、数学、科学和逻辑等明确定义问题域的推理能力。在训练过程中发现Chain-of-Thought存在语言混杂现象,尤其是多语言提示场景下。为此引入语言一致性奖励机制,基于目标语言词占比计算。尽管消融实验显示该机制略微影响模型性能,但提升了人类使用体验。最终将任务准确率和语言一致性奖励合并计算总体奖励,持续RL训练直至模型在推理任务上收敛。

拒绝采样与监督微调

推理RL收敛后,利用检查点生成后续SFT数据。不同于专注推理的冷启动阶段,此阶段整合多领域数据以增强模型的写作、角色扮演等通用能力。具体实施如下:

推理数据构建 通过对RL训练检查点执行拒绝采样生成推理轨迹。扩展了评估机制,除规则型奖励外,引入基于DeepSeek-V3判断的生成式奖励模型。优化输出质量,过滤混杂语言、冗长段落和代码块。对每个提示词进行多样本采样,保留正确结果。最终获得约60万条推理训练样本。

非推理数据整合 在写作、事实QA、自我认知和翻译等领域,采用DeepSeek-V3流程和部分SFT数据。对复杂非推理任务,通过提示DeepSeek-V3生成前置CoT;对简单查询则直接响应。累计获取约20万条非推理训练样本。使用总计约80万样本数据对DeepSeek-V3-Base执行两轮微调。

全场景强化学习

优化人类偏好对齐,实施第二阶段RL训练,着重提升模型实用性、安全性和推理能力。采用多元奖励信号和多样化提示分布:

  • 推理数据:延续DeepSeek-R1-Zero方法,在数理逻辑领域应用规则型奖励
  • 通用数据:采用奖励模型捕捉复杂场景下的人类偏好
  • 实用性评估:专注于响应摘要,确保输出的实用性和相关性
  • 安全性保障:全面评估推理过程和摘要,识别并降低潜在风险

通过奖励信号和数据分布的系统整合,实现了推理能力和用户体验的均衡发展。

知识蒸馏:增强小型模型的推理能力

本研究采用DeepSeek-R1生成的80万训练样本,对Qwen和Llama等开源模型进行直接SFT微调,旨在将DeekSeek-R1的推理能力迁移至计算效率更高的小型模型。

实验结果表明,这种直接知识蒸馏方法显著提升小型模型的推理性能

研究选用的基础模型包括:Qwen2.5-Math-1.5B、Qwen2.5-Math-7B、Qwen2.5-14B、Qwen2.5-32B、Llama-3.1-8BLlama-3.3-70B-Instruct

选择Llama-3.3的原因在于其推理能力较Llama-3.1略有优势

蒸馏过程中仅采用SFT,未纳入RL阶段,尽管引入RL可能带来显著的性能提升。研究重点在于验证知识蒸馏技术的有效性,为后续学术界对RL优化的深入研究奠定基础。

实验设计与评估

研究采用多维度基准测试体系评估模型性能:

标准评估基准 8类16个评估标准如下所示:

  • 知识理解类:MMLU、MMLU-Redux、MMLU-Pro
  • 跨语言评估:C-Eval、CMMLU
  • 格式理解:IFEval
  • 长文本处理:FRAMES
  • 专业知识:GPQA Diamond
  • 事实问答:SimpleQA、C-SimpleQA
  • 编程能力评估: SWE-Bench Verified、Aider、LiveCodeBench、Codeforces
  • 数学能力测试: CNMO 2024、AIME 2024

除标准基准测试外,研究还使用LLM作为评估器评估模型在开放式生成任务上的表现。具体而言,遵循AlpacaEval 2.0Arena-Hard的原始配置,使用GPT-4-Turbo-1106作为成对比较的评估器。评估时仅输入最终摘要以避免长度偏差。对于蒸馏模型,报告其在AIME 2024、MATH-500、GPQA Diamond、Codeforces和LiveCodeBench上的代表性结果。

评估用prompt 不同的评估标准采用不同的prompt,具体如下所示:

  • 基础评估:采用simple evals框架标准prompt评估MMLU、DROP、GPQA Diamond和SimpleQA
  • 特殊处理: MMLU-Redux采用Zero-Eval prompt格式实现零样本评估,MMLU-Pro、C-Eval、CLUE-WSC将原少样本prompt改造为零样本形式
  • 编程评估: HumanEval-Mul覆盖8种主流编程语言,LiveCodeBench采用CoT格式,Codeforces基于10个Div.2竞赛题目与专家测试用例,SWE-Bench通过无代理框架验证

值得注意的是,DeepSeek-R1的输出在每个基准测试上限制为最多32,768个token。

基准模型 研究与多个强基准模型进行全面对比,包括DeepSeek-V3、Claude-Sonnet-3.5-1022、GPT-4o-0513、OpenAI-o1-miniOpenAI-o1-1217。鉴于在中国大陆访问OpenAI-o1-1217 API的限制,其性能数据来源于官方报告。对于蒸馏模型,额外与开源模型QwQ-32B-Preview进行比较。

生成配置 所有模型的最大生成长度设置为32K token。对需要采样的基准测试,采用0.6的temperature参数、0.95的top-p值,并为每个查询生成64个响应以估算pass@1。

DeepSeek-R1评估结果

表4 | DeepSeek-R1与其他代表性模型的比较。

在面向教育的知识基准测试(如MMLU、MMLU-Pro和GPQA Diamond)中,DeepSeek-R1相较于DeepSeek-V3展现出优越性能。这一进步主要归因于STEM相关问题准确率的提升,这得益于大规模RL带来的显著进步。

此外,DeepSeek-R1在依赖长文本理解的问答任务FRAMES上表现卓越,展示了其强大的文档分析能力。这凸显了推理模型在AI驱动的搜索和数据分析任务中的潜力

在事实性基准测试SimpleQA上,DeepSeek-R1的表现优于DeepSeek-V3,证明了其处理基于事实查询的能力。类似地,在该基准测试中也观察到OpenAI-o1超越GPT-4o的趋势。

然而,DeepSeek-R1在中文SimpleQA基准测试中的表现不如DeepSeek-V3,主要是由于安全性RL后倾向于拒绝回答某些查询。若不考虑安全性RL,DeepSeek-R1可以达到超过70%的准确率。

DeepSeek-R1在IF-Eval(一个用于评估模型遵循格式指令能力的基准测试)上也取得了令人瞩目的成果。这些改进可归因于在最终阶段的SFT和RL训练中引入了指令遵循数据。

此外,在AlpacaEval 2.0和ArenaHard上的出色表现表明DeepSeek-R1在写作任务和开放域问答方面具有优势。其显著优于DeepSeek-V3的表现凸显了大规模RL的泛化效益,不仅提升了推理能力,还改善了各个领域的性能。

而且DeepSeek-R1生成的摘要长度简洁,在ArenaHard上平均为689个token,在AlpacaEval 2.0上平均为2,218个字符。这表明DeepSeek-R1在基于GPT的评估中避免了引入长度偏差,进一步证实了其在多任务场景下的稳健性。

数学任务上,DeepSeek-R1展现出与OpenAI-o1-1217相当的性能,大幅超越其他模型。在LiveCodeBench和Codeforces等编码算法任务上也观察到类似趋势,其中注重推理的模型在这些基准测试中占据主导地位。

在面向工程的编码任务上,OpenAI-o1-1217在Aider上优于DeepSeek-R1,但在SWE Verified上表现相当。考虑到目前相关RL训练数据量仍然非常有限,研究团队认为DeepSeek-R1的工程性能将在下一版本中得到改善。

蒸馏模型评估

表5 | DeepSeek-R1蒸馏模型与其他可比模型在推理相关基准测试上的比较。

如表5所示,仅通过蒸馏DeepSeek-R1的输出,高效的DeepSeek-R1-7B(即DeepSeek-R1-Distill-Qwen-7B,以下类似缩写)就能在各方面超越GPT-4o-0513等非推理模型。

DeepSeek-R1-14B在所有评估指标上超越QwQ-32B-Preview,而DeepSeek-R1-32B和DeepSeek-R1-70B在大多数基准测试中显著超越o1-mini。这些结果展示了知识蒸馏的巨大潜力

此外,研究发现对这些蒸馏模型应用RL能带来显著的进一步提升。考虑到这值得进一步探索,此处仅呈现简单SFT蒸馏模型的结果。

讨论

蒸馏与强化学习对比

通过蒸馏DeepSeek-R1,小型模型能够取得出色的结果。然而,仍有一个问题待解答:

模型是否可以通过本文讨论的大规模RL训练而不依赖蒸馏来达到相当的性能?

为回答这个问题,研究团队对Qwen-32B-Base使用数学、代码和STEM数据进行了超过10K步的大规模RL训练,得到DeepSeek-R1-Zero-Qwen-32B。

如表6所示的实验结果表明,32B基础模型经过大规模RL训练后,达到了与QwQ-32B-Preview相当的性能。然而,从DeepSeek-R1蒸馏得到的DeepSeek-R1-Distill-Qwen-32B在所有基准测试中的表现都显著优于DeepSeek-R1-Zero-Qwen-32B

因此,可以得出两个结论:

首先,将更强大的模型蒸馏到较小的模型中可以产生优异的结果,而较小的模型依靠本文提到的大规模RL需要巨大的计算力,甚至可能无法达到蒸馏的性能水平

其次,虽然蒸馏策略既经济又有效,但要突破智能的边界可能仍需要更强大的基础模型和更大规模的强化学习

未成功的尝试

在开发DeepSeek-R1的早期阶段,研究也遇到了失败和挫折。在此分享这些失败经验以提供见解,但这并不意味着这些方法无法开发出有效的推理模型。

过程奖励模型(PRM)

PRM是一种合理的方法,可以引导模型采用更好的方法解决推理任务。然而,在实践中,PRM有三个主要限制可能阻碍其最终成功。

首先,在一般推理中明确定义细粒度步骤具有挑战性。其次,确定当前中间步骤是否正确是一项具有挑战性的任务。使用模型的自动标注可能无法产生令人满意的结果,而手动标注不利于规模化。第三,一旦引入基于模型的PRM,必然导致奖励欺骗,重新训练奖励模型需要额外的训练资源,并使整个训练流程变得复杂。

总之,虽然PRM在对模型生成的前N个响应重新排序或辅助引导搜索方面表现良好,但在实验中,相比其在大规模强化学习过程中引入的额外计算开销,其优势有限

蒙特卡洛树搜索(MCTS)

AlphaGoAlphaZero的启发,研究探索使用MCTS来增强测试时计算的可扩展性。这种方法包括将答案分解为更小的部分,使模型能够系统地探索解决方案空间。为此,提示模型生成多个标签,对应搜索所需的具体推理步骤。在训练方面,首先使用收集的提示通过预训练值模型引导的MCTS寻找答案。随后,使用产生的问答对来训练actor模型和值模型,不断改进过程。

然而,这种方法在扩大训练规模时遇到几个挑战。首先,与搜索空间相对明确的象棋不同,token生成呈现指数级更大的搜索空间。为解决这个问题,为每个节点设置最大扩展限制,但这可能导致模型陷入局部最优。其次,值模型直接影响生成质量,因为它指导搜索过程的每个步骤。训练细粒度值模型本质上是困难的,这使得模型难以迭代改进。虽然AlphaGo的核心成功依赖于训练值模型来逐步提升性能,但由于token生成的复杂性,这一原则在团队的设置中难以复制。

总之,虽然MCTS在与预训练值模型配对时可以改善推理性能,但通过自搜索迭代提升模型性能仍然是一个重大挑战

结论、局限性和未来工作

本文分享了通过RL增强模型推理能力的探索历程。DeepSeek-R1-Zero代表了一种不依赖冷启动数据的纯RL方法,在各种任务中取得了出色的表现。DeepSeek-R1通过结合冷启动数据和迭代RL微调展现出更强的性能,最终在多个任务上达到与OpenAI-o1-1217相当的水平。

研究进一步探索了将推理能力蒸馏到小型稠密模型的可能性。以DeepSeek-R1作为教师模型生成80万条数据,并对多个小型稠密模型进行微调。

结果令人鼓舞:DeepSeek-R1-Distill-Qwen-1.5B在数学基准测试中超越GPT-4o和Claude-3.5-Sonnet,在AIME上达到28.9%,在MATH上达到83.9%的成绩。其他稠密模型也取得了显著成果,大幅超越基于相同基础检查点的其他指令微调模型。

未来,计划在以下方向继续推进DeepSeek-R1的研究:

  • 通用能力:目前DeepSeek-R1在函数调用、多轮对话、复杂角色扮演和json输出等任务上的能力仍不及DeepSeek-V3。后续研究将探索如何利用长CoT增强这些领域的任务表现。
  • 语言混杂:DeepSeek-R1当前针对中文和英文进行了优化,在处理其他语言的查询时可能出现语言混杂问题。例如,即使查询使用非英文或中文的语言,DeepSeek-R1可能使用英语进行推理和响应。未来更新将着力解决这一限制。
  • 提示词工程:在评估DeepSeek-R1时发现,模型对prompt较为敏感。少样本提示会持续降低其性能。因此,建议用户直接描述问题并使用零样本设置指定输出格式以获得最佳结果
  • 软件工程任务:由于评估时间较长影响RL过程效率,大规模RL尚未在软件工程任务中广泛应用。因此,DeepSeek-R1在软件工程基准测试上相比DeepSeek-V3未显示出显著改进。未来版本将通过对软件工程数据实施拒绝采样或在RL过程中引入异步评估来提高效率。

LLM训练-人工强化反馈对齐算法:RLHF, RLAIF, PPO, DPO and More

参考论文:A Comprehensive Survey of LLM Alignment Techniques: RLHF, RLAIF, PPO, DPO and More

相关博客:https://wqw547243068.github.io/rlhf

参考代码:Align Anything: Training All-modality Model with Feedback

端到端推理学习增强推理能力方法

a.推理时扩展(Inference-time scaling): 如链式思维(CoT)或自我一致性(Self-Consistency),以增强模型的推理能力;【cot:核心思想是将复杂问题分解为一系列可解释的中间步骤。通过明确的推理链条,模型能够逐步解决原本可能超出其直接推理能力的问题。思维链方法特别适用于涉及多步骤推理的任务,如数学题、多重逻辑推理问题等。 Self-Consistency 自我一致提示是在 CoT 基础上进一步优化,通过采样多条推理路径,找出最一致的答案。它适用于对结果准确性要求更高的场景,避免一次性推理路径的偶然性导致错误。】

b.纯强化学习(Pure Reinforcement Learning, RL): 通过强化学习训练模型,使其在没有监督数据的情况下,通过试错学习复杂任务; 【deepseek-R1-zero】

c.监督微调结合强化学习(SFT + RL): 首先对模型进行监督微调,然后使用强化学习进行进一步优化,以提高模型的推理能力。【deepseek-R1】

d.纯监督微调和蒸馏(Pure Supervised Fine-Tuning and Distillation)仅使用监督学习和模型蒸馏技术来增强模型的推理能力。【deepseek-R1-distill蒸馏模型

一个完整的LLM训练过程包含以下几步:

  • Model Initialization:加载模型和处理器
  • 数据准备:解析数据集并设置其格式
  • 模型推理:将数据输入到模型中并获取输出
  • 梯度更新:根据损失函数更新模型参数

对齐(alignment)其作用就是让 LLM 与人类的价值观保持一致。在对齐 LLM 方面,基于人类反馈的强化学习(RLHF)是一种突破性的技术。该方法催生了 GPT-4、Claude 和 Gemini 等强大模型。RLHF 之后,人们也探索了多种多样的对齐 LLM 的方法。但是,此前还没有人全面总结对齐 LLM 与人类偏好的方法。

Salesforce 决定填补这一空白,于近日发布了一份 37 页的综述报告,其中按类别总结了现有的研究文献,并详细分析了各篇论文。

Introduction

这篇论文分为四大主题:奖励模型、反馈、强化学习(RL)、优化。每个主题又包含进一步的子主题,如图 1 所示。

xPO LLM 与人类偏好保持一致的 13 个分类方向

奖励模型的子主题包括:1. 显式奖励模型与隐式奖励模型;2. 逐点奖励模型与偏好模型;3. 响应层面的奖励与 token 层面的奖励;4. 负偏好优化。

反馈的子主题包括:1. 偏好反馈与二元反馈;2. 成对反馈与列表反馈;3. 人类反馈与 AI 反馈。

强化学习的子主题包括:1. 基于参考的强化学习与无参考的强化学习;2. 长度控制式强化学习;3. 强化学习中的不同分支;4. 在线策略强化学习与离线策略强化学习

优化的子主题包括:1. 在线 / 迭代式偏好优化与离线 / 非迭代式偏好优化;2. 分离 SFT 和对齐与合并 SFT 和对齐。

Individual Paper Reviews in Detail

1. RLHF/PPO

LLM 的预训练要用到大量来自不同来源的语料库,而这本身就无法确保这些数据集的质量。此外,LLM 的主要目标是预测下一个 token,这个目标与「有用且安全地遵从用户指令」的目标并不一致。因此,LLM 可能会输出不真实、有害或对用户无用的内容。本质上讲,这些模型并未与用户意图对齐。RLHF/PPO 的主要目标是在各种任务上对齐语言模型与用户意图,其做法是使用人类反馈来微调模型。有关这个主题的研究有很多。

2. RLAIF

获取人类偏好数据集的成本不低,因此基于人工智能反馈的强化学习(RLAIF)诞生了。此外,随着 LLM 的能力不断进步,所能收集到的 AI 偏好数据集的质量也不断提高,由此可提升 LLM 的对齐效果。

3.直接人类偏好优化

传统 RLHF 方法通常涉及到优化源自人类偏好的奖励函数。该方法虽有效,但也可能带来一些难题,比如增大计算复杂度以及在估计和优化奖励时需要考虑偏置 – 方差权衡。参阅论文《High-dimensional continuous control using generalized advantage estimation》。

近期有研究探索了其它一些旨在根据人类偏好(无需依赖某个标量的奖励信号)来直接优化 LLM 策略的方法。

这些方法的目标是通过更直接地使用偏好数据来简化对齐流程、降低计算开销以及实现更稳健的优化。通过将该问题描述为一个偏好优化问题,而不是奖励估计和最大化问题,这些方法能提供一种将语言模型与人类判断对齐的不同视角

4.token 级 DPO

使用 DPO 时,奖励会被一起分配给 prompt 和响应。相反,使用 MDP 时,奖励会被分配给各个动作。后续的两篇论文在 token 层面阐述了 DPO 并将其应用扩展到了 token 级的分析。

5.迭代式 / 在线 DPO

使用 DPO 时,会使用所有可用的偏好数据集来对齐 LLM。为了持续提升 LLM,应当实现迭代式 / 在线 DPO。这就引出了一个有趣的问题:如何高效地收集新的偏好数据集。下面两篇论文深入探讨了这一主题。

  • 自我奖励式语言模型,参阅论文《Self-rewarding language models》。
  • CRINGE,参阅论文《The cringe loss: Learning what language not to model》。

6.二元反馈

事实证明,收集偏好反馈比收集二元反馈(比如点赞或点踩)的难度大,因此后者可促进对齐过程的扩展。KTO 和 DRO 这两项研究关注的便是使用二元反馈来对齐 LLM。

  • KTO,Kahneman-Tversky 优化,参阅论文《KTO: Model alignment as prospect theoretic optimization》。
  • DRO,直接奖励优化,参阅论文《Offline regularised reinforcement learning for large language models alignment》。

7.融合 SFT 和对齐

之前的研究主要还是按顺序执行 SFT 和对齐,但事实证明这种方法很费力,并会导致灾难性遗忘。后续的研究有两个方向:一是将这两个过程整合成单一步骤;二是并行地微调两个模型,最终再进行融合。

  • ORPO,比值比偏好优化,参阅论文《ORPO: Monolithic preference optimization without reference model》。
  • PAFT,并行微调,参阅论文《PAFT: A parallel training paradigm for effective llm fine-tuning》。

8.长度控制式 DPO 和无参考 DPO

之前有研究表明,LLM 的输出往往过于冗长。为了解决这个问题,R-DPO 和 SimPO 的关注重心是在不影响生成性能的前提下实现对响应长度的控制。

此外,DPO 必需参考策略来确保已对齐模型不会与参考模型有太大偏差。相较之下,SimPO 和 RLOO 提出了一些方法,可以在不影响 LLM 效果的情况下消除对参考模型的需求

9.逐列表的偏好优化

之前在 PPO 和 DPO 方面的研究关注的是成对偏好,而 RLHF 方面的研究则是收集逐列表的偏好来加速数据收集过程,之后再将它们转换成成对偏好。尽管如此,为了提升 LLM 的性能,直接使用逐列表的数据集来执行偏好优化是可行的。以下三篇论文专门讨论了这种方法。

  • LiPO,逐列表偏好优化,参阅论文《LIPO: Listwise preference optimization through learning-to-rank》。
  • RRHF,参阅论文《RRHF: Rank responses to align language models with human feedback without tears》。
  • PRO,偏好排名优化,参阅论文《Preference ranking optimization for human alignment》。

10.负偏好优化

这些研究有一个共同前提:当前这一代 LLM 已经在翻译和总结等任务上超越了人类性能。因此,可以将 LLM 的输出视为期望响应,而无需依靠将人类标注的数据视为偏好响应;这样做是有好处的。反过来,不期望得到的响应依然也可被用于对齐 LLM,这个过程就是所谓的负偏好优化(NPO)。

  • NN,否定负例方法,参阅论文《Negating negatives: Alignment without human positive samples via distributional dispreference optimization》。
  • NPO,负例偏好优化,参阅论文《Negative preference optimization: From catastrophic collapse to effective unlearning》。
  • CPO,对比偏好优化,参阅论文《Contrastive preference optimization: Pushing the boundaries of llm performance in machine translation》。

11.纳什学习

之前的研究通常是使用逐点奖励和 BT 模型来得到成对偏好。但是,这种方法比不上直接成对偏好建模并且无法解决成对偏好中的不一致问题。为了克服这些局限,一些研究提出了纳什学习方法。

  • 根据人类反馈的纳什学习,参阅论文《Nash learning from human feedback》。
  • SPPO,自博弈偏好优化,参阅论文《A minimaximalist approach to reinforcement learning from human feedback》。
  • DNO,直接纳什优化,参阅论文《Direct nash optimization: Teaching language models to self-improve with general preferences》。

LLM 对齐(Alignment)方法:SFT、PPO、DPO 、ORPO详细介绍

LLM(大语言模型)的对齐(Alignment)方法旨在让 AI 的输出更加符合人类预期,减少错误信息、有害内容或不准确的回答。主要总结LLM训练中的基本的对齐算法, 监督微调 (SFT)、直接偏好优化 (DPO) 和近端策略优化 (PPO)等。

SFT(Supervised Fine-Tuning,监督微调)

监督微调(SFT)是 LLM 训练中的第一步,通过高质量的人工标注数据集对模型进行微调,使其具备基础的任务能力。SFT 是所有对齐方法的基础,如 RLHF、DPO 等都依赖于一个经过 SFT 训练的模型作为初始状态。

过程

  1. 数据准备:收集高质量的指令-响应(Instruction-Response)数据集,例如人类标注的数据或合成的数据。
  2. 模型微调:使用交叉熵损失(Cross-Entropy Loss)训练模型,使其学习提供与标注数据匹配的答案。
  3. 效果:使模型在常见任务(如问答、代码生成、对话等)中表现更好,提高其对指令的遵循能力。

给定输入 x(Prompt) 和目标输出 y(Response),模型的目标是最大化生成目标文本的概率:

其中:

  • Pθ​(yt​∣x,y<t​) 是 LLM 在给定上下文下预测下一个 token yt​ 的概率。
  • 训练时采用交叉熵损失(Cross Entropy Loss)来优化模型参数 θ。

SFT 仅依赖于人工标注数据,无法让模型学习偏好信息(比如不同回答的优劣)。无法动态调整:SFT 训练后,模型固定,难以针对用户反馈进行调整。缺乏探索性:模型只会学到训练数据中的模式,无法进行强化学习优化。

DPO(Direct Preference Optimization,直接偏好优化)

论文:https://arxiv.org/abs/2305.18290

参考代码:https://github.com/eric-mitchell/direct-preference-optimization

DPO(直接偏好优化)是一种比 RLHF 更简单、更高效的对齐方法。
它不需要训练奖励模型(RM)和使用强化学习(RL),而是直接优化 LLM,使其更符合人类偏好数据

偏好数据

  • 每个输入 Prompt 对应两个候选回答:一个优选(Preferred y+),一个劣选(Dispreferred y−)。
  • 例如:

Prompt: “如何写一封正式的电子邮件?”
Response 1 (优选): “在邮件中应保持正式语气,并包含称呼、正文和署名。”
Response 2 (劣选): “随便写就行了,不要太在意格式。”

优化 LLM 使其更倾向于优选回答

只需要加载2个相同的模型,其中一个推理[reference model:old策略模型],另外一个模型[policy model 策略模型]训练,直接在偏好数据上进行训练即可:

Reference Model(以下简称Ref模型)一般也用SFT阶段得到的SFT模型做初始化,在训练过程中,它的参数是冻结的。Ref模型的主要作用是防止Actor”训歪”

损失函数: DPO 直接优化模型输出的偏好分布:

其中:

  • σ :sigmoid函数
  • β :超参数,一般在0.1 – 0.5之间
  • yw :某条偏好数据中好的response,w就是win的意思
  • yl :某条偏好数据中差的response,l就是loss的意思,所以偏好数据也叫comparision data
  • πθ(yw|x) :给定输入x, 当前 策略policy model生成好的response的累积概率(每个tokne的概率求和,具体看代码)
  • πref(yl|x) :给定输入x, 原始模型(reference model)生成坏的response的累积概率

开始训练时,reference model和policy model都是同一个模型,只不过在训练过程中reference model不会更新权重。

为了方便分析,我们把log里的分式展开,然后β设为1,并且暂时不看前面的log_sigmoid,那么上面的loss可以简化为:

由于最初loss前面是有个负号的,所以优化目标是让本简化公式最大,即我们希望左半部分和右半部分的margin越大越好,左半部分的含义是good response相较于没训练之前的累积概率差值,右半部分代表bad response相较于没训练之前的累计概率差值,如果这个差值,即margin变大了,就意味着:

  • 1)左边变大,右边变小,理想情况,good response概率提升,bad response概率下降
  • 2)左边变小,右边更小,good response概率下降,但是bad response概率下降的更多,生成的时候还是倾向于good response
  • 3)左边变的更大,右边只大了一点点,和2)同理

所以这个loss颇有一种对比的感觉。

PPO(Proximal Policy Optimization,近端策略优化)

论文:https://arxiv.org/abs/1707.06347

详解:https://zhuanlan.zhihu.com/p/677607581

人人都能看懂的RL-PPO理论知识

是OpenAI在2017提出的一种强化学习算法,是基于策略优化的算法,用于训练能够最大化累积奖励的智能体。PPO算法通过在每次更新时限制新策略与旧策略之间的差异,从而更稳定地更新策略参数。这种方法有助于避免训练过程中出现的不稳定性和剧烈波动,使得算法更容易收敛并学习到更好的策略。

强化学习基本概念

  • 强化学习的两个实体:智能体(Agent)环境(Environment)
  • 强化学习中两个实体的交互:
    • 状态空间S:S即为State,指环境中所有可能状态的集合
    • 动作空间A:A即为Action,指智能体所有可能动作的集合
    • 奖励R:R即为Reward,指智能体在环境的某一状态下所获得的奖励。

以上图为例,智能体与环境的交互过程如下:

  • 在 t 时刻,环境的状态为 St ,达到这一状态所获得的奖励为 Rt
  • 智能体观测到 St 与 Rt ,采取相应动作 At
  • 智能体采取 At 后,环境状态变为 St+1 ,得到相应的奖励 Rt+1

奖励值 Rt ,它表示环境进入状态 St 下的即时奖励
但如果只考虑即时奖励,目光似乎太短浅了:当下的状态和动作会影响到未来的状态和动作,进而影响到未来的整体收益。
所以,一种更好的设计方式是:t时刻状态s的总收益 = 身处状态s能带来的即时收益 + 从状态s出发后能带来的未来收益写成表达式就是:Vt=Rt+γVt+1

其中:

  • Vt : t 时刻的总收益,注意这个收益蕴涵了“即时”和“未来”的概念
  • Rt : t 时刻的即时收益
  • Vt+1 : t+1 时刻的总收益,注意这个收益蕴涵了“即时”和“未来”的概念。而 Vt+1 对 Vt 来说就是“未来”。
  • γ :折扣因子。它决定了我们在多大程度上考虑将“未来收益”纳入“当下收益”。

关键概念:

策略函数是一个 概率密度函数(PDF),输入时当前状态s,输出为一个概率分布,表征每个 action 的概率:

动作值函数:评价在状态 st 的情况下做出动作 at的好坏程度。

状态值函数:

  • 消掉了动作 A ,这样 Vπ 只跟状态 s 与策略函数 π 有关了。
  • 给定 π,可以评价当前状态的好坏;给定状态st,可以评价策略 π的好坏。

优势函数:有些时候我们不需要描述一个行动的绝对好坏,而只需要知道它相对于平均水平的优势。也就是说,我们只想知道一个行动的相对 优势 。这就是优势函数的概念。

一个服从策略 \pi 的优势函数,描述的是它在状态 s 下采取行为 a 比随机选择一个行为好多少(假设之后一直服从策略 \pi )。数学角度上,优势函数的定义为:

长期价值可以表示为状态值函数(State Value Function)或动作值函数(Action Value Function)。

优化方法:

value-based:优化状态动作值函数Q(s) ,贪心选择(确定性策略) :Q-Learning

policy-based :直接优化策略函数π(s, a),按概率采样(随机性策略) :REINFORCE

Actor-Critic •融合上述方法,同时优化Q与π:TRPO、PPO

NLP中的强化学习

在第一部分介绍了通用强化学习的流程,那么我们要怎么把这个流程对应到NLP任务中呢?换句话说,NLP任务中的智能体、环境、状态、动作等等,都是指什么呢?

回想一下我们对NLP任务做强化学习(RLHF)的目的:我们希望给模型一个prompt,让模型能生成符合人类喜好的response。再回想一下gpt模型做推理的过程:每个时刻 t 只产生一个token,即token是一个一个蹦出来的,先有上一个token,再有下一个token。


复习了这两点,现在我们可以更好解读上面这张图了:

  • 我们先喂给模型一个prompt,期望它能产出符合人类喜好的response
  • 在 t 时刻,模型根据上文,产出一个token,这个token即对应着强化学习中的动作,我们记为At 。因此不难理解,在NLP语境下,强化学习任务的动作空间就对应着词表。
  • 在 t 时刻,模型产出token At对应着的即时收益为Rt,总收益为Vt复习一下, Vt 蕴含着“即时收益”与“未来收益”两个内容)。这个收益即可以理解为“对人类喜好的衡量”。此刻,模型的状态从St变为St+1,也就是从“上文”变成“上文 + 新产出的token”
  • 在NLP语境下,智能体是语言模型本身,环境则对应着它产出的语料

这样,我们就大致解释了NLP语境下的强化学习框架,不过针对上面这张图,你可能还有以下问题:


(1)问题1:图中的下标是不是写得不太对?例如根据第一部分的介绍, At 应该对应着 Rt+1  At+1 应该对应着 Rt+2 ,以此类推?
答:你说的对。但这里我们不用太纠结下标的问题,只需要记住在对应的response token位置,会产生相应的即时奖励和总收益即可。之所以用图中这样的下标,是更方便我们后续理解代码。


(2)问题2:我知道 At 肯定是由语言模型产生的,那么 ,Rt,Vt 是怎么来的呢,也是语言模型产生的吗?
答:先直接说结论, At 是由我们的语言模型产生的, ,Rt,Vt 则分别由另外两个模型来产生,在后文中我们会细说。


(3)问题3:语言模型的参数在什么时候更新?是观测到一个 Rt,Vt ,就更新一次参数,然后再去产生 At+1 吗?
答:当然不是。你只看到某个时刻的收益,就急着用它更新模型,这也太莽撞了。我们肯定是要等有足够的观测数据了(例如等模型把完整的response生成完),再去更新它的参数。这一点我们也放在后文细说。


(4)问题4:再谈谈 Rt,Vt 吧,在NLP的语境下我还是不太理解它们
答:

  • 首先,“收益”的含义是“对人类喜好的衡量”
  • Rt :即时收益,指语言模型当下产生token At 带来的收益
  • Vt : 实际期望总收益(即时+未来),指对语言模型“当下产生token At ,一直到整个response生产结束”后的期收益预估。因为当下语言模型还没产出 At 后的token,所以我们只是对它之后一系列动作的收益做了估计,因而称为“期望总收益”。

RLHF中的四个重要角色:

我们从第二部分中已经知道:生成token At 和对应收益 Rt,Vt 的并不是一个模型。那么在RLHF中到底有几个模型?他们是怎么配合做训练的?而我们最终要的是哪个模型?

如上图,在RLHF-PPO阶段,一共有四个主要模型,分别是:

  • Actor Model:演员模型,这就是我们想要训练的目标语言模型
  • Critic Model:评论家模型,它的作用是预估总收益 Vt
  • Reward Model:奖励模型,它的作用是计算即时收益 Rt
  • Reference Model:参考模型,它的作用是在RLHF阶段给语言模型增加一些“约束”,防止语言模型训歪(朝不受控制的方向更新,效果可能越来越差)

其中:

  • Actor/Critic Model在RLHF阶段是需要训练的(图中给这两个模型加了粗边,就是表示这个含义);而Reward/Reference Model参数冻结的。
  • Critic/Reward/Reference Model共同组成了一个“奖励-loss”计算体系(我自己命名的,为了方便理解),我们综合它们的结果计算loss,用于更新Actor和Critic Model

Actor Model (演员模型)

正如前文所说,Actor就是我们想要训练的目标语言模型。我们一般用SFT阶段产出的SFT模型来对它做初始化。

我们的最终目的是让Actor模型能产生符合人类喜好的response。所以我们的策略是,先喂给Actor一条prompt (这里假设batch_size = 1,所以是1条prompt),让它生成对应的response。然后,我们再将“prompt + response”送入我们的“奖励-loss”计算体系中去算得最后的loss,用于更新actor。

Reference Model(参考模型)

Reference Model(以下简称Ref模型)一般也用SFT阶段得到的SFT模型做初始化,在训练过程中,它的参数是冻结的。Ref模型的主要作用是防止Actor”训歪”,那么它具体是怎么做到这一点的呢?

“防止模型训歪”换一个更详细的解释是:我们希望训练出来的Actor模型既能达到符合人类喜好的目的,又尽量让它和SFT模型不要差异太大。简言之,我们希望两个模型的输出分布尽量相似。那什么指标能用来衡量输出分布的相似度呢?我们自然而然想到了KL散度

如图所示:

  • 对Actor模型,我们喂给它一个prompt,它正常输出对应的response。那么response中每一个token肯定有它对应的log_prob结果呀,我们把这样的结果记为log_probs
  • 对Ref模型,我们把Actor生成的”prompt + response”喂给它,那么它同样能给出每个token的log_prob结果,我们记其为ref_log_probs
  • 那么这两个模型的输出分布相似度就可以用ref_log_probs - log_probs来衡量,我们可以从两个方面来理解这个公式:
    • 从直觉上理解,两个分布的相似度越高,说明Ref模型对Actor模型输出的肯定性越大。即Ref模型也认为,对于某个 St ,输出某个 At 的概率也很高( P(At|St) )。这时可以认为Actor模型较Ref模型没有训歪。
    • 从KL散度上理解

(当然这里不是严格的等于,只是KL散度的近似),这个值越小意味着两个分布的相似性越高。

注:你可能已经注意到,按照KL散度的定义,这里写成log_probs - ref_log_probs更合适一些。但是如果你看过一些rlhf相关的论文的话,你可能记得在计算损失函数时,有一项Rt−KL散度 (对这个有疑惑不要紧,我们马上在后文细说),即KL散度前带了负号,所以这里我写成ref_log_probs - log_probs这样的形式,更方便大家从直觉上理解这个公式。

现在,我们已经知道怎么利用Ref模型和KL散度来防止Actor训歪了。KL散度将在后续被用于loss的计算,我们在后文中会详细解释。

Critic Model(评论家模型)

Critic Model用于预测期望总收益 Vt ,和Actor模型一样,它需要做参数更新。实践中,Critic Model的设计和初始化方式也有很多种,例如和Actor共享部分参数、从RW阶段的Reward Model初始化而来等等。我们讲解时,和deepspeed-chat的实现保持一致:从RW阶段的Reward Model初始化而来。


你可能想问:训练Actor模型我能理解,但我还是不明白,为什么要单独训练一个Critic模型用于预测收益呢?
这是因为,当我们在前文讨论总收益 Vt (即时 + 未来)时,我们是站在上帝视角的,也就是这个 Vt 就是客观存在的、真正的总收益。但是我们在训练模型时,就没有这个上帝视角加成了,也就是在 t 时刻,我们给不出客观存在的总收益 Vt ,我们只能训练一个模型去预测它。


所以总结来说,在RLHF中,我们不仅要训练模型生成符合人类喜好的内容的能力(Actor),也要提升模型对人类喜好量化判断的能力(Critic)。这就是Critic模型存在的意义。我们来看看它的大致架构:

deepspeed-chat采用了Reward模型作为它的初始化,所以这里我们也按Reward模型的架构来简单画画它。你可以简单理解成,Reward/Critic模型和Actor模型的架构是很相似的(毕竟输入都一样),同时,它在最后一层增加了一个Value Head层,该层是个简单的线形层,用于将原始输出结果映射成单一的 Vt 值。

在图中, Vt 表示Critic模型对 t 时刻及未来(response完成)的收益预估。

 Reward Model(奖励模型)

Reward Model用于计算生成token At 的即时收益,它就是RW阶段所训练的奖励模型,在RLHF过程中,它的参数是冻结的。


你可能想问:为什么Critic模型要参与训练,而同样是和收益相关的Reward模型的参数就可以冻结呢?
这是因为,Reward模型是站在上帝视角的。这个上帝视角有两层含义:

  • 第一点,Reward模型是经过和“估算收益”相关的训练的,因此在RLHF阶段它可以直接被当作一个能产生客观值的模型。
  • 第二点,Reward模型代表的含义就是“即时收益”,你的token At 已经产生,因此即时收益自然可以立刻算出。

你还可能想问:我已经用Critic预测出 Vt 了,而这个 Vt 包含了“即时”和“未来”的概念,那我还需要代表“即时”的 Rt 做什么呢?直接用 Vt 不就好了吗?


为了解答这个问题,我们先回顾下1.2部分中给出的价值函数: Vt=Rt+γVt+1
这个函数告诉我们,我们当前可以用两个结果来表示 t 时刻的总收益:

  • 结果1:Critic模型预测的 Vt
  • 结果2:Reward模型预测的 Rt 和critic模型预测的 Vt+1

那么哪一个结果更靠近上帝视角给出的客观值呢?当然是结果2,因为结果1全靠预测,而结果2中的 Rt 是事实数据。
我们知道Critic模型也是参与参数更新的,我们可以用MSE(上帝视角的客观收益-Critic模型预测的收益)来衡量它的loss。但是上帝视角的客观收益我们是不知道的,只能用已知事实数据去逼近它,所以我们就用 Rt+γ∗Vt+1 来做近似。这就是 Rt,Vt 同时存在的意义

Reward模型和critic模型非常相似,这里我们就只给出架构图,不再做过多的说明。

RLHF中的loss计算

到目前为止,我们已经基本了解了RLHF的训练框架,以及其中的四个重要角色(训练一个RLHF,有4个模型在硬件上跑,可想而知对存储的压力)。在本节中,我们一起来解读RLHF的loss计算方式。在解读中,我们会再一次理一遍RLHF的整体训练过程,填补相关细节。在这之后,我们就可以来看代码解析了。


在第三部分的讲解中,我们知道Actor和Critic模型都会做参数更新,所以我们的loss也分成2个:

  • Actor loss:用于评估Actor是否产生了符合人类喜好的结果,将作用于Actor的BWD上。
  • Critic loss:用于评估Critic是否正确预测了人类的喜好,将作用于Critic的BWD上。

我们详细来看这两者。

Actor loss

(1)直观设计

我们先来看一个直观的loss设计方式:

  • Actor接收到当前上文 St ,产出token At ( P(At|St) )
  • Critic根据 St,At ,产出对总收益的预测 Vt
  • 那么Actor loss可以设计为: 

求和符号表示我们只考虑response部分所有token的loss,为了表达简便,我们先把这个求和符号略去(下文也是同理),也就是说:

我们希望minimize这个actor_loss。


这个设计的直观解释是:

  • 当 Vt>0 时,意味着Critic对Actor当前采取的动作给了正向反馈,因此我们就需要在训练迭代中提高 P(At|St) ,这样就能达到减小loss的作用。
  • 当 Vt<0 时,意味着Critic对Actor当前采取的动作给了负向反馈,因此我们就需要在训练迭代中降低 P(At|St) ,这样就能到达到减小loss的作用。

一句话总结:这个loss设计的含义是,对上文 St 而言,如果token At 产生的收益较高,那就增大它出现的概率,否则降低它出现的概率。

(2)引入优势(Advantage)

在开始讲解之前,我们举个小例子:
假设在王者中,中路想支援发育路,这时中路有两种选择:1. 走自家野区。2. 走大龙路。
中路选择走大龙路,当她做出这个决定后,Critic告诉她可以收1个人头。结果,此刻对面打野正在自家采灵芝,对面也没有什么苟草英雄,中路一路直上,最终收割2个人头。
因为实际收割的人头比预期要多1个,中路尝到了甜头,所以她增大了“支援发育路走大龙路”的概率。
这个多出来的“甜头”,就叫做“优势”(Advantage)。


对NLP任务来说,如果Critic对 At 的总收益预测为 Vt ,但实际执行 At 后的总收益是 Rt+γ∗Vt+1 ,我们就定义优势为:

我们用 Advt 替换掉 Vt ,则此刻actor_loss变为:

(3)重新设计 Rt

总结一下,到目前为止,我们的actor_loss形式为:

同时注意,这个actor_loss应该是response的所有token loss的sum或者avg。这里为了表达方便,我们的公式略去了求和或求平均的符号。

按照这个理解, Rt 应该表示每个Actor产出token At 带来的即时收益,正如下图所示(其中 T 表示最后一个时刻):

但在deepspeed-chat的RLHF实践中,对 Rt 做了另一种设计:

基于这些,上面这个对 Rt 的设计可理解成:

  • t≠T时,我们更加关心Actor是否有在Ref的约束下生产token At
  • $ t=T时,我们不仅关心Actor是否遵从了Ref的约束,也关心真正的即时收益Rt


为什么只有最后一个时刻的 Rt 被纳入了考量呢?这是因为在Reward模型训练阶段,就是用这个位置的 Rt 来表示对完整的prompt + response的奖励预测(但不妨碍你理解成是执行完 AT 的即时奖励),然后用这个指标来做模型eval的(但是Reward训练阶段算loss时,还是考虑了response部分所有token输出的reward值)。所以到了RLHF的场景下,其余时刻的即时奖励,我们就用“Actor是否遵循了Ref的约束”来进行评价。


需要注意的是, Rt 的设计并不只有这一种。deepspeed在自己的代码注释中也有提过,可以尝试把最后一个时刻的 RT 替换成所有token的即时奖励的平均值。如果站在这个角度理解的话,我们同样也可以尝试在每一个位置的奖励衡量上引入 Rt 。

代码实践如下:

def compute_rewards(self, prompts, log_probs, ref_log_probs, reward_score,
                        action_mask):
        """
        reward_function:计算最终的reward分数
        复习一下几个相关参数的默认值:
        self.kl_ctl = 0.1
        self.clip_reward_value = 5
        
        对于batch中的某个prompt来说,它最终的reward分数为:
        (1) 先计算actor和ref_model的logit相似度: -self.kl_ctl * (log_probs - ref_log_probs)
            其实写成self.kl_ctl * (ref_log_probs - log_probs)更好理解些
            这个值越大,说明ref_model对actor生成的结果的认可度越高(即表明rlhf没有训歪),
            没有训歪的情况下我们也应该给模型一些奖励,这个奖励就是self.kl_ctl * (ref_log_probs - log_probs)
            
        (2)由于我们只取最后一个token对应位置的分数作为reward_score,因此我们只需要:
            self.kl_ctl * (ref_log_probs - log_probs)的最后一位 + reward_score
         
         (3) 同时我们对reward_score也做了大小限制,最大不超过self.clip_reward_value(超过统一给成self.clip_reward_value),
             最小不低于-self.clip_reward_value(低于统一给成-self.clip_reward_value)
        
         (4) 最后返回的rewards大小为:(batch_size, 各条数据的长度),对batch中的每条数据来说:
             - response的最后一位:self.kl_ctl * (ref_log_probs - log_probs)的最后一位 + reward_score
             - response的其余位置:self.kl_ctl * (ref_log_probs - log_probs)
        
        """

        kl_divergence_estimate = -self.kl_ctl * (log_probs - ref_log_probs)
        rewards = kl_divergence_estimate
        # ---------------------------------------------------------------------------------------------------
        # response开始的位置
        # (因为我们对prompt做过padding处理,因此batch中每个prompt长度一致,也就意味着每个response开始的位置一致)
        # (所以这里start是不加s的,只是一个int)
        # ---------------------------------------------------------------------------------------------------
        start = prompts.shape[1] - 1
        # ---------------------------------------------------------------------------------------------------
        # response结束的位置
        # (因为一个batch中,每个response的长度不一样,所以response的结束位置也不一样)
        # (所以这里end是加s的,ends的尺寸是(batch_size,)
        # ---------------------------------------------------------------------------------------------------
        ends = start + action_mask[:, start:].sum(1) + 1
        # ---------------------------------------------------------------------------------------------------
        # 对rewards_score做限制
        # ---------------------------------------------------------------------------------------------------
        reward_clip = torch.clamp(reward_score, -self.clip_reward_value,
                                  self.clip_reward_value)
        batch_size = log_probs.shape[0]
        for j in range(batch_size):
            rewards[j, start:ends[j]][-1] += reward_clip[j] # 

        return rewards

(4)重新设计优势

好,再总结一下,目前为止我们的actor_loss为:

同时,我们对 Rt 进行来改造,使其能够衡量Actor模型是否遵从了Ref模型的约束。
现在我们把改造焦点放在 Advt 上,回想一下,既然对于收益而言,分为即时和未来,那么对于优势而言,是不是也能引入对未来优势的考量呢?这样,我们就可以把 Advt 改写成如下形式:


(熟悉强化学习的朋友应该能一眼看出这是GAE,这里我们不打算做复杂的介绍,一切都站在直觉的角度理解)
其中,新引入的 λ 也是一个常量,可将其理解为权衡因子,直觉上看它控制了在计算当前优势时对未来优势的考量。(从强化学习的角度上,它控制了优势估计的方差和偏差)

看到这里,你可能想问:这个代表未来优势的 Advt+1 ,我要怎么算呢?
注意到,对于最后一个时刻 t ,它的未来收益( VT+1 )和未来优势( AdvT+1 )都是0,也就是 AdvT=RT−VT ,这是可以直接算出来的。而有了 AdvT ,我们不就能从后往前,通过动态规划的方法,把所有时刻的优势都依次算出来了吗?

代码实践如下(其中返回值中的returns表示实际收益,将被用于计算Critic模型的loss,可以参见4.2,其余细节都在代码注释中):

 def get_advantages_and_returns(self, values, rewards, start):
        """
        Adopted from https://github.com/CarperAI/trlx/blob/main/trlx/models/modeling_ppo.py#L134
        
        没有引入GAE前的t时刻的优势值:
        detal_t = r_t + gamma * V_t+1 - V_t
        其中:
            - r_t表示t时刻的即时收益
            - V_t+1表示未来时刻的预期收益
            - r_t + gamma * V_t+1可理解成t时刻的实际预期收益
            - V_t可理解成t时刻的预估预期收益(是模型,例如critic model自己估算出来的)
        
        引入GAE后的t时刻的优势值:
        A_t = delta_t + gamma * lambda * A_t+1
        粗暴理解为在t时刻时,不仅考虑当下优势,还考虑了未来的优势
        为了知道A_t, 我们得知道A_t+1,所以在本算法中采取了从后往前做动态规划求解的方法,也即:
        假设T是最后一个时刻,则有A_T+1 = 0, 所以有: A_T = delta_T
        知道了A_T, 就可以依次往前倒推,把A_t-1, A_t-2之类都算出来了
        
        引入GAE后t时刻的实际预期收益
        returns_t = A_t + V_t
                  = delta_t + gamma * lambda * A_t+1 + V_t
                  = r_t + gamma * V_t+1 - V_t + gamma * lambda * A_t+1 + V_t
                  = r_t + gamma * (V_t+1 + lambda * A_t+1)
        
        注意,这里不管是advantages还是returns,都只算response的部分
        """
        
        # Adopted from https://github.com/CarperAI/trlx/blob/main/trlx/models/modeling_ppo.py#L134
        lastgaelam = 0
        advantages_reversed = []
        length = rewards.size()[-1]
        # 注意这里用了reversed,是采取从后往前倒推计算的方式
        for t in reversed(range(start, length)):
            nextvalues = values[:, t + 1] if t < length - 1 else 0.0
            delta = rewards[:, t] + self.gamma * nextvalues - values[:, t]
            lastgaelam = delta + self.gamma * self.lam * lastgaelam
            advantages_reversed.append(lastgaelam)
        advantages = torch.stack(advantages_reversed[::-1], dim=1) # 优势
        returns = advantages + values[:, start:] # 实际收益
        # values: 预期收益
        return advantages.detach(), returns

(5)PPO-epoch: 引入新约束

总结一下,目前为止我们的actor_loss为:

同时

  • 我们已经对Rt进行来改造,使其能够衡量Actor模型是否遵从了Ref模型的约束。
  • 我们已经对Advt进行改造,使其不仅考虑了当前时刻的优势,还考虑了未来的优势

基于这些改造,我们重新理一遍RLHF-PPO的训练过程。

  • 第一步,我们准备一个batch的prompts
  • 第二步,我们将这个batch的prompts喂给Actor模型,让它生成对应的responses
  • 第三步,我们把prompt+responses喂给我们的Critic/Reward/Reference模型,让它生成用于计算actor/critic loss的数据,按照强化学习的术语,我们称这些数据为经验(experiences)。critic loss我们将在后文做详细讲解,目前我们只把目光聚焦到actor loss上
  • 第四步,我们根据这些经验,实际计算出actor/critic loss,然后更新Actor和Critic模型

这些步骤都很符合直觉,但是细心的你肯定发现了,文字描述中的第四步和图例中的第四步有差异:图中说,这一个batch的经验值将被用于n次模型更新,这是什么意思呢?

我们知道,在强化学习中,收集一个batch的经验是非常耗时的。对应到我们RLHF的例子中,收集一次经验,它要等四个模型做完推理才可以,正是因此,一个batch的经验,只用于计算1次loss,更新1次Actor和Critic模型,好像有点太浪费了。

所以,我们自然而然想到,1个batch的经验,能不能用来计算ppo-epochs次loss,更新ppo-epochs次Actor和Critic模型?简单写一下伪代码,我们想要:

# --------------------------------------------------------------
# 初始化RLHF中的四个模型
# --------------------------------------------------------------
actor, critic, reward, ref = initialize_models()

# --------------------------------------------------------------
# 训练
# --------------------------------------------------------------
# 对于每一个batch的数据
for i in steps: 
    # 先收集经验值
    exps = generate_experience(prompts, actor, critic, reward, ref)
    # 一个batch的经验值将被用于计算ppo_epochs次loss,更新ppo_epochs次模型
    # 这也意味着,当你计算一次新loss时,你用的是更新后的模型
    for j in ppo_epochs:
        actor_loss = cal_actor_loss(exps, actor)
        critic_loss = cal_critic_loss(exps, critic)
        
        actor.backward(actor_loss)
        actor.step()
        
        critc.backward(critic_loss)
        critic.step()

而如果我们想让一个batch的经验值被重复使用ppo_epochs次,等价于我们想要Actor在这个过程中,模拟和环境交互ppo_epochs次。举个例子:

  • 如果1个batch的经验值只使用1次,那么在本次更新完后,Actor就吃新的batch,正常和环境交互,产出新的经验值
  • 但如果1个batch的经验值被使用ppo_epochs次,在这ppo_epochs中,Actor是不吃任何新数据,不做任何交互的,所以我们只能让Actor“模拟”一下和环境交互的过程,吐出一些新数据出来。

那怎么让Actor模拟呢?很简单,让它观察一下之前的数据长什么样,让它依葫芦画瓢,不就行了吗?我们假设最开始吃batch,吐出经验的actor叫 Actorold ,而在伪代码中,每次做完ppo_epochs而更新的actor叫 Actornew ,那么我们只要尽量保证每次更新后的 Actornew 能模仿最开始的那个 Actorold ,不就行了吗?

诶!是不是很眼熟!两个分布,通过什么方法让它们相近!那当然是KL散度!所以,再回到我们的actor_loss上来,它现在就可被改进成:

我们再稍作一些改动将log去掉(这个其实不是“稍作改动去掉log”的事,是涉及到PPO中重要性采样的相关内容,大家有兴趣可以参考这篇):

其中, Pold 表示真正吃了batch,产出经验值的Actor;P表示ppo_epochs中实时迭代更新的Actor,它在模仿 Pold 的行为。所以这个公式从直觉上也可以理解成:在Actor想通过模拟交互的方式,使用一个batch的经验值更新自己时,它需要收到真正吃到batch的那个时刻的Actor的约束,这样才能在有效利用batch,提升训练速度的基础上,保持训练的稳定。

但是,谨慎的你可能此时又有新的担心了:虽然我们在更新Actor的过程中用 Actorold 做了约束,但如果 Actorold 的约束能力不够,比如说 P(At|St)/Pold(At|St) 还是超出了可接受的范围,那怎么办?

很简单,那就剪裁(clip)它吧!

我们给 P(At|St)/Pold(At|St) 设置一个范围,例如(0.8 ,1.2),也就是如果这个值一旦超过1.2,那就统一变成1.2;一旦小于0.8,那就统一变成0.8。这样就能保证 Actor 和 Actorold 的分布相似性在我们的掌控之内了。此时actor_loss变为:

这时要注意,如果超过变化范围,将 P(At|St)/Pold(At|St) 强制设定为一个常数后,就说明这一部分的loss和Actor模型无关了,而 Advt 这项本身也与Actor无关。所以相当于,在超过约束范围时,我们停止对Actor模型进行更新。

整体代码如下:

    def actor_loss_fn(self, logprobs, old_logprobs, advantages, mask):
        """
        logprobs: 实时计算的,response部分的prob(只有这个是随着actor实时更新而改变的)
        old_logprobs:老策略中,response部分的prob (这个是固定的,不随actor实时更新而改变)
        advantages: 老策略中,response部分每个token对应的优势(这个是固定的,不随actor实时更新而改变)
        mask:老策略中,response部分对应的mask情况这个是固定的,不随actor实时更新而改变)
        
        之所以要引入logprobs计算actor_loss,是因为我们不希望策略每次更新的幅度太大,防止模型训歪
        
        self.cliprange: 默认值是0.2
        """
        ## policy gradient loss
        # -------------------------------------------------------------------------------------
        # 计算新旧策略间的KL散度
        # -------------------------------------------------------------------------------------
        log_ratio = (logprobs - old_logprobs) * mask
        ratio = torch.exp(log_ratio)
        # -------------------------------------------------------------------------------------
        # 计算原始loss和截断loss
        # -------------------------------------------------------------------------------------
        pg_loss1 = -advantages * ratio
        pg_loss2 = -advantages * torch.clamp(ratio, 1.0 - self.cliprange, 1.0 + self.cliprange)
        pg_loss = torch.sum(torch.max(pg_loss1, pg_loss2) * mask) / mask.sum() # 最后是取每个非mask的response token的平均loss作为最终loss
        return pg_loss

(6)Actor loss小结

(1)~(5)中我们一步步树立了actor_loss的改进过程,这里我们就做一个总结吧:

  • 我们已经对Rt进行来改造,使其能够衡量Actor模型是否遵从了Ref模型的约束
  • 我们已经对Advt进行改造,使其不仅考虑了当前时刻的优势,还考虑了未来的优势
  • 我们重复利用了1个batch的数据,使本来只能被用来做1次模型更新的它现在能被用来做ppo_epochs次模型更新。我们使用真正吃了batch,产出经验值的那个时刻的Actor分布来约束ppo_epochs中更新的Actor分布
  • 我们考虑了剪裁机制(clip),在ppo_epochs次更新中,一旦Actor的更新幅度超过我们的控制范围,则不对它进行参数更新。

Critic loss

我们知道,1个batch产出的经验值,不仅被用来更新Actor,还被用来更新Critic。对于Critic loss,我们不再像Actor loss一样给出一个“演变过程”的解读,我们直接来看它最后的设计。

首先,在之前的解说中,你可能有这样一个印象:

  • Vt :Critic对t时刻的总收益的预估,这个总收益包含即时和未来的概念(预估收益)
  • Rt+γ∗Vt+1 :Reward计算出的即时收益 Rt ,Critic预测出的 t+1 及之后时候的收益的折现,这是比 Vt 更接近t时刻真值总收益的一个值(实际收益)

所以,我们的第一想法是:

现在,我们对“实际收益”和“预估收益”都做一些优化。

(1)实际收益优化

我们原始的实际收益为 Rt+γ∗Vt+1 ,但是当我们在actor_loss中引入“优势”的概念时,“优势”中刻画了更为丰富的实时收益信息,所以,我们将实际收益优化为: Advt+Vt

(2)预估收益优化

我们原始的预估收益为 Vt 。
类比于Actor,Critic模型在ppo_epochs的过程中也是不断更新的。所以这个 Vt 可以理解成是 Criticold ,也就是真正吃了batch,参与产出经验的那个时候的Critic产出的收益预测结果。


我们同样想用旧模型去约束新模型,但对于Critic我们采用的约束策略就比较简单了,我们直接看代码,从中可以看出,我们用老 Vt 设计了了一个变动范围,然后用这个变动范围去约束新 Vt

# self.cliprange_value是一个常量
# old_values: 老critic的预测结果
# values:新critic的预测结果
values_clipped = torch.clamp(
            values,
            old_values - self.cliprange_value,
            old_values + self.cliprange_value,
        )

那么最终我们就取实际收益和预估收益的MSE做为loss就好,这里注意,计算实际收益时 Advt,Vt 都是老Critic(真正吃了batch的那个)产出的结果,而预估收益是随着ppo_epochs而变动的。


代码如下:

def critic_loss_fn(self, values, old_values, returns, mask):
        """
        values: 实时critic跑出来的预估预期收益(是变动的,随着ppo epoch迭代而改变)
        old_values:老critic跑出来的预估预期收益(是固定值)
        returns:实际预期收益
        mask:response部分的mask
        
        self.cliprange_value = 0.2
        """
        ## value loss
        # 用旧的value去约束新的value
        values_clipped = torch.clamp(
            values,
            old_values - self.cliprange_value,
            old_values + self.cliprange_value,
        )
        if self.compute_fp32_loss:
            values = values.float()
            values_clipped = values_clipped.float()
        
        # critic模型的loss定义为(预估预期收益-实际预期收益)**2
        vf_loss1 = (values - returns)**2
        vf_loss2 = (values_clipped - returns)**2
        vf_loss = 0.5 * torch.sum(
            torch.max(vf_loss1, vf_loss2) * mask) / mask.sum() # 同样,最后也是把critic loss平均到每个token上
        return vf_loss

PPO优化目标

(1)策略梯度算法:更新幅度大,不稳定

(2)TRPO(信任区域算法):加入KL散度约束条件,但需计算二阶导数,计算量大

(3)PPO(近端策略优化算法):

这里At为优势函数:Critic Model用于估计状态的价值函数 V(st),从而计算策略梯度中的优势值A(t),下面的 r(st,at) 函数就是 RM 模型的输出: 用于计算生成某个token的即时收益 。 下图转换参考:https://zhuanlan.zhihu.com/p/651780908

PPO训练流程

  • Actor Model:要训练的目标语言模型,策略网络
  • Critic Model:预估总收益
  • Reward Model:计算即时收益
  • Reference Model:在RLHF阶段给语言模型增加一些“约束”,防止语言模型训偏

ColossalChat RLFH过程也是非常接近ChatGPT的RLFH过程,RLFH过程主要涉及四个模型分别是Actor、Critic、RM、STF,损失函数也是由三个损失函数组成分别是策略损失、价值损失和 PTX 损失。

ColossalChat RLFH过程

策略损失函数计算:

策略损失计算过程

通过instruction dataset数据训练STF模型,通过计算sft model的logits和actor model(没有经过sft的model)的logits计算kl散度,然后加上reward model的打分变成 reward R奖励值,避免太过偏向reward model加入和sft model的kl散度,同时也避免强化学习将actor模型训歪。

这样做的目的就是避免模型训飞,让模型更新保持在一个小范围内。

价值损失函数计算:

上式R是reward model和sft model计算出来的反馈分数,V(s)是Critic Model输出的价值分数。主要是衡量reward分数和价值函数分数的均方误差。

ptx的损失计算:

计算Actor输出response和输入语料的回答部分的交叉熵损失函数,用来在PPO梯度中加入预训练梯度,以保持语言模型原有性能防止遗忘。这个就是instruct gpt论文中在强化学习中加入预训练梯度以防过度拟合ppo数据带来nlp通用任务能力的下降操作。

总的强化学习损失计算:

为什么RLHF中,PPO需要Critic模型而不是直接使用RewardModel

在强化学习中,PPO(Proximal Policy Optimization)算法是一种基于策略梯度的方法,用于训练强化学习智能体。PPO算法中引入Critic模型的主要目的是为了提供一个价值估计器,用于评估状态或状态动作对的价值,从而辅助策略的更新和优化。

虽然奖励模型(Reward Model)可以提供每个状态或状态动作对的即时奖励信号,但它并不能直接提供对应的价值估计。奖励信号只反映了当前动作的即时反馈,而并没有提供关于在长期时间尺度上的价值信息。

Critic模型的作用是估计状态或状态动作对的长期价值,也称为状态值函数或动作值函数。Critic模型能够学习和预测在当前状态下采取不同动作所获得的累积奖励,它提供了对策略改进的指导。PPO算法使用Critic模型的估计值来计算优势函数,从而调整策略的更新幅度,使得更有利于产生更高长期回报的动作被选择。

另外,Critic模型还可以用于评估不同策略的性能,为模型的评估和选择提供依据。PPO算法中的Actor-Critic架构允许智能体同时学习策略和价值函数,并通过协同训练来提高性能。

因此,在RLHF(Reinforcement Learning from Human Feedback)中,PPO算法需要Critic模型而不是直接使用奖励模型,是为了提供对状态或状态动作对的价值估计,并支持策略的改进和优化。Critic模型的引入可以提供更全面和准确的信息,从而增强算法的训练效果和学习能力。

即时奖励 与 状态动作对的长期价值 的差别是什么?

即时奖励(Immediate Reward)和状态动作对的长期价值(Long-Term Value)代表了强化学习中不同的概念和时间尺度。

即时奖励是指智能体在执行某个动作后立即获得的反馈信号。它通常由环境提供,用于表示当前动作的好坏程度。即时奖励是一种即时反馈,可以指示当前动作的立即结果是否符合智能体的目标。

而状态动作对的长期价值涉及更长时间尺度上的评估,它考虑了智能体在当前状态下选择不同动作所导致的未来回报的累积。长期价值可以表示为状态值函数(State Value Function)或动作值函数(Action Value Function)。

状态值函数(V-function)表示在给定状态下,智能体从该状态开始执行一系列动作,然后按照某个策略进行决策,从而获得的预期累积回报。状态值函数估计了智能体处于某个状态时所能获得的长期价值,反映了状态的优劣程度

动作值函数(Q-function)则表示在给定状态下,智能体选择某个动作后,按照某个策略进行决策,从该状态转移到下一个状态并获得预期累积回报的价值。动作值函数估计了在给定状态下采取不同动作的长期价值,可以帮助智能体选择在每个状态下最优的动作

长期价值考虑了智能体在未来的决策过程中所能获得的累积回报,相比之下,即时奖励只提供了当前动作的即时反馈。长期价值对智能体的决策具有更全面的影响,可以帮助智能体更好地评估当前状态和动作的长期效果,并指导智能体在长期时间尺度上作出更优的决策。

在强化学习中,长期价值的估计对于确定性策略选择和价值优化非常重要,而即时奖励则提供了对当前动作的直接反馈。这两者相互补充,结合起来可以帮助智能体实现更好的决策和学习效果。

PPO中优势函数指什么

在Proximal Policy Optimization(PPO)算法中,优势函数(Advantage Function)用于评估状态-动作对的相对优劣程度。它衡量了执行某个动作相对于平均水平的优劣,即在给定状态下采取某个动作相对于采取平均动作的效果。

优势函数可以用以下方式定义:Advantage(s, a) = Q(s, a) - V(s)

其中,Advantage(s, a)表示在状态 s 下采取动作 a 的优势函数值,Q(s, a) 表示状态动作对 (s, a) 的动作值函数(也称为动作优势函数),V(s) 表示状态值函数。

优势函数的作用在于帮助评估当前动作的相对价值,以便在策略更新过程中确定应采取的动作。通过比较不同动作的优势函数值,可以决定哪些动作是更好的选择。正的优势函数值表示执行的动作比平均水平更好,而负的优势函数值表示执行的动作比平均水平更差。

在PPO算法中,优势函数用于计算策略更新的目标,以便调整策略概率分布来提高优势函数为正的动作的概率,并降低优势函数为负的动作的概率,从而改进策略的性能。

总而言之,优势函数在PPO算法中用于评估状态-动作对的相对优劣,帮助确定应该采取的动作,并在策略更新过程中引导策略向更优的方向调整。

GRPO (Group Relative Policy Optimization)

ORPO偏好优化(Odds Ratio Preference Optimization)

ORPO: Monolithic Preference Optimization without Reference Model

核心: 最大化正样本的生成概率,最小化负样本的生成概率 。相比DPO 【加载2个模型,其中一个推理,另外一个训练,直接在偏好数据上进行训练】,只加载训练模型,直接在偏好数据上进行训练

本文提出的算法ORPO是对SFT的改进,通过修改SFT阶段的损失函数,将类似于DPO中偏好对齐的思想引入到SFT中,提出一种无需奖励模型和参考模型算法。同时,ORPO只有一阶段,不需要像DPO一样需要先SFT再DPO对齐。在众多大模型任务上的实验结果表明,与SFT,DPO等算法相比,ORPO更有优势。

本文提出的算法ORPO是对SFT的改进,修改了SFT阶段的损失函数。同时,与DPO/PPO相比,ORPO将原本分两步进行的过程(SFT+DPO/PPO)合并为一步,更加简洁高效。

现在有许多方法可以使大型语言模型(LLM)与人类偏好保持一致。以人类反馈为基础的强化学习(RLHF)是最早的方法之一,并促成了ChatGPT的诞生,但RLHF的成本非常高。与RLHF相比,DPO、IPO和KTO的成本明显更低,因为它们不需要奖励模型。

虽然DPO和IPO的成本较低,但它们仍需训练两个不同的模型。首先是监督微调(SFT)步骤,即训练模型按指令回答问题,然后使用SFT模型作为初始化和参考,以使模型与人类偏好一致。

ORPO是另一种新的LLM对齐方法,这种方法甚至不需要SFT模型。通过ORPO,LLM可以同时学习回答指令和满足人类偏好。

对于STF,它是在与选择的答案配对的提示上进行训练的。用于sft的数据集可以与偏好优化使用的相同,但不包括”被拒绝”的答案。所以可以直观地认为,应该能够微调一个基础LLM,使其在学习如何回答指令的同时,也学会惩罚和偏好某些答案。

SFT只用正样本更新策略,没有考虑到负样本,会把负样本生成的概率同时拉高,如下图所示:

由于SFT的损失函数对于rejected data没有惩罚项,SFT之后正样本和负样本的生成概率有可能同时上升。

odds定义:模型θ生成 输出序列y的可能性 比 不生成y序列的可能性 比值。

OR为正负样本的odds的比值:

ORPO算法要做的就是最大化OR,即最大化正样本的生成概率,最小化负样本的生成概率,LOR项用了和DPO类似的logsigmoid的形式:

ORPO就是在这个理论基础上建立的,ORPO简单地通过添加负对数似然损失与OR损失(OR代表奇异比)来修改训练损失:

OR损失对被拒绝的答案进行弱惩罚,而对选择的答案进行强有力的奖励。这里包含了一个超参数lambda用于加权OR损失。通过ORPO的损失,模型在学习了SFT期间的内容的同时,也学会了人类偏好。

ORPO需要数千个训练步骤来学习如何区分选择的响应和拒绝的响应。为了获得类似的结果,应该训练ORPO至少2000步,总批大小为64(如论文所述)。

ORPO 已经可以在Hugging Face库上使用了,并且它因为只修改了损失函数,所以可以很好的与现有的Lora方法集成

ORPO是一种单步微调和对准指令llm的新方法。它不需要任何奖励或SFT模型,并且ORPO比DPO和RLHF更简单。根据论文ORPO的性能与DPO相当或略好。但是ORPO需要几千个训练步骤来学习好的和坏的反应之间的区别。

SimPO 简单偏好优化:

算法的核心是将偏好优化目标中的奖励函数与生成指标对齐不需要ref参考模型

SimPO 包含两个主要组件:(1)在长度上归一化的奖励【/|y|】,其计算方式是使用策略模型的奖励中所有 token 的平均对数概率;(2)目标奖励差额 γ ,用以确保获胜和失败响应之间的奖励差超过这个差额 γ 。

DPO 是最常用的离线偏好优化方法之一。DPO 并不会学习一个显式的奖励模型,而是使用一个带最优策略的闭式表达式来对奖励函数 r 进行重新参数化:

其中 π_θ 是策略模型,π_ref 是参考策略(通常是 SFT 模型),Z (x) 是配分函数。通过将这种奖励构建方式整合进 Bradley-Terry (BT) 排名目标,DPO 可使用策略模型而非奖励模型来表示偏好数据的概率,从而得到以下目标:

DPO 的奖励与生成之间的差异。使用 (1) 式作为隐式的奖励表达式有以下缺点:(1) 训练阶段需要参考模型 π_ref,这会带来额外的内存和计算成本;(2) 训练阶段优化的奖励与推理所用的生成指标之间存在差异。具体来说,在生成阶段,会使用策略模型 π_θ 生成一个能近似最大化平均对数似然的序列,定义如下:

在解码过程中直接最大化该指标是非常困难的,为此可以使用多种解码策略,如贪婪解码、波束搜索、核采样和 top-k 采样。此外,该指标通常用于在语言模型执行多选任务时对选项进行排名。在 DPO 中,对于任意三元组 (x, y_w, y_l),满足奖励排名 r (x, y_w) > r (x, y_l) 并不一定意味着满足似然排名:

图片

事实上,在使用 DPO 训练时,留存集中大约只有 50% 的三元组满足这个条件。

构建在长度上归一化的奖励。很自然地,我们会考虑使用 (3) 式中的 p_θ 来替换 DPO 中的奖励构建,使其与引导生成的似然指标对齐。这会得到一个在长度上归一化的奖励:

其中 β 是控制奖励差异大小的常量。该团队发现,根据响应长度对奖励进行归一化非常关键;从奖励公式中移除长度归一化项会导致模型倾向于生成更长但质量更低的序列。这样一来,构建的奖励中就无需参考模型了,从而实现比依赖参考模型的算法更高的内存和计算效率。

SimPO 目标

目标奖励差额。另外,该团队还为 Bradley-Terry 目标引入了一个目标奖励差额项 γ > 0,以确保获胜响应的奖励 r (x, y_w) 超过失败响应的奖励 r (x, y_l) 至少 γ

两个类之间的差额已知会影响分类器的泛化能力。在使用随机模型初始化的标准训练设置中,增加目标差额通常能提升泛化性能。在偏好优化中,这两个类别是单个输入的获胜或失败响应。

在实践中,该团队观察到随着目标差额增大,生成质量一开始会提升,但当这个差额变得过大时,生成质量就会下降。DPO 的一种变体 IPO 也构建了与 SimPO 类似的目标奖励差额,但其整体目标的效果不及 SimPO。

目标。最后,通过将 (4) 式代入到 (5) 式中,可以得到 SimPO 目标:

总结起来,SimPO 采用了与生成指标直接对齐的隐式奖励形式,从而消除了对参考模型的需求。此外,其还引入了一个目标奖励差额 γ 来分离获胜和失败响应

KTO:Kahneman-Tversky Optimisation

特点:

KTO关注的是答案偏离平均水准的程度——比平均好还是坏。所以它的训练数据集是对单个问答的“好/差”标注,而不再是成对数据间谁好谁差(所以用户对LLM结果的点赞或踩就可以当做反馈使用了)。

KTO不需要偏好数据,可以直接利用二元信号标记的数据来训练算法,对于负样本更加敏感。 KTO并不需要一个数据对,只需要对生成的结果进行good/bad的二元标注即可。 【比如:ChatGPT UI 界面会输出两个答案,用户可以选择哪个更好,适用于从生产环境中运行的聊天模型的训练】

实验表明,KTO算法在一定参数范围内能够超过DPO算法,并且KTO可以处理数据正负样本不平衡的情况。同时,在跳过SFT阶段的情况下,直接使用KTO相比于直接使用 DPO,效果有很大提升。在数据正负样本比例失衡/偏好数据有非传递性/偏好数据有噪声/的情况下,使用KTO可能是更好的选择。

KTO 使用 Kahneman-Tversky 人类效用模型,论文提出直接最大化生成效用的 HALO, 而不是最大化偏好的对数可能性。

  • 在1B~30B尺度上与基于偏好的方法的性能相匹配或超过,尽管它只从二进制信号(0或者1)中学习输出是否可取。
  • 没有一个 HALO 普遍优越;
  • 最佳损失取决于最适合给定设置的归纳偏差,经常被忽视的考虑因素。

KTO算法的具体步骤如下:

  1. 定义效用函数:根据前景理论中的效用函数公式,定义一个效用函数,用于计算模型输出相对于参考点的效用。
  2. 计算参考点:根据概率分布Q(X’, Y’ | x, y),计算出一个参考点,用于衡量模型输出的效用。
  3. 计算模型输出的效用:对于每个输入,计算模型输出相对于参考点的收益或损失,然后使用效用函数计算这些收益或损失的效用。
  4. 优化模型参数:优化模型参数以最大化模型输出的总效用。

KTO 损失函数本质是把 pair-wise 公式变成 point-wise 方式,结合了HALOs以及二元信号数据的思想提出使用Kahneman-Tversky 优化的KTO算法:

其中 zo是KL散度项,参考点zo为最优策略下reward的期望值,最终可以推导成KL散度的形式,y’表示任意输出,在实际训练中,Z0表示batch平均水准的程度Z0当前batch里面的样本进行估计得到的,平均 reward,代表不好不坏的居中的结果。 LKTO 就是DPO中推导的reward函数形式。

按照上面的定义估计z0是不切实际的,因为从πθ采样很慢,人类无法感知πθ引起的完整分布。

这个估计是有偏差的,但这是可取的,因为它使我们更接近人类如何构建他们的主观参考点。

实际上KTO相对比DPO差异就两点

  • 对正负样本进行了加权:DPO里面是使用正负样本的reward差值进行sigmoid映射,但是KTO里面使用reward模型与KL散度之间的差异!(说是KL散度,但其实也是bad的log比值数值!不过不是同一个pair
  • 注意:在实践的时候,KL项并不参与反向传播,这其实就跟DPO更相似的。DPO使一个数据对,但是这里把DPO给拆分了,相当于对每一个样本单独进行最大化或最小化了,以及进行加权。另一个作用就是,如果 rKTO(x,y) 的差异与KL散度有足够区别的话,那对应的Loss也就比较小。因此,KTO会更加鼓励差异大的数据对。
  • 但其实我们可以从KTO的目标函数直接看到。由于KTO是分别针对单条数据,如果数据是正样本,那么一定要超过 zo 才会产生预测正确反馈;对于负样本,需要低于 zo才会产生预测正确反馈

KTO和DPO的选择

  • 数据比例:如果数据集是以good/bad形式进行标注,并且数据比例不平衡,那么选择KTO
  • 数据质量:如果你的偏好数据质量高,数据噪声小,那么DPO的效果更好。由于目前公开的数据集中存在的噪声较大,这就能解释为什么KTO的效果会超过DPO了。
  • 理论分析:KTO不会从负样本中学习到很高的反馈,也不会从正样本中学习到很低的反馈(所以对噪声比较鲁棒)

KTO 的工作原理:

  • 如果模型以直接(blunt manner)方式增加了理想示例的奖励,那么 KL 惩罚也会增加,并且不会取得任何进步。这迫使模型准确地了解是什么让输出变得理想,这样就可以增加奖励,同时保持 KL 项持平(甚至减少)。
  • 实际实现中,KL 项是通过当前batch里面的正负样本进行估计得到的可以认为是batch样本的平均水平】,详细 debug KTOTrainer 源代码

对成对偏好数据进行分配

  • 与大多数比对方法一样,DPO 需要一个成对偏好数据集(x, y_w, y_l),够根据一组标准(如有益性或有害性)来标记哪种模型响应更好。
  • 实践过程中,创建这些数据是一项耗时且成本高昂的工作。
  • ContextualAI 提出替代方案,称为 Kahneman-Taversky 优化(KTO),完全根据被标记为「好」或「坏」的样本(例如在聊天 UI 中看到的图标👍或👎)来定义损失函数。这些标签更容易获得, KTO 是一种很有前景的方法,不断更新在生产环境中运行的聊天模型。

与此同时,这些方法都有相应的超参数,其中最重要的是 β ,控制对使用模型的偏好程度的权重。这些方法已经在第三方库(如 huggingface TRL)中实现

KTO 数据集

KTO 不需要成对的偏好数据,实验时直接将 GPT-4 生成的响应归类为「好」标签,将 Llama Chat 13b 的响应视为「坏」标签。

KTO数据集与偏好数据集类似,但不同于给出一个更优的回答和一个更差的回答,KTO数据集对每一轮问答只给出一个 true/false 的 label。 除了 instruction 以及 input 组成的人类最终输入和模型回答 output ,KTO 数据集还需要额外添加一个 kto_tag 列(true/false)来表示人类的反馈。在一轮问答中其格式如下:

[
  {
    "instruction": "人类指令(必填)",
    "input": "人类输入(选填)",
    "output": "模型回答(必填)",
    "kto_tag": "人类反馈 [true/false](必填)"
  }
]

对于上述格式的数据, dataset_info.json 中的 数据集描述 应为:

"数据集名称": {
  "file_name": "data.json",
  "columns": {
    "prompt": "instruction",
    "query": "input",
    "response": "output",
    "kto_tag": "kto_tag"
  }
}

代码实现:

基于pytorch、deepspeed、transformers代码:https://github.com/PKU-Alignment/align-anything/tree/main/align_anything/trainers/text_to_text

sft训练代码:

def loss(self, sft_batch: SupervisedBatch) -> dict[str, torch.Tensor]:
    """Loss function for supervised finetuning."""
    outputs = self.model(**self.infer_batch(sft_batch))
    return {'loss': outputs.loss}

def train_step(self, sft_batch: SupervisedBatch) -> dict[str, Any]:
    """Performs a single training step."""
    loss = self.loss(sft_batch)['loss']
    self.model.backward(loss)
    self.model.step()

    return {
        'train/loss': loss.item(),
        'train/lr': self.model.optimizer.param_groups[0]['lr'],
    }

dpo训练代码:

https://blog.csdn.net/weixin_43013480/article/details/141370269

# 从 logits(未归一化的概率分布)中,提取 labels 对应类别的对数概率(log probabilities)。
def gather_log_probabilities(
    logits: torch.Tensor,  # size = (B, L, V)
    labels: torch.LongTensor,  # size = (B, L)
) -> torch.Tensor:  # size = (B, L)
    """Gather log probabilities of the given labels from the logits."""
    log_probs = F.log_softmax(logits, dim=-1)  # size = (B, L, V)
    gathered_log_probs = torch.gather(  # size = (B, L, 1)
        log_probs,
        dim=-1,
        index=labels.unsqueeze(dim=-1).to(torch.int64),
    )
    return gathered_log_probs.squeeze(dim=-1)  # size = (B, L)
def compute_log_probs(
    self,
    model: AutoModelForCausalLM,
    batch: PreferenceBatch,
) -> torch.Tensor:
    """Compute log probabilities of given sequences."""
    # 获得所有可能输出的log概率,logits 表示每个 token 位置的 未归一化的概率分布
    logits = model(**self.infer_batch(batch)).logits
    device = logits.device
    input_ids = batch['input_ids']
    #取得每个样本的回复长度,用于截取模型输出
    batch_size = len(batch['meta_info']['response_lens'])
    logprob_list = []
    for idx in range(batch_size):
        response_length = batch['meta_info']['response_lens'][idx]
        # 去除填充 (PAD) token,避免计算无效 token 的概率。
        raw_input_id = strip_pad(input_ids[idx], self.tokenizer.pad_token_id)
        #只保留 回复部分的 logits,丢弃 prompt 部分。 
        logit = logits[idx][-response_length:].unsqueeze(0)
        input_id = raw_input_id[-response_length:].unsqueeze(0)
        #计算对应的better 和worse 序列token 对数概率
        log_p = gather_log_probabilities(logit[:, :-1], input_id[:, 1:])
        logprob_list.append(log_p.squeeze(0))
    # 不同样本的 log_probs 长度不同,使用 pad_sequence 进行 padding,补齐到相同长度。
    return torch.nn.utils.rnn.pad_sequence(
        logprob_list, batch_first=True, padding_value=0.0
    ).to(device)

def loss(  # pylint: disable=too-many-locals
    self,
    batch: PreferenceBatch,
) -> dict[str, torch.Tensor]:
    """Loss function for the DPO algorithm."""
   #计算当前模型 (self.model.module) 在 batch 上的 log 概率。
    sequence_log_probs = self.compute_log_probs(
        self.model.module,
        batch,
    )
# better_sequence_log_probs (用户偏好的回复)
# worse_sequence_log_probs (用户不喜欢的回复)
    (
        better_sequence_log_probs,  # size = (B, L - 1)
        worse_sequence_log_probs,  # size = (B, L - 1)
    ) = sequence_log_probs.chunk(chunks=2, dim=0)
# 计算参考模型 (self.reference_model.module) 的对数概率 (log_probs)。
# reference_model 通常是 原始未优化的模型,作为对比基准。
# torch.no_grad() 表示 不计算梯度,避免影响参考模型。
    with torch.no_grad():
        ref_sequence_log_probs = self.compute_log_probs(  # size = (2 * B, L - 1)
            self.reference_model.module,
            batch,
        )
        ref_better_sequence_log_probs, ref_worse_sequence_log_probs = (
            ref_sequence_log_probs.chunk(chunks=2, dim=0)
        )

    losses = []
    better_sample_rewards = []
    worse_sample_rewards = []

    batch_size = better_sequence_log_probs.size(0)
    for i in range(batch_size):
# 计算 更好/更差回复的总 log 概率(即累加 token 级别 log 概率)。
        better_log_prob = better_sequence_log_probs[i, :].sum(dim=-1)
        worse_log_prob = worse_sequence_log_probs[i, :].sum(dim=-1)
        ref_better_log_prob = ref_better_sequence_log_probs[i, :].sum(dim=-1)
        ref_worse_log_prob = ref_worse_sequence_log_probs[i, :].sum(dim=-1)
# 当前模型比参考模型更偏好 better 回复 的程度。
        better_log_ratio = better_log_prob - ref_better_log_prob
# 当前模型比参考模型更偏好 worse 回复 的程度。
        worse_log_ratio = worse_log_prob - ref_worse_log_prob
# 计算 better 和 worse 的 log 比值差
# 使用 -logsigmoid(x) 计算负对数 sigmoid 损失,优化模型使其更倾向 better 回复。
# logsigmoid 的性质:
# 如果 x 很大,logsigmoid(x) ≈ 0,意味着损失小,模型已经正确偏好 better response。
# 如果 x 很小或负,logsigmoid(x) ≈ x,意味着损失大,模型没有正确区分 better 和 worse,需要优化。
        losses.append(
            -F.logsigmoid(
                self.cfgs.train_cfgs.scale_coeff * (better_log_ratio - worse_log_ratio),
            ),
        )
        better_sample_rewards.append(
            self.cfgs.train_cfgs.scale_coeff * better_log_ratio.detach(),
        )
        worse_sample_rewards.append(self.cfgs.train_cfgs.scale_coeff * worse_log_ratio.detach())
    loss = torch.stack(losses).mean()  # size = ()
    better_sample_reward = torch.stack(better_sample_rewards)  # size = (B,)
    worse_sample_reward = torch.stack(worse_sample_rewards)  # size = (B,)
# 计算 奖励 (reward)、准确率 (accuracy) 和奖励间距 (margin)。
    reward = better_sample_reward + worse_sample_reward  # size = (B,)
    reward_accuracy = (better_sample_reward > worse_sample_reward).float().mean()  # size = ()
    reward_margin = better_sample_reward - worse_sample_reward  # size = (B,)

    return {
        'loss': loss,
        'reward': reward,
        'better_sample_reward': better_sample_reward,
        'worse_sample_reward': worse_sample_reward,
        'reward_accuracy': reward_accuracy,
        'reward_margin': reward_margin,
    }

def train_step(
    self,
    batch: PreferenceBatch,
) -> dict[str, Any]:
    """Perform a single training step for DPO."""
    loss_dict = self.loss(batch=batch)
    loss = loss_dict['loss']
    self.model.backward(loss)
    self.model.step()

    with torch.no_grad():
        reward = loss_dict['reward'].mean()
        better_sample_reward = loss_dict['better_sample_reward'].mean()
        worse_sample_reward = loss_dict['worse_sample_reward'].mean()
        reward_accuracy = loss_dict['reward_accuracy']
        reward_margin = loss_dict['reward_margin'].mean()

        loss = get_all_reduce_mean(loss)
        reward = get_all_reduce_mean(reward)
        better_sample_reward = get_all_reduce_mean(better_sample_reward)
        worse_sample_reward = get_all_reduce_mean(worse_sample_reward)
        reward_accuracy = get_all_reduce_mean(reward_accuracy)
        reward_margin = get_all_reduce_mean(reward_margin)

    return {
        'train/loss': loss.item(),
        'train/reward': reward.item(),
        'train/better_sample_reward': better_sample_reward.item(),
        'train/worse_sample_reward': worse_sample_reward.item(),
        'train/reward_accuracy': reward_accuracy.item(),
        'train/reward_margin': reward_margin.item(),
        'train/lr': self.model.optimizer.param_groups[0]['lr'],
    }

ppo训练代码:

#使用策略模型 (Actor Model) 生成文本,并返回其 input_ids 和 attention_mask。
def actor_step(self, mini_prompt_only_batch: PromptOnlyBatch) -> dict[str, Any]:
    infer_batch = self.infer_batch(mini_prompt_only_batch)
    actor_batch = copy.deepcopy(infer_batch)
    sequences = self.actor_model.module.generate(
        **infer_batch,
        generation_config=self.generation_config,
        synced_gpus=True,
        do_sample=True,
    )
    attention_mask = sequences.not_equal(self.tokenizer.pad_token_id)
    actor_batch['input_ids'] = sequences
    actor_batch['attention_mask'] = attention_mask

    return actor_batch
# 计算奖励值 (reward) 和对抗奖励值 (reward_values)。 
def reward_model_step(self, actor_batch: PromptOnlyBatch) -> dict[str, Any]:
        reward_batch = copy.deepcopy(actor_batch)
        if self.reward_tokenizer is not self.tokenizer:
            reward_tokenize_output = batch_retokenize(
                actor_batch['input_ids'],
                src_tokenizer=self.tokenizer,
                dest_tokenizer=self.reward_tokenizer,
                skip_special_tokens=True,
                device=self.args.device,
            )
            reward_batch['input_ids'] = reward_tokenize_output['input_ids']
            reward_batch['attention_mask'] = reward_tokenize_output['attention_mask']
        reward_infer_batch = self.reward_infer_batch(reward_batch)
        reward_batch['reward'] = self.reward_model(**reward_infer_batch).end_scores.squeeze(dim=-1)
        critic_infer_batch = self.reward_infer_batch(actor_batch)
        scores = self.reward_critic_model(**critic_infer_batch).scores
        reward_batch['reward_values'] = scores.squeeze(dim=-1)[:, :-1]

        return reward_batch
#冻结模型参数,避免影响训练,采样多个 mini-batch,生成文本,计算奖励,计算 log 概率 (log_probs),计算参考模型的 log 概率 (ref_log_probs)
# 经验回放:生成训练数据并计算指标
  @torch.no_grad()
    def rollout(self, prompt_only_batch: PromptOnlyBatch) -> list[dict[str, Any]]:
        """Rollout a batch of experiences."""
        # freeze the model for rolling out
        self.set_train(mode=False)

        total_batch_size = prompt_only_batch['input_ids'].size(0)
        micro_batch_size = int(self.cfgs.train_cfgs.per_device_train_batch_size)
        micro_inference_batches = []
        micro_training_batches = []
        mini_batch = {}
        for i in range(0, total_batch_size, micro_batch_size):

            mini_batch = {
                key: prompt_only_batch[key][i : i + micro_batch_size] for key in prompt_only_batch
            }

            # actor generation
            actor_batch = self.actor_step(mini_batch)
            # reward model and reward critic model scoring
            reward_batch = self.reward_model_step(actor_batch)
            # calculate the log probabilities
            logits = self.actor_model(**actor_batch).logits
            ref_logits = self.actor_reference_model(**actor_batch).logits
            log_probs = gather_log_probabilities(logits[:, :-1], actor_batch['input_ids'][:, 1:])
            ref_log_probs = gather_log_probabilities(
                ref_logits[:, :-1], actor_batch['input_ids'][:, 1:]
            )

            micro_training_batch = {}
            micro_training_batch['prompt_idx'] = mini_batch['input_ids'].size(-1) - 1
            micro_training_batch['log_probs'] = log_probs
            micro_training_batch['ref_log_probs'] = ref_log_probs
            micro_training_batch['reward'] = reward_batch['reward']
            micro_training_batch['reward_values'] = reward_batch['reward_values']

            mini_batch['input_ids'] = reward_batch['input_ids']
            mini_batch['attention_mask'] = actor_batch['attention_mask']
            # add rollout results to the batches
            micro_inference_batches.append(mini_batch)
            micro_training_batches.append(micro_training_batch)

        # unfreeze the model for training
        self.set_train()

        return micro_inference_batches, micro_training_batches

#计算策略梯度损失
# 计算 PPO 损失函数:
# ratios = exp(new_log_probs - old_log_probs)(新旧策略比)。
# 裁剪 ratios 避免策略剧烈变化(PPO 关键)。
# return -masked_mean(surrogate, mask):最大化优势 𝐴𝑡
   
def actor_loss_fn(
        self,
        log_probs: torch.Tensor,  # size = (B, L - S)
        old_log_probs: torch.Tensor,  # size = (B, L - S)
        advantages: torch.Tensor,  # size = (B, L - S)
        mask: torch.BoolTensor,  # size = (B, L - S)
    ) -> torch.Tensor:  # size = ()
        # size = (B, L - S)
        ratios = torch.exp(log_probs - old_log_probs)
        surrogate1 = advantages * ratios
        surrogate2 = advantages * torch.clamp(
            ratios,
            1.0 - self.clip_range_ratio,
            1.0 + self.clip_range_ratio,
        )
        surrogate = torch.minimum(surrogate1, surrogate2)
        return -masked_mean(surrogate, mask)  # size = ()
#  rl_step函数是训练过程中使用强化学习(RL)更新策略的一步。在PPo算法中,rl_step是用来更新策略网络(actor)和价值网络(critic)的一部分。具体来说,这个函数通过计算强化学习损失(actor loss和critic loss),并通过反向传播优化这两个网络。
# reward_critic_model 评估奖励函数的 价值估计,用于计算 优势函数 𝐴𝑡不是直接计算奖励,而是估算未来可能获得的奖励。主要用于时间差分(TD learning)更新策略,类似于 价值函数。

def rl_step(
        self, inference_batch: dict[str, torch.Tensor], training_batch: dict[str, torch.Tensor]
    ) -> dict[str, Any]:
        """Perform a single update step with RL loss."""
        old_log_probs = training_batch['log_probs']
        ref_log_probs = training_batch['ref_log_probs']
        reward = training_batch['reward']
        old_reward_values = training_batch['reward_values']
        start = training_batch['prompt_idx']

        input_ids = inference_batch['input_ids']
        attention_mask = inference_batch['attention_mask']

        sequence_mask = attention_mask[:, 1:]

        with torch.no_grad():
            old_rewards = self.add_kl_divergence_regularization(
                reward,
                old_log_probs,
                ref_log_probs,
                sequence_mask,
            )
            reward_advantages, reward_returns = self.get_advantages_and_returns(
                old_reward_values,
                old_rewards,
                sequence_mask,
                start,
            )

        logits = self.actor_model(**inference_batch, use_cache=False).logits
        log_probs = gather_log_probabilities(logits[:, :-1], input_ids[:, 1:])
        actor_loss = self.actor_loss_fn(
            log_probs[:, start:],
            old_log_probs[:, start:],
            reward_advantages,
            sequence_mask[:, start:],
        )
        self.actor_model.backward(actor_loss)
        self.actor_model.step()

        reward_values = self.reward_critic_model(**inference_batch).scores
        reward_values = reward_values.squeeze(dim=-1)[:, :-1]
        reward_critic_loss = self.critic_loss_fn(
            reward_values[:, start:],
            old_reward_values[:, start:],
            reward_returns,
            sequence_mask[:, start:],
        )
        self.reward_critic_model.backward(reward_critic_loss)
        self.reward_critic_model.step()

        with torch.no_grad():
            mask = sequence_mask[:, start:]
            kl_divergence = ((old_log_probs - ref_log_probs)[:, start:] * mask).sum(dim=-1).mean()
            mean_generated_length = mask.sum(dim=-1).float().mean()
            max_generated_length = mask.sum(dim=-1).float().max()

            reward = reward.mean()
            reward_with_kl_penalty = (old_rewards[:, start:] * mask).sum(dim=-1).mean()
            reward_advantage = masked_mean(reward_advantages, mask)
            reward_return = masked_mean(reward_returns, mask)
            reward_value = masked_mean(reward_values[:, start:], mask)

            actor_loss = get_all_reduce_mean(actor_loss)
            reward_critic_loss = get_all_reduce_mean(reward_critic_loss)
            reward = get_all_reduce_mean(reward)
            reward_with_kl_penalty = get_all_reduce_mean(reward_with_kl_penalty)
            reward_advantage = get_all_reduce_mean(reward_advantage)
            reward_return = get_all_reduce_mean(reward_return)
            reward_value = get_all_reduce_mean(reward_value)
            kl_divergence = get_all_reduce_mean(kl_divergence)
            mean_generated_length = get_all_reduce_mean(mean_generated_length)
            max_generated_length = get_all_reduce_max(max_generated_length)

        dist.barrier()

        return {
            'train/actor_loss': actor_loss.item(),
            'train/reward_critic_loss': reward_critic_loss.item(),
            'train/reward': reward.item(),
            'train/reward_with_kl_penalty': reward_with_kl_penalty.item(),
            'train/reward_advantage': reward_advantage.item(),
            'train/reward_return': reward_return.item(),
            'train/reward_value': reward_value.item(),
            'train/kl_divergence': kl_divergence.item(),
            'train/actor_lr': self.actor_model.optimizer.param_groups[0]['lr'],
            'train/reward_critic_lr': self.reward_critic_model.optimizer.param_groups[0]['lr'],
            'train/mean_generated_length': mean_generated_length.item(),
            'train/max_generated_length': max_generated_length.item(),
        }

    def ptx_step(self, ptx_batch: dict[str, torch.Tensor]) -> dict[str, Any]:
        """Perform a single update step with PTX loss."""
        ptx_loss = self.actor_model(**self.infer_batch(ptx_batch)).loss
        self.actor_model.backward(self.ptx_coeff * ptx_loss)
        self.actor_model.step()
        ptx_loss = get_all_reduce_mean(ptx_loss)
        return {
            'train/ptx_loss': ptx_loss.item(),
        }

    def train(self) -> None:
        """Train the model."""
        self.logger.print('***** Running training *****')

        progress_bar = tqdm(
            total=self.total_training_steps,
            desc=f'Training 1/{self.cfgs.train_cfgs.epochs} epoch',
            position=0,
            leave=True,
            disable=not is_main_process(),
        )

        if self.cfgs.data_cfgs.eval_datasets:
            self.logger.print('\n***** Evaluating at the beginning *****')
            self.eval()

        num_prompt_only_batches = len(self.prompt_only_dataloader)
        num_ptx_batches = len(self.ptx_dataloader)
        num_ptx_replicas = (num_prompt_only_batches + num_ptx_batches - 1) // num_ptx_batches
        for epoch in range(int(self.cfgs.train_cfgs.epochs)):
            for prompt_only_batch, ptx_batch in zip(
                self.prompt_only_dataloader,
                itertools.chain.from_iterable([self.ptx_dataloader] * num_ptx_replicas),
            ):
                inference_batches, training_batches = self.rollout(prompt_only_batch)

                if self.use_ptx:
                    ptx_batches = self.split_ptx_micro_batches(ptx_batch)
                else:
                    ptx_batches = [None for _ in range(len(inference_batches))]
                torch.cuda.empty_cache()

                for _ in range(self.cfgs.train_cfgs.update_iters):
                    for inference_batch, training_batch, ptx_batch in zip(
                        inference_batches, training_batches, ptx_batches
                    ):
                        rl_info = self.rl_step(inference_batch, training_batch)

                        torch.cuda.empty_cache()
                        self.logger.log(rl_info, step=self.global_step)
                        if self.use_ptx:
                            ptx_info = self.ptx_step(ptx_batch)
                            torch.cuda.empty_cache()
                            self.logger.log(ptx_info, step=self.global_step)

                        self.global_step += 1
                        progress_bar.set_description(
                            f'Training {epoch + 1}/{self.cfgs.train_cfgs.epochs} epoch '
                            f'(reward {rl_info["train/reward"]:.4f})',
                        )
                        progress_bar.update(1)

                        if self.global_step % self.cfgs.logger_cfgs.save_interval == 0:
                            self.logger.print(f'Saving checkpoint at step {self.global_step} ...')
                            self.save(tag=self.global_step)
                            self.logger.print('Checkpoint saved.')

                        if (
                            self.cfgs.data_cfgs.eval_datasets
                            and self.cfgs.train_cfgs.eval_strategy == 'steps'
                            and self.global_step % self.cfgs.train_cfgs.eval_interval == 0
                        ):
                            self.logger.print(
                                f'\n***** Evaluating at step {self.global_step} *****',
                            )
                            self.eval()

RM奖励模型训练代码:

    def loss(
        self,
        batch: PreferenceBatch,
    ) -> dict[str, torch.Tensor]:
        """Loss function for the reward model."""
        (
            better_input_ids,  # size = (B, L)
            worse_input_ids,  # size = (B, L)
        ) = batch[
            'input_ids'
        ].chunk(chunks=2, dim=0)
        assert better_input_ids.size(0) == worse_input_ids.size(0), 'batch size mismatch!'

# scores:一般来说,这代表模型在每个时间步骤(或输入分段)上的奖励得分,通常是一个形状为 (B, L, 1) 的张量,其中 B 是批量大小,L 是输入序列的长度,1 是奖励得分的维度。
#end_scores:通常表示输入序列的结束阶段的奖励得分,这可能是在整个序列处理完成后,模型计算出的最终奖励。
        output = self.model(**self.infer_batch(batch))
        scores = output.scores
        end_scores = output.end_scores
        higher_rewards, lower_rewards = scores.squeeze(dim=-1).chunk(chunks=2, dim=0)
        higher_end_reward, lower_end_reward = end_scores.squeeze(dim=-1).chunk(chunks=2, dim=0)

        loss = -F.logsigmoid(higher_end_reward - lower_end_reward).mean()

        if self.cfgs.train_cfgs.regularization > 0.0:
            loss = (
                loss
                + self.cfgs.train_cfgs.regularization
                * torch.stack([lower_end_reward, higher_end_reward]).square().mean()
            )

        accuracy = (higher_end_reward > lower_end_reward).float().mean()  # size = ()
        return {
            'loss': loss,  # size = ()
            'higher_end_reward': higher_end_reward,  # size = (B,)
            'lower_end_reward': lower_end_reward,  # size = (B,)
            'higher_rewards': higher_rewards,  # size = (B, L)
            'lower_rewards': lower_rewards,  # size = (B, L)
            'accuracy': accuracy,  # size = ()
        }

    def train_step(
        self,
        batch: PreferenceBatch,
    ) -> dict[str, Any]:
        """Perform a single training step."""
        loss_dict = self.loss(batch)
        loss = loss_dict['loss']
        self.model.backward(loss)
        self.model.step()

        accuracy = loss_dict['accuracy']

        loss = get_all_reduce_mean(loss)
        accuracy = get_all_reduce_mean(accuracy)

        return {
            'train/loss': loss.item(),
            'train/accuracy': accuracy.item(),
            'train/lr': self.model.optimizer.param_groups[0]['lr'],
        }

orpo 训练代码:

相关介绍:https://github.com/Paul33333/ORPO https://zhuanlan.zhihu.com/p/688583797

# 从 logits(未归一化的概率分布)中,提取 labels 对应类别的对数概率(log probabilities)。
def gather_log_probabilities(
    logits: torch.Tensor,  # size = (B, L, V)
    labels: torch.LongTensor,  # size = (B, L)
) -> torch.Tensor:  # size = (B, L)
    """Gather log probabilities of the given labels from the logits."""
    log_probs = F.log_softmax(logits, dim=-1)  # size = (B, L, V)
    gathered_log_probs = torch.gather(  # size = (B, L, 1)
        log_probs,
        dim=-1,
        index=labels.unsqueeze(dim=-1).to(torch.int64),
    )
    return gathered_log_probs.squeeze(dim=-1)  # size = (B, L)

# compute_log_probs 的作用是计算给定序列的 log 概率(对数概率),主要用于评估语言模型(LLM)的生成质量。
def compute_log_probs(
        self,
        model: AutoModelForCausalLM,
        batch: PreferenceBatch,
    ) -> torch.Tensor:
        """Compute log probabilities of given sequences."""
        logits = model(**self.infer_batch(batch)).logits
        device = logits.device
        input_ids = batch['input_ids']
        batch_size = len(batch['meta_info']['response_lens'])
        logprob_list = []
        for idx in range(batch_size):
            response_length = batch['meta_info']['response_lens'][idx]  # for the eos token
            logit = logits[idx][-response_length:].unsqueeze(0)
            input_id = input_ids[idx][-response_length:].unsqueeze(0)
# logit[:, :-1]取 response 部分的 logits,去掉最后一个 token(因为 logits 预测的是下一个 token)input_id[:, 1:]: 取 response 部分的 token IDs,从第二个 token 开始(因为 log_probs 计算的是下一个 token 概率)。
作用:计算 response 部分每个 token 的 log 概率(对 logit 的 softmax 取对数)。
            log_p = gather_log_probabilities(logit[:, :-1], input_id[:, 1:]) 
            logprob_list.append(log_p.squeeze(0))
#pad填充,返回张量形状 (B, max_L_resp)
        return torch.nn.utils.rnn.pad_sequence(
            logprob_list, batch_first=True, padding_value=0.0
        ).to(device)
class ORPOTrainer(DPOTrainer):

    def loss(  # pylint: disable=too-many-locals
        self,
        batch: PreferenceBatch, # size = (2*B, L)
    ) -> dict[str, torch.Tensor]:
        """Loss function for the ORPO algorithm."""
        sequence_log_probs = self.compute_log_probs(
            self.model.module,
            batch,
        )
        (
            better_sequence_log_probs,  # size = (B, L - 1)
            worse_sequence_log_probs,  # size = (B, L - 1)
        ) = sequence_log_probs.chunk(chunks=2, dim=0)

        losses = []
        better_sample_rewards = []
        worse_sample_rewards = []

        better_input_ids, worse_input_ids = batch['input_ids'].chunk(chunks=2, dim=0)
        better_attention_mask, worse_attention_mask = batch['attention_mask'].chunk(chunks=2, dim=0)

        batch_size = better_input_ids.size(0)
#diverge_index 代表 better 和 worse 输入序列开始不同的位置:diverge_index,即它之后的 token 是模型生成的部分。
        for i in range(batch_size):
            if torch.all(torch.eq(better_input_ids[i], worse_input_ids[i])).item():
                continue
            better_end_index = better_attention_mask[i].nonzero()[-1].squeeze().item()
            worse_end_index = worse_attention_mask[i].nonzero()[-1].squeeze().item()
            diverge_index = (
                (better_input_ids[i] != worse_input_ids[i]).nonzero()[0].squeeze().item()
            )
            assert 0 <= diverge_index <= better_end_index, 'diverge index is out of range!'
            assert 0 <= diverge_index <= worse_end_index, 'diverge index is out of range!'
# better_seq_slice 和 worse_seq_slice 取从 diverge_index 开始到序列结束的部分(即模型生成的 token)。
            better_seq_slice = slice(diverge_index, better_end_index + 1)
            worse_seq_slice = slice(diverge_index, worse_end_index + 1)
            better_seq_length = better_end_index + 1
            worse_seq_length = worse_end_index + 1

            # size = ()
# better_log_prob: 计算 better 部分的总 log 概率。
# worse_log_prob: 计算 worse 部分的总 log 概率。
# 计算 对数比率(log ratio):
            better_log_prob = better_sequence_log_probs[i, better_seq_slice].sum(dim=-1)
            worse_log_prob = worse_sequence_log_probs[i, worse_seq_slice].sum(dim=-1)
            better_log_ratio = better_log_prob / better_seq_length
            worse_log_ratio = worse_log_prob / worse_seq_length
# 计算 ORPO 的 odds ratio loss:
            log_odds = (better_log_ratio - worse_log_ratio) - (
                torch.log1p(-torch.exp(better_log_ratio)) - torch.log1p(-torch.exp(worse_log_ratio))
            )
#  better 的 log 概率明显高于 worse,从而优化生成策略。
            odds_ratio_loss = -F.logsigmoid(log_odds)
# 最终损失
            sft_loss = -better_log_ratio
            losses.append(
                sft_loss + self.cfgs.train_cfgs.scale_coeff * odds_ratio_loss,
            )
            better_sample_rewards.append(
                self.cfgs.train_cfgs.scale_coeff * better_log_ratio.detach(),
            )
            worse_sample_rewards.append(self.cfgs.train_cfgs.scale_coeff * worse_log_ratio.detach())

        loss = torch.stack(losses).mean()  # size = ()
        better_sample_reward = torch.stack(better_sample_rewards)  # size = (B,)
        worse_sample_reward = torch.stack(worse_sample_rewards)  # size = (B,)
        reward = better_sample_reward + worse_sample_reward  # size = (B,)
        reward_accuracy = (better_sample_reward > worse_sample_reward).float().mean()  # size = ()
        reward_margin = better_sample_reward - worse_sample_reward  # size = (B,)

        return {
            'loss': loss,
            'reward': reward,
            'better_sample_reward': better_sample_reward,
            'worse_sample_reward': worse_sample_reward,
            'reward_accuracy': reward_accuracy,
            'reward_margin': reward_margin,
        }


def main():
    # setup distribution training
    deepspeed.init_distributed()
    current_device = get_current_device()
    torch.cuda.set_device(current_device)

    # read default configs from the yaml file
    task = os.path.join('text_to_text', 'orpo')
    dict_cfgs, ds_cfgs = read_cfgs(mode='train', task=task)

    # get custom configs from command line
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    _, unparsed_args = parser.parse_known_args()
    keys = [k[2:] for k in unparsed_args[1::2]]
    values = list(unparsed_args[2::2])
    unparsed_args = dict(zip(keys, values))
    for k, v in unparsed_args.items():
        dict_cfgs = update_dict(dict_cfgs, custom_cfgs_to_dict(k, v))

    # setup training
    cfgs = dict_to_namedtuple(dict_cfgs)
    seed_everything(cfgs.train_cfgs.seed)

    # finetune the model
    trainer = ORPOTrainer(cfgs=cfgs, ds_cfgs=ds_cfgs)
    trainer.train()
    trainer.save()

SimPO训练代码:

https://blog.csdn.net/weixin_43013480/article/details/141370269

# compute_log_probs 的作用是计算给定序列的 log 概率(对数概率),主要用于评估语言模型(LLM)的生成质量。

def compute_log_probs(
        self,
        model: AutoModelForCausalLM,
        batch: PreferenceBatch,
    ) -> torch.Tensor:
        """Compute log probabilities of given sequences."""
        logits = model(**self.infer_batch(batch)).logits
        device = logits.device
        input_ids = batch['input_ids']
        batch_size = len(batch['meta_info']['response_lens'])
        logprob_list = []
        for idx in range(batch_size):
            response_length = batch['meta_info']['response_lens'][idx]
            raw_input_id = strip_pad(input_ids[idx], self.tokenizer.pad_token_id)
            logit = logits[idx][-response_length:].unsqueeze(0)
            input_id = raw_input_id[-response_length:].unsqueeze(0)
            log_p = gather_log_probabilities(logit[:, :-1], input_id[:, 1:])
            logprob_list.append(log_p.squeeze(0))
        return torch.nn.utils.rnn.pad_sequence(
            logprob_list, batch_first=True, padding_value=0.0
        ).to(device)
class SimPOTrainer(DPOTrainer):

    def loss(  # pylint: disable=too-many-locals
        self,
        batch: PreferenceBatch,
    ) -> dict[str, torch.Tensor]:
        """Loss function for the SimPO algorithm."""
        sequence_log_probs = self.compute_log_probs(
            self.model.module,
            batch,
        )
# 使用 chunk 将 sequence_log_probs 按照第0维(批次维度)进行切分。将批次数据分为两部分:一部分对应 "更好"(better_sequence_log_probs),另一部分对应 "更差"(worse_sequence_log_probs)。每部分的大小为 (B, L - 1),B 是批次大小,L 是序列长度。  L-1 是为了删除最后的 <eos>
        (
            better_sequence_log_probs,  # size = (B, L - 1)
            worse_sequence_log_probs,  # size = (B, L - 1)
        ) = sequence_log_probs.chunk(chunks=2, dim=0)

        losses = []
        better_sample_rewards = []
        worse_sample_rewards = []

        better_input_ids, worse_input_ids = batch['input_ids'].chunk(chunks=2, dim=0)
        better_attention_mask, worse_attention_mask = batch['attention_mask'].chunk(chunks=2, dim=0)

        batch_size = better_input_ids.size(0)
        for i in range(batch_size):
#检查当前样本的 "更好" 和 "更差" 部分的 input_ids 是否相同。如果相同,跳过这个样本,因为它们对比不出差异。
            if torch.all(torch.eq(better_input_ids[i], worse_input_ids[i])).item():
                continue

#分别计算 "更好" 和 "更差" 样本的结束位置(通过 attention_mask 中的非零元素位置来确定)。
            better_end_index = better_attention_mask[i].nonzero()[-1].squeeze().item()
            worse_end_index = worse_attention_mask[i].nonzero()[-1].squeeze().item()
            better_input_length = better_end_index + 1
            worse_input_length = worse_end_index + 1
# diverge_index 是 "更好" 和 "更差" 样本之间的第一个差异位置。
            diverge_index = (
                (better_input_ids[i] != worse_input_ids[i]).nonzero()[0].squeeze().item()
            )
            assert 0 <= diverge_index <= better_end_index, 'diverge index is out of range!'
            assert 0 <= diverge_index <= worse_end_index, 'diverge index is out of range!'
#根据 diverge_index 进行切片,获取差异区域的对数概率。
#better_log_prob 和 worse_log_prob 是对应于 "更好" 和 "更差" 样本的对数概率的总和。
            better_seq_slice = slice(diverge_index, better_end_index + 1)
            worse_seq_slice = slice(diverge_index, worse_end_index + 1)
# 计算损失和奖励
            better_log_prob = better_sequence_log_probs[i, better_seq_slice].sum(dim=-1)
            worse_log_prob = worse_sequence_log_probs[i, worse_seq_slice].sum(dim=-1)
#在长度上归一化的奖励【/|y|】,其计算方式是使用策略模型的奖励中所有 token 的平均对数概率;
            better_log_ratio = better_log_prob / better_input_length
            worse_log_ratio = worse_log_prob / worse_input_length
#目标奖励差额γ,用以确保获胜和失败响应之间的奖励差超过这个差额γ
            losses.append(
                -F.logsigmoid(
                    self.cfgs.train_cfgs.scale_coeff * (better_log_ratio - worse_log_ratio)
                    - self.cfgs.train_cfgs.gamma,
                ),
            )
            better_sample_rewards.append(
                self.cfgs.train_cfgs.scale_coeff * better_log_ratio.detach(),
            )
            worse_sample_rewards.append(self.cfgs.train_cfgs.scale_coeff * worse_log_ratio.detach())
        loss = torch.stack(losses).mean()  # size = ()
        better_sample_reward = torch.stack(better_sample_rewards)  # size = (B,)
        worse_sample_reward = torch.stack(worse_sample_rewards)  # size = (B,)
        reward = better_sample_reward + worse_sample_reward  # size = (B,)
        reward_accuracy = (better_sample_reward > worse_sample_reward).float().mean()  # size = ()
        reward_margin = better_sample_reward - worse_sample_reward  # size = (B,)

        return {
            'loss': loss,
            'reward': reward,
            'better_sample_reward': better_sample_reward,
            'worse_sample_reward': worse_sample_reward,
            'reward_accuracy': reward_accuracy,
            'reward_margin': reward_margin,
        }

KTO训练代码:

#  创建 不匹配的提示-回答对:错位传入批次(batch)中的 answer_input_ids 和 answer_attention_mask 数据,以创建不匹配的提示-回答对。获取当前索引前一个样本作为回应(response)。如果当前索引是 0,则取最后一个样本作为回应。这是为了创建“不匹配”的数据对,即提示和回应不一定是成对的。
class UnmatchedSupervisedDataset(SupervisedDataset):

    def preprocess(
        self, raw_sample_for_prompt: dict[str, Any], raw_sample_for_response: dict[str, Any]
    ) -> SupervisedSample:
        return_dict = {}
        formatted_text, _ = self.template.format_unmatched_supervised_sample(
            raw_sample_for_prompt, raw_sample_for_response
        )
        return_dict['input_ids'] = self.tokenize(formatted_text)

        return return_dict

    def __getitem__(self, index: int) -> dict[str, torch.Tensor]:
        """Get a tokenized data sample by index."""
        raw_sample_for_prompt = self.raw_data[index]
        if index == 0:
            raw_sample_for_response = self.raw_data[-1]
        else:
            raw_sample_for_response = self.raw_data[index - 1]
        data = self.preprocess(raw_sample_for_prompt, raw_sample_for_response)
        return data

    def get_collator(self) -> Callable[[list[dict[str, torch.Tensor]]], dict[str, torch.Tensor]]:
        return UnmatchedSupervisedCollator(self.tokenizer.pad_token_id)

class KTOTrainer(DPOTrainer):

# 计算kl散度:通过计算当前模型(self.model.module)和参考模型(self.reference_model.module)之间的 KL 散度来比较它们的概率分布
# 选择最后一个 batch 的 KL 值可能只是实现上的简化。实际中,计算所有 batch 的 KL 散度并取平均,或者采取其他更复杂的策略,可能会增加额外的计算负担,而选择最后一个 batch 的 KL 值是一种更直接、简便的实现方式。
def compute_kl(self):
    random_dataset = UnmatchedSupervisedDataset(
        path=self.cfgs.data_cfgs.train_datasets,
        template=self.train_template,
        tokenizer=self.tokenizer,
        processor=self.processor,
        name=self.cfgs.data_cfgs.train_name,
        size=self.cfgs.data_cfgs.train_size,
        split=self.cfgs.data_cfgs.train_split,
        data_files=self.cfgs.data_cfgs.train_data_files,
        optional_args=self.cfgs.data_cfgs.train_optional_args,
    )
    seed = torch.randint(0, 100000, (1,)).item()
    torch.manual_seed(seed)
    self.random_dataloader = DataLoader(
        random_dataset,
        collate_fn=random_dataset.get_collator(),
        sampler=DistributedSampler(random_dataset, shuffle=True),
        batch_size=self.cfgs.train_cfgs.per_device_kl_batch_size,
    )
    for batch in self.random_dataloader:
        log_probs = self.compute_log_probs(  # size = (2 * B, L - 1)
            self.model.module,
            batch=batch,
        )
        ref_log_probs = self.compute_log_probs(  # size = (2 * B, L - 1)
            self.reference_model.module,
            batch=batch,
        )
        kl = (log_probs - ref_log_probs).mean()

        self.kl = max(kl, 0)
# 此方法是 DPO (Direct Preference Optimization) 算法的核心部分。它计算了在当前模型和参考模型之间的对比损失
    def loss(  # pylint: disable=too-many-locals
        self,
        batch: PreferenceBatch,
    ) -> dict[str, torch.Tensor]:
        """Loss function for the DPO algorithm."""
        sequence_log_probs = self.compute_log_probs(
            self.model.module,
            batch,
        )
        (
            better_sequence_log_probs,  # size = (B, L - 1)
            worse_sequence_log_probs,  # size = (B, L - 1)
        ) = sequence_log_probs.chunk(chunks=2, dim=0)

        with torch.no_grad():
            ref_sequence_log_probs = self.compute_log_probs(  # size = (2 * B, L - 1)
                self.reference_model.module,
                batch,
            )
            ref_better_sequence_log_probs, ref_worse_sequence_log_probs = (
                ref_sequence_log_probs.chunk(chunks=2, dim=0)
            )

        losses = []
        better_sample_rewards = []
        worse_sample_rewards = []

        better_input_ids, worse_input_ids = batch['input_ids'].chunk(chunks=2, dim=0)
        better_attention_mask, worse_attention_mask = batch['attention_mask'].chunk(chunks=2, dim=0)

        batch_size = better_input_ids.size(0)
        for i in range(batch_size):
            if torch.all(torch.eq(better_input_ids[i], worse_input_ids[i])).item():
                continue
            better_end_index = better_attention_mask[i].nonzero()[-1].squeeze().item()
            worse_end_index = worse_attention_mask[i].nonzero()[-1].squeeze().item()
            diverge_index = (
                (better_input_ids[i] != worse_input_ids[i]).nonzero()[0].squeeze().item()
            )
            assert 0 <= diverge_index <= better_end_index, 'diverge index is out of range!'
            assert 0 <= diverge_index <= worse_end_index, 'diverge index is out of range!'

            better_seq_slice = slice(diverge_index, better_end_index + 1)
            worse_seq_slice = slice(diverge_index, worse_end_index + 1)

            better_log_prob = better_sequence_log_probs[i, better_seq_slice].sum(dim=-1)
            worse_log_prob = worse_sequence_log_probs[i, worse_seq_slice].sum(dim=-1)
            ref_better_log_prob = ref_better_sequence_log_probs[i, better_seq_slice].sum(dim=-1)
            ref_worse_log_prob = ref_worse_sequence_log_probs[i, worse_seq_slice].sum(dim=-1)
            better_log_ratio = better_log_prob - ref_better_log_prob
            worse_log_ratio = worse_log_prob - ref_worse_log_prob

# 计算loss,kl值作为基准
            losses.append(
                self.cfgs.train_cfgs.scale_better
                * (1 - F.sigmoid(self.cfgs.train_cfgs.scale_coeff * (better_log_ratio - self.kl)))
                - self.cfgs.train_cfgs.scale_worse
                * (1 - F.sigmoid(self.cfgs.train_cfgs.scale_coeff * (self.kl - worse_log_ratio))),
            )
            better_sample_rewards.append(
                self.cfgs.train_cfgs.scale_coeff * better_log_ratio.detach(),
            )
            worse_sample_rewards.append(self.cfgs.train_cfgs.scale_coeff * worse_log_ratio.detach())
        loss = torch.stack(losses).mean()  # size = ()
        better_sample_reward = torch.stack(better_sample_rewards)  # size = (B,)
        worse_sample_reward = torch.stack(worse_sample_rewards)  # size = (B,)
        reward = better_sample_reward + worse_sample_reward  # size = (B,)
        reward_accuracy = (better_sample_reward > worse_sample_reward).float().mean()  # size = ()
        reward_margin = better_sample_reward - worse_sample_reward  # size = (B,)

        return {
            'loss': loss,
            'reward': reward,
            'better_sample_reward': better_sample_reward,
            'worse_sample_reward': worse_sample_reward,
            'reward_accuracy': reward_accuracy,
            'reward_margin': reward_margin,
        }
#执行训练步骤:这个方法在每一个训练步中计算并反向传播损失。它更新模型参数并计算并返回训练信息。
#奖励计算:通过 reward、better_sample_reward 和 worse_sample_reward 等指标来衡量模型的性能。
#全局平均:get_all_reduce_mean() 用于分布式训练,确保在多个设备上计算的值被平均,以保证训练的一致性。
def train_step(self, batch: PreferenceBatch) -> dict[str, Any]:
    """Perform a single training step for KTO."""
    loss_dict = self.loss(batch=batch)
    loss = loss_dict['loss']
    self.model.backward(loss)
    self.model.step()

    with torch.no_grad():
        reward = loss_dict['reward'].mean()
        better_sample_reward = loss_dict['better_sample_reward'].mean()
        worse_sample_reward = loss_dict['worse_sample_reward'].mean()
        reward_accuracy = loss_dict['reward_accuracy']
        reward_margin = loss_dict['reward_margin'].mean()

    loss = get_all_reduce_mean(loss)
    reward = get_all_reduce_mean(reward)
    better_sample_reward = get_all_reduce_mean(better_sample_reward)
    worse_sample_reward = get_all_reduce_mean(worse_sample_reward)
    reward_accuracy = get_all_reduce_mean(reward_accuracy)
    reward_margin = get_all_reduce_mean(reward_margin)

    return {
        'train/loss': loss.item(),
        'train/reward': reward.item(),
        'train/better_sample_reward': better_sample_reward.item(),
        'train/worse_sample_reward': worse_sample_reward.item(),
        'train/reward_accuracy': reward_accuracy.item(),
        'train/reward_margin': reward_margin.item(),
        'train/lr': self.model.optimizer.param_groups[0]['lr'],
    }

相关论文:

  • KTO,Kahneman-Tversky 优化,参阅论文《KTO: Model alignment as prospect theoretic optimization》。
  • DRO,直接奖励优化,参阅论文《Offline regularised reinforcement learning for large language models alignment》。
  • SimPO,简单偏好优化,参阅论文《SimPO: Simple preference optimization with a reference-free reward》

Baichuan-Omni-1.5 多模态语音大模型

Baichuan-Omni-1.5 🤗 | Baichuan-Omni-1.5-Base 🤗 | 技术报告 📖

OpenMM-Medical 🤗 | OpenAudioBench 🤗

Github: https://github.com/baichuan-inc/Baichuan-Omni-1.5/

Baichuan-Omni-1.5 是 Baichuan-omni 系列的最新、性能一流模型。该模型通过端到端方式训练和推理。与其他开源模型相比,Baichuan-Omni-1.5 在文本、图像、音频和视频输入的理解能力有显著提升,并支持了可控的实时语音对话和多模态实时交互的新功能。此外,Baichuan-Omni-1.5 也是目前最出色的开源医疗多模态模型。Baichuan-Omni-1.5 的主要特性包括:

  • 多模态理解和交互能力: Baichuan-Omni-1.5 接受图像、视频、文本、音频作为输入,并生成高质量文本和语音输出,能够在不损害任何模态能力的情况下实现无缝的高质量跨模态交互,并和用户进行实时语音对话。在针对全模态理解的综合评测基准 OminiBench 中,Baichuan-Omni-1.5 取得开源社区一流水平,并超过了 GPT-4o-mini
  • 优秀的视觉能力: Baichuan-Omni-1.5 在 OpenCompass 常用的十个视觉评测集上平均得分 73.3,在7B量级的大小下,在图像理解方面超越了 GPT-4o-mini,比GPT-4o-mini平均高出6分,并且和GPT-4o的结果相近。此外,视频理解表现也优于GPT-4V
  • 出色的语音能力: Baichuan-Omni-1.5 通过一个 8 层 RVQ 音频Tokenizer(Baichuan-Audio-Tokenizer),在 12.5 Hz 帧率下实现了语义和声学信息捕获的最佳平衡,支持高质量可控制声音的中英双语实时对话。Baichuan-Omni-1.5 在语音理解任务优于 GLM-4-Voice,并在语音对话的语义和声学评估中展现了开源模型中最高的语音生成性能。同时,我们还开源了音频理解和生成基准(OpenAudio-Bench),以评估音频的端到端能力。
  • 领先的医疗图像理解能力: 我们从开源数据集中收集了一个比较全面的医学理解基准(OpenMM-Medical)用于评估模型的医学能力。Baichuan-Omni-1.5 在 GMAI-MMBench 以及 OpenMM-Medical 上取得了最佳的表现。在 OpenMM-Medical 上,Baichuan-Omni-1.5 仅使用 7B 的 LLM 取得了 83.8% 的高分,超过 Qwen2-VL-72B 的 80.7%。
在图像、视频和音频模态上的评估。(左)Baichuan-Omni-1.5覆盖了比Qwen2 VL更多的模态,并且超越了当前领先的全模态模型VITA-1.5和MiniCPM-o 2.6。(右)各模态在所有基准测试中的平均得分。

简介

Baichuan-Omni-1.5是一款全模态模型,具备全模态理解能力,并提供端到端的音频生成能力。为了实现跨模态的流畅高质量互动,同时不妥协任何模态的能力,优先优化了三个关键方面。首先,建立了一个全面的数据清洗和合成管道,用于多模态数据处理,获得了约5000亿条高质量数据(包括文本、音频和视觉数据)。其次,设计了一种音频分词器(Baichuan-Audio-Tokenizer),能够同时捕捉音频中的语义和声学信息,从而实现无缝集成,并提升与多模态大语言模型(MLLM)的兼容性。最后设计了一个多阶段的训练策略,逐步整合多模态对齐和多任务微调,确保各模态之间的有效协同。Baichuan-Omni-1.5在全模态能力方面领先于当前的模型(包括GPT4o-mini和MiniCPM-o 2.6)。值得注意的是,在多个多模态医学基准测试中,它取得了与领先模型(如Qwen2-VL-72B)相媲美的成绩。

与开源对手相比,Baichuan-Omni-1.5在文本、图像、音频和视频输入的理解能力上表现出显著的提升。特别地,该模型在可控实时语音互动和跨模态实时协同理解方面展示了令人印象深刻的能力。除了其通用能力外,Baichuan-Omni-1.5在医疗领域作为最出色的多模态大语言模型(MLLM)脱颖而出。这为人工智能在促进人类社会福祉方面开辟了令人兴奋的新可能性。Baichuan-Omni-1.5的架构如下图所示。根据评估结果,我们总结了Baichuan-Omni-1.5的关键优势和贡献:

全模态互动:Baichuan-Omni-1.5旨在处理文本、图像、音频和视频输入,输出高质量的文本和语音。它能够实现无缝、高质量的跨模态互动,且不会妥协任何模态的能力。

卓越的视觉-语言能力:Baichuan-Omni-1.5在十个图像理解基准测试中平均得分为73.3,超越GPT-4o-mini平均6分。

统一且出色的语音能力:我们设计了一种8层的RVQ音频分词器(Baichuan-Audio-Tokenizer),在捕捉语义和声学信息之间达到了最佳平衡,帧率为12.5Hz,支持高质量的可控双语(中文和英文)实时对话。同时,我们还开源了音频理解和生成基准(OpenAudioBench),用于评估音频的端到端能力。

领先的医学图像理解能力:我们收集了一个全面的医学理解基准:OpenMM-Medical,这是多个现有数据集的整合。我们的模型在GMAI-MMBench和OpenMM-Medical上取得了最先进的表现。具体来说,在OpenMM-Medical上,Baichuan-Omni-1.5使用7B大语言模型取得了83.8%的得分,超过了Qwen2-VL-72B的80.7%。

模型架构:

Baichuan-Omni-1.5 的架构。我们的模型旨在处理纯文本/音频输入以及视频/图像与文本/音频的组合。在生成音频时,Baichuan-Omni-1.5 LLM Decoder 交替预测文本标记和音频标记。然后,音频解码器对音频令牌进行解码,以生成最终音频。

Baichuan-Omni-1.5

高质量预训练数据

Baichuan-Omni-1.5的预训练数据集展示:我们构建了一个广泛的全模态数据集,包括文本、图像-文本、视频-文本、音频-文本及它们之间的交互。我们的数据集还包含了交织的图像-音频-文本和视频-音频-文本数据。

构建了全面且高质量的跨模态数据集,包含文本、图像-文本、视频-文本、音频-文本及它们之间的交互

图像数据:我们将图像训练数据分为三种类型:交织的图像-文本数据、图像描述数据和问答数据。为了提高数据的多样性并提升模型性能,我们采用了以下两种图像数据合成策略:

1)我们利用公司内收集的书籍和论文,并解析它们生成交织的图像-文本、OCR数据和图表数据。这些数据具有高度的完整性、专业性和知识密集性。

2)我们根据[19]的方案,训练了一个专用的描述模型,能够生成所需的图像描述,如OCR提示。这些描述提供了图像内容的深入说明。

3)目前,大量的开源数据集主要是英文数据。为了避免模型的中文能力下降,我们合成了大量的中文描述和交织数据。

视频数据:视频数据集包含了广泛的公开资源,涵盖了视频分类 、动作识别 和时间定位等多种任务。视频-文本数据源可以分为视频描述数据和视频问答(QA)数据。

音频数据:音频数据可以大致分为两种主要类型:音频理解数据和音频生成数据。音频理解数据包括自动语音识别(ASR)、音频问答(AQA)、语音转文本翻译和音频-文本交织数据。音频生成数据包括文本转语音(TTS)、交织的文本转语音数据和纯音频数据。交织数据由文本和音频模态交替组成,并以标点符号分隔,以促进跨模态知识的迁移。完全对齐的生成数据由完全对齐的文本和音频内容组成,旨在增强模型在文本监督下生成音频标记的能力。音频-文本配对数据(例如ASR和TTS数据)提高了基础语音任务的表现。纯音频数据则增强了独立处理音频模态的能力。

文本数据:为了构建一个高质量的文本语料库,我们从各种来源聚合了数据,包括网页、书籍、学术论文、代码等。我们遵循之前研究中的数据处理指南,采用严格的选择方法,旨在提高文本语料库的多样性和质量。这种多样性确保训练语料库涵盖了广泛的主题和语言风格,适用于不同的应用。同时,我们的高质量处理技术旨在消除冗余和过滤噪声,从而丰富数据集的信息密度和整体效用。最终,我们获得了1.507亿条纯文本数据。

跨模态交互数据:为了增强模型的跨模态交互能力,我们合成了一系列跨模态交互数据集,涵盖了图像-音频-文本视频-音频-文本格式。图像-文本数据来源包括两种类型:图像-文本描述数据和图像-文本交织数据。具体而言,文本数据首先在句子级别进行分割。然后,将四分之一的文本通过我们的内部文本转语音(TTS)接口转换为音频元素。随后,我们利用生成的音频元素替换原始图像-文本数据中的相应文本句子。这种方法通过将多样化的音频元素融入现有的文本内容,促进了丰富的跨模态交互框架。我们的音频数据包含44种不同的语音类型,确保了语调的多样性。此设置配有任务提示,如“请听以下音频,描述图像的内容。您的任务是在听完后结合音频和图像补充额外信息”,旨在预测剩余的三分之二文本描述。对于视频-文本数据集,音频成分直接从原始视频中提取,作为跨模态的音频元素。总计,我们生成了1000亿个用于跨模态交互的标记数据。

模型结构:

Baichuan-Omni-1.5是一个统一的全模态模型,由视觉分支、音频分支和一个预训练的大型语言模型(LLM)主干组成,支持文本、音频、视觉输入以及端到端的文本和音频输出。

视觉分支
像当前主流的MLLM一样,视觉分支旨在将图像和视频输入处理为视觉标记,并将其与文本标记一起输入到LLM中。我们使用Qwen2-VL的NaViT作为视觉编码器,该编码器能够动态处理任意分辨率和纵横比的图像和视频。然后,我们应用一个由两层MLP组成的视觉投影器,将视觉特征压缩为2×2的因子,从而在性能和效率之间取得平衡。

音频分支
音频分支扩展了LLM,使其能够支持端到端的语音输入和输出。这通过引入Baichuan-Audio-Tokenizer和基于流匹配的解码器来实现,前者负责将音频信号转换为离散标记,后者负责将音频标记解码为语音波形。我们在下图中展示了详细信息。

Baichuan-Audio-Tokenizer基于残差向量量化(RVQ)和多目标训练,帧率为12.5 Hz。在使用Whisper Large Encoder 从Mel谱图特征中提取高级特征后,残差卷积网络执行下采样以获得低帧率序列特征。然后使用8层残差向量量化器对这些特征进行量化,生成音频标记。这些标记随后被输入到音频解码器和预训练的LLM中,分别执行Mel谱图重建和转录预测。音频解码器采用与Whisper编码器对称的结构,并使用多尺度Mel损失来增强声音重建的质量。在训练过程中,预训练LLM的参数保持不变,以确保音频标记器和文本空间之间的语义对齐。

除了传统的任务如ASR、AQA和S2TT外,我们还将一定比例的交织文本-音频数据融入其中,以提高VQ模块建模复杂上下文场景的能力。

为了进一步增强合成音频的质量和感知逼真度,音频解码器模块通过流匹配模型进行优化。借鉴Matcha-TTS 和CosyVoice 的设计,U-Net包括一个单独的下采样块、一个上采样块和12个中间块。具体而言,流匹配解码器在24 kHz音频数据上进行训练,以生成目标Mel谱图,然后使用HiFi-GAN 声码器将其转换为语音波形。

多阶段模型训练:

图像-文本预训练

图像-文本预训练阶段扩展了LLM,使其能够处理和理解视觉输入,使用3000亿图像-文本样本,该阶段可以分为两个部分。

第一阶段:在第一阶段,我们训练视觉投影器,利用开源图像描述数据(例如LAION-5B数据集),建立图像表示与文本之间的初步对齐。在此阶段,我们冻结LLM和视觉编码器,仅训练视觉投影器,学习率为1e−3。

第二阶段:在第二阶段,我们解冻视觉编码器和LLM,以促进图像和文本表示之间更好的对齐。具体来说,我们以学习率1e−5训练LLM和视觉投影器,并以更低的学习率1e−6训练视觉编码器。我们使用公共和内部图像文本数据,包含交织数据和图像描述数据,以增强视觉-语言的表现力。具体来说,我们收集并标注高质量的OCR数据和图表数据,以增强文本/图表识别和理解能力。此外,我们还使用高质量的纯文本数据,这些数据占总数据的40%,以更好地保持语言模型的原始能力。

图像-音频-文本预训练

图像-音频-文本预训练阶段扩展了一个预训练在视觉数据上的LLM,使其能够以端到端的方式理解音频数据,使用887k小时的语音-文本数据,并结合我们的Baichuan-Audio-Tokenizer、新引入的音频嵌入层和独立音频头

具体来说,Baichuan-Audio-Tokenizer生成的音频令牌首先通过音频嵌入层转化为音频嵌入。音频LLM交替生成对齐的文本令牌和音频令牌,使用一个特殊令牌实现文本与音频之间的模态切换。生成的音频令牌由独立的音频头处理,该音频头基于先前的工作设计,包含3层深度变换器和8个分类头。

为了缓解语音和文本特征之间显著差异带来的冲突,我们参考了之前的工作,采用音频和文本数据交织的方法进行预训练。此外,采用了两阶段训练策略,以保持原始LLM的文本知识,同时有效地整合音频模态。

第一阶段:在第一阶段,我们冻结LLM、视觉模块和音频标记器的参数,只有音频嵌入层和音频头的参数更新,学习率为1e−4。我们在这一阶段使用包括ASR、TTS、INTLV和ITTS数据的音频数据。

第二阶段:在第二阶段,训练扩展到除视觉编码器和音频标记器之外的所有参数,学习率为1e−5。具体来说,我们使用音频数据、图像数据和纯文本数据,分别占比0.2、0.4和0.4,这可以更好地提升音频能力,同时保持视觉和语言能力。

全模态预训练

基于之前预训练阶段获得的视觉和音频能力,我们继续使用高质量的跨模态交互数据集进行训练,数据集涵盖图像-音频-文本和视频-音频-文本格式,并将最大序列长度扩展到64k,以支持长时间的语音和视频流。具体来说,输入的视频帧以每秒1帧的速率进行采样,每个视频最多包含32帧。每个输入帧被调整为最大分辨率560×1120像素,以保持最佳质量和细节。这一精心配置在性能和效率之间达到了平衡,促进了有效的模型训练,同时管理了计算负载。此训练过程使用4e−6的低学习率,以进一步优化与语言模态和跨模态交互的对齐。

多模态监督微调

在本节中,我们描述了全模态监督微调(SFT)阶段,旨在增强模型在各种任务中执行复杂全模态指令的能力。我们收集了包括开源、合成和内部注释数据在内的综合数据集。这些数据集涵盖了多个任务,包含大约1700万个数据对,跨越文本、音频、图像-文本、视频-文本和图像-音频组合等多种模态。关于这些数据类型和数量的详细信息见表4。

实验

如表6所示,Baichuan-Omni-1.5 在纯文本基准测试中表现出色,特别是在与仅专注于语言模态的开源LLM模型相比时。例如,在通用MMLU基准测试中,Llama3-Instruct的得分为67.1%,而Baichuan-Omni-1.5则达到了72.2%。Baichuan-Omni-1.5在语言模态上的成功,主要归功于我们在训练策略上的调整以及多模态训练数据的平衡比例,其中保持了适当比例的纯文本数据。这些结果表明,我们的数据合成与平衡方法,以及多阶段的训练策略,能够有效解决在多模态训练过程中纯语言任务性能下降的问题。此外,相较于最新的开源多模态模型MiniCPM-o 2.6,Baichuan-Omni-1.5在中文基准测试中展示了明显的优势,例如CMMLU(63.3%对75.5%)和C-Eval(61.5%对73.1%),并且在通用基准测试中也大大超过了MiniCPM-o 2.6,MMLU(65.3%对72.2%)和AGIEval(50.9%对54.4%)。这些结果表明,相较于当前的全模态模型,这些模型在训练非文本模态数据后可能会导致文本理解能力下降,而我们的模型在理解纯文本方面依然保持强大能力。

如表7和表8所示,显然,我们的模型在大多数基准测试中优于最新的开源模型VITA-1.5和MiniCPM-o 2.6。例如,与最近的MiniCPM-o 2.6相比,我们的模型在包括MMBench、SEED-IMG、MME和MMMU在内的十个基准测试中的六个上表现更好,这些基准测试要求具备专家级的感知和推理能力。这表明,我们的全模态模型已经处于开源模型的前沿。此外,与其他非全模态模型相比,Baichuan-Omni-1.5也取得了相当或更优的表现。例如,与MiniCPM-Llama3-V 2.5相比,我们的模型在大多数视觉问答(VQA)任务中表现更好。总体而言,与Qwen2-VL-7B相比,我们的模型在各类图像理解基准测试中的表现相当。我们的模型在MMBench-CN(81.9%对83.6%)、MMMU(52.7%对53.9%)、MathVista-mini(58.2%对63.6%)和ChartQA(83.0%对84.9%)等方面取得了更好的表现。此外,值得注意的是,在MMBench-EN/CN和OCRBench上,我们的模型已经超越了像GPT4o这样的闭源模型。

总结

在这项工作中,我们介绍了Baichuan-Omni-1.5,一个全模态模型,代表了朝着开发一个涵盖所有人类感官的综合框架迈出的重要一步。通过使用高质量的多模态数据以及多阶段的全模态预训练和微调策略,Baichuan-Omni-1.5在处理视频、图像、文本和音频理解方面取得了优异的表现。Baichuan-Omni-1.5的关键特点包括:(1) 强大的纯文本和多模态理解能力;(2) 全模态输入(文本、图像、视频、文本)和双模态输出(文本和音频)的端到端并行处理;(3) 在医疗场景中的卓越表现;以及(4) 高质量的可控音频生成。

尽管这些结果很有前景,但每种模态的基础能力仍有相当大的改进空间。即:(1) 增强文本理解能力;(2) 支持更长的视频帧理解;以及(3) 改进音频理解和生成,不仅能识别人类声音,还能识别自然环境中的声音,如流水声、鸟鸣声和碰撞声等。

我们的未来研究将专注于完善这些领域,以确保开发出更复杂、更通用的模型,能够理解和与复杂环境互动。我们预计,在这些领域的持续进展将对实现人工通用智能(AGI)的更广泛目标做出重要贡献。

Kokoro TTS:支持多语言的轻量级TTS模型

最近,HuggingFace模型趋势榜上有一个很火的开源模型Kokoro-82M

开源模型:https://huggingface.co/hexgrad/Kokoro-82M

Kokoro-82M不是大模型,而是一个参数量只有82M的TTS(Text-to-Speech)模型。虽然模型不大,但是Kokoro-82M在TTS Arena榜单上排行第一!TTS Arena 是一个用于评估语音合成模型的平台,其灵感来源于 LMsys 的 Chatbot Arena。用户可以通过输入文本并对比两个模型的合成语音来投票选择更自然的结果,模型名称在投票后才会显示。该平台旨在解决语音合成领域缺乏有效质量评估方法的问题,通过公开排名使结果更透明且易于访问。虽然这个排行榜很有参考意义。

在人工智能语音合成技术快速发展的今天,Kokoro TTS 以其轻量级设计和高效性能脱颖而出。作为一个仅有82M参数的文本转语音(TTS)模型,Kokoro 在 TTS Spaces Arena 中击败了许多参数规模更大的竞争对手,成为语音合成领域的一颗新星。

模型架构与参数规模

Kokoro TTS 基于 StyleTTS 2 架构,其参数规模仅为82M,远低于许多主流 TTS 模型(如 XTTS v2 的467M 参数和 MetaVoice 的1.2B 参数),但在单声道设置下表现卓越。

支持的语音与语言

Kokoro 最新版(0.23)支持多语言支持与声音克隆,包括:中、英、法、日、韩。每种语言支持多种音色以及男、女声,每种语音包都经过专业调校,确保音质清晰自然。英语支持美国英语和英国英语,并提供了10种独特的语音包,包括男声和女声(如 af_bella、af_sarah、am_adam 等)。

不过还不支持中文或韩文中与英语混合的情况。

性能优势与创新点

Kokoro 的训练数据量不到100小时,远低于其他模型(如 XTTS v2 的10,000小时),但其在 TTS Spaces Arena 中排名第一,证明了其在参数效率上的优势。此外,Kokoro 采用 espeak-ng 进行字形到音素(g2p)转换,进一步提升了语音合成的自然度。

本地部署步骤

模型地址:https://huggingface.co/hexgrad/Kokoro-82M

以下步骤为notebook中使用

# 1️⃣ Install dependencies silently
!git lfs install
!git clone https://huggingface.co/hexgrad/Kokoro-82M
%cd Kokoro-82M
!apt-get -qq -y install espeak-ng > /dev/null 2>&1
!pip install -q phonemizer torch transformers scipy munch

# 2️⃣ Build the model and load the default voicepack
from models import build_model
import torch
device = 'cuda' if torch.cuda.is_available() else 'cpu'
MODEL = build_model('kokoro-v0_19.pth', device)
VOICE_NAME = [
    'af', # Default voice is a 50-50 mix of Bella & Sarah
    'af_bella', 'af_sarah', 'am_adam', 'am_michael',
    'bf_emma', 'bf_isabella', 'bm_george', 'bm_lewis',
    'af_nicole', 'af_sky',
][0]
VOICEPACK = torch.load(f'voices/{VOICE_NAME}.pt', weights_only=True).to(device)
print(f'Loaded voice: {VOICE_NAME}')

# 3️⃣ Call generate, which returns 24khz audio and the phonemes used
from kokoro import generate
text = "How could I know? It's an unanswerable question. Like asking an unborn child if they'll lead a good life. They haven't even been born."
audio, out_ps = generate(MODEL, text, VOICEPACK, lang=VOICE_NAME[0])
# Language is determined by the first letter of the VOICE_NAME:
#    'a' => American English => en-us
#    'b' => British English => en-gb

# 4️⃣ Display the 24khz audio and print the output phonemes
from IPython.display import display, Audio
display(Audio(data=audio, rate=24000, autoplay=True))
print(out_ps)

API 接口与 Docker 化部署

Kokoro-FastAPI 是一个基于 Docker 的 FastAPI 封装,支持 NVIDIA GPU 加速和队列处理功能。用户可以通过 API 接口发送文本转语音请求,并获取高质量的语音输出。

Kokoro-FastAPI地址:https://github.com/remsky/Kokoro-FastAPI

TangoFlux-TTA 高效的文本到音频 (TTA) 生成模型

NVIDIA发布了新模型TangoFlux,TangoFlux和Flux采用类似的MMDiT架构,但与Flux不同的是,TangoFlux是用于根据文本来生成与之匹配的音频(Text-to-Audio,TTA)。注意,TTA与文本生成语音(Text-to-Speech,TTS)是两个不同的任务,TTS是根据文本合成口语化的语音,而TTA更复杂,是根据文本内容生成相应的背景音、环境音或者情感表达的音频。TangoFlux模型参数只有515M,能够在单个A40 GPU上仅用3.7秒生成长达30秒的44.1kHz音频,而且效果上实现了SOTA,所以是一个又快又好的TTA模型。目前,TangoFlux的代码和代码均已经开源:

在对齐 TTA(文本到音频)模型时,一个关键挑战在于生成偏好对的困难,因为 TTA 缺乏像大型语言模型(LLMs)那样的结构化机制,例如可验证的奖励或黄金标准答案。为了解决这一问题,我们提出了一种新颖的框架——CLAP 排序偏好优化(CRPO),通过迭代生成和优化偏好数据来增强 TTA 的对齐性能。研究表明,使用 CRPO 生成的音频偏好数据集优于现有的替代方案。借助这一框架,TangoFlux 在客观和主观基准测试中均达到了最先进的性能。

贡献

  • 引入了 TANGOFLUX,这是一种基于修正流的小型高效 TTA 模型,能够在完全非专有的训练数据上实现最先进的性能。
  • 提出了 CRPO,这是一种简单而有效的策略,用于生成音频偏好数据并对修正流进行对齐,其在音频偏好数据集上的表现优于其他方法。
  • 公开发布了代码和模型权重,以促进文本到音频生成领域的研究。

方法:

TangoFlux 由 FluxTransformer 块组成,这些块是基于扩散变换器(Diffusion Transformer, DiT,Peebles & Xie,2023)和多模态扩散变换器(Multimodal Diffusion Transformer, MMDiT,Esser 等,2024)的模型,通过文本提示和时长嵌入进行条件化,以生成最高 44.1kHz、时长达 30 秒的音频。TangoFlux 从通过变分自动编码器(VAE,Kingma & Welling,2022)编码的音频潜在表示中学习修正流轨迹。

TangoFlux 的训练流程包括三个阶段:预训练、微调和偏好优化。通过 CRPO 对 TangoFlux 进行对齐,CRPO 通过迭代生成新的合成数据并构建偏好对,执行偏好优化。整体训练流程如图 1 所示。

TangoFlux在模型架构上参考了Flux,也是采用混合MMDiT和DiT block的transformer,首先是6层MMDiT block,然后跟着18层DiT block,模型的特征维度是1024,总参数量为515M。类似SD和Flux,这里也是采用了一个音频VAE(来源Stable Audio Open)将音频编码成一定长度的latents,然后用DiT来生成latents。这里的文本编码器采用FLAN-T5,除了文本特征,还用一个小的网络将音频时长编码成一个embedding,并和文本特征拼接在一起,从而实现对生成音频长度的控制。训练也是采用Flow Matching。

音频编码

使用 Stable Audio Open Evans et al. 的 VAE,它能够将 44.1kHz 的立体声音频波形编码为音频潜在表示。给定一个立体声音频 X∈ℝ2×d×s⁢r ,其中 d 是 时长duration 和 s⁢r 是采样率 sampling rate,VAE 编码 X 为潜在表示 Z∈ℝL×C ,其中 L ,C 分别是潜在序列长度和通道大小。VAE 将 latent 表示 Z 解码回原始立体声音频 X 。整个 VAE 在 TangoFlux 训练期间保持冻结。

Model Conditioning

为了实现不同长度音频的可控生成,我们采用了文本调节和持续时间调节。文本调节根据提供的描述控制生成的音频的事件,而持续时间调节指定所需的音频长度,最长可达 30 秒。

文本条件。给定音频的文本描述,我们从预训练的文本编码器FLAN-T5中获取文本编码 ct⁢e⁢x⁢t 

持续时间编码。为了生成可变长度的音频,我们首先使用一个小型神经网络将音频持续时间编码成一个 duration embedding cd⁢u⁢r 。这与文本编码 ct⁢e⁢x⁢t 连接并馈送到 TangoFlux 以控制音频输出的持续时间。

模型架构

采用混合 MMDiT 和 DiT 架构作为 TangoFlux 的主干, 首先是6层MMDiT block,然后跟着18层DiT block,模型的特征维度是1024,总参数量为515M

Flow Matching

流匹配(Flow Matching)基于连续归一化流框架。该方法通过学习一个时间相关的向量场,将来自简单先验分布(例如高斯分布)的样本映射到复杂的目标分布,从而生成目标分布的样本。

在 TTA(文本到音频)领域的先前研究中,例如 AudioBox(Vyas 等,2023)和 Voicebox(Le 等,2023),主要采用了 Lipman 等(2023)提出的最优传输条件路径(Optimal Transport conditional path)。然而,我们的方法采用了 修正流(Rectified Flows,Liu 等,2022),这是一种从噪声到目标分布的直线路径,代表了最短路径

整流流(Rectified Flows)。给定音频样本的潜在表示 x₁ 和服从正态分布 x₀ ∼ N(0, I) 的噪声样本,通过时间步 t ∈ [0, 1] 可以构建训练样本 xₜ。模型通过学习预测速度 vₜ = dxₜ/dt 来引导 xₜ 向 x₁ 演化。尽管存在多种构建传输路径 xₜ 的方法,我们采用了 Liu 等人(2022)提出的整流流(RFs)。该方法在目标分布与噪声分布之间构建直线路径作为前向过程,其定义如公式(1)所示。经验表明,当减少采样步数时,整流流具有更高的采样效率且性能下降更少(Esser 等人,2024)。我们用 θ 表示模型 u 的参数,该模型通过直接回归预测速度 u(xₜ, t; θ) 与真实速度 vₜ 的匹配,其损失函数如公式(2)所示。

推理。在推理过程中,我们从先验分布 x~0∼𝒩⁢(𝟎,𝐈) 中采样噪声,并使用常微分方程求解器根据模型在每个时间步 t 长预测的速度 vt 来计算 x1 。在此过程中,我们使用 Euler 求解器。

CLAP 排名偏好优化 (CRPO)

CLAP 排名偏好优化 (CRPO) 利用文本-音频联合嵌入模型作为代理奖励模型,根据与输入描述的相似性对生成的音频进行排名,然后构建偏好对。

我们首先设置了一个 Ta ngoFlux 架构的预训练检查点作为要对齐的基础模型,用 π0 表示。此后,CRPO 迭代地将 checkpoint πk≔u⁢(⋅;θk) 对齐到 checkpoint πk+1 中,从 k=0 开始。每个这样的对齐迭代都包括三个步骤:(i) 批量在线数据生成,(ii) 奖励估计和偏好数据集创建,以及 (iii) πk+1 通过直接偏好优化进行微调 πk 。

Main Results

表 1:跨各种指标的音频生成模型比较。Output length 表示生成的音频的持续时间。度量包括 FD 
openl3表示 Frechet 距离、 passt KL 表示 KL 散度和 CLAP score 表示对齐。所有推理时间都是在同一个 A40 GPU 上计算的。我们在 #Params 列中报告可训练参数。

表 1 在客观指标方面将 TangoFlux 与 AudioCaps 上先前的文本到音频生成模型进行了比较。表 2 报告了具有多个事件的提示(即多事件提示)的模型性能。

Qwen2 Lora LLM微调训练教程

Github:https://github.com/datawhalechina/self-llm/tree/master

 本项目是一个围绕开源大模型、针对国内初学者、基于 Linux 平台的中国宝宝专属大模型教程,针对各类开源大模型提供包括环境配置、本地部署、高效微调等技能在内的全流程指导,简化开源大模型的部署、使用和应用流程,让更多的普通学生、研究者更好地使用开源大模型,帮助开源、自由的大模型更快融入到普通学习者的生活中。

  本项目的主要内容包括:

  1. 基于 Linux 平台的开源 LLM 环境配置指南,针对不同模型要求提供不同的详细环境配置步骤;
  2. 针对国内外主流开源 LLM 的部署使用教程,包括 LLaMA、ChatGLM、InternLM 等;
  3. 开源 LLM 的部署应用指导,包括命令行调用、在线 Demo 部署、LangChain 框架集成等;
  4. 开源 LLM 的全量微调、高效微调方法,包括分布式全量微调、LoRA、ptuning 等。

环境配置

在完成基本环境配置和本地模型部署的情况下,你还需要安装一些第三方库,可以使用以下命令:

python -m pip install --upgrade pip
# 更换 pypi 源加速库的安装
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

pip install modelscope==1.18.0
pip install transformers==4.44.2
pip install streamlit==1.24.0
pip install sentencepiece==0.2.0
pip install accelerate==0.34.2
pip install datasets==2.20.0
pip install peft==0.11.1

模型下载

使用 modelscope 中的 snapshot_download 函数下载模型,第一个参数为模型名称,参数 cache_dir 为模型的下载路径。

在 /root/autodl-tmp 路径下新建 model_download.py 文件并在其中输入以下内容,粘贴代码后请及时保存文件,如下图所示。并运行 python /root/autodl-tmp/model_download.py 执行下载,模型大小为 15GB,下载模型大概需要 5 分钟。

import torch
from modelscope import snapshot_download, AutoModel, AutoTokenizer
import os
model_dir = snapshot_download('qwen/Qwen2.5-7B-Instruct', cache_dir='/root/autodl-tmp', revision='master')

指令集构建

LLM 的微调一般指指令微调过程。所谓指令微调,是说我们使用的微调数据形如:

{
  "instruction": "回答以下用户问题,仅输出答案。",
  "input": "1+1等于几?",
  "output": "2"
}

其中,instruction 是用户指令,告知模型其需要完成的任务;input 是用户输入,是完成用户指令所必须的输入内容;output 是模型应该给出的输出。

即我们的核心训练目标是让模型具有理解并遵循用户指令的能力。因此,在指令集构建时,我们应针对我们的目标任务,针对性构建任务指令集。例如,在本节我们使用由笔者合作开源的 Chat-甄嬛 项目作为示例,我们的目标是构建一个能够模拟甄嬛对话风格的个性化 LLM,因此我们构造的指令形如:

{
  "instruction": "你是谁?",
  "input": "",
  "output": "家父是大理寺少卿甄远道。"
}

我们所构造的全部指令数据集在根目录下。

数据格式化

Lora 训练的数据是需要经过格式化、编码之后再输入给模型进行训练的,如果是熟悉 Pytorch 模型训练流程的同学会知道,我们一般需要将输入文本编码为 input_ids,将输出文本编码为 labels,编码之后的结果都是多维的向量。我们首先定义一个预处理函数,这个函数用于对每一个样本,编码其输入、输出文本并返回一个编码后的字典:

def process_func(example):
    MAX_LENGTH = 384    # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性
    input_ids, attention_mask, labels = [], [], []
    instruction = tokenizer(f"<|im_start|>system\n现在你要扮演皇帝身边的女人--甄嬛<|im_end|>\n<|im_start|>user\n{example['instruction'] + example['input']}<|im_end|>\n<|im_start|>assistant\n", add_special_tokens=False)  # add_special_tokens 不在开头加 special_tokens
    response = tokenizer(f"{example['output']}", add_special_tokens=False)
    input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
    attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]  # 因为eos token咱们也是要关注的所以 补充为1
    labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]
    if len(input_ids) > MAX_LENGTH:  # 做一个截断
        input_ids = input_ids[:MAX_LENGTH]
        attention_mask = attention_mask[:MAX_LENGTH]
        labels = labels[:MAX_LENGTH]
    return {
        "input_ids": input_ids,
        "attention_mask": attention_mask,
        "labels": labels
    }

Qwen2 采用的 Prompt Template格式如下:

<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
你是谁?<|im_end|>
<|im_start|>assistant
我是一个有用的助手。<|im_end|>

加载 tokenizer 和半精度模型

模型以半精度形式加载,如果你的显卡比较新的话,可以用 torch.bfolat形式加载。对于自定义的模型一定要指定 trust_remote_code参数为 True

tokenizer = AutoTokenizer.from_pretrained('/root/autodl-tmp/qwen/Qwen2.5-7B-Instruct/', use_fast=False, trust_remote_code=True)

model = AutoModelForCausalLM.from_pretrained('/root/autodl-tmp/qwen/Qwen2.5-7B-Instruct/', device_map="auto",torch_dtype=torch.bfloat16)

定义 LoraConfig

LoraConfig这个类中可以设置很多参数,但主要的参数没多少,简单讲一讲,感兴趣的同学可以直接看源码。

  • task_type:模型类型
  • target_modules:需要训练的模型层的名字,主要就是 attention部分的层,不同的模型对应的层的名字不同,可以传入数组,也可以字符串,也可以正则表达式。
  • rlora的秩,具体可以看 Lora原理
  • lora_alphaLora alaph,具体作用参见 Lora 原理

Lora的缩放是啥嘞?当然不是 r(秩),这个缩放就是 lora_alpha/r, 在这个 LoraConfig中缩放就是 4 倍。

config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
    inference_mode=False, # 训练模式
    r=8, # Lora 秩
    lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理
    lora_dropout=0.1# Dropout 比例
)

自定义 TrainingArguments 参数

TrainingArguments这个类的源码也介绍了每个参数的具体作用,当然大家可以来自行探索,这里就简单说几个常用的。

  • output_dir:模型的输出路径
  • per_device_train_batch_size:顾名思义 batch_size
  • gradient_accumulation_steps: 梯度累加,如果你的显存比较小,那可以把 batch_size 设置小一点,梯度累加增大一些。
  • logging_steps:多少步,输出一次 log
  • num_train_epochs:顾名思义 epoch
  • gradient_checkpointing:梯度检查,这个一旦开启,模型就必须执行 model.enable_input_require_grads(),这个原理大家可以自行探索,这里就不细说了。
args = TrainingArguments(
    output_dir="./output/Qwen2.5_instruct_lora",
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    logging_steps=10,
    num_train_epochs=3,
    save_steps=100,
    learning_rate=1e-4,
    save_on_each_node=True,
    gradient_checkpointing=True
)

使用 Trainer 训练

trainer = Trainer(
    model=model,
    args=args,
    train_dataset=tokenized_id,
    data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)
trainer.train()

加载 lora 权重推理

训练好了之后可以使用如下方式加载 lora权重进行推理:

from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
from peft import PeftModel

model_path = '/root/autodl-tmp/qwen/Qwen2.5-7B-Instruct/'
lora_path = 'lora_path'

# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_path)

# 加载模型
model = AutoModelForCausalLM.from_pretrained(model_path, device_map="auto",torch_dtype=torch.bfloat16)

# 加载lora权重
model = PeftModel.from_pretrained(model, model_id=lora_path, config=config)

prompt = "你是谁?"
messages = [
    {"role": "system", "content": "现在你要扮演皇帝身边的女人--甄嬛"},
    {"role": "user", "content": prompt}
]

text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

model_inputs = tokenizer([text], return_tensors="pt").to('cuda')

generated_ids = model.generate(
    model_inputs.input_ids,
    max_new_tokens=512
)
generated_ids = [
    output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]

response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]

print(response)

MinMo: A Multimodal Large Language Model for Seamless Voice Interaction

FunAudioLLM Team   Tongyi Lab, Alibaba Group

用于语音交互的语音-文本多模态模型的先前工作可以大致分为 原生 模型和 对齐 模型。原生多模态模型使用单个框架同时对语音和文本的端到端理解和生成进行建模,然而,他们面临着语音和文本序列长度之间巨大差异、语音预训练不足以及灾难性地忘记文本LLMs的挑战;对齐的多模态模型在保持文本LLMs,然而,现有模型通常在小规模语音数据上进行训练,在有限的语音任务集上进行研究,并且缺乏对丰富而细致的说话风格的指令跟踪能力的系统探索

MinMo,这是一种多模态大型语言模型,具有大约 8B 参数,可实现无缝语音交互。 解决了先前对齐多模态模型的主要局限性。在 140 万小时的不同语音数据和广泛的语音任务上,通过语音到文本对齐、文本到语音对齐、语音到语音对齐和双工交互对齐等多个阶段来训练 MinMo。 经过多阶段训练后,MinMo 在保持文本LLMs,并且还促进了全双工对话,即用户和系统之间的同时双向通信。 此外,还提出了一种新颖而简单的语音解码器,它在语音生成方面优于以前的模型。MinMo 增强的指令跟踪功能支持根据用户指令控制语音生成,包括各种细微差别,包括情绪、方言和语速,并模仿特定声音。对于 MinMo,语音到文本的延迟约为 100 毫秒,理论上全双工延迟约为 600 毫秒,实际约为 800 毫秒。

  • 最先进的 (SOTA) 性能:MinMo 在语音对话、多语言语音识别、多语言语音翻译、情感识别、说话人分析和音频事件分析等基准测试中实现了当前的 SOTA 性能,同时还确保了文本大型模型的功能基本保持不变。
  • 指令控制音频生成:MinMo 支持端到端语音交互,按照用户指示控制生成音频的情感、方言和说话风格,以及模仿特定的语音音调,生成效率超过 90%。
  • 低延迟双工语音交互:MinMo 支持全双工语音交互,可实现流畅的多轮对话并防止背景噪音打断。语音到文本的延迟约为 100 毫秒,全双工延迟理论上约为 600 毫秒,实际约为 800 毫秒。
基准测试的性能概览:演示了 MinMo 在自动语音识别 (ASR)、语音到文本翻译 (S2TT)、口语问答 (SQA) 方面的功能,包括语音到文本 (S2T) 和语音到语音 (S2S)、语音分类 (VSC)、语音情感识别 (SER)、语言识别 (LID)、年龄识别和性别检测。MinMo 在所有这些任务上都超越了以前的 SOTA 模型

Introduction  

无缝语音交互表示用户与系统进行实时、自然、相关且类似人类的语音对话。促进无缝语音交互带来了巨大的挑战:

  • 系统需要准确、全面地理解音频,包括理解内容以及语音中的副语言线索(例如,情感、韵律)以及音频事件;
  • 系统应产生自然且富有表现力的语音响应;
  • 系统应向用户提供相关且合理的响应,作为智能聊天机器人;
  • 系统应支持全双工对话(同时双向通信),即系统在说话时倾听,用户在系统说话时可以自由打断,然后系统要么继续语音,要么停止响应,倾听用户,并提供对新用户查询的响应。

近年来,无缝语音交互系统获得了巨大的发展势头,尤其是随着多模态大型语言模型的进步,例如 GPT-4o和 Moshi。这些系统不仅可以产生自然而富有表现力的语音,还可以理解文字以外的线索,包括情感语气和音频事件。当前用于语音交互的多模态语言模型可分为两大类。

第一类包括原生多模态模型,例如 Moshi  和 GLM-4-Voice 。这些模型通常使用仅解码器的 Transformer 作为主干,在单个框架内同时对语音和文本模态的理解和生成进行建模;它们通常需要使用语音和文本数据进行预训练。这些模型存在两个主要限制。首先,在语音离散化之后,语音标记序列的长度通常是文本长度的两倍以上(例如,在 Moshi 中每秒 12.5 个标记)。随着模型大小的增长,序列长度的这种差异会带来挑战,例如 175B GPT-3 。其次,与文本相比,语音数据的稀缺性导致语音-文本训练数据高度不平衡,进而导致灾难性的遗忘。

第二类包括对齐的多模态模型,集成语音功能,同时旨在保持现有预训练文本LLM。这导致中间输出仍然包含文本,如 Llama-Omni  和 Freeze-Omni  等模型所示。然而,这些基于对齐的模型通常是在有限的语音数据(LLaMA-Omni 为 200K 样本,Freeze-Omni 为 120K 小时)上进行训练的,这导致了关于大型语音数据集对模型功能的影响以及原始文本LLM可能受到影响的问题。此外,尚未对这些模型进行广泛的语音任务调查,例如语音翻译、情感识别、说话人分析、语言识别和音频事件检测。此外,这些模型缺乏对丰富而细致的说话风格的指令遵循能力的系统评估,也缺乏对全双工对话能力的开发和评估,以实现无缝语音交互。

在这项工作中,我们引入了一种新的多模态大型语言模型 MinMo,以解决现有对齐多模态模型的这些局限性。MinMo 经过超过 140 万小时的语音数据训练,包括各种任务,例如语音转文本、文本转语音和语音转语音。这种广泛的培训使 MinMo 能够在各种基准上实现最先进的 (SOTA) 性能。还采用了一些方法,有效缓解了在大规模数据集训练后,模型对原始文本语言模型(text-LLM)聊天能力的灾难性遗忘问题,同时增强了语音理解和生成能力。

还提出了一种新颖的语音解码器,在保持结构简单的同时,兼具了优异的语音生成性能。LLaMA-Omni 使用一种非自回归(NAR)流式 Transformer,它将语言模型(LLM)的输出隐藏状态作为输入,并通过连接时序分类(CTC)预测响应的离散语音标记序列。然而,这种方法的性能相比自回归语音解码器较差。Freeze-Omni 使用了三种语音解码器,包括 NAR 前缀语音解码器、NAR 语音解码器和 AR(自回归)语音解码器,使模型结构更为复杂。有别于这两种策略,我们为 MinMo 设计了一种自回归(AR)流式 Transformer,其通过固定比例将 LLM 的输出隐藏状态与语音标记混合处理。

开发了一种机制,可以有效地促进与 MinMo 的全双工相互作用。具体来说,我们实现了一个全双工预测模块,该模块利用文本LLM 的语义理解功能来决定是继续系统响应,还是让步、倾听和响应新的用户查询。对于 MinMo,语音到文本的延迟约为 100 毫秒;全双工延迟理论上约为 600 毫秒,实际时约为 800 毫秒。

Related Work

Multimodal Spoken Dialogue Models:

多种语音基础模型已被开发用于通用音频理解,但尚未系统地探索其在语音交互方面的应用。例如,Qwen2-Audio 将 Whisper 语音编码器与预训练文本语言模型(LLM)集成,并通过多任务预训练和基于指令的监督微调使 LLM 具备语音理解能力。SALMONN 是另一种用于通用音频理解的语音-文本 LLM,通过 Q-Former 将单独的语音和音频编码器与预训练文本 LLM 集成,并采用 LoRA 进行模态对齐。

由于本研究旨在开发一种端到端多模态模型以实现无缝语音交互,我们将重点比较 MinMo 与用于语音交互的语音-文本模型(或称多模态语音对话模型)。同时或受到 GPT-4o 的启发,多模态语音对话模型的开发正如火如荼地进行,以实现与用户实时语音对话的能力。(Ji et al., 2024a) 对近期的语音对话模型进行了深入综述。一些研究支持传统的回合制语音聊天(即半双工通信),但无法处理全双工语音交互(即同时双向通信)。这些模型包括协作系统和端到端框架。

PSLM (Mitsui et al., 2024) 是一种协作系统,因为它依赖 ASR 处理音频输入,这会丢弃副语言信息并导致错误传播。PSLM 同时生成语音和文本标记,从而减少语音生成延迟;然而,其响应质量较低。与 PSLM 等协作系统不同,端到端框架直接接受音频输入并生成音频输出。

Llama-Omni (Fang et al., 2024) 和 Mini-Omni (Xie & Wu, 2024) 是两个近期的端到端框架,但尚未针对全双工通信进行训练。Llama-Omni 集成了 Whisper 语音编码器、语音适配器、流式语音解码器和声码器,并以预训练文本 LLM 作为基础。其语音解码器以非自回归(NAR)方式生成与生成文本前缀对应的离散单元。该模型采用两阶段训练策略:第一阶段冻结语音编码器,仅对语音适配器和 LLM 进行自回归训练;第二阶段冻结语音编码器、语音适配器和 LLM,仅使用 CTC 损失对语音解码器进行训练。Llama-Omni 被评估在语音转文本指令执行和语音转语音指令执行任务上的表现。

Mini-Omni 同样采用 Whisper 编码器,并通过适配器进行最小化训练,以保留 LLM 的能力。该模型通过模态对齐、适配器训练和多模态微调三个阶段进行训练。Mini-Omni 同时生成文本和音频标记,并填充 N 个标记以确保首先生成对应的文本标记,从而指导音频标记的生成。

MinMo 支持全双工语音对话。现有的全双工语音聊天系统同样可以分为协作系统和端到端模型两类。在协作系统中,VITA (Fu et al., 2024) 同时运行两个模型,即生成模型和监控模型,以支持全双工通信。当生成模型正在生成系统响应时,监控模型会监控环境,并在检测到有效的用户打断后结合上下文对新的用户查询提供响应,同时生成模型暂停并切换到监控角色。值得注意的是,VITA 仍然依赖外部的 TTS 模块生成语音输出。

另一种协作系统 (Wang et al., 2024a) 则通过 LLM 接入 ASR 模块和流式 TTS 模块运行。该系统不需要模态对齐,而是对预训练的文本 LLM 进行监督微调,采用以下范式:在每个时间步,LLM 要么处理一个输入标记,要么生成一个文本标记,要么输出一个特殊的控制标记,用于在 SPEAK 和 LISTEN 状态之间进行状态转换。所有这些任务都被定义为基于单一序列化流对话的下一个标记预测。全双工对话学习基于由 GPT-4 合成的数据进行,这些数据生成了包含不同类型用户打断的对话。值得注意的是,由于其级联架构,该系统面临高达 680ms 的高延迟问题。

在端到端全双工模型中,早期的 dGSLM (Nguyen et al., 2022) 提出了一个双塔架构,用于联合处理用户语音和系统语音的音频标记流。然而,该方法存在以下几个缺点:它依赖于仅基于语音的训练,未能利用预训练文本 LLM 的能力;仅使用语义标记,未充分建模语音的声学信息;不支持在线模式。LSLM (Ma et al., 2024b) 使用仅解码器的 Transformer 生成语音标记,并通过流式 SSL 编码器处理监听标记。该模型引入了一个中断标记,用于在检测到用户尝试轮流对话时停止发言。然而,模型在生成合理响应方面表现不足。

在最近的端到端全双工模型中,Moshi、GLM-4-Voice 、SyncLM 、IntrinsicVoice 和 Omni-Flatten原生多模态模型。这些模型在单一框架中同时建模语音和文本模态的理解和生成,基于 GPT 主干,并需要使用语音和文本数据进行自监督自回归预训练。如第 1 节所述,这些原生多模态模型需要应对语音标记与文本标记之间序列长度差异显著的问题,以及语音-文本训练数据高度不平衡及其导致的灾难性遗忘。IntrinsicVoice 使用 GroupFormer 从 LLM 的隐藏状态生成 HuBERT 标记,有效地将语音序列缩短到与文本序列相当的长度。Omni-Flatten 采用分阶段的逐步后期训练策略,通过块级的单流平铺语音和文本标记来学习全双工和无文本的语音到语音交互。

与这些原生多模态模型不同,我们的 MinMo 属于对齐多模态模型类别,该类别还包括 Llama-Omni、Mini-Omni2和 Freeze-Omni 。对齐多模态模型集成了语音功能,同时旨在保留现有预训练文本 LLM 的能力。Mini-Omni2 引入了基于命令的中断机制以支持全双工对话,但仅在 ASR 任务上进行评估,并与 Whisper、VITA 和 Mini-Omni 进行比较。Freeze-Omni是一个语音到语音模型,通过冻结预训练的文本 LLM 来保留其能力。它支持流式输入语音和生成流式输出语音,使用多任务训练,并通过块级状态预测来建模全双工语音交互。

我们的 MinMo 与这些对齐多模态模型在以下方面不同:我们探索了在更大规模的语音数据集(1.4 百万小时多样化语音数据,相较于 LLaMA-Omni 的 20 万样本和 Freeze-Omni 的 12 万小时)以及更广泛的语音任务上训练 MinMo。MinMo 还通过新颖的语音解码器、增强的指令跟随能力,以及对全双工语音对话能力的系统性训练和评估,与现有的对齐多模态模型形成了差异化。

文本风格 – 可控语音合成

多模态语音对话模型相比于基于文本的对话模型的显著特征在于其能够理解和生成超越文本内容的声学信息。语音模态不仅包含内容,还包括情感、方言和语速等声学信息。一个智能的多模态语音对话模型应该能够全面理解输入语音中的声学信息(例如情感),并且能够生成带有指定情感、方言、语速的响应,甚至模仿特定的声音,从而在沟通中实现更深层次的理解和响应。

协作系统如 ParalinGPT 、E-Chat 和 Spoken-LLM 通过整合副语言特征增强对情感等声学信息的理解。这些系统可以与可控风格的文本转语音(TTS)系统结合,以生成带有特定情感、语速和音量的响应。在文本风格可控 TTS 方面取得了显著进展,例如 TextrolSpeech 、PromptTTS 、PromptTTS2、InstructTTS 和 ControlSpeech 。

与这些协作系统相比,Moshi 使用一个具有单一演员声音和超过 70 种说话风格录音的 TTS 引擎,生成训练数据以支持在端到端模型中理解和生成声学信息。GLM-4-Voice利用高质量的多轮语音对话数据,这些数据针对特定语音风格需求(如语速、情感或方言)进行定制,从而支持风格可控的语音对话。然而,据我们所知,尚无研究表明对齐的多模态模型能够支持风格可控的语音生成。

与之前的研究声称对齐的多模态模型(如 Llama-Omni 和 Freeze-Omni)仅允许语言模型控制语音的内容而非风格和韵律相反,我们在本研究中提出了一种新型的流式语音解码器,用于对齐多模态模型 MinMo,并发现该解码器不仅增强了指令跟随能力,还使 MinMo 能够生成符合用户指定情感、方言、语速以及模仿特定声音的语音。

MinMo

模型架构

语音编码器采用预训练的 SenseVoice-large 编码模块,提供了强大的语音理解能力,支持多语言语音识别、情感识别和音频事件检测。输入投影器由一个随机初始化的两层 Transformer 和一个 CNN 层组成,用于维度对齐和降采样。我们选用预训练的 Qwen2.5-7B-instruct 模型作为预训练的文本 LLM,因为其在各种基准测试中的表现卓越(Team, 2024)。此外,我们利用 CosyVoice 2的流式音频生成机制,该机制具备低延迟并提供具有竞争力的语音合成性能。

对于每批接收的五个文本标记,我们将这些标记及其对应的最终隐藏层向量同时传递到输出投影器和语音标记语言模型(Voice Token LM)。输出投影器是一个单层线性模块,随机初始化用于维度对齐。语音标记语言模型(Voice Token LM)采用预训练的 CosyVoice 2 LM 模块,并以自回归方式生成十五个语音标记,确保高效且无缝的音频合成。这些语音标记由 Token2wav 合成器模块实时处理,生成最终的音频输出。

Token2wav 合成器包括一个预训练的流匹配模型,该模型将标记转换为梅尔频谱图,以及一个预训练的声码器,将梅尔频谱图转换为波形,两者均来源于 CosyVoice 2。MinMo 通过额外的隐藏嵌入实现端到端的完整训练,这些嵌入有助于根据用户指令控制语音风格,例如情感、方言和语速等。语音生成的详细信息详见 3.2 节。

全双工预测器模块由一个单层 Transformer 和一个线性 softmax 输出层组成,两者均为随机初始化。该模块实时预测是否响应用户指令或暂时停止当前系统播报,以处理来自用户的进一步音频输入。一旦全双工预测器决定系统响应是合适的,MinMo 会生成文本输出并同步以逐标记方式生成音频标记。

Streaming Voice Decoder

语音解码器包括三个组件:输出投影器、语音标记语言模型(Voice Token LM)和流式标记到波形(Token2wav)合成器。

输出投影器对齐 LLM 的维度与语音解码器的维度。LLM 的隐藏状态包含丰富的上下文信息,但语义上可能不够明确;而采样得到的文本标记更加精确,与生成的文本一致。同时,当前用户输入的隐藏状态包含显式的指令信息。在每轮对话中,用户输入的嵌入与 LLM 最后一层输出的隐藏状态将沿特征维度拼接,形成查询嵌入。查询嵌入与五个采样文本标记的嵌入,以及 LLM 最后一层输出的隐藏状态,将沿序列维度拼接并输入到投影器中。投影器的输出被称为语义向量,这些向量代表了丰富且准确的语义信息。

在输出投影器之后,使用语音标记语言模型(Voice Token LM)以自回归方式生成语音标记。该语言模型在交替的文本和语音标记序列上运行。具体而言,我们以 5:15 的固定比例混合语义向量和语音标记,即每五个语义向量后跟随十五个语音标记。

在训练过程中,采用教师强制策略,并引入一个特殊标记,用于指示下一个语义向量应被连接。当 LLM 的文本响应完成且语义向量耗尽时,我们插入一个“语音轮次”(turn of speech)标记,提示语音标记语言模型接下来的标记应完全为语音标记。当生成“语音结束”(end of speech)标记时,语音合成过程结束。

为了从语音令牌重建波形,我们使用现成的流式 token2wav 合成器。token2wav 合成器包含一个数据块感知流匹配模型和一个 mel 到 wave 声码器,能够以 15 个令牌为块合成波形。

语音解码器的理论延迟可以按以下方式计算:

其中 dl⁢l⁢m 表示 LLM 生成一个文本令牌的计算时间, dl⁢m 表示 LM 生成一个语音令牌的时间, ds⁢y⁢n 表示 token2wav 合成器生成每个语音令牌对应的波形的时间。

Tasks and Training Data

MinMo 的训练任务包括四类,包括 Speech-to-TextText-to-SpeechSpeech-to-Speech 和 Speech-to-ControlToken 任务。表 2 列出了每个类别中的具体任务及其相应的数据量表。

Speech-to-Text 任务。此类别包含大约 120 万小时的语音-文本配对数据,包括自动语音识别 (ASR)、语音到文本翻译 (S2TT)、语言识别 (LID)、上下文偏差语音识别、语音情感识别 (SER)、音频事件检测 (AED)、说话人分析、口语平滑等任务。这些任务的训练数据以 ChatML 格式组织,如以下示例所示:

Text-to-Speech tasks。该类别的数据主要由基础语音合成数据组成,与训练 CosyVoice 2 的数据相同。它包括 170000 小时的文本语音配对数据,并支持四种语言:中文、英文、韩文和日文。此外,还有大约 1000 小时的音频生成数据由指令控制。这些说明扩展为包括由 Qwen-Max 生成的自然语言描述,利用人类标记的属性,例如情感、语速、方言和角色扮演。

Speech-to-Speech 任务。 Speech-to-Speech 数据主要通过模拟获取,包括大约 10000 小时的多轮对话语音和 100 小时的风格可控多轮对话语音。模拟 Speech-to-Speech 聊天数据的方法如下:

对于主要来源于 Alpaca 和 ShareGPT3 的文本聊天数据,我们利用 CosyVoice 的零样本上下文生成方法 将用户文本转换为用户语音。我们使用来自选定说话人的 2 小时数据对 CosyVoice 的基本模型进行微调,为目标说话人创建一个语音合成模型,称为 CosyVoice-SFT。该模型合成了助手的语音(即系统语音)。使用zero-shot上下文生成进行用户语音合成的优势在于它能够确保生成的用户语音的多样性,从而增强 MinMo 的泛化性。

为了解决合成音频和真实音频之间的差异,我们从 ASR 数据中选择合适的真实语音作为用户语音查询,并使用相应的文本作为 Qwen-Max 的输入生成响应文本,然后使用 CosyVoice-SFT 模型将其合成为辅助语音。这种方法进一步增强了模型对真实用户音频输入的鲁棒性。

为了生成涵盖不同说话风格的对话语音,我们最初使用 Qwen-Max 来创建丰富的风格可控、多轮次文本对话集合。用户查询通过 Cosyvoice 的 zero-shot generation 转换为语音。随后,我们使用 Cosyvoice 2 来生成助手的富有表现力的语音。具体来说,我们将助手的响应内容和指导提示输入到 Cosyvoice 2 中,以合成特定风格的语音。此外,使用小型、多样化和初步录制的语音语料库作为提示语音,通过零镜头生成合成表达性响应语音。前一种方法增强了模拟语音的多样性,而后者更有效地构建了各种风格的表现力。

Speech-to-ControlToken 任务。Speech-to-ControlToken 数据主要由两部分组成。第一部分是从现有的真实语音交互数据中提取的,而第二部分是使用文本对话数据进行模拟的。具体来说,现有的真实语音交互数据包括 Alimeeting 、Fisher 等资源,以及我们内部的语音交互数据,总共大约 3000 小时。模拟数据主要包括开源 MOSS 数据集 和通过合成我们内部文本对话数据的口语对话,产生了大约 1000 小时的语音聊天数据。当使用这些语音交互数据构建双工训练数据时,我们应用启发式规则在样本上自动注释双工标签,如下所示:

  • 对于助手的轮流,将用户轮到的终点作为助手轮次的起点。
  • 对于用户的轮次,将助手轮次结束后的时间间隔 T 作为用户轮次的起点,其中 T∼𝒩⁢(0.6,0.42) 。
  • 对于用户的 Back-Channel,当用户(以对话中的一个说话者为用户)无法打断另一个说话者时,我们从语音交互数据中选择实例,并将其视为用户 Back-channel 的训练样本。

模型训练

MinMo 通过四个对齐阶段逐步训练:(1) 语音到文本对齐,(2) 文本到语音对齐,(3) 语音到语音对齐,以及 (4) 双工交互对齐。通过四个对齐阶段,MinMo 获得了端到端的音频理解和生成能力,同时保留了主干文本LLM,实现了低延迟并为用户提供了无缝的语音聊天体验,类似于 GPT-4o。这四个阶段详细说明如下。

语音到文本对齐Pre-align > Full-Align > SFT[LoRA]

第一阶段使用表 2 所示的语音转文本数据,将音频模态的输入潜在空间与预训练文本LLM。此阶段包括对图 3 中的输入投影和语音编码器的逐步更新,以及使用 LoRA 更新文本 LLM。考虑到语音编码器和LLM (Qwen2.5-7B) 是预先训练的,而输入投影的参数是随机初始化的,我们使用表 2 所示的语音转文本数据子集执行预对齐训练 (Pre-align),仅更新输入投影器。这个 Pre-align 阶段有效地防止了随机初始化的参数在初始训练阶段对预训练的 Voice Encoder 产生大的梯度影响。在预对齐之后,我们使用完整的语音转文本数据来训练输入投影器和语音编码器,同时保持 LLM 参数冻结,这个过程称为完全对齐。在 Full-Align 之后,使用涵盖各种任务的大约 130 万个样本进行指令微调 (SFT)。在此阶段,LLM 使用 LoRA 进行更新,从而增强模型遵循指令的能力。Full-Align 和 SFT 阶段中使用的特定数据比例如图 4 所示。Pre-Align 阶段使用大约 1/10 的 Full-Align 数据。

文本到语音对齐

第二阶段使用文本转语音数据将文本LLM音频模态的输出潜在空间对齐。此阶段首先训练 Output Projector,然后联合训练 Output Projector 和 Voice Token LM,同时保持其他 MinMo 参数冻结。除了基本的文本转语音功能外,我们还利用端到端框架使 MinMo 能够在语音交互中遵循用户指示,从而提供更富有表现力和娱乐性的音频响应。例如,用户可以通过指令控制音频输出的情感、语速、方言口音或说话人风格。构建了大约 1000 小时的 Instruct 语音合成数据,格式如表 3 所示。

语音到语音对齐。

第三阶段使用大约 10,000 小时的配对音频数据继续训练 MinMo。与文本到语音对齐阶段一致,我们继续仅更新 Output Projector 和语音令牌 LM。语音到语音对齐的训练数据不仅包括一般的语音到语音对话,还包括具有各种设置的音频生成指令,例如采用特定的方言、语速和口语对话的情感。我们发现,即使不更新 LLM,仅通过利用与小规模指令数据集对齐的嵌入(150 小时),大型模型仍然可以学习相当有效的音频生成控制能力。

双工交互对齐。

在完成前三个训练阶段后,MinMo 获得了音频理解、音频生成和半双工语音对话的能力。在此基础上,我们进一步添加了一个全双工模块,该模块经过 4000 小时的长篇人际口语对话训练。Full Duplex Predictor 模块在此阶段专门进行训练。Full Duplex Predictor 采用 全双工预测器将LLM的隐藏嵌入作为输入,用于预测模型是否需要生成响应。全双工预测器利用LLM固有的语义理解能力来判断:1)模型是否应该回应当前用户查询,2)模型是否应该停止正在进行的语音输出以聆听用户查询并提供适当的响应。来预测模型是否需要生成响应

Experiments

根据多个基准评估 MinMo:

语音识别和翻译

在普通话、英语、日语、韩语和其他六种语言的公共测试集上评估了 MinMo 的语音到文本转录功能。

对于普通话 (ZH)、日语 (JA)、韩语 (KO) 和粤语 (YUE),我们采用字符错误率 (CER) 来评估转录性能。对于英语 (EN)、德语 (DE)、法语 (FR)、俄语 (RU)、西班牙语 (ES) 和意大利语 (IT),单词错误率 (WER) 用作评估指标。

与 Whisper Large v3 和 Qwen2-Audio相比,MinMo 在各种语言的大多数测试集上都实现了卓越的 ASR 性能

在提示符显示或不带有 LID【语言ID】 信息的 Common Voice 上进行测试时,Whisper Large v3 和 Qwen2-Audio 的平均错误率存在显著差距,这表明这两个模型强烈依赖 LID 信息。相比之下,无论是否存在语言识别,MinMo 都表现出稳健且一致的 ASR 性能。

多语言语音翻译

在 Fleurs  和 CoVoST2  测试集上评估了语音到文本的翻译能力。

与其他端到端基线相比,MinMo 在中 ↔ 英和日 ↔ 英翻译上实现了 SOTA 性能,在其他语言对上实现了顶级性能。我们将这种强劲的性能归功于广泛的语音翻译训练数据(表 2 中 451K 小时的 S2TT 训练数据)和强大的音频编码器。 值得注意的是,尽管我们只用 CoVoST2 集(不包括 Fleurs 集)来增强我们的训练数据,但我们的模型在两个测试集上保持一致的性能,表明具有高鲁棒性。

Language Identification  语言识别

使用 Fleurs 数据集,该数据集涵盖 102 种语言。MinMo 实现了 85.3% 的语言识别准确率,优于表 7 所示的所有先前模型。具体来说,零样本 Whisper-V3 经常将粤语错误地归类为中文,而 MinMo 可以准确地识别粤语。

上下文偏见语音识别

上下文偏见,或称为热词定制,允许用户根据特定的上下文或热词获得定制化的ASR(自动语音识别)结果。MinMo通过集成用于上下文偏见的高级提示增强了ASR功能。我们为对齐和SFT阶段准备了相应的训练数据,通过将热词组织到语音处理指令之前的提示中,从而实现有效的定制。评估包括热词偏见测试和一般偏见测试,如表8所示。

热词偏见测试涉及使用SeACo-Paraformer(Shi et al., 2024)提供的三个数据集,这些数据集包含用于偏见评估的热词。一般偏见测试则使用包含较少热词的数据集,用于评估对无关热词的抗干扰能力。

MinMo 在 ASR 准确性(带和不带热词)以及硬质热词的召回率方面优于竞争性基线 SeACo-Paraformer

Speech Analysis and Understanding

Speech Emotion Recognition

使用来自 EmoBox 的七个广泛使用的情绪识别数据集评估了 MinMo 的语音情感识别 (SER) 能力,包括 CREMA-D (Cao et al., 2014)、MELD (Poria et al., 2019)、IEMOCAP (Busso et al., 2008)、MSP-Podcast (Martinez-Lucas et al., 2020)、CASIA (Zhang & Jia, 2008)、MER2023 (Lian et al., 2023) 和 ESD (周 et al., 2021)。这些数据集包括中英文语言和场景,例如表演、电视剧和日常对话。我们采用未加权平均准确率 (UA)、加权平均准确率 (WA) 和宏观 F1 分数 (F1) 作为评价指标。引用了来自最近的 SER 工具包 EmoBox(马 et al., 2024a)的这些测试集的结果。我们还使用其发布的模型检查点评估了基线音频LLM 模型 SALMONN 和 Qwen-Audio。

MinMo 在该基准测试的所有任务上都优于所有基线模型,包括语言 ID、性别、年龄、情感、发声分类任务,除了在声音问题分类任务上表现优于 Qwen-Audio

Audio Event Understanding

使用 Air-Bench 基准测试,将 MinMo 的语音和音频事件理解能力与其他 Audio-LLM。结果如表 12 所示。在语音声音分类任务 (Vocal Sound) 上,MinMo 超越了所有基线模型。然而,我们发现,在更复杂的声音问答任务中,MinMo 的表现比 Qwen-Audio 差,尽管性能仍然优于其他模型。这可以归因于两个因素:首先,在语音编码器和训练范式中,MinMo 主要是为语音交互而设计的,因此一些声音问题可能会超出其范围;其次,在评估过程中,MinMo 会预测音频中发生的情况,而不是严格选择 Air-Bench 提供的选项,因此 MinMo 生成的一些正确或相似的正确响应与后处理脚本的错误选择保持一致。

Speaker Analysis  

说话人分析涉及几项对于理解音频数据并与之交互至关重要的任务,包括性别检测、年龄估计、说话人计数、说话人识别、多说话人识别和目标说话人识别。结果表明,MinMo 在性别检测和年龄估计任务上优于所有基线模型

Speech-to-Text Enhancement

Spoken Language Smoothing【口语平滑】

口语语言平滑任务以口语ASR(自动语音识别)转录文本为输入,输出正式风格的书面文本。表14展示了口语语言平滑的一些示例。为该任务,我们通过扩展为ASR转录文本的口语到书面转换而创建的SWAB数据集(Liu et al., 2025),构建了一个多领域数据集用于训练和评估。SWAB数据集源自中文和英文的会议、播客及讲座。

在为原始视频和音频生成ASR转录文本后,大约十位注释人员根据ASR转录文本创建正式风格的书面文本,同时保留原始内容。SWAB的训练集包含20,000段文本,测试集包括100段中文和英文的随机抽样段落。我们对SWAB测试集进行全面微调,并将MinMo与基于Qwen2.5-7B的模型进行比较,结果如表15所示。

在客观指标评估中,我们使用BLEU(Papineni et al., 2002)、ROUGE(Lin, 2004)和BLEURT(Sellam et al., 2020),以人工目标为参考。然而,我们注意到口语语言平滑任务具有显著的主观性和多样性,因此基于词汇匹配的客观指标可能无法充分反映模型性能。因此,我们采用人工和LLM注释来提供信实性(S-Faithful,即对原始内容的信实性)和正式性(S-Formal)的排名评估。自动化LLM评分的提示见附录A.1。

表15显示,我们的模型与Qwen2.5-7B的性能相当,表明MinMo在平滑口语语言方面具有较好的能力。

标点插入和反向文本规范化

对于标点插入 (PUNC) 和反向文本归一化 (ITN) 任务,我们使用来自 Fleurs 数据集的中文和英文数据。我们将 MinMo 与 SenseVoice-L 和 whisper-large-v3 进行比较,如表 16 所示。鉴于标点符号插入和 ITN 任务的主观性,我们使用 GPT-4 Turbo 对三个结果进行排序进行评估。附录 A.2 中提供了自动评分的任务提示。第一名获得 3 分,第二名获得 2 分,第三名获得 1 分。最终分数是所有分数的平均值。在准备测试数据时,我们使用随机选项洗牌和多轮评分,以减少使用 ChatGPT 进行评估时的不确定性。最终结果表明,MinMo 在标点插入和 ITN 的主观评价中表现更好

Voice Generation

文本到语音(TTS)

为了评估我们语音解码器的合成准确性,我们将最新的SEED测试集(Anastassiou et al., 2024)转换为ChatLM格式。在该格式中,文本以用户内容的形式呈现,并以“Copy:”命令为前缀,LLM预计会复制该文本。测试集包括2,020个中文案例和1,088个英文案例。对于中文案例,我们使用了Paraformer-zh模型(Gao et al., 2022),而英文案例则使用了Whisper-large V3(Radford et al., 2023)。鉴于LLM存在的指令跟随问题,我们在推理过程中应用了教师强制方案,以最小化输入和输出文本之间的差异。语音解码器的内容一致性通过中文的CER(字符错误率)和英文的WER(词错误率)进行评估。

我们的发现表明,即使在应用了教师强制方案的情况下,只有大约20%的测试案例的输入和输出文本与LLM完全一致。由于不一致的输入和输出可能导致语音解码器的隐藏状态混乱,因此仅包括输入和输出文本一致的测试案例来计算错误率。结果如表17所示。我们观察到,与TTS基线模型CosyVoice 2.0-SFT(Du et al., 2024b)相比,MinMo在中文测试集上表现出稍微较低的内容一致性和语音质量。在英文测试集上,MinMo在内容一致性上表现相似,但NMOS(语音质量评分)稍低。这个下降可以归因于微调的说话人不同的声学特性,这影响了识别模型和NMOS评分器。然而,这种下降不会显著影响人类的理解。因此,主观评估可能更适合语音到语音的语音聊天模型,我们将在未来的工作中进一步探讨这一点。

指令跟随语音生成

为了评估指令跟随语音生成的性能,我们开发了一个包含30个会话和122个回合的多轮中文语音到语音测试集,涉及12种指令控制类型。这些控制类型包括情感(高兴、悲伤、惊讶、愤怒、恐惧)、方言(粤语、四川话)、语速(快、慢)、角色扮演(机器人、佩佩)以及默认风格。

为了评估指令跟随语音生成的准确性,听众根据指令类型对生成的音频进行分类。如表18所示,MinMo在指令控制准确性方面优于基线模型GLM-4-Voice,特别是在方言和角色扮演方面。

Voice Chat

为了将基础模型的对话功能转移到语音模态,我们为语音转文本 (speech2text) 和语音转语音 (speech2speech) 场景构建了多轮次对话数据。speech2text 数据主要分为两部分。首先,它源自开源多轮次纯文本数据,我们使用零样本文本转语音 (TTS) 技术合成用户轮次。其次,我们使用真实的自动语音识别 (ASR) 训练数据作为聊天查询,从大型模型获取文本响应,从而为 speech2text 生成交互式训练数据。

MinMo 模型在语音转语音 (S2S) 模式下与现有基线相比具有显著优势,实现了新的最先进的 (SOTA) 结果。在语音转文本 (S2T) 模式下,它还在 Llama Question 和 Web Question 数据集上实现了 SOTA 性能。但是,MinMo 的测试结果仍然表明,与 S2T 模式相比,S2S 模式的性能明显下降。我们将其归因于这样一个事实,即测试集中的许多答案都具有丰富的文本结构和专业词汇,这对模型的文本转语音 (TTS) 功能提出了更高的要求。此外,在 S2S 评估中用于获取答案文本换语音的自动语音识别 (ASR) 模型也会在一定程度上影响 S2S 指标。

从表 20 中可以观察到,通过将额外的 speech2text 任务数据整合到 MinMo 训练中,我们能够有效地保持基础模型的对话能力。与 ASR 与纯文本基本模型相结合的性能相比,MinMo 的对话能力在很大程度上保持一致。但是,MinMo 的响应分数略低于 Ground Truth 响应的质量。我们认为这种差异可以归因于两个主要原因。首先,多个语音任务的集成和在基础模型上实现 LoRA 训练,在一定程度上削弱了原始大型语言模型 (LLM。该表显示,与 ChitChat 测试集相比,MinMo 在 Alpaca 测试集上表现出更大的性能变化。其次,MinMo 的音频理解能力还有进一步改进的空间,并且仍有可能降低 ASR 任务中的字符错误率 (CER)。

Full Duplex Spoken Dialogue

为了评估 MinMo 在全双工语音交互中的能力,我们构建了三个测试集:中文 Alimeeting 数据集、英文 Fisher 数据集和一个模拟测试集,旨在更接近真实的人机对话场景。我们从两个角度评估 MinMo 的全双工能力:预测性能和预测效率。关于预测性能,评估分为三个任务:辅助轮流、用户轮流和用户反向引导。对于轮流任务,我们采用正 F1 分数作为分析指标,并引入了偏移距离 ( K ) 来更好地分析模型的性能。对于用户反向通道任务,我们利用准确性来评估 MinMo 识别反向通道话语的能力。

MinMo 模型在人机对话数据集上表现出值得称道的结果,无论是用户轮流还是助手轮流。当 K=10 时,预测性能接近 99%。在实际人机对话的测试集中,与人机对话测试集相比,MinMo 模型在辅助轮流上的表现表现出一定程度的下降。我们认为这主要是由于真实人类对话中背景噪音、语速、停顿和其他因素的高度可变性,这可能导致模型在助理轮流任务中出现一定程度的误判。但是,对于人与人对话中的用户轮流预测,MinMo 模型仍然保持了高水平的灵敏度和预测性能,确保在用户说话时系统及时停止说话,从而避免与用户重叠语音。这种敏感性和对用户语音的尊重也解释了为什么 MinMo 模型对用户反向通道评论保持 70%-80% 的预测准确性,如表所示。这与 user turn-taking model 的调优是一致的,表明两者之间存在一定的权衡。

为了对 MinMo 双工模式进行效率分析,我们还分别对人机对话和人机对话测试集进行了测试。如表 23 所示,MinMo 在用户轮流中的平均响应延迟为 250ms。在人机测试集中观察到最快的响应速度,为 88.8 毫秒,而最具挑战性的 Alimeeting 测试集显示延迟为 448.8 毫秒。在辅助轮流方面,MinMo 的平均响应延迟在 660ms 左右,与用户轮流预测所需的响应时间相比,这要长。我们将其归因于以下事实:用户轮流涉及用户语音的开始部分,而助手轮流涉及用户轮到即将结束的部分。因此,助理轮流的上下文语义信息更加全面,从而缩短了决策所需的时间滞后。

Full Duplex System Latency

MinMo 的双工交互由四个模块组成:全双工预测器,负责双工控制,语音转文本模块(语音编码器+输入投影仪+LLM)、文本转语音标记模块(输出投影仪+语音标记 LM)和 Token2Wav 模块。表 21 显示了每个模块的延迟。以 Assistant Turn-taking 为例,当用户的实际语音结束时,双工模型通常需要 250 ms 的延迟进行评估。在 Speech-to-Text 过程中,对前 5 个文本标记的预测大约需要 150 毫秒。预测最初的 15 个语音令牌大约需要 70 毫秒,从语音令牌转换到第一个音频数据包需要额外的 130 毫秒。因此,在开发基于 MinMo 的全双工语音对话系统时,助理轮流的标准体验延迟约为 250 + 150 + 70 + 130 = 600 毫秒。上述数值估计值是在使用 L20 GPU 和 BF16 模型格式进行测试期间得出的。

总结/局限性

MinMo 在超过 140 万小时语音的广泛数据集上进行了训练,展示了各种基准(包括口语对话、多语言语音识别和情感识别)的最新性能。通过利用多阶段对齐策略,MinMo 巧妙地平衡了音频理解和生成,同时最大限度地减少了基于文本的 LLMs。一项关键创新是 MinMo 用于流式端到端音频生成的新颖对齐方法。通过利用文本模型的隐藏层表示,MinMo 的语音解码器实现了结构简单性和有竞争力的性能以及低延迟。这种方法显著增强了模型的指令遵循能力,能够生成细致入微的语音,准确反映用户指定的情感、方言和说话风格。此外,MinMo 支持全双工交互,以大约 600 毫秒的延迟提供无缝的对话体验。总之,MinMo 代表了语音交互系统领域的重大进步。它不仅解决了序列长度差异和数据不平衡的固有挑战,还为自然和富有表现力的语音交互设定了新标准,为多模态语言模型的未来发展铺平了道路。

MinMo 有一些需要解决的局限性。首先,MinMo 基于预训练的文本大模型,使用对齐方式集成音频理解和音频生成能力;文本大型模型仅参与 LoRA 更新,其遵循各种指令(例如语言和任务遵循)的能力需要改进。需要进一步探索以确定使用更多高质量的文本数据对文本大型模型进行更全面的更新是否可以增强其指令跟踪能力。其次,MinMo 的端到端音频生成存在一些长尾发音错误问题。这个问题部分是由于保留了 LLM,部分原因是端到端建模输出文本中的一些特殊符号无法有效地转换为语音。可以探索数据扩展来解决这些长尾问题。此外,由 MinMo 中的指令控制的音频生成的整体效率需要提高。这部分是由于当前指令数据的整体大小较小,并且仅使用隐藏嵌入进行端到端对齐的限制,这限制了历史信息的传输。最后,虽然 MinMo 实现了基于语义的双工模块,但它仍然需要单独的 AEC 【AEC模块用于消除语音通信中由于麦克风拾取扬声器输出信号而产生的回声。】和 VAD 模块。将来,将探索完全端到端的双工模型。

SLAM-Omni: 单阶段训练下支持可控音色的语音对话模型

近几个月来,许多端到端的语音对话系统涌现,旨在解决级联系统中交互延迟过高以及基于文本交互下副语言信息丢失的问题。然而,目前大多数语音对话模型依赖于大量的语音对话数据以及高昂的训练代价,且存在响应音色单一的弊端。

近日,上海交通大学计算机系X-LANCE实验室联合微软亚洲研究院推出了面向低资源场景下支持可控音色的语音对话模型——SLAM-Omni。该模型只需要在4张GPU上单阶段训练15小时,即可获得远超此前同等规模模型的对话能力,并且具有优越的语音质量以及生成语音-文本一致性。在更大规模数据集上的实验表明SLAM-Omni在中文对话以及多轮对话上都有不俗的表现。

语音对话系统建模

本文首先探索了主流的语音对话系统建模方案,现有端到端系统主要通过将文本作为中间输出或隐藏状态来利用预训练的大语言模型(LLM)。这些方法可以分为文本驱动建模和音频-文本联合建模两类。

文本驱动建模保留了LLM原始架构,将文本隐状态传递给语音解码器生成音频,能够有效保留LLM的知识,使用其隐藏状态作为语音解码器的输入用于音频生成,但由于只使用文本tokens进行自回归建模,难以捕捉音频的情感和语调等副语言特征。音频-文本联合建模分为交替和并行两种范式,均将音频 tokens加入自回归建模,理论上提升对非语言信息的建模能力。交替范式通过交替使用文本和音频tokens进行生成,需要大量的语音-文本交替数据并重新训练LLM。而并行范式则并行地对文本和音频tokens自回归生成。SLAM-Omni在此基础上,通过预测单层语义tokens并结合语义分组建模的方式来加速音频生成,显著降低了训练成本。

主要贡献:

  • 提出了第一个针对具有说话者解耦语义token的语音交互系统的零样本音色控制解决方案。
  • 提出语义组建模方法来加速单层语义语音标记生成和模型训练。
  • 历史文本提示是为了在SDM【Existing spoken dialogue models】 中进行高效的多轮历史建模而提出的。
  • SLAM-Omni 是第一个实现单阶段训练的语音助手,需要最少的数据和计算资源
  • 实验表明,SLAM-Omni 在文本相关任务上优于类似规模的先前模型,并且在所有现有 SDM 中在声学质量和语音文本对齐方面表现出卓越的性能。更大数据集上的结果证明了其多语言和多轮对话能力。

方法

模型概述

SLAM-Omni通过将系统提示、历史文本提示和用户语音嵌入拼接作为输入,并在Vocoder中通过语者提示来控制音色【 借鉴TTS模型 cosyvoice: 条件流匹配模型 +HifiGAN】;同时,采用语义分组建模加速自回归过程中的语音token生成。

输入语音建模

SLAM-Omni使用Whisper编码器从用户语音指令中提取音频特征(50 Hz)。Whisper作为在大规模跨语言语音数据上训练的语音识别模型,提供了精准的转录和强大的多语言支持,是SLAM-Omni实现多轮多语言对话能力的基础。我们通过降采样处理音频特征,将多个连续帧合并,并通过线性投影将其转换为与LLM嵌入维度对齐的形式。这些处理后的音频特征与文本提示嵌入一起,作为输入传递给LLM。

输出语音建模

在语音输出方面,SLAM-Omni采用并行的音频-文本联合建模,并行地自回归预测文本和音频的语义tokens。为此,我们扩展了LLM的词表,新增了音频tokens的码本,并将原始的词嵌入矩阵与新嵌入合并。在每个生成步骤中,LLM输出的logits包含了文本和音频tokens的预测分布。然而,由于文本tokens(约为3Hz)和音频语义tokens(50Hz)的频率差异,直接以相同速率生成这两种tokens会导致语音对话模型的训练和推理成本大幅增加,同时增加了实时语音生成的延迟。

为了解决这一问题,本文提出了“语义分组建模”方法每步生成多个音频tokens,从而缓解频率不匹配带来的挑战。该方法通过线性层将音频logits投影到分组logits中,并对应的在训练过程中将原语义token序列按组进行划分。通过这种方式,模型能够在自回归过程的每步中同时处理多个音频tokens,从而加速语音生成并极大地降低训练和推理的成本。模型的训练目标可以表示为文本层和音频层交叉熵损失的加权和。

可控音色建模

SLAM-Omni通过将语音内容建模为语义tokens,天然地实现了音色与语言信息的解耦,将zero-shot音色控制从TTS扩展到了语音对话系统上。借鉴TTS模型(Cosyvoice)中的技术,SLAM-Omni使用条件流匹配模型将语义tokens和语者提示信息转换为mel频谱图,并通过HiFi-GAN合成波形。此外,为了支持实时语音生成,SLAM-Omni在流匹配的Transformer架构中采用了块因果注意力机制。

多轮对话

过去的语音对话模型在多轮对话建模上通常将文本和音频tokens交替作为历史,但较长的音频token序列显著提高了训练成本,限制了对话轮次。此外,较长的历史也会影响模型的上下文学习能力,并可能导致早期对话内容的遗忘。为了解决这些问题,SLAM-Omni引入“历史文本提示”(Historical Text Prompting)的方案,仅使用文本模态来表示对话历史。在多轮对话交互中,SLAM-Omni采用模板:<系统提示> <历史文本> <输入> <响应>。其中,系统提示指定模型角色和任务,历史提示则以文本形式存储过去的对话内容。这种方式与LLM的训练模式高度契合,同时避免了长音频序列建模的负担,使得模型能够在受限的上下文窗口内处理更多的对话轮次。在推理过程中,通过Whisper提取的语音特征可以解码成输入语音的转录文本,模型输出的文本tokens则通过分词器转换为文本。每轮对话中,由此得到的问题-响应文本对会被追加到历史对话中,以便下一轮使用。如图所示,第一轮语音对话的转录被纳入历史提示中,第二轮推理时计算得到的KV键值缓存可以在第三轮及以后的对话中复用,从而提高多轮推理的效率。

单阶段训练

此前的端到端语音对话模型通常需要进行模态适配、模态对齐和有监督微调等多阶段训练,这不仅需要精细的训练策略,还涉及多个超参数的调整,带来了显著的时间和计算成本。而SLAM-Omni通过简化为单阶段微调训练,能够在较小的数据集上快速收敛,展现了高效的训练效果。在我们的实验探索中,TTS和ASR的预训练都展示了快速的损失收敛,表明我们的方法无需大规模的模态对齐预训练。同时,进一步的实验还揭示,预训练实际上可能对模型的指令跟随能力和预训练知识保留产生负面影响。

实验设置

由于大多数开源的对话数据集仅为文本格式,我们通过zero-shot TTS系统合成语音对话语料。具体而言,我们使用CosyVoice模型生成用户输入的语音,同时随机从音色库中抽取语者提示,以控制音色。对于语音响应,我们使用CosyVoice模型生成语义tokens,它们在SLAM-Omni训练过程中作为目标音频tokens使用。我们使用的训练数据集包括VoiceAssistant-400K、英语多轮数据集UltraChat和中文对话数据集Belle_train_3.5M_CN。为了确保数据质量,我们清理了数据中的书面体(如表情符号、URL等),并限制了语音问题和响应的时长,以更好地模拟自然对话场景。在SLAM-Omni的主要实验中,仅使用VoiceAssistant-400K数据集,其他数据集则用于补充实验,评估模型在多轮和多语言对话任务中的表现。

对于用户输入,采用CosyVoice-300M模型来产生相应的语音。声音音色是通过从音色库中随机采样扬声器提示来控制的,该音色库包含来自seed-tts-eval3的1007个英语和1010个中文人类音频提示。对于助理响应,我们使用 CosyVoice-300M-SFT 的文本到令牌 LLM 来生成语义令牌这些令牌在 SLAM-Omni 训练期间用作目标音频令牌

在训练和推理过程中,为确保在低资源环境下的公平比较,我们使用Qwen2-0.5B作为LLM骨干,并选择Whisper-small作为语音编解码器。在主要实验中,SLAM-Omni采用的语义分组大小为G = 3。在单阶段训练中,SLAM-Omni进行全量微调,只有Whisper编码器保持冻结。整个训练过程大约需要在4个A100 GPU上进行15小时

为了全面评估语音对话模型的语音交互能力,本文提出了一个新的评测框架,涵盖理解、推理和口语对话三个关键环节。通过设计八个测试集,我们分别从这三方面考察模型的表现。在“理解”部分,评估模型是否能够理解并跟随用户指令;在“推理”部分,通过逻辑、数学和常识问题测试模型的推理能力;而在“口语对话”部分,我们测试模型在开放式对话场景下的交互能力。评估指标包括内容质量(通过ChatGPT评分)、语音质量(通过UTMOS评分)以及语音与文本的一致性(通过WER评分)。

实验结果

实验结果表明,SLAM-Omni在低资源场景下的表现超越了同规模的语音对话模型,在语音内容、音频质量和语音-文本一致性上显著提升,特别是在UTMOS和ASR-WER评分上表现突出,显示出其在音频建模方面的优势。在ChatGPT评测中,尽管和更大规模的模型相比仍存在差距,SLAM-Omni在理解、推理和口语对话能力上显著超越了同规模的Mini-Omni系列,表明其保留了更多的预训练LLM知识和指令跟随能力。

在音频质量和语音-文本一致性上,SLAM-Omni的表现优于所有其他语音对话模型,特别是在ASR-WER指标上,表明其语音-文本对齐更加紧密。而其他模型在生成过程中容易出现生成音频与文本不对齐的情况,尤其在长内容生成时,容易出现音频中断或长时间的静默,导致其UTMOS和ASR-WER评分较低。

消融实验表明,语义分组建模显著提高了生成语音与文本的对齐度,尤其当组大小G≥3时,ASR-WER低于5%,相比之下,没有执行分组算法的模型(G=1)的ASR-WER高达18.23%。这一差距主要来源于音频和文本token之间的频率不匹配。通过减少音频序列长度,语义分组建模有效缓解了这一问题,同时减少了训练和推理成本,并加速了音频生成,提供了更流畅的用户体验。

关于训练策略的消融实验表明,传统的多阶段训练方法虽然能略微提高模型的音频-文本对齐度,但在语音交互任务上的整体表现并未显著改善。相比之下,SLAM-Omni采用单阶段训练策略,显著提高了ChatGPT评分,并保持了相当的音频质量。通过直接在语音到语音数据上进行单阶段微调,SLAM-Omni能够更好地保留预训练LLM的知识,避免了传统预训练任务带来的知识流失问题,提高了训练效率。

附录:

Pre-training Details

对于ASR和TTS预训练,专门使用VoiceAssistant-400K数据集来确保一致性并避免引入外部数据。在 ASR 预训练期间,提供语音指令作为输入,其相应的转录文本作为目标输出。相反,对于 TTS 预训练,语音响应的转录被用作输入文本,而相应的语义token被设置为预测目标。优化和学习策略与微调期间采用的策略一致,值得注意的是,在 ASR 预训练期间仅计算文本层损失,而 TTS 预训练专门关注多层音频损失作为训练目标。

曲线显示,ASR 和 TTS 任务都实现了快速收敛,证明了模型在短时间内有效“理解”和“生成”语音的能力。这一观察表明,理解和生成任务中的模态对齐本质上是简单的,需要最少的预训练工作。此外,如表 6 所强调的,直接对语音到语音任务进行训练可以产生卓越的性能,同时减轻通常与预训练相关的知识退化。

总结 

本文提出了SLAM-Omni,一种单阶段训练下支持可控音色的端到端语音对话模型。通过语义分组建模,SLAM-Omni有效地对齐了音频和文本模态,同时加速了训练和推理过程。采用有监督的语义tokens解耦说话人信息,使得SLAM-Omni实现zero-shot音色控制。为了解决长音频历史带来的问题,我们引入了历史文本提示技术,将对话历史存储为文本,并通过键值缓存提高多轮推理效率。在少量数据训练仅仅60个GPU小时下,SLAM-Omni在文本相关能力上超越了同规模的语音对话模型,并在音质和语音-文本对齐方面表现优越。

MiniCPM-o 2.6: 端侧可用的 GPT-4o 级视觉、语音、多模态实时流式大模型

➤ 项目网站 🔗https://github.com/OpenBMB/MiniCPM-o

➤ 模型权重 🔗https://huggingface.co/openbmb/MiniCPM-o-2_6

🔗https://modelscope.cn/models/OpenBMB/MiniCPM-o-2_6

➤ Demo https://minicpm-omni-webdemo-us.modelbest.cn/

MiniCPM-o 2.6部署教程 详细的部署教程请参考文档

简介

多模态大模型的蓬勃发展始于视觉和语言,其中开源社区在图像理解能力方面实现了越来越强的性能表现。然而,我们的物理世界本质上是一个并行的连续多模态信息流,而当前大多数多模态大模型缺乏处理这样信息流的能力。最近的 GPT-4o 和 Gemini 2.0 等突破性工作迈出了朝这个目标的第一步,为领域的未来发展建立了雄心勃勃且充满希望的方向。

为了促进开源社区的探索,我们推出了 MiniCPM-o 2.6,一个从 MiniCPM-V 系列升级而来的最新性能最佳的端侧多模态大模型。该模型接受图像、视频、文本和音频输入,并以端到端方式生成高质量的文本和语音输出。虽然总参数量仅有 8B,MiniCPM-o 2.6 的视觉、语音和多模态流式能力达到了 GPT-4o-202405 级别,是开源社区中模态支持最丰富、性能最佳的模型之一。其主要特性包括:

  • 领先的视觉能力。 MiniCPM-o 2.6 在 OpenCompass 榜单上(综合 8 个主流多模态评测基准)平均得分 70.2,以 8B 量级的大小在单图理解方面超越了 GPT-4o-202405、Gemini 1.5 Pro 和 Claude 3.5 Sonnet 等主流商用闭源多模态大模型。此外,它的多图和视频理解表现也优于 GPT-4V 和 Claude 3.5 Sonnet,并展现出了优秀的上下文学习能力。
  • 出色的语音能力。 MiniCPM-o 2.6 支持可配置声音的中英双语实时语音对话。MiniCPM-o 2.6 在语音理解任务(如 ASR 和 STT 等)优于 GPT-4o-realtime,并在语音对话的语义和声学评估中展现了开源社区最佳的语音生成性能。它还支持情绪/语速/风格控制、语音克隆、角色扮演等进阶能力。
  • 强大的多模态流式交互能力。 作为一项新功能,MiniCPM-o 2.6 能够接受连续的视频和音频流,并和用户进行实时语音交互。在综合评测基准 StreamingBench 中(包含实时视频理解、全模态视音频理解、多模态上下文理解三大类评测),MiniCPM-o 2.6 取得开源社区最佳水平,并超过了 GPT-4o-202408 和 Claude 3.5 Sonnet。
  • 强大的 OCR 能力及其他功能。 MiniCPM-o 2.6 进一步优化了 MiniCPM-V 2.6 的众多视觉理解能力,可以处理任意长宽比的高清图像,像素数可达 180 万(如 1344×1344像素)。在 OCRBench 上取得 25B 以下最佳水平,超过 GPT-4o-202405 等商用闭源模型。基于最新的 RLHF-VRLAIF-V 和 VisCPM 技术,该模型具备了可信的多模态行为,在 MMHal-Bench 上超过了 GPT-4o 和 Claude 3.5,并支持英语、中文、德语、法语、意大利语、韩语等30多种语言的多模态交互。
  • 卓越的效率。  除了对个人用户友好的模型大小,MiniCPM-o 2.6 还表现出最先进的视觉 token 密度(即每个视觉 token 编码的像素数量)。它仅需 640 个 token 即可编码 180 万像素图像,比大多数模型少 75%。这一特性显著优化了模型的推理速度、首 token 延迟、内存占用和功耗。因此,MiniCPM-o 2.6 可以首次支持 iPad 等终端设备上的高效多模态实时流式交互。
多模态大模型发展趋势。多模态大模型的性能持续增强,并在真实世界的流式场景中支持越来越多的模态能力。与传统视觉语言模型的线性发展不同,这种发展趋势更加多维,就像攀登深处的山峰一样,可能蕴含更多变革性的宝藏。

我们将介绍 MiniCPM-o 2.6 中所面临的主要挑战及其相应的解决方案。

🤔 如何实现多模态流式交互?

人类能够持续感知来自环境的视觉、音频和文本信息流,并以较低延迟生成语音和文本回复,但这对当前的多模态大模型来说是一个重大挑战。

💡 1. 我们将各模态的离线编码/解码器改造为支持在线模式,从而支持流式输入/输出处理。

大多数模态的编码器和解码器都是离线的,大语言模型必须等待完整的视觉/音频输入的编码完成后才能进行处理,用户也必须等待语音完整生成后才能收听。我们在时间维度上将不同模态的输入/输出流分割成小块,并以块为单位进行编码/解码以实现更低的延迟。对于语音生成来说,我们设计了一种新颖的流式注意力策略使解码器能够在接收到第一个文本块时就开始生成音频。

💡 2. 我们提出了一种全模态时分复用机制来处理并行多模态流。

借鉴通信领域的时分复用技术,我们将每个模态的信息流分割成小块(每秒一块),并将同一秒内的所有模态信息组合成一个紧凑的序列单元输入给大语言模型主干。基于这个策略,主干模型可以高效地在线处理多模态信息流。

🤔 如何实现高效的端到端声音、情感、口音和语速可控的语音对话?

大多数传统语音对话模型都是基于 ASR-LLM-TTS 流水线实现的,会丢失大量用户复杂情感和环境声音等细粒度信息。而直接使用大语言模型生成音频 token 的端到端模型在训练和推理过程中都存在计算效率低的问题,且训练时数据效率也较低。

💡 我们基于混合的端到端架构在自然语音上进行了大规模预训练。

我们同时通过连续特征和文本将大语言模型和一个轻量级语音解码器连接起来。大语言模型产生的连续特征确保语音监督信号能够以端到端方式反向传播到全部模型参数,从而支持更高的能力上限。文本连接则提供了强大的语义控制并减少了训练数据需求。为了学习丰富的细粒度语音知识,我们在自然语音数据进行了大规模预训练,然后将模型与用户指令对齐。

端到端全模态架构

我们首先介绍 MiniCPM-o 2.6 的端到端全模态整体架构。该模型基于SigLip-400M、Whisper-medium-300M、ChatTTS-200M 和 Qwen2.5-7B-Instruct,总共有 8B 参数。整体架构如下所示。

端到端语音建模

大多数现有的多模态模型依赖 ASR 和 TTS 工具搭建流水线来理解和生成语音,导致了明显的语音信息损失和比较有限的语音控制能力。我们提出一种混合连接的端到端语音建模架构,在保证模型能力上限的同时具备优秀的训练和推理效率。

音频编码

我们首先使用 Whisper 编码音频输入,然后进一步压缩其特征表示来减小后续的计算开销。默认情况下,Whisper 编码器会为每秒音频生成 50 个 token。受 LLaVA-UHD 的启发,我们在将音频 token 输入到大语言模型主干之前会进一步压缩音频 token,以提高 token 信息密度和计算效率。实验结果表明从 50 token/秒压缩到 25 token/秒时,造成的信息损失几乎可以忽略,并可提高近一倍的计算效率。

语音解码

为了实现对语音输出的精细控制,我们首先从大语言模型主干中获得连续特征作为 speech embedding,然后通过大语言模型继续生成文本。speech embedding 和生成的文本同时会输入给解码器作为生成语音梅尔谱的控制条件。MiniCPM-o 2.6 使用了初始化自 ChatTTS 的轻量级自回归语音解码器来联合建模输入的 speech embedding、文本和输出的音频 token。

语音到语音架构

我们直接通过编码后的音频特征(不使用 ASR)将音频编码器与大语言模型连接起来。大语言模型和语音解码器则以混合方式连接:(1)speech embedding 连续特征控制语音、情感、口音及其他细粒度语音特征。在训练过程中,来自语音解码器的梯度会反向传播到包含大语言模型主干和音频编码器的整个模型参数。模型通过端到端方式训练,没有使用任何中间损失和监督。(2)我们还将来自大语言模型的文本输入到语音解码器,来提供更好的语义控制和训练数据效率。

得益于端到端的架构设计,MiniCPM-o 2.6 成为首个支持端到端语音克隆的通用语音对话模型。我们发现 MiniCPM-o 2.6 在语音克隆任务中可以达到与一些专业 TTS 工具相近的性能。

端到端视觉理解

MiniCPM-o 2.6 采用 LLaVA-UHD 的自适应视觉编码方案以实现高清视觉理解,支持处理最高 180 万像素(例如 1344×1344)的任意长宽比图像。具体而言,我们先将图像划分为多个切片,使得每个切片在分辨率和长宽比方面接近 ViT 的预训练设置。为了提高计算效率,我们支持每张图像最多 9 个切片。然后我们将每个切片及原始完整图像输入给 SigLIP 以获取视觉特征。最后,我们应用 perceiver resampler 将每个图像切片的特征序列压缩为 64 个视觉 token。更多细节参见 LLaVA-UHD 和MiniCPM-V 原始论文。

全模态流式机制

本章我们介绍 MiniCPM-o 2.6 的全模态流式机制,包括:(1)改造语音编码器和解码器以支持在线流式处理,(2)使大语言模型主干能够处理并行的多模态流信息。

流式音频编码

大多数现有模型只能在整个音频输入信号完整后才开始进行音频编码,从而引入了显著的延迟。为了解决这个问题,我们将输入音频分割成多个片段,每个片段是表示一秒钟的音频的固定数量音频 token。在音频编码过程中,每个片段采用因果注意力机制进行编码仅关注自身及之前的片段,从而满足在线流式编码的需求,同时与离线整体编码相比保持了最小的信息损失。

每个小方块表示1s的音频片段

流式语音解码

大多数语音生成模型要求在开始生成音频之前,所有文本 token 都已经完整就位,如图 A 所示。虽然这种方式在离线任务中表现良好,但无法满足流式场景的需求。对于流式模型而言,我们需要在部分文本生成时就开始生成(和播放)相应音频。

为了实现流式语音生成,我们每次输入固定数量的文本 token(一个大小为 n 的片段),而解码器立即输出固定数量的音频 token(一个大小为 m 的片段)。这个过程会对下一个文本 token 和音频 token 片段重复进行,以此类推。值得注意的是,文本片段与其对应音频片段之间的并不是精确对齐的。因此在实际应用中,我们为文本 token 片段的大小保留了更大的空余以避免意外情况。

为了实现上述流式策略并尽可能减小对当前最佳语音解码器的改动,我们主要引入了两个改变:

  1. 为文本预留前 N 个位置: 我们使用语音解码器上下文中的前 N 个位置来放置 speech embedding 和生成中的文本。
  2. 引入流式因果注意力掩码: 每个新生成的音频片段只能关注到已经生成的前几个文本 token 片段和其之前的所有音频 token。

在图 A-E 中,我们展示了每当引入新文本 token 和音频 token 时,注意力掩码是如何一步一步变化的。通过控制文本和音频 token 之间的片段注意力掩码,我们使得音频能以流式方式生成和播放。

  1. (文本和音频生成的开始)图 B 中,大语言模型主干生成 speech embedding 和前 n 个文本 token(此处 n=2)。然后,我们将 <Text BOS>(1 个位置)、speech embedding(1个位置)和文本 token(n个位置)输入到语音解码器中(总共输入了 2+n 个位置),基于此,模型可以生成最多 m 个音频 token(此处 m=4),但仅关注前 2+n 个位置以及所有之前的音频 token 位置。
  2. (步骤 1 的重复)图 C 中,大语言模型生成下一段 n 个文本 token。我们将这些新文本 token 输入到语音解码器中。然后,解码器生成下一段 m 个音频 token。
  3. (文本生成结束)图 D 中,大语言模型完成所有文本 token 的生成,产生了最后的 k 个文本 token(k<=n,此处 k=2)。剩余的 k 个文本 token 被输入到语音解码器中。然后,语音解码器生成下一个m个音频 token。
  4. (音频生成结束)图 E 中,语音解码器继续生成音频 token,直到生成 <Audio EOS> token。

这里的 N、n 和 m 是超参数。在实际应用中,我们使用 N=300、n=10、m=50,以实现性能与推理速度之间的平衡。

流式视觉理解

我们将视频流表示为以 1 帧每秒(1 fps)的帧序列。我们每秒钟从输入的视觉流中采样一帧,并使用自适应高分辨率编码策略对其进行编码,然后将编码后的特征输入给大语言模型主干。

为了在效率和性能之间取得平衡,我们设计了一种简单的混合分辨率策略。具体而言,我们只对用户请求结束时的最后一帧进行高分辨率编码(例如 1344 x 896),而其他帧则使用中等分辨率(例如 448 x 448)。

全模态时分复用

为了实现对不同模态的时间对齐的流式理解,我们提出了一种全模态时分复用机制。如整体框架图所示,我们首先使用共享的时间线对齐来自多个模态的信息流。受到通信领域时分复用技术的启发,我们将每个模态的流分成小块(每秒一块),并将同一秒块内的所有模态信息聚集成一个紧凑的序列单元。大语言模型则按时间顺序处理这些多模态序列单元。

需要注意的是,MiniCPM-o 2.6 可以独立于用户提问持续感知多模态输入流,这与现有的多模态流式模型只在用户提问期间获取单帧或少数几帧图像信息是不同的。通过这种方式,MiniCPM-o 2.6 能够处理需要时序记忆和推理的任务(例如,“球在哪个杯子里?”、“我刚才擦掉了哪些字?”)并原生支持多轮流式对话。

延迟分析

我们提供了纯音频模式下首次响应延迟的具体拆解分析。首次响应延迟指从用户请求结束到模型语音开始播放的延迟。作为参考,GPT-4o 在纯语音模式下的初始响应延迟约为 1.8 秒。MiniCPM-o 2.6 在iPad Pro(M4 芯片)上的初始响应延迟分解如下表所示:

多模态系统提示词

社区在使用文本提示词方面已经积累了丰富的经验,一个好的系统提示可以定义模型的角色、增强其准确性、优化细节表现和令模型聚焦重点。

生成语音回复的端到端全模态模型则面临新的挑战。模型输出的音频特征——如声音、语调、口音和其他细微特征至关重要,但无法仅通过文本传达。为了应对这一挑战,我们提出了多模态系统提示词的概念,允许用户通过声学特征控制模型的声音使其与用户意图相符

具体来说,多模态系统提示词包括传统的文本系统提示词和用于确定模型声音的音频部分。这一设计使得用户在推理时可以灵活配置声音。用户可以通过提供一段语音样例作为音频系统提示,来完成端到端的声音克隆;还可以通过将音频系统提示留空,基于语言描述要求模型创建一种新声音(例如 “请使用低沉有磁性的声音和我说话”)。

模型训练

MiniCPM-o 2.6 采用多阶段训练以逐步学习新模态的知识,从而避免模态冲突并将各种模态能力整合到一个模型中。整体的训练阶段可以分为预训练、指令微调和偏好对齐阶段。

预训练

我们首先分别对视觉和音频进行预训练以学习每种模态,然后进行全模态预训练以对齐这些模态。

视觉预训练。 我们利用大规模图像-文本对来对齐视觉和语言模块。在此阶段,我们仅更新视觉编码模块,让模型具备基本的图像理解和 OCR 能力。然后,我们在图文交替数据上训练视觉编码器和大语言模型,使模型具备多图理解和多模态上下文学习能力。

音频预训练。 我们使用音频-文本对数据来训练连接部分的权重,以实现音频模态和主干的对齐。为了学习丰富的细粒度语音知识,我们在自然语音数据上进行大规模端到端的预训练,然后根据用户指令对模型进行对齐。

全模态预训练。 在这一阶段,我们结合来自大规模网络视频的视频和音频流,使用 OTDM 机制使模型能够从不同模态中获取和对齐丰富的知识。

全模态指令微调

该阶段使用高质量的多模态数据进行监督微调,包括视觉问答、语音理解、语音生成和多模态流式视频(带音频)理解数据。我们对模型进行全参数微调以统一模型的视觉能力、语音理解和生成能力,以及流式多模态能力,同时增强模型的指令遵循能力。

偏好对齐

最后,MiniCPM-o 2.6 采用 RLAIF-V 技术以进一步提高模型的可信度和综合能力。在这个阶段,模型使用分而治之的策略对不同的回复进行评分以构建偏好数据集,并进行直接偏好优化训练(DPO)。同时,我们还特别将相比图像幻觉更常见的视频幻觉比例降低了63%。我们也使用了 MMPR 等开源偏好数据集来多样化训练数据。

评测

我们充分评估了 MiniCPM-o 2.6 的视觉理解、语音对话和多模态流式交互能力。实验结果表明,该模型在视觉、语音和多模态流式交互能力上的整体表现与 GPT-4o-202405 相当。

MiniCPM-o 2.6 的 iPad Pro 实机演示和 web demo 演示样例:

局限性

  1. 可能不稳定的语音输出。 多模态系统提示词使得更灵活的语音控制和许多有趣的功能成为可能,但也给语音输出的稳定性带来了更多挑战。传统的语音对话模型通过全参数记忆单一输出声音,与之相比,MiniCPM-o 2.6 需要从多模态系统提示词中提取和复制声音。受到该问题影响,语音生成结果可能会存在背景噪音和无意义声音等。
  2. 长语音生成。MiniCPM-o 2.6 原生支持最长 45 秒的单次语音生成,我们通过滑动窗口技术来支持生成更长的语音。模型训练与滑动窗口推理之间的差距可能导致长语音生成过程中的不稳定现象。
  3. 多模态流式交互能力。 作为一项实验性能力,模型的多模态流式交互能力在感知、理解和推理能力上仍然有限。我们期待社区的共同努力构建更优秀和可靠的性能效果。
  4. 网页 demo 高延迟。 用户在使用托管在海外服务器上的网页 demo 时可能会遇到异常的高延迟情况。我们建议在本地部署 demo(例如 4090 GPU 可以流畅运行)或使用合适的网络连接。

参考文献

  1. MiniCPM-V: A GPT-4V Level MLLM on Your Phone. 2024.
  2. RLHF-V: Towards Trustworthy MLLMs via Behavior Alignment from Fine-grained Correctional Human Feedback. CVPR 2024.
  3. RLAIF-V: Aligning MLLMs through Open-Source AI Feedback for Super GPT-4V Trustworthiness. 2024.
  4. LLaVA-UHD: an LMM Perceiving Any Aspect Ratio and High-Resolution Images. ECCV 2024.
  5. Large Multilingual Models Pivot Zero-Shot Multimodal Learning across Languages. ICLR 2024.