深度学习中的网络剪枝(pruning)简介

参考文章:
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. 介绍
    参数:权重和偏差
    系数:权重
    如何定义”不重要的权重“呢?文中说了一个经典的例子:
    这个函数有两个系数:1和5。



    当我们改变第一个系数1时,函数图像只会发生微小的改变。(如图1) 因此这个系数可以看作”不重要的系数“。丢弃这样的系数不会严重影响函数的表现。


    图一
  2. 神经网络中的应用
    如何定义神经网络中的不重要的权重呢?
  • 分析梯度下降(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.

下图展示了以上代码中的变量传递过程:


image.png

我自己写了一段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数据集上来讨论这些概念。我们使用一个浅的全连接层网络,该网络的拓扑结构如下:


图2

这个网络一共有20410个可训练的
参数,训练该网络10个epoch就可以得到一个好的baseline。


图3

下面我们对这个网络进行剪枝,我们用到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。
下面是训练结果。红色线对应剪枝实验。从训练结果中我们发现,剪枝并不影响模型表现。

图4

注意事项:

  • 在训练时,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

方法二: 随机初始化一个网络,从头开始剪枝和训练

除了我们运用一个随机初始化的网络去训练以外,其他与第一种方法都保持一致。


图5

绿色线代表从头开始训练的实验。我们可以发现表现没有另外俩实验那么好。
图6意味着从头开始训练花费最多的时间,因为网络需要在保持目标稀疏性的基础上去更新参数


图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变小,准确率依旧很高。

图7

我们可以量化(quantize)模型去进一步减小size。在量化时,我们依旧需要去掉剪枝外包装(pruning wrappers)
图8

压缩率(compression ratio)是另一个评价剪枝算法效率的技巧。压缩率是剪枝后的网络中剩余参数的倒数。
这种量化方法称为post-training quantization。
至此,我们可以总结优化模型的步骤如下:


image.png

一些最新的pruning话题和方法

两个需要思考的问题:

  • 当我们重新训练剪枝后的网络时,我们是否可以把未被剪枝的权重(unpruned weights)初始化为他们原始的权重幅度?如果我们基于训练好的网络(网络A)得到了一个剪枝后的网络,考虑网络A 的原始幅度(个人理解:未被剪枝的权重初始化为未被剪枝前训练好的权重)
  • 当我们把magnitude-based pruning用到transfer learning的时候,我们如何定义权重的重要性?

Of Winning Tickets

第一个问题已经在这篇文章中被深入探索了。因此,在对已经被训练好的网络进行剪枝之后,有用上面描述的方法进行初始化的那些子网络被看作中彩票(Winning Tickets)。

图9

这个方法背后的原理可以解释为:在网络的最初训练过程中,特定的参数初始化会引导优化过程。这些在优化过程中响应较好的权重(即:在优化过程中变化较大(travel further))最终也会成为彩票赢家(winning ticker)。因此,为了更好地再训练,如果我们把这些权重初始化为他们最可能的初始幅度,优化过程就会因为这些权重的初始化而变得更好。这段解释参考this beautiful explanation

Of Systematic Exploration of Lottery Ticket Hypothesis

Generalization of Lottery Ticket Hypothesis

Pruning based on Weight Movements

总结和展望

你可能感兴趣的:(深度学习中的网络剪枝(pruning)简介)