谈谈音频信号处理中CNN的因果性

本文对笔者关于CNN因果性的理解作以记录,适合对音频信号处理以及神经网络有基础的读者阅读。如有表述不当之处欢迎批评指正。欢迎任何形式的转载,但请务必注明出处。

1. 前述

  音频信号处理已经进入了神经网络时代,而CNN由于其强大的建模能力,已被广泛地应用在了各种音频信号处理网络中,像最近几届DNS Challenge的冠军所提出的网络均大量使用了CNN。与图像处理不同,音频信号处理在大多数应用场景下都需要满足实时性要求,比如在线会议场景,直播场景等。满足实时性一般需要满足以下两个条件:算法是因果的计算复杂度要低。本文主要讨论如何控制CNN网络的因果性,并不对其计算复杂度做过多讨论。

  从网络架构来说,以CNN为主的音频信号处理网络可以分为以下两大类:

  • 只包含卷积的Encoder架构
  • 即包含卷积又包含转置卷积的Encoder-Decoder架构

本文对上述两种网络架构均进行了讨论,并进一步讨论了在实际coding过程中应该怎么做以及可能遇到的一些问题。

2. 因果性

  在讨论之前,先简单介绍下系统的因果性。因果性是指:当一个系统当前时刻的输出只依赖于当前时刻的输入以及(或)过去时刻的输入时,该系统就是因果的。与之相对应的,当一个系统当前时刻的输出会依赖未来时刻的输入时,该系统就是非因果的。

  可见,在实时应用中,音频信号处理算法往往需要是因果的,以对当前时刻的输入信号作出及时的响应。

3. Encoder 架构

  Encoder架构主要由卷积网络构成, 该节先从最简单的一层卷积网络开始讨论,接着拓展到多层卷积网络的情况。

3.1. 一层卷积网络

  考虑一个只包含了一层二维卷积的网络,该二维卷积在时间维度上的参数为: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 一层卷积网络因果性示例

  

  • 1(a)选取label信号中的最后3帧,这种选取方式确定了该网络是因果的。因为从图中可以看出当前时刻的输出帧只利用了当前时刻的输入帧以及历史的两帧信息;
  • 1(b)选取label信号中最中间的3帧,这种选取方式确定了该网络是非因果的。因为从图中可以看出当前时刻的输出帧除了利用当前时刻的输入帧以及历史的1帧信息外,还使用了未来的1帧信息;
  • 1(c)选取label信号中最前面的3帧,这种选取方式确定了该网络是非因果的。因为从图中可以看出当前时刻的输出帧除了利用当前时刻的输入帧外,还使用了未来的2帧信息;

3.1.3. 补零操作

  通过上述分析,相信读者已经熟悉该如何决定卷积网络的因果性,以及如何控制卷积网络的感受野。那么在实际coding的过程中,当拥有了输入数据和等时长的label数据之后,该怎么做,才能实现图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. 总结

从对一层卷积网络的分析过程来看,可以得到以下两个重要结论:

  • label信号中选取所需信号的过程决定了卷积网络的因果性和感受野;
  • 在具体coding的过程中,可以通过给输入数据补零来实现不同的因果关系

3.2. 多层卷积网络

   本节考虑包含了两层二维卷积的网络,且每层二维卷积在时间维度上的参数均为:kernel=3, stride=1。类似于图1,现给出相应的两层CNN网络的因果性示例图,如图2所示。
图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),可以在输入层和隐藏层的最前面分别补两帧零。

逐层补零只考虑当前层。以图2(a)为例,输入层为5帧数据,为了使第一层CNN的输出也为5帧数据,并且保持只看当前帧和未来帧的因果性,需要给输入层的最后面补kernel-1=3-1=2帧零。同样,为了使第二层CNN的输出也为5帧数据,并且保持只看当前帧和未来帧的因果性,需要给第一层CNN输出(隐藏层)的最后面补kernel-1=3-1=2帧零。可以看出逐层补零保证每层CNN的输出时长都和label信号的时长保持一致。

3.2.3. 两种补零方式对比

  两种不同的补零方式可以说体现出了两种不同的思维方式。

  • 输入层补零:将多层CNN网络当成一个整体看待。先分析这个整体总共需要补多少零,需要怎么补,然后直接只在输入层操作就行;
  • 逐层补零:将多层CNN网络中的每一层当成单独的个体看待。根据当前层的kernelstride求出当前层需要补多少零,根据要满足的因果性和感受野确定当前层的零该怎么补,然后在当前层直接操作就行。 逐层补零能保证每层的输出时长和输入时长(有效的输入时长,即补零之前的时长)一致,而且当前层只需干好它自己要干的事就好,不必考虑其他层。层与层之间的关系可以用一句古语来概括“各人自扫门前雪,莫管他人瓦上霜”。

