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)时,脑子里都会过一遍这个流程。

量化参数计算流程 输入浮点数据 [r_min, r_max] 选择量化方式 对称量化 非对称量化 对称量化 Z = 0 S = max(|r|) / q_max 非对称量化 Z = round(q_min - r_min/S) S = (r_max - r_min) / (q_max - q_min) 计算 Scale 和 Zero Point 输出 S 和 Z 💡 注意: 对称量化计算简单,但可能 浪费整数范围;非对称量化 更精确,但计算稍复杂。

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 是量化的基石,对称和非对称是两种主流策略。理解了这些,后面的量化训练和推理优化就好办了。