[声纹识别基础] TDNN怎么用的一维卷积?

587 阅读4分钟

时延神经网络(TDNNs)常用于声纹识别领域,例如著名的X-VECTOR基础结构就是TDNNs;它可以视作一维卷积神经网络1(1-d CNNs),甚至有人认为TDNNs是CNNs/LeNet-5的早期版本。

计算机视觉盛行的如今,数字图像处理中二维卷积(空域滤波/模版运算/互相关运算2)对于我们而言也不再陌生,无非就是中心数值等于按元素相乘后再相加嘛。

可是,到底如何理解一维卷积?它和二维卷积又有什么联系? 不妨通过实验,一探究竟。

单输入通道/单输出通道

预期结果

首先,我们暂时忽略通道。假定输入为[1,2,1,1,3][1, 2, -1, 1, -3],核函数为[1,0,1][1, 0, -1]

卷积核是一维的,移动方向也自然是一维的

如果卷积核在输入向量上移动,请问结果是多少呢?可以看看我有没有算错。

截屏2023-01-03 09.43.48.png

顺带一提,这里输入是一行五列的矩阵。不过换一个角度,我们也可以认为每次输入的是3帧信息,并按照某种权重加和汇入1个新帧。

这里的3是卷积核尺寸(kernel sizekernel\ size),从共享参数的全连接输入的角度说,3也是上下文所涵盖的范围,(n1,n+1) or {n1,n,n+1}(n-1,n+1) \ or \ \{n-1, n, n+1\}

参考代码

# 输入
import torch.nn as nn
from torch.nn import Parameter
import torch.nn.functional as F
import torch
import numpy as np
import math

input = torch.tensor([
    [[1, 2, -1, 1, -3], ]
], dtype=torch.float)

conv1d = nn.Conv1d(in_channels=1,
                   out_channels=1,
                   kernel_size=3,
                   stride=1,
                   padding=0,
                   dilation=1,
                   bias=False,
                   padding_mode='zeros')


# 卷积核
conv1d.weight = nn.Parameter(torch.tensor([
    [[1., 0, -1], ]
]))

# 输出
conv1d(input)

运行结果

 tensor([[[2., 1., 2.]]], grad_fn=<ConvolutionBackward0>)

多输入通道/单输出通道

但是,一般说来,TDNNs输入的声学特征并不是一个向量,而是一个矩阵,横轴(宽度)与时间有关【不固定】,纵轴(高度)和频率相关【固定】,所以它有时也被称为时频谱图(Spectrogram)。

图片来自MathWorks

你还可以在 CHROME MUSIC LAB:SPECTROGRAM 上,亲自动手玩玩可视化自己的或自然的声音。

截屏2023-01-03 10.26.58.png

当然,上文所指的广义的时频谱也分很多种(常见的有MFCC、MFBank、MelSpec等),对应各种不同的处理方式(例如,模拟人耳对频率、声强的非线性,平稳信号等),不过它们一般都要经历短时傅里叶变换(分帧、加窗、离散傅里叶变换)。

预期结果

我们暂时忽略输出通道。从一维卷积的视角说,[频率,时间] 应当被视作 [通道数,输入长度],即每个通道各有一个卷积核或一组共享权重,每个通道独立计算再把各个通道的结果相加3

假定输入为双通道(例如表示高频和低频)[[1,2,1,1,3],[3,1,0,1,2]][[1, 2, -1, 1, -3],[3, 1, 0, -1, 2]]各通道上的核函数分别为[1,0,1][1, 0, -1][0.5,0,0.5][0.5, 0, 0.5],请问这次结果又是多少呢?相信你肯定比我算的快。

截屏2023-01-03 11.00.23.png

参考代码

# 输入
input = torch.tensor([
    [[1, 2, -1, 1, -3],
     [3, 1, 0, -1, 2]]
], dtype=torch.float)

