Created 05/28/2020 at 04:11PM

对Expressive TTS的实验记录,从5月12日开始

方法概览

学界以GST-Tacotron/reference-encoder-Tacotron开始,逐渐涌现了很多基于深度学习方法关于Expressive/Style TTS的研究,其中GST-Tacotron可以进行style transfer和style control,reference-encoder-Tacotron可以进行style transfer,由于GST-Tacotron可以对style进行提取聚类,更具实用性,后续的研究主要基于GST-Tacotron,主要集中在改进GST-Tacotron中的reference encoder + style token layer的模式,衍生出了基于VAE的方法/基于flow的方法,或者是强调多个style的解耦,包括speaker信息和style的解耦、style之间的解耦,这些方法都有以下共性:

  1. 数据集在全局角度上是具有多个style的;
  2. 对语音的全局特征进行style control;

这样就存在以下问题:

  1. 对数据集的多样性要求较高;
  2. 对语音的可控性仍然较弱;

下面的三个idea主要就是要解决上述的问题,其中第一个idea试图去解决使用单一global style的数据集让model去建模style的强弱,第二,三个idea试图让model可以进行更加细粒度的style control。

Idea

分别构建Non-Expressive/Expressive数据

大致思路

  1. 构建expressive/non-expressive数据:expressive使用ground-truth数据,non-expressive使用DNN模型去生成,因为模型生成的数据存在over-smoothing的特性,在语调变化的方面不如ground-truth;
  2. 使用上述两种数据去训练一个模型,模型具有两个embedding,分别指代expressive和non-expressive,期望通过embedding向量空间的组合或者通过对expressive embedding乘以style-scale,以获得更expressive的样本。

实验进展

  1. 训练一个基于lstm的TTS模型,具有encoder和decoder,encoder output会经过length regulator进行expand,decoder不存在auto-regressive过程(不存在auto-regressive可能会让模型生成的数据更加smoothing),训练之后发现模型生成的数据语调表现比较好,没有实现over-smoothing的效果;
  2. 由1,推测可能是具有encoder的原因,所以直接丢弃encoder,在phone-embedding层直接expand输入decoder(第一个版本还有一个conv层才会输入decoder,后来也丢弃了conv层),训练之后发现模型合成的声音太差,语音上有明显的间断感;
  3. FastSpeech结构使用了Parallel的Transformer结构,这样可能会实现over-smoothing的效果,于是用FastSpeech对数据集进行训练,发现语调学习的仍然不错,没有实现over-smoothing的效果;
  4. 项目地址
  5. 改进:不去训练一个TTS模型,而去训练一个AutoEncoder,在bottleneck处加入一个正则项去做over-smoothing,项目地址,正则(L2范数)如下:,最后失败了,推测原因over-smoothing的效果很难使用一个简单的L2正则去刻画:
class SMAELoss(nn.Module):
    def __init__(self):
        super(SMAELoss, self).__init__()
        self.mse_loss = nn.MSELoss()
        self.l1_loss = nn.L1Loss()

    def forward(self, mel, encoder_output, mel_target):
        mel_target.requires_grad = False
        mel_loss = self.mse_loss(mel, mel_target)
        l2_loss = hp.alpha * torch.Tensor([torch.norm(encoder_output[i], p="fro")
                                           for i in range(encoder_output.size(0))])
        l2_loss = torch.sum(l2_loss) / encoder_output.size(0)

        return mel_loss, l2_loss

思考

  1. 这些模型没有实现over-smoothing的效果推测可能是数据集规模较小,并且多样性较低,但如果使用大规模多样性数据集,那么这个idea试图解决的问题也不存在了;
  2. (待定)over-smoothing的数据在信息总量上会少于ground-truth(因为模型生成出来的数据必然会导致信息损耗),这样就有以下推论non-expressive -> expressive,模型要学到更多的信息,那么用调节embedding指向的向量空间能否让模型生成的数据比ground truth更加的expressive?因为如果这样可以做到的话,模型生成的数据信息反而会比ground truth更多。

可变长style embedding

Motivation

语音更加的Expressive,可能包含着对某些字词的重读/强调,或者是用不同的语调说同一句话,语音的发出者为了让同一句话具有更加丰富的含义所以才用不同的方式去说同一句话,所以Expressive可能更像是一种local style,而不是一种global Style。如果可以去建模出同一个音素的不同style,就可以对生成语音进行更加细粒度的控制,从而合成出更加expressive的语音,以往的方法主要集中在对语音的global style进行建模,一方面对语音数据集的多样性有要求,另一方面对语音的控制能力也较弱。

对帧级别的style进行建模

  1. 模型结构(decode时加入PPGdecoder时不加入PPG,并加入Discriminator):
