加速卷积神经网络训练而不会对精度产生重大影响的方法
全连接层是神经网络大内存占用的主要原因,但是速度很快,而卷积虽然参数数量紧凑,但却消耗了大部分计算能力。事实上,卷积计算器非常昂贵,所以它们是我们需要这么多计算能力来训练和运行最先进的神经网络的主要原因。
有一些方法可以加速卷积,而不会严重降低模型的准确性。
卷积的核分解(Factorization/Decomposition of convolution’s kernels)
瓶颈层(Bottleneck Layers)
更宽的卷积(Wider Convolutions)
深度可分卷积(Depthwise Separable Convolutions)
简单分解
我们从NumPy中的以下示例开始
>>> from numpy.random import random
>>> random((3, 3)).shape == (random((3, 1)) * random((1, 3))).shape
>>> True
你可能会问,为什么我会向你展示这个愚蠢的片段?那么答案就是,它表明你可以写一个N×N矩阵,将卷积核看作是2个较小的矩阵/内核的乘积,其形状为Nx1和1xN。回想一下,卷积操作需要in_channels * n * n * out_channels 参数或权重。此外,请记住,每个权重/参数都需要激活。所以,减少参数的数量将减少所需的操作次数和计算成本。
考虑到卷积运算实际上是用张量乘法完成的,而张量乘法是多项式依赖于张量的大小,正确应用的因式分解应该产生有形的加速。
Keras看起来像这样:
# k - kernel size, for example 3, 5, 7...
# n_filters - number of filters/channels
# Note that you shouldn't apply any activation
# or normalization between these 2 layers
fact_conv1 = Conv(n_filters, (1, k))(inp)
fact_conv1 = Conv(n_filters, (k, 1))(fact_conv1)
不过,请注意,不建议将最接近输入卷积层的因素考虑在内。此外,分解3x3的卷积甚至会破坏网络的性能。最好将它们保存在更大的内核中。
在我们深入探讨这个话题之前,有一种更稳定的方式来分解大内核:代之以将更小的内核堆叠起来。例如,不是使用5x5卷积,而是使用两个3x3卷积,或者如果要替换7x7内核,则使用3个。
瓶颈层
瓶颈层背后的主要思想是通过减少输入通道的数量(也就是输入张量的深度)来减小内核大于1x1的卷积层中输入张量的大小。
这是它的Keras代码:
从keras.layers导入Conv2D
from keras.layers import Conv2D
# given that conv1 has shape (None, N, N, 128)
conv2 = Conv2D(96, (1, 1), ...)(conv1) # squeeze
conv3 = Conv2D(96, (3, 3), ...)(conv2) # map
conv4 = Conv2D(128, (1, 1), ...)(conv3) # expand
几乎所有的CNN,从革命性的InceptionV1到现代的DenseNet,都以不同的方式使用瓶颈层。这种技术有助于保持参数的数量,从而降低计算成本。
更宽的卷积
加速卷积的另一个简单方法是所谓的宽卷积层。你看,你的模型有越多的卷积层,它会变得越慢。然而,你需要大量卷积的表现力。你做什么?您使用的是less-but-fatter层,其中fat意味着每层更多的内核。它为什么有效?因为GPU或其他大规模并行机器对于处理单个大块数据而不是很多小数据很容易。
# convert from
conv = Conv2D(96, (3, 3), ...)(conv)
conv = Conv2D(96, (3, 3), ...)(conv)
# to
conv = Conv2D(128, (3, 3), ...)(conv)
# roughly, take the sqrt of the number of layers you want
# to merge and multipy the number to
# the number of filters/channels in the initial convolutions
# to get the number of filters/channels in the new layer
深度可分卷积
在深入研究这种方法之前,请注意,它非常依赖于在给定框架中实现的可分离卷积。就我而言,TensorFlow可能会对此方法进行一些特定的优化,而对于其他后端,如Caffe,CNTK或PyTorch,则尚不清楚。
这个想法是,不是在图像的所有通道上共同进行卷积运算,而是在每个通道上以深度运行单独的2D卷积channel_multiplier。in_channels * channel_multiplier中间通道得到串接在一起,并映射到out_channels使用1x1卷积。 这样一来,训练的参数就会显着减少。
# in Keras
from keras.layers import SeparableConv2D
...
net = SeparableConv2D(32, (3, 3))(net)
...
# it's almost 1:1 similar to the simple Keras Conv2D layer
请注意,可分离的卷积有时不会受到训练。在这种情况下,请将深度乘数从1修改为4或8.还要注意,这些对于小型数据集(如CIFAR 10,而且在MNIST上)并不那么有效。另外要记住的是,不要在网络的早期阶段使用可分离的卷积。
CP分解和高级方法
上面显示的分解方案在实践中运行良好,但相当简单。有许多作品,其中包括V. Lebedev等的作品。它们向我们展示了不同的张量分解方案,这大大减少了参数的数量,从而减少了所需的计算次数。
下面是如何在Keras中进行CP分解的代码片段:
# **kwargs - anything valid for Keras layers,
# like regularization, or activation function
# Though, add at your own risk
# Take a look into how ExpandDimension and SqueezeDimension
# are implemented in the associated Colab Notebook
# at the end of the article
first = Conv2D(rank, kernel_size=(1, 1), **kwargs)(inp)
expanded = ExpandDimension(axis=1)(first)
mid1 = Conv3D(rank, kernel_size=(d, 1, 1), **kwargs)(exapanded)
mid2 = Conv3D(rank, kernel_size=(1, d, 1), **kwargs)(mid1)
squeezed = SqueezeDimension(axis=1)(mid2)
last = Conv2D(out, kernel_size=(1, 1), **kwargs)(squeezed)
应该注意TensorTrain分解和Tucker等方案。对于PyTorch和NumPy,有一个名为Tensorly的伟大库,可为您执行所有低级别实现。