深度学习中的网络剪枝(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

总结和展望

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343