在华为ModelArts上进行Tacotron2实验、部署DiffSinger在线训练与推理

举报
xiyuchen 发表于 2023/10/15 14:50:11 2023/10/15
【摘要】 姓名:席宇辰 学号:1120202444  摘要实验报告记录了我在进行结课作业时完成的所有任务,整理了完成这些任务所需要的必要的基础知识、完成实验过程中搜集的资料,记录了我对一些模型项目代码的改进、重构的详细细节,以及在进行实验中遇到的错误及其修正。实验报告主要包含以下两部分内容,它们将顺序出现在后面的小节中:两个华为架构、模型相关的TTS实验:其中第一个实现使用了华为的机器学习的平台和架构...

姓名:席宇辰 学号:1120202444

 

 

  1. 摘要

实验报告记录了我在进行结课作业时完成的所有任务,整理了完成这些任务所需要的必要的基础知识、完成实验过程中搜集的资料,记录了我对一些模型项目代码的改进、重构的详细细节,以及在进行实验中遇到的错误及其修正。

实验报告主要包含以下两部分内容,它们将顺序出现在后面的小节中:

    1. 两个华为架构、模型相关的TTS实验:其中第一个实现使用了华为的机器学习的平台和架构,第二个实验围绕华为诺亚提出的Grad-TTS模型进行展开。两个实验都涉及到了华为自研内容的部署和实现。除实验内容外还总结了一些MindSpore官方实现中的Tacotron中的疑问和错误。
    2. 歌声合成项目(TTS/SVS):Diffsinger的原理学习、部署和应用。这一部分包含了扩散模型、Diffsinger的实现、部署介绍,以及对语音识别与合成学科分支下的歌声合成相关领域的回顾和总结。

报告的最后是课程和实验总结。

 

  1. 华为实验1:在华为ModelArts上进行Tacotron2实验

在这部分实验中,在ModelArts上完成了Tacotron2在数据集LJSpeech1.1上的训练与推理。在实验过程中发现了教程中可能存在的错误,所以重构了MindSpore官方给出的Tacotron2算法中的代码。同时在官方Tacotron实现的数据预处理部分加入了tar.gz2文件的预处理,使其能够直接接收OBS中的LJSpeech1.1.tar.gz2文件并进行数据集的预处理,提高了代码的鲁棒性。

    1. 华为ModelArts

ModelArts是一个一站式的开发平台,能够支撑开发者从数据到AI应用的全流程开发过程。包含数据处理、模型训练、模型管理、模型部署等操作,并且提供AI Gallery功能,能够在市场内与其他开发者分享模型。 ModelArts支持应用到图像分类、物体检测、视频分析、语音识别、产品推荐、异常检测等多种AI应用场景。

ModelArts的应用结构如下图所示。

ModelArts在本次实验中常用的服务或选项有:

  • codelab:进行非算法的人工预处理、试验
  • 算法的创建与管理:创建后台算法
  • 训练的创建与管理:根据后台算法建立训练事务

ModelArts是华为云服务的一部分,如果需要使用ModelArts进行后台训练的创建与管理,还会用到其他的华为云服务。ModelArts和其他华为云服务之间的关系如下图所示。

其中 OBS服务 在本次实验过程中十分重要,它负责基本的算法存储、数据集存储,还要保存我们的输入输出日志(算法出错时导出报错信息)。

在实验过程中,可以认为OBS是外部存储空间。购买并使用的算法另有硬盘空间。创建训练时,需要进行内容的下载和上传。

其中CPU、GPU是训练时购买,OBS服务需要提前购买。使用ModelArts创建后台算法时需要进行的主要准备由上图所示。

    1. 华为昇思MindSpore

昇思MindSpore是一个全场景深度学习框架,旨在实现易开发、高效执行、全场景覆盖三大目标。 其中,易开发表现为API友好、调试难度低;高效执行包括计算效率、数据预处理效率和分布式训练效率;全场景则指框架同时支持云、边缘以及端侧场景。 昇思MindSpore各个架构模块之间的关系如下图所示:

昇思MindSpore与我们将要使用的ModelArts应用使能之间的关系如下图所示。

我们在实验中使用了MindSpore的AI框架。具体表现在:硬件方面选择 Ascend MindSpore1.70 作为基本库,实验进行的 Tacotron2 模型使用了官方实现的MindSpore版本。具体的服务关系如下图所示。需要注意的是MindSpore是有底层硬件Ascend支持的框架,我们在应用时必须要选择官方的或者自己实现的MindSpore算法。

上图是MindSpore和硬件以及算法之间的基本关系。

    1. Tacotron、Tacotron2模型

      1. Tacotron

语音合成系统通常包含多个阶段,例如 TTS FrontendAcoustic modelVocoder 。构建这些组件通常需要广泛的领域专业知识,并且可能包含脆弱的设计选择。在很多人困扰于繁杂的特征处理的时候,Google推出了 Tacotron,一种从文字直接合成语音的端到端的语音合成模型,虽然在效果上相较于传统方法要好,但是相比 Wavenet 并没有明显的提升(在Tacotron2中得到提升),不过它更重要的意义在于端到端。

Tacotron是第一个真正意义上的端到端TTS模型。

Tacotron的基础架构是Seq2Seq模型,该模型包括编码器,基于注意力的解码器和post-processing net,从高层次上讲,模型将字符作为输入,并生成频谱图,然后将其转换为波形。

模型的总体结构如下图所示。

 

在Tacotron1中,文字经过Pre-net处理之后需要进一步传入到一个名为CBHG的模块之中,形成隐藏表示层。CBHG完成的任务就是从预处理的简单序列之中提取特征,然后再将处理后得到的结果送入到注意力模型之中。注意力模型在所有的解码过程中都有所应用。

Tacotron1的主要结构可以抽象为下图:

作者使用比较简单的 Griffin-Lim 算法来生成最终的波形。由于decoder生成的是梅尔频谱,因此需要转换成linear-scale spectrogram才能使用Griffin-Lim算法,这里作者同样使用 CBHG 来完成这个任务。Geinffin-Lim算法只是一个神经网络的简单替代,由于这一部分的替代,所以Tacotron第一代的意义主要集中在 端到端 ,它在某些情况下的表现甚至不如同期其他TTS方法。

实际上post-processing net中的CBHG是可以被替换成其它模块用来生成其它方案的,比如直接生成waveform,在Tacotron2中,CBHG就被替换为Wavenet来直接生成波形。

      1. Tacotron2

Tacotron2的主要结构如下图所示

Tacotron2模型包括一下两部分:1.具有注意力的循环序列到序列特征预测网络,该网络根据输入字符序列预测梅尔谱帧的序列 2.WaveNet的修改版,可生成以预测的梅尔谱帧为条件的time-domain waveform样本。

它和Tacotron的主要区别在于:

不使用CBHG,而是使用普通的LSTM和Convolution layer;decoder每一步只生成一个frame;增加post-net,即一个5层CNN来精调mel-spectrogram。

Tacotron2的平均意见分数与Ground truth相近,是一个效果很好的端到端的TTS模型。

    1. ModelArts配置

mindSpore通过 .yaml 文件进行输入输出的管理。

Code文件的结构如下所示,它包含了必要的脚本、python代码、配置文件。

