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或者 2 n 2^n 2n,即把乘法操作用二进制移位来实现。但在某些硬件上,二进制移位实现并不比乘法,加法好。并且,只有当Bit大的时候,乘法操作才显得比较"昂贵"。从上面的介绍引出这篇论文的目的,即是要将乘法的输入:权重和激活值都量化成比较小的位宽,即int8量化。
同时,量化一般可以分为两种模式,即训练后量化(post-training-quantizated)以及训练时量化(quantization-aware-training)。训练后量化比较容易理解,即将训练后的模型中的权重从float32量化到int8,并以int8的形式保存,但在实际推理时,还需要反量化为浮点数类型进行计算。这种量化方式在大模型上的效果很好,因为大模型的抗噪能力很强,但在小模型上表现就比较差了。而训练中量化意思是在训练的过程中引入伪量化操作,即在前向传播的时候,采用量化后的权重和激活值,但在反向传播的时候仍然对float类型的权重进行梯度下降,前向推理时全部使用int8的方式进行计算。
3. 方法
这篇论文提出了一种将float32量化为int8的方法,并给出了一个训练和推理框架,推理框架使得模型可以在能执行整型运算的计算设备上高效运行,训练框架和推理框架相辅相成,可以显著降低量化过程中的精度损失。
3.1 量化推理
3.1.1 量化方案
首先,定义
q
q
q代表量化后的值,
r
r
r代表原始的float32
值,这篇论文抛弃了之前使用查表的方式将浮点数映射为整数的方法,而是直接引入了一个映射关系来表示,如公式(1)所示:
其中
S
S
S代表缩放系数,
Z
Z
Z代表
z
e
r
o
−
p
o
i
n
t
zero-point
zero−point,即真实浮点数
0
0
0映射到整数时所对应的值,和
q
q
q的数据类型一致。对于int8
量化,
q
q
q就是8-bit
整数,对于B-bit
量化,q
就是B-bit
的实数,对于有bias
的情况,就固定量化为·32-bit
的实数。其中
S
S
S的计算方式为:
S = a r r a y m a x − a r r a y m i n 2 b i t w i d t h − 1 S=\frac{array_{max}-array_{min}}{2^{bit_width}-1} S=2bitwidth−1arraymax−arraymin
然后
Z
Z
Z可以表示为:
Z
=
r
o
u
n
d
(
−
a
r
r
a
y
m
i
n
S
)
Z = round(-\frac{array_{min}}{S})
Z=round(−Sarraymin)
其中
r
o
u
n
d
round
round算子表示:
r
o
u
n
d
(
x
)
=
{
0
,
x
<
0
⌊
x
⌉
,
0
<
⌊
x
⌉
<
2
n
−
1
2
n
−
1
x
>
2
n
−
1
round(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⌉,2n−1x<00<⌊x⌉<2n−1x>2n−1
再从公式(1)推导得到反量化公式,这是训练的时候反向传播要用到的:
q
=
r
o
u
n
d
(
Z
+
r
S
)
q=round(Z+\frac{r}{S})
q=round(Z+Sr)
如果我们用C++里面的结构体来表示这个数据结构,那么就可以写成下面的形式:
可以将卷积层的量化过程总结如下,这部分借鉴了一篇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)可以看到,每个 a r r a y array array中的实数 r i r_i ri都表示带有一对参数 S S S和 Z Z Z的实数 q i q_i qi。则对实数矩阵 R 1 R_1 R1, R 2 R_2 R2做乘法,其结果矩阵的每个实数可以用下面的公式表示:
这个公式可以重写为:
其中:
可以看到
M
M
M是式子(3)中唯一不是整数的值,并且经验发现
M
M
M的值总是在
(
0
,
1
)
(0,1)
(0,1)中,所以可以将
M
M
M表示为下面的式子:
其中
n
n
n是非负整数,
M
0
M_0
M0是一个整数。这样实数运算就变成了整数运算,同时
2
−
n
2^{-n}
2−n可以用移位运算。这个
n
n
n就是上面介绍的卷积层量化过程中的右移参数。
注意,这里还有一个关键点就是在预测阶段,权重矩阵的量化系数 S S S可以通过已有的参数统计出来。而激活层的量化参数是大量训练数据指数移动均值计算出来的,所以这里才会有 q 3 q_3 q3没出来,但先使用了 S 3 S_3 S3。
3.1.3 零点的有效处理
在上面的公式(4)中因为两个矩阵都需要减去各自的零点Z
值,减法运算后得到的值可能会突破int8
范围,到时候就需要int16
来存储,但整个运算为了控制在int8的类型下计算,论文做了下面的变换。
这样可以有效的避免计算过程中的值溢出int8
范围。但可以发现,这个等效变换仍然没有改变整个计算的复杂度,都为
O
(
2
N
3
)
O(2N^3)
O(2N3)。
3.1.4 融合一个层
前面描述了权重的矩阵计算,但在神经网络中还有偏置bias
和激活函数的映射,因为int8
类型的运算完之后的值应该是在int32
之内的,所以bias
选择int32
的类型,这样的选择一是因为bias
在整个神经网络中只占据极少的一部分,此外bias
的作用其实非常重要,高精度的bias
可以降低模型的偏差。因此加上bias
之后就变成了int32
,我们需要再次转换成int8
类型(反量化),之后再进入到激活中。具体如下图所示:
再用公式详细表达一下,定义bias
的量化:
其中,
S
b
i
a
s
S_{bias}
Sbias用
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
欢迎关注我的微信公众号GiantPandaCV,期待和你一起交流机器学习,深度学习,图像算法,优化技术,比赛及日常生活等。