参考文章:
https://towardsdatascience.com/scooping-into-model-pruning-in-deep-learning-da92217b84ac
参考代码:
https://colab.research.google.com/github/matthew-mcateer/Keras_pruning/blob/master/Model_pruning_exploration.ipynb
(一些术语我渐渐地就直接写英文了。)
零、介绍
剪枝(pruning)丢弃了不严重影响模型表现的权重。
一、函数和神经网络中的“不重要”
-
介绍
参数:权重和偏差
系数:权重
如何定义”不重要的权重“呢?文中说了一个经典的例子:
这个函数有两个系数:1和5。
当我们改变第一个系数1时,函数图像只会发生微小的改变。(如图1) 因此这个系数可以看作”不重要的系数“。丢弃这样的系数不会严重影响函数的表现。
- 神经网络中的应用
如何定义神经网络中的不重要的权重呢?
- 分析梯度下降(gradient descent)中的优化过程(optimization)。所有的权重都用相同的梯度量级(gradient maginitudes)进行更新。损失函数的梯度与权重(和偏差)有关。在优化过程中,有的权重比其他权重用更大的幅度量级(有正有负)进行更新,这些权重可以看作”更重要“的权重。
(个人理解:大权重幅度会导致input经过该权重后变化更大) - 训练结束之后,我们检查网络每一层的权重幅值,并找出”重要“的权重。寻找方法如下(heuristics):
(1) 降序排列权重幅值
(2)找到在队列中更早出现的那些幅值(对应weight maginitudes更大)”那些“具体有多少,取决于有百分之多少的权重需要被剪枝。(percentage of weights to be pruned)
(3)设定一个阈值,权重幅值在阈值之上的权重会被视为是重要的权重。这个阈值的设定也有以下几种方法:
(a)这个阈值可以是整个网络中最小的权重梯度
(b)这个阈值可以是该网络中某一层的最小权重阈值。在这种情况下,不同层的“重要”权重之间是有偏差的。
如果上面难以理解,没关系,接下来结合具体代码解释上面的概念。
对训练好的神经网络剪枝
首先我们讨论基于幅值的剪枝(magnitude-based pruning)。权重幅值(weight magnitude)为剪枝的标准。
在这段代码中,先取出权重,然后进行从小到大排列。基于稀疏百分比(sparsity_percentage=0.7),把权重中的从小到大排列的前百分之七十的权重设置为0。
# Copy the kernel weights and get ranked indices of the
# column-wise L2 Norms
kernel_weights = np.copy(k_weights)
ind = np.argsort(np.linalg.norm(kernel_weights, axis=0))
# Number of indices to be set to 0
sparsity_percentage = 0.7
cutoff = int(len(ind)*sparsity_percentage)
# The indices in the 2D kernel weight matrix to be set to 0
sparse_cutoff_inds = ind[0:cutoff]
kernel_weights[:,sparse_cutoff_inds] = 0.
下图展示了以上代码中的变量传递过程:
我自己写了一段python代码来理解每个函数,写完发现上图中范数计算部分没显示完整,前两个范数都不是0,是0.6左右的值,就是每一列两项求二维范数,代码如下:
import numpy as np
a = np.array([[-0.4711,0.4448,0.065],[0.4070,0.4301,-0.7560]])
b = np.linalg.norm(a, axis=0)
c = np.argsort(np.linalg.norm(a, axis=0))
sparsity_percentage = 0.7
cutoff = int(len(c)*sparsity_percentage)
sparse_cutoff_inds = c[0:cutoff]
a[:,sparse_cutoff_inds] = 0
这个方法也能用在偏差(bias)上。在这个例子中,我们所分析的这一层,接收到的输入的shape是(1,2)并且有3个神经元。剪枝后建议重新训练网络,以补偿在网络performance上的损失。在重新训练的时候需要注意,剪枝后的那些权重在重新训练的过程中不能被更新。
模型剪枝技巧
下面我们在MNIST数据集上来讨论这些概念。我们使用一个浅的全连接层网络,该网络的拓扑结构如下:
这个网络一共有20410个可训练的
参数,训练该网络10个epoch就可以得到一个好的baseline。
下面我们对这个网络进行剪枝,我们用到tensorflow里的tensorflow_model_optimization函数,这个函数给我们提供了两种剪枝技巧:
- 拿一个训练好的网络,剪枝并且再训练
- 随机初始化一个网络,从头开始剪枝和训练
我们打算去实验这两个方法
注意:tfmot
提供了封装函数去剪枝模型内的特定层。具体内容见此链接
方法一: 拿一个训练好的网络,剪枝并且再训练
我们拿来了之前训练好的网络,然后我们需要有一个pruning schedule,同时在训练过程中保证sparsity level constant (即每一层固定为0的权重数目占总数目的百分比)以下代码完成了上述任务:
pruning_schedule = tfmot.sparsity.keras.ConstantSparsity(
target_sparsity=target_sparsity,
begin_step=begin_step,
end_step=end_step,
frequency=frequency
)
pruned_model = tfmot.sparsity.keras.prune_low_magnitude(
trained_model, pruning_schedule=pruning_schedule
)
一个被剪枝过的模型在再次重新训练之前需要重新编译(re-compile)。以下代码进行了编译和打印。
pruned_model.compile(loss='sparse_categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
pruned_model.summary()
这是打印结果:
Layer (type) Output Shape Param #
=================================================================
prune_low_magnitude_conv2d ( (None, 26, 26, 12) 230
_________________________________________________________________
prune_low_magnitude_max_pool (None, 13, 13, 12) 1
_________________________________________________________________
prune_low_magnitude_flatten (None, 2028) 1
_________________________________________________________________
prune_low_magnitude_dense (P (None, 10) 40572
=================================================================
Total params: 40,804
Trainable params: 20,410
Non-trainable params: 20,394
我们发现参数的数目有变化。这是因为tfmot
对网络里每一个被剪枝的权重加了一个 non-trainable mask。这个mask的值为0或1。
下面是训练结果。红色线对应剪枝实验。从训练结果中我们发现,剪枝并不影响模型表现。
注意事项:
- 在训练时,pruning schedule必须明确具体。我们可以具体化这个回调函数UpdatePruningStep去看训练过程中的pruning update
- PruningSummaries提供了在训练过程中稀疏性和幅度阈值的总结[here](https://tensorboard.dev/experiment/sRQnrycaTMWQOaswXzClYA/#scalars&_smoothingWeight=0
- pruning schedule可以被视为一个超参数(hyperparameter)。基于这个想法,
tfmot
提供了另一种pruning schedule PolynomialDecay - 在pruning schedule中,
end_step
不高于trained epoch的数目。我们可以设置frequence
的值(剪枝需要被使用的频率)以得到在要求的稀疏度下的网络的良好表现。
以下代码可以去测验tfmot
是否达到了目标稀疏程度
for layer in model.layers:
if isinstance(layer, pruning_wrapper.PruneLowMagnitude):
for weight in layer.layer.get_prunable_weights():
print(np.allclose(
target_sparsity, get_sparsity(tf.keras.backend.get_value(weight)),
rtol=1e-6, atol=1e-6)
)
def get_sparsity(weights):
return 1.0 - np.count_nonzero(weights) / float(weights.size)
运行该代码,对所有被剪枝的层,将会输出True
方法二: 随机初始化一个网络,从头开始剪枝和训练
除了我们运用一个随机初始化的网络去训练以外,其他与第一种方法都保持一致。
绿色线代表从头开始训练的实验。我们可以发现表现没有另外俩实验那么好。
图6意味着从头开始训练花费最多的时间,因为网络需要在保持目标稀疏性的基础上去更新参数
评价表现
我们需要深入思考以下两个问题:
- 输出剪枝和未剪枝的参数,压缩他们,计算size
- 量化(quantize)剪枝和未剪枝的参数,压缩他们,计算size和评估表现
我们用到标准的zipfile
库去压缩模型为zip
格式。我们在序列化剪枝后的模型时,需要用到这个函数[tfmot.sparsity.keras.strip_pruning](https://www.tensorflow.org/model_optimization/api_docs/python/tfmot/sparsity/keras/strip_pruning)
,这个函数会移除tfmot
给模型加的剪枝外包装(pruning wrappers),否则我们看不到剪枝后在size上的效果的。
def get_gzipped_model_size(file):
_, zipped_file = tempfile.mkstemp('.zip')
with zipfile.ZipFile(zipped_file, 'w', compression=zipfile.ZIP_DEFLATED) as f:
f.write(file)
return os.path.getsize(zipped_file)
在Keras版本代码中,这段代码中的file
应该是指向直接序列化的Keras模型的路径。
可以看到剪枝后的模型size变小,准确率依旧很高。
我们可以量化(quantize)模型去进一步减小size。在量化时,我们依旧需要去掉剪枝外包装(pruning wrappers)
压缩率(compression ratio)是另一个评价剪枝算法效率的技巧。压缩率是剪枝后的网络中剩余参数的倒数。
这种量化方法称为post-training quantization。
至此,我们可以总结优化模型的步骤如下:
一些最新的pruning话题和方法
两个需要思考的问题:
- 当我们重新训练剪枝后的网络时,我们是否可以把未被剪枝的权重(unpruned weights)初始化为他们原始的权重幅度?如果我们基于训练好的网络(网络A)得到了一个剪枝后的网络,考虑网络A 的原始幅度(个人理解:未被剪枝的权重初始化为未被剪枝前训练好的权重)
- 当我们把magnitude-based pruning用到transfer learning的时候,我们如何定义权重的重要性?
Of Winning Tickets
第一个问题已经在这篇文章中被深入探索了。因此,在对已经被训练好的网络进行剪枝之后,有用上面描述的方法进行初始化的那些子网络被看作中彩票(Winning Tickets)。
这个方法背后的原理可以解释为:在网络的最初训练过程中,特定的参数初始化会引导优化过程。这些在优化过程中响应较好的权重(即:在优化过程中变化较大(travel further))最终也会成为彩票赢家(winning ticker)。因此,为了更好地再训练,如果我们把这些权重初始化为他们最可能的初始幅度,优化过程就会因为这些权重的初始化而变得更好。这段解释参考this beautiful explanation