CNN的压缩和加速

来源:互联网 发布:淘宝买零食吃靠谱吗 编辑:程序博客网 时间:2024/06/08 03:41

转自:http://hyichao.github.io/cv/2017/07/11/cnn-compression.html

新鲜入职,需要学习和接触更多新的东西。接下来的一段时间,研究的主题是卷积神经网络(CNN)的压缩和加速。由于以前只在同学交流中简单了解过这一领域的概况,故首先需要花一两周时间看看论文和相关资料,了解现阶段比较主流,比较有效的CNN压缩加速方法。

调研起点

从一篇实验室师弟的PR论文开始了解压缩方面的内容…(师弟太凶残,汗颜)

X. Xiao et al. Building fast and compact convolutional neural networks for offline handwritten Chinese character recognition. Pattern Recognition, 2017.

CNN模型近年发展非常迅猛,在多项视觉任务中都展现出极佳的性能。同时研究者们也一致认同CNN模型的模型体积巨大和模型计算性能低是比较显然的缺点,所以研究者们从2015年起慢慢地开始探索如何压缩加速CNN模型。之所以能够压缩加速模型,是因为CNN模型本身具有较大的参数冗余。既然有冗余,就可以压缩。既然有冗余,就可以加速。那么首先我们需要确定对于一个CNN模型,哪一个部分是容易压缩的,哪一个部分是容易加速的。

模型设计

首先,如何让一个CNN模型变小,第一个最简单的办法是,合理调超参数! 通过一定的可视化技术或者其他方法,让模型设计者能够掌握一个合理的模型参数,特别是神经元数量。如果模型设计得非常不合理,那花费更多的时间和精力去压缩加速实际上是杀鸡用牛刀。只是对于这个问题,有这么一篇有意思的论文,提出在模型的损失函数中加入惩罚项,包括模型的Density和模型的Diversity。

S. Wang et al. Training Compressed Fully-Connected Networks with a Density-Diversity Penalty. ICLR, 2017

Density指的是模型参数的冗余度,就是零和极小值的多少;Diversity指的是参数的多样性,即如果参数能够聚类成为少数几个类别,那么就是多样性低,反之就是多样性丰富。实际上论文的目的不是通过加入惩罚项直接训练一个很小的模型,而是通过这么一个惩罚,使得模型在训练时能够尽可能冗余,尽可能多样性低,这样在后续就可以更大程度低剪枝量化编码。关于剪枝量化编码,在后面会详细分析。

全连接层

当通过良好的设计得到一个合理的CNN模型后,实际上可以发现,参数比较多的是全连接层,因为全连接层参数的存储复杂度计算如下:
S = P(上一层神经元数量) * P(当前层神经元数量)
所以对于大类别,例如文字分类等任务,全连接层的参数数量就一言不合到达千万级别。然而,实际上在这书以千万计的参数中,有大量的参数值极小,例如处于(-0.0001,0.0001)区间。这样数值的神经元信息量极少,但是却和其他信息量大的神经元一般占据同样大小的储存空间和计算性能。所以,全连接层是当前压缩加速的重点关照区域。

剪枝(Network Pruning)

S. Han et al. Deep Compression: Compressing Deep Neural Networks with Pruning, Trained Quantization and Huffman Coding. ICLR, 2016

Y. Sun et al. Sparsifying Neural Network Connections for Face Recognition, CVPR, 2016.

剪枝方法基本流程如下:

    1. 正常流程训练一个神经网络。以CAFFE为例,就是普普通通地训练出一个caffemodel。
    1. 确定一个需要剪枝的层,一般为全连接层,设定一个裁剪阈值或者比例。实现上,通过修改代码加入一个与参数矩阵尺寸一致的mask矩阵。mask矩阵中只有0和1,实际上是用于重新训练的网络。
    1. 重新训练微调,参数在计算的时候先乘以该mask,则mask位为1的参数值将继续训练通过BP调整,而mask位为0的部分因为输出始终为0则不对后续部分产生影响。
    1. 输出模型参数储存的时候,因为有大量的稀疏,所以需要重新定义储存的数据结构,仅储存非零值以及其矩阵位置。重新读取模型参数的时候,就可以还原矩阵。

特别地,由于计算稀疏矩阵在CPU和GPU上都有特定的方法,所以前向计算也需要对一些部分进行代码修改。GPU上计算稀疏需要调用cuSPARSE库,而CPU上计算稀疏需要mkl_sparse之类的库去优化稀疏矩阵的计算,否则达不到加速效果