tacotron2/
؋── eval.py // 评估条目
؋── generate_hdf5.py // 从数据集中生成hdf5文件
─ ljspeech_config.yaml
── model_utils
│ ؋── config.py // 解析参数
│ ─ device_adapter.py // ModelArts的设备适配器
│ ؋── __init__.py // init 文件
│ ؋── local_adapter.py // 本地适配器
│ └── moxing_adapter.py // ModelArts的Moxing适配器
── README.md // 关于Tacotron2的描述
── requirements.txt // reqired package
脚本
│ ─ run_distribute_train.sh // 启动分布式培训
│ ؋── run_eval.sh // 启动评估
│ └── run_standalone_train.sh // 启动独立培训
── src
│ ؋── callback.py // 用于监控培训的回调
│ ؋── dataset.py // 定义数据集和采样器
│ ؋─ hparams.py // Tacotron2 配置
│ ؋── rnn_cells.py // rnn 单元格实现
│ ؋── rnns.py // lstm 实现与长度掩码
│ ── tacotron2.py // Tacotron2 网络
│ ── 文本
│ │ ── cleaners.py // 干净的文本序列
│ │ ؋── cmudict.py // 定义 cmudict
│ │ ؋── __init__.py // 处理文本序列
││ ؋── numbers.py // 规范化数字
│ │ └── symbols.py // 编码符号
│ └── utils
│ ── audio.py // 提取音频功能
│ └── convert.py // 通过 meanvar 规范化 mel spectrogram
└── train.py // 培训条目

Code文件来自于https://gitee.com/mindspore/models/tree/master/official/audio/Tacotron2#tacotron2-description。它是MindSpore的官方实现。MindSpore架构下还实现了多种语音合成与识别框架。代码中的随机量是官方设置好的,如果修改随机量可能会产生不同的训练效果,在实验中我没有进行随机数的额外设定。

Code文件中需要特别关心的是:

train.py

ljspeech_config.yaml

generate_hdf5.py

这三个文件是要进行特殊设置、调用的,无论是从华为云控制台上进行设置,还是直接修改或者调用。文件需要保存在购买的OBS桶之中,然后记录好路径。算法创建过程中会将这些超参数设置为空,我们需要再创建训练实例时根据路径指明输入输出文件夹在桶中的路径。训练示例运行时会自动从路径中下载内容到cache之中。

    1. 训练设置与步骤

由于我们在ModelArts之中进行的训练,所以这里不需要进行Ascend的配置和MindeSpore的安装。需要注意的是 Codelab 中并不包含MindSpore相关架构,使用的话必须进行配置,并且在我的实践中发现其CPU属性可能是不确定的,在安装Ascend过程中出现了一些错误。所以实验并没有使用bash的方法进行自动训练和推理。

      1. yaml文件配置

首先是训练流程相关的配置信息。需要注意的是,yaml既可以在文件中修改之后上传到OBS之中,也可以在算法设计、训练管理界面使用超参数进行修改。在UI界面中进行超参数的修改会覆盖源文件中的参数设置。

ata_url: "" # set on the page
train_url: "" # set on the page
checkpoint_url: "" # set on the page
# Path for local
data_path: "/cache/data" # download data to data_path from data_url(obs address) 
output_path: "/cache/train" # upload output data from output_path dirs to train_url(obs address)
load_path: "/cache/checkpoint_path" # download checkpoint to load_path from checkpoint_url(obs address)
device_target: "Ascend"
need_modelarts_dataset_unzip: False #这里如果进行解压缩会出现流程问题
modelarts_dataset_unzip_name: "LJSpeech-1.1"

这里是与训练相关的配置信息。

‘pretrain_ckpt": '/path/to/model. ckpt'# use pretrained ckpt at training phase
'model_ckpt': '/path/to/model.ckpt'
# use pretrained ckpt at inference phase
'Ir': 0.002
# initial learning rate
'batch_size': 16
# training batch size
'epoch_num': 2000
# total training epochs
'warmup_epochs': 30
# warmpup lr epochs
'save_ckpt_dir:' './ckpt'
# specify ckpt saving dir
'keep_checkpoint_max': 10
# only keep the last keep_checkpoint_max checkpoint
'text': 'text to synthesize'
'dataset_path': '/dir/to/hdf5'
'data_name': 'ljspeech'
'audioname': 'text2speech'
'run distribute': False
'device_id': 0
# specify text to synthesize at inference
# specify dir to haf5 file
# specify dataset name
# specify filename for generated audio
# whether distributed training
# specify which device to use

基本的配置参照下面的内容:

执行a或b。


a.在[DATASET_NAME]_config.yaml文件上设置“enable_modelarts=True”。
在[DATASET_NAME]_config.yaml文件上设置“dataset_path='/cache/data/[DATASET_NAME]”。
在[DATASET_NAME]_config.yaml文件上设置“data_name='[DATASET_NAME]'”。
(选项)在您需要的[DATASET_NAME]_config.yaml文件上设置其他参数。


b.在网站UI界面上添加“enable_modelarts=True”。
在网站用户界面界面上添加“dataset_path='/cache/data/[DATASET_NAME]”。
在网站UI界面上添加“data_name='[DATASET_NAME]”。
(选项)在UI中设置其他参数。
      1. OBS桶的配置(文件结构配置)

如图进行桶内容配置。其中output、log为空,它们要存储从训练实例中上载得到的文件。

      1. 算法超参数配置

然后进入算法管理界面,这里面我们要设置好算法的路径,配置好相应的超参数及其默认值。这些参数将会直接覆盖掉yaml文件中的参数,如果没有设置,则默认为yaml文件中预先写入的参数,比如路径String。

      1. 算法CPU、框架配置

同样在算法管理界面,我们选择训练引擎和算法框架。

算法引擎: Ascend-Powered-Engine

算法框架: mindspore_1.7.0-cann_5.1.0-py_3.7-euler_2.8.3-aarch64

所以我们将使用Ascend引擎、MindSpore架构进行训练。

      1. 创建训练实例

首先我们选择数据输入路径。

使用相似的方法选择输出路径、日志路径。

最后配置相应的参数,将一个算法实例化。右侧是最终的运行环境结构。

      1. 训练

加载数据并进行训练,数据集大小为13104个文件,它必须是hdf5文件,否则会出现严重错误,本节最后介绍相关错误的修正。

    1. 推理/评估设置与步骤

      1. 关于yaml与超参数的设置如下

执行a或者b


a.在[DATASET_NAME]_config.yaml文件上设置“enable_modelarts=True”。
在[DATASET_NAME]_config.yaml文件上设置“data_name='[DATASET_NAME]'”。
在[DATASET_NAME]_config.yaml文件上设置“model_ckpt='/cache/checkpoint_path/model.ckpt'”。
在[DATASET_NAME]_config.yaml文件上设置“text='text to synthesize'”。
在[DATASET_NAME]_config.yaml文件上设置“checkpoint_url='s3://dir_to_trained_ckpt/'”。
(选项)在您需要的[DATASET_NAME]_config.yaml文件上设置其他参数。


b。在网站UI界面上添加“enable_modelarts=True”。
在网站UI界面上添加“data_name='[DATASET_NAME]”。
在网站UI界面上添加“model_ckpt=/cache/checkpoint_path/model.ckpt”。
在网站UI界面上添加“text='text to synthesize”。
在网站用户界面界面上添加“checkpoint_url='s3://dir_to_trained_ckpt/'”。
(选项)在网站UI界面上添加其他参数。
      1. 算法启动的 .py 程序设置

在这一步我们需要设置启动python程序为 eval.py ,实例化之后训练将执行eval的评估算法。

推理脚本的内容如下所示

    1. 遇到的流程错误与MindSpore代码重构

      1. 错误分析

在MindSpore官方给出的教程中,建议我们上传LJSpeech的ZIP文件。

