GitHub项目推荐:Tacotron2实现语音合成的Python版本
作者:禅与计算机程序设计艺术
1.简介
Tacotron2 (Text-To-Speech),是 Google 的开源语音合成神经网络模型,由两部分组成:编码器(Encoder)和转换器(Attention decoder)。它的主要特点就是生成语音波形的同时输出文字描述,这种模型比较适合生成长文本的音频文件,比如电子书、新闻等。
本文将使用 Python 的 TensorFlow 和 PyTorch 框架对 Tacotron2 模型进行实践并展示如何使用 Python 实现基于 Tacotron2 的语音合成。为了便于阅读,文章将详细阐述相关知识背景及其发展历程,并给出了许多参考文献。
2.背景介绍
什么是语音合成?
语音合成(Text-to-speech,TTS)是通过计算机将文字转化为人类可以识别和理解的声音信号的一项技术。它是用计算机生成的高质量人机对话语音的关键技术之一。
目前市面上常用的语音合成工具有有
- 专门用于制作和编辑语音的软件,如 Windows 的 Windows SAPI、Mac OS X 的 VoiceOver 或 Linux 的 Speech Dispatcher;
- 在线服务,如 Google 的 Cloud Text-to-Speech、Amazon 的 Polly、百度的 AiTalk、腾讯的 TTS 等。
除此之外,还有一些特殊领域的应用,例如自动驾驶汽车、虚拟现实、语言翻译、导航系统等。这些都需要自动生成大量的语音数据。因此,语音合成技术是一个十分重要的研究方向。
语音合成的分类
-
生成模型
-
Autoregressive model
-
WaveNet
是一种自动回归的循环神经网络模型,主要用于生成连续的音频信号,是语音合成领域的标准模型。WaveNet 的一个显著特点是能够快速生成高质量的语音,同时也易于训练和实现。
-
PixelCNN
使用 CNN 对图像像素信息进行建模,以此来生成音频信号。由于采用卷积神经网络的自回归特性,PixelCNN 的生成速度快,且语音生成质量高。但缺点在于存在梯度消失或爆炸的问题,在长文本生成中容易出现困难。
-
-
Non-autoregressive model
-
Sequence-to-sequence models
是指通过学习序列到序列的映射关系来生成序列。传统的 Seq2Seq 模型中采用 RNNs 作为编码器,再用另一个 RNNs 进行解码。这种方式只能利用输入序列中的一小段信息,无法捕获整个序列的信息。Seq2Seq 模型的一个改进方法是使用注意力机制。如 Transformer、Tacotron2 等。
-
Attention-based model
以多头注意力机制为代表,该模型不仅考虑输入序列信息,还考虑了历史目标状态。即每一步生成的时候,会根据历史状态、当前位置及当前输入条件生成目标状态。这样就可以捕获到上下文信息。
注意力模型有一个显著的优势就是可以处理长文本的生成。相比于传统的 Seq2Seq 模型,只要对齐正确,注意力模型就能准确捕捉各个词语之间的关系。
-
-
Hybrid model
混合模型是通过结合不同模型的优点而设计的。如 Seq2Seq+Attention 模型,先使用 Seq2Seq 模型来学习文本到音频的映射关系,然后再加上 Attention 模型来获得更丰富的上下文信息。
-
-
预训练模型
所谓的预训练模型,就是把大量的语料训练好的模型作为起始点,再根据自己的任务进行微调。这也是当前语音合成的主流方式。
-
Supervised learning
通过标注的语料库进行预训练。如普通话发音模型、英文单词发音模型、中文汉字拼音模型等。
-
Unsupervised learning
无监督预训练是指不需要标注数据的前向传播过程。一般使用语言模型(LM)来解决这一问题,LM 首先从文本中抽取一定的概率分布,如概率 p(x),然后根据概率分布采样得到下一个字符 x‘,接着依次计算每个字符的概率分布。这样可以使得生成的文本具有更大的多样性。
由于 LM 本身就是无监督学习,所以一般情况下都有足够的数据进行训练。但是,有一些特定任务的 LM 需要更多的训练数据才能达到很好的效果。例如,手写数字识别需要训练一个 LM 来生成手写数字。
-
-
评价指标
有两种常见的评价指标,即均方根误差(RMSE)和平均绝对欧氏距离(MAE)。它们衡量的是两个向量集合之间的差异。
- RMSE
均方根误差用来衡量生成的语音波形和真实语音之间的差异。它通常被认为是一个相对较好的评价指标。
- MAE
平均绝对欧氏距离用来衡量生成的文本和真实文本之间的差异。它可以用来评估模型是否产生了错误的文本,并且它还可以反映模型的文字生成能力。
3.基本概念术语说明
音素(phoneme)和韵律词(word boundary)
在计算机辅助语音合成中,一个音素是一个音节的最小单位,如英文中的 ‘t’,汉字中的 ‘琦’。
在现代汉语中,一个词的音节可能由多个音素组成,如汉字“花”可以拆分为音素“花”、“火”、“光”,在不同的语境下其读音也不同。由于语言中的音节数量众多,而且很多音节具有层次结构,因此语言的发音和语法结构复杂性也很高。
韵律词(word boundary)是指一个词内部的断句符号或分隔符,例如汉语中的逗号、句号、顿号。它可以帮助模型更好地生成连贯的语句。
Mel Spectrogram
Mel 频率倒谱系数(Mel-frequency cepstrum coefficients)是一种常见的特征提取方式。它的提出源自人类语音的共振峰特性,可以有效地抹平语音的频谱信息,并保留语音的时变特性。
Mel Spectrogram 可以视为频谱图上的离散版,它能更方便地分析语音的结构性信息。它将音频的频率轴替换为 Mel 频率轴,通过 Mel Filter Bank 将原始信号分解为 Mel 频率系数,并按时间顺序排列,构成 Mel Spectrogram。
Teacher Forcing
Teacher Forcing 是一种强化学习的技术,它可以让模型在训练过程中仔细借鉴教师的意图来选择下一步的动作。
在 Tacotron2 中,每一次预测都会带来一个教师 forcing,即使用真实的下一帧音频作为输入,而不是预测的输出。这样可以充分利用模型学习到的知识,避免直接简单复制前面的输出。
4.核心算法原理和具体操作步骤以及数学公式讲解
实现原理
Tacotron2 模型
Tacotron2 模型由编码器(Encoder)和转换器(Attention decoder)两部分组成。
Encoder
Encoder 是一个卷积神经网络(CNN),它将输入的音频信号(通常是批量的短音频片段)转换为固定维度的特征向量,这个向量表示输入音频的整体特征。
它主要由两部分组成:
-
Prenet
用作网络初始化。Prenet 是一个密集连接网络,它接受音频特征作为输入,经过一系列 Dense 层和激活函数后,输出一个与输入相同大小的向量。这是因为卷积神经网络的权重共享导致信息的丢失,所以添加一个额外的层可以一定程度上缓解这一问题。
-
CBHG Module
CBHG Module 是 Convolutional Bank + Highway Network(卷积bank + 门控神经网络)的缩写。它首先将输入音频分解为几个通道,然后通过一系列卷积核进行处理。卷积核的数量可以通过堆叠的卷积层来调整。然后将多个通道的特征叠加起来得到最终的特征向量。
最后,Highway Network 是一个多层感知器(MLP),它接受前面的特征向量作为输入,并且使用一个门控单元来控制输出。门控单元的计算公式如下:
H = s i g m o i d ( W x ) ∗ i n p u t f e a t u r e + ( 1 − s i g m o i d ( W x ) ) ∗ H p r e v H = sigmoid(Wx) * input_feature + (1 - sigmoid(Wx)) * H_prev H=sigmoid(Wx)∗inputfeature+(1−sigmoid(Wx))∗Hprev
其中 W 为可训练参数,sigmoid 函数作为激活函数。这样做的目的是为了引入非线性因素,提升模型的表达能力。
Decoder with Attention Mechanism
转换器(Attention decoder)的核心模块是 Multi-head attention。它允许模型关注输入序列不同位置上相同或相关的元素,并根据注意力权重进行聚合,从而生成相应的输出。
Multi-head attention 提供了一种有效的方式来学习不同上下文信息之间的关联,可以帮助模型捕捉到长期依赖关系。具体来说,它分割输入音频为几个步骤(时间步),并将每个步骤的特征向量传递给一个 head。每个 head 中的计算公式如下:
s c o r e = d o t ( q u e r y , k e y ) / s q r t ( d i m k ) w e i g h t = s o f t m a x ( s c o r e ) o u t p u t = w e i g h t ∗ v a l u e score = dot(query, key) / sqrt(dim_k) \\ weight = softmax(score)\\ output = weight * value score=dot(query,key)/sqrt(dimk)weight=softmax(score)output=weight∗value
其中 query、key、value 分别表示输入特征,query 表示查询向量,key/value 表示键值向量。softmax 函数用来计算注意力权重,注意力权重随着时间步的增加而降低,这样可以消除长期依赖影响。
Decoder 输出的计算公式如下:
decoder_input = concat(context vector, token)
decoder_hidden = tanh(FC(concat(decoder_input, encoder_outputs)))
attention_weights = []
for i in range(num_steps):context_vector, alignment_weights = self._compute_attention(decoder_hidden, encoder_outputs)decoder_hidden, decoder_output = self._predict_next_frame(decoder_hidden, context_vector)attention_weights.append(alignment_weights)
context_vector 表示 decoder 在当前时间步的注意力上下文向量,alignment_weights 表示注意力权重。之后的 decoder_hidden 和 decoder_output 分别表示第 i 个时间步的隐藏状态和输出,可以用来生成下一个音素的概率分布。
实现流程
数据准备
在实现 Tacotron2 时,首先需要准备好训练数据。训练数据包括音频和对应的文本,它可以通过两种方式获取:
-
手动录入音频和对应的文本。
-
通过自动语音识别(ASR)系统自动生成音频和对应的文本。
这里假设已经获取到了训练数据。训练数据应当满足以下条件:
-
音频格式:WAV。
-
音频采样率:16 kHz。
-
每条语音的长度:约等于 20 秒。
-
文本长度:约为 100 个字符。
-
发音人:一名男性发音人。
安装环境
安装所需的依赖库:
- tensorflow==1.14.0
- torch==1.2.0
- librosa>=0.7.0
- pyworld>=0.2.13
- tensorboardX>=1.8
配置参数
Tacotron2 的超参数很多,配置时应当注意灵活性。可以先使用默认的参数进行尝试,确认结果没有问题后,再按照自己的需求更改参数。
import numpy as np
import os
import time
from hparams import hparams
import tensorflow as tf
import torch
from torch import nn
from train import graph_creator
import utilos.environ["CUDA_VISIBLE_DEVICES"]="0"
if not os.path.exists("logs"):os.mkdir("logs")
logdir = "logs/" + str(int(time.time()))
writer = tf.summary.FileWriter(logdir)
checkpoint_path = "model/tacotron2_model.ckpt"
data_path = "/home/xxx/data/train_data.txt"
text_cleaners = ["english_cleaners"]hparams.set_hparam('batch_size', 32) # Batch size for training and evaluation
hparams.set_hparam('epochs', 100000) # Number of epochs to train
hparams.set_hparam('learning_rate', 1e-3) # Learning rate
hparams.set_hparam('decay_steps', 5000) # Decay steps for lr decay
hparams.set_hparam('decay_rate', 0.9999) # Rate of lr decay
hparams.set_hparam('dropout', 0.5) # Dropout probability
hparams.set_hparam('zoneout', 0.1) # Zoneout probability for all LSTM cells except the last one
hparams.set_hparam('n_mel_channels', 80) # Number of channels in mel spectrogram feature map
hparams.set_hparam('hop_length', 256) # Length of hop between STFT windows
hparams.set_hparam('win_length', 1024) # Window length for STFT analysis
hparams.set_hparam('sample_rate', 16000) # Sample rate of the audio signal
hparams.set_hparam('frame_shift_ms', None) # Amount to shift the sampling grid in ms
hparams.set_hparam('fmin', 40.) # Minimum frequency in Hz for Mel filter bank
hparams.set_hparam('fmax', 8000.) # Maximum frequency in Hz for Mel filter bank
hparams.set_hparam('bits', 9) # Bits per character
hparams.set_hparam('symbols_embedding_dim', 512) # Dimensions of embedding space for symbols (characters or phonemes)
hparams.set_hparam('encoder_layers', 3) # Number of layers used in the encoder part of the network
hparams.set_hparam('encoder_conv_filters', 32) # Number of filters used for convolution in the encoder part of the network
hparams.set_hparam('encoder_conv_kernel_sizes', [5, 5]) # Size of kernel used for each convolution layer in the encoder part of the network
hparams.set_hparam('encoder_conv_activation','relu') # Activation function used in the encoder part of the network
hparams.set_hparam('encoder_lstm_units', 256) # Number of units used for LSTM layer in the encoder part of the network
hparams.set_hparam('attention_type', 'bah_mon') # Type of attention mechanism to use
hparams.set_hparam('attention_dim', 128) # Dimension of attention layer
hparams.set_hparam('attention_filters', 32) # Number of filters used for convolution in the attention part of the network
hparams.set_hparam('attention_kernel_size', [31]) # Size of kernel used for convolution in the attention part of the network
hparams.set_hparam('decoder_layers', 2) # Number of layers used in the decoder part of the network
hparams.set_hparam('decoder_lstm_units', 1024) # Number of units used for LSTM layer in the decoder part of the network
hparams.set_hparam('prenet_layers', [256, 128]) # Number of layers and number of units used for prenet in decoder part of the network
hparams.set_hparam('max_iters', 1000) # Max iterations for training data
hparams.set_hparam('stop_threshold', 0.5) # Threshold for stopping training based on convergence of loss values
hparams.set_hparam('use_saved_learning_rate', False) # Whether to load saved learning rate or reset it to default
hparams.set_hparam('save_step_interval', 1000) # Interval at which to save checkpoints
hparams.set_hparam('eval_interval', 5000) # Interval at which to run eval step during training
hparams.set_hparam('restore_from', '') # If provided, restore from checkpoint path
训练模型
tf.reset_default_graph()
sess = tf.Session()
inputs, outputs = graph_creator.create_model(hparams)
optimizer, global_step = graph_creator.create_optimizer(inputs, hparams)
sess.run(tf.global_variables_initializer())
loss_metric = tf.metrics.mean_squared_error(predictions=outputs[1], labels=inputs[1])
saver = tf.train.Saver()
try:ckpt_state = tf.train.get_checkpoint_state(checkpoint_path)if ckpt_state is not None:tf.logging.info('Loading checkpoint {}'.format(ckpt_state.model_checkpoint_path))saver.restore(sess, ckpt_state.model_checkpoint_path)
except ValueError:pass
start_epoch = sess.run([global_step])[0] // len(util.load_filepaths(data_path))
print("Start Epoch:", start_epoch + 1)for epoch in range(start_epoch, hparams.epochs):start = time.time()iter_data = util.iter_data(hparams, inputs, outputs, data_path, text_cleaners, shuffle=True)running_loss = 0.0batch_count = 0for x, y in iter_data():_, current_step = sess.run([optimizer, global_step], feed_dict={inputs[0]: x['sequences'],inputs[1]: y,inputs[2]: x['mel_specs'][:, :, :hparams.n_frames_per_step*2],inputs[3]: np.array([len(seq) for seq in x['sequences']])})running_loss += sess.run(loss_metric)[0]batch_count += 1writer.add_summary(sess.run(merged_summaries,{loss_placeholder: running_loss/(batch_count + 1)}),global_step=current_step)if current_step % hparams.save_step_interval == 0:saver.save(sess, checkpoint_path, global_step=current_step)if current_step >= hparams.max_iters:breakprint('Epoch {} Loss {:.4f}'.format(epoch + 1, running_loss / batch_count))print('Time taken for 1 epoch {} sec\n'.format(time.time() - start))sess.run(lr_scheduler.step(), feed_dict={lr_placeholder: new_lr})
测试模型
def synthesize(text, speaker_id='speaker_1'):sequence = np.asarray(text_to_sequence(text, ['english_cleaners']))[None, :]sequence = np.pad(sequence, ((0, hparams.max_len - sequence.shape[1]), (0, 0)), mode='constant', constant_values=0)mel_spec = graph_creator.synthesize(hparams, speaker_id, sequence)return postprocess_spectrogram(mel_spec.T).astype(np.float32)
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
