0%

PortaSpeech

摘要

PortaSpeech:Portable and High-Quality Generative Text-to-Speech

Paper

https://arxiv.org/abs/2109.15166
PortaSpeech

论文的出发点:

  • VAE模型擅长捕捉长范围之间的语义特征(如韵律),但是结果会 blurry (mel谱会模糊掉) 和 unnatural.
  • normallzing flow 能够较好的重构mel谱的细节,但是在模型参数量有限的情况下表现较差

论文的核心方法:

  • 轻量级VAE建模韵律,基于flow的后处理网络增强mel谱的细节表达
  • 为了压缩模型大小,将基于flow的后处理网络中的affine coupling layers引入了分组参数共享机制
  • 为了提高TTS的表达能力,将原来音素级别的hard-alignment改变为词级别的hard-alignment,词级别到音素级别利用attention进行soft-alignment.

这里利用Praat看一下音素级别hard-alignment的问题:
phoneme
利用词级别能改善,但是也会存在问题:
word
利用句子级别可能会更好,但是存在鲁棒性降低的风险。

代码详解

https://github.com/NATSpeech/NATSpeech该代码使用 pytorch框架

Text Encoder 模块

  1. phoneme Encoder(FFT blocks), 将输入的音素编码到隐状态[1, 128]维向量,如果存在多说话人,在这里可以加入 speaker embedding,或者 emotion emdedding

  2. 将音素级别的 hidden state 转换为 word 级别,利用 word 与 phoneme 的对应关系,将一个 word 中音素的 hidden state 求平均即可获得 word 级别 hidden state(论文中命名为word leval pooling operation, WP)

  3. word Encoder(FFT blocks), 将第2步生产的 word hidden state 再经过几个fft进行编码

  4. 将第1步得到的 phoneme 级别的 hidden state 经过 Duration Predictor,获得音素级别的 duration;利用 word 及 phoneme 的对应关系,将 phoneme 级别 duration 进行求和,获得 word 级别的 duration

  5. 通过 LengthRegular 机制将 word 级别 hidden state(第3步结果) 根据 word duration 进行expand [batch_size, mel_T, hidden_size]

  6. Attention:

    6.1. 第1步获得的 phoneme hidden state 与位置编码 cat,经过线性层将维度映射到原来的 hidden size. 生成 K and V(两个是一样的).

    6.2. 第5步获得的经过 expand 的 word hidden state 与位置编码cat,经过线性层将维度映射到原来的 hidden size,再经过一个文本后处理层,得到最后的 Q

    6.3. 这里有 attn_mask,确保attn的是单词对应到本单词的音素。

    6.4. 输出 x 和 attention weight 结果,这里又利用了残差网络的思路,将 x 和 第6.3步的Q进行了相加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def run_text_encoder(self, txt_tokens, word_tokens, ph2word, word_len, mel2word, mel2ph, style_embed, ret):
word2word = torch.arange(word_len)[None, :].to(ph2word.device) + 1 # [B, T_mel, T_word]
# word2word [B, T_word], [1, 37]
src_nonpadding = (txt_tokens > 0).float()[:, :, None] # [1, 91, 1]
# 将音素通过encoder获得context的隐层状态
ph_encoder_out = self.encoder(txt_tokens) * src_nonpadding + style_embed # [1, 91, 128]
# 这里 encoder 的结构是: fft

# self.hparams['use_word_encoder'] = False
if self.hparams['use_word_encoder']:
word_encoder_out = self.word_encoder(word_tokens) + style_embed
ph_encoder_out = ph_encoder_out + expand_states(word_encoder_out, ph2word)
# self.hparams['dur_level'] == 'word' is True (强制对齐word级别duration, word 到 phone 利用 attention)
if self.hparams['dur_level'] == 'word':
word_encoder_out = 0
h_ph_gb_word = group_hidden_by_segs(ph_encoder_out, ph2word, word_len)[0] # [1, 37, 128]
# 这里是把 phone 级别的 hidden state 转化为 word 级别的(!!!求平均!!!)

word_encoder_out = word_encoder_out + self.ph2word_encoder(h_ph_gb_word)
# word_encoder_out 是 又经过了几个 fft blocks 之后的结果(ph2word_encoder)

