ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

中文语音合成实战:Tacotron中文改造、注意力机制与训练避坑指南

中文语音合成实战:Tacotron中文改造、注意力机制与训练避坑指南

简介:面向语音合成与自然语言处理研究人员及开发者,这份算法工程资料包基于改造的 Tacotron 端到端序列到序列模型,系统讲解中文语音合成完整训练方案。内容覆盖中文音节结构与声调特性的输入表示、注意力机制改进、声学特征选取,以及 WaveNet/WaveRNN 声码器替换思路,并附有 TTS-CORPUS、AISHELL 等开源数据集的获取方式与详细环境搭建教程。资源包共 36 个文件,以 18 个 Python 脚本为主体,涵盖模型核心代码、训练配置、依赖清单,并附可运行的 Notebook 演示与说明文档,另有 lib、tests 等辅助模块,压缩包整体仅 11.47MB,便于快速下载部署。已有 691 人学习下载。通过数据预处理脚本与训练流程对照,读者可从零构建中文语音合成系统,掌握注意力机制调优、声码器选型及 MOS 评估方法,也可为定制中文语音模型提供工程参照,适合具备一定深度学习基础、希望实战端到端语音合成的中高级开发者与研究者。

1. 把Tacotron改成说中文的系统:这份资源到底解决了什么

很多跑过英文 Tacotron 的人,第一次把模型切到中文语料时,合成出来的是那种每个字都读得清楚、但连起来像机器念经的声音。这个资源包就是冲着这个问题来的:它基于 PyTorch 版的 Tacotron 做了面向中文的改造,压缩包里带完整训练代码、注意力可视化 notebook、推理脚本和 setup.py,配套数据集的获取方式和环境搭建教程都写进了文档里。适合已经跑通英文 TTS、想转中文方向的人,也适合刚接触端到端语音合成、想从代码层面理解注意力机制怎么处理中文语流的学习者。如果你只是想要一个能直接部署的成品音色,这个包给不了现成模型,它给你的是从零训练出一套中文声学模型的能力,以及训练路上每一个坑的绕行路线。

2. 中文改造点与代码结构:从拼音输入到注意力机制的落地改动

2.1 中文和英文的差异,决定了Tacotron必须改三处

Tacotron 最早是为英文设计的,英文还能按单词边界切分,中文是连续语流,字与字之间没有天然分隔;再加上声调直接决定语义,没有一个明确的“字→拼音+声调”映射,模型很难把输入文本映射到正确的韵律上。这个包在改造时主要动了三块:输入表示用拼音序列加声调数字(nǐ hǎo 写成 ni3 hao3)替代英文音素序列;注意力机制改成更适应连续语流的位置敏感注意力(location-sensitive attention),缓解英文版常见的注意力塌缩;声学特征上使用 80 维梅尔谱作为预测目标,而不是直接把文字映射到波形。这样改造之后,模型学习的不是“汉字→音频”的黑匣子映射,而是“音节+声调→声学特征”的可解释中间表示。实际训练时你会发现,loss 的下降曲线和英文版差别不大,但最终合成音色的自然度提升非常明显。

2.2 资源包代码结构:train.py入口、hparams.py配置、synthesis.py推理

先把压缩包解开,目录组织是这个样子的(以解压后的实际层级为准):根目录有 train.py、synthesis.py、hparams.py、setup.py,lib 目录下是一个 tacotron 子包,tacotron_pytorch 目录里是注意力实现和编码器/解码器核心代码,tests 和 notebooks 分别放测试脚本和一个叫 Test Tacotron 的 notebook。首次拿到这个包,我的建议是先不要看训练代码,先看 hparams.py,里面是全局超参数;再看 synthesis.py,搞清楚推理时怎么把文本转成 mel 谱;最后回到 train.py 看数据加载和训练循环。这个顺序能让你在二十多分钟内建立“数据从哪来、模型怎么算、结果怎么出”的完整链条,而不是被一堆互调的模块绕晕。

setup.py 是用来做包安装的,我习惯在 conda 环境里执行 pip install -e . 把它安装成可导入模块,具体命令到第 3 章再展开。这里只需要知道库的根目录是 lib/tacotron,写代码时 import 路径别写错。tests 目录里的 test_attention.py 和 test_tacotron.py 是单元测试,用来验证模块能不能正常前向传播,跑训练前先把这两个文件执行一遍,能排除不少环境层面的低级问题。

