深度学习算法优化系列 | Google CVPR2018 int8量化算法

1. 前言
这是Google在CVPR 2018上发表的一篇int8量化的论文,题目为《Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference》。也是入门量化最经典的论文之一。论文介绍了一种只使用整数运算的量化方式,相比于浮点数运算效率更高。一起先来看看这篇论文吧。论文的axriv地址可以在附录中找到。

2. 背景
模型量化仍然属于模型压缩的范畴,而模型压缩的目的是降低模型占用的内存大小,加快模型推理速度。在这之前,主要有两方面的研究用于减少模型的大小和前向推理的时间。一是在网络结构上的改进,诸如MobileNet,SqueezeNet,ShuffleNet和DenseNet等等。二是量化权重和激活函数,将32位的浮点数用更低位的数来表示,如half-float,int,bit等等。然而这些方法并没有在一个合理的BaseLine基础上进行评估。这些网络的BaseLine几乎都是选在AlexNet,VGG16,GoogleNet这种大型网络,而这些大型网络在设计时为了达到高准确率存在很多冗余,所以在压缩这些网络时都有不小的效果体现。其二在于量化方法没有在真正的硬件上进行有效性证明。有的方法只在权重上进行量化,仅仅关心设备的存储,而不关心计算效率。有的方法如2-bit/3-bit权重网络和bit-shifit网络,它们把权重限制为0或者2n2^n2 
n
 ,即把乘法操作用二进制移位来实现。但在某些硬件上,二进制移位实现并不比乘法,加法好。并且,只有当Bit大的时候,乘法操作才显得比较"昂贵"。从上面的介绍引出这篇论文的目的,即是要将乘法的输入:权重和激活值都量化成比较小的位宽,即int8量化。

同时,量化一般可以分为两种模式,即训练后量化(post-training-quantizated)以及训练时量化(quantization-aware-training)。训练后量化比较容易理解,即将训练后的模型中的权重从float32量化到int8,并以int8的形式保存,但在实际推理时,还需要反量化为浮点数类型进行计算。这种量化方式在大模型上的效果很好,因为大模型的抗噪能力很强,但在小模型上表现就比较差了。而训练中量化意思是在训练的过程中引入伪量化操作,即在前向传播的时候,采用量化后的权重和激活值,但在反向传播的时候仍然对float类型的权重进行梯度下降,前向推理时全部使用int8的方式进行计算。

3. 方法
这篇论文提出了一种将float32量化为int8的方法,并给出了一个训练和推理框架,推理框架使得模型可以在能执行整型运算的计算设备上高效运行,训练框架和推理框架相辅相成,可以显著降低量化过程中的精度损失。

3.1 量化推理
3.1.1 量化方案
首先,定义qqq代表量化后的值,rrr代表原始的float32值,这篇论文抛弃了之前使用查表的方式将浮点数映射为整数的方法,而是直接引入了一个映射关系来表示,如公式(1)所示:


其中SSS代表缩放系数,ZZZ代表zero−pointzero-pointzero−point,即真实浮点数000映射到整数时所对应的值,和qqq的数据类型一致。对于int8量化,qqq就是8-bit整数,对于B-bit量化,q就是B-bit的实数,对于有bias的情况,就固定量化为·32-bit的实数。其中SSS的计算方式为:

S=arraymax−arraymin2bitwidth−1S=\frac{array_{max}-array_{min}}{2^{bit_width}-1}S= 

bit 
w
​    
 idth
 −1
array 
max
​    
 −array 
min
​    
 
​    
 

然后ZZZ可以表示为:
Z=round(−arrayminS)Z = round(-\frac{array_{min}}{S})Z=round(− 
S
array 
min
​    
 
​    
 )

