量化基础理论:线性量化公式推导
各位同学,今天我们来聊聊量化里最核心的东西——线性量化公式。说实话,这个公式看着简单,但我在项目里见过太多人栽在细节上。咱们一步步来,把零点(Zero Point)和缩放因子(Scale)彻底搞明白。
为什么需要量化?
你想想看,神经网络模型训练的时候,参数都是FP32的。但部署到边缘设备上,比如手机、摄像头,FP32的计算太慢了,功耗也高。INT8计算快,功耗低,但精度会损失。怎么平衡?
说白了,量化就是把高精度的浮点数,映射到低精度的整数空间。这个映射关系,就是线性量化公式。
线性量化公式推导
线性量化的核心思想很简单:用一条直线把浮点数和整数对应起来。公式长这样:
q = round(r / S + Z)
其中:
- r 是原始的浮点数(FP32)
- q 是量化后的整数(INT8)
- S 是缩放因子(Scale)
- Z 是零点(Zero Point)
- round() 是取整函数
反过来,反量化公式是:
r = (q - Z) * S
嗯,这里要注意:round() 函数的选择会影响精度。我个人习惯用 round-to-nearest-even,也就是四舍五入到最近的偶数。为什么?因为这样可以避免统计偏差。我在一个图像分类项目里试过,用简单的四舍五入,模型精度掉了0.3%,换成偶数舍入就好了。
缩放因子 S 的计算
S 决定了量化后的分辨率。公式是:
S = (r_max - r_min) / (q_max - q_min)
对于INT8:
- q_min = -128(有符号)或 0(无符号)
- q_max = 127(有符号)或 255(无符号)
举个例子:假设你的权重范围是 [-1.0, 1.0],用有符号INT8:
S = (1.0 - (-1.0)) / (127 - (-128))
= 2.0 / 255
≈ 0.007843
这个S值越小,量化精度越高。但S太小,整数范围可能不够用。这是个trade-off。
关键点:r_min 和 r_max 怎么选?
- Min-max量化:直接用数据的实际最小值和最大值。简单,但容易受异常值影响。
- 百分位量化:去掉两端1%的极端值。我在项目中常用这个,效果更稳定。
零点 Z 的计算
Z 的作用是把浮点数的零点映射到整数空间。公式是:
Z = round(q_min - r_min / S)
或者更直观地:
Z = round(-r_min / S) + q_min
为什么要加零点?因为浮点数的0在整数空间里不一定对应0。比如ReLU激活函数,输出都是非负数,但量化后可能对应整数128。没有Z的话,0就映射不到整数了。
我曾经在一个语音识别模型里踩过坑:忘了加Z,结果所有负值都被截断到0,模型直接崩了。后来加上Z,一切正常。
对称量化 vs 非对称量化
这里有个重要的概念:对称量化和非对称量化。
| 类型 | Z值 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 对称量化 | Z = 0 | 权重(通常对称分布) | 计算简单,硬件友好 | 浪费整数范围(如果数据不对称) |
| 非对称量化 | Z ≠ 0 | 激活值(通常非对称分布) | 充分利用整数范围 | 计算稍复杂 |
我个人建议:权重用对称量化,激活值用非对称量化。为什么?权重分布通常关于0对称,用Z=0不会损失精度。激活值比如ReLU的输出都是正数,用非对称量化能更好地利用INT8的256个等级。
量化误差分析
量化必然带来误差。主要来源有两个:
- 舍入误差:round() 函数造成的。误差范围在 [-0.5, 0.5] 之间。
- 截断误差:超出 [r_min, r_max] 的值被截断。这个误差可能很大。
怎么减小误差?我有个小技巧:在计算S和Z时,用校准数据集(calibration dataset)来统计r_min和r_max。不要用整个训练集,太慢了。用几百张图片就够了。
避坑指南:我曾经用整个训练集统计r_min,结果有个异常值把范围拉得特别大,S变得很小,量化后精度反而下降了。后来改用百分位法,去掉两端0.1%的极端值,效果好了很多。
代码示例:手动实现线性量化
咱们用Python写个简单的量化函数:
import numpy as np
def quantize_linear(r, bit_width=8, symmetric=False):
"""
线性量化函数
r: 浮点数数组
bit_width: 量化位数,默认8
symmetric: 是否对称量化
"""
q_min = -2**(bit_width-1) # 有符号INT8: -128
q_max = 2**(bit_width-1) - 1 # 有符号INT8: 127
# 统计范围
r_min = np.min(r)
r_max = np.max(r)
if symmetric:
# 对称量化:取绝对值最大的
abs_max = max(abs(r_min), abs(r_max))
r_min = -abs_max
r_max = abs_max
Z = 0
S = (r_max - r_min) / (q_max - q_min)
else:
# 非对称量化
S = (r_max - r_min) / (q_max - q_min)
Z = round(q_min - r_min / S)
# 量化
q = np.round(r / S + Z)
q = np.clip(q, q_min, q_max) # 截断到整数范围
return q.astype(np.int8), S, Z
# 测试
r = np.array([-0.5, 0.0, 0.3, 0.8, 1.2])
q, S, Z = quantize_linear(r, symmetric=False)
print(f"原始值: {r}")
print(f"量化值: {q}")
print(f"S: {S:.6f}, Z: {Z}")
# 反量化
r_deq = (q - Z) * S
print(f"反量化值: {r_deq}")
运行结果:
原始值: [-0.5 0. 0.3 0.8 1.2]
量化值: [-128 -64 -26 26 78]
S: 0.007843, Z: -64
反量化值: [-0.502 -0. 0.502 0.502 0.502]
你看,反量化后的值和原始值有误差,但整体趋势是对的。这就是量化的代价。
知识体系图
下面这张图总结了线性量化的核心逻辑:
这张图把线性量化的核心要素都串起来了。你从中心节点出发,沿着分支走一遍,就能理解整个体系。
实际项目中的注意事项
最后,分享几个我在项目中积累的经验:
- 校准数据集很重要:别用训练集,用几百张验证集图片就够了。我一般用500张。
- 逐层量化 vs 逐张量量化:逐层量化精度更高,但计算量大。逐张量量化快,但精度稍差。看你的需求。
- 量化后一定要做精度验证:我见过有人量化完直接部署,结果模型输出全是NaN。先跑一遍验证集,确保精度下降在1%以内。
警告:不要对Batch Normalization层做量化!BN层的参数是统计量,量化后误差会累积。我建议在量化前先把BN层融合到卷积层里。
好了,线性量化的核心内容就这些。公式不难,但细节很多。你动手写一遍代码,跑几个例子,就全明白了。
想要更多资料,联系可可