模型剪枝-ICLR2017-Pruning Filters for Efficient ConvNets
Filter 修剪
由于CNN通常在不同的 Filter 和特征信道之间具有显着的冗余,论文中通过修剪 Filter 来减少CNN的计算成本。与在整个网络中修剪权重相比, Filter 修剪是一种自然结构化的修剪方法,不会引入稀疏性,因此不需要使用稀疏库或任何专用硬件。通过减少矩阵乘法的次数,修剪 Filter 的数量与加速度直接相关,这很容易针对目标加速进行调整。
卷积核层面的模型剪枝
本质方法。就是计算每一个filter上权重绝对值之和,去掉m个权重最小的filters,并同时去掉与其相关的特征图及下一层的所有相关的输入filters;

筛选需要裁减的卷积核步骤为:
1.对每个Filter,使用L1-Norm来计算每一个filter上权重绝对值之和;
2.对所得权重之和进行排序,和大小反映了相关filter的重要性;
3.选择前k个较大的权重之和保留,建立一个mask,大保留的部分为1,小于阈值的部分为0。
cfg_mask = []
layer_id = 0 # 统计层数
for m in model.modules(): # 遍历vgg的每个module
if isinstance(m, nn.Conv2d): # 如果发现卷积层
out_channels = m.weight.data.shape[0]
# cfg[layer_id]: 每一层要保留的通道数量
if out_channels == cfg[layer_id]:
# 如果这一层的通道数已经满足,直接进入下一层循环
cfg_mask.append(torch.ones(out_channels))
layer_id += 1
continue
# 克隆所有卷积层的权重
weight_copy = m.weight.data.abs().clone()
weight_copy = weight_copy.cpu().numpy()
# weight_copy: [c_out, c_in, kernal, kernal]
# L1_norm : [c_out]
L1_norm = np.sum(weight_copy, axis=(1, 2, 3))
# arg_max为从大到小排序后的下标
arg_max = np.argsort(L1_norm)[::-1]
# 取前cfg[layer_id]个较大值
arg_max_rev = arg_max[:cfg[layer_id]]
assert arg_max_rev.size == cfg[layer_id], "size of arg_max_rev not correct"
# 删除的通道mask=0,保留的通道mask=1
mask = torch.zeros(out_channels)
mask[arg_max_rev.tolist()] = 1
# 记录每个卷积层保留的权重
cfg_mask.append(mask)
layer_id += 1
elif isinstance(m, nn.MaxPool2d):
layer_id += 1
AI生成项目python

之后需要进行BN2D层的剪枝,即需要丢弃刚才被抛弃的卷积核。
start_mask = torch.ones(3)
layer_id_in_cfg = 0
end_mask = cfg_mask[layer_id_in_cfg]
for [m0, m1] in zip(model.modules(), newmodel.modules()):
# 对BN2d层进行剪枝
if isinstance(m0, nn.BatchNorm2d):
# 获取大于0的所有数据的索引,使用squeeze变成向量
idx1 = np.squeeze(np.argwhere(np.asarray(end_mask.cpu().numpy())))
if idx1.size == 1:
idx1 = np.resize(idx1,(1,))
# 用经过剪枝后的层参数的替换原来的
# [c]
m1.weight.data = m0.weight.data[idx1.tolist()].clone()
m1.bias.data = m0.bias.data[idx1.tolist()].clone()
m1.running_mean = m0.running_mean[idx1.tolist()].clone()
m1.running_var = m0.running_var[idx1.tolist()].clone()
# 下一层
layer_id_in_cfg += 1
# 当前在处理的层的mask
start_mask = end_mask
# 全连接层不做处理
if layer_id_in_cfg < len(cfg_mask): # do not change in Final FC
end_mask = cfg_mask[layer_id_in_cfg]
AI生成项目python

最后需要进行卷积层剪枝,根据前后BN层的保留层,可以计算得到卷积层保留的卷积核大小(上层BN层输出,下层BN层输入),保留前后BN的对应保留的元素,其余剪枝。
# 对卷积层进行剪枝
elif isinstance(m0, nn.Conv2d):
# 卷积后面会接bn
idx0 = np.squeeze(np.argwhere(np.asarray(start_mask.cpu().numpy())))
idx1 = np.squeeze(np.argwhere(np.asarray(end_mask.cpu().numpy())))
print('In shape: {:d}, Out shape {:d}.'.format(idx0.size, idx1.size))
if idx0.size == 1:
idx0 = np.resize(idx0, (1,))
if idx1.size == 1:
idx1 = np.resize(idx1, (1,))
# 剪枝
# [c_out, c_in, kernal, kernal]
w1 = m0.weight.data[:, idx0.tolist(), :, :].clone()
w1 = w1[idx1.tolist(), :, :, :].clone()
m1.weight.data = w1.clone()
AI生成项目python

最后对FC层进行剪枝,由于最后一层FC层的输出是固定的(分类类数),因此只对FC层的输入维度进行剪枝,也是根据上一层BN层的输出,对应保留的元素,其余剪枝。
# 对全连接层进行剪枝
elif isinstance(m0, nn.Linear):
# 最后一层全连接层进行剪枝
if layer_id_in_cfg == len(cfg_mask):
idx0 = np.squeeze(np.argwhere(np.asarray(cfg_mask[-1].cpu().numpy())))
if idx0.size == 1:
idx0 = np.resize(idx0, (1,))
# [c_out, c_in]
m1.weight.data = m0.weight.data[:, idx0].clone()
m1.bias.data = m0.bias.data.clone()
layer_id_in_cfg += 1
continue
# 其余全连接层不剪枝
m1.weight.data = m0.weight.data.clone()
m1.bias.data = m0.bias.data.clone()
AI生成项目python

对BN1d层不剪枝
# 对BN1d层不进行剪枝,直接使用原始模型参数
elif isinstance(m0, nn.BatchNorm1d):
m1.weight.data = m0.weight.data.clone()
m1.bias.data = m0.bias.data.clone()
m1.running_mean = m0.running_mean.clone()
m1.running_var = m0.running_var.clone()
AI生成项目python
多层同时裁剪
多层同时修剪:
作者给出了2中修剪思路:
1)独立修剪:修剪时每一层是独立的。
2)贪心修剪:修剪时考虑之前图层中删除的 Filter 。
两种方法的区别:独立修剪在计算(求权重绝对值之和)时不考虑上一层的修剪情况,所以计算时下图中的黄点仍然参与计算;贪心修剪计算时不计算已经修剪过的,即黄点不参与计算。
结果证明第二种方法的精度高一些。

卷积核层面的剪枝的优点
删除整个不重要的过滤器的好处有很多:1)修剪后的模型在网络结构上没有区别,因此它可以被任何现成的深度学习库完美支持。2)内存占用将显著减少。这种记忆的减少不仅来自于模型参数本身,还来自于中间激活,这在以往的研究中很少被考虑。3)由于修剪后的网络结构没有受到破坏,可以通过其他压缩方法进一步压缩和加速,如参数量化方法。4)修剪后的模型可以极大地加速更多的视觉任务,如目标检测、语义分割等。
