細讀經典:HiFiGAN,擁有多尺度和多周期判別器的高效聲碼 ...?
簡介
HiFiGAN是近年來在學術界和工業界都較為常用的聲碼器,能夠將聲學模型產生的頻譜轉換為高質量的音頻,這種聲碼器采用生成對抗網絡(Generative Adversial Networks,GAN)作為基礎生成模型,相比于之前相近的MelGAN,貢獻點主要在:
生成器
HiFiGAN的生成器主要有兩塊,一臺是上采樣結構,具體是由一維轉置卷積組成;二是所謂的多感受野融合(Multi-Receptive Field Fusion,MRF)模塊,主要負責對上采樣獲得的采樣點進行優化,具體是由殘差網絡組成。
作為聲碼器的生成器,不但需要負責將頻譜從頻域轉換到時域,而且要進行上采樣(upsampling)。以80維梅爾頻譜合成16kHz的語音為例,假設幀移為10ms,則每個幀移內有160個語音樣本點,需要通過80個梅爾頻譜值獲得,因此,需要利用卷積網絡不斷增加輸出“長度”,降低輸出“通道數”,直到上采樣倍數達到160,通道數降低為1即可。
對于上采樣操作,可以使用插值算法進行處理,比如最近鄰插值(Nearest neighbor interpolation),雙線性插值(Bi-Linear interpolation),雙立方插值(Bi-Cubic interpolation)等等,但是這些插值算法說到底是人工規則,而神經網絡可以自動學習合適的變換,轉置卷積(ConvTransposed)(也有稱反卷積Deconvolution、微步卷積Fractionally-strided Convolution)則是合適的上采樣結構。一般的卷積中,每次卷積操作是對輸入張量和卷積核的每個元素進行相乘再加和,一臺卷積操作是多對一的映射關系,而轉置卷積則反過來,是一對多的映射關系。從計算機的內部實現來看,定義:
;同時按照步長,左側填充0偏移該卷積核向量,最終,卷積核向量的個數為輸出張量元素個數,則構成的卷積核張量大小為
,卷積核張量和輸入張量矩陣乘,獲得輸出張量,重塑大小為
。
此時,如果使用卷積核張量的轉置
矩陣乘展平的,得到的結果就是,和剛剛的輸入張量大小相同,完成了一次轉置卷積。實際上,上述操作并非可逆關系,卷積將輸入張量“下采樣”到輸出張量,本質是有損壓縮的過程,由于在卷積中使用的卷積核張量并非可逆矩陣,轉置卷積操作之后并不能恢復到原始的數值,僅僅是恢復到原始的形狀,這其實也就是線性譜與梅爾頻譜關系,加權求和得到梅爾頻譜之后就回不來了,頂多求梅爾濾波器組的偽逆,近似恢復到線性譜。
此外,在使用轉置卷積時需要注意棋盤效應(Checkboard artifacts)。棋盤效應主要是由于轉置卷積的“不均勻重疊”(Uneven overlap)造成的,輸出上每個像素接受的信息量與相鄰像素不同,在輸出上找不到連續且均勻重疊的區域,表現是圖像中一些色塊的顏色比周圍色塊要深,像棋盤上的方格,參見:Deconvolution and Checkerboard Artifacts。避免棋盤效應的方法主要有:kernel_size的大小盡可能被stride整除,盡可能使用stride=1的轉置卷積;堆疊轉置卷積減輕重疊;網絡末尾使用
的轉置卷積等等。
通過上述的原理部分,可以看出卷積和轉置卷積是對偶運算,輸入變輸出,輸出變輸入,卷積的輸入輸出大小關系為:
![]()
那么轉置卷積的輸入輸出大小則為:
![]()
當然,加入dilation之后,大小計算稍復雜些,參見:Pytorch-ConvTranspose1d,Pytorch-Conv1d。
self.ups = nn.ModuleList()
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)),kernel_size=k, stride=u, padding=(k-u)//2)))論文對應的代碼中,對于hop_size=256來說,h.upsample_rates和h.upsample_kernel_sizes分別為:
"upsample_rates": [8,8,2,2],
"upsample_kernel_sizes": [16,16,4,4],根據轉置卷積的輸入輸出大小關系:
![]()
用于上采樣的轉置卷積,通過設置合適的padding,配合卷積核大小(kernel_size)和步進(stride),就可以實現輸出與輸入大小呈“步進倍數”的關系。設置參數時,必須保持幀移點數是步進(或者代碼中所謂的上采樣率update_rates)的乘積,在上例中,也就是:
![]()
剛剛說到,轉置卷積的上采樣容易導致棋盤效應,因此每次轉置卷積上采樣之后,都會跟著一臺多感受野融合(MRF)的殘差網絡,以進一步提升樣本點的生成質量。多感受野融合模塊是一種利用帶洞卷積和普通卷積提高生成器感受野的結構,帶洞卷積的擴張倍數逐步遞增,如dilation=1,3,5,每個帶洞卷積之后,跟著卷積核大于1的普通卷積,從而實現帶洞卷積和普通卷積的交替使用。帶洞卷積和普通卷積的輸入輸出大小保持不變,在一輪帶洞和普通卷積完成之后,原始輸入跳連到卷積的結果,從而實現一輪“多感受野融合”。多感受野融合的具體實現上,論文中提出了兩種參數量不同的殘差網絡。一種是參數量較多,多組帶洞卷積(dilation=1,3,5)和普通卷積交替使用:
class ResBlock1(torch.nn.Module):
def __init__(self, h, channels, kernel_size=3, dilation=(1, 3, 5)):
super(ResBlock1, self).__init__()
self.h = h
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])))
])
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)原始論文中附錄的代碼中,config_v1.json和config_v2.json均使用該種多感受野融合(MRF)模塊。另外一種MRF大大減少了參數量,僅由兩層帶洞卷積(dilation=1,3)組成:
class ResBlock2(torch.nn.Module):
def __init__(self, h, channels, kernel_size=3, dilation=(1, 3)):
super(ResBlock2, self).__init__()
self.h = h
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)注意到兩種MRF都使用了weight_norm對神經網絡的權重進行規范化,相比于batch_norm,weight_norm不依賴mini-batch的數據,對噪音數據更為魯棒;并且,可以應用于RNN等時序網絡上;此外,weight_norm直接對神經網絡的權重值進行規范化,前向和后向計算時,帶來的額外計算和存儲開銷都較小。weight_norm本質是利用方向和幅度張量替代權重張量:
![]()
方向張量和大小相同,幅度張量比少一維,使得能夠比較容易的整體縮放。不直接優化,而是訓練和。
同時注意到,在推理時需要remove_weight_norm,這是因為訓練時需要計算權重矩陣的方向和幅度張量,而在推理時,參數已經優化完成,weight_norm可加可不加,所以在推理時就直接移除weight_norm機制,參見:Weight Normalization in PyTorch,melgan/issue#37。
每個卷積核的0填充個數都調用了get_padding函數,利用填充保證輸入輸出的長寬大小一致,該填充大小的計算方法:
![]()
判別器
HiFiGAN的判別器有兩個,分別是多尺度和多周期判別器,從兩個不同角度分別鑒定語音。多尺度判別器源自MelGAN聲碼器的做法,不斷平均池化語音序列,逐次將語音序列的長度減半,然后在語音的不同尺度上施加若干層卷積,最后展平,作為多尺度判別器的輸出。多周期判別器則是以不同的序列長度將一維的音頻序列折疊為二維平面,在二維平面上施加二維卷積。
多尺度判別器
多尺度判別器的核心是先進行平均池化,縮短序列長度,每次序列長度池化至原來的一半,然后進行卷積。具體來說,多尺度判別器首先對原樣本點進行一次“原尺寸判別”,使用一維卷積的參數規范化方法為譜歸一化(spectral_norm);接著對樣本點序列進行平均池化,依次將序列長度減半,然后對“下采樣”的樣本點序列進行判別,使用一維卷積的參數規范化方法為權重歸一化(weight_norm)。在每一臺特定尺度的子判別器中,首先進行若干層卷積,均采用分組卷積,并利用對應方法對參數進行規范化;接著利用leaky_relu激活;在經過多個卷積層之后,最后利用輸出通道為1的卷積層進行后處理,展平后作為輸出。
class MultiScaleDiscriminator(torch.nn.Module):
def __init__(self):
super(MultiScaleDiscriminator, self).__init__()
self.discriminators = nn.ModuleList([
DiscriminatorS(use_spectral_norm=True),
DiscriminatorS(),
DiscriminatorS(),
])
self.meanpools = nn.ModuleList([
AvgPool1d(4, 2, padding=2),
AvgPool1d(4, 2, padding=2)
])
def forward(self, y, y_hat):
y_d_rs = []
y_d_gs = []
fmap_rs = []
fmap_gs = []
for i, d in enumerate(self.discriminators):
if i != 0:
y = self.meanpools[i-1](y)
y_hat = self.meanpools[i-1](y_hat)
y_d_r, fmap_r = d(y)
y_d_g, fmap_g = d(y_hat)
y_d_rs.append(y_d_r)
fmap_rs.append(fmap_r)
y_d_gs.append(y_d_g)
fmap_gs.append(fmap_g)
return y_d_rs, y_d_gs, fmap_rs, fmap_gs上述代碼中y_d_rs和y_d_gs分別是真實和生成樣本的多尺度判別器展平后的整體輸出,fmap_rs和y_d_gs分別是真實和生成樣本經過每一層卷積的特征圖(feature map)。子判別器DiscriminatorS由若干層卷積組成,最后一層輸出通道為1,之后對輸出進行展平。注意到,與MelGAN不同,多尺度判別器的第一臺子判別器DiscriminatorS使用譜歸一化spectral_norm,之后兩個子判別器則是正常使用權重歸一化weight_norm規整可訓練參數。譜歸一化實際就是在每次更新完可訓練參數之后都除以的奇異值,以保證整個網絡滿足利普希茨連續性,使得GAN的訓練更穩定。參見:GAN 的譜歸一化(Spectral Norm)和矩陣的奇異值分解(Singular Value Decompostion)。DiscriminatorS的具體實現如下:
class DiscriminatorS(torch.nn.Module):
def __init__(self, use_spectral_norm=False):
super(DiscriminatorS, self).__init__()
norm_f = weight_norm if use_spectral_norm == False else spectral_norm
self.convs = nn.ModuleList([
norm_f(Conv1d(1, 128, 15, 1, padding=7)),
norm_f(Conv1d(128, 128, 41, 2, groups=4, padding=20)),
norm_f(Conv1d(128, 256, 41, 2, groups=16, padding=20)),
norm_f(Conv1d(256, 512, 41, 4, groups=16, padding=20)),
norm_f(Conv1d(512, 1024, 41, 4, groups=16, padding=20)),
norm_f(Conv1d(1024, 1024, 41, 1, groups=16, padding=20)),
norm_f(Conv1d(1024, 1024, 5, 1, padding=2)),
])
self.conv_post = norm_f(Conv1d(1024, 1, 3, 1, padding=1))
def forward(self, x):
fmap = []
for l in self.convs:
x = l(x)
x = F.leaky_relu(x, LRELU_SLOPE)
fmap.append(x)
x = self.conv_post(x)
fmap.append(x)
x = torch.flatten(x, 1, -1)
return x, fmapx是子判別器展平后的整體輸出,大小為[B,l];fmap是經過每一層卷積后的特征圖(feature map),類型為list,元素個數為卷積層數,上述代碼中有8個卷積層,則fmap元素個數為8,每個元素是大小為[B,C,l']的張量。
多周期判別器
多周期判別器的重點是將一維樣本點序列以一定周期折疊為二維平面,例如一維樣本點序列[1,2,3,4,5,6]如果以3為周期,折疊成二維平面則是[[1,2,3],[4,5,6]],然后對這個二維平面施加二維卷積。具體來說,每個特定周期的子判別器首先進行填充,保證樣本點數是周期的整倍數,以省事“折疊”為二維平面;接下來進入多個卷積層,輸出通道數分別為[32,128,512,1024],卷積之后利用leaky_relu激活,卷積層參數規范化方法均為權重歸一化(weight_norm);然后經過多個卷積層之后,利用一臺輸入通道數為1024,輸出通道為1的卷積層進行后處理,最后展平作為多周期判別器的最終輸出。多周期判別器包含多個周期不同的子判別器,在論文代碼中周期數分別設置為[2,3,5,7,11]。
class MultiPeriodDiscriminator(torch.nn.Module):
def __init__(self):
super(MultiPeriodDiscriminator, self).__init__()
self.discriminators = nn.ModuleList([
DiscriminatorP(2),
DiscriminatorP(3),
DiscriminatorP(5),
DiscriminatorP(7),
DiscriminatorP(11),
])
def forward(self, y, y_hat):
y_d_rs = []
y_d_gs = []
fmap_rs = []
fmap_gs = []
for i, d in enumerate(self.discriminators):
y_d_r, fmap_r = d(y)
y_d_g, fmap_g = d(y_hat)
y_d_rs.append(y_d_r)
fmap_rs.append(fmap_r)
y_d_gs.append(y_d_g)
fmap_gs.append(fmap_g)
return y_d_rs, y_d_gs, fmap_rs, fmap_gs上述代碼中y_d_rs和y_d_gs分別是真實和生成樣本的多周期判別器輸出,fmap_rs和fmap_gs分別是真實和生成樣本經過每一層卷積后輸出的特征圖(feature map)。子判別器DiscriminatorP由若干層二維卷積組成:
class DiscriminatorP(torch.nn.Module):
def __init__(self, period, kernel_size=5, stride=3, use_spectral_norm=False):
super(DiscriminatorP, self).__init__()
self.period = period
norm_f = weight_norm if use_spectral_norm == False else spectral_norm
self.convs = nn.ModuleList([
norm_f(Conv2d(1, 32, (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
norm_f(Conv2d(32, 128, (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
norm_f(Conv2d(128, 512, (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
norm_f(Conv2d(512, 1024, (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
norm_f(Conv2d(1024, 1024, (kernel_size, 1), 1, padding=(2, 0))),
])
self.conv_post = norm_f(Conv2d(1024, 1, (3, 1), 1, padding=(1, 0)))
def forward(self, x):
fmap = []
# 1d to 2d
b, c, t = x.shape
if t % self.period != 0: # pad first
n_pad = self.period - (t % self.period)
x = F.pad(x, (0, n_pad), "reflect")
t = t + n_pad
x = x.view(b, c, t // self.period, self.period)
for l in self.convs:
x = l(x)
x = F.leaky_relu(x, LRELU_SLOPE)
fmap.append(x)
x = self.conv_post(x)
fmap.append(x)
x = torch.flatten(x, 1, -1)
return x, fmapx是子判別器展平后的整體輸出,大小為[B,l];fmap是經過每一層卷積后的特征圖(feature map),類型為list,元素個數為卷積層數,上述代碼中有6個卷積層,則fmap元素個數為6,每個元素是大小為[B,C,l',period]的張量。
損失函數
HiFiGAN的損失函數主要包括三塊,一臺是GAN原始的生成對抗損失(GAN Loss);第二是梅爾頻譜損失(Mel-Spectrogram Loss),將生成音頻轉換回梅爾頻譜之后,計算真實和生成梅爾頻譜之間的L1距離;第三個是特征匹配損失(Feature Match Loss),主要是對比中間卷積層真實和合成樣本之間的差異。
生成對抗損失
HiFiGAN的本質仍然是一臺生成對抗網絡,判別器計算樣本是真實樣本的概率,生成器生成以假亂真的樣本,最終達到生成器合成接近真實的樣本,以致于判別器無法區分真實和生成樣本。HiFiGAN使用LS-GAN,將原始GAN中的二元交叉熵替換為最小二乘損失函數。判別器的生成對抗損失定義為:
![]()
對應的判別器代碼實現:
def discriminator_loss(disc_real_outputs, disc_generated_outputs):
loss = 0
r_losses = []
g_losses = []
for dr, dg in zip(disc_real_outputs, disc_generated_outputs):
r_loss = torch.mean((dr-1)**2)
g_loss = torch.mean(dg**2)
loss += (r_loss + g_loss)
r_losses.append(r_loss.item())
g_losses.append(g_loss.item())
return loss, r_losses, g_losses生成器的生成對抗損失定義為:
![]()
其中,
表示真實音頻,
表示梅爾頻譜。
對應的生成器代碼實現:
def generator_loss(disc_outputs):
loss = 0
gen_losses = []
for dg in disc_outputs:
l = torch.mean((dg-1)**2)
gen_losses.append(l)
loss += l
return loss, gen_losses
借鑒Parallel WaveGAN等前人工作,向GAN中引入重建損失和梅爾頻譜損失,可以提高模型訓練初期的穩定性、生成器的訓練效率和合成語音的保真度。具體來說,梅爾頻譜損失就是計算合成語音提取頻譜和真實語音提取頻譜之間的L1距離:
![]()
其中,
表示將語音轉換為梅爾頻譜的映射函數。
對應的損失函數實現:
loss_mel = F.l1_loss(y_mel, y_g_hat_mel)上述代碼中,y_mel表示真實語音對應的梅爾頻譜,y_g_hat_mel表示合成波形之后,合成語音轉換得到的梅爾頻譜。
特征匹配損失
鑒別器損失是用來度量從真實和生成樣本中提取的特征差異,具體來說,就是計算真實和合成樣本每一層卷積輸出之間的L1距離:
![]()
其中,
表示判別器中提取特征的層數,
表示提取的特征,
表示第
層判別器網絡提取的特征數量。對應的代碼為:
def feature_loss(fmap_r, fmap_g):
loss = 0
for dr, dg in zip(fmap_r, fmap_g):
for rl, gl in zip(dr, dg):
loss += torch.mean(torch.abs(rl - gl))
return loss整體損失
生成器的總體損失為:
![]()
其中,
,
。
判別器的總體損失為:
![]()
因為HiFiGAN的判別器是由多尺度判別器和多周期判別器組成,因此生成器的總體損失又可以寫作:
![]()
對應的代碼為:
# L1 Mel-Spectrogram Loss
loss_mel = F.l1_loss(y_mel, y_g_hat_mel) * 45
y_df_hat_r, y_df_hat_g, fmap_f_r, fmap_f_g = mpd(y, y_g_hat)
y_ds_hat_r, y_ds_hat_g, fmap_s_r, fmap_s_g = msd(y, y_g_hat)
loss_fm_f = feature_loss(fmap_f_r, fmap_f_g)
loss_fm_s = feature_loss(fmap_s_r, fmap_s_g)
loss_gen_f, losses_gen_f = generator_loss(y_df_hat_g)
loss_gen_s, losses_gen_s = generator_loss(y_ds_hat_g)
loss_gen_all = loss_gen_s + loss_gen_f + loss_fm_s + loss_fm_f + loss_mel判別器的總體損失又可以寫作:
![]()
其中,
表示第
個MPD和MSD的子判別器。
對應的代碼為:
# MPD
y_df_hat_r, y_df_hat_g, _, _ = mpd(y, y_g_hat.detach())
loss_disc_f, losses_disc_f_r, losses_disc_f_g = discriminator_loss(y_df_hat_r, y_df_hat_g)
# MSD
y_ds_hat_r, y_ds_hat_g, _, _ = msd(y, y_g_hat.detach())
loss_disc_s, losses_disc_s_r, losses_disc_s_g = discriminator_loss(y_ds_hat_r, y_ds_hat_g)
loss_disc_all = loss_disc_s + loss_disc_f實驗
實驗中使用了3個版本,V1是大模型音質最優版本,V2是V1的縮小版,相比于V1,V2的上采樣隱層維度由512降低為128,V3是最小版,生成器上采樣中的多尺度融合模塊經過了簡化。自然度評分上,GT(4.45)WaveNet-MoL(4.02)MelGAN(3.79)HiFiGAN-V1(4.36)HiFiGAN-V2(4.23)HiFiGAN-V3(4.05)。
yangjinnew 13小時前 如果建模16k音頻,參數如何設置
羅剎斗戰勝佛 13小時前 “如果以3為周期,折疊成二維平面則是[[1,2,3],[4,5,6]]”這句話是不是寫錯了呀,因為按照原文的示意圖,如果以3為周期,那么實際上得到的是[[1, 4], [2, 5], [3, 6]]
HiFiGAN是近年來在學術界和工業界都較為常用的聲碼器,能夠將聲學模型產生的頻譜轉換為高質量的音頻,這種聲碼器采用生成對抗網絡(Generative Adversial Networks,GAN)作為基礎生成模型,相比于之前相近的MelGAN,貢獻點主要在:
- 引入了多周期判別器(Multi-Period Discriminator,MPD)。HiFiGAN同時擁有多尺度判別器(Multi-Scale Discriminator,MSD)和多周期判別器,目標就是盡可能增強GAN判別器甄別合成或真實音頻的能力。
- 生成器中提出了多感受野融合模塊。WaveNet為了增大感受野,疊加帶洞卷積,逐樣本點生成,音質確實很好,但是也使得模型較大,推理速度較慢。HiFiGAN則提出了一種殘差結構,交替使用帶洞卷積和普通卷積增大感受野,保證合成音質的同時,提高推理速度。
生成器
HiFiGAN的生成器主要有兩塊,一臺是上采樣結構,具體是由一維轉置卷積組成;二是所謂的多感受野融合(Multi-Receptive Field Fusion,MRF)模塊,主要負責對上采樣獲得的采樣點進行優化,具體是由殘差網絡組成。
作為聲碼器的生成器,不但需要負責將頻譜從頻域轉換到時域,而且要進行上采樣(upsampling)。以80維梅爾頻譜合成16kHz的語音為例,假設幀移為10ms,則每個幀移內有160個語音樣本點,需要通過80個梅爾頻譜值獲得,因此,需要利用卷積網絡不斷增加輸出“長度”,降低輸出“通道數”,直到上采樣倍數達到160,通道數降低為1即可。
對于上采樣操作,可以使用插值算法進行處理,比如最近鄰插值(Nearest neighbor interpolation),雙線性插值(Bi-Linear interpolation),雙立方插值(Bi-Cubic interpolation)等等,但是這些插值算法說到底是人工規則,而神經網絡可以自動學習合適的變換,轉置卷積(ConvTransposed)(也有稱反卷積Deconvolution、微步卷積Fractionally-strided Convolution)則是合適的上采樣結構。一般的卷積中,每次卷積操作是對輸入張量和卷積核的每個元素進行相乘再加和,一臺卷積操作是多對一的映射關系,而轉置卷積則反過來,是一對多的映射關系。從計算機的內部實現來看,定義:
- 為輸入張量,大小為
- 為輸出張量,大小為
為卷積核,大小為
此時,如果使用卷積核張量的轉置
此外,在使用轉置卷積時需要注意棋盤效應(Checkboard artifacts)。棋盤效應主要是由于轉置卷積的“不均勻重疊”(Uneven overlap)造成的,輸出上每個像素接受的信息量與相鄰像素不同,在輸出上找不到連續且均勻重疊的區域,表現是圖像中一些色塊的顏色比周圍色塊要深,像棋盤上的方格,參見:Deconvolution and Checkerboard Artifacts。避免棋盤效應的方法主要有:kernel_size的大小盡可能被stride整除,盡可能使用stride=1的轉置卷積;堆疊轉置卷積減輕重疊;網絡末尾使用
通過上述的原理部分,可以看出卷積和轉置卷積是對偶運算,輸入變輸出,輸出變輸入,卷積的輸入輸出大小關系為:
那么轉置卷積的輸入輸出大小則為:
當然,加入dilation之后,大小計算稍復雜些,參見:Pytorch-ConvTranspose1d,Pytorch-Conv1d。
怎樣通俗易懂地解釋反卷積?在HiFiGAN具體的代碼上,上采樣層定義為:
一文搞懂反卷積,轉置卷積
Deconvolution and Checkerboard Artifacts
如何去除生成圖片產生的棋盤偽影?
A guide to convolution arithmetic for deep learning
Pytorch-ConvTranspose1d
Pytorch-Conv1d
self.ups = nn.ModuleList()
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)),kernel_size=k, stride=u, padding=(k-u)//2)))論文對應的代碼中,對于hop_size=256來說,h.upsample_rates和h.upsample_kernel_sizes分別為:
"upsample_rates": [8,8,2,2],
"upsample_kernel_sizes": [16,16,4,4],根據轉置卷積的輸入輸出大小關系:
用于上采樣的轉置卷積,通過設置合適的padding,配合卷積核大小(kernel_size)和步進(stride),就可以實現輸出與輸入大小呈“步進倍數”的關系。設置參數時,必須保持幀移點數是步進(或者代碼中所謂的上采樣率update_rates)的乘積,在上例中,也就是:
剛剛說到,轉置卷積的上采樣容易導致棋盤效應,因此每次轉置卷積上采樣之后,都會跟著一臺多感受野融合(MRF)的殘差網絡,以進一步提升樣本點的生成質量。多感受野融合模塊是一種利用帶洞卷積和普通卷積提高生成器感受野的結構,帶洞卷積的擴張倍數逐步遞增,如dilation=1,3,5,每個帶洞卷積之后,跟著卷積核大于1的普通卷積,從而實現帶洞卷積和普通卷積的交替使用。帶洞卷積和普通卷積的輸入輸出大小保持不變,在一輪帶洞和普通卷積完成之后,原始輸入跳連到卷積的結果,從而實現一輪“多感受野融合”。多感受野融合的具體實現上,論文中提出了兩種參數量不同的殘差網絡。一種是參數量較多,多組帶洞卷積(dilation=1,3,5)和普通卷積交替使用:
class ResBlock1(torch.nn.Module):
def __init__(self, h, channels, kernel_size=3, dilation=(1, 3, 5)):
super(ResBlock1, self).__init__()
self.h = h
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])))
])
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)原始論文中附錄的代碼中,config_v1.json和config_v2.json均使用該種多感受野融合(MRF)模塊。另外一種MRF大大減少了參數量,僅由兩層帶洞卷積(dilation=1,3)組成:
class ResBlock2(torch.nn.Module):
def __init__(self, h, channels, kernel_size=3, dilation=(1, 3)):
super(ResBlock2, self).__init__()
self.h = h
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)注意到兩種MRF都使用了weight_norm對神經網絡的權重進行規范化,相比于batch_norm,weight_norm不依賴mini-batch的數據,對噪音數據更為魯棒;并且,可以應用于RNN等時序網絡上;此外,weight_norm直接對神經網絡的權重值進行規范化,前向和后向計算時,帶來的額外計算和存儲開銷都較小。weight_norm本質是利用方向和幅度張量替代權重張量:
方向張量和大小相同,幅度張量比少一維,使得能夠比較容易的整體縮放。不直接優化,而是訓練和。
同時注意到,在推理時需要remove_weight_norm,這是因為訓練時需要計算權重矩陣的方向和幅度張量,而在推理時,參數已經優化完成,weight_norm可加可不加,所以在推理時就直接移除weight_norm機制,參見:Weight Normalization in PyTorch,melgan/issue#37。
每個卷積核的0填充個數都調用了get_padding函數,利用填充保證輸入輸出的長寬大小一致,該填充大小的計算方法:
判別器
HiFiGAN的判別器有兩個,分別是多尺度和多周期判別器,從兩個不同角度分別鑒定語音。多尺度判別器源自MelGAN聲碼器的做法,不斷平均池化語音序列,逐次將語音序列的長度減半,然后在語音的不同尺度上施加若干層卷積,最后展平,作為多尺度判別器的輸出。多周期判別器則是以不同的序列長度將一維的音頻序列折疊為二維平面,在二維平面上施加二維卷積。
多尺度判別器
多尺度判別器的核心是先進行平均池化,縮短序列長度,每次序列長度池化至原來的一半,然后進行卷積。具體來說,多尺度判別器首先對原樣本點進行一次“原尺寸判別”,使用一維卷積的參數規范化方法為譜歸一化(spectral_norm);接著對樣本點序列進行平均池化,依次將序列長度減半,然后對“下采樣”的樣本點序列進行判別,使用一維卷積的參數規范化方法為權重歸一化(weight_norm)。在每一臺特定尺度的子判別器中,首先進行若干層卷積,均采用分組卷積,并利用對應方法對參數進行規范化;接著利用leaky_relu激活;在經過多個卷積層之后,最后利用輸出通道為1的卷積層進行后處理,展平后作為輸出。
class MultiScaleDiscriminator(torch.nn.Module):
def __init__(self):
super(MultiScaleDiscriminator, self).__init__()
self.discriminators = nn.ModuleList([
DiscriminatorS(use_spectral_norm=True),
DiscriminatorS(),
DiscriminatorS(),
])
self.meanpools = nn.ModuleList([
AvgPool1d(4, 2, padding=2),
AvgPool1d(4, 2, padding=2)
])
def forward(self, y, y_hat):
y_d_rs = []
y_d_gs = []
fmap_rs = []
fmap_gs = []
for i, d in enumerate(self.discriminators):
if i != 0:
y = self.meanpools[i-1](y)
y_hat = self.meanpools[i-1](y_hat)
y_d_r, fmap_r = d(y)
y_d_g, fmap_g = d(y_hat)
y_d_rs.append(y_d_r)
fmap_rs.append(fmap_r)
y_d_gs.append(y_d_g)
fmap_gs.append(fmap_g)
return y_d_rs, y_d_gs, fmap_rs, fmap_gs上述代碼中y_d_rs和y_d_gs分別是真實和生成樣本的多尺度判別器展平后的整體輸出,fmap_rs和y_d_gs分別是真實和生成樣本經過每一層卷積的特征圖(feature map)。子判別器DiscriminatorS由若干層卷積組成,最后一層輸出通道為1,之后對輸出進行展平。注意到,與MelGAN不同,多尺度判別器的第一臺子判別器DiscriminatorS使用譜歸一化spectral_norm,之后兩個子判別器則是正常使用權重歸一化weight_norm規整可訓練參數。譜歸一化實際就是在每次更新完可訓練參數之后都除以的奇異值,以保證整個網絡滿足利普希茨連續性,使得GAN的訓練更穩定。參見:GAN 的譜歸一化(Spectral Norm)和矩陣的奇異值分解(Singular Value Decompostion)。DiscriminatorS的具體實現如下:
class DiscriminatorS(torch.nn.Module):
def __init__(self, use_spectral_norm=False):
super(DiscriminatorS, self).__init__()
norm_f = weight_norm if use_spectral_norm == False else spectral_norm
self.convs = nn.ModuleList([
norm_f(Conv1d(1, 128, 15, 1, padding=7)),
norm_f(Conv1d(128, 128, 41, 2, groups=4, padding=20)),
norm_f(Conv1d(128, 256, 41, 2, groups=16, padding=20)),
norm_f(Conv1d(256, 512, 41, 4, groups=16, padding=20)),
norm_f(Conv1d(512, 1024, 41, 4, groups=16, padding=20)),
norm_f(Conv1d(1024, 1024, 41, 1, groups=16, padding=20)),
norm_f(Conv1d(1024, 1024, 5, 1, padding=2)),
])
self.conv_post = norm_f(Conv1d(1024, 1, 3, 1, padding=1))
def forward(self, x):
fmap = []
for l in self.convs:
x = l(x)
x = F.leaky_relu(x, LRELU_SLOPE)
fmap.append(x)
x = self.conv_post(x)
fmap.append(x)
x = torch.flatten(x, 1, -1)
return x, fmapx是子判別器展平后的整體輸出,大小為[B,l];fmap是經過每一層卷積后的特征圖(feature map),類型為list,元素個數為卷積層數,上述代碼中有8個卷積層,則fmap元素個數為8,每個元素是大小為[B,C,l']的張量。
多周期判別器
多周期判別器的重點是將一維樣本點序列以一定周期折疊為二維平面,例如一維樣本點序列[1,2,3,4,5,6]如果以3為周期,折疊成二維平面則是[[1,2,3],[4,5,6]],然后對這個二維平面施加二維卷積。具體來說,每個特定周期的子判別器首先進行填充,保證樣本點數是周期的整倍數,以省事“折疊”為二維平面;接下來進入多個卷積層,輸出通道數分別為[32,128,512,1024],卷積之后利用leaky_relu激活,卷積層參數規范化方法均為權重歸一化(weight_norm);然后經過多個卷積層之后,利用一臺輸入通道數為1024,輸出通道為1的卷積層進行后處理,最后展平作為多周期判別器的最終輸出。多周期判別器包含多個周期不同的子判別器,在論文代碼中周期數分別設置為[2,3,5,7,11]。
class MultiPeriodDiscriminator(torch.nn.Module):
def __init__(self):
super(MultiPeriodDiscriminator, self).__init__()
self.discriminators = nn.ModuleList([
DiscriminatorP(2),
DiscriminatorP(3),
DiscriminatorP(5),
DiscriminatorP(7),
DiscriminatorP(11),
])
def forward(self, y, y_hat):
y_d_rs = []
y_d_gs = []
fmap_rs = []
fmap_gs = []
for i, d in enumerate(self.discriminators):
y_d_r, fmap_r = d(y)
y_d_g, fmap_g = d(y_hat)
y_d_rs.append(y_d_r)
fmap_rs.append(fmap_r)
y_d_gs.append(y_d_g)
fmap_gs.append(fmap_g)
return y_d_rs, y_d_gs, fmap_rs, fmap_gs上述代碼中y_d_rs和y_d_gs分別是真實和生成樣本的多周期判別器輸出,fmap_rs和fmap_gs分別是真實和生成樣本經過每一層卷積后輸出的特征圖(feature map)。子判別器DiscriminatorP由若干層二維卷積組成:
class DiscriminatorP(torch.nn.Module):
def __init__(self, period, kernel_size=5, stride=3, use_spectral_norm=False):
super(DiscriminatorP, self).__init__()
self.period = period
norm_f = weight_norm if use_spectral_norm == False else spectral_norm
self.convs = nn.ModuleList([
norm_f(Conv2d(1, 32, (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
norm_f(Conv2d(32, 128, (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
norm_f(Conv2d(128, 512, (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
norm_f(Conv2d(512, 1024, (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
norm_f(Conv2d(1024, 1024, (kernel_size, 1), 1, padding=(2, 0))),
])
self.conv_post = norm_f(Conv2d(1024, 1, (3, 1), 1, padding=(1, 0)))
def forward(self, x):
fmap = []
# 1d to 2d
b, c, t = x.shape
if t % self.period != 0: # pad first
n_pad = self.period - (t % self.period)
x = F.pad(x, (0, n_pad), "reflect")
t = t + n_pad
x = x.view(b, c, t // self.period, self.period)
for l in self.convs:
x = l(x)
x = F.leaky_relu(x, LRELU_SLOPE)
fmap.append(x)
x = self.conv_post(x)
fmap.append(x)
x = torch.flatten(x, 1, -1)
return x, fmapx是子判別器展平后的整體輸出,大小為[B,l];fmap是經過每一層卷積后的特征圖(feature map),類型為list,元素個數為卷積層數,上述代碼中有6個卷積層,則fmap元素個數為6,每個元素是大小為[B,C,l',period]的張量。
損失函數
HiFiGAN的損失函數主要包括三塊,一臺是GAN原始的生成對抗損失(GAN Loss);第二是梅爾頻譜損失(Mel-Spectrogram Loss),將生成音頻轉換回梅爾頻譜之后,計算真實和生成梅爾頻譜之間的L1距離;第三個是特征匹配損失(Feature Match Loss),主要是對比中間卷積層真實和合成樣本之間的差異。
生成對抗損失
HiFiGAN的本質仍然是一臺生成對抗網絡,判別器計算樣本是真實樣本的概率,生成器生成以假亂真的樣本,最終達到生成器合成接近真實的樣本,以致于判別器無法區分真實和生成樣本。HiFiGAN使用LS-GAN,將原始GAN中的二元交叉熵替換為最小二乘損失函數。判別器的生成對抗損失定義為:
對應的判別器代碼實現:
def discriminator_loss(disc_real_outputs, disc_generated_outputs):
loss = 0
r_losses = []
g_losses = []
for dr, dg in zip(disc_real_outputs, disc_generated_outputs):
r_loss = torch.mean((dr-1)**2)
g_loss = torch.mean(dg**2)
loss += (r_loss + g_loss)
r_losses.append(r_loss.item())
g_losses.append(g_loss.item())
return loss, r_losses, g_losses生成器的生成對抗損失定義為:
其中,
對應的生成器代碼實現:
def generator_loss(disc_outputs):
loss = 0
gen_losses = []
for dg in disc_outputs:
l = torch.mean((dg-1)**2)
gen_losses.append(l)
loss += l
return loss, gen_losses
GAN萬字長文綜述梅爾頻譜損失
借鑒Parallel WaveGAN等前人工作,向GAN中引入重建損失和梅爾頻譜損失,可以提高模型訓練初期的穩定性、生成器的訓練效率和合成語音的保真度。具體來說,梅爾頻譜損失就是計算合成語音提取頻譜和真實語音提取頻譜之間的L1距離:
其中,
對應的損失函數實現:
loss_mel = F.l1_loss(y_mel, y_g_hat_mel)上述代碼中,y_mel表示真實語音對應的梅爾頻譜,y_g_hat_mel表示合成波形之后,合成語音轉換得到的梅爾頻譜。
特征匹配損失
鑒別器損失是用來度量從真實和生成樣本中提取的特征差異,具體來說,就是計算真實和合成樣本每一層卷積輸出之間的L1距離:
其中,
def feature_loss(fmap_r, fmap_g):
loss = 0
for dr, dg in zip(fmap_r, fmap_g):
for rl, gl in zip(dr, dg):
loss += torch.mean(torch.abs(rl - gl))
return loss整體損失
生成器的總體損失為:
其中,
判別器的總體損失為:
因為HiFiGAN的判別器是由多尺度判別器和多周期判別器組成,因此生成器的總體損失又可以寫作:
對應的代碼為:
# L1 Mel-Spectrogram Loss
loss_mel = F.l1_loss(y_mel, y_g_hat_mel) * 45
y_df_hat_r, y_df_hat_g, fmap_f_r, fmap_f_g = mpd(y, y_g_hat)
y_ds_hat_r, y_ds_hat_g, fmap_s_r, fmap_s_g = msd(y, y_g_hat)
loss_fm_f = feature_loss(fmap_f_r, fmap_f_g)
loss_fm_s = feature_loss(fmap_s_r, fmap_s_g)
loss_gen_f, losses_gen_f = generator_loss(y_df_hat_g)
loss_gen_s, losses_gen_s = generator_loss(y_ds_hat_g)
loss_gen_all = loss_gen_s + loss_gen_f + loss_fm_s + loss_fm_f + loss_mel判別器的總體損失又可以寫作:
其中,
對應的代碼為:
# MPD
y_df_hat_r, y_df_hat_g, _, _ = mpd(y, y_g_hat.detach())
loss_disc_f, losses_disc_f_r, losses_disc_f_g = discriminator_loss(y_df_hat_r, y_df_hat_g)
# MSD
y_ds_hat_r, y_ds_hat_g, _, _ = msd(y, y_g_hat.detach())
loss_disc_s, losses_disc_s_r, losses_disc_s_g = discriminator_loss(y_ds_hat_r, y_ds_hat_g)
loss_disc_all = loss_disc_s + loss_disc_f實驗
實驗中使用了3個版本,V1是大模型音質最優版本,V2是V1的縮小版,相比于V1,V2的上采樣隱層維度由512降低為128,V3是最小版,生成器上采樣中的多尺度融合模塊經過了簡化。自然度評分上,GT(4.45)WaveNet-MoL(4.02)MelGAN(3.79)HiFiGAN-V1(4.36)HiFiGAN-V2(4.23)HiFiGAN-V3(4.05)。
yangjinnew 13小時前 如果建模16k音頻,參數如何設置
羅剎斗戰勝佛 13小時前 “如果以3為周期,折疊成二維平面則是[[1,2,3],[4,5,6]]”這句話是不是寫錯了呀,因為按照原文的示意圖,如果以3為周期,那么實際上得到的是[[1, 4], [2, 5], [3, 6]]