在这里插入图片描述
这是Google在CVPR 2018上发表的一篇int8量化的论文,题目为《Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference》。也是入门量化最经典的论文之一。论文介绍了一种只使用整数运算的量化方式,相比于浮点数运算效率更高。一起先来看看这篇论文吧。论文的axriv地址可以在附录中找到。
模型量化仍然属于模型压缩的范畴,而模型压缩的目的是降低模型的内存大小,加快模型推理速度。在这之前,主要有两方面的研究用于减少模型的大小和前向推理的时间。一是在网络结构上的改进,诸如MobileNet,SqueezeNet,ShuffleNet和DenseNet等等。二是量化权重和激活函数,将32位的浮点数用更低位的数来表示,如half-float,int,bit等等。然而这些方法并没有在一个合理的BaseLine基础上进行评估。这些网络的BaseLine几乎都是选在AlexNet,VGG16,GoogleNet这种大型网络,而这些大型网络在设计时为了达到高准确率存在很多容易,所以在压缩这些网络时都有不小的效果提现。其二在于很量化方法没有在真正的硬件上进行有效性证明。有的方法只在权重上进行量化,仅仅关心设备的存储,而不关心计算效率。有的方法如2-bit/3-bit权重网络和bit-shifit网络,它们把权重限制为0或者,即把乘法操作用二进制移位来实现。但在某些硬件上,二进制移位实现并不比乘法,加法好。并且,只有当Bit大的时候,乘法操作才显得比较"昂贵"。从上面的介绍引出这篇论文的目的,即是要将乘法的输入:权重和激活值都量化成比较小的位宽,即int8量化。
同时,量化一般可以分为两种模式,即训练后量化(post-training-quantizated)以及训练时量化(quantization-aware-training)。训练后量化比较容易理解,即将训练后的模型中的权重从float32量化到int8,并以int8的形式保存,但在实际推理时,还需要反量化为浮点数类型进行计算。这种量化方式在大模型上的效果很好,因为大模型的抗噪能力很强,但在小模型上表现就比较差了。而训练中量化意思是在训练的过程中引入伪量化操作,即在前向传播的时候,采用量化后的权重和激活值,但在反向传播的时候仍然对float类型的权重进行梯度下降,前向推理时全部使用int8的方式进行计算。
这篇论文提出了一种将float32量化为int8的方法,并给出了一个训练和推理框架,推理框架使得模型可以在能执行整型运算的计算设备上高效运行,训练框架和推理框架相辅相成,可以显著降低量化过程中的精度损失。
首先,定义代表量化后的值,代表原始的float32
值,这篇论文抛弃了之前使用查表的方式将浮点数映射为整数的方法,而是直接引入了一个映射关系来表示,如公式(1)所示:
其中代表缩放系数,代表,即真实浮点数映射到整数时所对应的值,和的数据类型一致。对于int8
量化,就是8-bit
整数,对于B-bit
量化,q
就是B-bit
的实数,对于有bias
的情况,就固定量化为·32-bit
的实数。其中的计算方式为:
然后可以表示为:
其中算子表示:
再从公式(1)推导得到反量化公式,这是训练的时候反向传播要用到的:
如果我们用C++里面的结构体来表示这个数据结构,那么就可以写成下面的形式:
可以将卷积层的量化过程总结如下,这部分借鉴了一篇CSDN博主的流程,链接放在附录的参考博客1了。卷积层的量化过程表示为:
lhs_quantized_val
, uint8
类型, 偏移量 lhs_zero_point
, int32
类型。rhs_quantized_val
, uint8
类型, 偏移量 rhs_zero_point
, int32
类型。uint8
到int32
类型。int32_accumulator
求和有溢出的风险,可以换成固定点小数乘法。这部分公式表示为:int32_accumulator += (lhs_quantized_val(i, j) - lhs_zero_point) * (rhs_quantized_val(j, k) - rhs_zero_point)
。quantized_multiplier
, int32
类型和右移次数记录right_shift
, int
类型。将int32_accumulator
右移right_shift
位。int32
结果,仍有溢出风险,可以换为固定点小数乘法。这部分公式表示为:quantized_multiplier * int32_accumulator
。result_zero_point
。right_shift
位还原,得到int32
的结果。max
和min
。值得注意的一点事,如果有连续的层需要进行量化操作时,就没有必要反量化了,如上面的10->11
步骤,但这很有可能带来乘加累积导致的溢出,所以每层量化似乎似乎是比较稳妥的做法。
从公式(1)可以看到,每个中的实数都表示带有一对参数和的实数。则对实数矩阵,做乘法,其结果矩阵的每个实数可以用下面的公式表示:
这个公式可以重写为:
其中:
可以看到是式子(3)中唯一不是整数的值,并且经验发现的值总是在中,所以可以将表示为下面的式子:
其中是非负整数,是一个整数。这样实数运算就变成了整数运算,同时可以用移位运算。这个就是上面介绍的卷积层量化过程中的右移参数。
注意,这里还有一个关键点就是在预测阶段,权重矩阵的量化系数可以通过已有的参数统计出来。而激活层的量化参数是大量训练数据指数移动均值计算出来的,所以这里才会有没出来,但先使用了。
在上面的公式(4)中因为两个矩阵都需要减去各自的零点Z
值,减法运算后得到的值可能会突破int8
范围,到时候就需要int16
来存储,但整个运算为了控制在int8的类型下计算,论文做了下面的变换。
这样可以有效的避免计算过程中的值溢出int8
范围。但可以发现,这个等效变换仍然没有改变整个计算的复杂度,都为。
前面描述了权重的矩阵计算,但在神经网络中还有偏置bias
和激活函数的映射,因为int8
类型的运算完之后的值应该是在int32
之内的,所以bias
选择int32
的类型,这样的选择一是因为bias
在整个神经网络中只占据极少的一部分,此外bias
的作用其实非常重要,高精度的bias
可以降低模型的偏差。因此加上bias
之后就变成了int32
,我们需要再次转换成int8
类型(反量化),之后再进入到激活中。具体如下图所示:
再用公式详细表达一下,定义bias
的量化:
其中,用int32
表示。将weights
和input
执行矩阵乘法后加上bias
,公式表达为:
得到了int32
之后的结果后需要再次转换成int8
类型(反量化),之后再执行激活函数的操作。
在介绍中提到,后处理量化过程适合大模型,而小模型会导致精度损失比较大。论文认为后处理量化主要存在两点问题:
因此,论文提出了一种在前向传播阶段模拟量化的方法,反向传播和平常一样,所有的权重和biases都用浮点数保存以微调小变化。具体的量化方法如下: 1、 weights再输入进行卷积之前就开始量化,如果有bn层,将bn层融入到weights中。 2、 激活在激活函数执行完之后再量化。
如下图所示:
量化的公式如下:
这和上面介绍的推理阶段的量化公式完全一致,我就不再赘述了。
对于上面的量化范围(a, b)
,weight
和activation
是不一样的,对于weight
来说很简单,就是该权重中最大最小值,但是对于activation
是不太一样的, 对于activation
采用了EMA
(滑动平均)来对输入中的最大最小值计算得到的,但是在训练初期,因为输入的值变化较大,会影响到滑动平均的值,因此在初期不对activation
做量化,而是在网络稳定之后再引入。
对于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