StarGAN-VC实战:基于非并行数据的语音音色转换全流程解析 1. 项目概述从“非并行”到“音色转换”的核心挑战做语音合成或者语音转换的朋友对“音色转换”这个概念一定不陌生。简单说就是保留一句话的内容和韵律但把说话人的声音换成另一个人的。比如让一段新闻播报用你朋友的声音说出来或者让一段语音教学用更亲切的语调呈现。传统的语音转换方法比如基于高斯混合模型GMM或隐马尔可夫模型HMM的往往需要“并行语料”——也就是同一个句子由源说话人和目标说话人各说一遍。这种数据获取成本极高几乎只存在于实验室的特定数据库中严重限制了技术的实际应用。所以“非并行”语音转换即只使用不同说话人各自独立的语音数据他们说的话内容完全不同就成了一个极具吸引力的研究方向。而StarGAN-VC正是这个领域里一个里程碑式的模型。我第一次接触这个工作时就被它巧妙的思路吸引了它把计算机视觉领域大放异彩的生成对抗网络GAN和StarGAN的思想成功地迁移到了语音的一维时序信号上。这个项目要做的就是亲手实现它理解它如何仅用非并行数据就能学习到不同说话人之间复杂的音色映射关系。2. 核心原理拆解StarGAN-VC如何“无中生有”要理解StarGAN-VC得先拆开来看它的两个核心部分StarGAN的架构思想和语音领域的适配。2.1 StarGAN一个生成器应对多个域StarGAN本身是为多域图像转换设计的。想象一下你有一个图像生成器它需要把金毛犬的照片转换成哈士奇或者转换成猫。传统方法可能需要为每对转换训练一个独立的模型金毛-哈士奇金毛-猫这显然低效。StarGAN的创新在于它只使用一个生成器通过向生成器输入一个“目标域标签”比如一个代表“哈士奇”的向量来指示它应该将输入图像转换成哪个域。在训练时生成器不仅要把输入图像金毛转换成目标域图像假哈士奇还要能把生成的假哈士奇图像再转换回原始域金毛并要求这个“循环”回来的图像和原图尽可能一致。这就是循环一致性损失。同时还有一个判别器它不仅要判断图像是真是假还要判断它属于哪个域金毛、哈士奇还是猫。通过这种对抗训练和循环约束单个生成器就学会了所有域之间的复杂映射关系。2.2 语音信号的独特处理从梅尔谱图出发语音是时序的一维信号直接套用图像处理的方法行不通。StarGAN-VC的关键预处理步骤是将语音转换为梅尔谱图。梅尔谱图是一种二维时频表示横轴是时间纵轴是梅尔频率一种模拟人耳听觉特性的频率刻度颜色深浅代表能量强度。它完美地将一维语音信号“图像化”了保留了语音的时序结构和频谱特征同时又可以被卷积神经网络CNN处理。因此StarGAN-VC的流程可以概括为输入源说话人语音 - 提取梅尔谱图二维矩阵。生成将源梅尔谱图和目标说话人标签one-hot向量一起输入生成器G。输出生成器G输出一个“伪造”的目标说话人梅尔谱图。判别判别器D接收这个伪造谱图判断它a) 是不是一个真实的梅尔谱图对抗损失b) 是否属于目标说话人域域分类损失。循环将生成的假目标谱图连同源说话人标签再次输入同一个生成器G试图重建回源谱图并与原始源谱图计算差异循环一致性损失。身份映射为了保持输入语音的内容不变还会将源谱图和源说话人标签输入G要求输出尽可能与输入相同身份映射损失。通过这几种损失的共同约束生成器在“欺骗”判别器的过程中学会了只改变与说话人身份音色相关的频谱特征而保留语音内容时序和频谱包络结构和韵律信息。3. 环境准备与数据预处理实战理论清晰了接下来就是动手。实现StarGAN-VC环境搭建和数据预处理是第一步也是最容易踩坑的地方。3.1 开发环境配置清单我推荐使用Python 3.8和PyTorch 1.9的组合兼容性和社区支持都比较好。以下是我的核心依赖清单# 核心框架 torch1.9.0 torchaudio0.9.0 # 用于音频处理和梅尔谱图提取 # 数据处理与科学计算 numpy librosa0.8.0 # 音频处理备用功能强大 scipy # 进度显示与日志 tqdm tensorboard # 用于训练过程可视化强烈推荐注意PyTorch和CUDA版本的匹配是关键。如果你的机器有NVIDIA GPU务必去PyTorch官网根据你的CUDA版本选择对应的安装命令。使用nvidia-smi查看CUDA版本。版本不匹配会导致无法调用GPU训练速度慢如蜗牛。3.2 语音数据集选择与预处理流水线非并行转换不需要成对数据因此数据集选择灵活很多。常用的有VCTK包含109位以英语为母语的说话人每人朗读数百句不同的文本口音一致音质干净是学术研究的首选。CMU ARCTIC包含4位说话人2男2女的大量语音同样非常干净。AISHELL-3一个大规模中文普通话多说话人语音合成数据集包含218位说话人适合中文场景。这里以VCTK为例讲解预处理步骤。我们的目标是将所有.wav文件转换为标准化的梅尔谱图.npy文件并保存对应的说话人ID。步骤1统一音频格式VCTK的采样率是48kHz但为了减少计算量并统一我们通常下采样到16kHz或24kHz。同时将所有音频归一化到相同的音量水平如-3 dB。import librosa import soundfile as sf def preprocess_audio(wav_path, target_sr24000, norm_db-3): # 加载音频 audio, sr librosa.load(wav_path, srtarget_sr) # 音量归一化 rms np.sqrt(np.mean(audio**2)) target_rms 10**(norm_db / 20) audio audio * (target_rms / (rms 1e-6)) return audio, target_sr步骤2提取梅尔谱图这是最关键的一步。梅尔谱图的参数设置直接影响模型效果。帧长 (n_fft)通常取1024或2048。较长的帧能提供更好的频率分辨率但会降低时间分辨率。对于语音1024在24kHz下约43ms是个不错的起点。帧移 (hop_length)通常取256是帧长的1/4在时间平滑度和计算量间取得平衡。梅尔滤波器组数量 (n_mels)80或128。越多对频谱的刻画越细但计算量也越大。80是一个广泛使用的值。def extract_melspectrogram(audio, sr24000, n_fft1024, hop_length256, n_mels80): # 使用librosa或torchaudio计算梅尔谱图 # 这里以librosa为例 mel_spec librosa.feature.melspectrogram(yaudio, srsr, n_fftn_fft, hop_lengthhop_length, n_melsn_mels) # 转换为对数刻度分贝符合人耳感知 log_mel_spec librosa.power_to_db(mel_spec, refnp.max) # 归一化到[-1, 1]区间方便神经网络处理 log_mel_spec (log_mel_spec - log_mel_spec.min()) / (log_mel_spec.max() - log_mel_spec.min()) * 2 - 1 return log_mel_spec.T # 转置使形状为 (时间帧数, 梅尔通道数)步骤3数据切片与组织原始语音长短不一直接输入网络不方便。常见的做法是固定一个长度如128帧从长的语音中随机裁剪片段进行训练。我们需要构建一个数据集类它能够根据说话人ID加载对应的所有梅尔谱图文件路径。在__getitem__方法中随机选择一个说话人的一个语音文件并从中随机裁剪出固定长度的片段。返回这个片段矩阵和对应的说话人标签整数索引。class VoiceDataset(torch.utils.data.Dataset): def __init__(self, data_root, speakers, segment_frames128): self.data [] # 存储(谱图路径, 说话人id)对 self.speakers speakers self.segment_frames segment_frames # 遍历data_root组织数据... def __getitem__(self, index): spec_path, spk_id self.data[index] log_mel_spec np.load(spec_path) # 形状 (T, n_mels) # 随机裁剪 if log_mel_spec.shape[0] self.segment_frames: start np.random.randint(0, log_mel_spec.shape[0] - self.segment_frames) segment log_mel_spec[start:startself.segment_frames, :] else: # 如果语音太短进行填充实践中应尽量避免 segment np.pad(log_mel_spec, ((0, self.segment_frames - log_mel_spec.shape[0]), (0, 0)), modeconstant) # 转换为Tensor并增加通道维度 (1, frames, n_mels) 模拟图像通道 segment torch.FloatTensor(segment).unsqueeze(0) spk_label torch.LongTensor([spk_id]) return segment, spk_label实操心得数据预处理的质量决定了模型的天花板。务必确保所有音频的采样率、音量、静音处理可选可使用librosa.effects.trim保持一致。梅尔谱图的归一化方式也很重要全局归一化整个训练集计算均值和方差通常比单文件归一化效果更稳定。4. 模型架构的代码级实现StarGAN-VC的模型并不复杂但其设计细节直接影响性能。我们分别实现生成器G和判别器D。4.1 生成器G带有自适应实例归一化的编码器-解码器生成器采用U-Net类似的编码器-解码器结构中间通过残差块连接。核心技巧是在解码器的每个上采样层前加入自适应实例归一化AdaIN将目标说话人标签信息注入到特征图中。import torch.nn as nn import torch.nn.functional as F class ResidualBlock(nn.Module): def __init__(self, dim): super().__init__() self.block nn.Sequential( nn.Conv2d(dim, dim, 3, 1, 1), nn.InstanceNorm2d(dim, affineTrue), # 使用InstanceNorm nn.ReLU(inplaceTrue), nn.Conv2d(dim, dim, 3, 1, 1), nn.InstanceNorm2d(dim, affineTrue) ) def forward(self, x): return x self.block(x) class AdaIN(nn.Module): 自适应实例归一化用目标域的缩放和偏置参数来调制特征图 def __init__(self, style_dim, channels): super().__init__() # 将说话人标签映射为风格向量并线性变换出gamma和beta self.fc_gamma nn.Linear(style_dim, channels) self.fc_beta nn.Linear(style_dim, channels) def forward(self, x, style_vector): # x: (B, C, H, W), style_vector: (B, style_dim) gamma self.fc_gamma(style_vector).unsqueeze(2).unsqueeze(3) # (B, C, 1, 1) beta self.fc_beta(style_vector).unsqueeze(2).unsqueeze(3) # 计算x的实例统计量 x_mean torch.mean(x, dim[2,3], keepdimTrue) x_std torch.std(x, dim[2,3], keepdimTrue) 1e-8 # 应用AdaIN: gamma * ((x - mean)/std) beta return gamma * ((x - x_mean) / x_std) beta class Generator(nn.Module): def __init__(self, n_speakers, style_dim64, base_channels32): super().__init__() # 说话人嵌入层将说话人ID映射为风格向量 self.spk_embed nn.Embedding(n_speakers, style_dim) # 编码器 self.enc1 nn.Conv2d(1, base_channels, 7, 1, 3) # 初始卷积 self.enc2 nn.Sequential( nn.Conv2d(base_channels, base_channels*2, 4, 2, 1), nn.InstanceNorm2d(base_channels*2), nn.ReLU(), ResidualBlock(base_channels*2) ) self.enc3 nn.Sequential( # 继续下采样... nn.Conv2d(base_channels*2, base_channels*4, 4, 2, 1), nn.InstanceNorm2d(base_channels*4), nn.ReLU(), ResidualBlock(base_channels*4) ) # 中间残差块 self.res_blocks nn.Sequential(*[ResidualBlock(base_channels*4) for _ in range(6)]) # 解码器带AdaIN self.dec1 self._make_decoder_block(base_channels*4, base_channels*2, style_dim) self.dec2 self._make_decoder_block(base_channels*2, base_channels, style_dim) self.final_conv nn.Conv2d(base_channels, 1, 7, 1, 3) self.tanh nn.Tanh() def _make_decoder_block(self, in_c, out_c, style_dim): return nn.Sequential( nn.Upsample(scale_factor2, modenearest), nn.Conv2d(in_c, out_c, 5, 1, 2), AdaIN(style_dim, out_c), nn.ReLU() ) def forward(self, x, target_spk_id): # x: (B, 1, Frames, Mels), target_spk_id: (B,) style_vec self.spk_embed(target_spk_id) # (B, style_dim) # 编码 e1 self.enc1(x) e2 self.enc2(e1) e3 self.enc3(e2) # 残差 h self.res_blocks(e3) # 解码 AdaIN d1 self.dec1[0](h) # Upsample d1 self.dec1[1](d1) # Conv d1 self.dec1[2](d1, style_vec) # AdaIN注入风格 d1 self.dec1[3](d1) # ReLU # 可选的跳跃连接类似U-Net将编码器特征与解码器特征拼接有助于保留细节 d1 d1 e2 d2 self.dec2[0](d1) d2 self.dec2[1](d2) d2 self.dec2[2](d2, style_vec) d2 self.dec2[3](d2) d2 d2 e1 # 最终输出 out self.final_conv(d2) return self.tanh(out)4.2 判别器D兼具真伪与域分类能力的卷积网络判别器是一个标准的卷积分类器但输出两个头一个用于判断真伪二分类一个用于判断属于哪个说话人多分类。class Discriminator(nn.Module): def __init__(self, n_speakers, base_channels32): super().__init__() # 共享特征提取层 self.shared_layers nn.Sequential( nn.Conv2d(1, base_channels, 4, 2, 1), # (B, 32, H/2, W/2) nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(base_channels, base_channels*2, 4, 2, 1), nn.InstanceNorm2d(base_channels*2), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(base_channels*2, base_channels*4, 4, 2, 1), nn.InstanceNorm2d(base_channels*4), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(base_channels*4, base_channels*8, 4, 2, 1), nn.InstanceNorm2d(base_channels*8), nn.LeakyReLU(0.2, inplaceTrue), ) # 真伪判别头 self.adv_head nn.Conv2d(base_channels*8, 1, kernel_size3, stride1, padding1) # 说话人分类头 self.cls_head nn.Sequential( nn.AdaptiveAvgPool2d(1), # 全局平均池化得到 (B, C, 1, 1) nn.Flatten(), nn.Linear(base_channels*8, n_speakers) ) def forward(self, x): features self.shared_layers(x) # (B, 256, H/16, W/16) # 真伪判别输出对每个空间位置都输出一个值最后取平均作为最终的真伪分数 adv_out self.adv_head(features) # (B, 1, H/16, W/16) adv_out torch.sigmoid(adv_out.mean(dim[2,3])) # (B, 1) # 说话人分类输出 cls_out self.cls_head(features) # (B, n_speakers) return adv_out.squeeze(1), cls_out # 返回 (B,), (B, n_speakers)5. 损失函数设计与训练策略StarGAN-VC的训练是多种损失函数的博弈。理解每个损失的作用和权重设置是调参的关键。5.1 四大损失函数详解对抗损失 (Adversarial Loss)让生成器G产生的谱图尽可能“骗过”判别器D。使用最小二乘GANLSGAN的损失训练更稳定。对于判别器D要最大化对真实谱图的判别为真最小化对生成谱图的判别为真。loss_D_adv E[(D_real - 1)^2] E[(D_fake)^2]对于生成器G要让它生成的谱图被判别为真。loss_G_adv E[(D_fake - 1)^2]域分类损失 (Domain Classification Loss)确保生成的谱图被正确分类为目标说话人。对于判别器D在输入真实谱图时它的分类头要能正确预测其说话人标签。loss_D_cls CrossEntropy(D_cls(real), real_label)对于生成器G它生成的假谱图输入判别器后分类头应预测为目标说话人标签。loss_G_cls CrossEntropy(D_cls(fake), target_label)循环一致性损失 (Cycle Consistency Loss)这是保证内容不变性的核心。将生成的假目标谱图fake_B和源说话人标签label_A输入G得到重建谱图rec_A要求rec_A与原始输入real_A尽可能相似。使用L1损失loss_cyc ||rec_A - real_A||_1身份映射损失 (Identity Mapping Loss)将源谱图real_A和源标签label_A输入G要求输出idt_A与real_A尽可能相似。这有助于稳定训练防止生成器对输入做过多的不必要的修改。同样使用L1损失loss_idt ||idt_A - real_A||_15.2 训练循环与参数更新训练采用交替更新的策略先更新判别器D再更新生成器G。# 伪代码流程 for epoch in range(num_epochs): for batch_real, batch_label in dataloader: # 1. 准备数据 real_A batch_real.to(device) label_A batch_label.to(device).squeeze() # 随机选择目标说话人标签不能与源相同 label_B torch.randint(0, n_speakers, label_A.size()).to(device) # 确保label_B ! label_A label_B (label_B 1) % n_speakers # 简单处理确保不同 # 2. 训练判别器D optimizer_D.zero_grad() # 生成假谱图 fake_B generator(real_A, label_B) # 判别器对真实和假谱图的判断 d_real_adv, d_real_cls discriminator(real_A) d_fake_adv, _ discriminator(fake_B.detach()) # 注意detach防止梯度传到G # 计算D的对抗损失和分类损失 loss_D_adv torch.mean((d_real_adv - 1)**2) torch.mean(d_fake_adv**2) loss_D_cls F.cross_entropy(d_real_cls, label_A) loss_D loss_D_adv lambda_cls * loss_D_cls loss_D.backward() optimizer_D.step() # 3. 训练生成器G optimizer_G.zero_grad() # 再次生成假谱图这次不detach fake_B generator(real_A, label_B) d_fake_adv, d_fake_cls discriminator(fake_B) # 计算G的对抗损失和分类损失 loss_G_adv torch.mean((d_fake_adv - 1)**2) loss_G_cls F.cross_entropy(d_fake_cls, label_B) # 计算循环一致性损失 rec_A generator(fake_B, label_A) loss_cyc F.l1_loss(rec_A, real_A) # 计算身份映射损失 idt_A generator(real_A, label_A) loss_idt F.l1_loss(idt_A, real_A) # G的总损失 loss_G loss_G_adv lambda_cls * loss_G_cls lambda_cyc * loss_cyc lambda_idt * loss_idt loss_G.backward() optimizer_G.step()超参数设置经验lambda_cls分类损失权重通常设为1.0或2.0。lambda_cyc循环一致性损失权重至关重要通常设为10.0。这个值太小会导致内容信息丢失音色变了说的话也变了太大会导致模式崩溃所有输出都趋同。lambda_idt身份映射损失权重通常设为5.0有助于稳定训练初期。学习率使用Adam优化器初始学习率lr0.0001beta(0.5, 0.999)是GAN训练的经典配置。批量大小受限于GPU显存通常从8或16开始尝试。更大的批量有助于稳定训练。6. 从梅尔谱图到可听语音声码器的选择与使用模型训练好后我们得到的是目标说话人的梅尔谱图。要把它变回可以播放的.wav文件需要一个声码器。声码器的任务是根据梅尔谱图或类似的声学特征重建出高质量的时域波形。这是一个极具挑战性的任务。6.1 声码器选项对比Griffin-Lim算法一种经典的相位重建算法基于梅尔谱图的幅度信息迭代估计相位。优点无需训练实现简单。缺点重建语音质量较差有明显的“机械声”或嗡嗡声仅适用于演示或快速验证。预训练的神经声码器这是目前的主流和推荐选择。它们通常在大规模高质量语音数据上预训练好可以直接调用效果远好于Griffin-Lim。WaveNet / WaveRNN自回归模型质量高但推理慢。MelGAN / HiFi-GAN基于GAN的声码器质量和速度的完美平衡是当前的首选。它们被训练成直接由梅尔谱图生成波形推理速度极快实时因子远大于1。Parallel WaveGAN另一种高质量的GAN声码器。6.2 使用HiFi-GAN声码器实战以开源的HiFi-GAN为例我们可以轻松集成。# 假设我们已经有了训练好的StarGAN-VC生成器 generator # 以及一段源语音的梅尔谱图 src_mel (形状: 1, 1, T, 80) import torch from models import Generator from hifigan.models import Generator as HiFiGAN from hifigan.env import AttrDict from hifigan.meldataset import mel_spectrogram import json import soundfile as sf # 1. 加载训练好的StarGAN-VC生成器 generator Generator(n_speakers4).to(cuda) generator.load_state_dict(torch.load(stargan_vc_generator.pth)) generator.eval() # 2. 加载预训练的HiFi-GAN声码器 # 首先加载配置文件 with open(hifigan/config.json) as f: h AttrDict(json.load(f)) hifigan HiFiGAN(h).to(cuda) hifigan.load_state_dict(torch.load(hifigan/generator.pth)) hifigan.eval() hifigan.remove_weight_norm() # 移除训练时的权重归一化加速推理 # 3. 进行音色转换 src_mel torch.from_numpy(src_mel).unsqueeze(0).unsqueeze(0).to(cuda) # (1,1,T,80) target_spk_id torch.tensor([2]).to(cuda) # 假设目标说话人ID是2 with torch.no_grad(): converted_mel generator(src_mel, target_spk_id) # (1,1,T,80) # 将生成的梅尔谱图调整到HiFi-GAN期望的输入范围例如[0, 1] # 注意HiFi-GAN训练时使用的梅尔谱图归一化方式必须与你的预处理方式匹配 # 这里假设我们的生成器输出是tanh后的[-1,1]需要映射到[0,1] converted_mel (converted_mel 1) / 2 # 调整维度HiFi-GAN通常期望输入为 (1, n_mels, T) converted_mel converted_mel.squeeze(1).transpose(1, 2) # (1, 80, T) # 生成波形 waveform hifigan(converted_mel).squeeze().cpu().numpy() # 4. 保存音频 sf.write(converted_audio.wav, waveform, samplerate24000)重要提示声码器的输入梅尔谱图必须与其训练时使用的特征提取参数帧长、帧移、梅尔滤波器数量、归一化范围完全一致。否则即使梅尔谱图看起来正常重建出的语音也可能失真严重。最稳妥的方法是直接使用声码器作者提供的特征提取函数如mel_spectrogram来预处理你的训练数据并在推理时用同样的函数处理原始音频再将生成的梅尔谱图用同样的逆归一化方式处理最后喂给声码器。7. 训练过程监控、问题排查与效果评估GAN的训练 notoriously unstable notoriously unstable出了名的不稳定。没有好的监控和排查手段就像在黑暗中摸索。7.1 使用TensorBoard进行可视化监控务必使用TensorBoard来记录损失曲线和生成样本。损失曲线同时绘制loss_G,loss_D,loss_G_adv,loss_G_cls,loss_cyc,loss_idt。健康的训练中G和D的损失应该相互震荡而不是一方持续下降另一方持续上升这表示模式崩溃。循环一致性损失应稳步下降并保持在一个较低的值。音频/谱图对比定期如每1000步从验证集中采样生成转换后的梅尔谱图并与源谱图、目标真实谱图进行对比。可以直接在TensorBoard中嵌入音频片段直观地听效果。7.2 常见训练问题与解决方案模式崩溃 (Mode Collapse)生成器发现只生成某一种或几种能骗过判别器的样本导致所有输入都输出极其相似的结果。现象不同源语音、不同目标说话人转换出的声音听起来都一样。排查检查循环一致性损失loss_cyc是否变得异常大检查生成的谱图是否缺乏多样性解决调整损失权重适当增大lambda_cyc如从10调到20加强内容约束。使用梯度惩罚在判别器损失中加入WGAN-GP的梯度惩罚项可以稳定训练。尝试不同的架构在生成器的残差块中使用谱归一化Spectral Norm代替实例归一化。检查数据确保每个说话人的数据量足够且质量均匀。判别器过强判别器过早地完美区分真假样本导致生成器梯度消失无法学习。现象loss_D很快降到接近0loss_G居高不下或变为NaN。解决降低判别器的能力减少判别器的层数或通道数。给判别器添加噪声在判别器的输入中加入高斯噪声。使用标签平滑在训练判别器时将真实样本的标签从1改为0.9~1.0之间的随机数假样本标签从0改为0.0~0.1之间。调整学习率降低判别器的学习率或使用更大的批量大小。生成器过强生成器过早地完美欺骗判别器导致判别器学不到东西。现象loss_G很快降到接近0loss_D居高不下。解决与上一条相反可以增强判别器或降低生成器的学习率。语音质量差有杂音或断断续续检查声码器匹配这是最常见的原因确保梅尔谱图特征提取参数与声码器完全匹配。检查梅尔谱图范围生成器输出的梅尔谱图值域是否在声码器期望的范围内如[0,1]或[-1,1]进行必要的缩放和偏移。检查数据预处理音频中是否有过多的静音或噪声预处理时是否做了音量归一化7.3 主观与客观评估主观评估 (MOS, Mean Opinion Score)最可靠的评估方法。邀请听者对转换语音的自然度、相似度进行打分如1-5分。虽然费时费力但对于最终效果评判至关重要。客观评估梅尔倒谱失真 (MCD)比较转换后的梅尔倒谱与目标真实梅尔倒谱之间的差异。值越小越好。但MCD与主观听感并非完全线性相关。说话人验证相似度使用一个预训练的说话人验证模型如ECAPA-TDNN计算转换语音与目标说话人真实语音的嵌入向量之间的余弦相似度。分数越高说明音色越像。语音识别词错误率 (WER)将转换后的语音用ASR系统识别计算词错误率。如果WER相比源语音大幅上升说明转换过程严重破坏了语音内容信息这是循环一致性损失失效的表现。在我自己的训练过程中最大的体会就是耐心和系统性排查。不要一看到损失曲线不好看就盲目调参。先固定一组经典参数如论文中的参数跑一个baseline。然后用TensorBoard仔细分析问题到底出在哪一步是判别器太强还是循环一致性没起作用或者是数据本身有问题每次只调整一个变量并做好实验记录这样才能高效地找到最优解。