草庐IT

【ECAPA_TDNN 下 】代码和论文细节分析

崔西的梅尔之旅 2023-04-18 原文

ECAPA_TDNN代码和论文细节分析

来源:INTERSPEECH 2020
机构:比利时根特大学
论文地址:
源码地址:
论文阅读博客:ECAPA_TDNN 上

一、数据部分(dataloader.py)

  1. 数据集: Voxceleb2 5994个说话人
  2. 数据增强: 每个话语生成6个额外的样本
    (1) 结合MUSAN(嘈杂的人声,噪声)数据集提供的RIR数据集(混响)生成三个。
    (2) 利用Sox (tempo up, tempo down)和ffmpeg (alternating opus or aac compression) 生成三个。
    (3) SpecAugment算法:随机掩码。(在第二部分具体说明)

MUSAN数据集下载:wget https://www.openslr.org/resources/17/musan.tar.gz
RIR数据集下载:wget https://openslr.org/resources/28/rirs_noises.zip

  1. 相关代码
    数据增强过程如下:先对音频长度进行调整,再通过选择语句随机选择增强方式。
audio, sr = soundfile.read(self.data_list[index])		
#将所有音频调整为一个长度
length = self.num_frames * 160 + 240
if audio.shape[0] <= length:
	shortage = length - audio.shape[0]
	audio = numpy.pad(audio, (0, shortage), 'wrap')
start_frame = numpy.int64(random.random()*(audio.shape[0]-length))
audio = audio[start_frame:start_frame + length]
audio = numpy.stack([audio],axis=0)
# 数据增强
augtype = random.randint(0,5)
if augtype == 0:   # Original
	audio = audio
elif augtype == 1: # Reverberation混响
	audio = self.add_rev(audio)
elif augtype == 2: # Babble
	audio = self.add_noise(audio, 'speech')
elif augtype == 3: # Music
	audio = self.add_noise(audio, 'music')
elif augtype == 4: # Noise
	audio = self.add_noise(audio, 'noise')
elif augtype == 5: # Television noise
	audio = self.add_noise(audio, 'speech')
	audio = self.add_noise(audio, 'music')
return torch.FloatTensor(audio[0]), self.data_label[index]

如下为混响增强,随机从数据集中选取混响音频,再增加混响音频的维度与人声音频保持一致,最后对人声音频和混响音频做一个卷积。

#添加混响
def add_rev(self, audio):
	rir_file    = random.choice(self.rir_files)
	rir, sr     = soundfile.read(rir_file)
	rir         = numpy.expand_dims(rir.astype(numpy.float),0) 
	rir         = rir / numpy.sqrt(numpy.sum(rir**2))
	return signal.convolve(audio, rir, mode='full')[:,:self.num_frames * 160 + 240]

如下为噪声增强,先获得人声音频的db,随机选出n个噪声音频,将噪声音频长度调整至与人声音频一致,再获得噪声音频的db,随机获取noise信噪比,然后计算出噪声系数,并与噪声音频相乘。将所有噪声音频进行concatencate,再与人声音频叠加。

def add_noise(self, audio, noisecat):
  #numpy.mean(audio ** 2) 为信号功率
	clean_db    = 10 * numpy.log10(numpy.mean(audio ** 2)+1e-4) 
	numnoise    = self.numnoise[noisecat]
	noiselist   = random.sample(self.noiselist[noisecat], random.randint(numnoise[0],numnoise[1]))
	noises = []
	for noise in noiselist:
		#假设噪声音频长度已调整至人声音频一致
		noise_db = 10 * numpy.log10(numpy.mean(noiseaudio ** 2)+1e-4) 
		noisesnr   = random.uniform(self.noisesnr[noisecat][0],self.noisesnr[noisecat][1])
		#noiseaudio乘以噪声系数
		noises.append(numpy.sqrt(10 ** ((clean_db - noise_db - noisesnr) / 10)) * noiseaudio)
	noise = numpy.sum(numpy.concatenate(noises,axis=0),axis=0,keepdims=True)
	return noise + audio

二、网络结构(model.py)

2.1 整体网络结构

网络结构如下

  1. 数据增强
  2. TDNN block
  3. 多层特征聚合
  4. 注意力统计池化
  5. FC+BN
  6. 输出
