news 2026/10/28 7:23:54

VITS2端到端TTS模型原理与ONNX CPU部署实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VITS2端到端TTS模型原理与ONNX CPU部署实践指南

1. 为什么我会写下这份 VITS2 读书笔记

1.1 从 VITS 到 VITS2:端到端 TTS 的演进

做 TTS 的朋友,大概率都经历过“先提特征、再丢给声码器”的两阶段时代。那时候最头疼的不是模型本身不够强,而是上游发音不准、下游音质差,两套系统各干各的,出了问题还得靠人去猜锅在哪一环。VITS 出现以后,这个局面发生了实质变化:它把文本编码、时长预测、声学建模、声码生成全部放进一个模型里训练,中间只保留一个可选的 mel 谱作为监督信号,真正做到了端到端。

VITS2 是 VITS 的升级版,在它基础上针对几个非常具体的问题做了改动。这里我不打算照着论文复述一遍公式,而是把我在落地中真正用到的、想明白的部分写出来。尤其是“神经网络 TTS 在 CPU 上能不能跑?”、“ONNX 部署到底要改哪些东西?”、“接入‘阅读’这类本地朗读工具时,语音包为什么经常声音不稳?”这几个问题,我会尽量讲得比论文笔记更贴近使用场景。

1.2 这篇笔记适合谁看

如果你只是想把一个 TTS 模型跑通、合成几句话,官方仓库的 README 就够了。我这份笔记更偏向三类人:

  • 第一次接触 VITS / VITS2,想搞清楚“变分自编码器在 TTS 里到底起什么作用”的初学者。
  • 手里有训练好的模型文件,准备转 ONNX,再通过 sherpa-onnx 在 CPU 上做离线推理的工程向同学。
  • 在做本地朗读工具、移动端语音包,希望理解“为什么同一个模型,别人部署出来音质比我好”的人。

这份笔记没有严格的算法推导,但我会把关键模块的作用、参数之间怎么联动,以及踩过的坑都交代清楚。因为 VITS2 这套东西,你说它难,其实推理路径就那么几条;你说它简单,训练、转换、部署每一步都能给你整出点意料之外的问题。

2. VITS2 架构拆解:训练与推理视角对照

2.1 条件变分自编码器到底在做什么

先用大白话解释一下 VITS2 的核心思想。模型其实在做一件事:给定一行文本,先从文本里抽出一个“先验分布”,再从真实音频中抽出一个“后验分布”,然后让这两个分布在训练时尽量靠近。生成语音的时候,没有后验分布可用,模型就把先验分布里的采样结果通过一系列变换,送到解码器里合成波形。

那个“一系列变换”就是 Flow 模块,它负责把简单的先验分布逐步塑造成更贴近真实语音的分布。VITS 原文里,Flow 模块通常由一堆可逆的卷积层组成,比如仿射耦合层、ActNorm、可逆 1x1 卷积。这些层的共同特点是:正变换和逆变换都能算,逆变换时不用重新求梯度。

我在第一次读 VITS 论文的时候,死活想不明白一个点:为什么用 Flow 不直接用一个自回归解码器?后来理解了,自回归模型在逐步输出时会有误差累积,而且速度慢。Flow 则是一次性计算整段序列的变换,速度上有天然优势。VITS2 在这个大框架上没变,但把 Flow 内部的层结构做了调整。我自己的理解是:VITS2 并没有把 Flow 变复杂,反而是通过规范化让 Flow 的数值更稳定,训练更容易收敛。

在 VITS2 里,整个系统大致可以拆成这几个模块:

模块输入输出在推理时的作用
文本编码器音素序列、说话人 ID文本隐含表示提供条件信息
时长预测器文本隐含表示、文本音素嵌入每个音素的时长分布决定每个音素停留多少帧
Flow 模块后验采样的表示先验分布中的潜在表示让先验和后验对齐
解码器潜在表示序列原始波形真正生成音频

2.2 文本编码器与 BERT 去歧义,对中文特别有价值

VITS2 论文里特别强调了一个问题:同音字(homophone)带来的声学不确定性。这个问题在中文 TTS 里太常见了。“行”字可以读 háng 也可以读 xíng,“地”字做助词时读轻声“de”,做名词时读“dì”。如果只看单字,没有上下文信息,模型根本不可能知道该读哪个音。

VITS2 给出的方案是增强文本编码器。它提供了两种变体:一种是继续用 Flow 来做文本编码,另一种是引入预训练语言模型嵌入,最典型的就是 BERT 表示。用 BERT 的意思是说,先把整句文本送入预训练 BERT,拿到每个 token 的上下文相关表示,再把这些表示作为附加输入,和音素序列拼在一起送进 TTS 的文本编码器。

