文章目录
前言
非并行多域语音转换(VC)是一种在不依赖于并行数据的情况下学习多个域之间的映射的技术。这是重要的,但具有挑战性,因为需要学习多个映射,并且显式监督不可用。最近,StarGAN-VC因其仅使用单个生成器即可解决此问题而备受关注。然而,真实语音和转换语音之间仍然存在差距。为了弥补这一差距,我们重新思考了StarGAN-VC的条件方法,这是在单个模型中实现非并行多域VC的关键组成部分,并提出了一种改进的变体,称为StarGAN-VC2。特别是,我们从两个方面重新思考了条件方法:训练目标和网络架构。对于前者,我们提出了一种源和目标条件对抗性损失,允许所有源域数据转换为目标域数据。对于后者,我们介绍了一种基于调制的条件方法,该方法可以以特定领域的方式转换声学特征的调制。我们在非并行多说话人VC上评估了我们的方法。客观评估表明,我们提出的方法在全局和局部结构测量方面都提高了语音质量。此外,主观评估表明,StarGAN-VC2在自然度和说话者相似性方面优于StarGAN-VC。
正文
论文: StarGAN-VC2:Rethinking Conditional Methods for StarGAN-Based Voice Conversion
论文连接
变声器
- 变声器的工作原理是什么呢?
- 其实就是把语音特征进行转换,只不过内容不能变!
VC:Voice Conversion
- 如何构建一个变声器呢?思想跟stargan差不多,细节完全不同
- 需要输入什么?1.声音数据;2.标签编码;
- 整体来说还是GAN模型,主要解决数据特征提取,网络模型定义
- stargan-vc2是升级版,前身还有cyclegan-vc和stargan-vc
使用数据集
- VCC2016
- 4个人的声音数据,相当于4个domain,他们之间相互转换【‘SF1’,‘SF2’,‘TM1’,‘TM2’】
- 论文中选择的特征为:MFCCs(梅尔倒谱系数);log F0(基频的对数);APs(声学参数);
- 论文使用输入特征为:batchsize135*128**(35为特征个数,128为指定特征维度或者切分)**
科普:
1.梅尔倒谱系数,用于表示语音信号的特性。MFCCs是语音信号处理中常用的一种参数,通过对语音信号进行傅里叶变换和一系列数学处理,得到一组倒谱系数,用于描述语音信号的频谱特性和声道形状。
2.log F0 表示基频的对数。基频是指声音信号中最低的频率成分,通常与声道的振动频率相关,对于语音信号来说,基频决定了音调的高低。
3.APs(Acoustic Parameters)是声学参数,用于描述语音信号的声学特性。APs 可以包括多种参数,如频谱参数、能量参数、持续时间参数等,用于分析和表示语音信号的不同特征。
输入数据格式
- 频率:每秒钟波峰所发生的数目称之为信号的频率,用单位千赫兹(kHz)表示
- 例如:0.1毫秒完成4.8次采样,则1秒48000次采样,采样率48KHZ
预处理数据
- 16KHZ重采样
- 预加重:补偿高频信号,让高频信号权重更大一些,因为它信息多
- 分帧:类似时间窗口,得到多个特征段
论文中的特征汇总
- 基频特征(F0):声音可以分解成不同频率的正弦波,其中最低的那个波谷。
- 频谱包络:语音是一个时序信号,如采样频率为16kHz的音频文件(每秒包含16000个采样点)分帧后得到了多个子序列,然后对每个子序列进行傅里叶变换操作,就得到了频率-振幅图(也就是描述频率-振幅图变化趋势的)
- Aperiodic参数:基于F0与频谱包络计算得到
科普:
傅里叶变换的作用
1. 信号分析:傅里叶变换可以将语音信号从时域转换到频域,从而提供语音信号的频谱信息。这有助于分析和理解语音信号的频率特性。
2. 特征提取:通过对语音信号进行傅里叶变换,可以提取出语音信号中的频率分量,这些频率分量可以作为语音信号的特征参数,用于后续的语音识别和分类。
3. 降噪和预处理:傅里叶变换还可以用于语音信号的降噪和预处理。通过去除语音信号中的噪声和干扰,可以提高语音识别的准确性和鲁棒性。
Aperiodic参数的作用
在语音合成中,为了获得更自然的声音,激励源不仅包含周期信号,还需要包含一些非周期信号。Aperiodic参数就是用于描述这种非周期信号的特性,它与周期信号一起共同构成了语音信号的激励源。通过对Aperiodic参数的提取和分析,可以更好地理解和表示语音信号的特性,从而提高语音合成的质量和自然度。
MFCC
MFCC(Mel Frequency Cepstral Coefficients)梅尔频率倒谱系数。
流程为:
1. 预处理:对原始语音信号进行预处理,包括预加重、分帧和加窗等操作。
2. 傅里叶变换:对每帧语音信号进行傅里叶变换,得到语音信号的频谱。
3. 梅尔滤波:将语音信号的频谱映射到梅尔刻度上,并通过一组三角滤波器组进行滤波,得到每个滤波器的输出。
4. 对数运算和离散余弦变换(DCT):对每个滤波器的输出取对数,并进行离散余弦变换,得到MFCC系数。
科普:
1. 通俗解释:FFT之后就把语音转换到频域,MEL滤波器变换后相当于得到更符合人类听觉的效果:
2. 最后DCT相当于提取每一帧的包络(这里面特征多),包络是指一个信号的最大值和最小值构成的曲线,它反映了信号幅度的变化。在语音信号处理中,包络通常用于描述语音信号的幅度变化特性。
网络架构
- 生成器:输入就是提取好的特征,输出也就是特征
- 感觉就是编码-解码的过程,其中引入了IN和GLU单元
语音数据包含的成分
图中的Content Encoder是提取声音内容;Speaker Encoder是提取声音特征;然后经过解码器Decoder,拼接成一个新的有内容和新特征的声音,进而实现变声器的效果。
思考:
- 变声器虽然把咱们动静给改了,但是内容没变吧!
- 编码时如何保留住原始内容呢?这就得去掉声音中特性的部分
- 解码时如何放大个性呢?还是需要再处理解码特征
Instance Normalization(IN)
把声音特征去掉,就是声音内容了。
IN是在每个样本的每个通道上单独进行归一化操作。具体来说,IN计算每个样本每个通道的均值和方差,并使用它们对该通道的数据进行标准化。
Adaptive Instance Normalization(AdaIn)
IN是一种用于解决内部协变量偏移问题的归一化方法,它通过对每个通道的激活进行归一化,使得网络的训练更加稳定。然而,IN在风格迁移任务中可能会导致风格的丢失。
为了解决这个问题,AdaIn被提了出来。AdaIn不仅对每个通道的激活进行归一化,还学习了一个缩放因子和偏移因子,这两个因子是根据数据风格计算得出的。这样,AdaIn就可以将内容的激活分布调整为风格的激活分布,从而实现了风格的迁移。
生成器 PS(PixelShuffle)
不会…
判别器
- GSP:global sum pooling:一个特征图压缩成一个点,batch512hw,压缩成batch512的点
- 然后,标签通过embeding编码成512维特征(batch*512),内积得到batch个判别结果
代码
- preprocess.py
这段代码执行了以下操作:
- 导入所需的库和模块。
- 定义了命令行参数解析器(ArgumentParser)。该解析器用于从命令行输入的参数中解析出指定的参数值。
- 定义了
--dataset
参数,用于指定数据集名称,默认值为VCC2016
,可选值为VCC2016
和VCC2018
。 - 定义了
--input_dir
参数,用于指定输入数据的目录,默认值为./data/spk
。 - 定义了
--output_dir
参数,用于指定处理后数据的输出目录,默认值为./data/processed
。
- 定义了
- 解析命令行参数,并将解析后的值保存到相应的变量中。
- 创建输出目录(如果不存在)。
- 根据选择的数据集,设置采样率。
- 如果数据集为
VCC2016
,则采样率为16000 Hz。 - 如果数据集为
VCC2018
,则采样率为22050 Hz。
- 如果数据集为
- 调用
wav_to_mcep_file
函数,将输入目录中的音频文件转换为梅尔倒谱系数(MCCs),并保存到输出目录中的文件中。 - 实例化
GenerateStatistics
类的对象,并指定数据集的输出目录。 - 调用
generate_stats
方法,生成统计特征(均值和标准差)。 - 调用
normalize_dataset
方法,对数据集进行归一化处理。 - 输出程序执行的持续时间。
总的来说,这段代码执行了音频数据的预处理过程,包括将音频转换为梅尔倒谱系数(MCCs),计算统计特征,并对数据集进行归一化处理。
if __name__ == "__main__":
start = datetime.now()
parser = argparse.ArgumentParser(description='Convert the wav waveform to mel-cepstral coefficients(MCCs)\
and calculate the speech statistical characteristics.')
input_dir = './data/spk'
output_dir = './data/processed'
dataset_default = 'VCC2016'
parser.add_argument('--dataset', type=str, default=dataset_default, choices=['VCC2016', 'VCC2018'],
help='Available datasets: VCC2016 and VCC2018 (Default: VCC2016).')
parser.add_argument('--input_dir', type=str, default=input_dir, help='Directory of input data.')
parser.add_argument('--output_dir', type=str, default=output_dir, help='Directory of processed data.')
argv = parser.parse_args()
input_dir = argv.input_dir
output_dir = argv.output_dir
os.makedirs(output_dir, exist_ok=True)
"""
Sample rate:
VCC2016: 16000 Hz
VCC2018: 22050 Hz
"""
if argv.dataset == 'VCC2016':
sample_rate = 16000
else:
sample_rate = 22050
wav_to_mcep_file(input_dir, sample_rate, processed_filepath=output_dir)
# 生成统计特征
generator = GenerateStatistics(output_dir)
generator.generate_stats()
generator.normalize_dataset()
end = datetime.now()
print(f"* Duration: {end-start}.")
这段代码定义了一个名为load_wavs
的函数,用于加载音频文件并进行预处理。
函数的参数包括:
dataset
:数据集路径,指定包含音频文件的目录。sr
:采样率。
函数的流程如下:
- 创建一个空字典
data
,用于存储音频文件的路径。 - 使用
os.scandir
遍历数据集目录中的文件和子目录。 - 对于每个子目录,将其作为键添加到
data
字典中,并创建一个空列表作为对应的值。 - 使用
os.scandir
遍历子目录中的音频文件。 - 对于每个音频文件,将其路径添加到相应子目录的列表中。
- 输出加载的键(子目录名称)。
- 创建一个空字典
resdict
,用于存储处理后的音频数据。 - 初始化计数器
cnt
为0。 - 遍历
data
字典的键值对,进行音频文件的预处理。 - 对于每个音频文件路径,提取文件名作为新的键。
- 使用
librosa.load
函数加载音频文件,将其转换为浮点数类型的波形数据。 - 使用
librosa.effects.trim
函数对波形数据进行修剪,突出高频信号。 - 应用预加重滤波器,将修剪后的波形数据附加到之前的采样点后面。
- 将处理后的波形数据添加到
resdict
字典中对应的子目录和文件键中。 - 输出进度点"."(每处理一个音频文件输出一个点)。
- 递增计数器
cnt
。 - 输出总音频文件数量。
- 返回处理后的
resdict
字典。
总的来说,这段代码实现了对音频文件的加载和预处理操作。它遍历数据集目录并获取每个子目录中的音频文件路径。然后,它使用Librosa库加载音频文件,对其进行修剪和预加重滤波器处理,并将处理后的音频数据保存在一个字典中。最后,返回包含处理后数据的字典。
# 用于加载音频文件。
def load_wavs(dataset: str, sr):
"""
`data`: contains all audios file path.
`resdict`: contains all wav files.
"""
data = {}
with os.scandir(dataset) as it:
for entry in it:
if entry.is_dir():
data[entry.name] = []
with os.scandir(entry.path) as it_f:
for onefile in it_f:
if onefile.is_file():
data[entry.name].append(onefile.path)
print(f'* Loaded keys: {data.keys()}')
resdict = {}
cnt = 0
for key, value in data.items():
resdict[key] = {}
for one_file in value: #预处理,突出高频信号,因为一般发音的话高频信号能表达更多有用的信息
filename = one_file.split('/')[-1].split('.')[0]
newkey = f'{filename}'
wav, _ = librosa.load(one_file, sr=sr, mono=True, dtype=np.float64)#sr:采样率 mono:单通道
y, _ = librosa.effects.trim(wav, top_db=15)
wav = np.append(y[0], y[1: ] - 0.97 * y[: -1])
resdict[key][newkey] = wav
print('.', end='')
cnt += 1
print(f'\n* Total audio files: {cnt}.')
return resdict
# 用于生成指定大小的迭代器块。
# iterable:可迭代对象。
# size:块大小。
# 函数功能:
# 将可迭代对象拆分为指定大小的块。
# 逐块返回生成器。
这段代码定义了一个名为chunks
的函数,用于将可迭代对象按照指定大小拆分成连续的子块。
函数的参数包括:
iterable
:可迭代对象,即需要拆分的对象。size
:块的大小,指定每个子块的元素个数。
函数的流程如下:
- 使用
range
函数生成一个以size
为步长的索引序列,遍历这个索引序列。 - 在每次迭代中,根据当前索引截取
iterable
中的连续子序列,子序列从当前索引开始,长度为size
。 - 使用
yield
语句将每个子序列作为生成器的输出。 - 持续迭代直到所有的子块都被生成和返回。
总的来说,这段代码实现了将一个可迭代对象拆分为连续的子块,每个子块包含指定数量的元素。它通过生成器的方式返回拆分后的子块,可以在需要的时候逐个获取子块,而不是一次性生成和返回所有的子块。
def chunks(iterable, size):
"""
Yield successive n-sized chunks from iterable.
"""
for i in range(0, len(iterable), size):
yield iterable[i: i + size]
# 用于将音频文件转换为MCEP特征。
# sr: 采样率。
# 进行音频文件转换和特征提取。
这段代码定义了一个名为wav_to_mcep_file
的函数,用于将音频文件转换为MCEP特征并保存为文件。
函数的参数包括:
dataset
:数据集路径,指定包含音频文件的目录。sr
:采样率。processed_filepath
:转换后的特征文件保存路径,默认为"./data/processed"。
函数的流程如下:
- 使用
shutil.rmtree
函数删除已存在的processed_filepath
目录及其内容。 - 使用
os.makedirs
函数创建processed_filepath
目录,如果目录已存在则不进行任何操作。 - 使用
glob.glob
函数计算数据集中所有音频文件的数量,并输出总音频文件数量。 - 调用
load_wavs
函数加载和预处理音频数据,将结果存储在字典d
中。 - 遍历字典
d
中的每个说话者。 - 获取当前说话者的音频数据列表
values_of_one_speaker
。 - 使用
chunks
函数将音频数据列表拆分为指定大小的块one_chunk
。 - 初始化一个空列表
wav_concated
用于存储拼接后的音频数据。 - 对于每个块,将其数据拼接到
wav_concated
列表中。 - 将
wav_concated
列表转换为NumPy数组wav_concated
。 - 调用
cal_mcep
函数计算MCEP特征,得到基频(f0)、频谱包络(ap)和梅尔频谱倒谱系数(mcep)。 - 根据说话者名称和块索引生成新的名称
newname
。 - 构建特征文件路径
file_path_z
,使用np.savez
函数将f0和mcep保存为NPZ文件。 - 输出保存特征文件的消息。
- 对于每个块中的每个帧,在MCEP矩阵中滑动窗口,每次取连续的
FRAMES
帧作为一个音频片段。 - 如果音频片段的帧数等于
FRAMES
,则生成临时文件名temp_name
并构建保存路径filePath
,使用np.save
函数将音频片段保存为NPY文件。 - 输出保存音频片段文件的消息。
总的来说,这段代码实现了将音频文件转换为MCEP特征,并将特征数据保存为文件。它遍历数据集中的音频文件,将它们按照指定大小进行拼接,并计算拼接后音频数据的MCEP特征。然后,它将特征数据保存为NPZ格式的文件,以及根据帧窗口滑动的方式,将每个音频片段保存为单独的NPY文件。
def wav_to_mcep_file(dataset: str, sr: int, processed_filepath: str='./data/processed'):
"""
Convert wavs to MCEPs feature using image representation.
"""
shutil.rmtree(processed_filepath)
os.makedirs(processed_filepath, exist_ok=True)
allwavs_cnt = len(glob.glob(f'{dataset}/*/*.wav'))
print(f'* Total audio files: {allwavs_cnt}.')
d = load_wavs(dataset, sr)
for one_speaker in d.keys():
values_of_one_speaker = list(d[one_speaker].values())
for index, one_chunk in enumerate(chunks(values_of_one_speaker, CHUNK_SIZE)):
wav_concated = []
temp = one_chunk.copy()
for one in temp:
wav_concated.extend(one)
wav_concated = np.array(wav_concated)
f0, ap, mcep = cal_mcep(wav_concated, sr, FEATURE_DIM, FFTSIZE, SHIFTMS, ALPHA)
newname = f'{one_speaker}_{index}'
file_path_z = os.path.join(processed_filepath, newname)
np.savez(file_path_z, f0=f0, mcep=mcep)
print(f'[SAVE]: {file_path_z}')
for start_idx in range(0, mcep.shape[1] - FRAMES + 1, FRAMES):
one_audio_seg = mcep[:, start_idx: start_idx + FRAMES]
if one_audio_seg.shape[1] == FRAMES:
temp_name = f'{newname}_{start_idx}'
filePath = os.path.join(processed_filepath, temp_name)
np.save(filePath, one_audio_seg)
print(f'[SAVE]: {filePath}.npy')
-
solver.py
- 恢复或加载预训练的模型。
- 从指定的测试目录读取数据,包括源说话者的音频数据和说话者标签。
- 获取目标说话者列表。
- 对于每个目标说话者,进行以下操作:
- 将源说话者和目标说话者的标签进行编码转换。
- 对于给定的音频文件和内容,进行以下操作:
- 提取声音特征,包括基频(f0)、包络(ap)和梅尔倒谱系数(mcep_norm)。
- 创建一个空列表,用于存储转换结果。
- 对于每个特征帧,进行以下操作:
- 将特征帧从源说话者转换为目标说话者。
- 对转换后的特征进行一些后处理操作。
- 将转换结果添加到列表中。
- 将转换结果拼接起来,并进行一些修剪操作。
- 进行音频合成,生成转换后的音频。
- 将转换后的音频保存到文件中。
class Solver(object):
def __init__(self, data_loader, config):
self.config = config
self.data_loader = data_loader
self.num_spk = config.num_spk
if config.dataset == 'VCC2016':
self.sample_rate = 16000
else:
self.sample_rate = 22050
self.lambda_cyc = config.lambda_cyc
self.lambda_gp = config.lambda_gp
self.lambda_id = config.lambda_id
# Training configurations.
self.data_dir = config.data_dir
self.test_dir = config.test_dir
self.batch_size = config.batch_size
self.num_iters = config.num_iters
self.num_iters_decay = config.num_iters_decay
self.g_lr = config.g_lr
self.d_lr = config.d_lr
self.n_critic = config.n_critic
self.beta1 = config.beta1
self.beta2 = config.beta2
self.resume_iters = config.resume_iters
# Test configurations.
self.test_iters = config.test_iters
self.trg_speaker = ast.literal_eval(config.trg_speaker)
self.src_speaker = config.src_speaker
self.use_tensorboard = config.use_tensorboard
self.device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
self.spk_enc = LabelBinarizer().fit(speakers)
self.log_dir = config.log_dir
self.sample_dir = config.sample_dir
self.model_save_dir = config.model_save_dir
self.result_dir = config.result_dir
self.log_step = config.log_step
self.sample_step = config.sample_step
self.model_save_step = config.model_save_step
self.lr_update_step = config.lr_update_step
self.build_model()
# Only use tensorboard in train mode.
if self.use_tensorboard and config.mode == 'train':
self.build_tensorboard()
#构建 生成器 和 判别器
def build_model(self):
self.G = Generator(num_speakers=self.num_spk)
self.D = Discriminator(num_speakers=self.num_spk)
#优化器,用于更新生成器G,D的参数
self.g_optimizer = torch.optim.Adam(self.G.parameters(), self.g_lr, [self.beta1, self.beta2])
self.d_optimizer = torch.optim.Adam(self.D.parameters(), self.d_lr, [self.beta1, self.beta2])
self.print_network(self.G, 'G')
self.print_network(self.D, 'D')
#将生成器G的模型参数移动到指定的计算设备上
self.G.to(self.device)
self.D.to(self.device)
#目的是打印模型的结构和参数数量
def print_network(self, model, name):
num_params = 0
for p in model.parameters():
num_params += p.numel()
print(model)
print(f"* ({name}) Number of parameters: {num_params}.")
#记录日志
def build_tensorboard(self):
from logger import Logger
self.logger = Logger(self.log_dir)
#更新生成器(generator)和判别器(discriminator)的学习率
def update_lr(self, g_lr, d_lr):
"""
Decay learning rates of the generator and discriminator.
"""
for param_group in self.g_optimizer.param_groups:
param_group['lr'] = g_lr
for param_group in self.d_optimizer.param_groups:
param_group['lr'] = d_lr
def train(self):
# Learning rate cache for decaying.
g_lr = self.g_lr
d_lr = self.d_lr
start_iters = 0
if self.resume_iters:
print(f'Resume at step {self.resume_iters}...')
start_iters = self.resume_iters
self.restore_model(self.resume_iters)
norm = Normalizer()
data_iter = iter(self.data_loader)
g_adv_optim = 0
g_adv_converge_low = True # Check which direction `g_adv` is converging (init as low).
g_rec_optim = 0
g_rec_converge_low = True # Check which direction `g_rec` is converging (init as low).
g_tot_optim = 0
g_tot_converge_low = True # Check which direction `g_tot` is converging (init as low).
print('\n* Start training...\n')
start_time = datetime.now()
for i in range(start_iters, self.num_iters):
try:
x_real, speaker_idx_org, label_org = next(data_iter)
#print (x_real.shape,speaker_idx_org.shape,label_org.shape)
except:
data_iter = iter(self.data_loader)
x_real, speaker_idx_org, label_org = next(data_iter)
rand_idx = torch.randperm(label_org.size(0))
label_trg = label_org[rand_idx]
speaker_idx_trg = speaker_idx_org[rand_idx]
x_real = x_real.to(self.device)
label_org = label_org.to(self.device) # Original domain one-hot labels.
label_trg = label_trg.to(self.device) # Target domain one-hot labels.
speaker_idx_org = speaker_idx_org.to(self.device) # Original domain labels.
speaker_idx_trg = speaker_idx_trg.to(self.device) # Target domain labels.
"""
Discriminator training.
"""
CELoss = nn.CrossEntropyLoss()
# Loss: st-adv.
out_r = self.D(x_real, label_org, label_trg)
x_fake = self.G(x_real, label_org, label_trg)
out_f = self.D(x_fake.detach(), label_org, label_trg)
d_loss_adv = F.binary_cross_entropy_with_logits(input=out_f, target=torch.ones_like(out_f, dtype=torch.float)) + \
F.binary_cross_entropy_with_logits(input=out_r, target=torch.ones_like(out_r, dtype=torch.float))
# Loss: gp.
alpha = torch.rand(x_real.size(0), 1, 1, 1).to(self.device)
#print (alpha.shape)
x_hat = (alpha * x_real.data + (1 - alpha) * x_fake.data).requires_grad_(True)
#print (x_hat.shape)
out_src = self.D(x_hat, label_org, label_trg)
#print (out_src.shape)
d_loss_gp = self.gradient_penalty(out_src, x_hat)
# Totol loss: st-adv + lambda_gp * gp.
d_loss = d_loss_adv + self.lambda_gp * d_loss_gp
self.reset_grad()
d_loss.backward()
self.d_optimizer.step()
loss = {}
loss['D/d_loss_adv'] = d_loss_adv.item()
loss['D/d_loss_gp'] = d_loss_gp.item()
loss['D/d_loss'] = d_loss.item()
"""
Generator training.
"""
if (i + 1) % self.n_critic == 0:
# Loss: st-adv (original-to-target).
x_fake = self.G(x_real, label_org, label_trg)
g_out_src = self.D(x_fake, label_org, label_trg)
g_loss_adv = F.binary_cross_entropy_with_logits(input=g_out_src, target=torch.ones_like(g_out_src, dtype=torch.float))
# Loss: cyc (target-to-original).
x_rec = self.G(x_fake, label_trg, label_org)
g_loss_rec = F.l1_loss(x_rec, x_real)
# Loss: id (original-to-original).
x_fake_id = self.G(x_real, label_org, label_org)
g_loss_id = F.l1_loss(x_fake_id, x_real)
# Total loss: st-adv + lambda_cyc * cyc (+ lambda_id * id).
# Only include Identity mapping before 10k iterations.
if (i + 1) < 10 ** 4:
g_loss = g_loss_adv \
+ self.lambda_cyc * g_loss_rec \
+ self.lambda_id * g_loss_id
else:
g_loss = g_loss_adv + self.lambda_cyc * g_loss_rec
# Check convergence direction of losses.
if (i + 1) == 20 * (10 ** 3): # Update optims at 20k iterations.
g_adv_optim = g_loss_adv
g_rec_optim = g_loss_rec
g_tot_optim = g_loss
if (i + 1) == 70 * (10 ** 3): # Check which direction optims have gone over 70k iters.
if g_loss_adv > g_adv_optim:
g_adv_converge_low = False
if g_loss_rec > g_rec_optim:
g_rec_converge_low = False
if g_loss > g_tot_optim:
g_tot_converge_low = False
print('* CONVERGE DIRECTION')
print(f'adv_loss low: {g_adv_converge_low}')
print(f'g_rec_loss los: {g_rec_converge_low}')
print(f'g_loss loq: {g_tot_converge_low}')
# Update loss for checkpoint saving.
if (i + 1) > 75 * (10 ** 3):
if g_tot_converge_low:
if (g_loss_adv < g_adv_optim and abs(g_loss_adv - g_adv_optim) > 0.1) and g_loss_rec < g_rec_optim:
self.save_optim_checkpoints('g_adv_rec_optim-G.ckpt', 'g_adv_rec_optim-D.ckpt', 'adv+rec')
elif not g_tot_converge_low:
if (g_loss_adv > g_adv_optim and abs(g_loss_adv - g_adv_optim) > 0.1) and g_loss_rec < g_rec_optim:
self.save_optim_checkpoints('g_adv_rec_optim-G.ckpt', 'g_adv_rec_optim-D.ckpt', 'adv+rec')
if g_adv_converge_low:
if g_loss_adv < g_adv_optim:
g_adv_optim = g_loss_adv
self.save_optim_checkpoints('g_adv_optim-G.ckpt', 'g_adv_optim-D.ckpt', 'adv')
elif not g_adv_converge_low:
if g_loss_adv < g_adv_optim:
g_adv_optim = g_loss_adv
self.save_optim_checkpoints('g_adv_optim-G.ckpt', 'g_adv_optim-D.ckpt', 'adv')
if g_rec_converge_low:
if g_loss_rec < g_rec_optim:
g_rec_optim = g_loss_rec
self.save_optim_checkpoints('g_rec_optim-G.ckpt', 'g_rec_optim-D.ckpt', 'rec')
elif not g_rec_converge_low:
if g_loss_rec > g_rec_optim:
g_rec_optim = g_loss_rec
self.save_optim_checkpoints('g_rec_optim-G.ckpt', 'g_rec_optim-D.ckpt', 'rec')
if g_tot_converge_low:
if g_loss < g_tot_optim:
g_tot_optim = g_loss
self.save_optim_checkpoints('g_tot_optim-G.ckpt', 'g_tot_optim-D.ckpt', 'tot')
elif not g_tot_converge_low:
if g_loss > g_tot_optim:
g_tot_optim = g_loss
self.save_optim_checkpoints('g_tot_optim-G.ckpt', 'g_tot_optim-D.ckpt', 'tot')
self.reset_grad()
g_loss.backward()
self.g_optimizer.step()
loss['G/g_loss_adv'] = g_loss_adv.item()
loss['G/g_loss_rec'] = g_loss_rec.item()
loss['G/g_loss_id'] = g_loss_id.item()
loss['G/g_loss'] = g_loss.item()
# Print training information.
if (i + 1) % self.log_step == 0:
et = datetime.now() - start_time
et = str(et)[: -7]
log = "Elapsed [{}], Iteration [{}/{}]".format(et, i + 1, self.num_iters)
for tag, value in loss.items():
log += ", {}: {:.4f}".format(tag, value)
print(log)
if self.use_tensorboard:
for tag, value in loss.items():
self.logger.scalar_summary(tag, value, i + 1)
# Translate fixed images for debugging.
if (i + 1) % self.sample_step == 0:
with torch.no_grad():
d, speaker = TestSet(self.test_dir, self.sample_rate).test_data()
original = random.choice([x for x in speakers if x != speaker])
target = random.choice([x for x in speakers if x != speaker])
label_o = self.spk_enc.transform([original])[0]
label_t = self.spk_enc.transform([target])[0]
label_o = np.asarray([label_o])
label_t = np.asarray([label_t])
for filename, content in d.items():
f0 = content['f0']
ap = content['ap']
mcep_norm_pad = pad_mcep(content['mcep_norm'], FRAMES)
convert_result = []
for start_idx in range(0, mcep_norm_pad.shape[1] - FRAMES + 1, FRAMES):
one_seg = mcep_norm_pad[:, start_idx: start_idx + FRAMES]
one_seg = torch.FloatTensor(one_seg).to(self.device)
one_seg = one_seg.view(1, 1, one_seg.size(0), one_seg.size(1))
o = torch.FloatTensor(label_o)
t = torch.FloatTensor(label_t)
one_seg = one_seg.to(self.device)
o = o.to(self.device)
t = t.to(self.device)
one_set_return = self.G(one_seg, o, t).data.cpu().numpy()
one_set_return = np.squeeze(one_set_return)
one_set_return = norm.backward_process(one_set_return, target)
convert_result.append(one_set_return)
convert_con = np.concatenate(convert_result, axis=1)
convert_con = convert_con[:, 0: content['mcep_norm'].shape[1]]
contigu = np.ascontiguousarray(convert_con.T, dtype=np.float64)
f0_converted = norm.pitch_conversion(f0, speaker, target)
wav = synthesis_from_mcep(f0_converted, contigu, ap, self.sample_rate, FFTSIZE, SHIFTMS, ALPHA)
name = f'{speaker}-{target}_iter{i + 1}_{filename}'
path = os.path.join(self.sample_dir, name)
print(f'[SAVE]: {path}')
librosa.output.write_wav(path, wav, self.sample_rate)
# Save model checkpoints.
if (i + 1) % self.model_save_step == 0:
G_path = os.path.join(self.model_save_dir, '{}-G.ckpt'.format(i + 1))
D_path = os.path.join(self.model_save_dir, '{}-D.ckpt'.format(i + 1))
torch.save(self.G.state_dict(), G_path)
torch.save(self.D.state_dict(), D_path)
print(f'Save model checkpoints into {self.model_save_dir}...')
# Decay learning rates.
if (i + 1) % self.lr_update_step == 0 and (i + 1) > (self.num_iters - self.num_iters_decay):
g_lr -= (self.g_lr / float(self.num_iters_decay))
d_lr -= (self.d_lr / float(self.num_iters_decay))
self.update_lr(g_lr, d_lr)
print (f'Decayed learning rates, g_lr: {g_lr}, d_lr: {d_lr}.')
def gradient_penalty(self, y, x):
"""
Compute gradient penalty: (L2_norm(dy / dx) - 1) ** 2.
(Differs from the paper.)
"""
weight = torch.ones(y.size()).to(self.device)
dydx = torch.autograd.grad(outputs=y,
inputs=x,
grad_outputs=weight,
retain_graph=True,
create_graph=True,
only_inputs=True)[0]
dydx = dydx.view(dydx.size(0), -1)
dydx_l2norm = torch.sqrt(torch.sum(dydx ** 2, dim=1))
return torch.mean((dydx_l2norm - 1) ** 2)
#每次迭代都需要清除优化器的梯度
def reset_grad(self):
self.g_optimizer.zero_grad()
self.d_optimizer.zero_grad()
#保存
def save_optim_checkpoints(self, g_name, d_name, type_saving):
G_path = os.path.join(self.model_save_dir, g_name)
D_path = os.path.join(self.model_save_dir, d_name)
torch.save(self.G.state_dict(), G_path)
torch.save(self.D.state_dict(), D_path)
print(f'Save {type_saving} optimal model checkpoints into {self.model_save_dir}...')
#G和D 的参数
def restore_model(self, resume_iters):
print(f'Loading the trained models from step {resume_iters}...')
G_path = os.path.join(self.model_save_dir, '{}-G.ckpt'.format(resume_iters))
D_path = os.path.join(self.model_save_dir, '{}-D.ckpt'.format(resume_iters))
self.G.load_state_dict(torch.load(G_path, map_location=lambda storage, loc: storage))
self.D.load_state_dict(torch.load(D_path, map_location=lambda storage, loc: storage))
# 将声音从一个说话者转换为另一个说话者
def convert(self):
"""
Convertion.
"""
# 恢复或加载预训练的模型
self.restore_model(self.test_iters)
norm = Normalizer()
# 从指定的测试目录读取数据,并获取源说话者的音频数据和说话者标签。
d, speaker = TestSet(self.test_dir, self.sample_rate).test_data(self.src_speaker)
# 获取目标说话者的列表。
targets = self.trg_speaker
for target in targets:
#打印当前目标说话者的名称。
print(f'* Target: {target}')
assert target in speakers
# 将源说话者和目标说话者的标签进行编码转换。
label_o = self.spk_enc.transform([self.src_speaker])[0]
label_t = self.spk_enc.transform([target])[0]
label_o = np.asarray([label_o])
label_t = np.asarray([label_t])
# 在不进行梯度计算的上下文中执行以下操作。
with torch.no_grad():
# 对于给定的音频文件和内容进行以下操作。
for filename, content in d.items():
# 提取声音特征,包括基频(f0),包络(ap),和梅尔倒谱系数(mcep_norm)。
f0 = content['f0']
ap = content['ap']
mcep_norm_pad = pad_mcep(content['mcep_norm'], FRAMES)
# 创建一个空列表用于存储转换结果。
convert_result = []
# 对于每个特征帧进行以下操作。
for start_idx in range(0, mcep_norm_pad.shape[1] - FRAMES + 1, FRAMES):
one_seg = mcep_norm_pad[:, start_idx: start_idx + FRAMES]
one_seg = torch.FloatTensor(one_seg).to(self.device)
one_seg = one_seg.view(1, 1, one_seg.size(0), one_seg.size(1))
o = torch.FloatTensor(label_o)
t = torch.FloatTensor(label_t)
one_seg = one_seg.to(self.device)
o = o.to(self.device)
t = t.to(self.device)
# 将特征帧从源说话者转换为目标说话者。
one_set_return = self.G(one_seg, o, t).data.cpu().numpy()
one_set_return = np.squeeze(one_set_return)
one_set_return = norm.backward_process(one_set_return, target)
convert_result.append(one_set_return)
convert_con = np.concatenate(convert_result, axis=1)
convert_con = convert_con[:, 0: content['mcep_norm'].shape[1]]
contigu = np.ascontiguousarray(convert_con.T, dtype=np.float64)
f0_converted = norm.pitch_conversion(f0, speaker, target)
wav = synthesis_from_mcep(f0_converted, contigu, ap, self.sample_rate, FFTSIZE, SHIFTMS, ALPHA)
name = f'{speaker}-{target}_iter{self.test_iters}_{filename}'
path = os.path.join(self.result_dir, name)
print(f'[SAVE]: {path}')
# 将转换后的音频保存到文件中。
librosa.output.write_wav(path, wav, self.sample_rate)
- model.py
这段代码定义了一个下采样模块(DownsampleBlock)的类,实现了声音特征的下采样操作。以下是它的主要功能:
-
初始化函数(
__init__
):- 接收输入和输出的通道数(
dim_in
和dim_out
)。 - 定义卷积层、实例归一化层和GLU激活函数组成的网络模块,用于特征的下采样。
- 接收输入和输出的通道数(
-
前向传播函数(
forward
):- 接收输入张量
x
。 - 通过卷积层、实例归一化层和GLU激活函数进行特征提取和下采样操作。
- 返回下采样后的特征张量。
- 接收输入张量
具体来说,forward
函数中的操作如下:
- 通过卷积层和实例归一化层对输入张量
x
进行卷积操作。 - 将卷积结果输入到GLU激活函数中,GLU激活函数在通道维度上对特征进行切分,并对其中一半通道应用sigmoid函数。
- 将卷积结果和经过sigmoid后的结果相乘,实现GLU(门限化线性单元)操作。
- 返回下采样后的特征张量。
总体上,这段代码定义了一个包含卷积、实例归一化和GLU激活函数的下采样模块,用于将输入特征进行降维和下采样操作。
#下采样
class DownsampleBlock(nn.Module):
def __init__(self, dim_in, dim_out, kernel_size, stride, padding, bias):
super(DownsampleBlock, self).__init__()
self.conv_layer = nn.Sequential(
nn.Conv2d(in_channels=dim_in,
out_channels=dim_out,
kernel_size=kernel_size,
stride=stride,
padding=padding,
bias=bias),
nn.InstanceNorm2d(num_features=dim_out, affine=True),
nn.GLU(dim=1)
)
self.conv_gated = nn.Sequential(
nn.Conv2d(in_channels=dim_in,
out_channels=dim_out,
kernel_size=kernel_size,
stride=stride,
padding=padding,
bias=bias),
nn.InstanceNorm2d(num_features=dim_out, affine=True),
nn.GLU(dim=1)
)
def forward(self, x):
# GLU 有选择性
return self.conv_layer(x) * torch.sigmoid(self.conv_gated(x))
这段代码定义了一个上采样模块(UpSampleBlock)的类,实现了声音特征的上采样操作。以下是它的主要功能:
-
初始化函数(
__init__
):- 接收输入和输出的通道数(
dim_in
和dim_out
)。 - 定义转置卷积层、像素洗牌层(PixelShuffle)和GLU激活函数组成的网络模块,用于特征的上采样。
- 接收输入和输出的通道数(
-
前向传播函数(
forward
):- 接收输入张量
x
。 - 通过转置卷积层、像素洗牌层和GLU激活函数进行特征的上采样操作。
- 返回上采样后的特征张量。
- 接收输入张量
具体来说,forward
函数中的操作如下:
- 通过转置卷积层对输入张量
x
进行反卷积操作。 - 将反卷积结果输入到像素洗牌层中,像素洗牌层将特征图的通道数缩小,并将特征重新排列。
- 将洗牌后的特征输入到GLU激活函数中,GLU激活函数在通道维度上对特征进行切分,并对其中一半通道应用sigmoid函数。
- 将反卷积结果和经过sigmoid后的结果相乘,实现GLU(门限化线性单元)操作。
- 返回上采样后的特征张量。
总体上,这段代码定义了一个包含转置卷积、像素洗牌和GLU激活函数的上采样模块,用于将输入特征进行扩展和上采样操作。
#上采样
class UpSampleBlock(nn.Module):
def __init__(self, dim_in, dim_out, kernel_size, stride, padding, bias):
super(UpSampleBlock, self).__init__()
self.conv_layer = nn.Sequential(
nn.ConvTranspose2d(in_channels=dim_in,
out_channels=dim_out,
kernel_size=kernel_size,
stride=stride,
padding=padding,
bias=bias),
nn.PixelShuffle(2),
nn.GLU(dim=1)
)
self.conv_gated = nn.Sequential(
nn.ConvTranspose2d(in_channels=dim_in,
out_channels=dim_out,
kernel_size=kernel_size,
stride=stride,
padding=padding,
bias=bias),
# ps层
nn.PixelShuffle(2),
nn.GLU(dim=1)
)
def forward(self, x):
# GLU
return self.conv_layer(x) * torch.sigmoid(self.conv_gated(x))
这段代码实现了AdaIN(Adaptive Instance Normalization)模块,它的作用是对输入进行实例标准化,并应用风格编码来调整输出。以下是代码的功能解释:
-
初始化函数(
__init__
):- 接收输入通道数(
dim_in
)和风格编码的数量(style_num
)作为参数。 - 初始化设备类型为CUDA或CPU,并创建一个全连接层(
self.fc
)用于将风格编码映射为适应实例标准化所需的参数。
- 接收输入通道数(
-
前向传播函数(
forward
):- 接收输入张量(
x
)和风格编码(c
)作为参数。 - 通过全连接层将风格编码映射为尺寸为
dim_in * 2
的张量h
。 - 调整
h
的尺寸为(batch_size, dim_in, 1)
,其中batch_size
是输入张量的批量大小。 - 计算输入张量
x
在第二个维度上的均值u
,结果维度为(batch_size, dim_in, 1)
。 - 计算输入张量
x
在第二个维度上的方差var
,结果维度为(batch_size, dim_in, 1)
。 - 计算标准差
std
,通过对方差加上一个很小的常量(1e-8
)然后取平方根得到。 - 使用
torch.chunk
函数将h
张量沿着第二个维度分割成两个张量,分别赋给gamma
和beta
,维度为(batch_size, dim_in, 1)
。 - 将输入张量
x
减去均值u
后除以标准差std
,然后乘以缩放因子(1 + gamma)
,再加上偏移因子beta
,得到最终的输出。
- 接收输入张量(
总的来说,这段代码实现了AdaIN操作,通过风格编码调整输入张量的均值、方差和标准差,并将调整后的结果与输入相结合,得到经过实例标准化且风格化的输出。
#adaIn
class AdaptiveInstanceNormalization(nn.Module):
"""
AdaIN block.
"""
def __init__(self, dim_in, style_num):
super(AdaptiveInstanceNormalization, self).__init__()
self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
self.fc = nn.Linear(style_num, dim_in * 2)
def forward(self, x, c):
h = self.fc(c)
#print (h.shape)
h = h.view(h.size(0), h.size(1), 1)
#print (h.shape)
u = torch.mean(x, dim=2, keepdim=True)
#print (u.shape)
var = torch.mean((x - u) * (x - u), dim=2, keepdim=True)
#print (var.shape)
std = torch.sqrt(var + 1e-8)
#print (std.shape)
gamma, beta = torch.chunk(h, chunks=2, dim=1)#分离出来
return (1 + gamma) * (x - u) / std + beta
这段代码实现了有条件的实例标准化(Conditional Instance Normalization)模块,称为CIN(CIN block)。下面是代码功能的解释:
-
初始化函数(
__init__
):- 接收输入通道数(
dim_in
)和风格编码的数量(style_num
)作为参数。 - 初始化设备类型为CUDA或CPU,并创建两个线性层(
self.gamma
和self.beta
)用于将风格编码映射为每个通道的缩放因子和偏移因子。
- 接收输入通道数(
-
前向传播函数(
forward
):- 接收输入张量(
x
)和风格编码(c
)作为参数。 - 计算输入张量
x
在第二个维度上的均值u
,结果维度为(batch_size, dim_in, 1)
。 - 计算输入张量
x
在第二个维度上的方差var
,结果维度为(batch_size, dim_in, 1)
。 - 计算标准差
std
,通过对方差加上一个很小的常量(1e-8
)然后取平方根得到。 - 使用线性层
self.gamma
将风格编码c
映射为具有大小为dim_in
的缩放因子gamma
,将其形状调整为(batch_size, dim_in, 1)
。 - 使用线性层
self.beta
将风格编码c
映射为具有大小为dim_in
的偏移因子beta
,将其形状调整为(batch_size, dim_in, 1)
。 - 将输入张量
x
减去均值u
后除以标准差std
,得到标准化后的结果h
,形状为(batch_size, dim_in, num_elements)
,其中num_elements
是输入张量的元素数量。 - 将标准化的结果
h
与缩放因子gamma
相乘,并加上偏移因子beta
,得到最终的输出。
- 接收输入张量(
总的来说,这段代码实现了有条件的实例标准化操作(CIN),根据给定的风格编码调整输入张量的均值、方差和标准差,并将调整后的结果与输入相结合,得到具有风格化效果的输出。
class ConditionalInstanceNormalisation(nn.Module):
"""
CIN block.
"""
def __init__(self, dim_in, style_num):
super(ConditionalInstanceNormalisation, self).__init__()
self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
self.dim_in = dim_in
self.style_num = style_num
self.gamma = nn.Linear(style_num, dim_in)
self.beta = nn.Linear(style_num, dim_in)
def forward(self, x, c):
u = torch.mean(x, dim=2, keepdim=True)
var = torch.mean((x - u) * (x - u), dim=2, keepdim=True)
std = torch.sqrt(var + 1e-8)
gamma = self.gamma(c.to(self.device))
gamma = gamma.view(-1, self.dim_in, 1)
beta = self.beta(c.to(self.device))
beta = beta.view(-1, self.dim_in, 1)
h = (x - u) / std
h = h * gamma + beta
return h
这段代码实现了一个残差块(Residual Block)模块。下面是代码功能的解释:
-
初始化函数(
__init__
):- 接收输入通道数(
dim_in
)、输出通道数(dim_out
)、卷积核大小(kernel_size
)、步长(stride
)、填充(padding
)以及风格编码数量(style_num
)作为参数。 - 创建一个一维卷积层(
self.conv_layer
),将输入通道数从dim_in
转换为输出通道数dim_out
。 - 创建一个自适应实例标准化模块(
self.adain
),接收输入通道数dim_out
和风格编码数量style_num
作为参数。 - 创建一个全局线性单元(GLU)模块(
self.glu
),用于将特征图的维度沿着第一个维度进行切分并进行门控操作。
- 接收输入通道数(
-
前向传播函数(
forward
):- 接收输入张量(
x
)和风格编码(c_
)作为参数。 - 将输入张量
x
通过一维卷积层self.conv_layer
,得到输出张量x_
。 - 将输出张量
x_
通过自适应实例标准化模块self.adain
,使用风格编码c_
对其进行风格化调整。 - 将风格化后的结果
x_
通过全局线性单元(GLU)模块self.glu
进行门控操作,得到最终的输出。 - 将最终的输出张量与原始输入张量
x
进行加和操作,得到残差块的输出结果。
- 接收输入张量(
总的来说,这段代码实现了一个残差块模块,它通过一维卷积、自适应实例标准化和门控操作对输入张量进行变换,然后将变换后的结果与原始输入进行残差连接,得到最终的输出结果。
#残差模块
class ResidualBlock(nn.Module):
def __init__(self, dim_in, dim_out, kernel_size, stride, padding, style_num):
super(ResidualBlock, self).__init__()
self.conv_layer = nn.Conv1d(in_channels=dim_in,
out_channels=dim_out,
kernel_size=kernel_size,
stride=stride,
padding=padding)
self.adain = AdaptiveInstanceNormalization(dim_in=dim_out, style_num=style_num)
# self.cin = ConditionalInstanceNormalisation(dim_in=dim_out, style_num=style_num)
self.glu = nn.GLU(dim=1)
def forward(self, x, c_):
x_ = self.conv_layer(x)
x_ = self.adain(x_, c_)
x_ = self.glu(x_)
return x + x_
这段代码实现了一个生成器(Generator)模块,用于音频转换任务。下面是代码功能的解释:
-
初始化函数(
__init__
):- 接收一个可选参数
num_speakers
,表示要生成的音频的说话者数量,默认为4。 - 创建一个
Generator
类的实例。 - 设置设备为GPU(如果可用)或CPU。
- 接收一个可选参数
-
前向传播函数(
forward
):- 接收输入张量(
x
),源说话者编码(c
)和目标说话者编码(c_
)作为参数。 - 将源说话者编码
c
和目标说话者编码c_
沿着第一个维度进行连接,得到一个新的张量c_onehot
。 - 获取输入张量的宽度大小。
- 将输入张量通过第一个卷积层
self.conv_layer_1
,得到输出张量x
。 - 将输出张量
x
通过第一个下采样层self.down_sample_1
,得到输出张量x
。 - 将输出张量
x
通过第二个下采样层self.down_sample_2
,得到输出张量x
。 - 将输出张量
x
重塑为三维张量,其形状为(-1, 2304, width_size // 4)
。 - 将重塑后的张量通过下采样模块
self.down_conversion
,得到输出张量x
。 - 将输出张量
x
通过一系列的残差块模块(self.residual_1
到self.residual_9
),并使用合并后的说话者编码c_onehot
对其进行风格化处理。 - 将输出张量
x
通过上采样模块self.up_conversion
,得到输出张量x
。 - 将输出张量
x
重塑为四维张量,其形状为(-1, 256, 9, width_size // 4)
。 - 将重塑后的张量通过第一个上采样层
self.up_sample_1
,得到输出张量x
。 - 将输出张量
x
通过第二个上采样层self.up_sample_2
,得到输出张量x
。 - 将输出张量
x
通过最后的卷积层self.out
,得到输出张量out
。 - 对输出张量
out
进行形状调整,去掉最后一个时间步的值,得到最终的输出结果out_reshaped
。
- 接收输入张量(
总的来说,这段代码实现了一个音频转换任务的生成器模块。它包含了一系列的下采样层、残差块层和上采样层,以及卷积层和实例标准化层等操作,用于将输入音频转换为目标音频。
#生成器
class Generator(nn.Module):
def __init__(self, num_speakers=4):
super(Generator, self).__init__()
self.num_speakers = num_speakers
self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# Initial layers.
self.conv_layer_1 = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=128, kernel_size=(5, 15), stride=(1, 1), padding=(2, 7)),
nn.GLU(dim=1)
)
# Down-sampling layers.
self.down_sample_1 = DownsampleBlock(dim_in=64,
dim_out=256,
kernel_size=(5, 5),
stride=(2, 2),
padding=(2, 2),
bias=False)
self.down_sample_2 = DownsampleBlock(dim_in=128,
dim_out=512,
kernel_size=(5, 5),
stride=(2, 2),
padding=(2, 2),
bias=False)
# Reshape data (This operation is done in forward function).
# Down-conversion layers.
self.down_conversion = nn.Sequential(
nn.Conv1d(in_channels=2304,
out_channels=256,
kernel_size=1,
stride=1,
padding=0,
bias=False),
nn.InstanceNorm1d(num_features=256, affine=True)
)
# Bottleneck layers.
self.residual_1 = ResidualBlock(dim_in=256,
dim_out=512,
kernel_size=5,
stride=1,
padding=2,
style_num=self.num_speakers * 2)
self.residual_2 = ResidualBlock(dim_in=256,
dim_out=512,
kernel_size=5,
stride=1,
padding=2,
style_num=self.num_speakers * 2)
self.residual_3 = ResidualBlock(dim_in=256,
dim_out=512,
kernel_size=5,
stride=1,
padding=2,
style_num=self.num_speakers * 2)
self.residual_4 = ResidualBlock(dim_in=256,
dim_out=512,
kernel_size=5,
stride=1,
padding=2,
style_num=self.num_speakers * 2)
self.residual_5 = ResidualBlock(dim_in=256,
dim_out=512,
kernel_size=5,
stride=1,
padding=2,
style_num=self.num_speakers * 2)
self.residual_6 = ResidualBlock(dim_in=256,
dim_out=512,
kernel_size=5,
stride=1,
padding=2,
style_num=self.num_speakers * 2)
self.residual_7 = ResidualBlock(dim_in=256,
dim_out=512,
kernel_size=5,
stride=1,
padding=2,
style_num=self.num_speakers * 2)
self.residual_8 = ResidualBlock(dim_in=256,
dim_out=512,
kernel_size=5,
stride=1,
padding=2,
style_num=self.num_speakers * 2)
self.residual_9 = ResidualBlock(dim_in=256,
dim_out=512,
kernel_size=5,
stride=1,
padding=2,
style_num=self.num_speakers * 2)
# Up-conversion layers.
self.up_conversion = nn.Conv1d(in_channels=256,
out_channels=2304,
kernel_size=1,
stride=1,
padding=0,
bias=False)
# Reshape data (This operation is done in forward function).
# Up-sampling layers.
self.up_sample_1 = UpSampleBlock(dim_in=256,
dim_out=1024,
kernel_size=(5, 5),
stride=(1, 1),
padding=2,
bias=False)
self.up_sample_2 = UpSampleBlock(dim_in=128,
dim_out=512,
kernel_size=(5, 5),
stride=(1, 1),
padding=2,
bias=False)
# TODO: The last layer differs from the paper.
self.out = nn.Conv2d(in_channels=64,
out_channels=1, # 35 in paper
kernel_size=(5, 15),
stride=(1, 1),
padding=(2, 7),
bias=False)
def forward(self, x, c, c_):
c_onehot = torch.cat((c, c_), dim=1).to(self.device)
width_size = x.size(3)
#print (x.shape)
x = self.conv_layer_1(x)
#print (x.shape)
x = self.down_sample_1(x)
#print (x.shape)
x = self.down_sample_2(x)
#print (x.shape)
x = x.contiguous().view(-1, 2304, width_size // 4)
#print (x.shape)
x = self.down_conversion(x)
#print (x.shape)
x = self.residual_1(x, c_onehot)
#print (x.shape)
x = self.residual_2(x, c_onehot)
#print (x.shape)
x = self.residual_1(x, c_onehot)
#print (x.shape)
x = self.residual_1(x, c_onehot)
#print (x.shape)
x = self.residual_1(x, c_onehot)
#print (x.shape)
x = self.residual_1(x, c_onehot)
#print (x.shape)
x = self.residual_1(x, c_onehot)
#print (x.shape)
x = self.residual_1(x, c_onehot)
#print (x.shape)
x = self.residual_1(x, c_onehot)
#print (x.shape)
x = self.up_conversion(x)
#print (x.shape)
x = x.view(-1, 256, 9, width_size // 4)
#print (x.shape)
x = self.up_sample_1(x)
#print (x.shape)
x = self.up_sample_2(x)
#print (x.shape)
out = self.out(x)
#print (out.shape)
out_reshaped = out[:, :, : -1, :]
#print (out.shape)
return out_reshaped
这段代码实现了一个鉴别器(Discriminator)模块,用于音频转换任务中的对抗训练过程。下面是代码功能的解释:
-
初始化函数(
__init__
):- 接收一个可选参数
num_speakers
,表示要生成的音频的说话者数量,默认为4。 - 创建一个
Discriminator
类的实例。 - 设置设备为GPU(如果可用)或CPU。
- 接收一个可选参数
-
前向传播函数(
forward
):- 接收输入张量(
x
),源说话者编码(c
)和目标说话者编码(c_
)作为参数。 - 将源说话者编码
c
和目标说话者编码c_
沿着第一个维度进行连接,得到一个新的张量c_onehot
。 - 将输入张量通过第一个卷积层
self.conv_layer_1
,并和经过门控线性单元(GLU)激活后的结果相乘,得到输出张量x
。 - 将输出张量
x
通过第一个下采样层self.down_sample_1
,得到输出张量x
。 - 将输出张量
x
通过第二个下采样层self.down_sample_2
,得到输出张量x
。 - 将输出张量
x
通过第三个下采样层self.down_sample_3
,得到输出张量x
。 - 将输出张量
x
通过第四个下采样层self.down_sample_4
,得到输出张量x_
。 - 对输出张量
x_
进行池化操作,通过对其在第2和第3维度求和,得到池化结果张量h
。 - 将池化结果张量
h
通过全连接层self.fully_connected
,得到输出张量x
。 - 将说话者编码的投影
p
通过线性映射层self.projection
,得到输出张量p
。 - 将投影结果张量
p
与池化结果张量h
按元素相乘,并在第1维度上求和,得到最终的输出结果x
。
- 接收输入张量(
总的来说,这段代码实现了一个音频转换任务的鉴别器模块。它包含了一系列的卷积层、下采样层、全连接层和线性映射层,用于对生成器生成的音频进行判别,判断其是否为目标说话者的音频。
#判别器
class Discriminator(nn.Module):
def __init__(self, num_speakers=4):
super(Discriminator, self).__init__()
self.num_speakers = num_speakers
self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# Initial layers.
self.conv_layer_1 = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=128, kernel_size=(3, 3), stride=(1, 1), padding=1),
nn.GLU(dim=1)
)
self.conv_gated_1 = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=128, kernel_size=(3, 3), stride=(1, 1), padding=1),
nn.GLU(dim=1)
)
# Down-sampling layers.
self.down_sample_1 = DownsampleBlock(dim_in=64,
dim_out=256,
kernel_size=(3, 3),
stride=(2, 2),
padding=1,
bias=False)
self.down_sample_2 = DownsampleBlock(dim_in=128,
dim_out=512,
kernel_size=(3, 3),
stride=(2, 2),
padding=1,
bias=False)
self.down_sample_3 = DownsampleBlock(dim_in=256,
dim_out=1024,
kernel_size=(3, 3),
stride=(2, 2),
padding=1,
bias=False)
self.down_sample_4 = DownsampleBlock(dim_in=512,
dim_out=1024,
kernel_size=(1, 5),
stride=(1, 1),
padding=(0, 2),
bias=False)
# Fully connected layer.
self.fully_connected = nn.Linear(in_features=512, out_features=1)
# Projection.
self.projection = nn.Linear(self.num_speakers * 2, 512)
def forward(self, x, c, c_):
c_onehot = torch.cat((c, c_), dim=1).to(self.device)
#print (x.shape)
x = self.conv_layer_1(x) * torch.sigmoid(self.conv_gated_1(x))
#print (x.shape)
x = self.down_sample_1(x)
#print (x.shape)
x = self.down_sample_2(x)
#print (x.shape)
x = self.down_sample_3(x)
#print (x.shape)
x_ = self.down_sample_4(x)
#print (x.shape)
h = torch.sum(x_, dim=(2, 3)) # sum pooling
#print (h.shape)
x = self.fully_connected(h)
#print (x.shape)
p = self.projection(c_onehot)
#print (p.shape)
x += torch.sum(p * h, dim=1, keepdim=True)
#print (x.shape)
return x
总结
Discriminator的作用就是用来判别给定的image是否是符合要求的图片[也就是real_image]。本质上,Generator生成的输出 Y 要能够骗过Discriminator,也就是使得Discriminatol的输出为1。这样代表生成质量还可以,如果不行,Generator就继续向着能够骗到Discriminator的方向学,如果能够骗过,Discriminator也是会学习的,他会寻找Generator生成的和real_image之间的差别,进一步提升辨别能力。这样的对抗过程使得Generator能力越来越强。