def forward(self, x, aug):
      #数据增强
	    with torch.no_grad():
	        x = self.torchfbank(x)+1e-6
	        x = x.log()   
	        x = x - torch.mean(x, dim=-1, keepdim=True)
	        if aug == True:
	            x = self.specaug(x)
	    
	    #相当于一个TDNN block
	    x = self.conv1(x)
	    x = self.relu(x)
	    x = self.bn1(x)
	    
	    #多层特征聚合
	    x1 = self.layer1(x)
	    x2 = self.layer2(x+x1)
	    x3 = self.layer3(x+x1+x2)
	
	    x = self.layer4(torch.cat((x1,x2,x3),dim=1))
	    x = self.relu(x)
	    
	    #注意力统计池化
	    t = x.size()[-1] 
	    global_x = torch.cat((x,torch.mean(x,dim=2,keepdim=True).repeat(1,1,t), torch.sqrt(torch.var(x,dim=2,keepdim=True).clamp(min=1e-4)).repeat(1,1,t)), dim=1)
	    w = self.attention(global_x)
	    mu = torch.sum(x * w, dim=2)
	    sg = torch.sqrt( ( torch.sum((x**2) * w, dim=2) - mu**2 ).clamp(min=1e-4) )
	    x = torch.cat((mu,sg),1)

	    x = self.bn5(x)
	    x = self.fc6(x)
	    x = self.bn6(x)
	
	    return x

2.2 SpecAugment算法

SpecAugment算法是一种添加掩码的数据增强算法,步骤如下:

  1. 预加重:PreEmphasis(torch.nn.Module)
  2. 提取梅尔
    torchaudio.transforms.MelSpectrogram(sample_rate=16000, n_fft=512, win_length=400, hop_length=160, f_min = 20, f_max = 7600, window_fn=torch.hamming_window, n_mels=80)
  3. 将梅尔进行零均值归一化,可以直接将Mask位置设为0
  4. 时间维度掩码
  5. 频率维度掩码

总代码:

 with torch.no_grad():
     #预加重和提取梅尔
	 x = self.torchfbank(x)+1e-6
	 #对数梅尔
	 x = x.log()   
	 x = x - torch.mean(x, dim=-1, keepdim=True)
	 if aug == True:
	     #添加掩码
	     x = self.specaug(x)

掩码部分主要代码:
1.获取梅尔的维度,分别赋值为batch, fea, time
batch为每批次输入梅尔的数量;
fea为每一个梅尔的特征维度,这里应该为80;
time为每一个梅尔的时间维度
2.掩码的长度:生成[batch, 1, 1]维数组
3.掩码的位置:生成[batch, 1 ,1]维数组,根据长度和梅尔的维度调整
4.生成一个D维张量,并将其增加维度至[1,1,D]
5.根据掩码长度和掩码位置得到掩码:[batch, 1 , D] ->[batch, D] ->[batch, 1 , D] or [batch, D, 1]
6.将梅尔掩码的地方赋值为0

def mask_along_axis(self, x, dim):
    original_size = x.shape
    batch, fea, time = x.shape
    if dim == 1:
        D = fea
        width_range = self.freq_mask_width
    else:
        D = time
        width_range = self.time_mask_width

    mask_len = torch.randint(width_range[0], width_range[1], (batch, 1), device=x.device).unsqueeze(2)
    mask_pos = torch.randint(0, max(1, D - mask_len.max()), (batch, 1), device=x.device).unsqueeze(2)
    arange = torch.arange(D, device=x.device).view(1, 1, -1)
    mask = (mask_pos <= arange) * (arange < (mask_pos + mask_len))
    mask = mask.any(dim=1)

    if dim == 1:
        mask = mask.unsqueeze(2)
    else:
        mask = mask.unsqueeze(1)
    #用0填充张量x中对应mask位置处为True的元素
    x = x.masked_fill_(mask, 0.0)
    return x.view(*original_size)

2.3 注意力统计池化

主要是通过两个公式计算加权平均和加权标准差:
μ c = ∑ t T α t , c h t , c \mu_{c} = \sum^{T}_{t}\alpha_{t,c}h_{t,c} μc=tTαt,cht,c
σ c = ∑ t T α t , c h t , c 2 − μ c 2 \sigma_{c} = \sqrt{\sum^{T}_{t}\alpha_{t,c}h^{2}_{t,c}-\mu^2_{c}} σc=tTαt,cht,c2μc2
池化层的最终输出由加权平均μ和加权标准差σ的向量串联得到。

# 得到时间帧
t = x.size()[-1]
# 获取时间帧维度的均值和标准差,然后串联原始数据
mean = torch.mean(x,dim=2,keepdim=True).repeat(1,1,t)
standrad = torch.sqrt(torch.var(x,dim=2,keepdim=True).clamp(min=1e-4)).repeat(1,1,t))
global_x = torch.cat((x, mean, standrad), dim=1)
#通过注意力网络得到注意力矩阵w
w = self.attention(global_x)
self.attention = nn.Sequential(
            nn.Conv1d(4608, 256, kernel_size=1),
            nn.ReLU(),
            nn.BatchNorm1d(256),
            nn.Tanh(), # I add this layer
            nn.Conv1d(256, 1536, kernel_size=1),
            nn.Softmax(dim=2),
            )

mu = torch.sum(x * w, dim=2)
sg = torch.sqrt( ( torch.sum((x**2) * w, dim=2) - mu**2 ).clamp(min=1e-4) )
x = torch.cat((mu,sg),1)

2.4 SE Res2Blocks

2.4.1 SE block

一维SE blocks,重新缩放帧级特征,得到通道的重要性。
过程为:
1.特征通过全局平均池化进行压缩
2.用两个全连接层,主要是为了应用relu和sigmoid(将输出映射至0和1)。第一个全连接层降低维度,第二个全连接层恢复维度。
3.输出为输入乘以权重矩阵。

 def __init__(self, channels, bottleneck=128):
    super(SEModule, self).__init__()
    self.se = nn.Sequential(
        #全局平均池化压缩为1个数
        nn.AdaptiveAvgPool1d(1),
        nn.Conv1d(channels, bottleneck, kernel_size=1, padding=0),
        nn.ReLU(),
        nn.Conv1d(bottleneck, channels, kernel_size=1, padding=0),
        nn.Sigmoid(),
        )

def forward(self, input):
    #获得权重矩阵
    x = self.se(input)
    return input * x

2.4.2 res2net

res2net主要是利用细粒度的多尺度信息,产生多个感受野的组合。下面左图是res2net多尺度的具体做法,右图是本论文res2net模块的网络结构。
由左图可得,res2net将传统resnet中的3*3卷积进行了多尺度的解耦,在1 * 1卷积之后对通道进行分组,尺度越大计算开销越大。
由右图可知,包含了扩展卷积和前后密集层,第一个密集层用于降低维度,第二个密集层用于恢复维度,最后由SE模块缩放每一个通道。

本文所用的res2net采用了8尺度,在代码中x1是作为最后一个直接送到。

def forward(self, x):
     residual = x
     #############################
     out = self.conv1(x)
     out = self.relu(out)
     out = self.bn1(out)
     #############################
     #############################
     #这里是res2net的核心
     spx = torch.split(out, self.width, 1)
     #分块卷积计算
     for i in range(self.nums):
       if i==0:
         sp = spx[i]
       else:
         sp = sp + spx[i]
       sp = self.convs[i](sp)
       sp = self.relu(sp)
       sp = self.bns[i](sp)
       if i==0:
         out = sp
       else:
         out = torch.cat((out, sp), 1)
     #cat x1的块
     out = torch.cat((out, spx[self.nums]),1)
     ###############################
     ###############################
     out = self.conv3(out)
     out = self.relu(out)
     out = self.bn3(out)
     ###############################
     out = self.se(out)
     out += residual
     return out 

2.5 MFA多层特征聚合

MFA是多层特征聚合,将SE Res2Blocks输出特征映射连接起来

整体代码

x1 = self.layer1(x)
x2 = self.layer2(x+x1)
x3 = self.layer3(x+x1+x2)
x = self.layer4(torch.cat((x1,x2,x3),dim=1))
x = self.relu(x)

三、损失函数AAMsoftmax(loss.py)

详细介绍见:https://blog.csdn.net/qq_39478403/article/details/116788113
加性角度边界损失最早用于人脸识别任务。
原理:最大化类间间距,最小化类内间距。
softmax loss在决策边界产生明显的模糊性,但是AAMsoftmax通过添加加性角度边距可以扩大类间的间隙。

  1. 归一化输入特征和FC层权重。令所得归一化特征 x i x_{i} xi与第j类别的FC层权重点乘得到FC层的第j个输出 c o s θ j cosθ_{j} cosθj,将特征 x i x_{i} xi预测为第j类的预测值
