当前位置:   article > 正文

论文代码学习—HiFi-GAN(1)——生成器generator代码

论文代码学习—HiFi-GAN(1)——生成器generator代码

引言

  • 这里翻译了HiFi-GAN这篇论文的具体内容,具体链接
  • 这篇文章还是学到了很多东西,从整体上说,学到了生成对抗网络的构建思路,包括生成器和鉴定器。细化到具体实现的细节,如何 实现对于特定周期的数据处理?在细化,膨胀卷积是如何实现的?这些通过文章,仅仅是了解大概的实现原理,但是对于代码的实现细节并不是很了解。如果要加深印象,还是要结合代码来具体看一下实现的细节。
  • 本文主要围绕具体的代码实现细节展开,对于相关原理,只会简单引用和讲解。因为官方代码使用的是pytorch,所以是通过pytorch展开的。
  • 因为篇幅限制,这部分仅仅讲述了整个项目的具体架构,详细读了,对应的readme.md文件,并对model.py文件中的generator生成器进行代码分析。

正文

摘要

  • 本文主要结合代码介绍HiFi-GAN的具体实现,主要从一下几个方面
    • 生成器的具体实现,包括损失函数和网络架构
      *多感知区域融合模块
    • 鉴别器的具体实现
      • 多尺度鉴别器的实现:如何实现连续特征的获取,长依赖路径的获取
      • 多感知域鉴别器的实现:膨胀卷积网络的实现,对于残差模块的修改
    • 鉴别器和生成器的整体链接
  • 在对于代码学习的过程中,除了会加深原理的理解之外,还会运行相关代码,查看运行结果。
  • 论文翻译和讲解的具体地址,论文翻译

整体原理回顾

  • 梅尔频谱图输入到生成器中,然后输出的是原始波形图

    • 生成器是有若干个反卷积单元构成,
    • 反卷积单元:反卷积层和多感知融合模块构成
    • 多感知域融合模块:若干个残差模块的累加和
    • 残差模块:若干个膨胀卷积修改
  • 具体的结构图如下
    在这里插入图片描述

  • 生成器总结

    • 生成器在各个层次都涉及到了不同尺度和不同感知域的信息融合
      • 生成器的每一个子模块,是不同上采样的结果,最后累加和,不同尺度的信息进行融合
      • 在每一个子模块的MRF多感知域融合模块,也是涉及到了不同卷积核的残差模块累加
      • 在精细化,每一个残差模块又涉及到不同的膨胀卷积层的累加和
    • 每一个层次都涉及到了不同尺度的信息的累加和,所以信息获取的更加全面。

readme.md文件阅读

  • 在作者提供的项目中,作者提供了具体的训练方法并且提供了预训练好的模型。
Pre-requisites先决条件
  • Python >= 3.6
  • 克隆本仓库的具体内容,链接
  • 按照 “requirements.txt”安装对应的需求包
  • 下载LJ语音数据并提取出对应的内容,同时将所有的音频文件移动LJSpeech-1.1/wavs路径下方
训练Training
  • 训练指令,见下方
python train.py --config config_v1.json
  • 1
  • 如果要训练V2或者V3版本的生成器具,使用config_v1.json或者config_v3.json替代config_v1.json。配置文件的备份和身成模型将会默认保存在cp_hifigan目录下方
  • 如果你要将之保存到其他的地方,你可以在训练的时加上“–checkpoint_path”参数
  • 下图是在训练V1生成器时的loss变化情况(这训练的太久了,我根本跑不了那么多,估计要自己训练,太费劲了)
    在这里插入图片描述
Pretrained Model预训练模型
  • 你可以使用我们提供的已经训练好的模型,链接

  • 训练模型的具体细节如下,你可以按照自己的需要选取。我们提供了带有鉴别器权重的统一模型,可以用来作为迁移训练的基础。(我估计在vq-vae模型中,人家就对hifi-gan模型进行了迁移学习)
    在这里插入图片描述

  • 表格介绍

    • LJSpeech
      • 是一个广泛使用的英文文本到语音(TTS)任务的音频数据集,用于训练和评估文本到语音合成模型。它由英国爱丁堡大学的合成和语音技术小组创建,包含一系列女性说话人的单声道语音音频和对应的文本。
    • VCTK
      • VCTK(Voice Cloning Toolkit)是一个流行的英语语音数据集,用于语音合成、语音识别和其他语音相关任务的研究和开发。该数据集由剑桥大学的计算机实验室创建,目的是为了促进语音合成和语音相关技术的发展。
    • Tacotron2
      • 端到端的语音合成模型,将文本转换为相应的语音音频,谷歌开发的
      • 该模型将文本映射为mel频谱图,然后使用WaveNet将mel频谱图转变为高质量的语音音频。
      • 使用自注意机制,来捕捉输入文本的上下文关系,一边更好理解文本的语义。同时还使用全局注意力机制,用于在生成mel频谱图时聚焦输入文本的重要部分
