音频信号处理已经进入了神经网络时代,而CNN由于其强大的建模能力,已被广泛地应用在了各种音频信号处理网络中,像最近几届DNS Challenge的冠军所提出的网络均大量使用了CNN。与图像处理不同,音频信号处理在大多数应用场景下都需要满足实时性要求,比如在线会议场景,直播场景等。满足实时性一般需要满足以下两个条件:算法是因果的且计算复杂度要低。本文主要讨论如何控制CNN网络的因果性,并不对其计算复杂度做过多讨论。
从网络架构来说,以CNN为主的音频信号处理网络可以分为以下两大类:
只包含卷积的Encoder架构
即包含卷积又包含转置卷积的Encoder-Decoder架构
本文对上述两种网络架构均进行了讨论,并进一步讨论了在实际coding过程中应该怎么做以及可能遇到的一些问题。
在讨论之前,先简单介绍下系统的因果性。大致意思为:当一个系统当前时刻的输出只依赖于当前时刻的输入以及(或)过去时刻的输入时,该系统就是因果的。与之相对应的,当一个系统当前时刻的输出会依赖未来时刻的输入时,该系统就是非因果的。
可见,在实时应用中,音频信号处理算法往往需要是因果的,以对当前时刻的输入信号作出及时的响应。
该节先从最简单的一层CNN开始讨论,接着拓展到多层CNN的情况。
3.1. 一层CNN网络
考虑一个只包含了一层二维卷积的网络,该二维卷积在时间维度上的参数为:kernel=3, stride=1。
3.1.1. 网络的输入,输出以及label
假设此时输入给网络一个时长为5帧的音频信号,那么输出信号的时长应该为5-kernel+1 = 5-3+1=3帧,网络的输出比输入少了2帧(这2帧作为音频信号的上下文信息被网络消耗掉了)。那么问题来了:输出信号只有3帧,但label信号和输入信号一样都是5帧,输出信号的时长不等于label信号的时长,该怎么计算loss函数那?
3.1.2. 决定网络的因果性
一个自然而然的想法就是:从label信号中选取3帧信号,保证label信号和输出信号的时长一样不就得了?没错,确实是这样做,而且正是该选取过程决定了卷积网络的因果性。
以目前所讨论的这个网络来说,有3种选取方案,如图1所示。

图 1 一层卷积网络因果性示例
图1(a)选取label信号中的最后3帧,这种选取方式确定了该网络是因果的。因为从图中可以看出当前时刻的输出帧只利用了当前时刻的输入帧以及历史的两帧信息;
图1(b)选取label信号中最中间的3帧,这种选取方式确定了该网络是非因果的。因为从图中可以看出当前时刻的输出帧除了利用当前时刻的输入帧以及历史的1帧信息外,还使用了未来的1帧信息;
图1(c)选取label信号中最前面的3帧,这种选取方式确定了该网络是非因果的。因为从图中可以看出当前时刻的输出帧除了利用当前时刻的输入帧外,还使用了未来的2帧信息;
3.1.3. 补零操作
通过上述分析,相信读者已经熟悉该如何决定卷积网络的因果性,以及如何控制卷积网络的感受野。那么在实际coding的过程中该怎么做,才能实现图1所示的3种方式那?主要有以下两种方法:
丢label中的数据
给输入数据补零
丢label中的数据:这种实现方式其实就是直接根据3.1.2.小节所分析的来做的。丢弃label信号中的前两帧信号得到的就是图1(a)所表示的;丢弃label信号中的第一帧和最后一帧信号得到的就是图1(b)所表示的;丢弃label信号中的最后两帧信号得到的就是图1(c)所表示的;
给输入数据补零:那么我可不可以保持label数据不变,通过其他方法来实现那?当然可以!正所谓“山不过来,我就过去”,既然要求label数据不变,那我就变输入数据,给输入数据补零。相信读者在一些论文或者开源代码中也见过补零这种实现方式。补零相比上述实现方式有什么好处那?笔者考虑了一下,主要想出了以下两点说得过去的原因:
为了不浪费数据。从上述分析可以看到,当卷积网络在时间维度上的kernel大于1时,网络的输出时长会比输入时长少kernel-1帧。而kernel越大,网络的输出时长就越小。为了能将辛辛苦苦生成的训练数据全部用上,可以给输入信号补kernel-1帧的零,这样就能使得网络的输出时长等于输入时长(有效的输入时长,即补零之前的时长);
为了和实时推理代码保持一致。考虑一个实时应用会遇到的一个场景:假设某个音频处理算法运行在时频域(其STFT的窗长和帧长均为20ms,帧移为10ms),当算法接收到第一帧10ms信号的时候,为了做STFT,往往需要在前面补一帧10ms的零,凑足20ms。在因果卷积网络中补零的原因之一也是为了和这种场景保持一致。
不同的补零方式会导致不同的因果关系,下面就详细说说,具体该怎么补零。
为了实现图1(a),可以在输入数据的最前面补两帧零;
为了实现图1(b),可以在输入数据的最前面和最后面各补一帧零;
为了实现图1(c),可以在输入数据的最后面补两帧零。
3.1.4. 总结
从对一层CNN网络的分析过程来看,可以得到以下两个重要结论:
从label信号中选取所需信号的过程决定了卷积网络的因果性和感受野;
在具体coding的过程中,可以通过给输入数据补零来实现不同的因果关系。
3.2. 多层CNN网络
本节考虑包含了两层二维卷积的CNN网络,且每层二维卷积在时间维度上的参数均为:kernel=3, stride=1。类似于图1,现给出相应的两层CNN网络的因果性示例图,如图2所示。

图 2 二层卷积网络因果性示例
需要注意的是,图2只给出了其中的三种输出结果,还有其余两种输出结果读者可自行分析。通过图2可以看出:
图2(a)表示的是非因果网络,因为输出帧除了利用当前时刻的输入帧外,还使用了未来的4帧信息;
图2(b)表示的是非因果网络,因为输出帧除了利用当前时刻的输入帧以及历史的2帧信息外,还使用了未来的2帧信息;
图2(c)表示的是因果网络,因为输出帧只利用了当前时刻的输入帧以及历史的4帧信息。
与3.1.节的分析过程一样,在实际coding过程中,该如何补零才能使得两层CNN网络实现图2中所示的因果性那?对于两层CNN网络来说,笔者能想到两种补零方式:一次性补零和逐层补零。下面就详细讨论下这两种方式,以及各自的优缺点。
3.2.1. 一次性补零
一次性补零其实就是把两层卷积网络等效于一层卷积网络。上述两层卷积网络可以等效于kernel=5, stride=1 的一层卷积网络(只是从因果性和感受野角度来说是可以等效的)。从3.1.3.小节的分析可知:
给输入数据的最后面补四帧零,可以实现图2(a);
给输入数据的最前面和最后面各补两帧零,可以实现图2(b);
给输入数据的最前面补四帧零,可以实现图2(c)。
也就是说一次性补零只在输入数据上补零,网络隐藏层不需要补零;下面要讨论的逐层补零除了在输入层补零外也在网络隐藏层补零。
3.2.2. 逐层补零
逐层补零是只考虑当前层。具体来说,在实际coding的过程中需要:
为了实现图2(a),需要在输入层和隐藏层的最后面分别补两帧零;
为了实现图2(b),需要在输入层和隐藏层的最前面和最后面分别补一帧零;
为了实现图2(c),需要在输入层和隐藏层的最前面分别补两帧零。
故事还在继续中...