2.3 先跑通 Test Tacotron.ipynb:用注意力可视化验证模型能对齐

notebooks 里的 Test Tacotron.ipynb 是一个独立的验证脚本,不是训练脚本。它做的事情是加载一个预训练 checkpoint(或者随机初始化的模型),输入一段拼音序列,逐帧输出注意力权重矩阵。注意力矩阵是所有 TTS 调试里最该看的东西:训练收敛的模型,注意力矩阵应该是一条接近对角线的带状分布;如果注意力发散或者塌缩成一团,后面合成出的音频必然乱。

加载检查点并打印注意力权重的过程大概是:

import torch from tacotron_pytorch.tacotron import Tacotron # 加载模型,n_symbols 要和你训练时的字典大小一致 model = Tacotron(n_symbols=120, n_frames=80, reduction_factor=2) checkpoint = torch.load("checkpoint_epoch_50.pth", map_location="cpu") model.load_state_dict(checkpoint["model"]) model.eval() # 模拟输入:拼音序列转成ID,结尾补上结束符 text_ids = torch.tensor([ [23, 45, 67, 89, 0], # 数字对应拼音字典索引,0是结束符 [21, 43, 65, 87, 0] ]) with torch.no_grad(): mel_out, attn_map = model(text_ids) print(attn_map.shape) # (batch, decoder_steps, encoder_steps)

这段代码里,n_frames 是梅尔谱维度,由 hparams.py 里的 num_mels 决定;reduction_factor 如果设成 2,解码器每一步会输出 2 帧谱,注意力图的行数会相应变短。attn_map 是三维张量,把第二维和第三维用 matplotlib 画成 heatmap,就能看到每个解码步对输入音节的关注分布。我一般会在训练每五千步后手动跑一次这个 notebook,把热力图存下来对比,比起只看 loss 曲线要直观得多。很多“以为在训练、其实在瞎学”的问题,都是在这一步发现的。

3. 环境搭建与中文数据集准备:从裸机到第一条训练命令

3.1 环境版本:这是 PyTorch 系,别被 README 里的 TensorFlow 说法带偏

这里有一个非常容易踩的坑:这个包的摘要和部分历史文档里会写明“Python 3.x + TensorFlow 2.x”,但实际压缩包里的代码主力是 tacotron_pytorch 目录,整个实现是基于 PyTorch 的。以代码目录为准:训练入口 train.py、模型定义都在 PyTorch 框架下,你在装环境时应该装 PyTorch 而不是 TensorFlow。版本选择上,PyTorch 1.8 到 2.x 都能跑;Python 建议 3.8~3.10,太新的 Python 3.12 在部分依赖的编译环节容易翻车。

我通常这样建环境:

conda create -n tts_chinese python=3.9 -y conda activate tts_chinese pip install torch==1.13.1+cu117 torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cu117 pip install numpy scipy librosa matplotlib pypinyin jupyter pip install -e . # 以可编辑模式安装当前包

参数说明:torch 和 torchaudio 的版本要配对,cu117 是 CUDA 11.7 的预编译版本,如果你的 GPU 驱动只支持 CUDA 11.x 以下,可以用 cu113 或 cu116 替换;librosa 负责音频读取和梅尔谱提取,0.9 或 0.10 版本都兼容这个包的接口;pypinyin 用于把汉字转成带声调拼音,是中文 TTS 预处理的核心依赖,后面处理数据时一定会用到。逻辑说明:pip install -e . 把库以开发模式装进当前环境,之后你在任何目录 import 这个包都会指向当前源码,改代码不用重装。

注意:本资源以 PyTorch 版本为准。README 或摘要里如果出现“TensorFlow 2.x”字样,是早期英文版遗留的说明,不要据此装 TensorFlow,否则后续代码一行都跑不起来。

3.2 中文数据集选型:AISHELL-3 和 BZNSYP 怎么选

数据集决定了你这个模型的声音底子。就这个包的需求来说,数据要包含两个要素:一是清洗过的音频(wav 格式,采样率一般 22050Hz),二是与音频逐句对应的文本标注。中文开源 TTS 里最常见的两个选择是 AISHELL-3 和 BZNSYP。AISHELL-3 是一个多说话人数据集,包含大约 85 小时的中文录音,文本带拼音标注;BZNSYP 是标贝女声音库,约 12 小时单人录音,音色一致性高、噪声低。如果你只想快速验证训练流程,我建议先用 BZNSYP,单说话人、音频干净、预处理脚本好写;如果要做多说话人音色控制,再切到 AISHELL-3。

