我们之前介绍了Grad-TTS:Diffusion在语音合成中的端到端实现,现在来实践Grad-TTS 代码,边做边发现新问题和挑战。
代码与数据准备
官方代码在https://github.com/huawei-noah/Speech-Backbones/tree/main/Grad-TTS。可以用torchaudio.datasets.LJSPEECH()
和torchaudio.datasets.LIBRITTS()
下载训练所需的单人和多人语音数据。另外,训练的采样率22050Hz,但是Grad-TTS/data.py中用于加载数据的是torchaudio.load()
,这个函数会默认使用音频自己的采样率,可能会加载到24000Hz的样本,这样就无法通过assert sr == self.sample_rate
这一句,可以改成
if sr!=self.sample_rate:
audio = ta.transforms.Resample(orig_freq = sr, new_freq = self.sample_rate)(audio)
如果使用librosa.load()
,默认采样率就是22050,如果原始音频是 24000 Hz,加载后会通过插值算法调整到 22050 Hz,相当于上面的torchaudio.transforms.Resample()
这个操作了。
重采样后音高还是会和原始的保持一致,不过高频可能就丢了,cap会从12000Hz 降到11025 Hz。
训练过程
接下来看一下训练效果。文章追踪了3个损失函数,duration loss、prior loss 和diffusion loss,分别关注duration predictor、encoder和score function。
由于资源有限,2张V100上训练了一天,大约145个epochs。可以看到prior loss和duration loss一直在下降,而diffusion loss表现出一些震荡和挣扎。
这也是合乎情理的。duration loss 和 prior loss要处理的是相对结构化且维度较低的任务——对齐多简单,几个时间长度比较一下;潜在空间的概率也还好,稍微和目标y像一点,就方便对齐了,参数也少,它本身也不是一定要存在的。所以它们能更快找到优化方向,稳定收敛。diffusion loss就不一样了,它需要在时间步x频段这样高维的梅尔频谱空间里中,学习如何把纯噪声一点点精确重建成准确的梅尔频谱。这无疑是极具挑战性的,而且加了噪,收敛慢且易震荡也是正常的。
不过,即便数值上的diffusion loss只有微小且缓慢的下降,对模型的改观却是举足轻重的。也许它正在学习怎么捕捉梅尔频谱里对人类听觉感知超重要的细微结构,什么高频细节啊,谐波啊,这些数值损失的微小改善,往往能带来巨大的感知质量提升。反观duration loss和prior loss,这些loss的下降主要影响模型内部结构,是局部的改观,不会直接反映在最终的梅尔频谱上,梅尔频谱的纹理变化主要还是由diffusion loss驱动。我们可以从图中看到,对比第2087步和第33168步,prior生成的要送入U-NET的频谱没有太多变化,对齐方面也肉眼看不出什么优化,但是最终的梅尔频谱明显变得更精细,这直接上就是由diffusion loss的微小变化主导的。
通过这些中间过程图也能发现,encoder之后的梅尔图谱比较粗线条,而经过diffusion调整后就变得非常细粒度。
损失函数调整
这三个目标是直接相加的。duration loss是y和mu_y的MSE,prior loss是对数似然函数,diffusion loss也是一些平方和,所以它们三个在训练时可以差不多都在控制在1附近。不过这样稍许有些粗糙,因为diffusion loss的量级还是要更小一些,比如到后期基本dur、prior、diff就是0.5、1.5、0.1这样。如果diff权重变大,它会不会训练得更好呢?我将diff loss变成原来的5倍,发现开头的40个epoch左右收敛得更快了(后面还没有算,按照文中提到的170万次迭代,如果是多说话人版本,大约820个epoch),对于计算资源有较强约束的朋友们是一个好消息。
并且似乎从听感上来说,提升diff loss的权重也会加速收敛。我以Kendrick Lamar《N95》的一句歌词为例,“Take off the money phone; Take off the car loan; Take off the flex and the white lies”,对比最终模型、原版epoch40模型和调整后epoch40模型,可以发现调整后的元音基本成型了,原版还是没有把“money phone”说对。
wav效果一览
最终模型
原始模型epoch40
调整模型epoch40
在300多个epoch之后,原版的diff loss因为量级太小而被另两个loss主导,导致loss反弹,实际听感变差,连性别都变了。而修正后的loss持续下降。因为diff loss是最终极、最重要的,所以它的下降是模型最需要的。
推理效果
官方提供了很多消融实验的音频结果和最终模型的展示结果,仔细分辨可以听出是合成的但是非常流畅,也有语音的抑扬顿挫,极少情况下会出现电声等artifacts,语音质量和韵律自然度在同时期是有明显进步的。开头的十几个epoch有比较强的底噪且雌雄莫辨,直到近百个epoch后说话的音色逐渐正常。
由于是非自回归模型,推理速度也很快,如果推理迭代取10步的话,real-time factor(RTF)大约在0.18左右。10步已经能够得到比较清晰有质感的音频了。奇怪的是,尝试了40步、100步、1000步的迭代,10步是好于40步和100步的,也好于小于10步的效果。步数过少很好理解,此时去噪可能不够彻底。步数过多是出于何种原因?试了好几个音色和句子都是这样,不知道是巧合还是内部机制造成的?比如求解ODE时离散化误差可能在更多迭代中累积,降低了稳定性,而步长极小时又用每一步的精确性补偿了误差累积?
速度方面,提供了一个超参,可以通过梅尔频谱的抽帧和加帧来实现变速,试了一下1.5倍速和0.5倍速都还是比较丝滑的。
局限性与改进方向
经过代码实践和效果评估,我能想到2个可以改进的点
- 字典优化。Grad-TTS依赖于cmu-dict,每次文字进来都要用字典转换成音素才能输入,所以暂时没法处理其它语种、多音字、生僻字甚至方言发音的处理。是否能在编码器前再增加发音预测模块?(比如基于BERT的G2P模型或者更先进的模型)。
多语种的话,利用英语预训练模型初始化低资源语种的编码器,然后再慢慢增强,也让MAS算法支持不同语言的韵律对齐模式。
自定义音色。Grad-TTS的音色都是训练数据里的,一共274个人,只能用他们的编码来合成新的语音。最简单的就是自定义说话人编码,反正它进入U-NET也是自己独立一个channel。但是一般情况下人们也不知道输入的编码什么声音,所以更需要做的是音色迁移,这个就要参VALL-E或YourTTS来编码音色做迁移了,比如把情感标签、语调强度、音色高低这些风格标记独立编码学习,可以独立进入score function的通道,也可以作为扩散过程的噪声参数。
duration predictor 以音色为条件。看起来所有的说话人共用一个duration predictor,无论谁说话这个映射都是同一个。如果speaker embedding能作为duration predictor的一个输入,应该会更贴合每个说话人的个性。
同一个词组的变化。目前同一个单词或者词组会用同一个语调说出,例如刚才的歌词案例,所有的take off都是一样的调调,给人一种非真人感。是否这就是非自回归的缺点?