本次实践一共在两个网络上进行实验,一个是基于VGG-backbone的CRNN模型,一个是基于DenseNet-backbone的文字识别模型。
结论先行:
其中在VGG的网络上,net-slimming可以发挥作用,第一轮剪枝20%效果很好,在真实的测试场景中没有精度的降低,且时间减少明显;但是迭代剪枝20%到第二轮效果就开始变差,识别的文字有1%精度的降低。
在DenseNet的剪枝上,net-slimming效果较差,至少对于文字识别任务来说,总的参数量和时间没有明显降低。
零、network-slimming剪枝的原理
来自《Learning Efficient Convolutional Networks through Network Slimming》这篇2017年ICCV的剪枝论文,这篇文章很巧妙的借用网络中的BN层实现通道剪枝。
主要的原理是在损失函数中添加针对BN层的Lasso正则化LASSO会使得BN层的参数尽可能稀疏化,不重要的参数尽可能的趋向于0。这种过程在传统的机器学习 ,可以用来进行参数选择,那么在BN层也可以进行通道选择。
BN层在神经网络学习 的引入可以保证每层输入都是正太同分布的,避免层间的方差偏移;主要是为了增强泛化能力。
但是从另一个角度来说,BN层后面的激活函数,激活函数之后是新的conv ,如果在BN 层的参数γ比较小,那么输入到下一层卷积层的输入也就比较小,相当于没有什么重要作用;那么这些输入或许可以被剪枝。
基于这种思想,作者设计了稀疏化 损失函数
损失函数的前面一半是正常 损失函数,后面一半是针对BN层的正则化;其中γ就是所有BN层的scaling factor,λ 平衡损失函数和正则化的系数,一般取0.00001或者更小。
剪枝的过程如上图所示,首先是卷积层,然后是BN层,如果BN层的 参数小于阈值,则:
- 剪掉前面卷积层对应输出位置的通道;
- 剪掉BN层的通道数量,保留较大的通道;
- 减掉BN层后面的卷积层的对应输入通道
一、基于VGG-backbone的CRNN模型剪枝
1.1 CRNN模型简述
crnn模型是ocr的入门模型了,在github上https://github.com/bgshih/crnn可以找到具体的模型代码。简单的模型就是一个VGG-backbone级联两层BiLSTM实现。
本次实验采用了多种训练和剪枝方式。训练方式包括从头开始训练以及直接从剪枝之后的参数开始训练。剪枝方式包括逐层等比例剪枝以及全局剪枝。在这里主要是BackBone,也就是LSTM之前的所有层进行剪枝。因为VGG是简单的级联网络,所以剪枝直接按照network-slimming开源代码进行相应的修改即可实现。
这里对VGG的网络结构不做过多的展开。
1.2剪枝效果分析
全局剪枝是对模型所有的BN层参数排序之后,集中根据比率R计算阈值,然后逐层进行删除。
等比例剪枝是对每个BN层分别排序,然后每层剪掉指定比率R的参数。
全局剪枝预先并不知道剪枝之后每层网络的通道数量;等比例剪枝则预先已经知道剪枝之后的网络结构了,主要是学习其中的模型参数。
经典的剪枝按照三步走实现。而在最新的研究论文表示模型剪枝学习到的参数并不重要,直接从头开始学习剪枝之后的模型参数可以达到等于或者优于finetune之后的效果,也就是认为对于等比例剪枝来说,不需要经历的过程,直接根据预先设定的剪枝结构train即可。
那是实际实验的效果如何?
1.2.1 全局剪枝VS等比例剪枝
以下实验结果来自千万级别文本数据,训练epoch均达到8-10左右,取其中最优效果
index | 剪枝比率 | 全局剪枝 | 等比例剪枝 | 从头开始训练 | finetune | 字段准确率 |
---|---|---|---|---|---|---|
1 | 原始模型 | × | × | × | × | 89.04% |
2 | 20% | √ | × | √ | × | 88.06% |
3 | 36% | √ | × | √ | × | 86.83% |
4 | 36% | × | √ | √ | × | 88.34% |
5 | 36% | × | √ | × | √ | 86.33% |
结论:
- 通过index3和index4的比较,在文字识别模型上,等比例剪枝优于全局剪枝;在《Rethinking the Value of NetWork Pruning》这篇论文中,作者的实验表明VGG网络而言,全局剪枝要优于等比例剪枝。这个结论和我这边的现象矛盾。剪枝本身可以理解为寻找特定任务在指定冗余网络结构上的最优网络模型。那么有可能因为对于目标分类任务来说,基于VGG结构的最优模型就是非等比率的网络层构成;而对于文字识别任务来说,输入都是指定尺寸的,且输出维度较多,最优模型就是等比率的网络结构了。可见在阅读论文的时候,需要实际操作,因为作者的结论都是基于实验的,而基于个人或者实验室的实验都是有偏向的。
- 实验index4和index5的比较可以发现,从头开始训练的效果优于finetune的效果。这大概是因为finetune比较容易让网络陷入局部最优,我采用的是RMS的优化器,采用的学习率也比较小,因此很难逃脱局部最优点。
二、基于DenseNet的文字识别模型
2.1 DenseNet网络结构
基于DenseNet的文字识别模型是DenseNet的backBone后面添加两层BILSTM层。和CRNN的区别仅仅在backbone的不同,但是实际效果相比于基于VGG的CRNN可以提升2到3个点。
DenseNet的网络结构可以参考DenseNet阅读笔记这篇笔记。此处我使用的DenseNetbackbone是由4个DenseBlock串联三个Transition模块构成的。DenseBlock用于抽取特征,Transition模块用于缩小网络的。
大体结构是
对于DenseBlock内部的有多个DenseLayer,其中每个DenseLayer的输入都是前面所有层的输出concat的结果。
2.2 DenseNet剪枝策略
回顾一下network-slimming剪枝算法,主要是对BN层的缩放因子gama进行的。比如某个BN层原本32个通道,剪枝之后保留16个通道,则意味着输入BN层的需要删除16个通道,可是DensLayer的输入不仅仅是这层的输入,也是后面所有denseLayer层的输入,如果直接删除的通道,那么后面的DenseLayer层的输入也就跟着减小了,但是在这一层显得无用的通道并不一定在后面的DenseLayer层中没有作用。所以不能删减输入的通道数量
再来看一下DenseLayer层的结构。一个DenseLayer的结构是
其中第一个层直面前面层的混合输入,不能减少,只能在BN层之后添加一个层。这一层是一个的数组。默认全一表示直接输出所有通道。某些位置为0表示对应位置的通道不输出到下一层。这样不直接删除层的输入通道,但是可以减少层后面的卷积层的输入通道数量。
下面是channel-selection层的代码:
class channel_selection(nn.Module):
"""
Select channels from the output of BatchNorm2d layer. It should be put directly after BatchNorm2d layer.
The output shape of this layer is determined by the number of 1 in `self.indexes`.
"""
def __init__(self, num_channels):
"""
Initialize the `indexes` with all one vector with the length same as the number of channels.
During pruning, the places in `indexes` which correpond to the channels to be pruned will be set to 0.
"""
super(channel_selection, self).__init__()
self.indexes = nn.Parameter(torch.ones(num_channels),requires_grad=False)
def forward(self, input_tensor):
"""
Parameter
---------
input_tensor: (N,C,H,W). It should be the output of BatchNorm2d layer.
"""
selected_index = np.squeeze(np.argwhere(self.indexes.data.cpu().numpy()))
if selected_index.size == 1:
selected_index = np.resize(selected_index, (1,))
output = input_tensor[:, selected_index, :, :]
return output
第一个层可以直接剪枝,输入为上一个BN层的剪枝之后的通道数量,输出为下一个BN层剪枝之后的通道数量。
第二个层也可以正常剪枝,因为其输入没有共享。
第二个层输入层为第二个BN层剪枝之后的通道数量,输出不能改变,依旧是DenseLayer每层规定的输出量。
这样就规定了DenseBlock内部的剪枝策略。
Transition层的主要目标是特征图缩小,其结构是。输入首先接一个BN层,但是这个BN层也直接对上面的输入通道剪枝,因为这里的输入是前面一个DenseBlock结构的输出,也是一个concat结果,也需要在BN层之后添加层。
如此剪枝策略就已经设计好了。
2.3 结果分析
在DenseNet的剪枝效果不是很理想,对于采用LSTM作为序列特征提取的文字识别模型来说,产出比较差,不适合network-slimming剪枝算法。
- DenseNet中的DenseBlock内部连接结构决定了部分卷积和BN层无法直接剪枝,需要添加channel_selection层,这样不仅没有减少参数,同时还增加了参数
- 剪枝只对卷积层实现,没有对LSTM层和embedding层实现。但是DenseNet本身就已经是一个小网络结构了,网络参数分布如下:可以看到在剪枝30%的情况下,卷积层确实从170万剪到了115万参数,但是因为RNN层和embedding层没有减少,总参数量达到了六百多万。BN层的参数也没有明显的减少。所以整体的参数减少很不明显。从保存的torch模型来看,模型大小从32.7M减小到30.4M,基本没啥影响。
剪枝前后 参数对比 | BN层参数 | 卷积层参数 | RNN层参数 | embedding层参数 | channel_selection层参数 | 总参数 | 不需要训练的参数 |
---|---|---|---|---|---|---|---|
原始模型 | 41824 | 1716064 | 2629632 | 3658203 | 0 | 8045723 | 0 |
添加channel_selectio层 | 41824 | 1716064 | 2629632 | 3658203 | 17168 | 8062891 | 17168 |
剪枝30% | 40542 | 1157944 | 2629632 | 3658203 | 17168 | 7503489 | 17168 |
- DenseNet的网络 结构导致占用大量的显存空间,导致Batchsize无法设置的很大,在这里只有48,而VGG网络可以设置到96,这就导致了训练时间长以及无法学习到BN层的generation信息。对于千万的数据规模,2天才能训练一个epoch,实验很慢。
- BN层的分布不适合剪枝。network-slimming算法是基于"较小范数不重要"的准则的(关于这块可以看小谈剪枝研究),这要求卷积核的范数分布偏差需要比较大,这样才能选择比较合适的剪枝阈值T,还需要卷积核中的最小范数足够小。也就是需要满足下面图1所示的蓝色的BN层分布才能有效剪枝。
但是在分析DenseNet训练了2万多epoch之后,发现BN层的参数分布如下,且这种分布并没有随着训练的加深而有所改善: