2、量化基础:从FP32到Int8的数值表示、量化误差来源与基本量化公式
TinyML 设备(如 Cortex-M 系列微控制器)通常没有 FPU(浮点运算单元)或仅有单精度 FPU,且 SRAM 往往只有几十到几百 KB。若直接运行 FP32 模型,不仅推理速度慢,内存也常常放不下。将权重和激活从 FP32 转换为 Int8,是让模型在 MCU 上落地的最关键一步。本节将深入拆解数值表示差异、量化误差的来源,以及对称/非对称量化的核心公式,为后续的时钟频率与功耗优化打下数学基础。
2.1 FP32 与 Int8 的数值表示差异
FP32(单精度浮点数)采用 IEEE 754 标准,由 1 位符号位、8 位指数位和 23 位尾数位组成。这种表示法能够覆盖从 ±1.4×10⁻⁴⁵ 到 ±3.4×10³⁸ 的极大动态范围,并且在小数值附近保持高精度。例如,0.1 在 FP32 中可以被精确表示为 0x3DCCCCCD,误差约为 1e-8 量级。
相比之下,Int8 只有 8 个比特位,总共能表示 256 个离散值。有符号 Int8 的范围是 -128 ~ 127,无符号 Int8 的范围是 0 ~ 255。Int8 没有指数位,因此无法直接表示小数,也无法覆盖大动态范围。但它的优势在于:单次乘法累加运算只需 1 个时钟周期(在 Cortex-M4/M7 上),而 FP32 乘法需要 3-15 个周期(取决于是否启用 FPU)。对于卷积层这种计算密集型操作,Int8 的吞吐量通常是 FP32 的 2-4 倍。
从信息论角度看,FP32 到 Int8 的转换本质上是将连续值域映射到离散值域。这个过程必然引入信息损失,但通过合理选择量化参数,可以将精度损失控制在 1%-3% 以内(对于大多数分类和检测任务)。关键是要理解:量化不是简单地取整,而是需要先确定缩放因子(Scale)和零点(Zero Point)。
2.2 量化误差的来源分析
量化误差主要来自三个方面,理解这些来源有助于我们在实际部署时采取针对性措施。
2.2.1 舍入误差(Rounding Error)
当浮点数值落在两个相邻量化级别之间时,round() 操作会将其归一到最近的整数。这个误差的最大值为 S/2(S 为缩放因子),且呈均匀分布。例如,若 S=0.1,则 0.05 可能被量化为 0 或 1,误差为 0.05。对于权重而言,这种随机误差通常会被卷积层的累加操作部分抵消,因为正负误差会相互平均。
2.2.2 截断误差(Clipping Error)
当浮点数值超出量化范围时(例如大于 127×S 或小于 -128×S),该值会被截断到边界值。这种误差是系统性偏差,不会相互抵消,会导致输出特征图整体偏移。截断误差的大小取决于数据分布与量化范围的匹配程度。如果权重中存在明显的离群值(Outlier),截断误差会显著增大。
2.2.3 精度损失(Precision Loss)
即使没有舍入和截断,Int8 的 256 个离散级别也限制了最小分辨率。对于数值范围在 [-1, 1] 的权重,量化步长约为 0.0078,这意味着小于该步长的权重变化无法被表示。这种精度损失在深层网络中会逐层累积,尤其是当激活值经过 ReLU 后分布变得偏斜时。
| 误差类型 | 产生原因 | 误差特性 | 缓解策略 |
|---|---|---|---|
| 舍入误差 | round() 取整 | 随机、均匀分布,最大 S/2 | 使用随机舍入或增加量化级别 |
| 截断误差 | 超出量化范围 | 系统性偏差,不可抵消 | 选择合适的 α 值,或使用非对称量化 |
| 精度损失 | 离散级别有限 | 小数值分辨率下降 | 对权重做缩放预处理,或使用混合精度 |
2.3 基本量化公式与两种量化方案
量化过程的核心是找到从浮点实数 r 到整数 q 的映射关系。最通用的形式为:
q = round(r / S) + Z
其中 S(Scale)是缩放因子,Z(Zero Point)是零点偏移。反量化公式为:
r ≈ (q - Z) × S
根据 Z 是否为零,量化方案分为两种:
2.3.1 对称量化(Symmetric Quantization)
对称量化假设浮点数据的分布关于原点对称,因此 Z = 0。此时公式简化为:
S = max(|r_min|, |r_max|) / 127
q = round(r / S)
这种方案实现简单,不需要存储零点,且 Int8 乘法累加时无需额外处理偏移。但缺点是:如果数据分布不对称(例如 ReLU 后的激活值全为正),会浪费一半的量化范围。对称量化常用于权重的量化,因为训练后的权重通常近似对称分布。
2.3.2 非对称量化(Asymmetric Quantization)
非对称量化允许零点偏移,能够充分利用 256 个量化级别。公式为:
S = (r_max - r_min) / 255
Z = round(-r_min / S)
q = round(r / S) + Z
对于 ReLU 后的激活值(范围 [0, 6]),非对称量化可以将整个范围映射到 [0, 255],分辨率提升约一倍。代价是推理时需要额外处理零点偏移,计算量略有增加。实际部署中,激活值通常使用非对称量化,权重使用对称量化,这是 TensorFlow Lite 和 PyTorch 的默认策略。
2.4 实战案例:一个 3×3 卷积层的量化过程
假设我们有一个训练好的 3×3 卷积层,其权重矩阵如下(FP32):
权重 W = [[0.15, -0.32, 0.08],
[0.21, 0.05, -0.18],
[-0.12, 0.27, -0.04]]
输入激活值(单通道)为:
输入 X = [[0.5, 1.2, 0.3],
[0.8, 0.6, 1.5],
[0.2, 0.9, 0.4]]
步骤 1:确定量化参数
对于权重,采用对称量化。计算 max|W| = 0.32,则 S_w = 0.32 / 127 ≈ 0.00252。量化后的权重为:
W_q = round(W / 0.00252) = [[60, -127, 32],
[83, 20, -71],
[-48, 107, -16]]
对于激活值,采用非对称量化。输入范围 [0.2, 1.5],则 S_x = (1.5-0.2)/255 ≈ 0.0051,Z = round(-0.2/0.0051) ≈ -39。量化后的输入为:
X_q = round(X/0.0051) + (-39) = [[59, 196, 20],
[118, 79, 255],
[0, 137, 39]]
步骤 2:Int8 卷积计算
在 MCU 上执行 Int8 乘累加(MAC)操作。以输出位置 (0,0) 为例:
acc = 60×59 + (-127)×196 + 32×20
+ 83×118 + 20×79 + (-71)×255
+ (-48)×0 + 107×137 + (-16)×39
= 3540 - 24892 + 640 + 9794 + 1580 - 18105 + 0 + 14659 - 624
= -13408
步骤 3:反量化得到 FP32 输出
输出缩放因子 S_out = S_w × S_x = 0.00252 × 0.0051 ≈ 1.285e-5。反量化后:
y = acc × S_out = -13408 × 1.285e-5 ≈ -0.1723
而 FP32 直接计算的结果为 -0.1718,误差仅为 0.3%。这个误差在后续层的累加中会被进一步平均化,最终对分类结果的影响通常小于 1%。
2.5 常见误区与总结复盘
在实际项目中,工程师常犯以下错误:
- 误区一:直接对 FP32 模型做 round() 取整。没有先计算 Scale 和 Zero Point,导致量化范围严重不匹配,精度暴跌。
- 误区二:忽略激活值的分布特征。对所有层使用相同的量化参数,没有针对每层单独统计 min/max。正确的做法是使用校准数据集(Calibration Dataset)统计每层激活值的实际范围。
- 误区三:认为量化后模型一定变慢。实际上,如果 MCU 没有 FPU,Int8 推理可能比 FP32 快 5-10 倍;即使有 FPU,Int8 也能减少内存带宽压力,从而降低功耗。
- 误区四:忽视 BatchNorm 的融合。BatchNorm 层在推理时可以融合到卷积层中,但量化时需要特别注意:必须先融合再量化,否则会引入额外的缩放误差。