下面再举个额外的例子,用以说明在实际coding过程中两种补零方式分别该怎么做。比如开发者要搭建一个两层CNN网络,且第一层CNN的参数配置为:kernel=3, stride=1,第二层CNN的参数配置为:kernel=2, stride=1。现在想要使这个网络的输出帧只能看未来一帧的输入信息。

   输入层补零的做法
   这个两层CNN网络可以等效为kernel=4, stride=1的一层CNN网络。因此,需要给输入数据补kernel-1=4-1=3帧零。为了只看未来一帧的输入信息,需要将2帧零补在输入数据的最前面,1帧零补在输入数据的最后面。

   逐层补零的做法
   可知输入层需要补3-1=2帧零,第一层CNN的输出需要补2-1=1帧零。接下来就是确定每层的零具体该怎么补。为了只看未来一帧的输入信息,可以有以下两种补零方案:

  • 输入层需要补的2帧零全补在最前面,且第一层CNN的输出需要补的1帧零补在最后面;这种方式使得第一层CNN没利用未来的输入信息,而第二层CNN利用了未来一帧的输入信息。
  • 输入层需要补的2帧零在最前面和最后面各补1帧,且第一层CNN的输出需要补的1帧零补在最前面;这种方式使得第一层CNN利用了未来一帧的输入信息,第二层CNN没有利用未来的输入信息。

  可以看到,在具体coding的时候,输入层补零这种方式实现方法唯一,而逐层补零这种方式有多种实现方法。

3.2.4. 逐层补零需要注意的地方

   假设一个这样的使用场景:1)模型是按照逐层补零的方式实现的因果模型;2)在验证模型效果的时候(即推理的时候)直接使用load()函数加载模型,而且按照线上运行的方式那样,每次给模型输入一帧数据,然后模型输出一帧数据;这个时候得到的输出结果很大概率上是不正确的。这是因为在训练模型的时候,模型的输入数据时长往往远远大于所补零的时长,除了输入数据中比较靠前的数据看到的历史信息是补的零之外,其余输入数据看到的历史信息基本都是有效的语音信息。而在推理过程中,每次只输入一帧数据,这导致输入的所有帧看到的历史信息都是补的零,而不是有效的语音信息。因此,输出结果大概率是不正确的。为了避免这种状况,1)只是为了验证模型效果的话,在推理的时候,可以增加模型的输入数据时长,而不是输入一帧数据;2)如果非要按照线上运行的方式那样,输入一帧数据输出一帧数据,那么就需要用c/python实现推理过程,与此同时需要维护好每层网络的输入buffer。这是一项工作量较大的工程。
   同样的场景,输入层补零的方式解决起来相对简单些,只需维护好输入层的buffer就行。

3.2.5. 总结

本节以二层卷积网络为例,分析了多层网络如何通过补零来实现不同的因果性。在实际coding过程中主要有输入层补零逐层补零两种方式,可根据需要选择适合的补零方式。

3.3. 总结

对于Encoder架构而言,通过上述的分析可知,不管是一层卷积网络还是多层卷积网络,不管是输入层补零还是逐层补零,如果想要实现的网络是因果的,那么零应该全部补在最前面,如果想要实现的网络是非因果的,那么应该在最后面补零。在最前面补了几帧零,整个网络就使用了多少帧的历史信息。同理,在最后面补了几帧零,整个网络就使用了多少帧的未来信息。

4. Encoder-Decoder 架构

   Encoder-Decoder是一种对称的网络结构。其中,Encoder包含的是卷积网络,Decoder包含的是转置(或称之为反转/逆)卷积网络。而且一般Decoder中每个转置卷积网络层和与之对应的Encoder中的卷积网络层的参数配置(kernel,stride…)是完全一样的。本节先简单回顾下转置卷积,然后分析一层卷积(转置卷积)网络的因果性,最后分析多层卷积(转置卷积)网络的因果性。

4.1. 转置卷积

  与卷积相反,转置卷积会增加输入特征的维度。本节以kernel=3, stride=1的一层转置卷积网络为例进行说明。

  当给同等参数配置下的一层卷积网络输入5帧的音频信号时,它会输出3帧的音频信号。而如果将这3帧音频信号输入给一层转置卷积网络,那么将会输出5帧音频信号。图3展示了该转置卷积网络的计算过程。
图3

3 转置卷积网络计算过程示例

  
  由于kernel=3,所以转置卷积网络每一帧输入所对应的输出都是3帧,将对应位置上的所有帧的输出相加得到转置卷积网络的最终输出。比如输出中的第二帧等于第一帧输出的第二个元素与第二帧输出的第一个元素之和,如图3中红圈所示。

