量化基础理论:线性量化公式推导

各位同学,今天我们来聊聊量化里最核心的东西——线性量化公式。说实话,这个公式看着简单,但我在项目里见过太多人栽在细节上。咱们一步步来,把零点(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个等级。

量化误差分析

量化必然带来误差。主要来源有两个:

  1. 舍入误差:round() 函数造成的。误差范围在 [-0.5, 0.5] 之间。
  2. 截断误差:超出 [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]

你看,反量化后的值和原始值有误差,但整体趋势是对的。这就是量化的代价。

知识体系图

下面这张图总结了线性量化的核心逻辑:

线性量化知识体系 线性量化 量化公式 q = round(r/S + Z) 缩放因子 S S = (r_max - r_min) / (q_max - q_min) 零点 Z Z = round(q_min - r_min/S) 对称量化 Z = 0,适合权重 非对称量化 Z ≠ 0,适合激活值 量化误差 舍入 + 截断

这张图把线性量化的核心要素都串起来了。你从中心节点出发,沿着分支走一遍,就能理解整个体系。

实际项目中的注意事项

最后,分享几个我在项目中积累的经验:

  • 校准数据集很重要:别用训练集,用几百张验证集图片就够了。我一般用500张。
  • 逐层量化 vs 逐张量量化:逐层量化精度更高,但计算量大。逐张量量化快,但精度稍差。看你的需求。
  • 量化后一定要做精度验证:我见过有人量化完直接部署,结果模型输出全是NaN。先跑一遍验证集,确保精度下降在1%以内。

警告:不要对Batch Normalization层做量化!BN层的参数是统计量,量化后误差会累积。我建议在量化前先把BN层融合到卷积层里。

好了,线性量化的核心内容就这些。公式不难,但细节很多。你动手写一遍代码,跑几个例子,就全明白了。

想要更多资料,联系可可