# Main Structrue
class TTSPPG(nn.Module):
    def __init__(self,
                 num_phn=hp.num_phn,
                 encoder_dim=hp.encoder_dim,
                 encoder_n_layer=hp.encoder_n_layer,
                 kernel_size=hp.kernel_size,
                 stride=hp.stride,
                 padding=hp.padding,
                 decoder_dim=hp.decoder_dim,
                 decoder_n_layer=hp.decoder_n_layer,
                 output_dim=hp.num_mels,
                 dropout=hp.dropout):
        super(TTSPPG, self).__init__()

        self.embedding = nn.Embedding(num_phn,
                                      encoder_dim)
        self.conv = modules.BatchNormConv1d(encoder_dim,
                                            encoder_dim,
                                            kernel_size,
                                            stride,
                                            padding)
        self.encoder = nn.LSTM(encoder_dim,
                               encoder_dim // 2,
                               encoder_n_layer,
                               batch_first=True,
                               bidirectional=True,
                               dropout=dropout)
        self.length_regulator = modules.LengthRegulator()

        self.style_decoder = lst.LST()

        # self.pre_decoder = nn.LSTM(decoder_dim,
        #                            decoder_dim,
        #                            decoder_n_layer,
        #                            batch_first=True,
        #                            bidirectional=False,
        #                            dropout=dropout)
        # self.ppg_linear = modules.Linear(decoder_dim, hp.ppg_dim)

        self.post_decoder = nn.LSTM(decoder_dim+hp.style_dim,
                                    decoder_dim,
                                    decoder_n_layer,
                                    batch_first=True,
                                    bidirectional=False,
                                    dropout=dropout)
        self.mel_linear = modules.Linear(decoder_dim, output_dim)

        self.postnet = modules.CBHG(hp.num_mels, K=8,
                                    projections=[256, hp.num_mels])
        self.last_linear = nn.Linear(hp.num_mels*2, hp.num_mels)

# Local Style Token Layer
class LST(nn.Module):
    """ Local Style Token
    第一次做的实验将decoder分为两个阶段,中间步解码出PPG,
    后来实验发现,PPG的加入对解耦context和style信息没有帮助;

    LST模块流程图:
      (Mel Spectrogram) ->
      [LSTM] ->
      (Encoder Output) ->
      [Style Predictor] ->
      (Style Probability) * [Style Embedding (Random Initialize; Learnable)] ->
      (Style Embedding: Frame Series)
    """

    def __init__(self):
        super(LST, self).__init__()
        self.encoder_dim = hp.encoder_dim
        self.encoder_n_layer = hp.encoder_n_layer // 2

        self.style_embedding = nn.Parameter(
            torch.FloatTensor(hp.token_num, hp.style_dim))
        nn.init.normal_(self.style_embedding, mean=0, std=0.5)

        self.conv = modules.BatchNormConv1d(hp.num_mels,
                                            self.encoder_dim,
                                            hp.kernel_size,
                                            hp.stride,
                                            hp.padding)
        self.encoder = nn.LSTM(self.encoder_dim,
                               self.encoder_dim // 2,
                               self.encoder_n_layer,
                               batch_first=True,
                               bidirectional=True,
                               dropout=hp.dropout)
        self.linear_1 = nn.Linear(self.encoder_dim, self.encoder_dim)
        self.linear_2 = nn.Linear(self.encoder_dim, hp.token_num)
        self.relu = nn.RReLU()
        self.softmax = nn.Softmax(dim=-1)

    def forward(self, mel, mel_pos):
        encoder_input = self.conv(mel.contiguous().transpose(
            1, 2)).contiguous().transpose(1, 2)

        # ...
        encoder_output, _ = self.encoder(encoder_input)
        # ...

        linear_output = self.relu(self.linear_1(encoder_output))

        linear_output = self.softmax(self.linear_2(linear_output))

        lst_output = torch.matmul(linear_output, self.style_embedding)

        return lst_output
  1. 存在问题:文本信息严重的泄露,因为从LST(Local Style Token)到decoder相当于一个AutoEncoder,模型更倾向于学习到自映射而不是phone->mel的映射(即便decoder解码过程使用PPG约束也不行,推测是因为PPG信息相对较少)
  2. 改进思路:要让Style_Embedding-Frame_Series(简称SEFS)不具有文本信息,也就是说从SEFS不能生成Mel Spectrogram,考虑对抗学习,训练一个Discriminator,input为SEFS,该Discriminator目标是由SEFS生成Mel Spectrogram,优化器的目标是让Discriminator生成的Mel Spectrogram越来越好,另外,主体模型的优化目标是要让Discriminator学的越来越差,也就是让SEFS包含越来越少的文本信息,通过设置上述的Discriminator,SEFS和文本信息实现了分离,信息泄露的问题得到了缓解
  3. 存在问题:虽然上面的对抗学习解决了信息泄漏的问题,但是模型并没有像预想的那样解耦出不同的style,
  4. 反思:推测原因是上面motivation主要关注的音素级别的style,但是对于帧级别上,没有实际意义的style,模型也就很难去学到style。

对音素级别的style进行建模

  1. 模型结构(项目地址):LST输入Mel Spectrogram,输出phone级别的style,和encoder output concatenate在一起输入length regulator,模型最大的不同是LST模块,这里最大的问题是输入(Mel Spectrogram)和输出(phone级别)长度不一致,这一块的模型设计我暂时没有很好的想法,下面是暂时的LST设计:
class LST(nn.Module):
    """ Local Style Token
    (Mel Spectrogram) ->
    [LSTM] ->
    (Encoder Output) ->
    [Pooling layer: 根据duration的信息寻找mel和phone的对齐,将一个phone对应的encoder output做平均池化] ->
    (Pooling Output) ->
    [Conv] ->
    (Style Probability) * [Style Embedding (Random Initialize; Learnable)] ->
    (Style Embedding: Phone Series)
    """

    def __init__(self):
        super(LST, self).__init__()
        self.encoder_dim = hp.encoder_dim
        self.encoder_n_layer = hp.encoder_n_layer // 2

        self.style_embedding = nn.Parameter(
            torch.FloatTensor(hp.token_num, hp.style_dim))
        nn.init.normal_(self.style_embedding, mean=0, std=0.5)

        self.conv = modules.BatchNormConv1d(hp.num_mels,
                                            self.encoder_dim,
                                            hp.kernel_size,
                                            hp.stride,
                                            hp.padding)
        self.encoder = nn.LSTM(self.encoder_dim,
                               self.encoder_dim // 2,
                               self.encoder_n_layer,
                               batch_first=True,
                               bidirectional=True,
                               dropout=hp.dropout)

        self.conv_post = modules.BatchNormConv1d(self.encoder_dim,
                                                 self.encoder_dim,
                                                 hp.kernel_size,
                                                 hp.stride,
                                                 hp.padding)
        self.linear_1 = nn.Linear(self.encoder_dim, self.encoder_dim)
        self.linear_2 = nn.Linear(self.encoder_dim, hp.token_num)
        self.relu = nn.ReLU()
        self.softmax = nn.Softmax(dim=-1)

    def pooling_core(self, encoder_output, duration, src_pos):
        result = list()
        for batch_ind in range(src_pos.size(0)):
            cur_index = 0
            sq_len = torch.max(src_pos[batch_ind]).item()
            one_batch = list()
            for i in range(sq_len):
                cut_len = duration[batch_ind][i]
                if cut_len == 0:
                    one_batch.append(one_batch[-1])
                else:
                    one_batch.append(torch.sum(
                        encoder_output[batch_ind][cur_index:cur_index+cut_len], 0)/cut_len.float())
                cur_index += duration[batch_ind][i]
            one_batch = torch.stack(one_batch)
            result.append(one_batch)
        output = utils.pad(result, mel_max_length=src_pos.size(-1))
        return output

    def forward(self, mel, mel_pos, duration, src_pos):
        encoder_input = self.conv(mel.contiguous().transpose(
            1, 2)).contiguous().transpose(1, 2)

        input_lengths = torch.max(mel_pos, -1)[0].cpu().numpy()
        index_arr = np.argsort(-input_lengths)
        input_lengths = input_lengths[index_arr]
        sorted_encoder_input = list()
        for ind in index_arr:
            sorted_encoder_input.append(encoder_input[ind])
        encoder_input = torch.stack(sorted_encoder_input)

        encoder_input = nn.utils.rnn.pack_padded_sequence(encoder_input,
                                                          input_lengths,
                                                          batch_first=True)
        self.encoder.flatten_parameters()
        encoder_output, _ = self.encoder(encoder_input)
        encoder_output, _ = nn.utils.rnn.pad_packed_sequence(encoder_output,
                                                             batch_first=True)
        origin_encoder_output = [0 for _ in range(encoder_output.size(0))]
        for i, ind in enumerate(index_arr):
            origin_encoder_output[ind] = encoder_output[i]
        encoder_output = torch.stack(origin_encoder_output)

        pooling_output = self.pooling_core(encoder_output, duration, src_pos)
        pooling_output = self.conv_post(
            pooling_output.contiguous().transpose(1, 2)).contiguous().transpose(1, 2)

        linear_output = self.relu(self.linear_1(pooling_output))
        linear_output = self.softmax(self.linear_2(linear_output))

        lst_output = torch.matmul(linear_output, self.style_embedding)
        return lst_output, encoder_output
  1. 存在问题:模型代码可能有bug,正在持续排查中,此外对目前LST模块的设计方法不满意,input(Mel Spectrogram)和output(Style Embedding)长度不一致的问题没有被解决。

基础模型

目前实验主要依据LSTM和Transformer模型,LSTM训练速度较慢但是音质较好,一张卡就可以实验,全并行化的Transformer模型训练速度较快但是相同step下音质比不上LSTM,此外占用显存较多,需要多卡并行训练。