AUTOVC 代码解析 —— model_vc.py
文章目录
简介
本项目一个基于 AUTOVC 模型的语音转换项目,它是使用 PyTorch 实现的(项目地址)。
AUTOVC 遵循自动编码器框架,只对自动编码器损耗进行训练,但它引入了精心调整的降维和时间下采样来约束信息流,这个简单的方案带来了显著的性能提高。(详情请参阅 AUTOVC 的详细介绍)。
由于 AUTOVC 项目较大,代码较多。为了方便学习与整理,将按照工程文件的结构依次介绍。
本文将介绍项目中的 model_vc.py 文件:设计了内容编码器,解码器,后置网络等网络模型。
类解析
LinearNorm
该类的意义为:线性规范层
LinearNorm 继承 PyTorch 中网络模型的基类: torch.nn.Module ;
故需要重载两个函数:__init__ 与 forward 。
下面依次介绍 LinearNorm 类的成员函数
__ init __
该函数的作用是: 创建 LinearNorm 线性规范层需要的元素。
输入参数:
in_dim : 输入数据大小
out_dim : 输出数据大小
bias : 偏置系数建立标志,默认为 True
w_init_gain : 非线性函数名,默认为 'linear'
输出参数: 无
代码详解:
def __init__(self, in_dim, out_dim, bias=True, w_init_gain='linear'):
# 调用父类初始化函数,初始化模型基本参数
super(LinearNorm, self).__init__()
# torch.nn.Linear 对输入的数据应用线性变换: ' y = xA^T + b '
# in_dim 为输入样本大小;out_dim 为输出样本大小bias 为加法偏差标志,若为 'False' 则不学习加法偏置,反之学习
# 在这里创建了一个成员函数 linear_layer 应用上述的线性层
self.linear_layer = torch.nn.Linear(in_dim, out_dim, bias=bias)
# torch.nn.init.xavier_uniform_是一个服从均匀分布的 Glorot 初始化器
# 表达式为: u = (−a, a) ;其中,a = gain * (6 / (fan_in + fan_out))^(1/2)
# 使用 xavier_uniform_ 初始化,通过网络层时,输入和输出的方差相同
# self.linear_layer.weight 作为被初始化的 Tensor ,gain 是一个可选的比例因子
# torch.nn.init.calculate_gain 能根据 w_init_gain (非线性函数名)返回给定非线性函数的推荐增益值
torch.nn.init.xavier_uniform_(
self.linear_layer.weight,
gain=torch.nn.init.calculate_gain(w_init_gain))
forward
该函数的作用是: 定义了执行网络模型的计算过程
输入参数:
x : 输入网络的数据样本
输出参数:
self.linear_layer(x) : 通过上文定义的网络模型计算的结果
代码详解:
def forward(self, x):
# 没什么特别的,直接走网络模型计算
return self.linear_layer(x)
ConvNorm
该类的意义为:卷积规范层
ConvNorm 继承 PyTorch 中网络模型的基类: torch.nn.Module ;
故需要重载两个函数:__init__ 与 forward 。
下面依次介绍 ConvNorm 类的成员函数
__ init __
该函数的作用是: 创建 ConvNorm 线性规范层需要的元素。
输入参数:
in_channels : 输入数据通道数
out_channels : 输出数据通道数
stride : 步幅大小,默认为 1
padding : 填补大小膨胀系数,默认为 None
dilation : 默认为 1
bias : 偏差学习标志,默认为 True
w_init_gain : 非线性函数名,默认为 'linear'
输出参数: 无
代码详解:
def __init__(self, in_channels, out_channels, kernel_size=1, stride=1,
padding=None, dilation=1, bias=True, w_init_gain='linear'):
# 调用父类初始化函数,初始化模型基本参数
super(ConvNorm, self).__init__()
# 若 padding 不存在,则计算一个合适的值,使处于边界的数据也能卷积
if padding is None:
# 若 kernel_size 不为奇数则警告
assert(kernel_size % 2 == 1)
# 计算合适的 padding 以满足处在边界的数据能被卷积层覆盖
padding = int(dilation * (kernel_size - 1) / 2)
# torch.nn.Conv1d 能对由几个输入平面组成的输入信号应用一维卷积;
# in_channels 为输入数据的通道数,out_channels 为输出数据的通道数,kernel_size 为卷积核函数的大小;
# stride 为卷积的步幅,padding 为在输入的两边加零填充的数量,dilation 为核元素间距,bias 为偏差学习标志;
# 这里创建了一个成员函数 conv 应用上述的 1 维卷积层。
self.conv = torch.nn.Conv1d(in_channels, out_channels,
kernel_size=kernel_size, stride=stride,
padding=padding, dilation=dilation,
bias=bias)
# torch.nn.init.xavier_uniform_ 介绍见上一个类:LinearNorm 的 __init__ 函数
# self.conv.weight 作为被初始化的 Tensor ,gain 是一个可选的比例因子
# torch.nn.init.calculate_gain 能根据 w_init_gain (非线性函数名)返回给定非线性函数的推荐增益值
torch.nn.init.xavier_uniform_(
self.conv.weight, gain=torch.nn.init.calculate_gain(w_init_gain))
forward
该函数的作用是: 定义了执行网络模型的计算过程
输入参数:
signal : 输入网络的数据样本
输出参数:
conv_signal : 通过上文定义的网络模型计算的结果
代码详解:
def forward(self, signal):
# 还是没什么特别的,走网络模型计算赋值给中间变量,然后直接返回
conv_signal = self.conv(signal)
return conv_signal
Encoder
该类的意义为:内容编码器
Encoder 继承 PyTorch 中网络模型的基类: torch.nn.Module ;
故需要重载两个函数:__init__ 与 forward 。
下面依次介绍 Encoder 类的成员函数
__ init __
该函数的作用是: 创建 Encoder 内容编码器需要的元素。
输入参数:
dim_neck : 输出数据维数
dim_emb : 说话人编码数据维数
freq : 采样系数
输出参数: 无
代码详解:
def __init__(self, dim_neck, dim_emb, freq):
# 调用父类初始化函数,初始化模型基本参数
super(Encoder, self).__init__()
# 赋值内容编码器输出维度(采样前)
self.dim_neck = dim_neck
# 赋值内容编码器采样系数
self.freq = freq
# 创建卷积列表
convolutions = []
# 创建 3 个 5 * 1 卷积规范层
for i in range(3):
# torch.nn.Sequential 是一个 Sequential 容器,模块将按照构造函数中传递的顺序添加到模块中,也可以传入一个有序模块。
# 这里将一个卷积规范层与一个批规范层进行组合
conv_layer = nn.Sequential(
# 若是第 1 层,则输入数据维度为 80+dim_emb ,否则为 512 ,输出维度为 512
# 卷积核大小为 5 ,步幅为 1 ,填充数量为 2 ,膨胀系数为 1 ,非线性激活函数为 'relu'
ConvNorm(80+dim_emb if i==0 else 512,
512,
kernel_size=5, stride=1,
padding=2,
dilation=1, w_init_gain='relu'),
# BatchNorm 就是在深度神经网络训练过程中使得每一层神经网络的输入保持相同分布,512 为输入数据的维度
nn.BatchNorm1d(512))
# 将组合成的(卷积规范+批规范)层添加进卷积列表
convolutions.append(conv_layer)
# nn.ModuleList 是一个储存不同 module,并自动将每个 module 的 parameters 添加到网络之中的容器
# 将上述的卷积组赋值给成员函数 convolutions
self.convolutions = nn.ModuleList(convolutions)
# 构架 LSTM 层,输入数据维数为 512 ,输出数据维数为 dim_neck ,设置输入输出数据格式为(批处理,序列,特性),并设置为双向 LSTM 层。
# 创建成员函数 lstm ,应用上述构建的 BLSTM 层
self.lstm = nn.LSTM(512, dim_neck, 2, batch_first=True, bidirectional=True)
forward
该函数的作用是: 定义了执行网络模型的计算过程
输入参数:
x : 输入网络的数据样本
c_org : 说话人编码
输出参数:
codes : 采样编码列表
代码详解:
def forward(self, x, c_org):
# 若 x 中维度 1 的维数是 1 ,则压缩
# 接着将 x 的维度 1 与维度 2 交换
x = x.squeeze(1).transpose(2,1)
# 将 c_org 末尾增加一个维度,并将最后一个维度拓展到与 x 最后一个维度相同的大小
c_org = c_org.unsqueeze(-1).expand(-1, -1, x.size(-1))
# 延维度 1 将 x 与 c_org 拼接到一起
x = torch.cat((x, c_org), dim=1)
# 遍历上文创建的 3 个 5 * 1 组合卷积块
for conv in self.convolutions:
# 将 x 通过为每个组合卷积层进行计算,并添加 relu 激活函数
x = F.relu(conv(x))
# 将计算后的 x 的维度 1 与维度 2 交换(还原之前的交换操作)
x = x.transpose(1, 2)
# 为了提高内存的利用率和效率,调用 flatten_parameters 让 parameter 的数据存放成 contiguous chunk (连续的块)
# 似乎与调用 tensor.contiguous 类似
# 似乎不加这句,多卡跑 RNN 会出错
self.lstm.flatten_parameters()
# 将通过组合卷积块的 x 输入到上文创建的 BLSTM 层中,得到输出 outputs
outputs, _ = self.lstm(x)
# 将输出分为两部分:上采样与下采样
out_forward = outputs[:, :, :self.dim_neck]
out_backward = outputs[:, :, self.dim_neck:]
# 创建采样编码列表
codes = []
# 间隔 self.freq (采样系数)在输出数据内循环操作
for i in range(0, outputs.size(1), self.freq):
# 为上采样与下采样分别取对应的值
# 沿最后一个维度将上采样与下采样拼接
# 将拼接的采样编码添加进采样编码列表中
codes.append(torch.cat((out_forward[:,i+self.freq-1,:],out_backward[:,i,:]), dim=-1))
# 返回采样编码列表
return codes
Decoder
该类的意义为:解码器
Decoder 继承 PyTorch 中网络模型的基类: torch.nn.Module ;
故需要重载两个函数:__init__ 与 forward 。
下面依次介绍 Decoder 类的成员函数
__ init __
该函数的作用是: 创建 Decoder 解码器需要的元素。
输入参数:
dim_neck : 内容编码器数据维数
dim_emb : 说话人编码数据维数
dim_pre : 输出数据维度
输出参数: 无
代码详解:
def __init__(self, dim_neck, dim_emb, dim_pre):
# 调用父类初始化函数,初始化模型基本参数
super(Decoder, self).__init__()
# 创建 LSTM 层,输入维度为 dim_neck*2+dim_emb (上下采样内容编码+说话人编码)
# 输出维度为 dim_pre ,复用层数为 1 ,提供输入和输出张量为(batch, seq, feature)
# 创建成员函数 lstm1 ,引用上述组建的 LSTM 层
self.lstm1 = nn.LSTM(dim_neck*2+dim_emb, dim_pre, 1, batch_first=True)
# 创建卷积列表
convolutions = []
# 循环 3 次创建规范卷积组
for i in range(3):
# 组合卷积层与批规范层, nn.Sequential 详细介绍见 Encode 类
conv_layer = nn.Sequential(
# 卷积规范层的输入输出维数都设定为 dim_pre ,其余与 Encode 类一致
ConvNorm(dim_pre,
dim_pre,
kernel_size=5, stride=1,
padding=2,
dilation=1, w_init_gain='relu'),
# 批规范层输入维数为 dim_pre
nn.BatchNorm1d(dim_pre))
# 将创建的卷积规范+批规范组合层添加进卷积列表
convolutions.append(conv_layer)
# 将上述的卷积组赋值给成员函数 convolutions
self.convolutions = nn.ModuleList(convolutions)
# 输入数据维度为 dim_pre ,输出数据维度为 1024 ,复用 2 次,格式为(batch, seq, feature)
# 创建成员函数 lstm2 ,应用上述组装的 LSTM 层
self.lstm2 = nn.LSTM(dim_pre, 1024, 2, batch_first=True)
# 创建成员函数 linear_projection 应用输入维度为 1024 , 输出维度为 80 的线性规范层。
self.linear_projection = LinearNorm(1024, 80)
forward
该函数的作用是: 定义了执行网络模型的计算过程
输入参数:
x : 输入网络的数据样本
输出参数:
decoder_output : 解码器输出
代码详解:
def forward(self, x):
# 使用定义的 lstm1 网络进行计算,将输出覆盖 x
x, _ = self.lstm1(x)
# 将 x 的维度 1 与维度 2 交换
x = x.transpose(1, 2)
# 遍历卷积列表
for conv in self.convolutions:
# 使用卷积层计算 x ,并使用 relu 激活函数,最后用计算结果覆盖 x 。
x = F.relu(conv(x))
# 将 x 的维度 1 与维度 2 交换(还原操作)
x = x.transpose(1, 2)
# 使用 lstm2 计算 x ,得到输出 outputs
outputs, _ = self.lstm2(x)
# 使用 linear_projection 计算 outputs ,得到输出 decoder_output
decoder_output = self.linear_projection(outputs)
# 将得到的输出 decoder_output 返回
return decoder_output
Postnet
该类的意义为:解码器后置网络
Postnet 继承 PyTorch 中网络模型的基类: torch.nn.Module ;
故需要重载两个函数:__init__ 与 forward 。
下面依次介绍 Postnet 类的成员函数
__ init __
该函数的作用是: 创建 Postnet 解码器后置网络需要的元素。
输入参数: 无
输出参数: 无
代码详解:
def __init__(self):
# 调用父类初始化函数,初始化模型基本参数
super(Postnet, self).__init__()
# 创建成员函数 convolutions ,使用 nn.ModuleList 简化模型的组装
self.convolutions = nn.ModuleList()
# 在卷积组中添加网络模型
self.convolutions.append(
# 使用 nn.Sequential 快速组合卷积规范层与批规范层
nn.Sequential(
# 卷积规范层输入维度为 80 ,输出维度为 512 ,卷积核大小为 5
# 步幅为 1 ,填充量为 2 ,膨胀系数为 1 ,使用 tanh 激活
ConvNorm(80, 512,
kernel_size=5, stride=1,
padding=2,
dilation=1, w_init_gain='tanh'),
# 批规范层输入输出数据维度为 512
nn.BatchNorm1d(512))
)
# 在卷积组中添加 4 个网络模型
for i in range(1, 5 - 1):
self.convolutions.append(
# 使用 nn.Sequential 快速组合卷积规范层与批规范层
nn.Sequential(
# 卷积规范层输入维度与输出维度为 512 ,卷积核大小为 5
# 步幅为 1 ,填充量为 2 ,膨胀系数为 1 ,使用 tanh 激活
ConvNorm(512,
512,
kernel_size=5, stride=1,
padding=2,
dilation=1, w_init_gain='tanh'),
# 批规范层输入输出数据维度为 512
nn.BatchNorm1d(512))
)
# # 在卷积组中添加最后 1 个网络模型
self.convolutions.append(
# 使用 nn.Sequential 快速组合卷积规范层与批规范层
nn.Sequential(
# 卷积规范层输入维度为 512 ,输出维度为 80 ,卷积核大小为 5
# 步幅为 1 ,填充量为 2 ,膨胀系数为 1 ,使用 linear 激活
ConvNorm(512, 80,
kernel_size=5, stride=1,
padding=2,
dilation=1, w_init_gain='linear'),
# 批规范层输入输出数据维度为 512
nn.BatchNorm1d(80))
)
forward
该函数的作用是: 定义了执行网络模型的计算过程
输入参数:
x : 输入后置网络的数据样本
输出参数:
x : 后置网络输出
代码详解:
def forward(self, x):
# 从卷积网络组中以此遍历事先组装的网络(除了最后一个)
for i in range(len(self.convolutions) - 1):
# 使用当前的网络模型计算 x ,并使用 tanh 激活函数,将计算结果覆盖 x
x = torch.tanh(self.convolutions[i](x))
# 取卷积网络组的最后一层,直接计算得到结果覆盖 x (不使用激活函数)
x = self.convolutions[-1](x)
# 返回后置网络的输出
return x
Generator
该类的意义为:发生器网络
Generator 继承 PyTorch 中网络模型的基类: torch.nn.Module ;
故需要重载两个函数:__init__ 与 forward 。
下面依次介绍 Generator 类的成员函数
__ init __
该函数的作用是: 创建 Generator 发生器网络需要的元素。
输入参数:
dim_neck : 内容编码器输出维度
dim_emb : 说话人编码器输出维度
dim_pre : 解码器输出维度
freq : 采样系数
输出参数: 无
代码详解:
def __init__(self, dim_neck, dim_emb, dim_pre, freq):
# 调用父类初始化函数,初始化模型基本参数
super(Generator, self).__init__()
# 创建内容编码器 Encoder ,设置输出维度为 dim_neck ,说话人编码为 dim_emb ,采样系数为 freq
# 创建成员函数 encoder ,应用上述组装的内容编码器
self.encoder = Encoder(dim_neck, dim_emb, freq)
# 创建解码器 Decoder ,设置内容编码为 dim_neck ,说话人编码为 dim_emb ,解码器输出为 dim_pre
# 创建成员函数 decoder ,应用上述组装的解码器
self.decoder = Decoder(dim_neck, dim_emb, dim_pre)
# 创建成员函数 postnet ,生成解码器的后置网络
self.postnet = Postnet()
forward
该函数的作用是: 定义了执行网络模型的计算过程
输入参数:
x : 输入网络的音频数据
c_org : 源说话人编码
c_trg : 目标说话人编码
输出参数:
mel_outputs : 初步转换结果
mel_outputs_postnet : 最终转换结果
torch.cat(codes, dim=-1) : 源说话人说话内容编码
代码详解:
def forward(self, x, c_org, c_trg):
# 输入音频数据 x 与说话人编码 c_org (源说话人编码),使用成员函数 encoder 得到源说话人说话内容编码
codes = self.encoder(x, c_org)
# 若目标说话人编码不存在
if c_trg is None:
# 直接返回源说话人说话内容编码(将上下采样部分沿最后一个维度连接)
return torch.cat(codes, dim=-1)
# 创建采样编码临时列表
tmp = []
# 拓展上下采样编码的维度,还原到同样的时间系数
for code in codes:
tmp.append(code.unsqueeze(1).expand(-1,int(x.size(1)/len(codes)),-1))
# 将采样编码临时列表拼接
code_exp = torch.cat(tmp, dim=1)
# 将目标说话人编码拓展到内容编码的维度,在将两者沿最后一个维度进行拼接,得到完整的编码器输出 encoder_outputs
encoder_outputs = torch.cat((code_exp, c_trg.unsqueeze(1).expand(-1,x.size(1),-1)), dim=-1)
# 将完整的编码器输出 encoder_outputs 输入解码器计算,得到解码器输出 mel_outputs (可以认为是初步的语音转换结果)
mel_outputs = self.decoder(encoder_outputs)
# 将初步语音转换结果 mel_outputs 以合适的数据格式输入解码器后置网络进行计算,得到后置网络输出 mel_outputs_postnet
mel_outputs_postnet = self.postnet(mel_outputs.transpose(2,1))
# 将初步语音转换结果 mel_outputs 与后置网络输出 mel_outputs_postnet 进行组合,将结果覆盖后置网络输出 mel_outputs_postnet
mel_outputs_postnet = mel_outputs + mel_outputs_postnet.transpose(2,1)
# 将转换结果的维度 1 进行压缩
mel_outputs = mel_outputs.unsqueeze(1)
mel_outputs_postnet = mel_outputs_postnet.unsqueeze(1)
# 返回初步转换结果 mel_outputs ,最终转换结果 mel_outputs_postnet ,源说话人说话内容编码 torch.cat(codes, dim=-1)
return mel_outputs, mel_outputs_postnet, torch.cat(codes, dim=-1)