3、权重量化入门:FP32到INT8的映射原理,对称量化与非对称量化的选择
好,咱们今天聊聊量化。说实话,很多做嵌入式AI的朋友一听到「量化」两个字就觉得头大。我刚开始接触的时候也一样,觉得不就是把32位浮点数变成8位整数嘛,能有多复杂?结果第一次在STM32上跑量化模型,精度直接掉了5个点,排查了整整两天才发现是量化参数没选对。
嗯,咱们今天就把这件事彻底讲明白。
3.1 为什么非要从FP32到INT8?
先问个问题:为什么非要量化?
你想想看,一个FP32的权重占4个字节,INT8只占1个字节。模型大小直接缩小到原来的四分之一。这还不算完——INT8的乘法运算在ARM Cortex-M系列上,比FP32快3到5倍。我在一个智能门锁项目里,把模型从FP32量化到INT8后,推理时间从120ms降到了35ms,电池续航直接翻倍。
说白了,量化就是用「精度换速度」和「精度换体积」。但关键问题是——怎么换才能不亏太多?
3.2 映射原理:从浮点到整数的桥梁
量化的核心就一句话:找到一种映射关系,让FP32的数值范围能对应到INT8的数值范围。
INT8能表示256个不同的值(-128到127,或者0到255)。而FP32的权重,比如一个卷积层的权重,可能分布在[-1.5, 1.5]这个区间里。我们要做的,就是把[-1.5, 1.5]里的每个浮点数,映射到[-128, 127]里的某个整数上。
这个映射关系,数学上可以写成:
q = round(r / S) + Z
其中:
- r 是原始的FP32值
- q 是量化后的INT8值
- S 是缩放因子(scale)
- Z 是零点偏移(zero point)
反过来,反量化就是:
r = (q - Z) * S
这个公式看起来简单,但实际用起来门道很多。我见过不少新手直接拿整个模型的权重最大值最小值去算S和Z,结果精度崩得一塌糊涂。为什么?因为不同层的权重分布差异太大了。
核心原则:每一层(甚至每个通道)都应该有自己的S和Z。这叫「逐层量化」或「逐通道量化」。千万别偷懒用全局量化参数。
3.3 对称量化 vs 非对称量化
好,现在咱们聊聊两种最常见的量化方式。
3.3.1 对称量化
对称量化的特点是:零点Z固定为0。也就是说,FP32的0值,量化后还是0。
公式简化成:
q = round(r / S)
S的计算方式:
S = max(|r_max|, |r_min|) / 127
举个例子:
假设某层权重范围是 [-1.2, 0.8]
对称量化:
S = max(1.2, 0.8) / 127 = 1.2 / 127 ≈ 0.00945
0.8 量化后:round(0.8 / 0.00945) ≈ 85
-1.2 量化后:round(-1.2 / 0.00945) ≈ -127
看到问题了吗?权重范围是[-1.2, 0.8],但对称量化把整个[-127, 127]的范围都用上了,可实际只用了[-127, 85]这一段。右边的[86, 127]全浪费了。这就是对称量化的缺点——当数据分布不对称时,会浪费一部分量化精度。
我的经验:对称量化特别适合权重分布接近对称的情况。比如经过BatchNorm后的激活值,或者用tanh激活的层。我在一个语音识别模型里试过,对称量化后精度只掉了0.3%,完全能接受。
3.3.2 非对称量化
非对称量化引入了零点Z,可以灵活调整映射范围。
公式:
q = round(r / S) + Z
S和Z的计算:
S = (r_max - r_min) / 255
Z = round(-r_min / S) (对于uint8,范围0到255)
还是刚才那个例子:
权重范围 [-1.2, 0.8]
非对称量化(用uint8,范围0-255):
S = (0.8 - (-1.2)) / 255 = 2.0 / 255 ≈ 0.00784
Z = round(1.2 / 0.00784) ≈ 153
0.8 量化后:round(0.8 / 0.00784) + 153 ≈ 255
-1.2 量化后:round(-1.2 / 0.00784) + 153 ≈ 0
看到了吗?非对称量化把整个[0, 255]范围都用上了,没有浪费。对于分布不对称的数据,非对称量化通常精度更高。
3.4 怎么选?一张表说清楚
| 对比维度 | 对称量化 | 非对称量化 |
|---|---|---|
| 零点Z | 固定为0 | 需要计算和存储 |
| 计算复杂度 | 低(不需要处理Z) | 稍高(需要处理Z的加减) |
| 精度(数据对称时) | 高 | 高 |
| 精度(数据不对称时) | 低(浪费量化范围) | 高 |
| 硬件支持 | 广泛(几乎所有硬件都支持) | 部分硬件不支持 |
| 典型应用场景 | 权重、tanh/sigmoid激活 | ReLU激活、偏置 |
避坑指南:我曾经在一个项目里,对ReLU后的激活值用了对称量化。ReLU的输出全是非负的,对称量化直接浪费了一半的量化范围。结果精度掉了2个点。后来换成非对称量化,精度立马回来了。记住:ReLU后的激活值,一定要用非对称量化。
3.5 实战建议:我的量化选择策略
说了这么多理论,咱们来点实际的。我个人习惯的量化选择策略是这样的:
- 权重:优先用对称量化。因为大部分模型的权重分布接近对称(尤其是经过正则化后),而且对称量化计算简单,硬件支持好。
- 激活值:看激活函数。
- ReLU/ReLU6:非对称量化(输出非负)
- tanh/sigmoid:对称量化(输出对称)
- 其他:先统计分布,再决定
- 偏置:用FP32或INT32。偏置对精度影响大,我一般保留FP32,或者用INT32量化。千万别把偏置量化到INT8,精度会崩。
- 先跑校准集。用几百张图片或几百条数据跑一遍推理,统计每层激活值的实际分布范围。别用理论范围,实际分布往往比理论范围小很多。
一句话总结:对称量化省心但挑数据,非对称量化灵活但稍复杂。我的建议是——权重用对称,激活用非对称,偏置用FP32。这个组合在90%的项目里都够用。
3.6 一个完整的量化示例
最后,咱们看一个简单的Python代码,演示如何对一组权重做对称量化和非对称量化:
import numpy as np
# 模拟一组FP32权重
weights = np.array([-1.2, -0.8, -0.3, 0.0, 0.5, 0.8, 1.1, 1.3], dtype=np.float32)
print("原始权重:", weights)
# 对称量化(int8,范围-128到127)
def symmetric_quantize(data):
max_abs = np.max(np.abs(data))
scale = max_abs / 127.0
quantized = np.round(data / scale).astype(np.int8)
return quantized, scale
# 非对称量化(uint8,范围0到255)
def asymmetric_quantize(data):
min_val = np.min(data)
max_val = np.max(data)
scale = (max_val - min_val) / 255.0
zero_point = np.round(-min_val / scale).astype(np.int32)
quantized = np.round(data / scale + zero_point).astype(np.uint8)
return quantized, scale, zero_point
# 执行量化
sym_q, sym_s = symmetric_quantize(weights)
asym_q, asym_s, asym_z = asymmetric_quantize(weights)
print("\n对称量化结果:")
print(f" 缩放因子 S = {sym_s:.6f}")
print(f" 量化值: {sym_q}")
print("\n非对称量化结果:")
print(f" 缩放因子 S = {asym_s:.6f}")
print(f" 零点 Z = {asym_z}")
print(f" 量化值: {asym_q}")
# 反量化验证
sym_deq = sym_q * sym_s
asym_deq = (asym_q.astype(np.float32) - asym_z) * asym_s
print("\n反量化后与原始值的误差:")
print(f" 对称量化误差: {np.mean(np.abs(weights - sym_deq)):.6f}")
print(f" 非对称量化误差: {np.mean(np.abs(weights - asym_deq)):.6f}")
运行这段代码,你会发现非对称量化的误差通常更小。但别忘了,非对称量化需要额外存储零点Z,计算时也多一步加减法。在资源极度受限的MCU上,这点开销可能就决定了能不能跑起来。
好了,关于权重量化的入门就聊到这儿。下一章咱们会深入讲量化感知训练(QAT),看看怎么在训练阶段就让模型「习惯」量化带来的误差。到时候我会分享一个我在TinyML竞赛里用QAT把精度从92%提到96%的真实案例,敬请期待。