DeepLearning——模型压缩剪枝量化

  • 模型压缩

  • 为什么需要模型压缩

  • 量化

  • 剪枝

  • 蒸馏

模型压缩

为什么需要模型压缩

  • 深度学习(Deep Learning)因其计算复杂度或者参数冗余,在一些场景和设备上限制了相应的模型部署,因此需要模型压缩、优化加速、异构计算等方法突破瓶颈。
  • 其实我们一般用框架训练好了模型的权重之后,我们需要把模型安装到其他设备上,例如PC端、服务器、嵌入式板子树莓派、tensorRT等等,不是所有的设备都具有我们训练时候的那种推理和计算能力。像嵌入式板子那种,对性能和功耗都有严格要求的,你总不能强迫公鸡下蛋吧。这样模型压缩算法的提出就应运而生啦。
  • 模型压缩算法能够有效的降低参数冗余,从而减少存储占用、通信宽带和计算复杂度,有助于深度学习的应用部署。具体可以划分为如下几种方法(后续重点介绍剪枝和量化):
总结 方法
线性或者非线性量化 1/2bits,int8和fp16等
结构或非结构剪枝 deep compression,channel pruning和network slimming等
网络结构搜索 NAS:Network Architecture Search:DARTS,DetNAS和NAS-FCOS
权重矩阵的低秩分解 知识蒸馏与网络结构简化(Squeeze-net,mobile-net,shuffle-net)

剪枝

DeepLearning——模型压缩剪枝量化_第1张图片

  • ☆剪枝时,针对净输出进行L1正则化,对净输出进行归一化约束,然后对净输出进行L1正则化,这样在把不必要权重剪枝的情况下,可以加大需要的权重,能力转移。
  • 工业运作是经常剪得就是神经元,如果剪神经元的参数,尽管参数变为0没有作用,但是在网络模型计算时仍然会进入模型计算。基本在支持精细化计算的时候才会使用参数剪枝。剪神经元则会相对应的减少计算量。

卷积核的剪枝

DeepLearning——模型压缩剪枝量化_第2张图片
DeepLearning——模型压缩剪枝量化_第3张图片

剪枝方法 依据
随机裁剪 随机
裁剪向量 计算二范数大小
裁剪核 对核进行裁剪
裁剪组 针对固定组的剪
裁剪通道 正常情况下用这个

DeepLearning——模型压缩剪枝量化_第4张图片
DeepLearning——模型压缩剪枝量化_第5张图片

  1. 剪枝常用手段
方法 描述
非结构剪枝 通常是连接级、细粒度的剪枝方法、精度相对较高,但是依赖于特定的算法库或者硬件平台的支持,如Deep Compression,Sparse-Winograd
结构剪枝 filter级或者layer级、粗粒度的剪枝方法,精度相对降低,但是剪枝策略更为有效,不需要特定算法库或者硬件平台的支持,能够直接在成熟深度学习框架上运行

结构剪枝

  • 局部方式,通过layer by layer方式,最小化输出FM重建误差的Channel Pruning,ThiNet ,Discrimination-aware Channel Pruning。
  • 全局方式的、通过训练期间对BN层Gamma系数添加L1正则化的Network Slimming;
  • 全局方式的、按Taylor准则对Filter做重要性排序的Neuron Pruning;
  • 全局方式的、可动态更新更新pruned filters 参数的剪枝方法;
  • 基于GAN思想的GAL方法,可裁剪包括Channel,Branch或Block等在内的异构结构;
  • 借助Geometric Median确定卷积滤波器冗余性的剪枝策略;
  • 基于Reingorcement Learning(RL) 实现每一层剪枝率的连续、精细控制,并可结合资源约束完成自动模型压缩;

SeNet网络权重剪枝思想

  • 卷积核,为了判断卷积核中那些比较有用,可以让卷积核乘以一个标量w形成一个新的卷积核,在反向梯度更新时,观察w的大小,可以知道每个卷积核的重要性比重权重,这样就可以知道哪些卷积核重要性。

代码演示

局部剪枝

import torch
from torch import nn
import torch.nn.utils.prune as prune
import torch.nn.functional as F

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        # 1 input image channel, 6 output channels, 3x3 square conv kernel
        self.conv1 = nn.Conv2d(1, 6, 3)
        self.conv2 = nn.Conv2d(6, 16, 3)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)  # 5x5 image dimension
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        # 最大赤化
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, int(x.nelement() / x.shape[0]))
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
model = LeNet().to(device=device)

按照pytorch上的代码先定义了一个网络结构,按照下一步代码main函数运行可以得到得到对应结果。

	# 定义模型一个卷积层
    module = model.conv1
    # 打印该层模型的参数
    print(list(module.named_parameters()))

DeepLearning——模型压缩剪枝量化_第6张图片
可以看到这一层的weight和bias

	# 用于剪枝时临时缓存的一些数据
	print(list(module.named_buffers()))

结果:[]

'''要修剪模块(conv1LeNet体系结构的层),请首先从中可用的修剪技术中选择修剪技术torch.nn.utils.prune(或 通过子类实现您自己 的修剪技术 BasePruningMethod)。然后,指定模块以及该模块中要修剪的参数的名称。最后,使用所选修剪技术所需的适当关键字参数,指定修剪参数。'''
	from torch.nn.utils import prune
	# 随机剪枝方法,针对这个模型的weight进行剪枝,剪枝比例0.3
	prune.random_unstructured(module, name="weight", amount=0.3)

下列运行代码:

# 函数运行
if __name__ == '__main__':
    model = LeNet().to(device=device)
    # 获取卷积层
    module = model.conv1
    # 打印参数,包括weight和偏值bias
    print(list(module.named_parameters()))
    # 剪枝时临时更改存储的数据掩码
    print(list(module.named_buffers()))
    # 按照0.3的权重比例,对网络该层的weight随机裁剪
    prune.random_unstructured(module, name="weight", amount=0.3)
    # 查看变化,看着值还没更改,但是已经生成了更改掩码,weight--->weight_orig(即追加"_orig"到初始参数name)来进行的。weight_orig存储未经修剪的张量版本。
    # print(list(module.named_parameters()))
    # 通过上面选择的修剪技术生成的修剪掩码被保存为名为weight_mask(即附加"_mask"到初始参数name)的模块缓冲区。
    # print(list(module.named_buffers()))
    # 已经改了
    print(module.weight)
    # 只显示对weight出现操作
    print(module._forward_pre_hooks)
    # L1剪枝。针对bias,剪3个最小的
    prune.l1_unstructured(module, name="bias", amount=3)
    # 打印,现在已经改为weight_orig 和 bias_orig
    print(list(module.named_parameters()))
    print(list(module.named_buffers()))
    print(module._forward_pre_hooks)
  • 剪枝尽量针对输出剪枝,针对输入剪枝不易操作。
 # 迭代修剪
    model = LeNet().to(device=device)
    # 获取卷积层
    module = model.conv1
    # dim :针对输出还是输入去剪枝,具体看形状 n:1,2 表示是L1,还是L2剪枝 name:针对weight还是bias
    prune.ln_structured(module, name="weight", amount=0.5, n=2, dim=0)
    print(module.weight)
    # 所有相关的张量,包括掩码缓冲区和用于计算修剪张量的原始参数,都存储在模型中state_dict ,因此可以根据需要轻松地序列化和保存。
    print(model.state_dict().keys())
    # 修剪永久化
    prune.remove(module, 'weight')
  • 多参数剪枝
for name, module in new_model.named_modules():
    # prune 20% of connections in all 2D-conv layers
    if isinstance(module, torch.nn.Conv2d):
        prune.l1_unstructured(module, name='weight', amount=0.2)
    # prune 40% of connections in all linear layers
    elif isinstance(module, torch.nn.Linear):
        prune.l1_unstructured(module, name='weight', amount=0.4)

print(dict(new_model.named_buffers()).keys())  # to verify that all masks exist

全局剪枝

model = LeNet()

parameters_to_prune = (
    (model.conv1, 'weight'),
    (model.conv2, 'weight'),
    (model.fc1, 'weight'),
    (model.fc2, 'weight'),
    (model.fc3, 'weight'),
)

prune.global_unstructured(
    parameters_to_prune,
    pruning_method=prune.L1Unstructured,
    amount=0.2,
   ''' 我们可以检查在每个修剪参数中引起的稀疏度,该稀疏度将不等于每层中的20%。但是,全局稀疏度将(大约)为20%。'''
)

