4、模型量化入门:量化原理(定点与浮点)、对称/非对称量化、量化粒度(per-tensor/per-channel)
模型量化是Edge AI部署中最常用、收益最直接的模型瘦身手段。它的核心思想是将神经网络中高精度的浮点参数(通常是FP32)转换为低比特的定点数(如INT8、INT16),从而在保持模型功能基本不变的前提下,显著减小模型体积、降低内存带宽需求,并利用硬件对定点运算的加速能力提升推理速度。本章将从量化数学原理出发,深入剖析定点与浮点的本质差异,详细讲解对称/非对称量化的数学映射关系,并对比per-tensor与per-channel两种量化粒度的适用场景与工程取舍。
4.1 定点与浮点:数值表示的底层差异
理解量化首先需要厘清定点数与浮点数的本质区别。在计算机数值体系中,浮点数(Floating Point)通过科学计数法表示实数,以FP32为例,它由1位符号位、8位指数位和23位尾数位组成,能够表示从极小到极大的动态范围(约10⁻⁴⁵到10³⁸),且精度随数值大小动态变化。这种灵活性使得浮点数成为深度学习训练阶段的标准格式——反向传播需要高精度梯度,任何精度损失都可能累积为训练发散。
定点数(Fixed Point)则完全不同。它使用固定的整数位宽和小数点位置来表示数值,例如Q7.8格式表示7位整数位、8位小数位。定点数的动态范围完全由位宽决定,INT8只能表示-128到127的整数(或通过缩放映射到特定小数范围)。这种"有限精度"看似是缺陷,却带来了三个关键优势:存储效率(INT8仅占FP32的1/4内存)、计算效率(整数运算在CPU/GPU/NPU上比浮点快2-4倍)、能耗效率(整数乘法器功耗远低于浮点乘法器)。
量化的本质就是找到一种最优映射,将浮点张量的数值分布"压缩"到定点整数域中。这个映射关系由两个参数决定:缩放因子S(Scale,浮点数值与整数数值的比例关系)和零点Z(Zero Point,浮点0值对应的整数偏移量)。反量化公式为:r = S × (q - Z),其中r为浮点值,q为量化后的整数值。量化过程则是:q = round(r / S) + Z。
4.2 对称量化与非对称量化:两种映射策略
根据零点Z的取值方式,量化分为对称量化和非对称量化两种策略。
对称量化强制零点Z=0,即浮点0值精确映射到整数0。此时量化公式简化为q = round(r / S),反量化为r = S × q。对称量化的优点是实现简单、硬件友好——许多NPU和DSP的整数乘法单元天然支持对称量化,无需额外的零点偏移计算。其缺点是当浮点数据分布不对称时(例如ReLU后的激活值全为正),对称量化会浪费一半的量化范围(负半轴完全空闲),导致有效精度降低。
非对称量化允许Z≠0,通过引入零点偏移来适配任意分布的数据。对于全为正的激活值,非对称量化可以将整数范围[0,255]完整映射到浮点范围[min, max],精度利用率更高。代价是推理时需要额外的零点减法运算,增加少量计算开销。
| 对比维度 | 对称量化 | 非对称量化 |
|---|---|---|
| 零点Z | 固定为0 | 可偏移(通常非0) |
| 量化公式 | q = round(r / S) | q = round(r / S) + Z |
| 适用场景 | 权重(分布近似对称) | 激活值(ReLU后全正) |
| 硬件友好度 | 高(无需偏移计算) | 中(需额外减法) |
| 精度利用率 | 低(分布不对称时浪费) | 高(可适配任意分布) |
| 典型位宽 | INT8, INT16 | UINT8(无符号) |
在实际工程中,权重通常使用对称量化,因为训练良好的网络权重分布近似于均值为0的高斯分布,对称性较好;激活值通常使用非对称量化,因为ReLU等激活函数会截断负值,导致分布严重偏斜。这种"混合策略"在TensorRT、ONNX Runtime等主流推理引擎中已成为默认配置。
4.3 量化粒度:per-tensor与per-channel的精度博弈
量化粒度决定了S和Z的计算范围。最粗粒度是per-tensor量化(也称per-layer),即整个张量(例如一个卷积层的所有权重)共享一组S和Z。实现最简单,但精度损失较大——因为不同输出通道的权重数值范围可能差异悬殊,统一缩放会放大小数值通道的量化误差。
per-channel量化(也称per-axis)则为每个输出通道独立计算S和Z。以卷积层为例,权重张量形状为[out_channels, in_channels, kh, kw],per-channel量化会为每个out_channel计算一组量化参数。这显著提升了量化精度,尤其对于深度可分离卷积、逐通道归一化等通道间差异大的层。代价是推理时需要按通道进行反量化,增加了计算复杂度和内存访问开销。
选择量化粒度需要权衡精度与效率:
- 硬件支持:部分NPU只支持per-tensor量化,强行使用per-channel会导致算子无法加速。
- 层类型:全连接层和1×1卷积对per-channel敏感度低,可用per-tensor;3×3/5×5卷积建议per-channel。
- 位宽:INT8下per-channel收益明显;INT16下per-tensor已足够。
4.4 案例拆解:MobileNetV2的INT8量化实战
下面通过一个具体案例展示量化参数的选择过程。假设我们要将MobileNetV2部署到Cortex-M7内核的MCU上,目标是将模型从8.7MB压缩至2.2MB以下,同时保证Top-1准确率下降不超过2%。
初始现象:直接使用per-tensor对称量化后,模型准确率从71.2%降至66.8%,下降4.4%,超出预期。分析发现,MobileNetV2的深度可分离卷积中,depthwise层的权重分布极不均匀——部分通道的权重范围仅为[-0.02, 0.02],而其他通道可达[-0.5, 0.5]。per-tensor量化时,S由全局最大值0.5决定,导致小范围通道的量化步长为0.0039,而实际信号幅度仅0.02,量化信噪比不足。
分析过程:我们对所有depthwise层的权重分布进行统计,发现约30%的通道权重范围小于全局范围的1/10。这些通道在per-tensor量化下有效精度仅相当于4-5bit。
解决方案:采用混合粒度策略——对depthwise层使用per-channel量化,对pointwise层(1×1卷积)保持per-tensor量化。同时将激活值改为非对称UINT8量化,以适配ReLU6后的[0,6]分布。
# 伪代码示例:量化参数计算
import numpy as np
def compute_scale_zero_minmax(tensor, bits=8, symmetric=False):
qmin, qmax = -2**(bits-1), 2**(bits-1)-1
if symmetric:
amax = np.max(np.abs(tensor))
scale = amax / qmax
zero = 0
else:
amin, amax = np.min(tensor), np.max(tensor)
scale = (amax - amin) / (qmax - qmin)
zero = qmin - round(amin / scale)
return scale, zero
# per-channel量化:对每个输出通道独立计算
weight = np.random.randn(32, 3, 3, 3) * 0.1 # 模拟权重
scales = np.zeros(32)
zeros = np.zeros(32)
for c in range(32):
scales[c], zeros[c] = compute_scale_zero_minmax(weight[c], symmetric=True)
结果对比:调整后模型大小降至2.1MB,准确率70.1%,仅下降1.1%,满足部署要求。推理速度从FP32的12ms/帧提升至INT8的4.5ms/帧,提升2.7倍。代价是per-channel量化使代码复杂度增加约15%,Flash占用增加8KB(用于存储每通道的Scale/Z表)。
4.5 常见误区与总结复盘
在量化实践中,以下误区值得警惕:
- 误区一:量化范围越准越好。使用min/max统计范围虽然能覆盖全部数据,但会放大离群点的影响。更稳健的做法是使用百分位数(如99.99%)或KL散度校准,牺牲少量范围换取更小的量化步长。
- 误区二:所有层必须用相同量化策略。不同层对量化的敏感度差异巨大。建议逐层分析敏感度,对敏感层保留更高精度(如INT16或跳过量化)。
- 误区三:忽略校准数据集的选择。校准数据应覆盖真实部署场景的输入分布。使用训练集校准可能导致过拟合,使用随机噪声则无法反映真实分布。
- 误区四:量化后不做微调。对于小模型或极端量化(如INT4),量化感知训练(QAT)比训练后量化(PTQ)效果更好,但需要额外的训练成本。