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 实战建议:我的量化选择策略

说了这么多理论,咱们来点实际的。我个人习惯的量化选择策略是这样的:

  1. 权重:优先用对称量化。因为大部分模型的权重分布接近对称(尤其是经过正则化后),而且对称量化计算简单,硬件支持好。
  2. 激活值:看激活函数
    • ReLU/ReLU6:非对称量化(输出非负)
    • tanh/sigmoid:对称量化(输出对称)
    • 其他:先统计分布,再决定
  3. 偏置:用FP32或INT32。偏置对精度影响大,我一般保留FP32,或者用INT32量化。千万别把偏置量化到INT8,精度会崩。
  4. 先跑校准集。用几百张图片或几百条数据跑一遍推理,统计每层激活值的实际分布范围。别用理论范围,实际分布往往比理论范围小很多。

一句话总结:对称量化省心但挑数据,非对称量化灵活但稍复杂。我的建议是——权重用对称,激活用非对称,偏置用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%的真实案例,敬请期待。