Fine-Tuning微调过程
  • 1、使用教师强制策略(每一次输入给模型重复训练的并不是模型自己预测的数据,而是真实数据)和Tacotron2生成mel频谱图并将之保存为numpy形式。生成的mel频谱图将会保存为.npy格式。这里是使用Tacotron2将音频转为mel频谱图。
  • 样例
Audio File : LJ001-0001.wav
 Mel-Spectrogram File : LJ001-0001.npy
  • 1
  • 2
  • 2、创建ft_dataset文件,并将生成的mel频谱图复制进去
  • 3、运行如下指令
python train.py --fine_tuning True --config config_v1.json
  • 1
Inference from wav file从wav音频文件中进行推理
  • 1、创建一个名为test_file的目录,并将需要推理的wav文件复制到对应的目录中

  • 2、运行如下指令

python inference.py --checkpoint_file [generator checkpoint file path]
  • 1
  • 生成的wav文件将保存在generated_files文件中
Inference for end-to-end speech synthesis用与端到端语音合成模型的推理
  • 1、创建test_mel_files目录,并将生成的mel频谱图复制到对应的目录中,你可以使用Tacotron2或者Glow-TTS生成对应的mel频谱图
  • 2、运行如下指令,生成的wav文件将会保存在generated_files_from_mel中
python inference_e2e.py --checkpoint_file [generator checkpoint file path]
  • 1

项目目录分析

  • 具体目录截图如下
    在这里插入图片描述
  • 具体介绍如下
    • config_v1.json——config_v3.json:模型的训练的配置文件,包括卷积层的核心、膨胀系数等模型参数
    • env.py文件:根据参数创建默认设置之外的文件,保存文件和生成模型的地方,都有默认的地方,这里检测一下,如果不是就自动创建相关文件
    • inference.py文件:调用训练好的生成器模型,根据给出的wav文件,生成新的对应的wav文件。会将音频文件转成mel频谱图,然后根据mel频谱图生成新的声音。
    • inference_e2e.py文件:较之与inference.py文件,这个文件直接将生成好的mel频谱图生成对应的波形图。主要是用来和别的模块进行结合。
    • meldataset.py文件:专门用来处理音频的数据包,包括加载音频文件、计算梅尔频谱图、以及创建用于训练的数据集合
    • models.py文件:定义了生成器和判别器模型的具体代码,包括生成器以及改良的残差模块,两种鉴别器,以及相关的损失函数。
    • train.py文件:定义了模型的具体训练过程,主要是训练函数,它首先初始化模型和优化器,然后加载数据集,并开始训练循环。在每个循环中,它首先计算判别器的损失并更新判别器的权重,然后计算生成器的损失并更新生成器的权重。它还包含了一些用于记录训练进度和保存模型的代码。
    • uitls.py文件:定义了一些训练模型中常用的函数,主要用于模型的初始化、权重归一化、图像绘制和检查点的加载和保存。

具体代码分析

models.py文件代码
Generator生成器具体讲解
  • 生成器主要负责根据梅尔频谱图生成对应的模型图。是由若干个不同时间频率的反卷积模块构成,具体见下图。

在这里插入图片描述

  • 重点在于反卷积模块的实现,每一个反卷积模块都是由一个不同尺度下的反卷积层和一个MRF多感知域融合模块构成,多感知融合模块MRF是由多个残差模块构成,每一个残差模块是由多个膨胀卷积层构成。具体见下图。

在这里插入图片描述

多感知域MRF中残差模块实现
  • 整个生成器都是通过多个残差模块的堆叠实现的,多感知领域融合,又是通过使用膨胀卷积实现的,项目具体实现过程中,出现了两种残差模块,这里分别根据代码进行介绍。

残差模块一

  • 具体代码
