本文主要讲解了端到端的语音合成模型VITS论文及项目实现~
论文题目:2021_VITS: Conditional Variational Autoencoder with Adversarial Learning for End-to-End Text-to-Speech
Paper:Conditional Variational Autoencoder with Adversarial Learning for End-to-End Text-to-Speech (arxiv.org)
Code:jaywalnut310/vits: VITS: Conditional Variational Autoencoder with Adversarial Learning for End-to-End Text-to-Speech (github.com)
论文总结
提出一种TTS模型框架VITS,用到normalizing flow和对抗训练方法,提高合成语音自然度,其中论文结果上显示已经和GT相当。是结合了VAE和FLOW的新架构。
编辑
在俩各数据集中的实验结果
编辑
论文的主要贡献:
首个自然度超过2-stage架构SOTA的完全E2E模型。MOS4.43, 仅低于GT录音0.03。
得益于图像领域中把Flow引入VAE提升生成效果的研究,成功把Flow-VAE应用到了完全E2E的TTS任务中。
训练非常简便,完全E2E。不需要像Fastspeech系列模型需要额外提pitch, energy等特征,也不像多数2-stage架构需要根据声学模型的输出来finetune声码器以达到最佳效果。
摆脱了预设的声学谱作为链接声学模型和声码器的特征,成功的应用来VAE去E2E的学习隐性表示来链接两个模块。
多人模型自然度不下降,不像其他模型趋于持平GT录音MOS分。
本文只详细翻译论文的第二部分,讲解模型的实现细节(编号与论文相同)。
2. 方法论Method
在这节中,论文解释了论文提出的方法以及构架,建议的方法主要在前三个小节:条件 VAE 表述(conditional VAE formulation);由变异推理得出的配准估计(alignment estimaion derived from variational inference);提高合成质量的对抗训练(adversarial estimation for improving synthesis quality)。整体架构将在本节末尾介绍。图 1a 和 1b 分别显示了我们方法的训练和推理过程。将方法称为带有对抗性学习的端到端文本到语音(VITS)。
2.1.可变推理
VITS 可以表示为条件 VAE,其目标是最大化可变下界,也称为证据下界 (ELBO)。其目标是最大化难以处理的数据的边际对数似然 log pθ(x|c):
编辑
其中,pθ(z|c) 表示给定条件 c 的潜变量 z 的先验分布,pθ(x|z) 是数据点 x 的似然函数,qφ(z|x) 是近似后验分布。那么训练损失就是负ELBO,可以看作是重建损失 log pθ(x|z) 和 KL 发散 log qφ(z|x) 的总和,其中 z ∼ qφ(z|x)。
2.1.2.损失
作为一个目标数据点,使用梅尔惠普图去替代音波文件,定义为Xmel.上采样潜变量z去求得y',通过一个解码器和转置y',
编辑
2.1.3.KL散度(KL-divergence)
编辑
2.2.Alignment Estimation
2.2.1.Monotonic Alignment Search(MAS)
为了估计输入文本和目标语音之间的对齐度 A,采用单调对齐搜索(Monotonic Alignment Search,MAS)(Kim 等人,2020 年)。数据参数最大化的对齐方法:
编辑
其中候选排列被限制为单调且不跳字,因为人类在阅读文本时人类在阅读文本时是按顺序排列的,不会跳过任何单词。为了找到最佳排列,Kim 等人(2020 年)使用了动态编程法。由于我们的目标是 ELBO,而不是精确的对数概率,因此很难在我们的环境中直接应用 MAS对数概率。因此,我们将 MAS 重新定义为寻找一个的对数似然最大化的对齐方式。潜变量 z 的对数概率最大的排列组合:
编辑
2.3.自适应训练
编辑
2.4.最终损失
将VAE和GAN训练进行融合,总损失如下编辑
2.5.模型框架
2.5.1.后置编码器
对于后置编码器,使用 WaveGlow(Prenger 等人,2019 年)中使用的非因果 WaveNet残差块用于 WaveGlow(Prenger 等人,2019 年)和 Glow-TTS(Kim 等人,2020 年)中使用的非因果 WaveNet 残差块。一个 WaveNet 残差块由多层扩张卷积和门控激活单元和跳接。块上方的线性投影层层产生正态分布的均值和方差。对于多扬声器情况、在残差块中使用全局调节(Oord 等人,2016 年)来添加说话者嵌入。
2.5.2.先验编码器
先验编码器由文本编码器和归一化流程 fθ 组成。输入音素 ctext 和一个归一化流 fθ 组成。提高先验分布的灵活性。文本编码器是一个变换器编码器(Vaswani 等人,2017 年),使用相对位置表示法(Shaw 等人,2018 年)而不是绝对位置编码。我们可以通过文本编码器和文本编码器上方的线性投影层,我们可以从 ctext 中获得隐藏表示 htext。产生用于构建先验分布的均值和方差。
归一化流程是一叠仿射耦合层(Dinh et al.堆叠的 WaveNet 剩余块。为简单起见,我们将
为简单起见,我们将归一化流设计为雅各布行列式为 1 的保容变换。在多人(multi-speaker)设置中,我们通过全局的方法将扬声器嵌入到归一化流中的残余块。
2.5.3.解码器
解码器本质上是 HiFi-GAN V1 生成器(Kong 等人,2020 年)。它由一叠转条件变异自动编码器与对抗学习用于端到端文本到语音技术.每个卷积之后都有一个多感知场融合模块(MRF)。多感知场融合模块的输出是具有不同感受野大小的残差块的输出之和。对于多人设置、添加了一个线性层,用于转换扬声器嵌入并将其添加到输入潜变量 z。
2.5.4.鉴别器
沿用 HiFi-GAN(Kong 等人,2020 年)中提出的多周期的判别器架构(Kong 等人,2020 年)。
多周期判别器是马尔可夫,基于窗口的子判别器的混合物(Kumar 等人,2019 年),每个子鉴别器都对输入波形的不同周期模式进行操作波形.
2.5.5.随机时长预测器
随机时长预测器根据条件输入 htext 估算音素时长的分布。为了对随机时长预测器进行有效的参数化,将残差块与扩张的、深度可分的卷积层进行堆叠。深度分离的卷积层堆叠残差块。还应用了神经样条流(neural spline flows)(Durkan 等人,2019 年),其形式为可逆非线性变换的形式。有理二次样条,对耦合层进行可逆非线性变换。神经样条流与常用的仿射流相比,以相似的参数数量与常用的仿射耦合层。对于多扬声器设置,我们添加了一个线性层,对扬声器嵌入进行变换,并将其添加到输入 htext 中。
2.论文项目实现
2.0.环境设置
git clone https://github.com/jaywalnut310/vits
cd vits
# 建议3.7
conda create -n vits python==3.7
install espeak
pip install -r requirements.txt
cd monotonic_align
mkdir monotonic_align
python setup.py build_ext --inplace
2.1.数据集准备
原论文中实验了俩个数据集
LJSpeech(单人,英语)
VCTK(多人,英语)
本文只实现多人语音合成,下载VCTK数据集【下载网址】
数据集1:LJSpeech-1.1
如果使用LJ语音数据集,下载并提取 LJ 语音数据集,然后重命名或创建指向数据集文件夹的链接:
ln -s /path/to/LJSpeech-1.1/wavs DUMMY1
数据集2:VCTK
点击下载所有文件【Download all files】后,(下载大约5h)
编辑
上传到服务器,放到项目路径下:自己新建的data,然后解压缩后如图
编辑
再将数据进行处理
# 方法 2 :把flac结尾的,替换为wav文件,然后对wav文件进行重采样(48000->22050),运行后保存为新的地址,所有文件都在同一文件夹下
import os
import librosa
import tqdm
import soundfile as sf
if __name__ == '__main__':
audioExt = 'flac'
outputExt = 'wav'
input_sample = 48000
output_sample = 22050
audioDirectory = ['/workspace/tts/vits/data/wav48_silence_trimmed']
outputDirectory = [ '/workspace/tts/vits/DUMMY2']
for i, dire in enumerate(audioDirectory):
# 寻找"directory"文件夹中,格式为“ext”的音频文件,返回值为绝对路径的列表类型
clean_speech_paths = librosa.util.find_files(
directory=dire,
ext=audioExt,
recurse=True,
)
for file in tqdm.tqdm(clean_speech_paths, desc='No.{} dataset resampling'.format(i)):
fileName = os.path.basename(file)
print(fileName)
if audioExt in fileName:
fileName=fileName.replace(audioExt,outputExt)
sr = librosa.load(file, sr=input_sample)
y_16k = librosa.resample(y, orig_sr=sr, target_sr=output_sample)
outputFileName = os.path.join(outputDirectory[i], fileName)
y_16k, output_sample)
将 wav 文件缩减采样至 22050 Hz。然后重命名或创建指向数据集文件夹:
ln -s /path/to/VCTK-Corpus/downsampled_wavs DUMMY2
在克隆的项目中,已经有对齐好的数据文件。
此时
编辑
需要将数据变为wav文件并采样到22050Hz
所需训练数据
编辑
数据处理后的,投入训练的数据
编辑
数据集3:自己的数据集
构建单调对齐搜索并运行预处理(如果使用自己的数据集)。
vits数据集需要指定的格式才能识别,如下图是多人训练中日混合模型的格式,本教程也是针对下载的数据集,如何整理成需要的格式,做一个简单入门教程。
数据预处理
# Cython-version Monotonoic Alignment Search
cd monotonic_align
mkdir monotonic_align
python setup.py build_ext --inplace
# Preprocessing (g2p) for your own datasets. Preprocessed phonemes for LJ Speech and VCTK have been already provided.
# python preprocess.py --text_index 1 --filelists filelists/ljs_audio_text_train_filelist.txt filelists/ljs_audio_text_val_filelist.txt filelists/ljs_audio_text_test_filelist.txt
# python preprocess.py --text_index 2 --filelists filelists/vctk_audio_sid_text_train_filelist.txt filelists/vctk_audio_sid_text_val_filelist.txt filelists/vctk_audio_sid_text_test_filelist.txt
编辑
2.2.训练
单人用train.py,多人用train_ms.py训练
# LJ Speech
python train.py -c configs/ljs_base.json -m ljs_base
# VCTK
python train_ms.py -c configs/vctk_base.json -m vctk_base
2.3.推理
自己稍微修改后的推理文件 inference.py
import matplotlib.pyplot as plt
import IPython.display as ipd
import os
import json
import math
import torch
from torch import nn
from torch.nn import functional as F
from torch.utils.data import DataLoader
import commons
import utils
from data_utils import TextAudioLoader, TextAudioCollate, TextAudioSpeakerLoader, TextAudioSpeakerCollate
from models import SynthesizerTrn
from text.symbols import symbols
from text import text_to_sequence
from scipy.io.wavfile import write
def get_text(text, hps):
text_norm = text_to_sequence(text, hps.data.text_cleaners)
if hps.data.add_blank:
text_norm = commons.intersperse(text_norm, 0)
text_norm = torch.LongTensor(text_norm)
return text_norm
####################### LJSpeech ###########################
hps = utils.get_hparams_from_file("./configs/ljs_base.json")
net_g = SynthesizerTrn(
len(symbols),
hps.data.filter_length // 2 + 1,
hps.train.segment_size // hps.data.hop_length,
**hps.model).cuda()
_ = net_g.eval()
stn_tst = get_text("VITS is Awesome!", hps)
with torch.no_grad():
x_tst = stn_tst.cuda().unsqueeze(0)
x_tst_lengths = torch.LongTensor([stn_tst.size(0)]).cuda()
audio = net_g.infer(x_tst, x_tst_lengths, noise_scale=.667, noise_scale_w=0.8, length_scale=1)[0][0,0].data.cpu().float().numpy()
ipd.display(ipd.Audio(audio, rate=hps.data.sampling_rate, normalize=False))
_ = utils.load_checkpoint("/path/to/pretrained_ljs.pth", net_g, None)
####################### VCTK ###########################
hps = utils.get_hparams_from_file("./configs/vctk_base.json")
net_g = SynthesizerTrn(
len(symbols),
hps.data.filter_length // 2 + 1,
hps.train.segment_size // hps.data.hop_length,
n_speakers=hps.data.n_speakers,
**hps.model).cuda()
_ = net_g.eval()
_ = utils.load_checkpoint("/path/to/pretrained_vctk.pth", net_g, None)
stn_tst = get_text("VITS is Awesome!", hps)
with torch.no_grad():
x_tst = stn_tst.cuda().unsqueeze(0)
x_tst_lengths = torch.LongTensor([stn_tst.size(0)]).cuda()
sid = torch.LongTensor([4]).cuda()
audio = net_g.infer(x_tst, x_tst_lengths, sid=sid, noise_scale=.667, noise_scale_w=0.8, length_scale=1)[0][0,0].data.cpu().float().numpy()
ipd.display(ipd.Audio(audio, rate=hps.data.sampling_rate, normalize=False))
####################### Voice Conversion ###########################
dataset = TextAudioSpeakerLoader(hps.data.validation_files, hps.data)
collate_fn = TextAudioSpeakerCollate()
loader = DataLoader(dataset, num_workers=8, shuffle=False,
batch_size=1, pin_memory=True,
drop_last=True, collate_fn=collate_fn)
data_list = list(loader)
with torch.no_grad():
x, x_lengths, spec, spec_lengths, y, y_lengths, sid_src = [x.cuda() for x in data_list[0]]
sid_tgt1 = torch.LongTensor([1]).cuda()
sid_tgt2 = torch.LongTensor([2]).cuda()
sid_tgt3 = torch.LongTensor([4]).cuda()
audio1 = net_g.voice_conversion(spec, spec_lengths, sid_src=sid_src, sid_tgt=sid_tgt1)[0][0,0].data.cpu().float().numpy()
audio2 = net_g.voice_conversion(spec, spec_lengths, sid_src=sid_src, sid_tgt=sid_tgt2)[0][0,0].data.cpu().float().numpy()
audio3 = net_g.voice_conversion(spec, spec_lengths, sid_src=sid_src, sid_tgt=sid_tgt3)[0][0,0].data.cpu().float().numpy()
print("Original SID: %d" % sid_src.item())
ipd.display(ipd.Audio(y[0].cpu().numpy(), rate=hps.data.sampling_rate, normalize=False))
print("Converted SID: %d" % sid_tgt1.item())
ipd.display(ipd.Audio(audio1, rate=hps.data.sampling_rate, normalize=False))
print("Converted SID: %d" % sid_tgt2.item())
ipd.display(ipd.Audio(audio2, rate=hps.data.sampling_rate, normalize=False))
print("Converted SID: %d" % sid_tgt3.item())
ipd.display(ipd.Audio(audio3, rate=hps.data.sampling_rate, normaliz
e=False))
如果处理自己的数据集:
# Cython-version Monotonoic Alignment Search
cd monotonic_align
python setup.py build_ext --inplace
# Preprocessing (g2p) for your own datasets. Preprocessed phonemes for LJ Speech and VCTK have been already provided.
# python preprocess.py --text_index 1 --filelists filelists/ljs_audio_text_train_filelist.txt filelists/ljs_audio_text_val_filelist.txt filelists/ljs_audio_text_test_filelist.txt
# python preprocess.py --text_index 2 --filelists filelists/vctk_audio_sid_text_train_filelist.txt filelists/vctk_audio_sid_text_val_filelist.txt filelists/vctk_audio_sid_text_test_filelist.txt
在本文中已经对数据集VCTK处理完了,如图
编辑
分别是语音路径,人员编号,预处理后的文本文件。
3.代码详解
VITS由于采用对抗训练的模式,模型主要包括生成器net_g
和判别器net_d
两大块,判别器仅在训练时使用。具体实现上,生成器net_g
由SynthesizerTrn
实现,包括先验编码器、随机时长预测器、解码器和后验编码器;判别器net_d
由MultiPeriodDiscriminator
实现,即HiFiGAN中的多周期判别器。
config.json
train训练部分
eval_interval为全权重保存间隔,这里按照默认的1000即可以满足保存的需求,设置过小会训练过程会耗费大量时间在保存上;设置过大如果训练出现问题无法满足及时保存最近的模型的需求。 epochs迭代次数,一般来说比较好的数据集质量不到一千合成也基本比较自然,作者建议一万,有时间还是建议一万。
data数据部分
是经过预处理的数据集文件
text_cleaners 是对文本的处理方法,中文chinese_cleaners,英文english_cleaners等等(处理方法不同,有声学和音素方法)
n_speakers说话人数,单人为0,多人按人物个数。
train_ms.py
VITS训练时,使用了混合精度训练,并且设置了对抗训练模式;其中判别器使用了多周期判别器,由多个子判别器组成,并且生成过程损失中还加上了feature_map损失。训练过程中,不是对完整的音频文件进行训练,而是提取一部分音频数据进行训练,进而在计算损失时,也要从ground truth中提取对应部分的数值进行计算。
losses.py
模型训练过程中涉及很多的损失,对抗训练过程中,判别器是常规的判别器损失结构,但是使用的是多周期判别器,由多个子判别器组成;生成器的损失,包括mel重建损失、KL散度、时长预测器损失、对抗训练生成损失以及特征图损失,其中时长预测器损失在模型forward函数中直接计算、mel重建损失是直接计算L1损失,剩下的四种损失在losses.py文件中定义,
问题与答案【Q&A】
【Q&A1】为什么推理时噪音(noise_scale)为0.667?
参考【4】,在TTS库中,推理时noise_scale= 0.667
且z_p = m_p + torch.randn_like(m_p) * torch.exp(logs_p) * self.inference_noise_scale
训练后,想要采样的分布可能与z_spec不完全匹配。此外,为了最大限度地提高高概率值的可能性,从较小的高斯开始缩放方差和样本,从而获得更好的经验抽样。
编辑
更多参数请参考【5】
再原网络中,推理时(训练时稍有不同)
noise_scale (float) - 用于推理中样本噪声张量的噪声标度。默认值为 0.667。
noise_scale_w(float) - 推理中随机持续时间预测器的噪声标度。默认值为 0.8。
length_scale (float) – 预测持续时间值的比例因子。值越小,语速越快。默认值为 1。
过程中遇到的错误及改正【PS】
【PS1】ModuleNotFoundError: No module named 'monotonic_align.monotonic_align'
编辑
解决方法
cd monotonic_align
mkdir monotonic_align
python setup.py build_ext --inplace
未找到解决方法前的错误记录
编辑
使用
python setup.py install
编辑
再次运行后,出现
error: could not create 'monotonic_align/core.cpython-38-x86_64-linux-gnu.so': No such file or directory
重新新建了一个环境
编辑
运行后出现
Segmentation fault (core dumped)
编辑
参考文献
【1】VITS 语音合成完全端到端TTS的里程碑_Terry_ZzZzZz的博客-CSDN博客
【2】细读经典:VITS,用于语音合成带有对抗学习的条件变分自编码器 - 知乎
【3】VITS 语音合成完全端到端TTS的里程碑_算法_Terry_ZzZzZz-兴智开发者社区 (csdn.net)
【4】 Why 0.667 during inference in vits? · coqui-ai/TTS · Discussion #3104 (github.com)
【5】VITS - TTS 0.21.3 documentation