针对剪枝这个环节,有很多新论文继续进行了展开,包括如何自动设定剪枝率,如何自适应设定剪枝阈值,之类之类,暂时不做关注。

量化(Quantization)

S. Han et al. Deep Compression: Compressing Deep Neural Networks with Pruning, Trained Quantization and Huffman Coding. ICLR, 2016

Y. Gong et al. Compressing Deep Convolutional Networks using Vector Quantization, ICLR(?), 2015.

量化的思想非常简单。CNN参数中数值分布在参数空间,通过一定的划分方法,总是可以划分称为k个类别。然后通过储存这k个类别的中心值或者映射值从而压缩网络的储存。

下面简单举出两个例子。

二值化(Binary)

将网络二值化的思路由来以久,因为二值化的参数不仅仅储存量低,计算也极快。例如将参数中的正数置为+1,负数置为-1(或者较大的50%置为+1,较小的50%置为-1)则四则运算可以转化为更简单的“于”“或”“非”“异或”等计算。但是论文已经指出,这个做法仅仅在mnist实验中能够保持较好的识别率,涉及到更加复杂的任务,网络的性能大大降低。

聚类(Clustering)

聚类方法在量化思路中是更为合理的实现。首先将参数空间通过kmeans方法聚类,得到k个聚类中心值,另辟储存空间存储这k个中心值序列。而参数矩阵本身则仅仅储存对应类中心的储存序列下标(float==>int),所以压缩率就是sizeof(int)/sizeof(float)

例如下面这个矩阵。

1.2  1.3  6.10.9  0.7  6.9-1.0 -0.9 1.0

设定类别数k=3,通过kmeans聚类。得到,

A类中心: 1.0 , 映射下标: 1B类中心: 6.5 , 映射下标: 2C类中心: -0.95 , 映射下标: 3

所以储存矩阵可以变换为:

1  1  21  1  23  3  1

当然,论文还提出需要对量化后的值进行重训练,挽回一点丢失的识别率 :) 基本上所有压缩方法都有损,所以重训练还是比较必要的。

编码(Encoding)

S. Han et al. Deep Compression: Compressing Deep Neural Networks with Pruning, Trained Quantization and Huffman Coding. ICLR, 2016

Huffman编码笔者已经不太记得了,好像就是高频值用更少的字符储存,低频则用更多。既然不懂,就不装懂了…

奇异值分解(SVD)

全连接层的SVD,原理参见本链接或者其他相关资料。由于奇异值分解所得到的奇异值按照降序排列的话,前若干个奇异值的能量占据了整个矩阵的90%甚至更多,意味着如果只保留前若干个奇异值,还原后的矩阵能够达到压缩效果。在CNN的全连接层中怎么应用SVD呢?

首先分析全连接层的权值结构。举个例子,全连接层FC1有1024个神经元,全连接层FC2有1000个神经元。这种结构常见于大类别分类任务。那么,其矩阵计算就是

[ 矩阵FC1: 1x1024 ] * [ 参数矩阵: 1024x1000 ] = [ 矩阵FC2: 1x1000 ]

要把中间参数矩阵的体积降下来,在CNN中常用的手法就是两个全连接层中加上中间层。 设中间层的神经元数量为k,FC1为m,FC2为n,则原本的参数数量为(m*n),加上中间层后就是(m*k+k*n)=k*(m+n)。所以当k<(m*n)/(m+n)的时候,参数量减少。

卷积层

比起全连接层的冗余,卷积层因为参数共享,冗余度并不高,但是卷积计算的时间消耗却是网络前向计算中的主要部分。
每一卷积层的计算复杂度如下:
T = C1(上一层通道数量)* Kh(卷积核高度)* Kw(卷积核宽度)* H(特征图高度)* W(特征图宽度)* Kn(卷积核数量)
每一个卷积层的参数储存复杂度如下: S = C1(上一层通道数量)* Kh(卷积核高度)* Kw(卷积核宽度)* Kn(卷积核数量)

所以在卷积层做文章的话,压缩而言效果并不明显。一个方法是否能够较好地加速卷积计算,更显重要。

低秩分解(Low Rank Expansion)

矩阵的秩概念上就是线性独立的纵列(或者横列)的最大数目。行秩和列秩在线性代数中可以证明是相等的,例如:

