CNN模型近年发展非常迅猛,在多项视觉任务中都展现出极佳的性能。同时研究者们也一致认同CNN模型的模型体积巨大和模型计算性能低是比较显然的缺点,所以研究者们从2015年起慢慢地开始探索如何压缩加速CNN模型。之所以能够压缩加速模型,是因为CNN模型本身具有较大的参数冗余。既然有冗余,就可以压缩。既然有冗余,就可以加速。那么首先我们需要确定对于一个CNN模型,哪一个部分是容易压缩的,哪一个部分是容易加速的。
模型设计
首先,如何让一个CNN模型变小,第一个最简单的办法是,合理调超参数! 通过一定的可视化技术或者其他方法,让模型设计者能够掌握一个合理的模型参数,特别是神经元数量。如果模型设计得非常不合理,那花费更多的时间和精力去压缩加速实际上是杀鸡用牛刀。只是对于这个问题,有这么一篇有意思的论文,提出在模型的损失函数中加入惩罚项,包括模型的Density和模型的Diversity。
|
Density指的是模型参数的冗余度,就是零和极小值的多少;Diversity指的是参数的多样性,即如果参数能够聚类成少数几个类别,那么就是多样性低,反之就是多样性丰富。实际上论文的目的不是通过加入惩罚项直接训练一个很小的模型,而是通过这么一个惩罚,使得模型在训练时能够尽可能冗余,尽可能多样性低,这样在后续就可以更大程度地剪枝和量化编码。关于剪枝和量化编码在后面会有详细分析。
全连接层
当通过良好的设计得到一个合理的CNN模型后,实际上可以发现,参数比较多的是全连接层,因为全连接层参数的存储复杂度计算如下:
所以对于大类别,例如文字分类等任务,全连接层的参数数量就一言不合到达千万级别。然而,实际上在这书以千万计的参数中,有大量的参数值极小,例如处于区间。这样数值的神经元信息量极少,但是却和其他信息量大的神经元一般占据同样大小的储存空间和计算性能。所以,全连接层是当前压缩加速的重点关照区域。
剪枝(Network Pruning)
|
|
剪枝方法基本流程如下:
- 正常流程训练一个神经网络,以Caffe框架为例,就是普普通通地训练出一个caffemodel;
- 确定一个需要剪枝的层,一般为全连接层,设定一个剪裁阈值或者比例。实现上,通过修改代码加入一个与参数矩阵尺寸一致的mask矩阵。Mask矩阵中只有0和1,实际上是用于重新训练的网络。
- 重新训练、微调,参数在计算的时候先乘以该mask,则mask中位置为1的参数值将继续训练,通过反向传播进行更新,而mask中位置为0的部分因为输出始终是0,则不对后续部分产生影响。
- 输出模型参数储存的时候,因为有大量的稀疏,所以要重新定义储存的数据结构,仅储存非零值以及其矩阵位置。重新读取模型参数的时候,就可以还原矩阵。
特别地,由于计算稀疏矩阵在CPU和GPU上都有特定的方法,所以前向计算也需要对一些部分进行代码修改。GPU上计算稀疏需要调用cuSparse库,而CPU上计算稀疏需要mkl_sparse之类的库去优化稀疏矩阵的计算,否则达不到加速效果。
针对剪枝这个环节,有很多新论文继续进行了展开,包括如何自动设定剪枝率,如何自适应设定剪枝阈值,暂时不做关注。
量化(Quantization)
|
|
量化的思路非常简单。CNN参数中的数值分布在参数空间,通过一定的划分方法,总是可以划分为个类别。然后通过储存个类别的中心值或者映射值从而压缩网络的储存。
下面简单地举出个例子。
二值化(Binary)
将网络二值化的思路由来已久,因为二值化的参数不仅仅储存量低,计算也极快。例如将参数的正数设置为1,负数设置为-1(或者较大的设为1,较小的设为-1)。则四则运算可以转化为更简单的 and, or, xor等计算。但是论文已经指出,这个做法仅仅在MNIST实验中能够保持很好的识别率,设计到更复杂的任务,网络的性能大大降低。
聚类(Clustering)
聚类方法在量化思路中是更为合理的实现。首先将参数空间通过kmeans方法聚类,得到个聚类中心值,另开辟储存空间来储存这个中心值序列。而参数矩阵本身则仅仅储存对应类中心的储存序列下标(float -> int),所以压缩率就是。
例如下面这个矩阵:
设定类别数为,通过kmeans聚类。得到,
- A类中心:1.0,映射下标:1
- B类中心:6.5,映射下标:2
- C类中心:-0.95,映射下标:3
所以储存矩阵可以变换为:
当然,论文还提出需要对量化后的值进行重新训练,挽回一点点丢失的识别率,基本上所有的压缩方法都会带来损失,所以重新训练还是比较必要的。
编码(Encoding)
S. Han et al. Deep Compression: Compressing Deep Neural Networks with Pruning, Trained Quantization and Huffman Coding. ICLR, 2016 |
Huffman编码笔者已经不太记得了,好像就是高频值用更少的字符去储存,低频则用更多的字符去储存。
奇异值分解(SVD)
由于奇异值分解所得到的奇异值按照降序排列的话,前若干个奇异值的能量占据了整个矩阵的,甚至更多,意味着如果只保留前若干个奇异值,还原后的矩阵能够达到压缩效果。在CNN的全连接层中怎么应用SVD呢?
首先分析全连接层的权值结构。举个例子,全连接层FC1有1024个神经元,全连接层FC2有1000个神经元。这种结构常见于大类别分类任务。那么其矩阵计算量就是:
要把中间参数矩阵的体积降下来,在CNN中常用的手法就是两个全连接层加上中间层。设中间层的神经元数量是,FC1为,FC2为,则原本的参数数量是,加上中间层后就是。所以当的时候,参数量减少。
卷积层
比起全连接层的冗余,卷积层因为参数共享,冗余度不高,但是卷积计算的时间消耗却是网络前向计算的主要部分。
每一卷积层的计算复杂度如下:
每一个卷积层的参数储存复杂度如下:
所以在卷积层做文章的话,压缩的效果并不明显。一个方法是否能够较好地加速卷积计算,更显重要。
低秩分解(Low Rank Expansion)
矩阵的秩概念上就是线性独立的纵列(或者横列)的最大数目。行秩和列秩在线性代数中可以证明是相等的,例如:
一个的矩阵如下,则行秩列秩
的矩阵如下,则行秩列秩秩
的矩阵如下,则行秩列秩秩
所以,低秩分解这个名字很唬人,实际上就是把较大的卷积核分解为两个级联的行卷积核和列卷积核。常见的就是一个的卷积层,替换为一个的卷积核加上一个的卷积核。容易计算得到,一个特征图,经过卷积核后得到的特征图输出,而替换为低秩后,则先得到的特征图,然后再得到的特征图。
低秩分解实际应用时其实有一个比较隐蔽的限制,以下简单计算证明一下。
设有输入三通道32*32的图像输入,卷积层conv1有16个3*3的卷积核,conv2为16个3*3的卷积核。现希望对conv1进行低秩分解,问是否能优化计算和储存性能? |
将conv1分解为的conv1-dec1和的conv1-dec2,原本conv1的参数个数为。分解后的conv1-dec1的参数个数为,分解后的conv1-dec2的参数个数为。
** 咦?!
什么鬼?!怎么分解完参数数量量还变大了?**
若是分解conv2呢?是否能优化计算和储存性能? |
将conv2分解为的conv2-dec1和的conv2-dec2,原本的conv2参数个数是:个。分解后的conv2-dec1参数个数是:。分解后的conv2-dec2的参数个数是:。
为什么呢? |
- 储存量
对某一层的卷积层,卷积参数数量为:
分解后则是,
所以只有当 时,才有低秩分解能够优化储存性能的说法。
在上述例子中若希望分解conv1优化储存性能,
则有,
所以,即分解层的卷积核不超过7个。
- 计算量
对于某一卷积层,卷积计算量为:
分解后则是,
令,得到
一般实际应用中常用,配合padding=1的话,得到
假设上述例子中,希望分解conv1使得计算量减半,则,应设置D为
,约等于4,即设置分解层为4核卷积。
剪枝/量化/编码
与全连接层的剪枝类似,在卷积层也可以进行剪枝优化,使得数值接近于零的权值连接断开,从而降低储存量。但是由于卷积层的参数在稀疏性上不强,该优化效率有限。量化和编码的情况也和剪枝类似,可以做,但是作用有限。