cosine = F.linear(F.normalize(x), F.normalize(self.weight))
  1. 根据正余弦公式计算 s i n θ j sin\theta_{j} sinθj
sine = torch.sqrt((1.0 - torch.mul(cosine, cosine)).clamp(0, 1))
  1. 根据当前夹角的正余弦,计算添加了加性角度边距m的 c o s ( θ + m ) cos(\theta+m) cos(θ+m)
phi = cosine * self.cos_m - sine * self.sin_m
  1. 松弛约束
phi = torch.where((cosine - self.th) > 0, phi, cosine - self.mm)
  1. 生成标签矩阵
one_hot = torch.zeros_like(cosine) #全0矩阵
one_hot.scatter_(1, label.view(-1, 1), 1) #在label索引上用1替换0
  1. 当输入特征x对应真实类别,采用新 Target Logit cos(θ_yi + m)
    其余并不对应输入特征x的真实类别的类,保持原有的logit
output = (one_hot * phi) + ((1.0 - one_hot) * cosine)
  1. 使用scale缩放新的logit
output = output * self.s
  1. 计算损失
loss = self.ce(output, label)
self.ce = nn.CrossEntropyLoss()

有关【ECAPA_TDNN 下 】代码和论文细节分析的更多相关文章

  1. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  2. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

    在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

  3. ruby-on-rails - 浏览 Ruby 源代码 - 2

    我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru

  4. ruby - 模块嵌套代码风格偏好 - 2

    我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的

  5. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  6. ruby - Net::HTTP 获取源代码和状态 - 2

    我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur

  7. 程序员如何提高代码能力? - 2

    前言作为一名程序员,自己的本质工作就是做程序开发,那么程序开发的时候最直接的体现就是代码,检验一个程序员技术水平的一个核心环节就是开发时候的代码能力。众所周知,程序开发的水平提升是一个循序渐进的过程,每一位程序员都是从“菜鸟”变成“大神”的,所以程序员在程序开发过程中的代码能力也是根据平时开发中的业务实践来积累和提升的。提高代码能力核心要素程序员要想提高自身代码能力,尤其是新晋程序员的代码能力有很大的提升空间的时候,需要针对性的去提高自己的代码能力。提高代码能力其实有几个比较关键的点,只要把握住这些方面,就能很好的、快速的提高自己的一部分代码能力。1、多去阅读开源项目,如有机会可以亲自参与开源

  8. 7个大一C语言必学的程序 / C语言经典代码大全 - 2

    嗨~大家好,这里是可莉!今天给大家带来的是7个C语言的经典基础代码~那一起往下看下去把【程序一】打印100到200之间的素数#includeintmain(){ inti; for(i=100;i 【程序二】输出乘法口诀表#includeintmain(){inti;for(i=1;i 【程序三】判断1000年---2000年之间的闰年#includeintmain(){intyear;for(year=1000;year 【程序四】给定两个整形变量的值,将两个值的内容进行交换。这里提供两种方法来进行交换,第一种为创建临时变量来进行交换,第二种是不创建临时变量而直接进行交换。1.创建临时变量来

  9. git使用常见问题(提交代码,合并冲突) - 2

    文章目录git常用命令(简介,详细参数往下看)Git提交代码步骤gitpullgitstatusgitaddgitcommitgitpushgit代码冲突合并问题方法一:放弃本地代码方法二:合并代码常用命令以及详细参数gitadd将文件添加到仓库:gitdiff比较文件异同gitlog查看历史记录gitreset代码回滚版本库相关操作远程仓库相关操作分支相关操作创建分支查看分支:gitbranch合并分支:gitmerge删除分支:gitbranch-ddev查看分支合并图:gitlog–graph–pretty=oneline–abbrev-commit撤消某次提交git用户名密码相关配置g

  10. ruby - 这两段代码有什么区别? - 2

    打印1:defsum(i)i=i+[2]end$x=[1]sum($x)print$x打印12:defsum(i)i.push(2)end$x=[1]sum($x)print$x后者是修改全局变量$x。为什么它在第二个例子中被修改而不是在第一个例子中?类Array的任何方法(不仅是push)都会发生这种情况吗? 最佳答案 变量范围在这里无关紧要。在第一段代码中,您仅使用赋值运算符=为变量i赋值,而在第二段代码中,您正在修改$x(也称为i)使用破坏性方法push。赋值从不修改任何对象。它只是提供一个名称来引用一个对象。方法要么是破坏性

随机推荐