3. 模型量化基础理论:从浮点到定点,我们到底在做什么?
好,咱们进入第三章。说实话,量化这个主题,是嵌入式AI部署里最绕不开、也最容易踩坑的一关。很多同学跑完模型,发现精度掉得厉害,第一反应就是“量化有问题”。但到底问题出在哪?是表示方式选错了,还是参数没算对?
这一章,我就带你把这些基础概念彻底理清楚。浮点与定点、线性与非线性、对称与非对称,还有那两个最关键的参数——scale和zero-point。搞懂这些,你才算真正入了量化的门。
3.1 浮点与定点表示:数字的两种“语言”
计算机里表示数字,主要有两种方式:浮点和定点。咱们先看看它们长什么样。
3.1.1 浮点数(Float)
浮点数,就是带小数点的数,比如3.14、-0.001。它用科学计数法来存:数值 = 符号位 × 尾数 × 2^指数。常见的32位浮点(FP32)里,1位符号、8位指数、23位尾数。好处是动态范围大,精度高。坏处呢?计算慢、占内存、功耗高。在MCU上跑FP32,基本是“杀鸡用牛刀”,而且牛刀还跑不动。
3.1.2 定点数(Fixed-point)
定点数,就是小数点位置固定的数。比如Q7格式(1位符号,7位小数),能表示的范围是[-1, 1 - 2^-7]。定点数计算快,硬件实现简单,但动态范围小,容易溢出或精度不够。
我在一个项目里遇到过,用定点数做音频特征提取,结果因为动态范围没算好,信号一强就直接饱和了。嗯,从那以后我养成了一个习惯:先估算数据的实际分布范围,再选定点格式。
| 特性 | 浮点数 (FP32) | 定点数 (Q7/Q15) |
|---|---|---|
| 动态范围 | 大 (~10^38) | 小 (取决于位宽) |
| 精度 | 高 (23位尾数) | 有限 (7/15位) |
| 计算速度 | 慢 (需要FPU) | 快 (纯整数运算) |
| 硬件开销 | 大 | 小 |
| 典型应用 | 训练、服务器推理 | MCU、DSP推理 |
核心观点:量化,本质上就是把浮点数“翻译”成定点数(或整数),让模型能在没有FPU的廉价芯片上跑起来。
3.2 线性量化与非线性量化:映射的两种“尺子”
量化就是找一个映射关系,把浮点值变成整数值。这个映射可以是线性的,也可以是非线性的。
3.2.1 线性量化
线性量化,说白了就是用一条直线做映射。公式很简单:
q = round(r / S) + Z
其中,r是浮点值,q是量化后的整数值,S是缩放因子(scale),Z是零点(zero-point)。
线性量化实现简单,硬件友好。CMSIS-NN里用的就是这种。但它的精度是均匀的——不管数值大小,误差都一样。对于权重分布比较均匀的网络(比如很多CNN),效果不错。
3.2.2 非线性量化
非线性量化,映射关系不是直线。比如对数量化:q = round(log2(r) / S)。它的特点是:小数值精度高,大数值精度低。这很符合人耳、人眼的感知特性——我们对微弱信号更敏感。
我记得有一次做语音唤醒词模型,用线性量化精度掉了3%,换成对数量化后只掉了0.5%。但代价是计算复杂,硬件不支持,最后还是老老实实用了线性量化+微调。
| 类型 | 映射关系 | 精度分布 | 硬件友好度 | 典型场景 |
|---|---|---|---|---|
| 线性量化 | 直线 | 均匀 | 高 | CNN、全连接层 |
| 非线性量化 | 曲线(如对数) | 非均匀 | 低 | 音频、传感器数据 |
我的建议:在MCU上做推理,优先选线性量化。除非你非常确定数据的分布特性,否则不要轻易上非线性——调试起来太痛苦了。
3.3 对称量化与非对称量化:零点到底要不要?
线性量化里,根据零点Z的取值,又分成两种:对称和非对称。
3.3.1 对称量化
对称量化,零点Z固定为0。公式简化为:q = round(r / S)。它的量化范围是对称的,比如int8的[-128, 127]映射到浮点的[-a, a]。
好处是计算简单,没有零点偏移。坏处是,如果数据分布不对称(比如ReLU后的输出全是非负的),就会浪费一半的量化范围。
3.3.2 非对称量化
非对称量化,零点Z可以不为0。公式就是完整的:q = round(r / S) + Z。它能灵活地匹配数据的实际分布范围,不浪费任何量化bin。
举个例子:一个浮点张量的范围是[0.0, 6.0]。对称量化会映射到[-6.0, 6.0],浪费了负半轴。非对称量化则映射到[0.0, 6.0],每个bin都用上了。
| 特性 | 对称量化 | 非对称量化 |
|---|---|---|
| 零点Z | 固定为0 | 可调 |
| 范围利用率 | 低(数据不对称时) | 高 |
| 计算复杂度 | 低 | 略高(需处理Z) |
| 典型应用 | 权重(通常对称) | 激活值(通常非对称) |
避坑指南:我曾经在一个项目中,对ReLU后的激活值用了对称量化,结果精度掉了5%。换成非对称后,精度损失降到了1%以内。记住:激活值用非对称,权重用对称,这是很多工业实践的默认选择。
3.4 量化参数:Scale和Zero-Point
好了,终于到了最核心的部分。量化参数就两个:S(scale)和Z(zero-point)。它们决定了整个量化映射。
3.4.1 Scale(缩放因子)
Scale决定了每个量化步长代表多大的浮点值。公式:S = (r_max - r_min) / (q_max - q_min)。比如int8量化,q_max=127,q_min=-128,分母就是255。
Scale越小,精度越高,但容易溢出。Scale越大,动态范围大,但精度低。这是个trade-off。
3.4.2 Zero-Point(零点)
Zero-point是浮点0值对应的量化值。公式:Z = round(q_min - r_min / S)。在非对称量化里,它确保浮点0能被精确表示——这对填充(padding)操作特别重要。
我个人习惯,在计算scale和zero-point时,会先用一个校准数据集(calibration dataset)跑一遍推理,收集每层激活值的实际范围。然后用这个范围去算参数。这样比用理论范围(比如[-6, 6])要准得多。
// 伪代码:计算scale和zero-point
float r_min = find_min(activation_tensor);
float r_max = find_max(activation_tensor);
int q_min = -128, q_max = 127;
float scale = (r_max - r_min) / (q_max - q_min);
int zero_point = round(q_min - r_min / scale);
// 量化
int8_t q = round(r / scale) + zero_point;
// 反量化
float r_hat = (q - zero_point) * scale;
关键点:量化不是简单的“四舍五入”。它是一整套参数计算和映射过程。scale和zero-point算对了,量化就成功了一半。
3.5 知识体系总览
为了让你更直观地理解这些概念之间的关系,我画了一张图。你可以把它当作本章的“地图”。
这张图把本章的核心概念串起来了。从最上层的“模型量化”出发,分支出三种表示方式,最终汇聚到量化参数的计算。你可以在后续学习中反复回看这张图,它会帮你建立全局视角。
个人经验:刚开始学量化时,别急着调参。先把这张图里的每个概念搞明白,尤其是scale和zero-point的计算。我曾经花了一周时间调试一个量化模型,最后发现是zero-point算错了——小数点后几位没处理好。这种坑,踩一次就够了。