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.00784
  • zero_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 量化参数的计算

好了,理论讲完了,咱们来点实际的。怎么计算 scalezero_point

假设我们要把浮点数范围 [x_min, x_max] 量化到整数范围 [q_min, q_max]

  1. 计算scale:
scale = (x_max - x_min) / (q_max - q_min)
  1. 计算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,没问题。

总结一下计算步骤:

  1. 确定浮点数范围 [x_min, x_max]
  2. 确定整数范围 [q_min, q_max]
  3. 计算 scale = (x_max - x_min) / (q_max - q_min)
  4. 计算 zero_point = round(q_min - x_min / scale)
  5. 检查 zero_point 是否在 [q_min, q_max] 范围内

2.4 实战中的注意事项

最后,分享几个我在实际部署中总结的经验:

  • 校准数据集:量化时需要一个校准数据集来统计激活值的范围。数据集要尽量覆盖真实场景,否则量化后的精度会崩。我见过有人用100张图片做校准,结果模型在真实场景下直接废了。
  • 逐层量化 vs 逐通道量化:逐层量化是所有通道共用一个 scalezero_point,计算简单但精度损失大。逐通道量化是每个通道独立计算,精度更好,但计算量也更大。我建议对关键层(比如第一层和最后一层)用逐通道量化。
  • 量化后的微调:量化后的模型最好做一下微调(Quantization-Aware Training, QAT),能挽回不少精度损失。不过QAT的训练时间会变长,需要权衡。

好了,这一章的内容就到这里。量化是端侧部署的基石,理解了这些原理,后面的章节咱们才能继续深入。下一章,我会带大家实战一下,用代码实现一个简单的量化工具。