实际用下来,BERT 嵌入对重音和语气起伏的影响很明显。在中文语料上,加入 BERT 之后,“他在地铁站里扫地”这种句子的后半段语调要自然很多,不会出现“地”字听起来像被单独拎出来的奇怪感觉。

需要提醒一点:BERT 表示并不是每个场景都要硬上。如果你做的是英文或者日文,或者语料本身很规范、文本经过严格的注音预处理,BERT 带来的收益有限,反而增加部署时的模型体积。所以 VITS2 留了两个选项,不是没有道理的。

2.3 时长预测与对齐机制

时长预测是 TTS 里很容易被忽略,但实际影响很大的模块。一个音素该持续 50 毫秒还是 200 毫秒,直接决定语速和节奏。VITS2 里的时长预测器是随机时长预测器,训练时会预测一个对数域的均值,推理时会在这个均值上加上随机扰动,让同一个文本每次合成的语速略有不同,听起来更接近真人。

训练阶段的对齐是用单调对齐搜索(Monotonic Alignment Search,MAS)完成的。这个算法本质上是在找“哪一段音频特征对应哪一个音素”的最优对齐路径,而且要求对齐是单调的,不能跳回过去。VITS2 在 MAS 基础上做了进一步处理:它把训练时得到的对齐结果重新标记成稳定的时长监督信号,减少因为对齐不平滑造成“吞字”“重复音”的问题。这一点我深有体会,后面会专门讲。

关于时长的单位,这里有个容易混淆的细节:VITS 系列模型内部是按帧走的,通常每一帧就是解码器输出音波时的一次步进。论文里好听叫“时长”,实际就是“该音素占多少帧”。合成时把所有音素的帧数累加,得到潜在表示的总长度,再送进解码器。

我一般给非专业同事解释时都这么说:文本编码器决定“说什么内容”,时长预测器决定“每个字停顿多久”,Flow 和解码器决定“音色和音质”。三个部分像流水线一样串联起来。

2.4 对抗训练与生成器目标

VITS2 延续了 VITS 的对抗训练思路。除了常规的 mel 谱重建损失和 KL 散度损失之外,模型还配了一个判别器(Discriminator)。判别器的作用是区分“真实波形”和“模型生成波形”,生成器则要尽量骗过判别器,让输出波形在统计分布上接近真声。

对抗损失加进来之后,最直观的感受是高频细节变好。早期的 VITS 偶有“塑料味”或“沙沙声”,大多是因为生成器在频谱的某些频段上偷懒,只保证 mel 谱接近,却没有把波形在时域上的真实感补齐。判别器直接把感知维度拉进来,强迫生成器再造出更细的纹理。

这个要记住的坑是:对抗训练容易过猛,判别器太强会导致生成器崩溃,输出要么全静音,要么高频噪音。VITS2 在代码里对各种损失权重做了初始化默认值,一般情况下不要随便改,除非你明确知道自己在做什么。

3. 用 VITS2 训练一个自己的模型,实际要准备什么

3.1 数据和预处理:先过文本注音这一关

数据集方面,英文常用 LJSpeech,中文可以用 AISHELL 或自己录制的朗读语料。VITS2 的预处理流程和 VITS 很相似,核心是把文本变成音素序列。

英文相对简单,用 espeak 或 phonemizer 把单词转成音素即可。中文就麻烦,因为“字”和“音”不是一一对应,最好先做词边界切分,再做注音,也就是转换到拼音或声母韵母序列。我处理中文时的习惯是:

  • 先做文本正则化,把数字“2024”转成“二零二四”或“两千零二十四”。
  • 再做分词,同时处理多音字。这里可以借助外部词典,也可以用 BERT 的上下文表征辅助判断。
  • 最后把每个汉字映射到音素表,并附上声调信息。

如果你打算训练中文模型,音素表里最好包含“声母 + 韵母 + 声调”的组合,或者直接用带声调的拼音字符串。模型本身不识字,它只关心音素序列和语音帧之间怎么对齐。如果音素表设计得太粗,比如把“shi”所有声调混在一起,合成结果就会出现腔调不准的问题。

3.2 训练中的显存、算力与 checkpoint 选择

VITS2 训练不是非常吃显存,但也不是随便一张显卡就能跑得很好。我用一块 24GB 显存的卡训练单说话人模型,batch size 可以开到 16 到 32,取决于序列长度。如果只有 8GB,也不是完全不能训,只是要大幅调小 batch,训练时间会拉长很多。

