A White Paper on Neural Network Quantization--阅读笔记1
- 一、模型量化的意义
- 二、量化主要做什么
- 三、目前量化主要分类
- 四、量化基本知识介绍
- 0、基本知识
- 1、误差来源
- 2、量化范围的设定
- 五、量化方法介绍
- 1、均匀仿射量化(Uniform affine quantization)
- 2、对称均匀量化(Symmetric uniform quantization)
- 2.1 、对称和非对称量化(Symmetric vs. asymmetric quantization)
- 3、二次幂量化(Power-of-two quantizer)
- 4、量化粒度(Quantization granularity)
- 5、量化模拟(Quantization simulation)
- 6、其它层量化(Other layers and quantization)
- 7、批量归一化的折叠(Batch normalization folding)
- 8、激活函数融合(Activation function fusing)
一、模型量化的意义
我们希望把一些大型的网络应用到对功耗和计算有严格要求的边缘设备中,对此降低神经网络在推理过程中的能耗和延迟就非常关键。神经网络量化是实现这些需求最有效的方法之一。
二、量化主要做什么
对权重和激活都使用低位宽表示(这里的激活不是指激活函数,而是指输入/输出的特征图也就是常说的feature map)
三、目前量化主要分类
-
训练后量化(PTQ)Post-Training Quantization
-
量化感知训练(QAT) Quantization
Aware Training
PTQ不需要重新进行训练或者是不需要标签数据,因此这是一种轻量化的量化方法。在大多数情况下PTQ在使用8bit量化时就足以接近浮点模型的精度。QAT需要微调model和带标签的数据,但是可以在更低位宽时取得更有竞争力的结果。
四、量化基本知识介绍
0、基本知识
首先,需要了解硬件上是如何模型推理的。我们指导神经网络满足公式:, y = W x + b y=Wx+b y=Wx+b,以下Fig1展示了神经网络(neural network (NN))加速器中计算矩阵-向量乘法的示意图。
Fig1:
这个结构一般是作为神经网络或者更大的矩阵-向量计算需求中的一个基本模块。这类硬件模块通过尽可能多的并行计算来提高NN的推理效率。这类NN加速器的两个基本组成部分是处理单元(PE) C n , m C_{n,m} Cn,m和累积器(ACC) A n A_n An,在Fig1的示例中,我们有16个处理单元(PE)和4个累加器(ACC)。计算过程为,累加器首先加载偏置值 b n b_n bn,然后我们将权重值 W n , m W_{n,m} Wn,m和输入值 X m X_m Xm 加载到数组中,并在单个循环中计算它们在各个处理元素 C n , m = W n , m X m C_{n,m}=W_{n,m}X_m Cn,m=Wn,mXm 中的乘积,然后将他们的乘积和与累加器中的 b n b_n bn进行累加,我们获得公式1:
A n = b n + ∑ m C n , m − − 公式 1 A_n=b_n+\sum_{m} C{n,m} -- 公式1 An=bn+m∑Cn,m−−公式1
上面说的操作也可以被称为乘累加(MAC)。对于更大的矩阵-向量乘法,这个过程将会被多次执行。一旦所有的循环执行完成,累加器中的结果将会被写回内存中,以便被神经网络的下一层所使用。神经网络通常使用FP32的权重和激活进行训练。如果我们要使用FP32进行推理,那处理单元(PE)和累加器(ACC)就必须支持浮点计算逻辑,同时我们还需要将32bit的数据从内存加载到处理器中。MAC操作和数据传输(加载和写回等)占据了神经网络推理过程中的大部分能耗。因此使用低bit的定点或者量化表示可以获得比较显著的好处。低bit定点例如int8不但减少了数据传输量,同时也降低了MAC的规模和能耗。这是因为数字运算的成本通常和所使用的bit位数成二次线性关系,同时定点加法比浮点加法的效率也更高。
为了从浮点运算转向高效的定点运算,我们需要一个将浮点向量转换为整数的方案。浮点向量
x
x
x可以近似表示为标量乘以整数值的向量。
X
^
=
s
x
.
X
i
n
t
≈
X
\textcolor{blue}{\hat{X}=s_{x}.X_{int}\approx{X}}
X^=sx.Xint≈X
其中
s
x
s_x
sx是浮点比例因子,
X
i
n
t
X_{int}
Xint 是量化后整数向量,例如 INT8。我们将向量的反量化版本表示为
X
^
\hat{X}
X^。通过量化权重和激活,我们可以写出量化版本的积累方程:
A
n
^
=
b
n
^
+
∑
m
W
n
,
m
^
X
m
^
=
b
n
^
+
∑
m
(
s
w
W
n
,
m
i
n
t
^
)
(
s
x
x
m
i
n
t
)
\hat{A_n} = \hat{b_n} + \sum_{m}{\widehat{W_{n,m}}\hat{X_m}}= \hat{b_n} + \sum_{m}(s_w{\widehat{W_{n,m}^{int}})(s_{x}x_{m}^{int})}
An^=bn^+m∑Wn,m
Xm^=bn^+m∑(swWn,mint
)(sxxmint)
= b n ^ + s w s x ∑ m W n , m i n t x m i n t − − 公式 3 =\hat{b_n}+s_ws_x\sum_{m}W_{n,m}^{int}x_m^{int}--公式3 =bn^+swsxm∑Wn,mintxmint−−公式3
请注意,我们对权重 s w s_w sw和激活 s x s_x sx使用了单独的比例因子。这提供了更多的灵活性并减少了量化误差。由于每个比例因子都应用于整个张量,因此这个方案允许我们将比例因子从公式(3)的求和中剔除,并以定点格式进行MAC操作。我们现在有意忽略了偏置(bias)的量化,因为偏置通常以较高的位宽(32位)存储,其比例因子取决于权重和激活的比例(需要进一步研究bias)。
Fig 2 显示了当我们引入量化时神经网络加速器的变化。在我们的示例中,使用 INT8 精度计算,但为了便于讨论,这可以是任何量化格式。保持较高的累加器位宽是很重要的。
在下一层使用之前,累加器中计算的32bit激活需要被写入内存,为了减少数据传输和下一层操作的复杂性,这些激活被量化为int8,这需要重新量化(requantization)的步骤,在此我们可以获得保存每一层的激活所对应的 s r q s_{rq} srq比例因子。
Fig2:
1、误差来源
接下来会有详细介绍
截断误差 Truncation Error (Clipping Error) TD
舍入误差 Rounding Error
2、量化范围的设定
1.量化范围的设置中 确定合理的截断阈值Qmin和Qmax,来权衡截断误差和舍入误差对量化的影响。
使用MSE均方误差
权重:通常不需要校准数据进行量化 (训练后权重就固定了)
激活:确定激活量化参数通常需要几批校准数据 (激活是与输入的数据有关)
五、量化方法介绍
1、均匀仿射量化(Uniform affine quantization)
均匀仿射量化也被称为非对称量化(asymmetric quantization),由三个量化参数定义:比例因子s 零点z(ZP)和比特宽度b(在实际操作的时候一般是没有位宽这个选项的,因为大多硬件已经定好了支持8bit还是4bit,不能支持任意bit的选择)。比例因子s和零点z用于将浮点值映射到整数范围内,其大小取决于位宽b。比例因子通常以浮点数表示,同时比例因子其实指代了量化器的步长。零点是一个整数,确保真实零点(真实的0)的量化没有错误。这对于确保像zero padding或ReLU这样的常规操作不会引起量化错误是很重要的。
这s、z,b三个量化参数被确定,我们就可以进行量化操作了。从一个实数向量
x
x
x开始,我们首先将其映射到无符号整数网格{0,…,2b-1}。
X
i
n
t
=
c
l
a
m
p
(
⌊
x
s
⌉
+
z
)
;
X
i
n
t
⊏
(
0
,
2
b
−
1
)
−
−
公式
4
X_{int}=clamp(\lfloor{\cfrac{x}{s}}\rceil+z);X_{int}\sqsubset{(0,2^b-1)}--公式4
Xint=clamp(⌊sx⌉+z);Xint⊏(0,2b−1)−−公式4
其中
⌊
.
⌉
\lfloor{.}\rceil
⌊.⌉是四舍五入取整(round-to-neares)。clamp()被定义为:
c
l
a
m
p
(
x
;
a
,
c
)
=
{
a
,
if
x
<
a
x
,
if
a
≤
x
≤
c
c
,
if
x
>
c
clamp(x;a,c)=\begin{cases} a, &\text{if } x<a \\ x, &\text{if } a\leq{x}\leq{c}\\ c, &\text{if }x>c \end{cases}
clamp(x;a,c)=⎩
⎨
⎧a,x,c,if x<aif a≤x≤cif x>c
为了得到接近真实输入的实数值,我们定义了一个反量化(de-quantization)操作:
x
≈
X
^
=
s
(
X
i
n
t
−
z
)
x\approx{\hat{X}=s(X_{int}-z)}
x≈X^=s(Xint−z)
结合上述两个步骤,我们可以为量化函数 q(·) 提供一般定义,此时量化函数q(·) 表示的是反量化后的浮点函数,如下所示:
X
^
=
q
(
X
;
s
,
z
,
b
)
=
s
[
c
l
a
m
p
(
⌊
x
s
⌉
+
z
)
−
z
]
\hat{X}=q(X;s,z,b)=s[clamp(\lfloor{\cfrac{x}{s}}\rceil+z)-z]
X^=q(X;s,z,b)=s[clamp(⌊sx⌉+z)−z]
通过以上反量化步骤,我可以定义量化范围的极限
(
q
m
i
n
,
q
m
a
x
)
(q_{min},q_{max})
(qmin,qmax),其中qmin = -sz,
q
m
a
x
=
s
(
2
b
−
1
−
z
)
q_{max}=s(2^b-1-z)
qmax=s(2b−1−z)。任何超过这个范围的输入
x
x
x都将会被截断到这个范围内,这个操作会导致一个截断误差(clipping error)。如果我们想减少截断误差,可以通过增大比例因子s从而扩大量化范围的方法来实现。然而增大比例因子s会导致舍入误差(rounding error)增加,因为舍入误差的范围是
[
−
1
2
s
,
1
2
s
]
[-\cfrac{1}{2}s, \cfrac{1}{2}s]
[−21s,21s]。接下来我们更详细地探讨了如何选择量化参数来实现截断误差和舍入误差之间权衡(trade-off )。量化分布如Fig3:
2、对称均匀量化(Symmetric uniform quantization)
对称量化将零点z限制为真实的0,这样就减少了公式4中进行累加操作时对零点z的额外计算开销。但由于缺少了偏移量,这限制了整数和浮点数的映射范围,因此选择有符号整数还是无符号整数就很关键。
X
^
=
s
X
i
n
t
X
i
n
t
=
c
l
a
m
p
(
⌊
x
s
⌉
)
;
(
0
,
2
b
−
1
)
unsigned int
X
i
n
t
=
c
l
a
m
p
(
⌊
x
s
⌉
)
;
(
−
2
b
−
1
,
2
b
−
1
−
1
)
signed int
\hat{X}=sX_{int}\\ X_{int}=clamp(\lfloor{\cfrac{x}{s}\rceil});\text{ }(0,2^b-1 )\text{ unsigned int } \\ X_{int}=clamp(\lfloor{\cfrac{x}{s}\rceil});\text{ }(-2^{b-1},2^{b-1}-1)\text{ signed int }
X^=sXintXint=clamp(⌊sx⌉); (0,2b−1) unsigned int Xint=clamp(⌊sx⌉); (−2b−1,2b−1−1) signed int
无符号对称量化非常适用于单尾分布如ReLU激活(如Fig4)。另一方面,有符号对称量化可以被用于大致关于零对称分布的数据:
2.1 、对称和非对称量化(Symmetric vs. asymmetric quantization)
对于所有模型中权重和激活,我们都需要选择合适一个量化方案。
非对称量化可以有更好的表达能力,因为它包含一个额外的偏移参数,但是,这也会导致更多的计算开销。
要知道为什么会导致额外的计算开销,需要了解清楚非对称权重 W ^ = s w ( W i n t − z w ) \widehat{W}=s_w(W_{int}-z_w) W =sw(Wint−zw)与非对称激活 X ^ = s x ( x i n t − z x ) \widehat{X}=s_x(x_{int}-z_x) X =sx(xint−zx)相乘时会发生什么:
W ^ X ^ = s w ( W i n t − z w ) s x ( x i n t − z x ) = s w s x W i n t x i n t − s w z w s x x i n t − s w s x z x W i n t + s w z w s x z x \widehat{W}\widehat{X}=s_w(W_{int}-z_w)s_x(x_{int}-z_x)\\ =s_ws_xW_{int}x_{int}-\textcolor{red}{s_wz_ws_xx_{int}}-\textcolor{blue}{s_{w}s_{x}z_{x}W_{int}}+s_wz_ws_xz_x W X =sw(Wint−zw)sx(xint−zx)=swsxWintxint−swzwsxxint−swsxzxWint+swzwsxzx
如果权重和激活都采用对称量化,第一项是我们必须有的**。第三和第四项只取决于预先知道的比例s、零点z和权重值,因此这两个部分可以被提前计算并添加到层的偏置中去,不需要额外的推理时计算成本**。然而第二项还取决月输入数据X,这意味着每次计算都将会导致更大的延迟和功耗开销,因为这部分相对于对称量化是额外的一部分。
由于这个原因对激活使用非对称量化(asymmetric activation quantization),对权重使用对称量化(symmetric weight quantization)是一种常见的方法,这可以避免额外的数据依赖项。
(然而要上硬件的话,这样的选择可能不一定会支持,这会导致加载的数据不对等。可能采用的都是对称量化。)
3、二次幂量化(Power-of-two quantizer)
二次幂量化是对称量化的一个特例(所以零点就是真实的0),其中比例因子被限制为二次幂, s = 2 ( − k ) s=2^{(-k)} s=2(−k)。这种选择可以提升硬件的计算效率,因为s的缩放操作对应于简单的位移。然而比例因子s的限制性表达可能会使截断误差和舍入误差之间的权衡变得复杂。
4、量化粒度(Quantization granularity)
目前为止我们为每个张量(tensor)定义了一组量化参数分别是权重量化参数和激活量化参数如公式3所示,这被称为按张量量化(也有叫per layer按层量化,对比的就是后面会提到的按通道量化)。我们还可以为张量的每个部分(例如权重的输入通道)定义一个单独的量化器,从而提高量化的粒度。在神经网络量化中,按tensor量化是最常见的量化粒度选择,因为它的硬件实现更简单:公式(3)中的所有累加器相同层Tensor都使用相同的 s w , s x s_w,s_x sw,sx比例因子。然而我们可以使用更细的量化粒度来进一步提高性能。例如对于权重量化,我们可以为每个输出通道指定一个不同的量化器。这就是所谓的按通道量化(per channel)。
其它工作采用了比per channel更细粒度的量化方案,使用按组(per group)对权重或者激活进行量化。增加组的粒度通常可以提升量化的准确性,但是要额外付出一些计算开销。额外开销的多少和具体硬件对累加器的实现有关。目前大多数的定点累加器都不支持这类操作。
5、量化模拟(Quantization simulation)
卷积层量化前向传递的示意图:a)计算图实际设备上的量化推理。 b) 通用量化推理的模拟浮点硬件。
Fig5:
Fig5 展示了在设备端进行推理时所有的输入(偏置、权重、激活的输入)都是定点格式。然而我们常见的深度学习训练框架和通用的硬件模拟设备在进行这些操作时都是采用的浮点。这就是为什么我们要加入量化器来引入量化效果的原因。
Fig6:模拟端输入给下一层的激活是int8
Fig6展示了如何在深度学习框架中对同一卷积层进行建模的方法。在权重和卷积之间添加量化器来模拟权重的量化,在激活之后添加量化器来模拟激活量化。偏置通常不量化,因为它们以更高精度进行存储。量化器实现了公式7的量化函数,每个量化器都由一组参数来进行定义(比例因子s、零点z、位宽b),量化器的输入输出都是浮点格式的,但是输出是限制在量化范围以内的。
6、其它层量化(Other layers and quantization)
在神经网络中还有许多其他类型的层被使用。如何对这些层进行建模,在很大程度上取决于具体的硬件实现。有时模拟量化和目标性能之间的不匹配就是因为这些层没有被正确量化。 在这里提供一些指导,说明如何为几个常用的层进行模拟量化。
最大值池化(Max pooling): 激活量化是不需要的,因为输入和输出的范围是一致的
均值池化(Average pooling): 整数的平均不一定是整数,因此需要在平均之后增加一个量化步骤。我们对输入和输出使用相同的量化器,但是求平均不会显著改变量化后值的范围。
逐点相加(Element-wise addition): 尽管计算行为很简单,但是这个操作确很难准确的进行模拟。在计算的时候两个输入的量化范围必须要完全匹配。如果输入的量化范围不匹配,就需要格外的注意才能确保计算能正确的执行。 因此没有公认的解决方案。
连接(Concatenation): 被连接的两个分支(两个是泛指)通常不共享量化参数,这意味着它们的量化范围不一定会重叠,因此再量化步骤可能是需要的。与逐点相加一样,你可以对网络进行优化(fine-tuning)以使得多个连接分支可以共享量化参数。
7、批量归一化的折叠(Batch normalization folding)
批量归一化是现代卷积网络的一个标准组件,首先,对线性输出层进行BN归一化,然后缩放(scale)和添加偏置(offset)。在端上推理时这个操作可以被融合到前一个或者后一个线性层中去,这就被称为批量归一化折叠(batch normalization folding)。对于此种方法相当于从网络中完全删除了批量归一操作,因为这个计算被吸收到相邻的线性层之中了。**这样操作的优点:除了减少额外的缩放和偏移计算,这个操作还可以省去额外的数据搬移和输出层的量化。**通常来说在推理过程中BN被定义为一个关于输出x的映射:
B
a
t
c
h
B
o
r
m
(
x
)
=
γ
(
x
−
μ
σ
2
+
ϵ
)
+
β
BatchBorm(x)=\gamma(\cfrac{x-\mu}{\sqrt{\sigma^2+\epsilon}})+\beta
BatchBorm(x)=γ(σ2+ϵx−μ)+β
其中
μ
\mu
μ 和
σ
\sigma
σ 是训练期间计算的均值和方差,作为批处理统计的指数移动平均值,
γ
\gamma
γ 和
β
\beta
β是每个通道学习的映射超参数。如果在线性层之后立即应用批量归一化y = BatchNorm(Wx),我们可以重写这部分操作,使批量归一化操作与线性层本身融合。假设一个权重矩阵
W
∈
R
(
n
×
m
)
W∈R^{(n×m)}
W∈R(n×m),我们对每个输出
y
k
for
k
=
{
1
,
.
.
.
,
n
}
y_k \text{ for }k=\{1,...,n\}
yk for k={1,...,n}应用批归一化。
y
k
=
B
a
t
c
h
N
o
r
m
(
W
k
.
x
)
=
γ
k
(
W
k
.
x
−
μ
k
σ
k
2
+
ϵ
)
+
β
k
=
γ
k
W
k
σ
k
2
+
ϵ
.
x
+
(
β
k
−
γ
k
μ
k
σ
k
2
+
ϵ
)
=
W
k
~
.
x
+
b
k
~
y_k=BatchNorm(W_k.x) \\ =\gamma_k(\cfrac{W_k.x-\mu_{k}}{\sqrt{\sigma_{k}^2+\epsilon}})+\beta_{k}\\ =\cfrac{\gamma_{k}W_k}{\sqrt{\sigma_{k}^2+\epsilon}} .x+(\beta_k-\cfrac{\gamma_k\mu_k}{\sqrt{\sigma_{k}^2+\epsilon}})\\ =\widetilde{W_k}.x+\tilde{b_k}
yk=BatchNorm(Wk.x)=γk(σk2+ϵWk.x−μk)+βk=σk2+ϵγkWk.x+(βk−σk2+ϵγkμk)=Wk
.x+bk~
W k ~ = γ k W k σ k 2 + ϵ b k ~ = β k − γ k μ k σ k 2 + ϵ \widetilde{W_k} =\cfrac{\gamma_{k}W_k}{\sqrt{\sigma_{k}^2+\epsilon}} \\ \tilde{b_k}=\beta_k-\cfrac{\gamma_k\mu_k}{\sqrt{\sigma_{k}^2+\epsilon}} Wk =σk2+ϵγkWkbk~=βk−σk2+ϵγkμk
8、激活函数融合(Activation function fusing)
例如Relu、sigmoid、swish
在简单量化加速器的部分,我们看到再量化是在矩阵乘法或者卷积计算之后。然而在实际情况中,通常是在卷积之后一般会有个激活函数。对于,将线性层的结果写回内存然后又加载到非线性层进行计算,这个操作是很浪费的。因此许多硬件方案都会在再量化之前应用非线性操作。
对于以上方案,我们只需要模拟非线性操作之后的数据增加再量化操作即可。
例如relu的非线性操作就很容易被再量化模块所模拟,因为你只需要将激活的最小值量化值设置为0即可。
对于其它更复杂的激活函数例如sigmoid或者swish则需要更多的专门的支持(一部分硬件对这类复杂的函数会采用泰勒展开,然后计算几次方以内的结果,还有的可能直接用查找表(LUT)来实现),如果没有专门的支持那我们就需要在非线性操作之前添加一个量化操作,并且非线性操作后再添加一个量化操作。以上操作可能会对量化模型的准确性有比较大的影响,虽然像swish这类比较新颖的激活函数可能会提高一些浮点下的精度。但这部分提高可能会在量化后消失,或者是在定点硬件上部署时推理效率降低。