其中roundroundround算子表示:
round(x)=⎧⎩⎨0,⌊x⌉,2n−1x<00<⌊x⌉<2n−1x>2n−1round(x)= \left\{\begin{aligned}0 ,& &x<0\\⌊x⌉ ,& & 0<⌊x⌉ <2^n-1\\2^n-1& & x>2^n-1\end{aligned}\right.
round(x)= 





​    
  
0,
⌊x⌉,

n
 −1
​    
  
​    
  
x<0
0<⌊x⌉<2 
n
 −1
x>2 
n
 −1
​    
 

再从公式(1)推导得到反量化公式,这是训练的时候反向传播要用到的:
q=round(Z+rS)q=round(Z+\frac{r}{S})q=round(Z+ 
S
r
​    
 )

如果我们用C++里面的结构体来表示这个数据结构,那么就可以写成下面的形式:

扫描二维码关注公众号,回复: 14725226 查看本文章

可以将卷积层的量化过程总结如下,这部分借鉴了一篇CSDN博主的流程,链接放在附录的参考博客1了。卷积层的量化过程表示为:

1、输入 量化的特征图lhs_quantized_val, uint8类型, 偏移量 lhs_zero_point, int32类型。

2、输入 量化的卷积核rhs_quantized_val, uint8类型, 偏移量 rhs_zero_point, int32类型。

3、转换uint8到int32类型。

4、每一块卷积求和,注意int32_accumulator求和有溢出的风险,可以换成固定点小数乘法。这部分公式表示为:
int32_accumulator += (lhs_quantized_val(i, j) - lhs_zero_point) * (rhs_quantized_val(j, k) - rhs_zero_point)。

5、输入量化的乘子quantized_multiplier, int32类型和右移次数记录right_shift, int类型。将int32_accumulator右移right_shift位。

6、计算乘法,得到int32结果,仍有溢出风险,可以换为固定点小数乘法。这部分公式表示为:quantized_multiplier * int32_accumulator。

7、加上结果的偏移量 result_zero_point。

8、左移right_shift位还原,得到int32的结果。

9、将int32类型结果 限幅到[0, 255], 再强制转换到 uint8类型。

10、之后再反量化到浮点数,更新统计输出值分布信息max和min。

11、再量化回uint8。

12、之后量化激活层。

13、最后反量化到浮点数,即卷积层的输出。

14、进入下一层,循环执行1-13步骤。

值得注意的一点事,如果有连续的层需要进行量化操作时,就没有必要反量化了,如上面的10->11步骤,但这很有可能带来乘加累积导致的溢出,所以每层量化似乎似乎是比较稳妥的做法。

3.1.2 纯整数算术矩阵乘法
从公式(1)可以看到,每个arrayarrayarray中的实数rir_ir 
i
​    
 都表示带有一对参数SSS和ZZZ的实数qiq_iq 
i
​    
 。则对实数矩阵R1R_1R 
1
​    
 ,R2R_2R 
2
​    
 做乘法,其结果矩阵的每个实数可以用下面的公式表示:


这个公式可以重写为:


其中:


可以看到MMM是式子(3)中唯一不是整数的值,并且经验发现MMM的值总是在(0,1)(0,1)(0,1)中,所以可以将MMM表示为下面的式子:


其中nnn是非负整数,M0M_0M 
0
​    
 是一个整数。这样实数运算就变成了整数运算,同时2−n2^{-n}2 
−n
 可以用移位运算。这个nnn就是上面介绍的卷积层量化过程中的右移参数。

注意,这里还有一个关键点就是在预测阶段,权重矩阵的量化系数SSS可以通过已有的参数统计出来。而激活层的量化参数是大量训练数据指数移动均值计算出来的,所以这里才会有q3q_3q 
3
​    
 没出来,但先使用了S3S_3S 
3
​    
 。

3.1.3 零点的有效处理
在上面的公式(4)中因为两个矩阵都需要减去各自的零点Z值,减法运算后得到的值可能会突破int8范围,到时候就需要int16来存储,但整个运算为了控制在int8的类型下计算,论文做了下面的变换。


这样可以有效的避免计算过程中的值溢出int8范围。但可以发现,这个等效变换仍然没有改变整个计算的复杂度,都为O(2N3)O(2N^3)O(2N 
3
 )。

3.1.4 融合一个层
前面描述了权重的矩阵计算,但在神经网络中还有偏置bias和激活函数的映射,因为int8类型的运算完之后的值应该是在int32之内的,所以bias选择int32的类型,这样的选择一是因为bias在整个神经网络中只占据极少的一部分,此外bias的作用其实非常重要,高精度的bias可以降低模型的偏差。因此加上bias之后就变成了int32,我们需要再次转换成int8类型(反量化),之后再进入到激活中。具体如下图所示:


再用公式详细表达一下,定义bias的量化:
其中,SbiasS_{bias}S 
bias
​    
 用int32表示。将weights和input执行矩阵乘法后加上bias,公式表达为:
得到了int32之后的结果后需要再次转换成int8类型(反量化),之后再执行激活函数的操作。

4. 模拟量化训练
在介绍中提到,后处理量化过程适合大模型,而小模型会导致精度损失比较大。论文认为后处理量化主要存在两点问题:

同一层不同通道的权重分布尺度差很多(超过100x)
离散的权重会导致所有剩余权重的精度下降
因此,论文提出了一种在前向传播阶段模拟量化的方法,反向传播和平常一样,所有的权重和biases都用浮点数保存以微调小变化。具体的量化方法如下:
1、 weights再输入进行卷积之前就开始量化,如果有bn层,将bn层融入到weights中。
2、 激活在激活函数执行完之后再量化。

如下图所示:


量化的公式如下:

这和上面介绍的推理阶段的量化公式完全一致,我就不再赘述了。

4.1 学习量化范围
对于上面的量化范围(a, b),weight和activation是不一样的,对于weight来说很简单,就是该权重中最大最小值,但是对于activation是不太一样的, 对于activation采用了EMA(滑动平均)来对输入中的最大最小值计算得到的,但是在训练初期,因为输入的值变化较大,会影响到滑动平均的值,因此在初期不对activation做量化,而是在网络稳定之后再引入。

4.2 折叠BN
对于bn层,在训练时是一个单独的层存在,但是在前向推理时为了提升效率是融合到卷积或全连接层的权重和偏置中的,如下图:

所以,为了模拟推断过程,训练时需要把BN层考虑到权重中,公式如下:


考虑了fold bn之后,最终可以用下图来表示训练过程中的量化:

后记
今天解读了CVPR 2018 《Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference》,对int8量化了有了基本认识,这两天随缘更新一个实战篇吧。

附录
论文原文:https://arxiv.org/pdf/1712.05877.pdf

参考博客1:https://blog.csdn.net/qq_19784349/article/details/82883271

参考博客2:https://blog.csdn.net/holmosaint/article/details/82423610
————————————————
版权声明:本文为CSDN博主「just_sort」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/just_sort/article/details/103704975

猜你喜欢

转载自blog.csdn.net/mp817/article/details/107180753