训练过程中建议开着 TensorBoard 盯几个关键指标:reconstruction loss、KL loss、duration loss。不要只看总 loss,因为几个 loss 的量纲不同,合在一起很容易掩盖问题。

训练到后期,模型会出现典型的“先稳后崩”现象。也就是说,前面几万步听感一直在变好,某一步之后突然开始出现尖锐噪音。这时候就要用验证集挑 checkpoint,手动试听,不要迷信自动保存的最后一步。我通常会保留每 5000 步的中间结果,到最后统一试听一遍再决定用哪个。

3.3 一个“能用”的模型并不是训练的终点

训练完成后,你手里的 checkpoint 是 PyTorch 格式,包含 generator、discriminator、optimizer 等完整状态。真正部署时用不到这么多东西,只需要把生成路径导出。

我一般建议这么做,几步搞定:

  1. 加载训练好的 generator,去掉判别器部分。
  2. 用一段固定文本做 dummy 输入,跑一遍 forward,确认输出形状。
  3. 保存 state_dict,后续转 ONNX 或直接做 TorchScript,都以这个文件为基准。

有件事要特别强调:VITS2 默认会带说话人嵌入,即使只训了一个人,嵌入层的维度也在那个位置。导出时不要为了省参数把说话人嵌入层删掉,否则会出现推理报错,或者声音音调变得异常。

4. 把 VITS2 转成 ONNX:我在部署时踩过的坑

4.1 为什么要用 ONNX 和 sherpa-onnx

训练完模型之后,直接拿 PyTorch 做推理也不是不行,但工程上问题很多。PyTorch 的运行时太重,移动端不方便,模型文件和推理代码耦合在一起,版本一升级就崩。ONNX 作为中间格式,可以把训练框架和部署框架解耦。

sherpa-onnx 则是一个专门做离线语音推理的轻量级库,支持 CPU,尤其适合在普通笔记本、Android 手机、嵌入式设备上跑。它还内置了 VITS 系列的加载器,这也是我最初接触 sherpa-onnx 的原因:想直接在手机上跑一个本地 TTS 语音包,不想接任何云服务。

后来我发现,sherpa-onnx 对 VITS 类模型的支持还算成熟,tokens、lexicon、dict 这些配套文件按规范放好后,调用接口非常简单。

4.2 导出过程实录:动态轴和前置处理别偷懒

把 VITS2 的 PyTorch 模型转成 ONNX,核心问题是动态 shape。因为输入文本长度是不固定的,每个音素数也不一样,所以 ONNX 里一定要把 batch 维和序列维设置成动态轴。

我当时大致按这个思路写导出脚本:

import torch import onnx model.eval() dummy_text = torch.randint(0, 50, (1, 20)) dummy_sid = torch.tensor([0], dtype=torch.long) dummy_noise = torch.randn(1, 32, 200) torch.onnx.export( model, (dummy_text, dummy_sid, dummy_noise), "vits2_onnx_model.onnx", input_names=["text", "sid", "noise"], output_names=["audio"], dynamic_axes={ "text": {0: "batch", 1: "length"}, "noise": {0: "batch", 2: "frames"}, }, opset_version=15, )

导出之后,先用 onnxruntime 跑一遍同样的输入,和 PyTorch 的输出对比,看误差是不是在可接受范围内。注意对比时不要看原始误差,要转成音频试听。数值上几个毫秒级的差异都会体现在音色细节上,重点是人耳是否可分辨。

VITS2 的 ONNX 导出还有一个常见的坑:fused 算子。模型里如果包含某些自定义算子或不太常见的卷积结构,旧版 opset 不支持,导出时会报错。解决方案一般有两种:一是提高 opset 版本,二是把不支持的层替换成等价的标准层。

4.3 CPU 推理与线程调优

sherpa-onnx 在 CPU 上跑 VITS2 的实时率,取决于模型大小和硬件。普通笔记本上,单说话人的 VITS2 模型,输出 16kHz 音频的实时率大概在 0.3 到 0.5 左右。意思是一秒钟的音频需要两到三秒生成,比实时要慢一些。如果你在服务器或较新的 CPU 上跑,多线程开启后性能会明显改善。

线程数不是越大越好。我在四核八线程的机器上做过测试,线程数从 1 加到 4,速度提升明显,但从 4 加到 8,反而略有下降。因为模型内部的算子并行度有限,线程调度开销反而变大了。建议先设成物理核数的一半,再逐步往上试探。