# run distributed training example
# (1) Add "config_path='/path_to_code/[DATASET_NAME]_config.yaml'" on the website UI interface.
# (2) Perform a or b.
# a. Set "enable_modelarts=True" on [DATASET_NAME]_config.yaml file.
# Set "run_distribute=True" on [DATASET_NAME]_config.yaml file.
# Set "dataset_path='/cache/data/[DATASET_NAME]'" on [DATASET_NAME]_config.yaml file.
# Set "data_name='[DATASET_NAME]'" on [DATASET_NAME]_config.yaml file.
# (option)Set other parameters on [DATASET_NAME]_config.yaml file you need.
# b. Add "enable_modelarts=True" on the website UI interface.
# Add "run_distribute=True" on the website UI interface.
# Add "dataset_path='/cache/data/[DATASET_NAME]'" on the website UI interface.
# Add "data_name='[DATASET_NAME]'" on the website UI interface.
# (option)Add other parameters on the website UI interface.
# (3) Upload a zip dataset to S3 bucket. (you could also upload the origin dataset, but it can be so slow.)
# (4) Set the code directory to "/path/to/tacotron2" on the website UI interface.
# (5) Set the startup file to "train.py" on the website UI interface.
# (6) Set the "Dataset path" and "Output file path" and "Job log path" to your path on the website UI interface.
# (7) Create your job.

其中第(4)点建议我们上传zip文件,不建议我们直接上传数据集。

在实践过程中,我总结了以下几点约束:

  • 3GB的ZIP文件无法在OBS之中进行解压。
  • 如果要上传多达13104个文件的文件夹,必须使用obsutill并建立ssh连接才能正常上传如此大量的小文件

所以我进行了如下设定(在 .yaml 上进行设置是完全类似的,不再赘述):

在训练中出现文件访问错误。堆栈访问到以下函数:

def prepare_dataloaders(dataset_path, rank_id, group_size):
'''prepare dataloaders'''
dataset = ljdataset(dataset_path, group_size)
ds_dataset = ds.GeneratorDataset(dataset,
['text_padded',
'input_lengths',
'mel_padded',
'gate_padded',
'text_mask',
'mel_mask',
'rnn_mask'],
num_parallel_workers=4,
sampler=Sampler(dataset.sample_nums,
rank_id,
group_size))
ds_dataset = ds_dataset.batch(hps.batch_size)
return ds_dataset

其中ljdataset访问dataset_path,并默认访问到的文件时hdf5格式,而教程中并没有特别说明这一点。所以流程之中,如果按照建议的ZIP文件上传数据集,我们必须在解压之后调用hdf5生成的 .py 文件进行数据集预处理。

出现错误的原因是“路径是一个文件夹而非文件”印证了这一事实。

 

      1. 重构代码

为了保证算法能够正常接收我们的ZIP数据集并进行处理,我们在数据集预处理的python代码段进行了重构:

import os
import argparse
import random
import h5py
from tqdm import tqdm
import numpy as np
import librosa
from src.utils.audio import load_wav, melspectrogram
from src.hparams import hparams as hps
from src.text import text_to_sequence
from src.utils import audio
random.seed(0)


def files_to_list(fdir):
''' collect text and filepath to list'''
f_list = []
with open(os.path.join(fdir, 'metadata.csv'), encoding='utf-8') as f:
for line in f:
parts = line.strip().split('|')
wav_path = os.path.join(fdir, 'wavs', '%s.wav' % parts[0])
f_list.append([wav_path, parts[1]])
return f_list


def get_mel_text_pair(filename_and_text):
'''preprocessing mel and text '''
filename, text = filename_and_text[0], filename_and_text[1]
text += '~'
text = get_text(text)
mel = produce_mel_features(filename)
print(mel.shape)
return (text, mel)


def get_text(text):
'''encode text to sequence'''
return text_to_sequence(text, hps.text_cleaners)


def get_mel(filename):
'''extract mel spectrogram'''
wav = load_wav(filename)
trim_wav, _ = librosa.effects.trim(
wav, top_db=60, frame_length=2048, hop_length=512)
wav = np.concatenate(
(trim_wav,
np.zeros(
(5 * hps.hop_length),
np.float32)),
0)
mel = melspectrogram(wav).astype(np.float32)
return mel


def produce_mel_features(filename):
'''produce Mel-Frequency features'''
wav, fs = librosa.load(filename, sr=22050)
wav = librosa.resample(wav, fs, 16000)
# between audio and mel-spectrogram
wav = audio.wav_padding(wav, hps)
assert len(wav) % hps.hop_size == 0
# Pre-emphasize
preem_wav = audio.preemphasis(wav, hps.preemphasis, hps.preemphasize)
# Compute the mel scale spectrogram from the wav
mel_spectrogram = audio.mel_spectrogram(preem_wav, hps).astype(np.float32)
mel = (mel_spectrogram + hps.max_abs_value) / (2 * hps.max_abs_value)
return mel.astype(np.float32)


def generate_hdf5_derictcall(fdir):
'''generate hdf5 file'''
f_list = files_to_list(fdir)
random.shuffle(f_list)
max_text, max_mel = 0, 0
for idx, filename_and_text in tqdm(enumerate(f_list)):
text, mel = get_mel_text_pair(filename_and_text)
max_text = max(max_text, len(text))
max_mel = max(max_mel, mel.shape[1])
with h5py.File('ljdataset.hdf5', 'a') as hf:
hf.create_dataset('{}_mel'.format(idx), data=mel)
hf.create_dataset('{}_text'.format(idx), data=text)

 

def prepare_dataloaders(dataset_path, rank_id, group_size):
'''prepare dataloaders'''
generate_hdf5_derictcall(dataset_path) #生成hdf5文件
dataset = ljdataset(dataset_path, group_size)
ds_dataset = ds.GeneratorDataset(dataset,
['text_padded',
'input_lengths',
'mel_padded',
'gate_padded',
'text_mask',
'mel_mask',
'rnn_mask'],
num_parallel_workers=4,
sampler=Sampler(dataset.sample_nums,
rank_id,
group_size))
ds_dataset = ds_dataset.batch(hps.batch_size)
return ds_dataset

另外,官方给出的代码只能处理ZIP数据集

而LJSpeech1.1的压缩格式为 tar.gz2

def modelarts_pre_process():
'''modelarts pre process function.'''
def unzip(zip_file, save_dir):
import zipfile
s_time = time.time()
if not os.path.exists(os.path.join(save_dir, config.modelarts_dataset_unzip_name)):
zip_isexist = zipfile.is_zipfile(zip_file)
if zip_isexist:
fz = zipfile.ZipFile(zip_file, 'r')
data_num = len(fz.namelist())
print("Extract Start...")
print("unzip file num: {}".format(data_num))
data_print = int(data_num / 100) if data_num > 100 else 1
i = 0
for file in fz.namelist():
if i % data_print == 0:
print("unzip percent: {}%".format(int(i * 100 / data_num)), flush=True)
i += 1
fz.extract(file, save_dir)
print("cost time: {}min:{}s.".format(int((time.time() - s_time) / 60),
int(int(time.time() - s_time) % 60)))
print("Extract Done.")
else:
print(zip_file, " is not zip.")
else:
print("Zip has been extracted.")
if config.need_modelarts_dataset_unzip:
zip_file_1 = os.path.join(config.data_path, config.modelarts_dataset_unzip_name + ".zip")
save_dir_1 = os.path.join(config.data_path)
sync_lock = "/tmp/unzip_sync.lock"
# Each server contains 8 devices as most.
if get_device_id() % min(get_device_num(), 8) == 0 and not os.path.exists(sync_lock):
print("Zip file path: ", zip_file_1)
print("Unzip file save dir: ", save_dir_1)
unzip(zip_file_1, save_dir_1)
print("===Finish extract data synchronization===")
try:
os.mknod(sync_lock)
except IOError:
pass
while True:
if os.path.exists(sync_lock):
break
time.sleep(1)
print("Device: {}, Finish sync unzip data from {} to {}.".format(get_device_id(), zip_file_1, save_dir_1))
config.save_ckpt_dir = config.save_ckpt_dir