if self.hparams['use_word_encoder']:
word_encoder_out = word_encoder_out + self.word_encoder(word_tokens)

mel2word = self.forward_dur(ph_encoder_out, mel2word, ret, ph2word=ph2word, word_len=word_len)
# forward_dur 是做什么的呢:利用 phone 级别 hidden state ---> phone 级别 duration ---> word duration
# mel2word: [1, 812] 表示mel的帧对应第i个word,【i】就是mel2word; 812是mel长度
mel2word = clip_mel2token_to_multiple(mel2word, self.hparams['frames_multiple'])
# print(self.hparams['frames_multiple']) 4
# mel 的 长度 必须是 4 的整数倍,否则删除后续帧
tgt_nonpadding = (mel2word > 0).float()[:, :, None]
# print(tgt_nonpadding.shape) # [1, 812, 1]
enc_pos = self.get_pos_embed(word2word, ph2word) # [B, T_ph, H]
# print(enc_pos.shape) # [1, 91, 128] # 91个 128维的位置编码
dec_pos = self.get_pos_embed(word2word, mel2word) # [B, T_mel, H]
# print(dec_pos) # [1, 812, 128] 812个 mel帧 128维的位置编码
dec_word_mask = build_word_mask(mel2word, ph2word) # [B, T_mel, T_ph] torch.Size([1, 812, 91])
# dec_word_mask 表示的是音素和帧的对齐,通过word进行连接,行表示帧,列表示音素,1表示有关系,0表示没有关系
x, weight = self.attention(ph_encoder_out, enc_pos, word_encoder_out, dec_pos, mel2word, dec_word_mask)
if self.hparams['add_word_pos']:
x = x + self.word_pos_proj(dec_pos)
ret['attn'] = weight
else:
mel2ph = self.forward_dur(ph_encoder_out, mel2ph, ret)
mel2ph = clip_mel2token_to_multiple(mel2ph, self.hparams['frames_multiple'])
mel2word = mel2ph_to_mel2word(mel2ph, ph2word)
x = expand_states(ph_encoder_out, mel2ph)
if self.hparams['add_word_pos']:
dec_pos = self.get_pos_embed(word2word, mel2word) # [B, T_mel, H]
x = x + self.word_pos_proj(dec_pos)
tgt_nonpadding = (mel2ph > 0).float()[:, :, None]
if self.hparams['use_word_encoder']:
x = x + expand_states(word_encoder_out, mel2word)
return x, tgt_nonpadding

Dencoder 模块 (FVAE)

输入:Text Encoder 获得的结果
输出:Mel spectrogram

  1. pre-net,将 text Encoder 的结果经过 pre-net 处理,类似于瓶颈层,维度变换[1, 128, 812] --> [1, 128, 203]; 这里进行的是一维卷积操作,卷积核大小 8, 步长为4,padding 为2,(812 + 2*2 - 8)/4 + 1=203

  2. VAE-Encoder, 将 mel spectrogram 与 condition 编码到 mean 和 sigma(这里用了log方差,为啥这样呢,因为方差都是非负数的,取log就不用特别设计激活函数了)

  3. 在VAE引入了prior_flow,由于单纯的正态分布过于简单,这种约束下生成模型的多样性会降低,prior_flow的作用是把正态映射到一个更复杂的分布

  4. VAE-Decoder, 利用第2步的输出与condition对mel spectrogram进行重构.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class FVAE(nn.Module):
