量化方案原理讲解

前面提到了量化就是将基于浮点数据类型的模型转换为定点数进行运算,其核心就是如何用定点数去表示模型中的浮点数,以及如何用定点运算去表示对应的浮点运算。 以float32转uint8为例,一种最简单的转换方法是直接舍去float32的小数部分,只取其中的整数部分,并且对于超出(0,255)表示范围的值用0或者255表示。 这种方案显然是不合适的,尤其是深度神经网络经过bn处理后,其中间层输出基本都是0均值,1方差的数据范围,在这种方案下因为小数部分被舍弃掉了,会带来大量的 精度损失。并且因为(0,255)以外的部分被clip到了0或255,当浮点数为负或者大于255的情况下,会导致巨大的误差。

这个方案比较接近我们常见编程语言中的类型转换逻辑,我们可以把它称之为类型转换方案。上面的分析可以看出,类型转换方案对于过大或者过小的数据都会产生较大的精度损失。

目前主流的浮点转定点方案基本采用均匀量化,因为这种方案对推理更友好。将一个浮点数根据其值域范围,均匀的映射到一个定点数的表达范围上。

均匀量化方案

我们假设一个浮点数x的值域范围为$\{x_{min}, x_{max}\}$,要转换到一个表达范围为(0,255)的8bit定点数的转换公式如下

$$x_{int} = round(x/s) + z$$ $$x_{Q} = clamp(0,255,x_{int})$$

其中$s$为scale,也叫步长,是个浮点数。$z$为零点,即浮点数中的0,是个定点数。 $$scale = (x_{max} - x_{min}) / 255$$ $$z = round(0 - x_{min}) / 255$$

由上可以看出均匀量化方案对于任意的值域范围都能表达相对不错的性能,不会存在类型转换方案的过小值域丢精度和过大值域无法表示的情况。 代价是需要额外引入零点$z$和值域$s$两个变量。同时我们可以看出,均匀量化方案因为$round$和$clamp$操作也是存在精度损失的,所以会对模型的性能产生影响。 如何减轻数据从浮点转换到定点的精度损失,是整个量化研究的重点。

注意零点很重要,因为我们的网络模型的padding,relu等运算对于0比较敏感,需要被正确量化才能保证转换后的定点运算的正确性。当浮点数的值域范围不包含零点的时候,为了保证正确量化,我们需要对其值域范围进行一定程度的缩放使其可以包含0点

均匀量化方案对应的反量化公式如下 $$x_{float} = (x_{Q} - z) * s$$

所以经过量化和反量化之后的浮点数与原来的浮点数存在一定的误差,这个过程的差异可以查看下图。量化对我们网络模型的参数进行了离散化,这种操作对于模型最终点数的影响程度取决于我们模型本身的参数分布与均匀分布的差异 此处需要插入图片,

接下来我们来看看如何用经过量化运算的定点卷积运算去表示一个原始的浮点卷积操作

\begin{aligned} conv(x, w) &= conv((x_{Q} - z_{x}) * s_{x}, (w_{Q} - z_{w}) * s_{w}) \\ &= s_{x}s_{w} conv(x_{Q} - z_{x},w_{Q} - z_{w} ) \\ &= s_{x}s_{w} (conv(x_{Q}, w_{Q}) - z_{x} \sum_{k,l,m}x_{Q} - z_{w}\sum_{k,l,m,n}w_{Q} + z_{x}z_{w}) \end{aligned}

其中$k,l,m,n$分别是$kernel\_size,output\_channel$和$input\_channel$的遍历下标。可以看出,当卷积的输入和参数的zero_point都是0的时候,浮点卷积将简化成

$$ conv(x, w) = s_{x}s_{w} (conv(x_{Q}, w_{Q})) $$ 即定点的卷积运算结果和实际输出只有一个scale上的偏差,大大的简化了定点的运算逻辑, 所以大部分情况下我们都是使用对称均匀量化。

当我们把定点量化对应的$zero\_point$固定在整型的0处时,便是对称均匀量化。我们以int8的定点数为例 (选取int8只是为了看上去更对称一些,选取uint8也是可以的), 量化公式如下

\begin{aligned} scale &= max(abs(x_{min}), abs(x_{max})) / 127 \\ x_{int} &= round(x/s) \\ x_{Q} &= clamp(-128,127,x_{int}) \end{aligned}

出于利用更快的SIMD实现的目的,我们会把卷积的weight的定点范围表示成(-127,127),对应的反量化操作为

$$ x_{float} = x_{Q}*s $$

由此可见,对称均匀量化的量化和反量化操作会更加的便捷一些 除此之外还有随机均匀量化等别的量化手段,因为大部分情况下我们都采用对称均匀量化,这里不再展开描述。

注解

megengine在用simd指令实现量化时,有部分kernel使用了16-bit的累加器去存储a*b+c*d的值(即乘法的结果累加一次的值), 这里的a,b,c,d都是qint8,不难发现,以上值当且仅当a,b,c,d都是-128时有可能会溢出,只要避开这种情况就不会有溢出的问题。 由于a,b,c,d中必然有两个值是weight,因此我们传统上的做法是把weight的量化范围定义为[-127, 127]

值域统计

上面均匀量化介绍里的关键就是$scale$和$zero\_point$,而它们是通过浮点数的值域范围来确定的。我们如何确定网络中每个需要量化的数据 的值域范围呢,一般有以下两种方案:

  • 一种是根据经验手动设定值域范围,在缺乏数据的时候或者对于一些中间feature我们可以这样做

  • 还有一种是跑一批少量数据,根据统计量来进行设定,这里统计方式可以视数据特性而定。

量化感知训练

在均匀量化的小节我们提到量化前后的误差主要取决于模型的参数和激活值分布与均匀分布的差异。对于量化友好的模型,我们只需要通过 值域统计得到其值域范围,然后调用对应的量化方案进行定点化就可以了。但是对于量化不友好的模型,直接进行量化会因为误差较大而使得 最后模型的正确率过低而无法使用。有没有一种方法可以在训练的时候就提升模型对量化的友好度呢?

答案是有的,我们可以通过在训练过程中,给待量化参数进行量化和反量化的操作,便可以引入量化带来的精度损失,然后通过训练让网络逐渐 适应这种干扰,从而使得网络在真正量化后的表现与训练表现一致。这个操作就叫量化感知训练,也叫qat (Quantization-aware-training)

其中需要注意的是,因为量化操作不可导,所以在实际训练的时候做了一步近似,把上一层的导数直接跳过量化反量化操作传递给了当前参数。

量化网络的推理流程

上面讲述了定点情况下卷积操作的形式,大家可以自己推导一下定点情况下激活函数relu情况。 对于bn,因为大部分网络在都会进行吸bn的操作,所以我们可以把它集成进conv里。

对于现成网络,我们可以在每个卷积层前后加上量化与反量化的操作,这样就实现了用定点运算替代浮点运算的目的。 更进一步的,我们可以在整个网络推理过程中维护每个量化变量对应的scale变量,这样我们可以在不进行反量化的情况下走完 整个网络,这样我们除了带来极少量额外的scale计算开销外,便可以将整个网络的浮点运算转换成对应的定点运算。具体流程可以 参考下图。

../../../_images/quantization-inference.jpg

值域统计和量化感知训练需要涉及的操作大部分都发生在训练阶段,megengine对于这两个操作都提供了相应的封装,并不需要我们手动实现

至此我们粗略的介绍了整个网络量化的定点转换以及转换后的计算方案。

参考文献: https://arxiv.org/pdf/1806.08342.pdf