官方给出的实现方法关键代码是:

path = config.modelarts_dataset_unzip_name + ".zip"

代码默认了文件必须是zip格式。所以这一实现对于LJSpeech官方给出的压缩格式并不能直接解压,需要我们进行二次压缩之后在进行上传。我重构了unzip的代码,使用了 tarfile python库进行解压,保证了没有二次压缩的出现,减少了工作量。

      1. 使用CodeLab直接生成hdf5文件

除了重构代码的方法之外,还可以通过Codelab直接从对象存储桶中读取文件,解压缩然后生成hdf5预处理文件。

生成完毕后,通过mox将文件下载会OBS之中再进行训练。

import moxing as mox
mox.file.copy('/home/ma-user/work/xxx.hdf5', 'obs://obsname/xxx.hdf5)
  1. 华为实验2:华为诺亚自研Grad-TTS模型

    1. 基于Glow-TTS实现基本的华为Grad-TTS模型

      1. Decoder替换

Grad-TTS,是一种新颖的文本到语音模型,具有基于分数的解码器,通过逐渐转换编码器预测的噪声并通过单调对齐搜索与文本输入对齐来生成梅尔频谱。随机微分方程的框架帮助Grad-TTS将传统的扩散概率模型推广到从具有不同参数的噪声中重建数据的情况,并允许通过明确控制声音质量和推理速度之间的权衡来使这种重建变得灵活。

考虑Glow-TTS的模型:

其中,将Glow-TTS的解码器替换为Diffusion解码器可以实现基本的Grad-TTS架构。

DiffusionDecoder的部分核心代码:

class DiffusionDecoder(nn.Module):
  def __init__(self, 
      unet_channels=64,
      unet_in_channels=2,
      unet_out_channels=1,
      dim_mults=(1, 2, 4),
      groups=8,
      with_time_emb=True,
      beta_0=0.05,
      beta_1=20,
      N=1000,
      T=1):
    super().__init__()
    self.beta_0 = beta_0
    self.beta_1 = beta_1
    self.N = N
    self.T = T
    self.delta_t = T*1.0 / N
    self.discrete_betas = torch.linspace(beta_0, beta_1, N)
    self.unet = unet.Unet(dim=unet_channels, out_dim=unet_out_channels, dim_mults=dim_mults, groups=groups, channels=unet_in_channels, with_time_emb=with_time_emb)
  def marginal_prob(self, mu, x, t):
    log_mean_coeff = -0.25 * t ** 2 * (self.beta_1 - self.beta_0) - 0.5 * t * self.beta_0
    mean = torch.exp(log_mean_coeff[:, None, None]) * x + (1-torch.exp(log_mean_coeff[:, None, None]) ) * mu
    std = torch.sqrt(1. - torch.exp(2. * log_mean_coeff))
    return mean, std
  def cal_loss(self, x, mu, t, z, std, g=None):
    time_steps = t * (self.N - 1)
    if g:
        x = torch.stack([x, mu, g], 1)
    else:
        x = torch.stack([x, mu], 1)
    grad = self.unet(x, time_steps)
    loss = torch.square(grad + z / std[:, None, None]) * torch.square(std[:, None, None])
    return loss
  def forward(self, mu, y=None, g=None, gen=False):
    if not gen:
      t = torch.FloatTensor(y.shape[0]).uniform_(0, self.T-self.delta_t).to(y.device)+self.delta_t  # sample a random t
      mean, std = self.marginal_prob(mu, y, t)
      z = torch.randn_like(y)
      x = mean + std[:, None, None] * z
      loss = self.cal_loss(x, mu, t, z, std, g)
      return loss
    else:
      with torch.no_grad():
        y_T = torch.randn_like(mu) + mu
        y_t_plus_one = y_T
        y_t = None
        for n in tqdm(range(self.N - 1, 0, -1)):
          t = torch.FloatTensor(1).fill_(n).to(mu.device)
          if g:
              x = torch.stack([y_t_plus_one, mu, g], 1)
          else:
              x = torch.stack([y_t_plus_one, mu], 1)
          grad = self.unet(x, t)
          y_t = y_t_plus_one-0.5*self.delta_t*self.discrete_betas[n]*(mu-y_t_plus_one-grad)
          y_t_plus_one = y_t
      return y_t      

而在Glow-TTS中,解码器的核心代码为:

class FlowSpecDecoder(nn.Module):
  def __init__(self, 
      in_channels, 
      hidden_channels, 
      kernel_size, 
      dilation_rate, 
      n_blocks, 
      n_layers, 
      p_dropout=0., 
      n_split=4,
      n_sqz=2,
      sigmoid_scale=False,
      gin_channels=0):
    super().__init__()
    self.in_channels = in_channels
    self.hidden_channels = hidden_channels
    self.kernel_size = kernel_size
    self.dilation_rate = dilation_rate
    self.n_blocks = n_blocks
    self.n_layers = n_layers
    self.p_dropout = p_dropout
    self.n_split = n_split
    self.n_sqz = n_sqz
    self.sigmoid_scale = sigmoid_scale
    self.gin_channels = gin_channels
    self.flows = nn.ModuleList()
    for b in range(n_blocks):
      self.flows.append(modules.ActNorm(channels=in_channels * n_sqz))
      self.flows.append(modules.InvConvNear(channels=in_channels * n_sqz, n_split=n_split))
      self.flows.append(
        attentions.CouplingBlock(
          in_channels * n_sqz,
          hidden_channels,
          kernel_size=kernel_size, 
          dilation_rate=dilation_rate,
          n_layers=n_layers,
          gin_channels=gin_channels,
          p_dropout=p_dropout,
          sigmoid_scale=sigmoid_scale))
  def forward(self, x, x_mask, g=None, reverse=False):
    if not reverse:
      flows = self.flows
      logdet_tot = 0
    else:
      flows = reversed(self.flows)
      logdet_tot = None
    if self.n_sqz > 1:
      x, x_mask = commons.squeeze(x, x_mask, self.n_sqz)
    for f in flows:
      if not reverse:
        x, logdet = f(x, x_mask, g=g, reverse=reverse)
        logdet_tot += logdet
      else:
        x, logdet = f(x, x_mask, g=g, reverse=reverse)
    if self.n_sqz > 1:
      x, x_mask = commons.unsqueeze(x, x_mask, self.n_sqz)
    return x, logdet_tot
  def store_inverse(self):
    for f in self.flows:
      f.store_inverse()
    1. 官方实现的Grad-TTS模型

      1. 华为诺亚的官方实现

代码来源:https://github.com/huawei-noah/Speech-Backbones/tree/main/Grad-TTS

class Diffusion(BaseModule):
    def __init__(self, n_feats, dim,
                 n_spks=1, spk_emb_dim=64,
                 beta_min=0.05, beta_max=20, pe_scale=1000):
        super(Diffusion, self).__init__()
        self.n_feats = n_feats
        self.dim = dim
        self.n_spks = n_spks
        self.spk_emb_dim = spk_emb_dim
        self.beta_min = beta_min
        self.beta_max = beta_max
        self.pe_scale = pe_scale
        
        self.estimator = GradLogPEstimator2d(dim, n_spks=n_spks,
                                             spk_emb_dim=spk_emb_dim,
                                             pe_scale=pe_scale)
    def forward_diffusion(self, x0, mask, mu, t):
        time = t.unsqueeze(-1).unsqueeze(-1)
        cum_noise = get_noise(time, self.beta_min, self.beta_max, cumulative=True)
        mean = x0*torch.exp(-0.5*cum_noise) + mu*(1.0 - torch.exp(-0.5*cum_noise))
        variance = 1.0 - torch.exp(-cum_noise)
        z = torch.randn(x0.shape, dtype=x0.dtype, device=x0.device, 
                        requires_grad=False)
        xt = mean + z * torch.sqrt(variance)
        return xt * mask, z * mask
    @torch.no_grad()
    def reverse_diffusion(self, z, mask, mu, n_timesteps, stoc=False, spk=None):
        h = 1.0 / n_timesteps
        xt = z * mask
        for i in range(n_timesteps):
            t = (1.0 - (i + 0.5)*h) * torch.ones(z.shape[0], dtype=z.dtype, 
                                                 device=z.device)
            time = t.unsqueeze(-1).unsqueeze(-1)
            noise_t = get_noise(time, self.beta_min, self.beta_max, 
                                cumulative=False)
            if stoc:  # adds stochastic term
                dxt_det = 0.5 * (mu - xt) - self.estimator(xt, mask, mu, t, spk)
                dxt_det = dxt_det * noise_t * h
                dxt_stoc = torch.randn(z.shape, dtype=z.dtype, device=z.device,
                                       requires_grad=False)
                dxt_stoc = dxt_stoc * torch.sqrt(noise_t * h)
                dxt = dxt_det + dxt_stoc
            else:
                dxt = 0.5 * (mu - xt - self.estimator(xt, mask, mu, t, spk))
                dxt = dxt * noise_t * h
            xt = (xt - dxt) * mask
        return xt
    @torch.no_grad()
    def forward(self, z, mask, mu, n_timesteps, stoc=False, spk=None):
        return self.reverse_diffusion(z, mask, mu, n_timesteps, stoc, spk)
    def loss_t(self, x0, mask, mu, t, spk=None):
        xt, z = self.forward_diffusion(x0, mask, mu, t)
        time = t.unsqueeze(-1).unsqueeze(-1)
        cum_noise = get_noise(time, self.beta_min, self.beta_max, cumulative=True)
        noise_estimation = self.estimator(xt, mask, mu, t, spk)
        noise_estimation *= torch.sqrt(1.0 - torch.exp(-cum_noise))
        loss = torch.sum((noise_estimation + z)**2) / (torch.sum(mask)*self.n_feats)
        return loss, xt
    def compute_loss(self, x0, mask, mu, spk=None, offset=1e-5):
        t = torch.rand(x0.shape[0], dtype=x0.dtype, device=x0.device,
                       requires_grad=False)
        t = torch.clamp(t, offset, 1.0 - offset)
        return self.loss_t(x0, mask, mu, t, spk)
      1. 训练与推理

训练模型使用的数据集为LJSpeech。本部分的推理过程由华为诺亚官方创建的笔记本在Colab上运行测试。Colab中笔记本的关键代码如下记录:

generator = GradTTS(len(symbols)+1, N_SPKS, params.spk_emb_dim, params.n_enc_channels, params.filter_channels,
                    params.filter_channels_dp, params.n_heads, params.n_enc_layers,
                    params.enc_kernel, params.enc_dropout, params.window_size,
                    params.n_feats, params.dec_dim, params.beta_min, params.beta_max,
                    pe_scale=1000)  # pe_scale=1 for `grad-tts-old.pt`
generator.load_state_dict(torch.load('./Grad-TTS/checkpts/grad-tts-libri-tts.pt', map_location=lambda loc, storage: loc))
_ = generator.cuda().eval()
cmu = cmudict.CMUDict('./Grad-TTS/resources/cmu_dictionary')

准备HIFI-GAN vocoder

with open('./Grad-TTS/checkpts/hifigan-config.json') as f:
    h = AttrDict(json.load(f))
hifigan = HiFiGAN(h)
hifigan.load_state_dict(torch.load('./Grad-TTS/checkpts/hifigan.pt',
                                   map_location=lambda loc, storage: loc)['generator'])
_ = hifigan.cuda().eval()
hifigan.remove_weight_norm()
%matplotlib inline

准备测试文本,测试内容为“Here are the match lineups for the Colombia Haiti match.”

text = "Here are the match lineups for the Colombia Haiti match."
x = torch.LongTensor(intersperse(text_to_sequence(text, dictionary=cmu), len(symbols))).cuda()[None]
x_lengths = torch.LongTensor([x.shape[-1]]).cuda()
x.shape, x_lengths

进行推理,推理后使用plt输出频谱图。

SPEAKER_ID = 15
t = dt.datetime.now()
y_enc, y_dec, attn = generator.forward(x, x_lengths, n_timesteps=10, temperature=1.5,
                                       stoc=False, spk=torch.LongTensor([SPEAKER_ID]).cuda() if N_SPKS > 1 else None,
                                       length_scale=0.91)
t = (dt.datetime.now() - t).total_seconds()
print(f'Grad-TTS RTF: {t * 22050 / (y_dec.shape[-1] * 256)}')
plt.figure(figsize=(15, 4))
plt.subplot(1, 3, 1)
plt.title('Encoder outputs')
plt.imshow(y_enc.cpu().squeeze(), aspect='auto', origin='lower')
plt.colorbar()
plt.subplot(1, 3, 2)
plt.title('Decoder outputs')
plt.imshow(y_dec.cpu().squeeze(), aspect='auto', origin='lower')
plt.colorbar()
plt.subplot(1, 3, 3)
plt.title('Alignment')
plt.imshow(attn.cpu().squeeze(), aspect='auto', origin='lower');

输出频谱图、对如下:

 

  1. 歌声合成实践(TTS&SVS)

    1. 歌声合成(SVS)与TTS(TTS&SVS)

歌声合成是TTS的一种,或者说一个相似的分支,他们的基本原理、模型结构相同。不同的是歌声合成的输入更加复杂,需要音素、音长、音高、微调参数等多种输入,它的输出是人类的歌声(不包括背景乐器),我们将歌曲中的纯人声部分称之为 干声 ,DiffSinger的输出就是 干声

歌声合成 (SVS) 旨在从乐谱中合成⾃然且富有表现⼒的歌声 (Wu and Luan 2020),越来越受到研究界和娱乐⾏业的关注 (Zhang et al. 2020)。 SVS 的管道通常包括⼀个声学模型,⽤于⽣成以乐谱为条件的声学特征(例如,梅尔频谱),以及⼀个将声学特征转换为波形的声码器(Nakamura 等⼈,2016 年)。

在已有的SVS模型中,一个重要的前提假设是 单峰分布 ,在这一前提假设下,会导致歌声的输出结果过于模糊和平滑。一些研究使用对抗生成网络来试图解决这一问题,但是GAN具有不稳定性,在产生的歌曲中会产生不自然的过渡

实际上,由于SVS的pipeline和TTS极其相近(或者说他们都是文本到声音的工作,只不过SVS的输入有预先加入的音乐四要素),所以DiffSinger还衍生出了DiffSpeech,由于原理几乎完全一致,所以后文不再赘述。

    1. 马尔科夫链与扩散模型

这一部分简单介绍了扩散模型。介绍是围绕着计算机视觉领域的问题展开的,但是扩散模型的本质可以在本节中得到基本的介绍。

扩散模型的灵感来自非平衡热力学。他们定义了一个扩散步骤的马尔可夫链,以缓慢地将随机噪声添加到数据中,然后学习反转扩散过程以从噪声中构建所需的数据样本。与 VAE 或流模型不同,扩散模型是通过固定过程学习的,并且潜在变量具有高维性(与原始数据相同)。

已经提出了几种基于扩散的生成模型,这些模型具有相似的思想,包括扩散概率模型(Sohl-Dickstein 等人,2015 年)、噪声条件评分网络(NCSN;Yang 和 Ermon,2019 年)和去噪扩散概率模型(DDPM;Ho 等人,2020 年)

DDPM包括两个过程:前向过程(forward process)和反向过程(reverse process),其中前向过程又称为扩散过程(diffusion process),如下图所示。无论是前向过程还是反向过程都是一个参数化的马尔可夫链(Markov chain),其中反向过程可以用来生成图片。

DDPM前向过程(扩散过程)。一句话概括,前向过程就是对原始图片x0不断加高斯噪声最后生成随机噪声xT的过程。前向过程是将原始图片变成随机噪声,而反向过程就是通过预测噪声 ϵ,将随机噪声 xT 逐步还原为原始图片 x0 ,如下图所示。

基于马尔可夫链的前向过程,其每一个epoch的逆过程都可以近似为高斯分布

 

上图是扩散模型的另一个示意,是对瑞士卷的建模与应用。

最终,扩散模型的采样和训练算法基本结构如下:

优点:易处理性和灵活性是生成建模中两个相互冲突的目标。易处理的模型可以进行分析评估并廉价地拟合数据(例如通过高斯或拉普拉斯),但它们不能轻易地描述丰富数据集中的结构。灵活的模型可以适应数据中的任意结构,但评估、训练或从这些模型中抽样通常是昂贵的。扩散模型既易于分析又灵活

缺点:扩散模型依赖于扩散步骤的长马尔可夫链来生成样本,因此在时间和计算方面可能非常昂贵。已经提出了新的方法来使过程更快,但采样仍然比 GAN 慢。

目前,扩散模型在AI绘图、CV等各个领域产生了巨大的推动影响。接下来要介绍并进行代码复现重写的DiffSinger&DiffSpeech就是扩散模型在语音合成与识别领域的一个应用之一。


    1. DiffSinger的实现

      1. Diffsinger与扩散模型

DIffSinger是一个基于扩散模型的歌声合成模型,他可以将背景噪声转化为梅尔频谱(当然,这一转换必须要输入一个基于严格规则的、保存了丰富的乐曲信息的乐谱)。 DiffSinger 中的 Diff 指的是 Diffusion

DiffSinger 是⼀个参数化的 ⻢尔可夫链 ,它迭代地将噪声转换为以乐谱为条件的梅尔谱图。通过隐式优化变分界,DiffSinger 可以得到稳定的训练并⽣成逼真的输出。DiffSinger 根据真实梅尔谱图的扩散轨迹与简单梅尔频谱解码器预测的扩散轨迹的交集,以⼩于扩散步骤总数的浅步开始⽣成。

DiffSinger使用了扩散模型,本质上是一个参数化的马尔科夫链。DiffSinger还提出了 浅层扩散 机制,这一机制大幅提升了训练和推理速度,使得扩散模型在TTS&SVS领域的应用能够在可控的时间、算力成本之中进行。浅层扩散机制的介绍将在下面一个小节进行。

上图是扩散模型在DiffSinger中的应用示意图。可以看到,DiffSinger在一个Ground Truth的频谱图上开始训练,逐渐向其中加入噪声,然后利用机器学习的方法学习如何从噪声之中还原梅尔频谱。DiffSinger使用了基于 前馈Transformer 的传统声学模型。

用一种直觉化的语言来重新概括:

扩散模型在破坏、拆解有序的梅尔频谱过程中学习有序梅尔频谱的结构与规律,这就像儿童拆解玩具来了解玩具的内部结构一样——扩散模型中所谓的破坏是向梅尔频谱中人为添加噪声来破坏其内部的有序性。

      1. 浅层扩散机制(Shallow Diffusion)

DiffSinger能够将扩散模型引入到TTS&SVS领域的关键工作之一是提出了浅层扩散机制。

朴素的DiffSinger的反向过程是从高斯白噪声开始的。这意味着每一次反向过程都需要从无到有地、完整地构建一个梅尔频谱——从算力的角度来说,这是难以接受的。尽管如此,采用这种反向方法的DiffSinger仍然提供了大量的具有参考价值的数据。

观察朴素DiffSinger产生的这些样本:

我们通过肉眼观察上面扩散进程中的不同阶段,其中最左侧是GroundTruth的梅尔频谱,最右侧是几乎看不到任何有效信息的高斯噪声化的梅尔频谱。

在GroundTruth的梅尔频谱图之中,相邻谐波之间有着丰富的细节,如果这些细节发生了任何丢失,都可以被人耳轻易地察觉到——这对于歌声合成是致命的(当然,如果听众已经接受了传统的UTAU、Vocaloid的歌声合成模式,这种细节的丢失可能会被听众认为是一种特色),尤其是当我们想要合成听起来是真人唱出来的干声时。

随着t的增加,我们可以发现,两个梅尔频谱之间的差异逐渐减小。在t = 50时,我们已经很难看出两个梅尔频谱噪声化之后的差异,而当t = 75之后,两个梅尔频谱之间的差异几乎无法分辨。

直觉的来说,如果从第75次之后开始,所有的梅尔频谱添加噪声后都不会有太大的变化,那么对于神经网络来说,它的在这些几乎没有什么变化的噪声化的梅尔频谱之间的学习没有过多的意义(或者说,如果反向生成梅尔频谱的过程中,开始的一段总会生成相似的内容,不如我们直接让扩散模型的反向起点设置为t = 50t = 75,因为这之后的学习没有产生任何差异化的内容)。

图解这一思路如下:

所以,DiffSinger的扩散模型的反向过程不是从一个完全高斯噪声化的梅尔频谱开始的,而是从一个相对规律(但几乎仍然是噪声)的梅尔频谱出发,然后产生不同的、符合目标的梅尔频谱。这一机制被称为浅层扩散机制。

浅层扩散的机制在性能优化方面起到了巨大的作用,降低了DiffSinger的扩散深度、推理和训练时长。

实际上,DiffSinger的浅层扩散机制不仅提升了推理和训练速度,还提高了听众的评分。我们可以以一种直觉的方式理解:

如果扩散必须深入到纯粹的高斯噪声才停止,那么学习过程中的相当大的一部分会是在几乎看不到任何规律的噪声之中进行,神经网络在这部分的学习过程中有可能出现各种各样的过拟合、抖动等问题,以至于最终会影响神经网络在真正差异化的部分的学习和表现。浅层扩散机制将神经网络从学不到太多的次纯噪声阶段中解放出来,让神经网络在差异化的部分进行学习,使得神经网络学习的内容减少、学习的质量上升。

浅层扩散机制的引入大幅提升了DiffSinger的表现。在平均评分中非常接近Ground Truth,高于目前的GAN、FFT等方法。

      1. 项目代码复现

DiffSinger的基本结构如下图所示

模型包括一个编码器。它需要对三部分内容进行编码:歌词、时长、音高。这对于一个歌声合成模型是必备的,歌声不仅包括歌词,还需要包括每一个汉字的时间长度、每一个汉字的音高。一些模型还支持MIDI的输入。模型还包括辅助解码器、降噪器、边界预测器。其中边界预测器是针对我们前面讲到的浅层扩散机制所设计的,它的功能就是确定扩散模型的深度。

模型的训练器算法:

模型的推理算法:

在本项目中,我们重现了DiffSinger中的一部分关键代码。

DiffSinger中使用了Opencpop数据集,下面是数据集预处理程序

class OpencpopBinarizer(MidiSingingBinarizer):
    def split_train_test_set(self, item_names):
        item_names = deepcopy(item_names)
        test_item_names = [x for x in item_names if any([x.startswith(ts) for ts in hparams['test_prefixes']])]
        train_item_names = [x for x in item_names if x not in set(test_item_names)]
        logging.info("train {}".format(len(train_item_names)))
        logging.info("test {}".format(len(test_item_names)))
        return train_item_names, test_item_names
    def load_meta_data(self, processed_data_dir, ds_id):
        from preprocessing.opencpop import File2Batch
        self.items = File2Batch.file2temporary_dict()

 

nsf_hifigan vocoder

import os
import torch
from modules.nsf_hifigan.models import load_model
from modules.nsf_hifigan.nvSTFT import load_wav_to_torch, STFT
from src.vocoders.base_vocoder import BaseVocoder, register_vocoder
from utils.hparams import hparams


@register_vocoder
class NsfHifiGAN(BaseVocoder):
    def __init__(self, device=None):
        if device is None:
            device = 'cuda' if torch.cuda.is_available() else 'cpu'
        self.device = device
        model_path = hparams['vocoder_ckpt']
        assert os.path.exists(model_path), 'HifiGAN model file is not found!'
        print('| Load HifiGAN: ', model_path)
        self.model, self.h = load_model(model_path, device=self.device)
    def spec2wav_torch(self, mel, **kwargs):  # mel: [B, T, bins]
        if self.h.sampling_rate != hparams['audio_sample_rate']:
            print('Mismatch parameters: hparams[\'audio_sample_rate\']=', hparams['audio_sample_rate'], '!=',
                  self.h.sampling_rate, '(vocoder)')
        if self.h.num_mels != hparams['audio_num_mel_bins']:
            print('Mismatch parameters: hparams[\'audio_num_mel_bins\']=', hparams['audio_num_mel_bins'], '!=',
                  self.h.num_mels, '(vocoder)')
        if self.h.n_fft != hparams['fft_size']:
            print('Mismatch parameters: hparams[\'fft_size\']=', hparams['fft_size'], '!=', self.h.n_fft, '(vocoder)')
        if self.h.win_size != hparams['win_size']:
            print('Mismatch parameters: hparams[\'win_size\']=', hparams['win_size'], '!=', self.h.win_size,
                  '(vocoder)')
        if self.h.hop_size != hparams['hop_size']:
            print('Mismatch parameters: hparams[\'hop_size\']=', hparams['hop_size'], '!=', self.h.hop_size,
                  '(vocoder)')
        if self.h.fmin != hparams['fmin']:
            print('Mismatch parameters: hparams[\'fmin\']=', hparams['fmin'], '!=', self.h.fmin, '(vocoder)')
        if self.h.fmax != hparams['fmax']:
            print('Mismatch parameters: hparams[\'fmax\']=', hparams['fmax'], '!=', self.h.fmax, '(vocoder)')
        with torch.no_grad():
            c = mel.transpose(2, 1)  # [B, T, bins]
            # log10 to log mel
            c = 2.30259 * c
            f0 = kwargs.get('f0')  # [B, T]
            if f0 is not None and hparams.get('use_nsf'):
                y = self.model(c, f0).view(-1)
            else:
                y = self.model(c).view(-1)
        return y
    def spec2wav(self, mel, **kwargs):
        if self.h.sampling_rate != hparams['audio_sample_rate']:
            print('Mismatch parameters: hparams[\'audio_sample_rate\']=', hparams['audio_sample_rate'], '!=',
                  self.h.sampling_rate, '(vocoder)')
        if self.h.num_mels != hparams['audio_num_mel_bins']:
            print('Mismatch parameters: hparams[\'audio_num_mel_bins\']=', hparams['audio_num_mel_bins'], '!=',
                  self.h.num_mels, '(vocoder)')
        if self.h.n_fft != hparams['fft_size']:
            print('Mismatch parameters: hparams[\'fft_size\']=', hparams['fft_size'], '!=', self.h.n_fft, '(vocoder)')
        if self.h.win_size != hparams['win_size']:
            print('Mismatch parameters: hparams[\'win_size\']=', hparams['win_size'], '!=', self.h.win_size,
                  '(vocoder)')
        if self.h.hop_size != hparams['hop_size']:
            print('Mismatch parameters: hparams[\'hop_size\']=', hparams['hop_size'], '!=', self.h.hop_size,
                  '(vocoder)')
        if self.h.fmin != hparams['fmin']:
            print('Mismatch parameters: hparams[\'fmin\']=', hparams['fmin'], '!=', self.h.fmin, '(vocoder)')
        if self.h.fmax != hparams['fmax']:
            print('Mismatch parameters: hparams[\'fmax\']=', hparams['fmax'], '!=', self.h.fmax, '(vocoder)')
        with torch.no_grad():
            c = torch.FloatTensor(mel).unsqueeze(0).transpose(2, 1).to(self.device)
            # log10 to log mel
            c = 2.30259 * c
            f0 = kwargs.get('f0')
            if f0 is not None and hparams.get('use_nsf'):
                f0 = torch.FloatTensor(f0[None, :]).to(self.device)
                y = self.model(c, f0).view(-1)
            else:
                y = self.model(c).view(-1)
        wav_out = y.cpu().numpy()
        return wav_out
    @staticmethod
    def wav2spec(inp_path, device=None):
        if device is None:
            device = 'cuda' if torch.cuda.is_available() else 'cpu'
        sampling_rate = hparams['audio_sample_rate']
        num_mels = hparams['audio_num_mel_bins']
        n_fft = hparams['fft_size']
        win_size = hparams['win_size']
        hop_size = hparams['hop_size']
        fmin = hparams['fmin']
        fmax = hparams['fmax']
        stft = STFT(sampling_rate, num_mels, n_fft, win_size, hop_size, fmin, fmax)
        with torch.no_grad():
            wav_torch, _ = load_wav_to_torch(inp_path, target_sr=stft.target_sr)
            mel_torch = stft.get_mel(wav_torch.unsqueeze(0).to(device)).squeeze(0).T
            # log mel to log10 mel
            mel_torch = 0.434294 * mel_torch
            return wav_torch.cpu().numpy(), mel_torch.cpu().numpy()

 

Diffusion类,扩散模型的核心结构

class GaussianDiffusion(nn.Module):
    def __init__(self, phone_encoder, out_dims, denoise_fn,
                 timesteps=1000, K_step=1000, loss_type=hparams.get('diff_loss_type', 'l1'), betas=None, spec_min=None,
                 spec_max=None):
        super().__init__()
        self.denoise_fn = denoise_fn
        if hparams.get('use_midi') is not None and hparams['use_midi']:
            self.fs2 = FastSpeech2MIDI(phone_encoder, out_dims)
        else:
            #self.fs2 = FastSpeech2(phone_encoder, out_dims)
            self.fs2 = ParameterEncoder(phone_encoder)
        self.mel_bins = out_dims
        if exists(betas):
            betas = betas.detach().cpu().numpy() if isinstance(betas, torch.Tensor) else betas
        else:
            if 'schedule_type' in hparams.keys():
                betas = beta_schedule[hparams['schedule_type']](timesteps)
            else:
                betas = cosine_beta_schedule(timesteps)
        alphas = 1. - betas
        alphas_cumprod = np.cumprod(alphas, axis=0)
        alphas_cumprod_prev = np.append(1., alphas_cumprod[:-1])
        timesteps, = betas.shape
        self.num_timesteps = int(timesteps)
        self.K_step = K_step
        self.loss_type = loss_type
        self.noise_list = deque(maxlen=4)
        to_torch = partial(torch.tensor, dtype=torch.float32)
        self.register_buffer('betas', to_torch(betas))
        self.register_buffer('alphas_cumprod', to_torch(alphas_cumprod))
        self.register_buffer('alphas_cumprod_prev', to_torch(alphas_cumprod_prev))
        # calculations for diffusion q(x_t | x_{t-1}) and others
        self.register_buffer('sqrt_alphas_cumprod', to_torch(np.sqrt(alphas_cumprod)))
        self.register_buffer('sqrt_one_minus_alphas_cumprod', to_torch(np.sqrt(1. - alphas_cumprod)))
        self.register_buffer('log_one_minus_alphas_cumprod', to_torch(np.log(1. - alphas_cumprod)))
        self.register_buffer('sqrt_recip_alphas_cumprod', to_torch(np.sqrt(1. / alphas_cumprod)))
        self.register_buffer('sqrt_recipm1_alphas_cumprod', to_torch(np.sqrt(1. / alphas_cumprod - 1)))
        # calculations for posterior q(x_{t-1} | x_t, x_0)
        posterior_variance = betas * (1. - alphas_cumprod_prev) / (1. - alphas_cumprod)
        # above: equal to 1. / (1. / (1. - alpha_cumprod_tm1) + alpha_t / beta_t)
        self.register_buffer('posterior_variance', to_torch(posterior_variance))
        # below: log calculation clipped because the posterior variance is 0 at the beginning of the diffusion chain
        self.register_buffer('posterior_log_variance_clipped', to_torch(np.log(np.maximum(posterior_variance, 1e-20))))
        self.register_buffer('posterior_mean_coef1', to_torch(
            betas * np.sqrt(alphas_cumprod_prev) / (1. - alphas_cumprod)))
        self.register_buffer('posterior_mean_coef2', to_torch(
            (1. - alphas_cumprod_prev) * np.sqrt(alphas) / (1. - alphas_cumprod)))
        self.register_buffer('spec_min', torch.FloatTensor(spec_min)[None, None, :hparams['keep_bins']])
        self.register_buffer('spec_max', torch.FloatTensor(spec_max)[None, None, :hparams['keep_bins']])

    1. 歌声合成的推理输入子问题

音乐基本要素是指构成音乐的各种元素,包括音的高低,音的长短,音的强弱和音色。所以一个SVS模型产生悦耳的干声必须要将音乐要素包含在文本数据之中。实际上SVS和TTS的主要差距就在于此,TTS合成时只需要指定文本。

    1. 训练

我们使用了Opencpop进行模型训练,训练时间较长。

首先准备安装环境

# Install PyTorch manually (1.8.2 LTS recommended)
# See instructions at https://pytorch.org/get-started/locally/
# Below is an example for CUDA 11.1
pip3 install torch==1.8.2 torchvision==0.9.2 torchaudio==0.8.2 --extra-index-url https://download.pytorch.org/whl/lts/1.8/cu111
# Install other requirements
pip install -r requirements.txt

然后进行数据集预处理

export PYTHONPATH=.
CUDA_VISIBLE_DEVICES=0 python data_gen/binarize.py --config configs/midi/cascade/opencs/ds1000.yaml

调用 run.py 训练

CUDA_VISIBLE_DEVICES=0 python run.py --config configs/midi/cascade/opencs/ds1000.yaml --exp_name $MY_DS_EXP_NAME --reset

TensorBoard训练记录示意

    1. 推理 

推理前需要我们准备输入数据。

我们可以手动编写数据,但是这种方法是难以调试和维护的:

inp = {
        'text': '小酒窝长睫毛AP是你最美的记号',
        'notes': 'C#4/Db4 | F#4/Gb4 | G#4/Ab4 | A#4/Bb4 F#4/Gb4 | F#4/Gb4 C#4/Db4 | C#4/Db4 | rest | C#4/Db4 | A#4/Bb4 | G#4/Ab4 | A#4/Bb4 | G#4/Ab4 | F4 | C#4/Db4',
        'notes_duration': '0.407140 | 0.376190 | 0.242180 | 0.509550 0.183420 | 0.315400 0.235020 | 0.361660 | 0.223070 | 0.377270 | 0.340550 | 0.299620 | 0.344510 | 0.283770 | 0.323390 | 0.360340',
        'input_type': 'word'
    }
target = "/content/DiffSinger/infer_out/小酒窝.wav"
ds.DiffSingerE2EInfer.example_run(inp, target=target)
Audio(filename=target)

手动编写数据必须包含以下三种内容:

  • 文本
  • 音高(唱名、绝对音高、升降调)
  • 时长(汉字时长)

另外,我们还可以使用类似Vocaloid等传统合成器的调音台进行调音,然后利用 OPENSVIP 开源软件进行DiffSinger的输入格式化批量生成。

上图是使用Xstudio3制作的歌唱序列,但是我们并不会使用Xstudio3进行合成,而是借用Xstudio进行歌唱序列的编辑。

下面是《我多想说再见啊》的工程文件的部分内容。工程由原工程作者分享。

"ph_seq": "AP sh ir zh e SP j v y i b a x in ch en SP z ai sh ou x in SP",
"note_seq": "rest D#3 D#3 C4 C4 rest D#4 D#4 C4 C4 A#3 A#3 C4 C4 C4 C4 rest D#3 D#3 G3 G3 G#3 G#3 rest",
"note_dur_seq": "0.6 0.4 0.4 0.6 0.6 0.1999999 0.4000001 0.4000001 0.3999999 0.3999999 0.4000001 0.4000001 0.2 0.2 0.3999999 0.3999999 0.2 0.3999999 0.3999999 0.6000004 0.6000004 1 1 0.05",

歌词内容为:"试着掬一把星辰在手心"。

音高在上面的代码段进行标记,用空格分开

时长在下面使用小数表示,标记了一个音素的时间长度。

下面是另一段示例:

"ph_seq": "AP q ve zh e SP zh u m i l ian y ao y van d e y En j in SP",
"note_seq": "rest C4 C4 F4 F4 rest D#4 D#4 D#4 D#4 C4 C4 A#3 A#3 A#3 A#3 G#3 G#3 A#3 A#3 C4 C4 rest",
"note_dur_seq": "0.6 0.4 0.4 1 1 0.2 0.2 0.2 0.1999998 0.1999998 0.4000001 0.4000001 0.4000001 0.4000001 0.5999999 0.5999999 0.1999998 0.1999998 0.4000001 0.4000001 1.4 1.4 0.05"

推理后在本地生成音频文件。

CUDA_VISIBLE_DEVICES=0 python run.py --exp_name $MY_DS_EXP_NAME --infer

推理的步数非常多,如果设置较高的迭代次数,如220k,需要相当长的时间才能完成推理。本理的结果请参考介绍视频。

在实验过程中我尝试了多次推理,DiffSinger必须要在大量资源的基础上才能够表现出相当好的效果。一般而言220k的迭代可以达到几乎完美的效果,130k和50k的效果表现稍差,下面是同一首歌不同迭代次数的对比。推理时迭代次数越多,效果越好,对算力要求越高。

220k:

50k:

18k:

最终我们演示的版本是220k迭代,效果几乎与真人没有差异,很难分辨出它是人工合成的。

  1. 感悟与总结

在完成本次实验的过程中,我浏览了大量语音识别与合成领域的开源项目,尤其是TTS及其衍生领域。TTS是一个具有广泛应用环境的领域,它不仅能够在某些情况下代替人声播音的工作,还可以生成具有更加复杂气息变化、语调变化、音高变化的歌声。

在我看来,TTS、SVS和CG相似,都是计算机科学技术、美学与艺术的完美结合,能够直接提升人们的生活水平。经过本门课程的学习和大作业的磨炼,我对语音合成方向产生了浓厚的兴趣——它和我一直以来的音乐爱好(Vocaloid)不谋而合,我会在未来继续在SVS&TTS领域加强学习。

最后感谢老师和助教的辛苦付出,本门课令我收获良多!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。