Stargan-vc2 代码阅读笔记


前言

  非并行多域语音转换(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)

 不会…

判别器

  1. GSP:global sum pooling:一个特征图压缩成一个点,batch512hw,压缩成batch512的点
  2. 然后,标签通过embeding编码成512维特征(batch*512),内积得到batch个判别结果
    在这里插入图片描述

代码

  1. preprocess.py

这段代码执行了以下操作:

  1. 导入所需的库和模块。
  2. 定义了命令行参数解析器(ArgumentParser)。该解析器用于从命令行输入的参数中解析出指定的参数值。
    • 定义了--dataset参数,用于指定数据集名称,默认值为VCC2016,可选值为VCC2016VCC2018
    • 定义了--input_dir参数,用于指定输入数据的目录,默认值为./data/spk
    • 定义了--output_dir参数,用于指定处理后数据的输出目录,默认值为./data/processed
  3. 解析命令行参数,并将解析后的值保存到相应的变量中。
  4. 创建输出目录(如果不存在)。
  5. 根据选择的数据集,设置采样率。
    • 如果数据集为VCC2016,则采样率为16000 Hz。
    • 如果数据集为VCC2018,则采样率为22050 Hz。
  6. 调用wav_to_mcep_file函数,将输入目录中的音频文件转换为梅尔倒谱系数(MCCs),并保存到输出目录中的文件中。
  7. 实例化GenerateStatistics类的对象,并指定数据集的输出目录。
  8. 调用generate_stats方法,生成统计特征(均值和标准差)。
  9. 调用normalize_dataset方法,对数据集进行归一化处理。
  10. 输出程序执行的持续时间。

  总的来说,这段代码执行了音频数据的预处理过程,包括将音频转换为梅尔倒谱系数(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:采样率。

函数的流程如下:

  1. 创建一个空字典data,用于存储音频文件的路径。
  2. 使用os.scandir遍历数据集目录中的文件和子目录。
  3. 对于每个子目录,将其作为键添加到data字典中,并创建一个空列表作为对应的值。
  4. 使用os.scandir遍历子目录中的音频文件。
  5. 对于每个音频文件,将其路径添加到相应子目录的列表中。
  6. 输出加载的键(子目录名称)。
  7. 创建一个空字典resdict,用于存储处理后的音频数据。
  8. 初始化计数器cnt为0。
  9. 遍历data字典的键值对,进行音频文件的预处理。
  10. 对于每个音频文件路径,提取文件名作为新的键。
  11. 使用librosa.load函数加载音频文件,将其转换为浮点数类型的波形数据。
  12. 使用librosa.effects.trim函数对波形数据进行修剪,突出高频信号。
  13. 应用预加重滤波器,将修剪后的波形数据附加到之前的采样点后面。
  14. 将处理后的波形数据添加到resdict字典中对应的子目录和文件键中。
  15. 输出进度点"."(每处理一个音频文件输出一个点)。
  16. 递增计数器cnt
  17. 输出总音频文件数量。
  18. 返回处理后的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:块的大小,指定每个子块的元素个数。

函数的流程如下:

  1. 使用range函数生成一个以size为步长的索引序列,遍历这个索引序列。
  2. 在每次迭代中,根据当前索引截取iterable中的连续子序列,子序列从当前索引开始,长度为size
  3. 使用yield语句将每个子序列作为生成器的输出。
  4. 持续迭代直到所有的子块都被生成和返回。

  总的来说,这段代码实现了将一个可迭代对象拆分为连续的子块,每个子块包含指定数量的元素。它通过生成器的方式返回拆分后的子块,可以在需要的时候逐个获取子块,而不是一次性生成和返回所有的子块。

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"。

函数的流程如下:

  1. 使用shutil.rmtree函数删除已存在的processed_filepath目录及其内容。
  2. 使用os.makedirs函数创建processed_filepath目录,如果目录已存在则不进行任何操作。
  3. 使用glob.glob函数计算数据集中所有音频文件的数量,并输出总音频文件数量。
  4. 调用load_wavs函数加载和预处理音频数据,将结果存储在字典d中。
  5. 遍历字典d中的每个说话者。
  6. 获取当前说话者的音频数据列表values_of_one_speaker
  7. 使用chunks函数将音频数据列表拆分为指定大小的块one_chunk
  8. 初始化一个空列表wav_concated用于存储拼接后的音频数据。
  9. 对于每个块,将其数据拼接到wav_concated列表中。
  10. wav_concated列表转换为NumPy数组wav_concated
  11. 调用cal_mcep函数计算MCEP特征,得到基频(f0)、频谱包络(ap)和梅尔频谱倒谱系数(mcep)。
  12. 根据说话者名称和块索引生成新的名称newname
  13. 构建特征文件路径file_path_z,使用np.savez函数将f0和mcep保存为NPZ文件。
  14. 输出保存特征文件的消息。
  15. 对于每个块中的每个帧,在MCEP矩阵中滑动窗口,每次取连续的FRAMES帧作为一个音频片段。
  16. 如果音频片段的帧数等于FRAMES,则生成临时文件名temp_name并构建保存路径filePath,使用np.save函数将音频片段保存为NPY文件。
  17. 输出保存音频片段文件的消息。

  总的来说,这段代码实现了将音频文件转换为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')
  1. solver.py

    1. 恢复或加载预训练的模型。
    2. 从指定的测试目录读取数据,包括源说话者的音频数据和说话者标签。
    3. 获取目标说话者列表。
    4. 对于每个目标说话者,进行以下操作:
      • 将源说话者和目标说话者的标签进行编码转换。
      • 对于给定的音频文件和内容,进行以下操作:
        • 提取声音特征,包括基频(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)
  1. model.py

这段代码定义了一个下采样模块(DownsampleBlock)的类,实现了声音特征的下采样操作。以下是它的主要功能:

  1. 初始化函数(__init__):

    • 接收输入和输出的通道数(dim_indim_out)。
    • 定义卷积层、实例归一化层和GLU激活函数组成的网络模块,用于特征的下采样。
  2. 前向传播函数(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)的类,实现了声音特征的上采样操作。以下是它的主要功能:

  1. 初始化函数(__init__):

    • 接收输入和输出的通道数(dim_indim_out)。
    • 定义转置卷积层、像素洗牌层(PixelShuffle)和GLU激活函数组成的网络模块,用于特征的上采样。
  2. 前向传播函数(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)模块,它的作用是对输入进行实例标准化,并应用风格编码来调整输出。以下是代码的功能解释:

  1. 初始化函数(__init__):

    • 接收输入通道数(dim_in)和风格编码的数量(style_num)作为参数。
    • 初始化设备类型为CUDA或CPU,并创建一个全连接层(self.fc)用于将风格编码映射为适应实例标准化所需的参数。
  2. 前向传播函数(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张量沿着第二个维度分割成两个张量,分别赋给gammabeta,维度为(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)。下面是代码功能的解释:

  1. 初始化函数(__init__):

    • 接收输入通道数(dim_in)和风格编码的数量(style_num)作为参数。
    • 初始化设备类型为CUDA或CPU,并创建两个线性层(self.gammaself.beta)用于将风格编码映射为每个通道的缩放因子和偏移因子。
  2. 前向传播函数(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)模块。下面是代码功能的解释:

  1. 初始化函数(__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),用于将特征图的维度沿着第一个维度进行切分并进行门控操作。
  2. 前向传播函数(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)模块,用于音频转换任务。下面是代码功能的解释:

  1. 初始化函数(__init__):

    • 接收一个可选参数num_speakers,表示要生成的音频的说话者数量,默认为4。
    • 创建一个Generator类的实例。
    • 设置设备为GPU(如果可用)或CPU。
  2. 前向传播函数(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_1self.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)模块,用于音频转换任务中的对抗训练过程。下面是代码功能的解释:

  1. 初始化函数(__init__):

    • 接收一个可选参数num_speakers,表示要生成的音频的说话者数量,默认为4。
    • 创建一个Discriminator类的实例。
    • 设置设备为GPU(如果可用)或CPU。
  2. 前向传播函数(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能力越来越强。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值