3. 量化数学原理:Scale、Zero Point 与对称/非对称量化
好,咱们进入量化的核心地带了。前面聊了为什么要量化,现在得搞清楚量化到底是怎么算的。说白了,就是把一个高精度的浮点数,映射到一个低精度的整数范围里。这个映射关系,就是靠两个参数来控制的:Scale(缩放因子)和Zero Point(零点)。
我记得刚接触量化时,总觉得这玩意儿很玄乎。后来自己手推了一遍公式,发现其实就是个线性变换。你想想看,把一个连续区间里的数,塞进一个离散的整数桶里,总得有个比例尺吧?Scale 就是这个比例尺,Zero Point 就是偏移量。
3.1 量化公式:从浮点到整数,再回来
先看正向量化,也就是从浮点数到整数的过程:
q = round(r / S) + Z
其中:
- r:原始的浮点数值(比如权重或激活值)
- S:Scale,缩放因子,一个浮点数
- Z:Zero Point,零点,一个整数
- q:量化后的整数值
反向量化,也就是从整数恢复成浮点数(注意,这个过程会有精度损失):
r_approx = (q - Z) * S
嗯,这里要注意。反向量化得到的 r_approx 和原始的 r 是有误差的,这个误差就是量化误差。我们做量化,本质上就是在精度和性能之间找个平衡点。
核心理解:Scale 决定了每个整数步长代表多大的浮点范围。Zero Point 则确保浮点中的 0 能被精确映射到整数空间。这一点很重要,尤其是在处理 Padding 或 ReLU 这类操作时,0 的精确映射能避免计算偏差。
3.2 Scale 和 Zero Point 怎么算?
这两个参数不是凭空来的,它们取决于你选择的量化范围。假设我们有一个浮点数的取值范围是 [r_min, r_max],要量化到 [q_min, q_max](比如 int8 就是 [-128, 127])。
Scale 的计算公式:
S = (r_max - r_min) / (q_max - q_min)
Zero Point 的计算公式:
Z = round(q_min - r_min / S)
或者更直观一点:
Z = round(q_max - r_max / S)
这两个公式算出来的 Z 应该是一样的(理论上)。但在实际代码中,我习惯用第一个,因为 r_min 通常更容易确定。
个人经验:我在项目中遇到过 Zero Point 算出来超出整数范围的情况。比如 int8 量化,Z 算出来是 130,但 int8 最大只能到 127。这时候需要做截断处理,把 Z 强制 clamp 到 [q_min, q_max] 范围内。别小看这个细节,漏掉它,推理结果可能直接崩掉。
3.3 对称量化 vs 非对称量化
好,接下来是重头戏。这两种量化方式,区别就在于 Zero Point 怎么处理。
3.4 对称量化(Symmetric Quantization)
对称量化的特点是:Zero Point 固定为 0。也就是说,浮点数的 0 映射到整数的 0。
公式简化成:
q = round(r / S)
r_approx = q * S
Scale 的计算也变了。因为 Z=0,我们只需要考虑浮点数的绝对值最大值:
S = max(|r_max|, |r_min|) / q_max
比如 int8 对称量化,q_max 通常是 127。如果浮点范围是 [-10.0, 8.0],那么 S = 10.0 / 127 ≈ 0.0787。
你可能会问:那浮点范围不对称怎么办?比如 [-10.0, 8.0],8.0 那边就浪费了。没错,对称量化确实会浪费一部分整数表示范围。但它也有好处——计算更简单,硬件实现更友好。
我曾经踩过的坑:做对称量化时,如果浮点数据分布严重偏向一侧(比如全是正数),那量化精度会非常差。因为一半的整数范围(负数部分)完全没用上。这时候就该考虑非对称量化了。
3.5 非对称量化(Asymmetric Quantization)
非对称量化就是保留 Zero Point 的完整形式。它能把浮点范围完整地映射到整数范围,不浪费任何整数桶。
公式就是我们最开始说的:
q = round(r / S) + Z
r_approx = (q - Z) * S
Scale 和 Zero Point 都根据实际的 [r_min, r_max] 来计算。这样,无论数据分布是偏左还是偏右,都能充分利用整个整数范围。
我个人的习惯是:权重用对称量化,激活值用非对称量化。为什么?因为权重的分布通常比较对称(训练时做了 weight decay 之类的正则化),而激活值的分布往往偏在一侧(比如 ReLU 之后全是非负数)。
| 对比维度 | 对称量化 | 非对称量化 |
|---|---|---|
| Zero Point | 固定为 0 | 根据数据范围计算 |
| 整数范围利用率 | 可能浪费(数据不对称时) | 完全利用 |
| 计算复杂度 | 低(无需处理 Z) | 稍高(需要处理 Z 的加减) |
| 硬件友好度 | 高(很多硬件原生支持) | 中等 |
| 适用场景 | 权重、分布对称的数据 | 激活值、分布偏斜的数据 |
3.6 量化参数的计算流程
下面这张图,是我自己总结的量化参数计算流程。每次做量化校准(Calibration)时,脑子里都会过一遍这个流程。
3.7 实际代码示例
光说不练假把式。下面是我写的一个简单的量化函数,包含了对称和非对称两种模式。你可以直接拿去跑跑看。
import numpy as np
def quantize(r, bit_width=8, symmetric=False):
"""
量化函数
r: 浮点数组
bit_width: 量化位宽
symmetric: 是否对称量化
"""
q_min = -2**(bit_width-1)
q_max = 2**(bit_width-1) - 1
r_min = np.min(r)
r_max = np.max(r)
if symmetric:
# 对称量化:Z=0
abs_max = max(abs(r_min), abs(r_max))
S = abs_max / q_max
Z = 0
else:
# 非对称量化
S = (r_max - r_min) / (q_max - q_min)
Z = round(q_min - r_min / S)
# 防止 Z 越界
Z = np.clip(Z, q_min, q_max)
# 正向量化
q = np.round(r / S) + Z
q = np.clip(q, q_min, q_max).astype(np.int8)
# 反向量化(用于对比误差)
r_approx = (q - Z) * S
return q, S, Z, r_approx
# 测试
data = np.array([-1.5, -0.8, 0.0, 0.6, 1.2, 2.3], dtype=np.float32)
q_sym, S_sym, Z_sym, r_sym = quantize(data, symmetric=True)
q_asym, S_asym, Z_asym, r_asym = quantize(data, symmetric=False)
print(f"原始数据: {data}")
print(f"对称量化: q={q_sym}, S={S_sym:.4f}, Z={Z_sym}")
print(f"非对称量化: q={q_asym}, S={S_asym:.4f}, Z={Z_asym}")
print(f"对称反量化误差: {np.mean((data - r_sym)**2):.6f}")
print(f"非对称反量化误差: {np.mean((data - r_asym)**2):.6f}")
跑一下试试:你会发现非对称量化的反量化误差通常更小。但别急着下结论,实际部署时还要考虑硬件是否支持非对称量化。有些 NPU 只支持对称量化,那就只能忍痛割爱了。
3.8 量化参数与位宽的关系
位宽直接影响量化精度。位宽越大,整数桶越多,量化误差越小。但代价是模型体积变大,推理速度变慢。
常见的位宽选择:
- int8:最常用,精度损失可控,速度提升明显
- int4:压缩率高,但精度损失较大,需要特殊处理
- int16:精度几乎无损,但压缩效果有限
我个人的经验是:先从 int8 对称量化开始。如果精度达标,就收工。如果不达标,再尝试非对称量化,或者混合精度(部分层用 int16,部分层用 int8)。
好,量化数学原理就聊到这儿。Scale 和 Zero Point 是量化的基石,对称和非对称是两种主流策略。理解了这些,后面的量化训练和推理优化就好办了。