量化

  • 低精度(Low precision)可能是最通用的概念。常规精度一般使用FP32(单精度32位浮点数)存储模型权重;低精度则表示FP16(半精度16位),INT8(8位的定点整数)等等数值格式,目前低精度往往指代INT8。
  • 混合精度(Mixed precision)在精度中使用FP32和FP16。FP16减少了一般的内存大小,但有些参数或者操作符必须采用FP32的格式才可以保持准确度。
  • 量化一般指INT8,将FP32类型转换为INT8类型,在损失少量精度的情况下,我们可以获得接近四倍的网络模型加速。
  • 为了解决存储和带宽的开销需求,获得更低的能耗和占用面积,同时获得更快的计算速度
  • 尚可接受的精度损失。即量化相当于对模型权重引入噪声,所幸CNN本身对噪声不敏感(在模型训练过程中,模拟量化所引入的权重加噪还有利于防止过拟合),在合适的比特数下量化后的模型并不会带来很严重的精度损失。按照gluoncv提供的报告,经过int8量化之后,ResNet50_v1和MobileNet1.0 _v1在ILSVRC2012数据集上的准确率仅分别从77.36%、73.28%下降为76.86%、72.85%。
  • 根据存储一个权重元素所需要的的位数还可以有以下的方法:
神经网络 方法
二值神经网络 在运行时权重和激活只取两种值(例如+1,-1)的神经网络,以及在训练师计算参数的梯度
三元权重网络 权重约束为+1,0,-1的神经网络
XNOR网络 过滤器和卷积层的输入是二进制的。XNOR网络主要视同二进制运算来近似卷积
  • 理论是一回事,实践又是一回事,如果一种技术方法难以推广到通用场景,泽需要进行大量的额外支持。花里胡哨的研究往往是过于棘手或前提假设过于强,以至于无法引入工业界。
  • 工业界最终选择是INT8量化——FP32在推理(inference)期间被INT8取代,训练仍然是FP32。TensorRT,TensorFlow,PyTorch.MxNet和其他许多深度学习软件都已经启用(或正在启用)量化。
  • 通常可以根据FP32和INT8的转换机制来对解决方案进行分类。一些框架简单地引入了Quantize和Dequantize层,当从卷积或全链接层送入或者取出时,它将FP32转换为INT8或相反。在这种情况下,模型本身的输入/输出采用FP32格式。深度学习框架加载模型,重写网络以插入Quantize和Dequantize层,并且将权重转换为INT8格式。
    DeepLearning——模型压缩剪枝量化_第7张图片
  • 在上图越往边上,下方映射的区间就代表更宽的区间,由于神经网络训练出来的权重一般都在0附近,那么在0附近可以近似于等分。
    方法一:
    DeepLearning——模型压缩剪枝量化_第8张图片
    方法2:
    DeepLearning——模型压缩剪枝量化_第9张图片
  • 静态量化
  1. 尽量 让卷积、BN、Relu组成结构
  2. 按照pytorch官网实现量化简单例子
  3. 对原始的网络结构调整
  4. 在前向计算时量化,后向反量化

蒸馏

  • 原理
    DeepLearning——模型压缩剪枝量化_第10张图片
    DeepLearning——模型压缩剪枝量化_第11张图片
  • 用能量温度的角度来看的话,初始温度设置高,模型会变得活跃,这样把老师模型(大模型)变得活跃,大模型学习硬标签,小模型跟着大模型一起学习,不过小模型学习的是大模型的输出,学习软标签。这样不断将温度调低,大模型就会渐渐变得稳定下来,也就是学习难度慢慢增大。小模型根据之前的软标签学习已经学习到一定经验,尽管学习难度增大,但是由于经验的原因,仍然可以继续学习。

蒸馏法升级(fps)

DeepLearning——模型压缩剪枝量化_第12张图片
DeepLearning——模型压缩剪枝量化_第13张图片
在特征层加上l2正则化损失,从特征层就开始学习,最后总损失是损失+特征层损失。

(NAS)

DeepLearning——模型压缩剪枝量化_第14张图片

  • 神经架构生成,自动搜索
  • 废钱——不想做

模拟退火法

  • paddleSlim(轻量级的集成框架,效果试过,不怎么好)

DeepLearning——模型压缩剪枝量化_第15张图片

  • 也有蒸馏思想,不断对小模型进行扰动,还加入了一定的探索机制。
  • 反正自己找的就是慢TT(平民玩家不好玩)

可微分神经架构搜索

  • 最近的研究——基于梯度的架构搜索(DARTS)
  • 先定义几个初始结构,卷积,最大池化等等,
  • 然后后面的结构全部与前面结构相连
  • 在每种结构前加一个系数w,开始训练,最后做一个softmax,就可以得到概率最大的那条线的组件。哪一个权重最大留哪条线(有点类似动态剪枝)
    DeepLearning——模型压缩剪枝量化_第16张图片

你可能感兴趣的:(深度学习,python,神经网络,人工智能,深度学习)