class ResBlock1(torch.nn.Module):
    def __init__(self, h, channels, kernel_size=3, dilation=(1, 3, 5)):
        # h: hyperparameters超参数
        # channels: 通道数
        # kernel_size: 卷积核大小
        # dilation: 膨胀率
        super(ResBlock1, self).__init__()
        self.h = h

        # 定义一个卷积层列表,并且每一个卷积层后面都跟着一个weight_norm,进行权重归一化
        self.convs1 = nn.ModuleList([
            weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[0],
                               padding=get_padding(kernel_size, dilation[0]))),
            weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[1],
                               padding=get_padding(kernel_size, dilation[1]))),
            weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[2],
                               padding=get_padding(kernel_size, dilation[2])))
        ])
        # 对每一个卷积层进行初始化,调用了utils.py中的init_weights函数
        self.convs1.apply(init_weights)

        self.convs2 = nn.ModuleList([
            weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1,
                               padding=get_padding(kernel_size, 1))),
            weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1,
                               padding=get_padding(kernel_size, 1))),
            weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1,
                               padding=get_padding(kernel_size, 1)))
        ])
        self.convs2.apply(init_weights)

    def forward(self, x):
        # 迭代遍历两个卷积列表中的每一对
        for c1, c2 in zip(self.convs1, self.convs2):
            xt = F.leaky_relu(x, LRELU_SLOPE)
            xt = c1(xt)
            xt = F.leaky_relu(xt, LRELU_SLOPE)
            xt = c2(xt)
            x = xt + x
        return x

    def remove_weight_norm(self):
        # 权重归一化可以帮助更好的训练模型
        # 但是在测试的时候,权重归一化会影响模型的性能,所以在测试的时候需要移除权重归一化
        for l in self.convs1:
            remove_weight_norm(l)
        for l in self.convs2:
            remove_weight_norm(l)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • resnet block1是分为六层,然后间隔使用了不同膨胀率的膨胀卷积进行计算,然后中间是使用了正常的一维卷积,膨胀率的列表如下{1,1,1,3,1,5}
  • 对于resnetblock1的具体网络结构如下:

在这里插入图片描述

残差模块二

  • 具体代码如下
class ResBlock2(torch.nn.Module):
    def __init__(self, h, channels, kernel_size=3, dilation=(1, 3)):
        super(ResBlock2, self).__init__()
        self.h = h
        # 两个卷积列表,并且每一个卷积后面都接上了一个weight_norm
        self.convs = nn.ModuleList([
            weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[0],
                               padding=get_padding(kernel_size, dilation[0]))),
            weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[1],
                               padding=get_padding(kernel_size, dilation[1])))
        ])
        self.convs.apply(init_weights)

    def forward(self, x):
        for c in self.convs:
            xt = F.leaky_relu(x, LRELU_SLOPE)
            xt = c(xt)
            x = xt + x
        return x

    def remove_weight_norm(self):
        # 权重归一化可以帮助更好的训练模型
        # 但是在测试的时候,权重归一化会影响模型的性能,所以在测试的时候需要移除权重归一化
        for l in self.convs:
            remove_weight_norm(l)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 这个 残差模块二,较之于残差模块一,就是膨胀率为3膨胀卷积和正常的卷积交错使用,膨胀率列表为{1,3,1,3,1,3}
  • 具体示意图如下

在这里插入图片描述

Generator生成器代码
  • 具体描述:生成器是全卷积神经网络,使用梅尔频谱图作为输入,然后通过反卷积对其进行上采样,直到输出序列的长度和原始波形图的时间分辨率相匹配。
  • 注意:这里是使用一维卷积,虽然梅尔频谱图是二维数据,横坐标是时间,纵坐标是频率,我们不直接对这个二维频谱图进行卷积,而是将频谱图的每一列看作是高维数据点,然后对这些高维数据点进行一维卷积
  • 生成器模型示意图
    • 整个生成器是由四部分构成,具体如下,注意,中间两个上采样层和残差模块是交叉实现的。
      • 前卷积层conv_pre:Conv1d(80, h.upsample_initial_channel, 7, 1, padding=3)
      • 上采样层ups:通过反卷积对输入的数据进行上采样,将低分辨率的梅尔频谱图转换成高分辨率的梅尔频谱图。
        • 上采样卷积核大小(upsample_kernel_sizes):上采样过程中,反卷积使用的卷积核大小,卷积核越多,获取的局部特征更加全面。
        • 上采样的倍数(upsample_rates):指上采样的过程中,输出的尺寸是输入尺寸的多少倍。比如说,如果这个值设置为2,那么采样之后的尺寸将会是输入尺寸的两倍。
      • 残差模块resnetBlocks:通过膨胀卷积实现,和上采样层交叉排列。然后在进行累加
      • 后卷积层post:weight_norm(Conv1d(ch, 1, 7, 1, padding=3)),最终的输出通道和原始的输入通道相同。

在这里插入图片描述

  • 总结,这部分是将梅尔频谱图一个80channel的信号,通过生成器,变成一个只有一个channel的波形图。

具体代码