3*3的矩阵如下,则 行秩==列秩==秩==31 2 34 5 67 8 91*3的矩阵如下,则 行秩==列址==秩==1[1 2 3] 3*1的矩阵如下,则 行秩==列址==秩==1[1] [2] [3] 

所以,低秩分解,这个名字虽然唬人,实际上就是把较大的卷积核分解为两个级联的行卷积核和列卷积核。常见的就是一个3*3的卷积层,替换为一个3*1的卷积层加上一个1*3的卷积核。容易计算得,一个特征图10*10,经过3*3卷积核后得到8*8的特征图输出,而替换为低秩后,则先得到10*8的特征图然后再得到8*8的特征图。

低秩分解实际应用时其实有一个比较隐蔽的限制,以下简单计算证明一下。

设有输入三通道32*32的图像输入,卷积层conv1有16个3*3的卷积核,conv2为16个3*3的卷积核。现希望对conv1进行低秩分解,问是否能优化计算和储存性能?

将conv1分解为1*3的conv1-dec1和3*1的conv1-dec2 原本conv1参数个数为: P = 3 * (3 * 3 * 16) = 432 分解后的conv1-dec1参数个数为: Pd1 = 3 * (1 * 3 * 16) = 144 分解后的conv1-dec2参数个数为: Pd2 = 16 * (3 * 1 * 16) = 768 

咦?!
什么鬼?!怎么分解完参数数量量还变大了?

若是分解conv2呢?是否能优化计算和储存性能?

将conv2分解为1*3的conv2-dec1和3*1的conv2-dec2 原本的conv2参数个数为: P = 16 * (3 * 3 * 16) = 2304 分解后的conv2-dec1参数个数为: Pd1 = 16 * (1 * 3 * 16) = 768 分解后的conv2-dec2参数个数为: Pd2 = 16 * (3 * 1 * 16) = 768 

为什么… :(

--- <储存量> ---对某一层卷积层,卷积参数数量为:P = C(上一层卷积数量) * Kh(卷积核高) * Kw(卷积核宽) * N(卷积核数量)分解后则是P = P1+P2 = C(上一层卷积数量) * Kh(卷积核高) * D(分解核数量) + D(分解核数量) * Kw(卷积核宽) * N(卷积核数量)所以只有当P1 + P2 < P  才有低秩分解能够优化储存性能的说法。在上述例子中若希望分解conv1优化储存性能,则有 D * ( 3 * 3 + 16 * 3) < 3 * 3 * 3 * 16所以 D < 7.58  即分解层的卷积核不超过7个--- <计算量> ---对于某一卷积层,卷积计算量为:     T = C(上一层卷积数量) * Kh(卷积核高) * Kw(卷积核宽) * N(卷积核数量) * Ho(输出特征图高度) * Wo(输出特征图宽度)分解后则是     T1 = C(上一层卷积数量) * Kh(卷积核高) * D(分解层卷积核数量) * Ho(输出特征图高度) * W(原特征图宽度)     T2 = D(分解层卷积核数量) * Kw(卷积核宽) * N(原卷积核数量) * Ho(输出特征图高度) * Wo(输出特征图宽度)   令    T1+T2 < T, 则计算量减少设    r = T/(T1+T2), 得   r = C * Kh * Kw * N * Wo / (C * Kh * D * W + D * Kw * N * Wo )一般实际应用中常用  Kh=Kw=3, 配合padding=1的话 则得   r = 3 * C * N / (C * D + D * N )   假设上述例子中,希望分解conv1使得计算量减半,则r=2,应设置D为   D = 3 * 3 *16 / (3 * 2 + 16 * 2) = 3.7 约等于 4 即设置分解层为4核卷积      

剪枝(Pruning)/量化(Quantization)/编码(Encoding)

与全连接层的剪枝类似,在卷积层也可以进行剪枝优化,使得数值接近于零的权值连接断开,从而降低储存量。但是由于卷积层的参数在稀疏性上不强,该优化效率有限。量化和编码的情况也和剪枝类似,可以做,但是作用有限。

小结

本文是笔者一天多时间通过阅读相关论文和其他资料根据个人理解所整理的CNN压缩加速常见方法。若有误请联系笔者。考虑到时间有限,很多方法的细节没有讲清楚,可能还有一些逻辑错误,以后翻修时一点点修正。


原创粉丝点击