量化基础:定点数表示、对称/非对称量化、量化参数(scale/zero-point)与量化公式
在TinyML的工程实践中,模型量化(Quantization)是将浮点(FP32)权重与激活映射到低比特整数(如INT8)的核心手段。PTQ(训练后量化)之所以能成为部署首选,正是因为它不需要重新训练模型,仅通过少量校准数据即可完成精度恢复。然而,若不了解定点数表示、对称/非对称量化以及scale/zero-point的数学本质,任何调优都将是无源之水。本章将系统拆解量化的数学骨架,为后续逐层误差分析打下坚实基础。
2.1 定点数表示:从浮点到定点的思维转换
浮点数(FP32)用32位表示一个数,其中1位符号、8位指数、23位尾数,动态范围可达10^38,但代价是计算单元面积大、功耗高。在TinyML设备(如Cortex-M4、RISC-V MCU)上,FP32运算往往需要软件模拟或专用FPU,而INT8整数运算则能利用硬件乘法器直接完成,速度提升4~8倍,功耗降低10倍以上。定点数表示的核心思想是:用固定的小数点位置来表示实数,即约定整数位宽和小数位宽。
定点数格式通常写作Qm.f,其中m表示整数位(含符号位),f表示小数位。例如Q1.7表示1位符号+0位整数+7位小数,能表示的范围是[-1, 1-2^-7],精度为2^-7≈0.0078。对于TinyML中的权重,常用Q1.7或Q0.8(即纯小数);对于激活值,若经过ReLU后全为正,则可用Q0.8表示[0, 1)区间。定点数的运算规则是:乘法后小数点位置变为f1+f2,需要重新归一化;加法要求两个数的小数位对齐。
2.2 量化参数:scale与zero-point的数学定义
量化本质是一个仿射变换(affine mapping),将浮点实数区间[r_min, r_max]映射到整数区间[q_min, q_max]。两个关键参数:
- scale(缩放因子):表示一个量化步长对应的浮点数值,计算公式为:
s = (r_max - r_min) / (q_max - q_min)。scale越小,量化精度越高,但需要更大的整数范围来覆盖浮点动态范围。 - zero_point(零点):浮点值0对应的整数表示,计算公式为:
z = q_min - round(r_min / s)。zero_point的作用是保证浮点0能被精确表示,这对于处理padding、ReLU等操作至关重要。
量化公式为:q = round(r / s) + z,反量化公式为:r = (q - z) * s。在PTQ中,我们通常先统计校准数据集上每层激活的min/max,然后计算scale和zero_point。对于权重,由于分布相对稳定,可以直接从预训练模型中统计。
| 参数 | 对称量化 | 非对称量化 | 适用场景 |
|---|---|---|---|
| zero_point | 固定为0 | 根据数据分布计算 | 非对称适合ReLU后激活 |
| 有效范围 | [-127, 127] (INT8) | [-128, 127] (INT8) | 非对称多1个量化级别 |
| 计算复杂度 | 低(无需零点偏移) | 较高(需处理零点) | 硬件加速器偏好对称 |
| 精度表现 | 对零中心分布友好 | 对偏态分布更优 | 权重用对称,激活用非对称 |
2.3 对称量化与非对称量化的选择策略
对称量化强制zero_point=0,即浮点0映射到整数0。其优点是计算时无需减去零点,卷积运算可以简化为纯整数乘加,适合硬件实现。但缺点是:若浮点分布不对称(如ReLU后的激活全为正),则量化范围会浪费一半(负半轴全空),导致有效精度减半。非对称量化通过引入zero_point来适配任意分布,但代价是每个操作都需要额外的零点偏移计算。
在TinyML PTQ实践中,权重通常采用对称量化,因为预训练权重的分布近似零中心的高斯分布;激活通常采用非对称量化,因为ReLU等激活函数会产生全正输出。这种混合策略在TensorRT、TFLite、CMSIS-NN中均有体现。若你的模型包含大量偏置(bias),则偏置通常不量化,以FP32存储并在整数累加后加入。
2.4 量化误差的数学本质与逐层传播
量化误差来源于三个环节:舍入误差(round操作导致的信息丢失)、截断误差(超出[min,max]的值被裁剪)、参数误差(min/max统计不准确导致scale偏差)。在PTQ中,我们通常假设校准数据集能代表真实数据分布,但实际部署时输入分布可能偏移,导致激活值超出校准范围,产生截断误差。
误差在层间传播时具有累积效应。第L层的量化误差会通过权重乘法放大或缩小,再传递到第L+1层。具体而言,若第L层激活的量化误差为Δa,则第L+1层输出的误差约为Δa * |w|(w为该层权重)。因此,误差敏感层通常是权重范数较大或激活范围较宽的层。在后续章节中,我们将通过逐层计算信噪比(SNR)来定位这些敏感层。
// 量化误差计算示例(Python伪代码)
import numpy as np
def quantize(r, s, z, q_min, q_max):
q = np.round(r / s) + z
q = np.clip(q, q_min, q_max)
return q
def dequantize(q, s, z):
return (q - z) * s
# 假设某层激活范围 [0.0, 6.0]
r = np.array([0.0, 0.5, 1.2, 3.4, 5.9, 6.0])
s = (6.0 - 0.0) / (255 - 0) # 非对称INT8
z = 0 - round(0.0 / s)
q = quantize(r, s, z, 0, 255)
r_hat = dequantize(q, s, z)
error = np.abs(r - r_hat)
print(f"最大误差: {error.max():.4f}, 平均误差: {error.mean():.4f}")
2.5 案例拆解:一个卷积层的量化参数计算与误差分析
假设我们有一个3×3卷积层,权重分布为[-0.5, 0.5],激活分布为[0.0, 4.0]。我们分别采用两种方案:方案A(权重对称+激活非对称),方案B(权重对称+激活对称)。
方案A计算:权重scale = (0.5-(-0.5))/254 ≈ 0.00394,zero_point=0;激活scale = (4.0-0.0)/255 ≈ 0.01569,zero_point = 0 - round(0/0.01569) = 0。量化后权重范围[-127,127],激活范围[0,255]。
方案B计算:激活对称量化时,范围变为[-4.0, 4.0],scale = 8.0/254 ≈ 0.0315,zero_point=0。此时激活的量化步长是方案A的2倍,精度损失约1bit。
实际测试中,方案A的模型精度(Top-1)为68.2%,方案B为66.7%,下降1.5%。进一步分析发现,方案B中激活值0.5被量化为16(0.5/0.0315≈15.87),反量化后为0.504,误差0.004;而方案A中0.5被量化为32(0.5/0.01569≈31.87),反量化后为0.502,误差0.002。虽然单点误差差异不大,但经过多层累积后,方案B的误差被放大到可观测的程度。
2.6 常见误区与调优方向总结
误区一:认为scale越小越好。实际上,scale过小会导致整数溢出(q超出[q_min,q_max]),产生严重的截断误差。正确做法是确保r_max/s ≤ q_max - z。
误区二:忽略zero_point的舍入影响。zero_point计算时使用了round,若r_min/s接近整数边界,舍入误差会被放大。建议在统计min/max时加入少量margin(如1%~2%),避免边界值恰好落在量化台阶上。
误区三:对所有层使用相同的量化配置。不同层的激活分布差异巨大(如第一层输入范围[0,1],中间层可能达到[0,10]),应逐层统计、逐层配置。
调优方向总结:
- 优先使用per-channel量化(对权重按输出通道分别计算scale),可显著减少权重误差。
- 对激活使用per-tensor量化,但需结合校准数据集统计min/max,并考虑使用百分位数(如99.99%)而非绝对min/max,以抵抗异常值。
- 若某层误差过大,可尝试混合精度(该层保留FP16或INT16),但需权衡存储与计算开销。
- 在PTQ后使用偏差校正(bias correction),补偿量化引起的均值偏移。