数据集时长说话人标注适用场景
BZNSYP约12小时单女声文本+拼音首次跑通训练全流程
AISHELL-3约85小时多说话人文本+拼音多音色/说话人控制
TTS-CORPUS数小时混合文本补充语料/跨域测试

选好数据集后,把音频统一处理成 22050Hz 单声道 wav,文件名按句号映射到文本文件,格式类似000001.wav 这是一段测试文本。,这个配对文件后面会被 train.py 读入。公开渠道可以直接下载这些数据集,注意看授权协议,个人学习和研究用途基本没问题,商用需要逐条确认。

3.3 数据预处理:把汉字转成拼音序列并提取梅尔谱

训练前必须做两步转换:文本层把汉字转成拼音加声调,音频层切帧并提取梅尔谱。我一般写一个独立的 preprocess.py 来处理,不直接改 train.py。

from pypinyin import pinyin, Style import librosa import numpy as np # 汉字转带声调拼音 def text_to_pinyin_ids(text): py = pinyin(text, style=Style.TONE3) # TONE3 输出 ni3 hao3 这种带数字声调 id_list = [vocab[item[0]] for item in py] # vocab 是拼音->索引字典 id_list.append(vocab["<end>"]) return np.array(id_list) # 音频转梅尔谱 def audio_to_mel(path, sr=22050, n_mels=80): wav, _ = librosa.load(path, sr=sr, mono=True) mel = librosa.feature.melspectrogram( y=wav, sr=sr, n_mels=n_mels, n_fft=1024, hop_length=256, win_length=1024 ) mel = librosa.power_to_db(mel) return mel.T # (frames, n_mels)

这里有几个值得注意的参数:Style.TONE3 会把“你好”输出成ni3 hao3,声调数字跟在后边,比用 TONE2 的声调符号(ní hǎo)更好切分;n_fft=1024、hop_length=256 是 Tacotron 常用的 STFT 配置,换算下来一个 1 秒的音频会得到约 86 帧梅尔谱,这个帧率直接决定解码步数;文本末尾的<end>结束符必须加,否则解码器不知道在哪里停,训练时长会因为对齐边界不明确而拉长。标点是另一套逻辑,我习惯把逗号和句号也做成单独的符号放进 vocab,而不是直接删掉,不然模型学不到停顿位置,合成长句时会出现一口气憋到底的听感。

4. 训练实战与调参:hparams.py里的关键参数和训练日志怎么看

4.1 第一次训练前的超参数设置

hparams.py 是全局超参数配置文件,涉及到中文训练最关键的参数主要有这几个:num_mels、reduction_factor、batch_size、learning_rate,以及注意力相关的维度。我建议第一次跑的时候把注意力维度保持默认,只动三个参数:batch_size 以显存能塞下为准、但不要超过 32;reduction_factor 设 2 或 3,合成时解码器每次输出 2 或 3 帧,能有效减少解码步数,训练更快,但过大的 reduction_factor 会让音质发糊;learning_rate 用 1e-3,配合预热和衰减。

给一份推荐的首次训练参数表:

参数推荐值说明
num_mels80梅尔谱维度,别低于 80,否则高频细节丢失
reduction_factor2大于 3 音质糊,等于 1 训练慢
batch_size16~32取决于 GPU 显存,AISHELL-3 用 32 可能 OOM
learning_rate1e-3配 Adam,注意力发散就降到 5e-4
max_iters200k+中文语料收敛往往比英文慢

4.2 train.py 的训练循环与日志

train.py 支持命令行参数覆盖 hparams.py 里的默认值。启动训练我一般这么跑:

python train.py --data_root=./data/bznsyp \ --checkpoint_dir=./checkpoints/ \ --batch_size=16 \ --reduction_factor=2 \ --max_iters=200000 \ --log_dir=./logs/

这个命令里的 data_root 指向预处理好的数据目录,目录里要有一个 metadata 文本文件,每行是“音频文件名加拼音/文本 ID 序列”,这是整个训练的数据入口。checkpoint_dir 用来存断点,我习惯每五千步手动把 checkpoints 目录打包一次,防止训练中断后没有后悔药。log_dir 是 TensorBoard 日志目录,loss 曲线和注意力图都能在里面看。

