3. 量化参数进阶:对称量化 vs 非对称量化、量化尺度(Scale)与零点(Zero Point)计算
聊完基础概念,咱们得进入真正的硬核环节了。量化参数怎么选,直接决定模型精度掉几个点。我见过不少团队,模型结构一样,就因为量化参数没调对,推理结果直接崩了。
说白了,量化就是把 float32 的权重和激活值,映射到 int8 或者 int4 的整数空间。这个映射怎么做的?核心就两个参数:Scale(尺度)和Zero Point(零点)。今天咱们就把这两个东西彻底讲透。
3.1 对称量化 vs 非对称量化:两种映射哲学
先看一张图,帮你快速建立直觉。
3.2 对称量化:简单粗暴,但有限制
对称量化,顾名思义,浮点数的正负范围是对称的。映射到 int8 时,整数范围是 [-128, 127]。Zero Point 固定为 0,不需要额外存储。
公式很简单:
q = round(x / scale)
scale = max(|x|) / 127
举个例子,假设权重范围是 [-3.0, 2.8],对称量化会取 max(|x|) = 3.0,scale = 3.0 / 127 ≈ 0.0236。然后每个浮点数除以 scale 再四舍五入。
核心优势:计算快,硬件友好。很多 NPU 和 DSP 原生支持对称量化,因为不需要处理 Zero Point 的偏移。
但有个坑。如果数据分布不对称,比如全是正数 [0.1, 3.0],对称量化会把负半轴的量化范围浪费掉。我做过一个实验,ReLU 后的激活值用对称量化,精度直接掉了 2%。
3.3 非对称量化:灵活,但多一个参数
非对称量化就聪明多了。它用浮点数的实际最小值和最大值来定范围,整数范围用 [0, 255](uint8)。
公式:
q = round(x / scale) + ZP
scale = (max - min) / 255
ZP = round(-min / scale)
还是刚才的例子,激活值范围 [0.1, 3.0],非对称量化会算:
- scale = (3.0 - 0.1) / 255 ≈ 0.0114
- ZP = round(-0.1 / 0.0114) ≈ -9
你看,Zero Point 不是 0 了。反量化的时候要记得减回去:
x = (q - ZP) * scale
我的经验:激活值量化,我基本都用非对称。尤其是 MobileNet、EfficientNet 这类轻量模型,激活值分布经常偏斜,非对称能多保住 0.5-1% 的精度。
3.4 Scale 和 Zero Point 的计算细节
纸上谈兵够了,咱们直接上代码。这是我在项目中实际用过的计算函数:
import numpy as np
def calc_symmetric_scale(x):
"""对称量化:只算 scale,ZP=0"""
max_abs = np.max(np.abs(x))
if max_abs == 0:
return 1.0
scale = max_abs / 127.0
return scale
def calc_asymmetric_params(x):
"""非对称量化:算 scale 和 ZP"""
x_min = np.min(x)
x_max = np.max(x)
if x_min == x_max:
return 1.0, 0
scale = (x_max - x_min) / 255.0
zp = round(-x_min / scale)
# 钳位到 [0, 255]
zp = max(0, min(255, zp))
return scale, zp
# 测试
weights = np.array([-2.5, -1.0, 0.0, 1.5, 3.0])
act = np.array([0.1, 0.5, 1.2, 2.8, 3.0])
s_sym = calc_symmetric_scale(weights)
print(f"对称 scale: {s_sym:.6f}") # 3.0/127 ≈ 0.023622
s_asym, zp = calc_asymmetric_params(act)
print(f"非对称 scale: {s_asym:.6f}, ZP: {zp}")
# (3.0-0.1)/255 ≈ 0.011373, ZP ≈ -9
我曾经踩过的坑:计算 ZP 时直接用了 round(-min/scale),没做钳位。结果有个极端情况,ZP 算出来是 -12,反量化后数据全偏了。记住,uint8 的 ZP 必须在 [0, 255] 范围内。
3.5 两种方案的对比与选型
| 维度 | 对称量化 | 非对称量化 |
|---|---|---|
| 整数范围 | [-128, 127] | [0, 255] |
| Zero Point | 固定 0 | 需计算,非 0 |
| 存储开销 | 少存一个参数 | 多存 ZP |
| 计算复杂度 | 低,硬件友好 | 略高,需处理偏移 |
| 精度表现 | 数据对称时好 | 数据偏斜时更好 |
| 推荐场景 | 权重、BN 后的值 | 激活值、Embedding |
我个人习惯:权重用对称,激活用非对称。权重经过训练后分布通常比较对称,用对称量化省一个参数,计算也快。激活值嘛,ReLU 系列出来全是非负的,非对称量化能充分利用 256 个整数级别。
3.6 实战中的避坑指南
最后分享几个我实际项目中遇到的坑:
- 离群点处理:如果数据里有一个特别大的值,比如 100,其他都在 [-1, 1],对称量化的 scale 会被拉得很大,导致大部分数据精度丢失。我一般会做离群点裁剪,或者用百分位法取范围。
- Scale 的精度:Scale 本身是 float32,但有些硬件要求 scale 是 float16 或者定点数。转换时精度损失可能累积,建议用高精度算完再转。
- ZP 的整数溢出:反量化时 (q - ZP) 可能超出 int16 范围,尤其是 q 和 ZP 都接近边界时。用 int32 做中间计算,最后再截断。
- 校准数据集:算 scale 和 ZP 时,用多少数据?太少不够统计,太多又慢。我一般用 500-1000 张图片,覆盖各种情况就够了。
一句话总结:对称量化省参数、跑得快,适合权重;非对称量化精度高、灵活,适合激活值。Scale 和 ZP 的计算看似简单,但离群点、精度转换、溢出这些细节,才是真正决定量化成败的地方。