memory 也要注意。CPU 推理时,如果同时处理很长的文本,潜在表示序列会很长,显存是没了,但内存占用会很夸张。我习惯的做法是先把长文本按标点拆成短句,每次生成一句,然后再把音频拼起来。这样做还有一个好处:单句内文本长度短,时长预测不容易失控。

下面是一个我在台式机上实际用过的 sherpa-onnx 调用示例,模型文件和 tokens 文件放在本地目录:

import sherpa_onnx import soundfile as sf config = sherpa_onnx.OfflineTtsConfig( model=sherpa_onnx.OfflineTtsModelConfig( vits=sherpa_onnx.OfflineTtsVitsModelConfig( model="vits2_tuned.onnx", tokens="tokens.txt", lexicon="lexicon.txt", dict_dir="dict", ), num_threads=4, provider="cpu", ), ) tts = sherpa_onnx.OfflineTts(config) audio = tts.generate("我们在这里验证离线语音合成效果。", sid=0, speed=1.0) if audio.samples is not None: sf.write("output.wav", audio.samples, audio.sample_rate)

这套代码跑通后,你想把它接到“阅读”这类本地朗读工具里就很简单了。工具通常只要求你提供一个音频文件输出接口,底层换成这个 TTS 引擎即可。

4.4 把 VITS2 接入本地朗读工具

很多朋友在做“语音朗读包”,其实本质就是把一个 TTS 引擎编译成独立模块,再给阅读类 App 提供音频输出。sherpa-onnx 官方也提供了 Android Demo,可以直接把 VITS2 模型放到 assets 目录里。

我建议本地语音包保持这么一套结构,干净清晰:

vits2_zh/ ├── model.onnx ├── tokens.txt ├── lexicon.txt └── dict/

tokens.txt 里每一行是一个 token,必须和训练时的音素表完全一致。如果 tokens 列表顺序和模型不匹配,合成出来往往是乱码或大量重复音。lexicon 和 dict 是给中文模型做字的音素映射用的,英文或纯拼音模型可以省略。

接入后的实际听感影响因素,我总结下来有三个:采样率、文本分句策略、说话人 ID。VITS2 通常输出 16kHz 单声道,如果你的 App 默认播放 44.1kHz,中间不做重采样,声音会变粗或变闷。最好在播放层做一次转采样。

5. 常见问题与避坑清单

5.1 声音吞字、语速忽快忽慢

模型输出音频时,如果某些音素对应的时长预测特别短,就会出现“吞字”,尤其发生在中文多字词、轻声字上。我遇到这种情况,第一反应不是去调模型,而是看输入文本是否经过了正确的正则化。

比如数字、英文、符号混在一起时,如果不先转换成对应的朗读形式,模型在音素序列里会遇到陌生 token,推理时就会硬着头皮给它一个很短的时长。先把“第3章”处理成“第三章”,“2024年”处理成“二零二四年”,吞字问题立刻缓解。

如果确认输入没问题,问题可能出现在时长预测器的温度参数上。VITS2 推理时通常可以设置 temperature,数值越大语速变化越剧烈,越小越平稳。遇到语速不稳定的情况,把 temperature 降到 0.66 左右试一下。

5.2 合成结果是白噪音或尖锐嘶嘶声

这是部署时最常见的崩溃型问题。原因多半是 ONNX 导出的输出层和输入 mel 特征的 hop 长度不匹配,或者 tokens 里混入了训练时根本没有的字。

我的排查顺序是固定的:

  1. 检查 tokens.txt 里是否有多余的空行或空白字符串。
  2. 用最简短的一句话测试,比如“你好”。
  3. 直接在 PyTorch 环境里加载同一个模型,跑同样输入,判断问题是出在模型本身还是出现在 ONNX 转换。
  4. 如果 PyTorch 正常而 ONNX 出错,重新导出,重点检查动态轴。

还有一个小概率情况:模型文件里混入了说话人嵌入的维度错误。比如你导入了多说话人模型,但推理时 sid 传了超出范围的数字。所以请仔细确认 sid 长度,范围通常从 0 到说话人数减一。

5.3 内存占用高、进程被系统杀掉

长文本直接合成时,潜在表示序列会被拉得很长。解码器内部用的是转置卷积,输入序列长度会随着层数增加而扩大,中间产生的张量很可能让内存瞬间翻好几倍。

解决方案分三层:

  • 应用层:把文本按标点拆成短句,每句单独合成。
  • 配置层:sherpa-onnx 中控制 max_num_sentences,限制并行处理的句子数。
  • 系统层:确认进程的虚拟内存上限,别用默认配置去扛长文本。

