源码中,SynthesizerTrn几乎整合了所有重要的东西,包括:
- 文本编码器(TextEncoder)
- hifigan中生成wave的生成器(Generator)
- mel频谱编码器(PosteriorEncoder)
- 流模型(Flow)
- 音素持续时间预测器(DurationPredictor)
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)
1.TextEncoder(TE, enc_p)
TE是文本编码器,用于将文本编码为潜空间向量。更准确地说,是将音素编码为潜向量。
其构造函数需要几个参数:
-
n_vocab:178 (来自 text.symbols)
-
inter_channels: 192
-
hidden_channels: 192 (MultiHeadAttention的中间特征数)
-
filter_channels: 768 (FFN的中间特征数)
-
n_heads: 2
-
n_layers: 6
-
kernel_size: 3
-
p_dropout: 0.1
1.1 输入输出 (I/O)
x, m_p, logs_p, x_mask = enc_p(x, x_lengths)
输入:
- x, 文本的音素, x.shape = [batch_size, phonemes]
phonemes为一个batch中长度最大的phonemes
- x_lengths, 各样本的音素长度 x_lengths.shape = [batch_size]
输出:
-
x, 来自emb+attention编码器(倒数第二层),是文本序列编码后的潜向量。
-
m_p 和 logs_p:最后增加一层proj(conv1d),是潜向量分布的均值和对数方差。
shape = [batch_size, hidden_features, time_features*2+1]
- x_mask:用于后续模块处理的掩码。
x_mask = commons.sequence_mask(x_lengths, x.size(2)) # [1, 1, time_features*2+1]
掩码用于屏蔽掉填充的部分。该掩码确保在计算自注意力时,填充部分不会影响到实际序列的表示。
1.2 模型结构 (Architecture)
结构如下:
-
词嵌入层- embedding
-
编码器 - attention.encoder (输出x)
-
卷积投影层proj - conv1d(输出 m_p 与 logs_p)
1.3 数据流
具体如下:
-
输入数据有x为一批数据(batch),
-
batch中样本长度最大的length为x_lengths
def forward(self, x, x_lengths): # shape: [batch_sizh, 311], [311] phonemes = 311
x = self.emb(x) * math.sqrt(self.hidden_channels) # [batch_sizh, 311, 192], inter_channels = 192
x = torch.transpose(x, 1, -1) # [b, h, t] = [batch_sizh, 192, 311]
x_mask = torch.unsqueeze(commons.sequence_mask(x_lengths, x.size(2)), 1).to(x.dtype) # [batch_sizh, 1, 311]
x = self.encoder(x * x_mask, x_mask) # [batch_sizh, 192, 311]
stats = self.proj(x) * x_mask # [batch_sizh, 384, 311]
m, logs = torch.split(stats, self.out_channels, dim=1)
print('10:',m.shape) # 10: torch.Size([batch_sizh, 192, 311])
print('11:',logs.shape) # 11: torch.Size([batch_sizh, 192, 311])
2.Generator(dec)
HiFiGAN 部分
2.1 参数设置
VCTK版的json设置:
"model": {
"inter_channels": 192,
"hidden_channels": 192,
"filter_channels": 768,
"n_heads": 2,
"n_layers": 6,
"kernel_size": 3,
"p_dropout": 0.1,
"resblock": "1",
"resblock_kernel_sizes": [3,7,11],
"resblock_dilation_sizes": [[1,3,5], [1,3,5], [1,3,5]],
"upsample_rates": [8,8,2,2],
"upsample_initial_channel": 512,
"upsample_kernel_sizes": [16,16,4,4],
"n_layers_q": 3,
"use_spectral_norm": false,
"gin_channels": 256
}
2.2 输入输出 (I/O)
o = dec(z_slice, g=g)
输入:
- z_slice, 频谱数据 shape = [batch_size, inter_channels, wave_lengths]
- g(可选), 人物ID的embedding, shape = [batch_szie, gin_channels]
输出:
- x, 音频的waveforms, shape = [2, 1, 25600]
2.3 模型结构
残差和卷积核的种类有关
1.conv_pre = conv1d (大卷积核 = 7)
2.cond = conv1d (用于speaker id )
3.leaky_relu + weight_norm + convtransposed1d + resblock * kernel_sizes [3,7,11]
resblocks:
第一种是 3*conv1d + 3*conv1d
另一种是 2*conv1d
4. leaky_relu + Conv1d + tanh (大卷积核 = 7)
2.4 数据流
输入mel-时频谱,输出waveforms
- 测试代码
import torch
import models
# 参数初始化
inter_channels = 192 # mel特征数
resblock = 1
resblock_kernel_sizes = [3,7,11]
resblock_dilation_sizes = [[1,3,5], [1,3,5], [1,3,5]]
upsample_rates = [8,8,2,2]
upsample_initial_channel = 512
upsample_kernel_sizes = [16,16,4,4]
gin_channels = 256
n_speakers = 108
# 初始化Generator
generator = models.Generator(inter_channels, resblock, resblock_kernel_sizes, resblock_dilation_sizes, upsample_rates, upsample_initial_channel, upsample_kernel_sizes, gin_channels=gin_channels)
# speakerID 映射
emb_g = torch.nn.Embedding(n_speakers, gin_channels)
# 假设输入为(batch_size, channels, time_steps)
x = torch.randn(2, inter_channels, 100) # 批大小为2,80维输入特征,长度100的时间序列
# 选择是否使用条件输入
speaker_id = torch.LongTensor([4,5]) # 果有条件输入,g也应是(batch_size, gin_channels, time_steps)
g = emb_g(speaker_id).unsqueeze(-1) # [1,256] => [1, 256, 1]
output = generator(x, g) # output.shape = [2, 1, 25600]
- Generator的前向传播函数
def forward(self, x, g=None):
x = self.conv_pre(x) # in:[2, 192, 100], out:[2, 512, 100]
if g is not None:
x = x + self.cond(g) # [2, 512, 100] + ([2, 512, 1])
for i in range(self.num_upsamples):
x = F.leaky_relu(x, modules.LRELU_SLOPE) # [2, 512, 100], [2, 256, 800], [2, 128, 6400], [2, 64, 12800]
x = self.ups[i](x) # [2, 256, 800], [2, 128, 6400],[2, 64, 12800], [2, 32, 25600]
xs = None
for j in range(self.num_kernels):
if xs is None:
xs = self.resblocks[i*self.num_kernels+j](x) # [2, 256, 800], [2, 128, 6400],[2, 64, 12800], [2, 32, 25600]
else:
xs += self.resblocks[i*self.num_kernels+j](x) # [2, 256, 800]*2, [2, 128, 6400]*2,[2, 64, 12800]*2, [2, 32, 25600]*2
x = xs / self.num_kernels # [2, 256, 800], [2, 128, 6400],[2, 64, 12800], [2, 32, 25600]
x = F.leaky_relu(x)
x = self.conv_post(x) # [2, 32, 25600] => [2, 1, 25600]
x = torch.tanh(x)
return x
3.PosteriorEncoder(PE,enc_q)
后验编码器,HiFiGAN部分
-
将人的声音(mel频谱)y作为输入,输出其潜向量z(声色特征),为送入flow进行text融合做准备,
-
用多人数据集训练时,可以输入g以区分不同人物。
-
PE仅用于训练和声色转化推理(得到人物声色的潜向量z),不用于tts推理。
-
tts推理的z来自时间预测器(DP, 输出y_length 与x_mask计算得到 y_mask)和文本编码器(TE, 输出 m_p 和logs_p计算 z )的计算
-
构造参数:
in_channels = 512, # spec_channels
out_channels = 192 , # inter_channels
hidden_channels = 192,
kernel_size = 5,
dilation_rate = 1,
n_layers = 16, # wavenet layers
gin_channels=0
3.1 输入输出 (I/O)
z, m_q, logs_q, mask = enc_q(y, y_lengths)
输入:
- y: mel时频谱, shape = [batch_size, spec_channels, max_lengths] ,case= [10, 513, 759])
- y_lengths, 样本长度数组, case = tensor([759, 748, 634, 592, 542, 514, 435, 375, 318, 215])
输出:
- z:音频隐空间变量(后验分布中采样)。
shape = [batch_size, out_channels, max_lengths], case = [10, 192, 759]
- m 和 logs:隐变量分布的均值和对数方差,这里是和z相同的矩阵。
m和 logs的 shape与z一样,都是[batch_size, out_channels, max_lengths]
- x_mask:掩码,供后续层使用。
shape = [batch_size, 1, max_lengths]
3.2 模型结构
- pre: conv1d
- encoder: WaveNet (膨胀卷积)
- proj: conv1d
3.3 WaveNet
-
扩展卷积 (Dilated Convolution)
模型中的 in_layers 是一系列具有不同扩展率的卷积层。通过扩展卷积核在输入上跳跃式取样,从而有效增加感受野。在这个模型中,膨胀率是按幂次递增的,具体公式为 dilation = dilation_rate ** i,其中 i 是第 i 层的索引。
-
残差连接 (Residual Connections)
res_skip_layers 是每层的残差或跳跃连接,用来将局部层的输出传递到下一层并进行融合,避免梯度消失问题。这种连接方式在 WaveNet 中十分常见。
-
双激活函数融合技术
sigmoid 作用类似于权重或门控单元,调节 tanh 输出的强度。
通过两者的结果相乘,模型能够在不同的部分学到更细腻的非线性关系。
@torch.jit.script
def fused_add_tanh_sigmoid_multiply(input_a, input_b, n_channels):
n_channels_int = n_channels[0]
in_act = input_a + input_b
t_act = torch.tanh(in_act[:, :n_channels_int, :])
s_act = torch.sigmoid(in_act[:, n_channels_int:, :])
acts = t_act * s_act
return acts
3.4 数据流
-
PosteriorEncoder
-
x_mask 对非最大长度的样本进行掩码填充
-
pre 压缩特征 spec_channels => hidden_channels; case: Size([10, 513, 759]) = > Size([10, 192, 759])
-
wavenet 提取特征, Size([10, 192, 759]) => Size([10, 384, 759])
-
proj 分割特征 m , logs, Size([10, 384, 759]) => Size([10, 192, 759])*2
-
def forward(self, x, x_lengths, g=None):
x_mask = torch.unsqueeze(commons.sequence_mask(x_lengths, x.size(2)), 1).to(x.dtype)
x = self.pre(x) * x_mask
x = self.enc(x, x_mask, g=g)
stats = self.proj(x) * x_mask
m, logs = torch.split(stats, self.out_channels, dim=1)
z = (m + torch.randn_like(m) * torch.exp(logs)) * x_mask
return z, m, logs, x_mask
- WaveNet
这里有15层(n_layers = 15),数据是 [10, 192, 759] => [10, 384, 759] => [10, 192, 759]
conv1d 完成上采样
commons.fused_add_tanh_sigmoid_multiply 完成下采样
def forward(self, x, x_mask, g=None, **kwargs):
output = torch.zeros_like(x)
n_channels_tensor = torch.IntTensor([self.hidden_channels])
if g is not None:
g = self.cond_layer(g)
for i in range(self.n_layers):
x_in = self.in_layers[i](x)
if g is not None:
cond_offset = i * 2 * self.hidden_channels
g_l = g[:,cond_offset:cond_offset+2*self.hidden_channels,:]
else:
g_l = torch.zeros_like(x_in)
acts = commons.fused_add_tanh_sigmoid_multiply(
x_in,
g_l,
n_channels_tensor)
acts = self.drop(acts)
res_skip_acts = self.res_skip_layers[i](acts)
if i < self.n_layers - 1:
res_acts = res_skip_acts[:,:self.hidden_channels,:]
x = (x + res_acts) * x_mask
output = output + res_skip_acts[:,self.hidden_channels:,:]
else:
output = output + res_skip_acts
return output * x_mask
4.DurationPredictor(DP)
训练时用于输出length,推理时根据DP的length,计算attention
注: DP在Vits1里是Flow结构,取名为StochasticDurationPredictor ,在Vits2中做了简化,用GAN方式训练,替代了flow结构,以提升计算效率, 同时将StochasticDurationPredictor改为为DurationPredictor。
4.1 输入输出 (I/O)
-
flow
1.forward
x.shape = [batch_size, in_features, phoneme_max_lenghts] x_mask.shape = [batch_size, 1, phoneme_max_lenghts] l_length.shape = [batch_size, in_features, phoneme_max_lenghts]
l_length = dp(x, x_mask, w, g=g)
l_length = l_length / torch.sum(x_mask)
2.inverse
logw.shape = [batch_size, 1, phoneme_max_lenghts]
logw = dp(x, x_mask, g=g, reverse=True, noise_scale=noise_scale_w)
-
decoder
logw.shape = [batch_size, 1, phoneme_max_lenghts]
l_length = [batch_size]
logw = dp(x, x_mask, g=g)
logw_ = torch.log(w + 1e-6) * x_mask
l_length = torch.sum((logw - logw_)**2, [1,2]) / torch.sum(x_mask) # for averaging
4.2 模型结构
- SDP
1. Conv1d
2. DDSConv
3. Conv1d
4. Flow ( forward: nll + log_q, inverse: log_w)
- DP
conv1d + relu + norm + drop
conv1d + relu + norm + drop
conv1d
4.3 对齐注意力矩阵 w
首选是dp运算需要有一个w,来自flow的z_p和 text_encoder 的 logs_p
-
负对数似然的计算,结合了流模型和序列对齐机制(alignment mechanism)。
- 负对数熵(Negative Cross-Entropy)
- z_p = flow(z, y_mask, g=None):对 z 进行流模型变换
- s_p_sq_r = torch.exp(-2 * logs_p):从标准差(logs_p)计算得到的方差逆,通过对 -2 * logs_p 进行指数运算得到,用于后续权重计算。
- neg_cent1:第一部分,高斯分布的常数项部分,包含常数 -0.5 * log(2 * pi) 和标准差的对数项 - logs_p。通过 torch.sum 对维度 1(即特征维度 d)进行求和,输出的维度是 [b, 1, t_s]。
- neg_cent2:第二部分,通过 z_p ** 2 计算平方,然后乘以方差的逆 s_p_sq_r。矩阵乘法 将维度 [b, t_t, d] 和 [b, d, t_s] 结合,结果是 [b, t_t, t_s],这代表了序列对齐的分布权重。
- neg_cent3:第三部分,m_p 是某种均值或期望值,通过 m_p * s_p_sq_r 将其与方差逆权重相乘,然后与 z_p 进行矩阵乘法,结果同样是 [b, t_t, t_s],代表了对齐过程中对均值的贡献。
- neg_cent4:第四部分,这一项类似于 neg_cent2,不过它是针对均值 m_p 的平方进行加权,反映了高斯分布的均值部分,通过对维度 1 求和,输出的形状是 [b, 1, t_s]。
- 四部分相加,得到完整的负对数熵 neg_cent,它的形状为 [b, t_t, t_s],表示目标序列和源序列之间的负对数熵(cross-entropy)。
2.对齐路径与注意力掩码
- attn_mask:注意力掩码,计算两个掩码 x_mask 和 y_mask 的外积,形状为 [b, t_t, t_s],用于限制注意力的范围。
- attn:调用 monotonic_align.maximum_path() 函数,找到 neg_cent 中的最佳对齐路径。这个函数的目标是通过动态规划,找到输入和输出序列的最优对齐路径。attn 是一个注意力矩阵,用来描述源序列到目标序列的映射关系。
- 输出w=attn.sum(2),计算对齐路径 attn 在维度 2 上的和, 是对齐后的权重,用于将输入序列转换为目标序列的加权和。
# negative cross-entropy
z_p = flow(z, y_mask, g=None)
s_p_sq_r = torch.exp(-2 * logs_p) # [b, d, t]
neg_cent1 = torch.sum(-0.5 * math.log(2 * math.pi) - logs_p, [1], keepdim=True) # [b, 1, t_s]
neg_cent2 = torch.matmul(-0.5 * (z_p ** 2).transpose(1, 2), s_p_sq_r) # [b, t_t, d] x [b, d, t_s] = [b, t_t, t_s]
neg_cent3 = torch.matmul(z_p.transpose(1, 2), (m_p * s_p_sq_r)) # [b, t_t, d] x [b, d, t_s] = [b, t_t, t_s]
neg_cent4 = torch.sum(-0.5 * (m_p ** 2) * s_p_sq_r, [1], keepdim=True) # [b, 1, t_s]
neg_cent = neg_cent1 + neg_cent2 + neg_cent3 + neg_cent4
attn_mask = torch.unsqueeze(x_mask, 2) * torch.unsqueeze(y_mask, -1)
attn = monotonic_align.maximum_path(neg_cent, attn_mask.squeeze(1)).unsqueeze(1).detach()
w = attn.sum(2)
负对数熵(negative cross-entropy)的计算,结合高斯分布的各项贡献(均值、方差),用于对输入序列和目标序列进行对齐。最终通过动态规划寻找最佳对齐路径,并输出对齐后的权重 w,在序列对齐中起到了关键作用。
4.4 对数相然-训练分布一致性
其次是sdp运算内部为一个flow,其需要通过正向flow训练,推理时即逆向flow时得到一个有效的时序预测矩阵 log_w:
-
正向过程(if not reverse)—对数据执行流体变换,通常用于训练模型:
- 输入数据:
- w: 输入的数据,应该是某种特征或编码。
- x_mask: 一个掩码,用于对输入和输出的特定部分进行屏蔽。
- x: 额外的条件信息,用于在流动中进行条件化。
- 初始化变量:
- flows: 流程(flow)的列表,代表一系列的可逆变换,用来改变输入数据的分布。
- logdet_tot_q: 初始化为 0,用来累加后续的对数行列式的变化(log-determinant)。
- h_w: 输入 w 的处理结果,使用 post_pre() 函数做初步处理,然后通过卷积层(post_convs())和全连接层(post_proj())进行转换。
- 初始采样:
- e_q: 从标准正态分布(torch.randn)中采样一个噪声张量,用于模拟潜在变量的采样。
- z_q: 初始潜在变量 z_q,由 e_q 进行赋值。
- 流动过程:
- 通过一系列 post_flows 对 z_q 和条件 g(即 x + h_w)进行逐步变换,并累积对应的对数行列式变化。
- 将潜在变量 z_q 分割为 z_u 和 z1,然后通过 sigmoid 函数计算 u,进而调整 w 并计算对数行列式的变化。
- 计算对数似然:
- 通过log_flow对z0进行进一步变换,并累积对数行列式变化。
- 最后,通过合并 z0 和 z1,再经过所有 flows 流程,计算输出张量的对数似然(nll)。
- 对数似然由 nll 和 logq 相加得到,并作为结果返回。
- 输入数据:
-
逆向过程(else)—对潜在变量进行逆向采样,通常用于生成任务:
1.流程反转:将流体模型的流程进行逆转,并去掉最后一个不必要的流(flows[:-2] + [flows[-1]])。2.初始采样:生成一个服从标准正态分布的噪声 z,用于开始逆向变换。
-
逆向流动过程:通过逆转的流程 flows,从 z 中逐步逆推得到原始数据的变换。
-
输出:将 z 分为 z0 和 z1,并返回 z0(即 logw),这是反向生成的数据。
-
if not reverse:
flows = self.flows
assert w is not None
logdet_tot_q = 0
h_w = self.post_pre(w)
#print(h_w.shape)
h_w = self.post_convs(h_w, x_mask)
h_w = self.post_proj(h_w) * x_mask
e_q = torch.randn(w.size(0), 2, w.size(2)).to(device=x.device, dtype=x.dtype) * x_mask
z_q = e_q
for flow in self.post_flows:
z_q, logdet_q = flow(z_q, x_mask, g=(x + h_w))
logdet_tot_q += logdet_q
z_u, z1 = torch.split(z_q, [1, 1], 1)
u = torch.sigmoid(z_u) * x_mask
z0 = (w - u) * x_mask
logdet_tot_q += torch.sum((F.logsigmoid(z_u) + F.logsigmoid(-z_u)) * x_mask, [1,2])
logq = torch.sum(-0.5 * (math.log(2*math.pi) + (e_q**2)) * x_mask, [1,2]) - logdet_tot_q
logdet_tot = 0
z0, logdet = self.log_flow(z0, x_mask)
logdet_tot += logdet
z = torch.cat([z0, z1], 1)
for flow in flows:
z, logdet = flow(z, x_mask, g=x, reverse=reverse)
logdet_tot = logdet_tot + logdet
nll = torch.sum(0.5 * (math.log(2*math.pi) + (z**2)) * x_mask, [1,2]) - logdet_tot
return nll + logq # [b]
else:
flows = list(reversed(self.flows))
flows = flows[:-2] + [flows[-1]] # remove a useless vflow
z = torch.randn(x.size(0), 2, x.size(2)).to(device=x.device, dtype=x.dtype) * noise_scale
for flow in flows:
z = flow(z, x_mask, g=x, reverse=reverse)
z0, z1 = torch.split(z, [1, 1], 1)
logw = z0
print(logw.shape)
return logw
正向过程计算对数似然,逆向过程则生成新的样本,确保输入输出的分布一致性。
5.Montonic Alignment Search (MAS)
仅用于训练,将x_mask和y_mask计算得到attn_mask,将logs_p和m_p (来自 enc_p) 与z_flow (enc_q)计算得到的neg_cent。
将 attn_mask 和 neg_cent送入MAS对齐音频和文本,输出attn,用于输出m_p 和 logs_p, 以计算kl损失函数。
5.1 MAS解析
代码:
attn = monotonic_align.maximum_path(neg_cent, attn_mask.squeeze(1)).unsqueeze(1).detach()
函数解析:
动态规划(DP)来寻找负对数熵矩阵neg_cent的最佳对齐路径:
-
动态规划的前向步骤:
- 对 neg_cent[y, x] 进行累加,计算每个点到起点的最大路径值。它根据来自前一行 y-1 的两个值(左上 x-1 或上方 x)进行更新。
- 设置flag:v_prev 和 v_cur 是用来跟踪前一行的两个潜在的路径值,选择其中较大的值更新当前位置。
2.反向路径回溯:
- 通过从最后一行回溯,找到最佳路径 path[y, index]。
- 当选择左上方 x-1 的路径值大于上方 x 的值时,将 index 左移。
3.掩码:
- attn_mask.squeeze(1)是一个掩码,用于限定哪些位置可以参与对齐,计算结果返回的是 attn,即对齐的注意力矩阵,它用于表示输入序列和输出序列之间的映射关系。
5.2 输入输出
neg_cent 是 monotonic_align.maximum_path 的唯一输入,它实际上已经编码了输入序列和输出序列之间的对齐关系。
序列对齐路径是找到从源序列到目标序列的最佳映射。neg_cent 中的得分越高,意味着该对齐路径的代价越小,需要在 neg_cent 矩阵中找到累积得分最低的路径。
neg_cent 是一个成本矩阵,它的每一行代表目标序列中的位置(音频),每一列代表源序列中的位置(文本),动态规划帮助我们找到最优的映射。
-
neg_cent1: 包含了高斯分布的常数项和标准差的对数部分。
-
neg_cent2, neg_cent3, neg_cent4: 这些项结合了模型生成的潜在变量 z_p 和高斯分布的均值 m_p,并通过加权求和的方式编码了目标序列与源序列的匹配程度。
-
neg_cent 实际上是一个矩阵,表示了目标语音序列(t_y)和源文本序列(t_x)之间的对齐成本,或负对数熵。矩阵的每个元素 neg_cent[y, x] 表示目标序列的第 y 个位置和源序列的第 x 个位置的对齐得分。
-
neg_cent[y, x] 表示目标序列的第 y 个位置最适合与源序列的第 x 个位置对齐。一个最优路径从左上角 (0, 0) 开始,沿着某些路径(向下或向右移动)到达右下角 (t_y-1, t_x-1),每一步都选择具有最小代价的方向。
5.3 分析
在 TTS 中,目标序列《音频帧》与源序列《音素》的长度不一致。由于音素的持续时间不同,一个音素可能会扩展到多个连续的音频帧,即帧延展,因此TTS的音频帧的数量远远多于音素的数量,即:
一个音素可能对应多个音频帧,形成了 一对多对齐 的情况。
- 示例:
假设文本中的音素序列为 [p, a, t],音频帧的数量远多于音素的数量,是 [帧1, 帧2, …, 帧9]。
假定MAS输出最短路径( path ),是一条从源序列到目标序列的映射路径。
在对齐矩阵 path 中,用二进制(0 或 1)表示:
- path[y, x] = 1 表示 目标序列的第 y 个位置与源序列的第 x 个位置对齐。
- path[y, x] = 0 表示 目标序列的第 y 个位置没有与源序列的第 x 个位置对齐,即这些位置不在对齐路径上。
其中, “p” 和 “t” 发音较短,对应 2 个帧,而 “a” 发音较长,对应 5 个帧。音素序列为 [p, a, t]与音频帧的对齐关系是:
path =
[
[1, 0, 0], # 帧1 对应音素 "p"
[1, 0, 0], # 帧2 对应音素 "p"
[0, 1, 0], # 帧3 对应音素 "a"
[0, 1, 0], # 帧4 对应音素 "a"
[0, 1, 0], # 帧5 对应音素 "a"
[0, 1, 0], # 帧6 对应音素 "a"
[0, 1, 0], # 帧7 对应音素 "a"
[0, 0, 1], # 帧8 对应音素 "t"
[0, 0, 1] # 帧9 对应音素 "t"
]
某些列中会有多个连续的 “1”,代表同一个音素扩展到多个帧的 一对多 对齐。
如上例,Monotonic Alignment是单调对齐矩阵,即每个音素的对齐只能向右或者向下推进,对齐不能出现跳跃,及逆向移动,以确保生成的音频帧顺序对应音素的顺序。
6.Flow
VITS的关键,训练有效的flow,可用于输出 Text 到 Speech 的语义潜向量
-
正向flow用于训练如何将“音频潜向量特征”与“文本潜项链特征”对齐
-
逆向flow将文本特征融入到音频潜向量
6.1 输入输出 (I/O)
6.2 模型结构
6.3 数据流
篇幅限制,flow部分暂略。