2. 模型量化基础:量化的数学原理、对称量化与非对称量化、量化参数(scale/zero_point)的计算
各位同学,欢迎来到第二章。今天咱们聊聊模型量化。
说实话,量化这个东西,我刚入行时也觉得挺玄乎的。不就是把32位浮点数变成8位整数嘛,能有多大区别?直到我第一次在嵌入式设备上跑语音模型,发现模型死活跑不起来——内存不够,推理速度也慢得离谱。嗯,从那以后,我才真正开始认真研究量化。
说白了,量化就是用更少的比特数来表示模型的参数和激活值。你想想看,一个FP32的模型,参数动辄几十MB甚至上百MB,放到手机或者智能音箱上,根本吃不消。量化之后,模型体积能缩小到原来的四分之一,推理速度也能翻倍。代价呢?精度会有一些损失,但通常可以接受。
2.1 量化的数学原理
先看一个最简单的例子。假设我们有一个浮点数 x,范围在 [x_min, x_max] 之间。我们要把它映射到一个整数 q,范围在 [0, 255](8位无符号整数)。
这个映射关系其实就是一个线性变换:
q = round(x / scale) + zero_point
反过来,从整数恢复成浮点数:
x_hat = (q - zero_point) * scale
这里有两个关键参数:scale(缩放因子)和 zero_point(零点偏移)。
我个人的理解是,scale 决定了每个整数步长代表多大的浮点数范围,而 zero_point 则负责把浮点数的零点映射到整数空间中的某个位置。
举个例子,假设浮点数范围是 [-1.0, 1.0],要量化到 [0, 255]:
- 范围宽度 = 1.0 - (-1.0) = 2.0
- 整数范围宽度 = 255 - 0 = 255
scale = 2.0 / 255 ≈ 0.00784zero_point = round(0 / 0.00784) + 0 = 128
你看,浮点数的0对应整数的128。这就是量化的核心思想。
核心公式:
量化:q = round(x / scale) + zero_point
反量化:x_hat = (q - zero_point) * scale
其中 scale = (x_max - x_min) / (q_max - q_min)
2.2 对称量化与非对称量化
接下来咱们聊聊两种常见的量化方式:对称量化和非对称量化。
2.2.1 对称量化
对称量化,顾名思义,就是浮点数的范围关于0对称。也就是说,x_min = -x_max。这时候,zero_point 就等于0。
公式就简化成:
q = round(x / scale)
x_hat = q * scale
我在项目中遇到过一种情况:模型的权重分布比较对称,比如ReLU之前的卷积层权重,正负值差不多。这时候用对称量化就很合适,计算也简单。
对称量化通常用有符号整数(比如int8)来表示,范围是 [-128, 127]。
小技巧:对称量化时,scale 的计算公式为 scale = max(|x_min|, |x_max|) / 127。注意这里分母是127,不是128。为什么?因为int8的范围是-128到127,但对称量化通常把-128保留给特殊用途(比如表示NaN)。
2.2.2 非对称量化
非对称量化就灵活多了。浮点数的范围不需要关于0对称,可以是任意区间 [x_min, x_max]。这时候 zero_point 就不为0了。
非对称量化通常用无符号整数(比如uint8)来表示,范围是 [0, 255]。
举个例子,假设某个激活层的输出范围是 [0.5, 3.5],全是正数。如果用对称量化,会浪费一半的整数范围(负数部分)。非对称量化就能完美解决这个问题。
我个人习惯是:权重用对称量化,激活值用非对称量化。为什么?权重的分布通常比较对称,而激活值经过ReLU之后全是非负的,非对称量化更高效。
注意:非对称量化虽然灵活,但计算时多了一个 zero_point 的加减操作,硬件实现上会稍微复杂一些。有些嵌入式芯片只支持对称量化,选型时要留意。
2.3 量化参数的计算
好了,理论讲完了,咱们来点实际的。怎么计算 scale 和 zero_point?
假设我们要把浮点数范围 [x_min, x_max] 量化到整数范围 [q_min, q_max]:
- 计算scale:
scale = (x_max - x_min) / (q_max - q_min)
- 计算zero_point:
zero_point = round(q_min - x_min / scale)
注意,zero_point 必须是一个整数,并且要在 [q_min, q_max] 范围内。
我曾经踩过一个坑:计算 zero_point 时忘记取整,结果量化后的结果全乱了。嗯,从那以后我每次都会检查 zero_point 是不是整数。
来看一个完整的例子。假设浮点数范围是 [-2.0, 6.0],量化到 [0, 255]:
scale = (6.0 - (-2.0)) / (255 - 0) = 8.0 / 255 ≈ 0.03137
zero_point = round(0 - (-2.0) / 0.03137) = round(63.75) = 64
验证一下:浮点数0对应的整数是64,没问题。
总结一下计算步骤:
- 确定浮点数范围
[x_min, x_max] - 确定整数范围
[q_min, q_max] - 计算
scale = (x_max - x_min) / (q_max - q_min) - 计算
zero_point = round(q_min - x_min / scale) - 检查
zero_point是否在[q_min, q_max]范围内
2.4 实战中的注意事项
最后,分享几个我在实际部署中总结的经验:
- 校准数据集:量化时需要一个校准数据集来统计激活值的范围。数据集要尽量覆盖真实场景,否则量化后的精度会崩。我见过有人用100张图片做校准,结果模型在真实场景下直接废了。
- 逐层量化 vs 逐通道量化:逐层量化是所有通道共用一个
scale和zero_point,计算简单但精度损失大。逐通道量化是每个通道独立计算,精度更好,但计算量也更大。我建议对关键层(比如第一层和最后一层)用逐通道量化。 - 量化后的微调:量化后的模型最好做一下微调(Quantization-Aware Training, QAT),能挽回不少精度损失。不过QAT的训练时间会变长,需要权衡。
好了,这一章的内容就到这里。量化是端侧部署的基石,理解了这些原理,后面的章节咱们才能继续深入。下一章,我会带大家实战一下,用代码实现一个简单的量化工具。