论文地址:原文
代码实现
中文翻译
PRUNING FILTER IN FILTER
Fanxu Meng 孟繁续
NeurIPS 2020
2020
剪枝已成为现代神经网络压缩和加速的一种非常有效的技术。现有的剪枝方法可分为两大类:滤波器剪枝(FP)和权重剪枝(WP)。与WP相比,FP在硬件兼容性方面胜出,但在压缩比方面失败。为了收敛两种方法的强度,我们提出在滤波器中对滤波器进行剪枝。具体来说,我们将滤波器F∈RC×K×K视为K个×K条,即1 × 1个滤波器∈RC,然后通过修剪条纹而不是整个滤波器,我们可以在硬件友好的同时实现比传统FP更细的粒度。我们称我们的方法为SWP (Stripe-Wise Pruning)。SWP的实现是通过引入一个新的可学习的矩阵,称为滤波器骨架,其值反映了每个滤波器的形状。正如一些最近的工作表明,修剪的结构比继承的重要权值更重要,我们认为单个过滤器的结构,即形状,也很重要。通过大量的实验,我们证明了SWP比之前基于fp的方法更有效,并在cifa10和ImageNet数据集上实现了最先进的剪枝率,而精度没有明显下降。有关代码载于[this url].
剪枝、SWP
这篇文章的工作总结为:
认为隐含在滤波器参数中的形状属性是很重要的。
用滤波器骨架(Filter Skeleton)学习滤波器形状,将形状和参数分离。
以滤波器的任意一条(Stripe)为单位,将滤波器裁剪为任意形状。
通过卷积计算方式变换,结构化实现逐条剪枝(Stripe-Wise Pruning)。
这篇文章主要考虑的是神经网络的结构属性。他这里的做法很有启发性,结合了两种结构化和非结构化剪枝中的典型方法。就是对weights剪枝和对filters剪枝,因为这两种剪枝方法各有优劣。非结构化的剪枝在硬件方面需要有专用的库支持,但是它的压缩率较高,对filters剪枝在硬件方面更兼容,但在压缩率方面不如前者。所以作者提出了一个方法,叫做在filters中剪枝filter。
那这是怎么做的呢?如上图中一个kernel,它的长和宽是相等的,是k×k×c,那我们就可以按照他的size把它剪成k×k个条,比如一个3×3的卷积,我们就可以把它剪成9 个条纹,然后通过修剪整个条而不是剪掉整个filter,显然就可以实现比传统的filters pruning更精细的一个粒度。这个方法作者叫做SWP。
对作者定义的FilterStripe层取代卷积层的剪枝方法也是理解代码的关键。按原论文说的,为了进行有效的修剪,我们设置了一个阈值δ,FS中对应值小于δ的条带在训练期间将不会更新,并且可以在之后进行修剪。值得注意的是,当对修剪后的网络进行推理时,由于过滤器被破坏,我们不能直接使用过滤器作为一个整体对输入特征图进行卷积。相反,我们需要独立地使用每个条带来执行卷积,并对每个条带产生的特征图求和,如图5所示。因为要对每个条带进行单独的卷积,所以就把每个条带进行了展平处理,把有用的条带进行卷积后的通带在前向传播的时候进行叠加,这样在BN操作的时候就和对正常卷积进行剪枝之后的操作相同了。
下面是剪枝之后打印出来的网络,可以看到FilterStripe的第一个参数就是FS骨架的叠加,对其相加求和正好就是后面第三个参数 输出通道数。BN层的通道数是卷积之后同一个FS骨架的通道数叠加,也就是上面说的正常卷积之后的操作了。
VGG(
(features): Sequential(
(0): FilterStripe(
tensor([[ 6, 15, 4],
[ 8, 13, 10],
[ 7, 13, 8]], device='cuda:0'),3, 84, kernel_size=(1, 1), stride=(1, 1)
)
(1): BatchNorm(41, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
(3): FilterStripe(
tensor([[40, 45, 33],
[46, 46, 33],
[34, 39, 31]], device='cuda:0'),41, 347, kernel_size=(1, 1), stride=(1, 1)
)
(4): BatchNorm(63, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(5): ReLU(inplace=True)
(6): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(7): FilterStripe(
tensor([[ 51, 74, 56],
[ 77, 107, 77],
[ 61, 70, 58]], device='cuda:0'),63, 631, kernel_size=(1, 1), stride=(1, 1)
)
(8): BatchNorm(127, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(9): ReLU(inplace=True)
(10): FilterStripe(
tensor([[ 98, 105, 98],
[106, 123, 98],
[ 93, 106, 100]], device='cuda:0'),127, 927, kernel_size=(1, 1), stride=(1, 1)
)
(11): BatchNorm(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(12): ReLU(inplace=True)
(13): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(14): FilterStripe(
tensor([[187, 189, 188],
[191, 213, 195],
[194, 182, 181]], device='cuda:0'),128, 1720, kernel_size=(1, 1), stride=(1, 1)
)
(15): BatchNorm(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(16): ReLU(inplace=True)
(17): FilterStripe(
tensor([[201, 189, 206],
[174, 151, 176],
[195, 180, 200]], device='cuda:0'),256, 1672, kernel_size=(1, 1), stride=(1, 1)
)
(18): BatchNorm(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(19): ReLU(inplace=True)
(20): FilterStripe(
tensor([[113, 118, 100],
[104, 127, 102],
[102, 115, 116]], device='cuda:0'),256, 997, kernel_size=(1, 1), stride=(1, 1)
)
(21): BatchNorm(244, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(22): ReLU(inplace=True)
(23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(24): FilterStripe(
tensor([[101, 123, 121],
[ 97, 154, 102],
[ 73, 110, 81]], device='cuda:0'),244, 962, kernel_size=(1, 1), stride=(1, 1)
)
(25): BatchNorm(417, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(26): ReLU(inplace=True)
(27): FilterStripe(
tensor([[ 74, 71, 53],
[ 27, 129, 28],
[ 42, 79, 55]], device='cuda:0'),417, 558, kernel_size=(1, 1), stride=(1, 1)
)
(28): BatchNorm(398, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(29): ReLU(inplace=True)
(30): FilterStripe(
tensor([[15, 21, 20],
[49, 76, 36],
[23, 16, 40]], device='cuda:0'),398, 296, kernel_size=(1, 1), stride=(1, 1)
)
(31): BatchNorm(272, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(32): ReLU(inplace=True)
(33): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(34): FilterStripe(
tensor([[ 0, 2, 0],
[ 0, 220, 2],
[ 0, 1, 0]], device='cuda:0'),272, 225, kernel_size=(1, 1), stride=(1, 1)
)
(35): BatchNorm(225, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(36): ReLU(inplace=True)
(37): FilterStripe(
tensor([[ 1, 10, 0],
[ 24, 245, 21],
[ 3, 10, 0]], device='cuda:0'),225, 314, kernel_size=(1, 1), stride=(1, 1)
)
(38): BatchNorm(314, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(39): ReLU(inplace=True)
(40): FilterStripe(
tensor([[ 11, 70, 3],
[ 18, 153, 19],
[ 8, 84, 14]], device='cuda:0'),314, 380, kernel_size=(1, 1), stride=(1, 1)
)
(41): BatchNorm(378, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(42): ReLU(inplace=True)
(43): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(44): AvgPool2d(kernel_size=1, stride=1, padding=0)
)
(classifier): Linear(in_features=378, out_features=10, bias=True)
)
以上图举例:
FS的sum(dim(1,2))四个值分别为 1、9、3、3
FS的sum(dim(0))是一个9*9的矩阵也就是FilterStripe的第一个参数应该是
tensor
[[ 2, 2, 1],
[ 3, 2, 2],
[ 1, 1, 2]]
其实就是FS的四个块对应位置求和。
那么FilterStripe的第三个参数展平之后的输出通道就是上面的矩阵相加也就是16.
因为FS没有全0的块,所以BN的处理仍然是展平之前的4.
这篇论文的作者跟rethinking the value of network pruning思路是一样的。 都是认为网络的体系结构很重要。而且本文中作者认为,Filter本身的结构也很重要。而且他的观点是,内核越大的filters性能越好。就要提出一个形状的概念。这个形状是什么意思呢?比如这个图
这是通道的L1范数值的示意图。从这个图可以看出,filters中并非所有的条纹贡献都相等,对应L1范数非常低的条带就可以删除。那删除以后保留最少条数的,同时保持filters功能的形状就叫做最佳的Filter形状。所以将要解决的一个问题就是我们怎么找到最佳的形状,还提出了一种filters框架来学习这个最佳形状。
将每个条纹分为多个组,并修剪每个组中的权重。然而,这些非结构化剪枝方法的一个缺点是所得到的权重矩阵是稀疏的,这在没有专用硬件/库的情况下不能实现压缩和加速。所以虽然这个方法很新颖,但是还是只能在GPU上加速,至于在IC或者ASIC上就很难支持了。
实现了对VGG网络模型的复现工作。