3、对称量化与非对称量化

好,咱们今天聊聊量化里最核心的两个流派——对称量化和非对称量化。说实话,我刚入行那会儿,觉得这俩不就是个公式区别嘛,后来在项目里踩了坑才明白,选错了量化方式,模型精度能直接掉好几个点。

对称量化原理

对称量化,说白了就是让量化后的数值范围关于零点对称。什么意思呢?

假设你有一组权重,分布在 [-3.0, 3.0] 之间。对称量化会把这个范围映射到 [-127, 127](int8)。注意,零点就是0,两边是对称的。

公式长这样:

scale = max(|x_min|, |x_max|) / 127
quantized_value = round(x / scale)

我举个例子你就明白了。假设权重最大值是3.0,最小值是-2.8。对称量化会取绝对值最大的那个,也就是3.0。然后 scale = 3.0 / 127 ≈ 0.0236。那么3.0量化后就是127,-3.0量化后就是-127。但你的实际最小值是-2.8,量化后是 -2.8 / 0.0236 ≈ -119。你看,-127到-119这段范围就浪费了。

核心特点:零点固定为0,不需要额外存储。计算时直接做整数乘法,硬件实现简单。

我个人习惯在权重量化时优先考虑对称量化。为什么?因为权重的分布通常比较对称,浪费不多。我在做MobileNet量化时试过,对称量化比非对称量化在推理速度上快了约15%,精度几乎没差别。

非对称量化原理

非对称量化就灵活多了。它不要求零点在中间,而是根据实际数据的 min 和 max 来定范围。

公式:

scale = (x_max - x_min) / 255
zero_point = round(-x_min / scale)
quantized_value = round(x / scale) + zero_point

还是刚才那个例子,权重范围 [-2.8, 3.0]。非对称量化会把这个范围完整映射到 [0, 255]。scale = (3.0 - (-2.8)) / 255 ≈ 0.0227。zero_point = round(2.8 / 0.0227) ≈ 123。也就是说,-2.8量化后是0,3.0量化后是255,中间值按比例映射。

你想想看,对称量化浪费了 [-3.0, -2.8) 这段,而非对称量化一点不浪费。所以对于分布不对称的数据,非对称量化精度更高。

我的经验:激活值(activation)通常分布不对称,比如ReLU后的输出全是非负的。这时候用非对称量化,精度能提升0.5-1%。我在做YOLOv5量化时,激活值用非对称量化,mAP只掉了0.3%,而用对称量化掉了1.2%。

Zero-point的作用

Zero-point,也叫零点偏移。它到底干嘛用的?

说白了,就是让浮点0能精确映射到整数0附近。为什么这么重要?

  • 填充操作:卷积里的padding通常填0。如果zero_point不是0,那padding填的整数就不是0,计算就乱了。
  • ReLU操作:ReLU会把负数截断为0。如果zero_point不是0,那截断后的整数就不是0,后续计算会出错。
  • 加法操作:两个量化数相加,如果zero_point不同,需要先对齐。这个我踩过坑——有一次两个分支的zero_point不一样,我忘了对齐,结果推理结果全错了。

我曾经犯过的错:在量化一个BERT模型时,我图省事把zero_point设成了0(相当于对称量化),结果精度掉了3%。后来排查发现,BERT的激活值分布严重不对称,zero_point必须动态计算。嗯,从那以后我再也不敢偷懒了。

量化参数计算

量化参数就两个:scale 和 zero_point。怎么算?

步骤一:收集统计量

你需要知道数据的 min 和 max。有两种方式:

  • 校准集(Calibration):跑一小批数据,记录每层的 min/max。我一般用500-1000张图就够了。
  • 运行时统计:边推理边统计,精度更高但计算量大。

步骤二:计算 scale 和 zero_point

量化类型 scale 公式 zero_point 公式
对称量化 (int8) max(|min|, |max|) / 127 0
非对称量化 (uint8) (max - min) / 255 round(-min / scale)

步骤三:量化与反量化

# 量化:浮点 → 整数
def quantize(x, scale, zero_point):
    return round(x / scale + zero_point)

# 反量化:整数 → 浮点
def dequantize(q, scale, zero_point):
    return (q - zero_point) * scale

这里有个细节要注意。量化后的值要 clamp 到目标范围。比如 int8 是 [-128, 127],uint8 是 [0, 255]。我见过有人忘了 clamp,结果溢出后数值全乱了。

避坑指南:计算 scale 时,分母不要用 128 或 256。对称量化用 127,非对称量化用 255。为什么?因为 int8 的范围是 -128 到 127,对称量化要留一个位置给 -128。非对称量化 uint8 是 0 到 255,共256个数,所以分母是255。这个细节,面试时经常考。

好了,对称量化和非对称量化就这些。总结一下:权重用对称量化,激活值用非对称量化,这是业界主流做法。但具体怎么选,还是要看你的模型和数据分布。我建议你跑个对比实验,哪个精度高用哪个。