4.2. 一层卷积(转置卷积)网络

  考虑一个kernel=3, stride=1的一层卷积(转置卷积)网络。假设网络的输入是时长为5帧的音频信号,那么经过卷积网络之后,时长变为3帧;接着,该3帧信号经过转置卷积网络之后,输出时长又变为5帧,与输入信号和label信号的时长是相等的。因此,此时可以正常计算loss函数。注意在此过程中,是没有额外的补零或者其他什么操作的。那么,此时网络的因果性和感受野是怎么样的那?可以通过图4分析一下整个计算过程。以输出的第3帧为例,可以看出计算该帧的过程中,除了使用输入的当前帧和历史两帧信息外,还使用了未来两帧信息。其余输出帧所使用的输入帧信息可以通过图4中的箭头走向分析得到。可以看到,此时网络是非因果网络,且使用了未来两帧的信息。
图4

4 一层卷积网络和转置卷积网络计算过程示例

4.2.1. 控制网络的因果性和感受野

  那么,在具体coding过程中,该怎么做才能得到一个因果网络,并且使得该网络输出的有效时长还是5帧那?答案是输入数据补零,输出数据丢帧。图5展示了该计算过程。
图5

5 因果网络计算过程示例

  
在输入数据的最前面补两帧零之后,网络的总体输入变为了7帧,经过卷积和转置卷积之后,网络的输出依然是7帧。但是label数据时长和补零前的输入数据时长一样,都是5帧。此时,为了计算loss函数,需要从输出的7帧数据中丢弃2帧数据。该丢弃过程在决定网络的因果性和感受野方面也起到了重要作用。

   从图5中的箭头走向可以分析出,为了得到因果网络,只能丢弃输出数据中的最后两帧;同样可以分析出,为了得到非因果网络,且使用未来一帧的输入信息,只能丢弃输出数据中的第一帧和最后一帧;为了得到非因果网络,且使用未来两帧的输入信息,只能丢弃输出数据中最前面的两帧。

4.2.2. 总结

  本节介绍了如何通过给输入数据补零和丢弃输出数据来达到控制网络因果性和感受野的目的。本节给出的示例只是在输入数据的最前面补零,读者可按照同样的方法分析一下给输入数据最后面补零,或者最前面和最后面都补零会达到什么样的效果。

4.3. 多层卷积(转置卷积)网络

  可使用3.2.小节中的分析方法,对多层卷积(转置卷积)网络的因果性和感受野进行分析。即,可以把多层卷积(转置卷积)网络当成一个整体,只在输入数据和输出数据上做相应的补零或丢帧操作。也可以把网络中的每一层当成一个单独的个体,在每一层上做相应的操作,通过控制每一层的因果性和感受野来控制整个网络的因果性和感受野。具体分析过程,此处不在赘述。。。

  如果想要使Encoder-Decoder架构的网络在实际推理的过程中输入一帧数据输出一帧数据,那么,同样也会遇到3.2.4.小节所提到的问题。不管训练的时候采用的是哪种补零(或者丢帧)方式,都需要动手实现网络的推理过程,并维护好每一层的输入输出buffer

4.4. 总结

  对于Encoder-Decoder架构的网络来说,为了控制网络的因果性和感受野,在实际coding的过程中,可以通过给输入数据补零,并且丢弃输出数据中的帧来实现。

5. 后记

  前前后后历时一周,终于抽时间把这篇很久之前就想写的博客写完了。笔者是想到哪写到哪,有的点可能也没考虑到,如果读者感觉哪块写的不清楚或者有不同的意见和建议,欢迎在评论区提出,大家一起探讨。

  “先把书读厚,再把书读薄”。再次感谢读者阅读到此处,和笔者一起经历了“先把书读厚”的过程。下面献上一张图片,大家一起“再把书读薄”。
图6
  这幅图是Ke Tan,DeLiang Wang的论文“A Convolutional Recurrent Neural Network for Real-time speech enhancement”中的图,笔者就是根据这幅图理解的因果卷积。从下往上看,这幅图可以理解为Encoder的计算过程,从上往下看,这幅图可以理解为Decoder的计算过程。只要能完全理解这幅图片,什么Encoder架构,Encoder-Decoder架构,什么输入层补零,逐层补零都不是问题,可以做到“一图走天下”。

  在CNN的基础上能构建的网络形式有很多,比如设置stride>1, dilation>1,比如网络中加入残差链接等。在coding的过程中,需要认真分析搭建的网络结构,以正确地控制网络的因果性和感受野。

  周末愉快!

猜你喜欢

转载自blog.csdn.net/wjrenxinlei/article/details/124200218