5.4 CPU 太慢,有没有优化手段

有。第一是靠 ONNX Runtime 的线程数和 intra-op 参数调整,这个最省事。第二是模型量化,把权重从 FP32 降到 INT8,VITS2 属于生成模型,量化后音质会损失,但在某些场景可以接受。第三是换更小的变体,比如减少 Flow 层数或隐藏维度。

我不建议一上来就做 INT8 量化。量化后的模型不是不能听,而是声调起伏变小,容易显得“机械”。先尝试 FP32 的线程优化,如果实在不行,再做精度折中。

5.5 说话人的音色像“另一个人”

训练时如果数据里混有多个人的声音,或者噪声背景差异大,生成音色就会有漂移。VITS2 虽然对说话人嵌入做了归一化处理,但前提是训练语料中确实有稳定、独立的音色特征。我自己训中文语料时都会先做一遍响度标准化,再把信噪比低的片段清掉,否则模型很容易把“噪声”学进音色里。

6. 最后说点个人体会

写这份笔记的时候,我又把最早训练的一批 VITS2 中间结果翻出来重新听了一遍。很多模型,客观指标看着还行,主观听感却完全不能用;反过来,有些指标一般的 checkpoint,听上去反而自然。TTS 这个领域,耳朵永远比数字诚实。

所以我的最终建议是:不要只看论文里的架构图,也不要只看训练曲线的漂亮程度。把模型导出成 ONNX,在真实的 CPU 环境里合成几十条不同长度的句子,放到目标设备上试听,再做修改。整个流程跑通之后,你才算真正“拥有”了一个可用的 VITS2 系统。如果后续要把模型接入朗读工具,我建议从 sherpa-onnx 的离线示例开始,先把“模型文件 + tokens + lexicon + 短句合成”链路跑通,再逐步加长文本和说话人数量。这个基础打得稳,比什么都重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 7:22:31

单文件AI编码代理:融合GUI自动化与MCP协议的开源实践

写这个项目的时候我其实没指望它能火。当时手头堆了好几个重复性工作:改前端样式、调接口、整理 GUI 自动化测试脚本,每个都得开着 IDE 一遍遍手动点。后来我把常用的编码辅助能力封装成一个只有单个文件的 AI 编码代理,它能直接操控图形界面…

作者头像 李华
网站建设 2026/10/28 7:22:10

YOLOv8模型训练与部署实战技巧

1. 项目概述:YOLO模型训练全流程解析YOLO(You Only Look Once)作为当前最流行的实时目标检测算法之一,其最新版本YOLOv8通过Ultralytics团队的开源实现,让模型训练变得前所未有的简单。我在工业质检和安防监控领域使用…

作者头像 李华
网站建设 2026/10/28 7:21:34

专科生论文写作神器:千笔智能体核心功能与实操指南

1. 项目背景与核心价值作为一名在学术写作领域深耕多年的从业者,我深刻理解专科生在论文写作过程中面临的独特挑战。这个群体往往缺乏系统的学术训练,却又需要在短时间内完成符合学术规范的论文作业。"千笔专业学术智能体"正是针对这一痛点设计…

作者头像 李华
网站建设 2026/10/28 7:20:12

Big Sur 11.0.1镜像虚拟机安装:格式与OpenCore调优

简介:面向希望在非Mac设备或虚拟机环境中体验macOS Big Sur的开发者、测试人员与系统爱好者,这份文档围绕11.0.1 20B50版本提供镜像获取信息与版本概览,帮助读者快速定位所需系统镜像并了解其适用场景。包内共1个docx文件,大小约3…

作者头像 李华
网站建设 2026/10/28 7:20:10

NovelAI绘画实战:从Stable Diffusion到二次元创作

1. 项目概述作为一名在AI绘画领域深耕多年的从业者,我见证了从早期GAN到如今Diffusion模型的整个技术演进历程。NovelAI作为基于Stable Diffusion的二次元特化版本,凭借其出色的画面表现力和对动漫风格的精准把控,迅速成为内容创作者的新宠。…

作者头像 李华
网站建设 2026/10/28 7:19:41

零基础转行网络安全:路线图与实战指南

1. 为什么网络安全行业吸引转行者?网络安全行业近年来成为转行热门选择并非偶然。根据我过去五年接触的数百名转行学员案例,这个行业的核心吸引力主要体现在三个方面:首先是薪资水平。网络安全工程师的平均薪资比传统IT岗位高出30%-50%&#…

作者头像 李华