def __init__(self,
c_in_out, hidden_size, c_latent,
kernel_size, enc_n_layers, dec_n_layers, c_cond, strides,
use_prior_flow, flow_hidden=None, flow_kernel_size=None, flow_n_steps=None,
encoder_type='wn', decoder_type='wn'):
super(FVAE, self).__init__()
self.strides = strides # [4]
self.hidden_size = hidden_size # 128
self.latent_size = c_latent # 16
self.use_prior_flow = use_prior_flow
if np.prod(strides) == 1:
self.g_pre_net = nn.Conv1d(c_cond, c_cond, kernel_size=1)
else:
# c_cond = 128
self.g_pre_net = nn.Sequential(*[
nn.Conv1d(c_cond, c_cond, kernel_size=s * 2, stride=s, padding=s // 2)
for i, s in enumerate(strides)
])
# nn.Conv1d 函数表示:维度计算,原来 203 是从这里来的,卷积后维度 = (原始维度 + 2*p - kernel_size) / stride + 1
# 203 = (812 + 4 - 8) / 4 + 1
self.encoder = FVAEEncoder(c_in_out, hidden_size, c_latent, kernel_size,
enc_n_layers, c_cond, strides=strides, nn_type=encoder_type)


if use_prior_flow:
self.prior_flow = ResFlow(
c_latent, flow_hidden, flow_kernel_size, flow_n_steps, 4, c_cond=c_cond)
self.decoder = FVAEDecoder(c_latent, hidden_size, c_in_out, kernel_size,
dec_n_layers, c_cond, strides=strides, nn_type=decoder_type)
self.prior_dist = dist.Normal(0, 1)

def forward(self, x=None, nonpadding=None, cond=None, infer=False, noise_scale=1.0):
"""

:param x: [B, C_in_out, T]
:param nonpadding: [B, 1, T]
:param cond: [B, C_g, T]
:return:
"""
# 这里三个参数分别表示:
# x: 真实mel谱图
# nonpadding:
# cond: encoder 的输出, 他的维度和 x 是相同的
if nonpadding is None:
nonpadding = 1
# cond 是 encoder 的输出
cond_sqz = self.g_pre_net(cond) # [1, 128, 203] 类似瓶颈层,先对输入进行一个一维卷积的操作,降低mel的维度

if not infer:
z_q, m_q, logs_q, nonpadding_sqz = self.encoder(x, nonpadding, cond_sqz)
# x: 真实 mel 谱 [1, 80, 812]
# cond_sqz: [1, 128, 203], 条件输入,这个是 text encoder 的 output
# z_q [1, 16, 203] # 这个表示 根据 m_q 和 logs_q 产生的随机结果
# m_q [1, 16, 203] # 这个表示 encoder 之后的均值
# logs_q [1, 16, 203] # 这个表示 log 的方差,为啥取log呢,因为方差都是正的,取log就不用特殊激活函数了

q_dist = dist.Normal(m_q, logs_q.exp())
if self.use_prior_flow:
logqx = q_dist.log_prob(z_q) # [1, 16, 203] 这个是为了后续计算 kl 散度损失
# prior_flow: 这里是因为单纯的 正太分布过于简单,这样约束下生成模型的多样性就会降低!该模块的作用是把 正太分布映射到
# 一个更复杂的分布 (利用 flow 模型将 z_q映射到z_p, 然后用正太分布计算概率,不是特别理解,后续需要再仔细看)
z_p = self.prior_flow(z_q, nonpadding_sqz, cond_sqz)
logpx = self.prior_dist.log_prob(z_p)
loss_kl = ((logqx - logpx) * nonpadding_sqz).sum() / nonpadding_sqz.sum() / logqx.shape[1]
else:
loss_kl = torch.distributions.kl_divergence(q_dist, self.prior_dist)
loss_kl = (loss_kl * nonpadding_sqz).sum() / nonpadding_sqz.sum() / z_q.shape[1]
z_p = None
return z_q, loss_kl, z_p, m_q, logs_q
else:
latent_shape = [cond_sqz.shape[0], self.latent_size, cond_sqz.shape[2]]
z_p = torch.randn(latent_shape).to(cond.device) * noise_scale
if self.use_prior_flow:
z_p = self.prior_flow(z_p, 1, cond_sqz, reverse=True)
return z_p

Post_Glow 模块

基于 normalizing flow 的后处理网络,由于VAE合成出来的是比较 blurry 的mel谱图,可以看如下的结果对比,利用 flow 后处理网络可以获得更优的细节表现,可以理解为 Tacotron2 的post-net的作用,不过基于 flow 的更好。
blurry

传统 flow 必须需要大量参数才能获得更好的结果,作者在 Affine Coupling Layer 加入了参数共享机制,缩小了模型。

关注到代码中 Post_Glow 是单独训练的,detach()之后,阻断了通过损失函数反向传播修改参数。

Glow 的更多细节可参考如下文章及博客:

损失函数包含如下四个部分:

  1. word level duration loss, MSE, log scale;
  2. VAE 中的重构损失,MAS
  3. VAE 中的分布约束损失,KL-divergence
  4. Post-Net 的非负对数似然