conv1d = nn.Conv1d(in_channels=2,
                   out_channels=1,
                   kernel_size=3,
                   stride=1,
                   padding=0,
                   dilation=1,
                   bias=False,
                   padding_mode='zeros')

# 卷积核
conv1d.weight = nn.Parameter(torch.tensor([
    [[1., 0, -1],
     [0.5, 0, 0.5]],
]))

# 输出
conv1d(input)

运行结果

 tensor([[[3.5000, 1.0000, 3.0000]]], grad_fn=<ConvolutionBackward0>)

多输入通道/多输出通道

预期结果

最后,该如何理解输出通道呢?其实无非就是把单输出通道的那一份卷积核,扩增几倍而已。例如,仍然假定输入为双通道[[1,2,1,1,3],[3,1,0,1,2]][[1, 2, -1, 1, -3],[3, 1, 0, -1, 2]]不过此时我们考虑简化的双输出通道的情况:

第一份输出:各输入通道上的核函数分别为[1,0,1][1, 0, -1][0.5,0,0.5][0.5, 0, 0.5]

第二份输出:各输入通道上的核函数仍然是[1,0,1][1, 0, -1][0.5,0,0.5][0.5, 0, 0.5]

那么,我们就可以得到双倍的输出。

参考代码

# 输入
input = torch.tensor([
    [[1, 2, -1, 1, -3],
     [3, 1, 0, -1, 2]]
], dtype=torch.float)

conv1d = nn.Conv1d(in_channels=2,
                   out_channels=2,
                   kernel_size=3,
                   stride=1,
                   padding=0,
                   dilation=1,
                   bias=False,
                   padding_mode='zeros')

# 卷积核
conv1d.weight = nn.Parameter(torch.tensor([
    [[1., 0, -1],
     [0.5, 0, 0.5]],
    
    [[1., 0, -1],
     [0.5, 0, 0.5]],
]))

# 输出
conv1d(input)

运行结果

tensor([[[3.5000, 1.0000, 3.0000],
         [3.5000, 1.0000, 3.0000]]], grad_fn=<ConvolutionBackward0>)

从二维卷积角度理解一维卷积

预期结果

其实,如果将各个通道的一维卷积核拼接起来:所谓的一维卷积在各通道上的加和,完全可以用二维卷积核[channels,kernel_size][channels, kernel\_ size]按元素相乘后再相加理解。

ecapa-tdnn-第 5 页.png

所以,许多博主所认为的一维卷积不代表卷积核是一维的,只代表卷积核的移动方向是一维的这种说法,从某种角度说,也有一定的道理。

v1_or_v2.png

不过值得注意的是,Pytorch中Conv1d和Conv2d输入维度不一样。

例如, 一维卷积 [batch,channels,length][batch, channels, length], 二维卷积 [batch,channels,height,width][batch, channels, height, width]

参考代码

# 输入
input = torch.tensor([
    [[1, 2, -1, 1, -3],
     [3, 1, 0, -1, 2]]
], dtype=torch.float)

input=input.unsqueeze(dim=1)

conv2d = nn.Conv2d(in_channels=1,
                   out_channels=2,
                   kernel_size=(input.shape[2],3),
                   stride=1,
                   padding=0,
                   dilation=1,
                   bias=False,
                   padding_mode='zeros')

# 卷积核
conv2d.weight = nn.Parameter(torch.tensor([
    [[[1., 0, -1],
     [0.5, 0, 0.5]]],
    
    [[[1., 0, -1],
     [0.5, 0, 0.5]]],
]))

# 输出
conv2d(input)

运行结果

tensor([[[[3.5000, 1.0000, 3.0000]],
         [[3.5000, 1.0000, 3.0000]]]], grad_fn=<ConvolutionBackward0>)

Footnotes

  1. danielpovey.com/files/2018_…

  2. zh.d2l.ai/chapter_con…

  3. zh.d2l.ai/chapter_con…