模型压缩

模型蒸馏

模型蒸馏属于模型压缩的一种方法,典型的Teacher-Student模型。

Net-T(教师模型):不做任何关于模型架构、参数量、是否集成的限制。输入X,输出Y对应的概率分布。
Net-X(学生模型):参数量小、模型结构相对简单。输入X,输出Y对应的概率分布。

泛化能力较强的Net-T,再利用Net-T来蒸馏训练Net-S,可以直接让Net-S去学习Net-T的泛化能力。
其中,最为直白高效的迁移泛化能力的方法:使用softmax层输出的概率分布作为“soft target”。
KD(Knowledge Distill)的训练方式使得每个样本给Net-S带来的信息量大于传统的训练方式。

“soft target”即在计算softmax时,score的值除以超参T。T越大,softmat输出的概率分布曲线越平坦,熵越大,那么Net-S对负标签的关注越大。

Net-S在训练时。损失由两部分构成:和,是Net-S在T温度下的softmax概率分布与Net-T的softmax概率分布的交叉熵。是Net-S正常情况的softmax概率分布与标签分布的交叉熵。和是可调整的超参。


是Net-T在温度T下的概率分布;
是Net-S在温度T下的概率分布;

注:在Net-S训练完毕后,做inference时其softmax的温度T要恢复到1。

模型量化

模型量化按照不同的维度有不同的分法,比如分为1bit量化、8bit量化、任意bit量化;或分为训练后动态量化、训练后静态量化、训练时量化,...

量化的概念:使用更少的bit来存储原本以浮点数存储的tensor,以及使用更少的bit来完成原本以浮点数完成的计算。

1.1二值量化(1bit量化)
典型代表:binarized neural networks二值量化模型,权重和激活值只有+1和-1。
将权重和每层的激活值全部二值化的方法有两种:
第一种:符号函数

第二种:以一定的概率赋值,类似dropout。
针对符号函数导数不连续问题,对sign(x)进行松弛,在-1到1之间采用线性函数
注:二值网络在训练过程中还是需要保存实数的参数。在进行权重参数更新时,裁剪超出[-1,1]的部分,保证权重参数始终是[-1,1]之间的实数。在使用参数时,将参数进行二值化。

1.2 8bit量化
8bit量化是最常见的量化方法。
TensorRT工具中的对称的8bit量化方案:通过最小化原始数据分布和量化后数据分布之间的KL散度来对激活值进行量化,即:

注:偏置可不需要,则:T=s*t,其中,s是比例因子。
示意图:

image.png

上图将-|max|和+|max|的FP32值映射为-127和127,中间值按照线性关系映射。
此方案容易造成较大精度损失,原因在于max存在离散点噪声。改进的方法:取阈值 。这样做的好处在于,一定程度抛弃了异常值。

如何确定K?
原始数据是一个FP32的分布,我们要用INT8的分布来表达这个Tensor,问题在于如何确定K。现在用一个指标来衡量FP32分布和INT8分布之间的差异程度,即KL散度。

其中,p是参考分布,q是量化分布。
K的经验范围是[128,2048),所以确定K的做法是遍历K的所有取值,并计算KL散度,取KL散度最小的时候对应的值。一旦K确定,比例因子s也就确定。
最后,通过公式,将原始FP32的分布映射到INT8的量化分布,超过阈值K的部分需要做截断。

1.2训练后动态量化
权重被提前量化,而激活值在推理过程中被动态量化。这种方法适用于模型执行时间由内存加载权重,而不是计算矩阵乘法所支配的情况,适用于批量较小的LSTM和Transformer类型。API:torch.quantization.quantize_dynamic()

torch.quantization.quantize_dynamic(model,qconfig_spec=None,dtype=torch.qint8,mapping=None,inplace=False)

quantize_dynamic把一个float model转化为dynamic quantized model,也就是只有权重被量化的model,dtype可取值float16或qint8。当对整个模型进行转化时,默认只对以下的op进行转化:
.Linear .LSTM .LSTMCell .RNNCell .GRUCell
因为 dynamic quantization只是把权重参数进行量化,而这些 layer 一般参数数量很大,在整个模型中参数量占比极高,因此边际效益高。对其它 layer进行 dynamic quantization 几乎没有实际的意义。

.qconfig_spec
None是默认值;
当qconfig_spec为一个set(),比如:{nn.LSTM, nn.Linear},意思是指定当前模型中的哪些 layer 要被 dynamic quantization;
当qconfig_spec为一个dict(),key 为 submodule 的 name 或 type,value 为 QConfigDynamic 实例。比如:

qconfig_spec = {nn.Linear:default_dynamic_qconfig,
nn.LSTM:default_dynamic_qconfig}

1.3训练后静态量化
最常用的方式。权重是提前量化的,并且基于在校准过程中观察模型表现来预先计算激活张量的比例因子和偏置,CNN是典型的用例。
和训练后动态量化相比,相同点是把网络的权重参数转从 float32 转换为 int8;不同点是,需要把训练集或者和训练集分布类似的数据喂给模型(该过程没有反向传播),然后通过每个 op 输入的分布特点来计算 activation 的量化参数。
步骤:
1、fuse_model
比如:

torch.quantization.fuse_modules(module,['conv1','bn1','relu1'],inplace=True)

一旦合并成功,那么原始网络中的 conv1 就会被替换为新的合并后的 module(因为其是 list 中的第一个元素),而 bn1、relu1(list 中剩余的元素)会被替换为 nn.Identity(),这个模块是个占位符,直接输出输入。
可合并的op如下:

Convolution,Batch Normlization
Convolution,Batch Normlization,Relu
Convolution,Relu
Linear,Relu
Batch Normlization, Relu

2、设置qconfig
qconfig 是要设置到模型或者模型的子 module 上的。qconfig 是 QConfig 的一个实例,QConfig 这个类就是维护了两个 observer,一个是 activation 所使用的 observer,一个是 op 权重所使用的 observer。

model.qconfig = torch.quantization.default_qconfig

3、prepare
prepare 用来给每个子 module 插入 Observer,用来收集和定标数据。以 activation 的 observer 为例,就是期望其观察输入数据得到四元组中的 min_val 和 max_val,至少观察个几百个迭代的数据吧,然后由这四元组得到 scale 和 zp 这两个参数的值。

torch.quantization.prepare(model, inplace=True)

4、喂数据
为了获取数据的分布特点,来更好的计算 activation 的 scale 和 zp。

for data in data_loader: model(data)

5、转化模型
第4步完成后,各个 op 权重的四元组(min_val,max_val,qmin, qmax)中的 min_val,max_val 已经有了,各个 op activation 的四元组(min_val,max_val,qmin, qmax)中的 min_val,max_val 也已经观察出来了。模型参数类型将FP32转成INT8。

1.5训练时量化
训练后量化若不能提供足够的准确性,可以在模块执行训练时量化。计算将在FP32中进行,将值取四舍五入以模拟INT8的量化效果。

模型剪枝

剪枝的两个原因:
(1)参数冗余,去部分参数,在overfitting的情况下可能会带来精度提升,类似dropout。
(2)以牺牲精度换取速度,在精度可接受的前提下,缩小模型结构。
(3)为什么不直接训练小模型,而通过大模型剪枝?我的理解是:大模型的参数量大,捕获的信息更全,然后去掉不重要的权重或结构,留下的信息量依然大;而通过小模型捕获信息的能力有限,无法与剪枝后的模型比拟。

根据粒度的不同,至少可以粗分为4个粒度。
1.细粒度剪枝(fine-grained):即对连接或者神经元进行剪枝,它是粒度最小的剪枝。
2.向量剪枝(vector-level):它相对于细粒度剪枝粒度更大,属于对卷积核内部(intra-kernel)的剪枝。
3.核剪枝(kernel-level):即去除某个卷积核,它将丢弃对输入通道中对应计算通道的响应。
4.滤波器剪枝(Filter-level):对整个卷积核组进行剪枝,会造成推理过程中输出特征通道数的改变。

细粒度剪枝(fine-grained),向量剪枝(vector-level),核剪枝(kernel-level)方法在参数量与模型性能之间取得了一定的平衡,但是网络的拓扑结构本身发生了变化,需要专门的算法设计来支持这种稀疏的运算,被称之为非结构化剪枝。
滤波器剪枝(Filter-level)只改变了网络中的滤波器组和特征通道数目,所获得的模型不需要专门的算法设计就能够运行,被称为结构化剪枝。

剪枝的过程主要分以下几步:
①训练网络;
②评估权重和神经元的重要性:可以用L1、L2来评估权重的重要性,用不是0的次数来衡量神经元的重要性;
③对权重或者神经元的重要性进行排序然后移除不重要的权重或神经元;
④移除部分权重或者神经元后网络的准确率会受到一些损伤,因此我们要进行微调,也就是使用原来的训练数据更新一下参数,往往就可以复原回来;
⑤为了不会使剪枝造成模型效果的过大损伤,我们每次都不会一次性剪掉太多的权重或神经元,因此这个过程需要迭代,也就是说剪枝且微调一次后如果剪枝后的模型大小还不令人满意就回到步骤后迭代上述过程直到满意为止。

在上述过程中,有重要的两个事情:1、如何去衡量权重或结构的重要性。2、剪枝后模型如何恢复精度。

1.1细粒度剪枝
减去连接或神经元。
(1)基于权重的幅度
权重的L1\L2范数越小,就认为这个权重越不重要,值为0的权重无意义。
(2)基于损失函数
从损失函数出发寻找对损失影响最小的神经元。
(3)基于激活值
如果某个神经元输出的激活值总是很小,则认为这个神经元不重要。

2.1粗粒度剪枝
对滤波器或者对特征通道进行裁剪。
(1)基于激活的稀疏性
通道的选择是通过LASSO regression来做的,也就是在损失函数中添加L1范数对权重进行约束,以目标函数优化的角度考虑,L1范数可以使得权重中大部分值为0,使得通道内权重具有稀疏性,从而可以将系数的channel剪掉。
(2)基于输出重建误差
剪掉当前层的若干通道后,重建网络结构使得损失信息最小。

你可能感兴趣的:(模型压缩)