class Generator(torch.nn.Module):

    def __init__(self, h):
        super(Generator, self).__init__()
        self.h = h
        # num_kernels是
        self.num_kernels = len(h.resblock_kernel_sizes)
        # num_upsamples是上采样的次数
        self.num_upsamples = len(h.upsample_rates)
        # 定义一个预处理卷积层,并且后面跟着一个weight_norm
        # 带有权重归一化的卷积层,输入通道数为80,输出通道数为h.upsample_initial_channel,卷积核大小为7,步长为1,padding为3
        self.conv_pre = weight_norm(Conv1d(80, h.upsample_initial_channel, 7, 1, padding=3))
        # 确定所使用的残差块类型
        resblock = ResBlock1 if h.resblock == '1' else ResBlock2

        '''
            定义上采样层模块
        '''
        self.ups = nn.ModuleList()
        # usample_rates是上采样的倍数,upsample_kernel_sizes是上采样的卷积核大小
        for i, (u, k) in enumerate(zip(h.upsample_rates, h.upsample_kernel_sizes)):
            self.ups.append(weight_norm(
                # ConvTranspose1d是一维的反卷积层,输入通道数为h.upsample_initial_channel//(2**i),
                # 输出通道数为h.upsample_initial_channel//(2**(i+1)),卷积核大小为k,步长为u,padding为(k-u)//2
                # 在对代码进行上采样的时候,使用的是反卷积,并且对通道数进行了缩减
                ConvTranspose1d(h.upsample_initial_channel//(2**i), h.upsample_initial_channel//(2**(i+1)),
                                k, u, padding=(k-u)//2)))

        '''
            定义残差模块
        '''
        self.resblocks = nn.ModuleList()
        for i in range(len(self.ups)):
            # 初始化残差模块,输入通道数为h.upsample_initial_channel//(2**(i+1)),
            # 输出通道数为h.upsample_initial_channel//(2**(i+1)),卷积核大小为3,步长为1,padding为1
            # 每一个上采样部分都有一个残差模块,这个残差模块的输入通道数和输出通道数都是h.upsample_initial_channel//(2**(i+1))
            ch = h.upsample_initial_channel//(2**(i+1))
            for j, (k, d) in enumerate(zip(h.resblock_kernel_sizes, h.resblock_dilation_sizes)):
                self.resblocks.append(resblock(h, ch, k, d))

        # 定义一个后处理卷积层,并且后面跟着一个weight_norm
        # 这个卷积层的输入通道数为h.upsample_initial_channel//(2**len(self.ups)),输出通道数为1,卷积核大小为7,步长为1,padding为3
        self.conv_post = weight_norm(Conv1d(ch, 1, 7, 1, padding=3))
        self.ups.apply(init_weights)
        self.conv_post.apply(init_weights)

    def forward(self, x):
        # 定义前向传播过程
        x = self.conv_pre(x)
        for i in range(self.num_upsamples):
            x = F.leaky_relu(x, LRELU_SLOPE)
            x = self.ups[i](x)
            xs = None
            for j in range(self.num_kernels):
                if xs is None:
                    xs = self.resblocks[i*self.num_kernels+j](x)
                else:
                    xs += self.resblocks[i*self.num_kernels+j](x)
            x = xs / self.num_kernels
        x = F.leaky_relu(x)
        x = self.conv_post(x)
        x = torch.tanh(x)

        return x

    def remove_weight_norm(self):
        # 权重归一化可以帮助更好的训练模型
        # 但是在测试的时候,权重归一化会影响模型的性能,所以在测试的时候需要移除权重归一化
        print('Removing weight norm...')
        for l in self.ups:
            remove_weight_norm(l)
        for l in self.resblocks:
            l.remove_weight_norm()
        remove_weight_norm(self.conv_pre)
        remove_weight_norm(self.conv_post)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
问题
  • 多个上采样层,是如何叠加到一块的?

    • 所有上采样层和残差模块都是在同一个维度(通道维度)上进行操作的,所以输出是可以直接相加的。
    • 在整个上采样的过程中,不同的上采样层和残差模块会对输入x进行不同的变换,从而提取出不同的特征,然后,这些特征被相加,形成更加丰富的特征表示。。
    • 最后还对所有残差模块的输出进行平均,防止模型的输出过于以来某一个特定残差模块
  • 为什么对梅尔频谱图进行多次上采样?

    • 虽然梅尔频谱图是音频信号的一种表示,但是不同于原始的音频信号,将原始的音频信号转成梅尔频谱图的过程中,会对是很多信息,比如说相位信息。所以,梅尔频谱图的时间分辨率远远低于原始的音频信号
    • 通过上采样,确保梅尔频谱图的时间分辨率和接近原始的音频信号,提高生成的波形图的质量。

总结

  • 本来想将所有的代码分析还有数据都写在同一个博客里面,但是一看一下,已经一万多字了,就暂时先到这里,后面关于鉴别器、损失函数以及具体的训练函数的代码,在后续的博客将继续跟进。

  • 这个生成器的代码看起来还是有点问题,对于每一个模块中间输出的张量大小,并没有任何的了解。这个要具体的跑起来才能看到,等我把整个模型都看完了,在放到服务器上跑一下。

  • 可以加群一块讨论一下关于声音生成的技术

    • 群号:722462964
      在这里插入图片描述
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/262182
推荐阅读
相关标签
  

闽ICP备14008679号