之前接触深度学习时结合自己负责的项目接触过关键点检测,语义分割这两个方向,当时主要研究了下对应方向的最新的网络以及曾经提出过的经典网络。在学习的过程中其实越来越体会到,选网络,训练,调参这些任务并不难的,因为这些在网上参考资料挺多的。其实深度学习这块最核心最重要的还是往应用端部署。很多网络能在显卡上达到实时性,但是怎么部署到算力有限的下位机中,这些在网上资料比较少,尤其是如果部署的平台比较小众,像一些国产的低端芯片,用的人比较少,然后可能本来芯片卖的就便宜,配套的资料不完善等等,这时候很多问题在网上可能都查不到,问别人吧,可能别人也没碰到过的,这个时候就真的得硬凭经验去填坑了,这些经验才是真的宝贵。所以希望以后深度学习这块多往应用端,部署端去做,所以就先开始研究了模型的剪枝。
当前模型剪枝分结构化剪枝和非结构化剪枝。结构化剪枝就是砍通道,针对channel进行剪枝,而非结构化剪枝就是保留通道数,只对卷积参数稀疏化。
对于有些硬件平台,如果有针对稀疏化模型的推理加速设计,则可以通过模型的稀疏化训练,最后对模型推理进行加速。但是如果有些硬件平台不支持,则无法通过稀疏化训练加速。
但是如果对channel进行裁剪,也就是结构化剪枝,则整个模型的结构会被压缩,这样就不受硬件平台限制了。也就是说,结构化剪枝不需要硬件支持,而非结构化剪枝需要硬件支持。
之前自己训练模型的时候,最后训练好模型后,都会再进行稀疏化训练,因为我们平台的硬件是支持非结构化剪枝的。稀疏化训练的过程大致如下,自己设一个最终要达到的稀疏化的比例,比如0.8,然后分N步进行训练,每一步训练就让每一层卷积中的0.8/N的参数稀疏化,不断累积N步,就让0.8的参数稀疏化了,每次更新参数的时候会排序,越接近0的参数越先置为0(即稀疏化),一旦稀疏化后,该参数后续就不再更新了。不同卷积层稀疏化程度不同,输入输出卷积个数越大,则稀疏化程度越高,反之越低。
稀疏化训练不会改变网络结构,模型大小是不会变小的,并且是需要硬件平台的一些支持才能实现推理加速。其实在研究Hrank论文之前,我有通过稀疏化训练来试图对网络进行结构化剪枝,当时我主要是通过对卷积层的稀疏度来判断该卷积层是否有用,然后认为稀疏度越高的卷积核,其作用越低,越应该优先裁掉。该思路跟Hrank论文思路有些相似。
Hrank的思路是先通过实验得出一个结论,就是每层卷积层之后的特征图的rank值不管训练多少个batch都不会变。这个我自己写代码实验验证过的。
也就是说,每次训练之初,模型的每层卷积后的特征图的rank值就定了,不管训练多少次都不会变,我猜测这个主要是跟模型初始化有关,一旦模型初始化好了之后,对应的每层的特征图的rank值就是定了的。针对这个猜测,我拿Hrank的开源代码来做过一些实验,实验过程是这样的,用vgg16跑cifar-10数据集,然后每次观察第1层和第2层的卷积后的特征图,并把对应特征图的rank值进行从小到大排序,我发现每次重新初始化后,特征图的rank值都会变化,而且每次初始化不同,对应rank的最小值到最大值的波动也不同,但是随着batch增大,rank值几乎不会有大的变化,基本上都是零点几的变化。
Hrank的开源代码中先需要生成每一层卷积层对应的mask,这个mask记录的就是每层卷积对应的每个cov的rank值,及对应rank值排序后的下标值。这个mask是通过跑要训练的数据集的前5个batch生成的卷积层对应的特征图的rank值。然后后续训练时就把每层的这些rank值拿来排序,rank值最小的那部分,就是要裁剪的channel。也就是通过小部分训练数据,即获取要裁剪的channel位置。训练的时候一层层卷积来裁剪,上一层裁剪好了再微调完了,再对下一层进行裁剪和微调。
为什么可以通过特征图的rank值排序,将低rank特征图对应的conv裁掉来剪枝呢?个人认为这个应该不难理解,在学矩阵的相关知识的时候我们就知道,矩阵的秩越大,说明全部为0的行就越少。特征图的rank值越低,那说明这张特征图中为0的行数越多。如果一个特征图中很多行为0 ,那说明这张特征图没什么信息,它往下一层传递的信息就很少,这个特征图其实就没什么用的,而这张特征图主要就是由它对应的卷积生成的,那说明该卷积核也没什么用。这点其实跟通过稀疏度来判断卷积核是否重要有点类似的。
Hrank最终训练得到的模型大小并没有减小,它只是在训练后把很多channel所有参数都置为0了,并没有把不为0的channel的参数拷贝出来重新设置通道数,构建新的模型。我写过代码把不为0的通道参数拷贝出来,然后重新构建模型,让训练的模型变小,最后也验证了下推理结果,跟没有这样操作的模型跑出来的结果一模一样。下面的代码就是把压缩后为0的channel去掉,不为0的channel拷贝出来,重新构建新的模型。
net_dict = net_par.state_dict()
model_dict = model.state_dict()
# print(type(net_dict))
for par1, par2 in zip(net_dict,model_dict):
# print(len(net_dict[par1].shape))
# print(net_dict[par1].shape)
if len(net_dict[par1].shape) == 4:
channel_not_zero = 0
channel_num = 0
for channel in net_dict[par1]:
# print(channel.shape)
temp_array = channel.detach().numpy()
if (np.all(temp_array) == 0.):
num = num + 1
else:
with torch.no_grad():
if conv_i == 0:
# print(channel_not_zero)
model_dict[par2][channel_not_zero] = channel
else:
conv_num = 0
prev_channel_num = 0
for prev_channel in pre_conv:
prev_channel_array = prev_channel.detach().numpy()
if (np.all(prev_channel_array) == 0.):
# print(n)
num = num + 1
else:
model_dict[par2][channel_not_zero][conv_num] = channel[prev_channel_num]
conv_num = conv_num +1
prev_channel_num = prev_channel_num + 1
# module2.bias[channel_not_zero] = module1.bias[channel_num]
channel_not_zero = channel_not_zero + 1
channel_num = channel_num +1
conv_i = conv_i + 1
pre_conv = net_dict[par1]
if len(net_dict[par1].shape) <= 2:
if net_dict[par1].shape == model_dict[par2].shape :
model_dict[par2] = net_dict[par1]
else:
channel_not_zero = 0
channel_num = 0
for channel in pre_conv:
# print(channel.shape)
temp_array = channel.detach().numpy()
if (np.all(temp_array) == 0.):
num = num + 1
else:
# print(111111111)
# print(net_dict[par1].shape)
# print(model_dict[par2].shape)
# print(channel_not_zero)
model_dict[par2][channel_not_zero] = net_dict[par1][channel_num]
channel_not_zero = channel_not_zero + 1
channel_num = channel_num + 1
我们训练网络的时候,进行剪枝都是直接把网络通道数减少,然后训练后跑测试集观察效果,如果发现效果没多大变化,就继续裁剪,然后再训练后再观察结果,一步步迭代,直到最终在效果和网络裁剪度之间达到一个平衡。
当前不管是结构化剪枝还是非结构化剪枝,都需要先设置裁剪度,或者稀疏化程度。现在即使换了Hrank的方式,也还是要设置裁剪程度或者比例,这个比例不好把控,也还是需要反复试验对比效果来确认。对于Hrank,我认为它提供了一种判断卷积核是否有用的依据,后续其实可以做很多试验,看看能否通过rank值,判断已经训练好的网络的哪些卷积层参数有冗余,可以针对那一层裁剪几个卷积核,再微调下的。或者训练一个网络,让每层卷积的前几个channe作用大一些,越靠后,重要性越低,这样是否能够在后续像剥洋葱一般,把后面的卷积核一点点剥掉,最后只留下最有用的卷积核。
此外,每层特征图的rank值的最大值和最小值的波动及分布是否跟卷积核个数有关,是否卷积核越多,rank值普遍越低,而卷积核越少,rank值就普遍越高?这些都需要后续试验来研究和验证的。