训练过程中,每 100 步会打印当前的 step、loss、学习率。真正需要盯的不只是 loss,还有一个细节:reduction_factor=2 时,解码器每一步输出 2 帧,如果 loss 在 10k 步后仍然在 1.0 以上且不下降,基本可以断定注意力没对齐,先停掉,回第 2.3 节把注意力图打出来看。另外,训练日志里如果出现nan,优先检查是不是学习率太高,或者输入序列里混了空文本,这是两个最常见的原因。因为 train.py 在历史版本里有细微改动,动手前先 grep 一遍 train.py,把 checkpoint 保存和加载的字段名确认好,能避免启动时踩到 KeyError。

4.3 GPU 训练的中断恢复与 checkpoint 管理

训练中段经常遇到断电、显存溢出被杀进程的情况,所以断点保存很重要。常见做法是给 train.py 加一个 --resume 参数,指定从哪个 checkpoint 恢复。如果你翻源码发现它没实现这个参数,可以直接在外层包一层脚本,每次启动前检查 checkpoint_dir 里最新的文件:

if ls checkpoints/*.pth > /dev/null 2>&1; then LATEST=$(ls -t checkpoints/*.pth | head -1) python train.py --data_root=./data/bznsyp \ --checkpoint_dir=./checkpoints/ \ --resume="$LATEST" else python train.py --data_root=./data/bznsyp \ --checkpoint_dir=./checkpoints/ fi

逻辑很简单:有 checkpoint 就从最新断点继续,没有才从头训练。注意恢复训练时,batch_size 和 reduction_factor 必须和保存 checkpoint 时保持一致,不然模型结构对不上,加载会直接报错。我个人的习惯是每次恢复前先看一眼 checkpoint 文件名里的 step 数,确认它比日志里最后打印的 step 新,否则你会把模型练回几天前的状态,白跑一大段。

5. 避坑指南:中文TTS训练最常见的五个翻车现场

5.1 注意力不收敛:loss 不降、合成的语音像卡带

现象:训练到 20k 步,loss 仍在 1.2 上下抖动,合出来的音频每个字间隔不均匀,像卡带。

原因:这是中文 TTS 训练最典型的注意力不收敛。中文标注里如果混入了过长句子(比如超过 30 个字),解码器的注意力窗口容易跨行塌缩;另外学习率设置过大也会导致注意力矩阵的梯度震荡。

解决:两个办法一起用。一是把数据里超过 25 个字的句子过滤掉或截断,保持序列长度分布均匀;二是把学习率从 1e-3 降到 5e-4,并配合预热,头两千步用小学习率让注意力稳定。改完这两个再看注意力图,一般 15k 步内能看到明显的对角线。

5.2 合成音色“电音感”重,听起来像机器唱出来的

现象:模型 loss 已经收敛,但合成音频的每个字都带着明显的金属味和数码味。

原因:这个包默认的声码器大概率是 Griffin-Lim,它在相位重建上先天不足,相位误差会直接体现在频谱细节上,听起来就是“电音感”。这不是模型没训好,而是声码器把 mel 谱还原成波形时损失了太多信息。

解决:先确认输出 mel 谱的幅值归一化是否与训练时一致,不少翻车其实是推理时把 DB 值直接当线性谱喂给了 Griffin-Lim。如果归一化没问题,再考虑换更先进的声码器,比如 WaveGlow 或 HiFi-GAN,把训练好的 Tacotron 当作声学模型,mel 谱输出到声码器生成波形。这个替换不改变 Tacotron 本身,但对最终音质的提升是决定性的。

5.3 多音字读错:模型把“重庆”读成“时钟的钟”

现象:训练完测试固定句子,“重庆”被读成 zhong4 qing4,“音乐”被读成 yin1 le4。

原因:拼音序列只保留了音节加声调,多音字在所有语境里只能对应一个读音,训练语料里出现频率高的那个读音会把模型绑死。模型没有上下文语义通道,它只是记忆了当前字词组合的统计规律。

解决:如果语料量足够,在词级别建立词典,把多音字放到词组里再按词组选读音;或者在预处理阶段用词法分析器(比如 jieba 分词加多音字表)对文本做一轮消歧,把“重庆”固定映射到 chong2。一个取巧的办法是给常用多音字加人工纠错表,在预处理脚本里直接覆盖测试集里出现的高频词。

5.4 长句 OOM,batch_size 调小也救不回来

现象:训练 20 字以内的句子没问题,一旦 batch 里出现一个 40 字的长句,显存直接溢出报 CUDA out of memory。

原因:Tacotron 的显存消耗与序列长度近似平方级关系,因为注意力矩阵的尺寸是 decoder_steps × encoder_steps,长句子会瞬间把 batch 的显存抬高。你以为 batch_size=16 很保守,但其中一条长句的显存占用可能顶得上其他样本的总和。

解决:最稳的方案是动态 batch,按句长分组,把接近长度的句子放在同一个 batch 里做 padding;或者干脆在数据过滤时把超长句单独抽出来,训练主数据只用 5~25 字的样本,长句留给推理阶段处理。再有就是把 reduction_factor 从 2 提到 3,解码步数减少三分之一,显存占用会明显下降。

5.5 训练时 loss 出现 nan,整个模型权重被污染

现象:训练到某个 step 后,loss 突然变成 nan,再训练多少步都拉不回来,checkpoint 全部报废。

原因:绝大多数是浮点溢出。要么是输入音频里有一段静音或爆音,mel 谱做 log 之后出现无穷大;要么是学习率在衰减过程中被除零。

解决:先从数据侧排查,对每个音频算一次 mel 谱的最大值,把 DB 值超过 100 的样本挑出来删除或重新导出;再在训练循环里加一个保护:loss 是 nan 时跳过当前 batch 并临时把学习率降一半。另外养成习惯,训练五千步时手动存一个 checkpoint,这样即使 nan 污染了最近的断点,也有后悔药可以回退。

6. 合成推理与效果验证:synthesis.py实操和MOS评测的完整流程

6.1 synthesis.py 的用法与参数

synthesis.py 是独立的推理脚本,输入一段中文文本,调用训练好的模型输出 mel 谱,再用默认声码器恢复成 wav。基本用法:

python synthesis.py --checkpoint=checkpoints/checkpoint_200000.pth \ --text="今天天气不错,适合出去走一走。" \ --output=./output/test1.wav \ --hparams="reduction_factor=2"

checkpoint 参数指向训练得到的模型断点;text 直接传汉字文本,脚本内部会先走一遍汉字转拼音的流程,所以要确保 synthesis.py 里调用的转换函数和训练预处理用的是同一套;output 指定保存路径;hparams 用来覆盖配置,这里的 reduction_factor 必须和训练时一致,否则模型输入输出的帧尺寸对不上,推理会直接报错。

如果脚本里没有内置汉字转拼音,我一般会改造一下,把第 3.3 节的 text_to_pinyin_ids() 直接搬进来,让分析文本进、拼音 ID 出,保持链路一致。合成短文本通常问题不大;如果文本很长,注意先手动加标点,否则模型会莫名憋出停顿感很怪的声音。输出的音频建议用 spectrogram 图快速扫一遍有没有断帧,再上耳朵听。

6.2 不用只靠耳朵:用 MOS 和 A/B 对比做量化筛选

音质好不好,耳朵听是主观的,作为工程判断必须有量化依据。常用的是 MOS,请 5~10 人听同一批合成语音,按 1~5 打分,5 分是真人水平。我的评测流程是每组 20 条文本,覆盖短句、长句、数字、多音字和标点符号,评测者盲听打分,最后取平均。

评分描述
5听不出是合成,与真人无异
4有轻微合成痕迹,但不影响理解
3能听懂,有明显机械感
2部分字词听不清,需要看文本辅助
1完全无法理解,像噪声

我自己的标准是平均 3.5 分以上才值得继续优化,4 分以上才是可交付的水平。如果同时有两个 checkpoint 拿不准,A/B 盲选可以快速筛掉音色更差那版,比一个人反复听有效得多。从那以后我每次训练完都不会直接下结论“效果还行”,而是强制走一遍固定流程:用 20 条测试文本统一合成,请至少三个人盲听打分,再把分数记到训练日志里。这个习惯救了我很多次,有些模型听起来顺耳但评分只有 3.0,有些听起来一般评分反而到 4.2,耳朵很容易被局部音色骗过去。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进