第2章:模型量化基础

2.1 量化的概念与原理

量化,说白了就是把模型里的高精度数字(比如FP32)变成低精度数字(比如INT8)。你想想看,一个FP32的数值占4个字节,INT8只占1个字节,一下子省了75%的空间。在边缘设备上,这可不是小数目。

我刚开始接触量化时,总觉得这玩意儿会严重掉精度。后来发现,其实大多数场景下,精度损失完全可以控制在1%以内。为什么?因为神经网络对噪声天生就有一定的容忍度——你想想,训练时本身就加了dropout、数据增强这些随机操作,模型早就习惯了不完美的输入。

量化的核心思想,就是用有限的离散值去逼近连续的浮点值。打个比方:你有一把尺子,FP32能精确到毫米,INT8只能精确到厘米。但如果你只是量桌子的长度,厘米级别的精度完全够用。

量化带来的好处:

  • 模型体积缩小:FP32→INT8,体积减少4倍
  • 推理速度提升:整数运算比浮点运算快2-4倍
  • 功耗降低:边缘设备电池续航更久
  • 内存带宽节省:传输更少的数据

我在项目中遇到过这样一个案例:一个目标检测模型在Jetson Nano上跑FP32版本只有8FPS,量化到INT8后直接飙到25FPS。嗯,这就是量化的魅力。

2.2 对称量化与非对称量化

量化方式主要分两种:对称量化和非对称量化。我习惯把它们比作「有符号整数」和「无符号整数」的关系。

对称量化

对称量化,就是量化后的整数范围关于0对称。比如INT8的对称量化,范围是[-128, 127]。它的特点是:浮点数的0一定对应整数的0。

公式很简单:

q = round(r / scale)

其中r是浮点数,q是量化后的整数,scale是缩放因子。

对称量化的好处是实现简单,计算效率高。但有个问题:如果浮点数的分布严重偏向一侧(比如ReLU后的激活值全是正数),那对称量化就会浪费一半的表示范围。

我的经验:对称量化适合权重,因为权重的分布通常比较对称。但激活值嘛...我建议用非对称量化。

非对称量化

非对称量化引入了zero_point(零点偏移),让量化范围可以整体平移。公式变成:

q = round(r / scale) + zero_point

这样,浮点数的0可以映射到任意整数,不再强制对应0。对于ReLU后的全正数激活值,非对称量化能充分利用INT8的[0, 255]范围。

我曾经踩过一个坑:用对称量化去量化MobileNet的激活值,精度掉了3%。换成非对称量化后,精度只掉了0.5%。所以,激活值量化我强烈推荐非对称方式。

对比项 对称量化 非对称量化
表示范围 [-128, 127] [0, 255] 或 [-128, 127]
零点对应 浮点0 = 整数0 浮点0 = 任意整数
计算复杂度 中(需处理zero_point)
适用场景 权重、分布对称的数据 激活值、分布偏斜的数据

2.3 量化参数的计算

量化参数就两个:scale(缩放因子)和zero_point(零点偏移)。怎么算?其实很简单。

计算scale

首先,你得知道浮点数的范围[min, max]。然后:

scale = (max - min) / (qmax - qmin)

对于INT8对称量化:qmin=-128, qmax=127
对于INT8非对称量化:qmin=0, qmax=255

举个例子:假设浮点数范围是[-1.0, 3.0],用非对称INT8量化:

scale = (3.0 - (-1.0)) / (255 - 0) = 4.0 / 255 ≈ 0.01569

计算zero_point

zero_point的计算公式:

zero_point = round(qmin - min / scale)

继续上面的例子:

zero_point = round(0 - (-1.0) / 0.01569) = round(63.75) = 64

验证一下:浮点0对应的整数是64,浮点-1.0对应的整数是0,浮点3.0对应的整数是255。完美覆盖了整个范围。

注意:zero_point计算出来后,一定要做clip操作,确保它在[qmin, qmax]范围内。我曾经因为忘记clip,导致量化后的结果完全不对,排查了半天才发现是zero_point越界了。

完整的量化与反量化

量化(浮点→整数):

q = round(r / scale) + zero_point
q = clip(q, qmin, qmax)

反量化(整数→浮点):

r = (q - zero_point) * scale

嗯,这里要注意:反量化后的值跟原始浮点值会有误差,这个误差就是量化误差。误差大小取决于scale——scale越大,误差越大。

2.4 实战中的量化策略选择

说了这么多理论,来点实际的。我个人习惯的量化策略:

  1. 权重:对称量化,per-channel(每通道一个scale)
  2. 激活值:非对称量化,per-tensor(整个张量一个scale)
  3. 偏置:不量化,保持FP32

为什么权重用per-channel?因为不同通道的权重分布差异可能很大,共用scale会损失精度。我做过实验,per-channel比per-tensor的精度高0.3-0.5%。

为什么激活值用per-tensor?因为per-channel的激活值量化计算量太大,边缘设备扛不住。而且激活值的分布通常比较稳定,per-tensor够用了。

避坑指南:我曾经在量化一个轻量级模型时,发现精度掉了2%。排查后发现是某个层的激活值范围特别大(max=100),导致scale太大。解决办法:用KL散度校准法,自动选择最优的截断阈值,把异常大值截断掉。

最后说一句:量化不是银弹。如果你的模型本身精度就不高,量化后可能会更糟。我建议先确保FP32模型的精度达标,再考虑量化。记住,量化只是锦上添花,不是雪中送炭。