卷积神经网络优化算法
| 知乎 |
引言
看到这个标题,很多朋友肯定安耐不住要说「不是吧,又来写这种陈词滥调被人写了几万遍的主题?」,还要附带狗头。我也很无奈啊,想码字奈何没硬货,只能东摘西抄了。不过呢,本文还是和其他相同主题有不同的内容,相信能给大家一点收获~
卷积(Convolution)是神经网络的核心计算之一,它在计算机视觉方面的突破性进展引领了深度学习的热潮。卷积的变种丰富,计算复杂,神经网络运行时大部分时间都耗费在计算卷积,网络模型的发展在不断增加网络的深度,因此优化卷积计算就显得尤为重要。
随着技术的发展,研究人员提出了多种优化算法,包括 Im2col、Winograd 等等。本文首先定义卷积神经网络的概念,继而简要介绍几种常见的优化方法,并讨论作者在该领域的一些经验。
卷积神经网络的概念
卷积神经网络(Convolution Neural Networks, CNN)的概念拓展自信号处理领域的卷积。信号处理的卷积定义为
由于对称性,
图一:信号处理中的卷积
公式1的离散形式为
将该卷积拓展到二维空间即可得到神经网络中的卷积,可简写为
其中
当应用到计算机视觉中处理图片时,图片的通道(Channel)可以对二维卷积简单堆叠,即
其中
很多时候,公式描述显得不是很直观,图二是堆叠的二维卷积的可视化。其中,与输入、输出、卷积核相关的标记带有前缀 I
、O
、K
。此外,本文图例对输出、输入、卷积核三者的着色一般为:橙色、黄色、绿色。
图二:卷积计算定义
当中张量的内存布局为 NHWC 时,卷积计算相应的伪代码如下。其中外三层循环遍历输出
for (int oh = 0; oh < OH; oh++) {
for (int ow = 0; ow < OW; ow++) {
for (int oc = 0; oc < OC; oc++) {
C[oh][ow][oc] = 0;
for (int kh = 0; kh < KH, kh++){
for (int kw = 0; kw < KW, kw++){
for (int ic = 0; ic < IC, ic++){
C[oh][ow][oc] += A[oh+kh][ow+kw][ic] * B[kh][kw][ic];
}
}
}
}
}
}
和矩阵乘的优化方法类似,我们也可针对该计算进行向量化、并行化、循环展开的基本的优化操作。卷积的问题在于其
Im2col 优化算法
作为早期的深度学习框架,Caffe 中卷积的实现采用的是基于 im2col 的方法,至今仍是卷积重要的优化方法之一。
Im2col 是计算机视觉领域中将图片转换成矩阵的矩阵列(column)的计算过程。从上一节的介绍中可以看到,二维卷积的计算比较复杂不易优化,因此在深度学习框架发展的早期,Caffe 使用 Im2col 方法将三维张量转换为二维矩阵,从而充分利用已经优化好的 GEMM 库来为各个平台加速卷积计算。最后,再将矩阵乘得到的二维矩阵结果使用 Col2im 将转换为三维矩阵输出。
算法过程
除非特别说明,本文默认采用的内存布局形式为 NHWC 。其他的内存布局和具体的转换后的矩阵形状或许略有差异,但不影响算法本身的描述。
图三:Im2col 算法计算卷积的过程
图三是使用 Im2col 算法计算卷积的过程示例,具体的过程包括(简单起见忽略 Padding 的情况,即认为
- 将输入由
根据卷积的计算特性展开成 形状的二维矩阵。显然,转换后使用的内存空间相比原始输入多约 倍。 - 权重的形状一般为
四维张量,可以将其直接作为形状为 的二维矩阵处理。 - 对于准备好的两个二维矩阵,将
作为累加求和的维度,运行矩阵乘可以得到输出矩阵 。这一过程可以直接使用各种优化过的 BLAS(Basic Linear Algebra Subprograms)库来完成。 - 输出矩阵
在内存布局视角即为预期的输出张量 。
Im2col 计算卷积使用 GEMM 的代价是额外的内存开销。这是因为原始卷积计算中,卷积核在输入上滑动以计算输出时,相邻的输出计算在空间上复用了一定的输入输出。而用 Im2col 将三维张量展开成二维矩阵时,这些原本可以复用的数据平坦地分布到矩阵中,将输入数据复制了
当卷积核尺寸
内存布局与卷积性能
神经网络中卷积的内存布局主要有 NCHW 和 NHWC 两种,不同的内存布局会影响计算运行时访问存储器的模式,特别是在运行矩阵乘时。本小节分析采用 Im2col 优化算法时计算性能性能和内存布局的关系。
在完成 Im2col 转换后,得到用于运行矩阵乘的输入矩阵和卷积核矩阵。对计算过程施加矩阵计算中常用的数据划分、向量化等优化方法(相关定义请参考通用矩阵乘(GEMM)优化算法)。下面着重分析在这种场景下,不同内存布局对性能的影响。
首先考虑 NCHW 内存布局,将 NCHW 内存布局的卷积对应到矩阵乘
图四:NCHW 内存布局卷积转换成的矩阵乘
对该矩阵施行划分后,我们详细分析局部性的表现,并标记在图四中。其中 Inside 表示
- 对输出而言,小块内访存局部性较差,这是因为每次向量化加载会加载四个元素,每次加载都会发生缓存缺失(Cache miss)。外部表现取决于全局计算方向——行优先则局部性较好,列优先则较差。输出的行为不是这里的讨论终点,毕竟它没有访存复用。
- 对卷积核而言,小块内访存局部性较差,这和输出类似。当小块加载发生缓存缺失时,处理器会一次性加载一个缓存行(Cache line),这使得后续的若干个小块访存都能缓存命中(Cache hit),直到该缓存行全部命中后进入下一个缓存行的范围。因此小块外部局部性较好。
- 对输入而言,小块内访存局部性表现同样不好。然而不同于卷积核,小块中因缓存缺失加载到缓存中的缓存行数据只会被使用一次,因为这种内存布局中下一个小块的地址范围一般超出了一个缓存行。因此输入的几乎每次内存加载都会发生高速缓存缺失——Cache 没有起到加速的作用,每次访存都需要到内存取数据。
因此,用 Im2col 处理卷积计算时,NCHW 布局对内存很不友好。
图五是与之相对的 NHWC 内存布局的示例。值得注意的是,NHWC 和 NCHW 中
图五:NHWC 内存布局卷积转换成的矩阵乘
类似地,分析三个张量的访存表现可知:
- 对输出而言,NHWC 和 NCHW 表现一样。
- 对输入而言,小方块的内部局部性表现不是很好,因为几次向量加载都会发生缓存不命中;而外部局部性表现则较好,因为在削减维度滑动使用的内存是连续的。这种表现和 NCHW 中卷积核的表现一样,整体来看都是对高速缓存比较友好的内存布局。
- 对卷积核而言,NHWC 的情况和 NCHW 中输入的情况类似,小块内和小块外的局部性都较差。
两种内存布局中的卷积核缓存表现并不是问题,因为卷积核在运行期间保持不变,可以在模型加载阶段转换卷积核的内存布局,使其在小块外的内存对缓存友好(例如将
因此,当使用 Im2col 方法计算时,整体的访存表现取决于输入的情况,即 NHWC 的内存布局要比 NCHW 内存布局更加友好。我们在实践过程中的一个实验表明,对于一个
空间组合优化算法
Im2col 是一种比较朴素的卷积优化算法,在没有精心处理的情况下会带来较大的内存开销。空间组合(Spatial pack)是一种类似矩阵乘中重组内存的优化算法。
图六:空间组合优化算法对计算的划分
空间组合优化算法是一种基于分治法(Divide and Conquer)的方法——它基于空间特性将卷积计算划分为若干份,分别处理。图六所示是在空间上将输出、输入划分为四份。
划分后,一个大的卷积计算被拆分为若干个小的卷积计算。虽然在划分的过程中计算总量不变,但计算小矩阵时访存局部性更好,可以借由计算机存储层次结构获得性能提升。这通过图七中的步骤来完成。该步骤和上节中 Im2col 重组内存的过程类似:
- 在 H 和 W 维度划分,将形状为
的输入张量拆分为 个(两个方向分别拆分 和 次)形状为 的张量,分别将这些小的张量组织为连续内存; - 将得到的
个输入张量分别和卷积核做二维卷积操作,即可得到 个形状为 的输出张量; - 将这些输出张量重组内存布局得到最终形状为
的输出。
图七:空间组合计算的步骤
值得注意的是,方便起见,上文的描述中忽略了 Padding 的问题。实际在步骤 1 中将输入张量划分为若干个小张量时,除了将划分的小块中原始数据拷贝外,还需要将相邻的小张量的边界数据拷贝。具体而言,如图八所示,空间拆分得到的小张量的形状实际上是:
这里的 VALID
时,它们可以忽略;规则为 SAME
时,位于源张量边界的一边 Padding 补 0
,不在源张量边界的 Padding 则使用邻居张量的值。只要考虑一下卷积的计算原理,这是显而易见的。
图八:空间组合算法的划分细节
上面的三个示例图都是拆分为 4 份的情况,实际应用中可以拆为很多份。例如可以拆成小张量边长为 4 或者 8 ,从而方便编译器向量化计算操作。随着拆分出的张量越小,其局部性也越高,负面作用是消耗的额外内存也越多。这些额外内存是由于 Padding 引入的。当拆分为
可以看到,随着拆分的粒度越小,额外消耗的内存越大。值得注意的是,当拆分到最细粒度时,即将在形状为
只做空间划分时,划分与卷积核无关。而如果在输出的通道维度划分,卷积核也可做相应的拆分。通道维度的划分相当于固定空间划分后简单的堆叠,不会对影响内存消耗,但会影响局部性。对于不同规模的卷积,寻找合适的划分方法不是一件容易的事情。正如计算机领域的许多问题一样,该问题也是可以自动化的,例如 AutoTVM 可以在这种情况下寻找较优的划分方法。
Winograd 优化算法
前两节介绍的两种算法,Im2col 在将三维张量组织成矩阵后调用 GEMM 计算库,这些计算库很大程度上使用一些基于访存局部性的优化;空间组合优化则本身就是利用局部性优化的方法。本小节介绍的 Winograd 优化算法则是矩阵乘优化方法中 Coppersmith–Winograd 算法的一种应用,是基于算法分析的方法。
Andrew Lavin 和 Scott Gray 在 Fast Algorithms for Convolutional Neural Networks 首次介绍了这种方法。该方法现已广泛应用于各种深度学习框架中。Winograd 算法在卷积优化中的应用的基本方法和矩阵乘中应用类似,通过技巧性的矩阵计算变换,减少计算过程所需的乘法数量。下面简要介绍一下基本原理(全部摘自原文)。
Winograd 已经证明了对于卷积核长度为
其中
将其拓展到卷积核大小为
与之对应的计算为
这是计算单个小局部二维卷积的方法。自然地,我们不能将其直接应用在卷积网络的计算中,否则产生的辅助矩阵规模太大,会影响实际的效果。另一方面,不同规模的卷积需要不同规模的辅助矩阵,实时计算出这些矩阵也不现实。实践中采用的方法是,将卷积计算用空间组合优化算法中的拆分方法,将输入拆分成若干个小规模卷积。例如拆分成每个小卷积输出
基于这种思想继续推导,令
令小卷积的编号为
取出求和部分,令
将按元素的操作标记为
此即为矩阵乘
而该矩阵乘可高效地求得。
在实践中,普遍应用的方法是将一切能固定下来的数据在网络运行前固定。例如,当设计好一个基于 Winograd 算法时,对于特定网络结构的
间接卷积优化算法
Marat Dukhan 在 QNNPACK(Quantized Neural Network PACKage)中推出了间接卷积算法(The Indirect Convolution Algorithm),似乎到目前为止(2019 年中)依然是所有已公开方法中最快的。最近作者发表了相关的文章来介绍其中的主要思想。
虽然该算法在 QNNPACK 中的实现主要用于量化神经网络(业界的其他量化优化方案都比较传统 TensorFlow Lite 使用 Im2col 优化算法、腾讯出品的 NCNN使用 Winograd 优化算法;OpenAI 出品的 Tengine 使用 Im2col 优化算法),但其是一种同样的优化算法设计。
本文写作时设计文章尚未公开,而理解该算法设计很多细节内容,最好能结合代码理解。我的 QNNPACK fork 包含一个 explained
分支,其中对增加了对部分代码的注释,可作参考。
计算工作流
间接卷积算法的有效工作以来一个关键的前提——网络连续运行时,输入张量的内存地址保持不变。这一特性其实比较容易满足,即使地址真的需要变化,也可以将其拷贝到固定的内存区域中。
图九:间接卷积算法工作流
图九是间接卷积算法工作流的详细过程。最左侧部分表示多个输入使用相同的输入缓冲区(Input Buffer)。间接卷积算法会在该输入缓冲区基础上构建如图十的「间接缓冲区」(Indirect Buffer),而间接缓冲区是间接卷积算法的核心。如图中右侧,在网络运行时,每次计算出
在实现中,软件的执行过程分为两部分:
- 准备阶段:加载模型,配置输入缓冲区;重排权重,使其内存布局适用于后续计算;
- 运行阶段:对于每个输入,运行
次核心循环,每次使用 GEMM 方法计算出 大小的输出。
图十:间接缓冲区
如相关章节的讨论,Im2col 优化算法存在两个问题,第一是占用大量的额外内存,第二是需要对输入进行额外的数据拷贝。这两点如何才能解决呢?间接卷积算法给出的答案是间接缓冲区(Indirect Buffer),如图十右半所示。
图十是常规的 Im2col 优化算法和间接卷积优化算法的对比。正如相关小节介绍的那样,Im2col 优化算法首先将输入拷贝到一个矩阵中,如图十中 Input 的相关箭头,然后执行矩阵乘操作。间接卷积优化算法使用的间接缓冲区中存储的其实是指向输入的指针(这也是间接卷积优化算法要求输入内存地址固定的原因),在运行时根据这些指针即可模拟出类似于 Im2col 的矩阵计算过程。
间接缓冲区布局
间接缓冲区可以理解为是一组卷积核大小的缓冲区,共有
图十一绘制了当
当计算
图十一:间接缓冲区详解
图中将平面缓冲区绘制成三维形式(增加 IC 维度),意在说明间接缓冲区的每个指针可索引 IC 个输入元素,而每个间接缓冲区索引的内容即为与权重对应的输入内存区域。
进一步地,左上角的输入缓冲区排列方式并不是最终的排布方法,实际上这些指针会被处理成图十一中部间接缓冲区的形式。将左上每个缓冲区中的指针打散,即可得到
使用间接缓冲区计算
我们已经知道了间接缓冲区的组织形式,以及其指针对应于输入内存的地址趋于,现在来研究在计算过程中如何使用这些缓冲区。
和上一小节一样,本小节的讨论大体都在计算
而卷积之所以可以使用 Im2col 优化算法,本质原因在于其拆解后忽略内存复用后的计算过程等价于矩阵乘。而间接缓冲区使得可以通过指针模拟出对输入的访存。在实际运行计算 p_indirect_buffer
)中获取。
这个过程的逻辑不易理解,这里再做一点补充说明。当上述的
这样一来,只要运行该计算核心
间接卷积优化算法解决了卷积计算的三个问题,第一是空间向量化问题,第二是地址计算复杂问题,第三是内存拷贝问题。一般计算卷积时都需要对输入补零(对于
讨论、总结与展望
至此,本文探讨了一些已经发表或开源的卷积神经网络的优化方法。这些优化方法或多或少地推动了深度学习技术在云端或移动端的应用,帮助了我们的日常生活。例如,最近上海人民乃至全中国人们头疼的垃圾分类问题,也可以利用深度学习方法来帮助人们了解如何分类。
从本文的集中优化方法中可以看到,卷积神级网络的优化算法依然可以囊括在基于算法分析的方法和基于软件优化的方法。实际上,在现代计算机系统中,基于软件优化的方法,特别是基于计算机存储层次结构的优化显得更为重要,因为其方法更容易挖掘和应用。另一方面也是因为现代计算机新增的专用指令已经可以抹除不同计算的耗时差异。在这种场景下,基于算法分析的方法要和基于软件优化的方法结合或许能取得更优的优化效果。
在实践中,我们参考 QNNPACK 计算矩阵乘的计算核,采用 Im2col 算法和简单的对输入输出内存布局重组、以及调整计算顺序等方法,获得了远超 QNNPACK 的性能。在树莓派3B+上的测试结果表明,我们的方法在计算量化卷积(忽略了深度卷积)时,四核 MobileNetV2 的计算性能(只含普通卷积)是 QNNPACK 的 2.14X。MobileNetV2 四核全网络运行性能是 TFLite(由 TensorFlow 1.14 编译)的 2.08X。
我们的全网络数据没有和 QNNPACK 对比主要是 QNNPACK 是一个加速库,只包含卷积这类操作,无法直接运行全网(也无心再使用 Caffe2 挂接 QNNPACK 运行)。另外,相比于卷积对 QNNPACK 的优势,我们全网对 TFLite 的优势并不没有特别突出(考虑到 QNNPACK 已经比 TFLite 快很多),这主要是因为其他算子优化不够,卷积的加速比被平均了。
有趣的是,我们开始优化工作时并没有计划超过 QNNPACK,毕竟我们的方法比 QNNPACK 多了大量的内存拷贝(重排输入输出),也占用了更多的内存(基于 Im2col 优化算法),计算核对比 QNNPACK 却没有优势。而优化的效果是如此的出人意料,足以让我们深思在现代计算机系统中,基于软件优化方法是不是还有更大的可挖掘空间。
最后,本文讨论的优化方法都是毕竟通用的方法,而随着神经网络处理器(如寒武纪 MLU、Google TPU)的发展,以及其他通用计算处理器的拓展(如点积相关的指令:Nvidia GPU DP4A、Intel AVX-512 VNNI、ARM SDOT/UDOT ),深度学习的优化或许还值得继续投入资源。
本文写作过程中参考了以下(包括但不限于)资料:


