3、模型量化基础:量化原理(FP32->INT8)、对称/非对称量化、量化感知训练(QAT)与训练后量化(PTQ)

好,咱们今天聊聊量化。说实话,量化这个话题在嵌入式端太重要了。你想想看,一个模型在GPU上跑得飞起,一放到手机或者边缘设备上,内存直接爆掉,推理速度慢得像蜗牛。这时候,量化就是你的救命稻草。

我刚开始接触量化的时候,也觉得不就是把32位浮点数转成8位整数嘛,有什么难的?后来踩了不少坑才明白,这里面的门道深着呢。今天我就把这几年的经验掰开了揉碎了讲给你听。

3.1 量化原理:从FP32到INT8

量化的本质是什么?说白了,就是用更少的比特数去表示一个数值范围。FP32用了32位,INT8只用了8位,存储一下子缩小了4倍,计算也能用更快的整数指令。

但问题来了——精度损失怎么办?嗯,这里有个核心思想:我们不需要精确表示每一个数值,只需要保证数值之间的相对关系大致正确就行。神经网络对噪声其实挺鲁棒的,这一点我在做图像分类模型时深有体会。

量化的数学公式其实很简单:

// 量化公式:将浮点数映射到整数
q = round(r / S) + Z

// 反量化公式:将整数还原为浮点数
r = (q - Z) * S

其中:
r  - 原始浮点数值
q  - 量化后的整数值
S  - 缩放因子(scale),浮点数
Z  - 零点偏移(zero point),整数

这里的S和Z就是我们要找的两个关键参数。S决定了每个量化步长代表多大的浮点数范围,Z则负责对齐零点。

核心要点:量化不是简单的截断,而是通过缩放和偏移,用有限的整数格子去覆盖原始的浮点数分布。你覆盖的范围越大,每个格子的精度就越粗;范围越小,越容易溢出。

3.2 对称量化 vs 非对称量化

这两种方式,我习惯把它们叫做「强迫症版本」和「灵活版本」。

对称量化

对称量化要求零点Z必须等于0。也就是说,浮点数的0必须对应整数的0。这样做的好处是计算简单,不需要考虑偏移量。但代价是什么?如果你的数据分布不是对称的(比如ReLU之后全是正数),那有一半的整数格子就浪费了。

// 对称量化示例
// 假设浮点数范围是 [-1.0, 1.0],量化到 [-127, 127]
S = 1.0 / 127 ≈ 0.00787
Z = 0

// 量化 0.5 -> round(0.5 / 0.00787) = round(63.5) = 64
// 反量化 64 -> 64 * 0.00787 = 0.5037

我在项目中遇到过一个问题:用对称量化去量化一个全是正数的特征图,结果精度掉了2个点。后来换成非对称量化,精度立马回来了。原因就是对称量化浪费了一半的表示能力。

非对称量化

非对称量化允许Z不为0。这样你可以灵活地调整量化范围,让整数格子完全覆盖你的数据分布。代价就是计算时多了一个偏移量,稍微复杂一点。

// 非对称量化示例
// 假设浮点数范围是 [0.0, 2.0],量化到 [0, 255]
S = (2.0 - 0.0) / 255 ≈ 0.00784
Z = round(0 - 0.0 / 0.00784) = 0

// 量化 1.5 -> round(1.5 / 0.00784 + 0) = round(191.3) = 191
// 反量化 191 -> (191 - 0) * 0.00784 = 1.497
对比项 对称量化 非对称量化
零点Z 固定为0 可调整
计算复杂度 中等
表示效率 数据对称时高 始终较高
适用场景 权重(通常对称分布) 激活值(通常非对称分布)

我的建议:权重用对称量化,激活值用非对称量化。这是工业界最常用的组合拳。当然,如果你用的推理框架只支持对称量化(比如某些老版本的TensorRT),那就只能硬着头皮上了。

3.3 量化感知训练(QAT)

QAT,全称是Quantization-Aware Training。说白了,就是在训练过程中模拟量化带来的误差,让模型学会适应这种「粗糙」的表示方式。

具体怎么做呢?在训练的前向传播中,插入伪量化节点(fake quantization nodes)。这些节点会模拟量化和反量化的过程,产生和实际推理时一样的精度损失。反向传播时,梯度还是用浮点数来更新——因为量化操作不可导,我们用的是直通估计器(STE)这个技巧。

// 伪量化操作(PyTorch风格伪代码)
def fake_quantize(x, scale, zero_point, bit_width=8):
    # 量化
    q = torch.round(x / scale + zero_point)
    # 截断到整数范围
    q = torch.clamp(q, 0, 2**bit_width - 1)
    # 反量化(模拟推理时的精度损失)
    x_hat = (q - zero_point) * scale
    return x_hat

# 前向传播时用 x_hat,反向传播时梯度直接通过

我曾经在一个语义分割项目上试过,直接PTQ(训练后量化)精度掉了5个点,换成QAT只掉了0.3个点。差距就是这么明显。但代价是什么?训练时间变长了,而且需要原始训练数据——有时候客户的数据拿不到,这就很头疼。

注意:QAT不是万能的。如果你的模型本身训练得就不够好,量化后会更差。我建议先把浮点模型的精度做到满意,再考虑量化的事。别指望量化能帮你「修复」模型缺陷。

3.4 训练后量化(PTQ)

PTQ,Post-Training Quantization,就是模型训练完之后直接量化。不需要重新训练,只需要一小部分校准数据来统计激活值的分布,算出S和Z。

PTQ的流程大概是这样的:

  1. 准备好校准数据集(几百到几千张图片就够了)
  2. 用浮点模型跑一遍前向,收集每一层的激活值分布
  3. 根据分布计算每层的S和Z(常用KL散度、均方误差等指标)
  4. 把权重和激活值的量化参数保存下来
  5. 导出量化后的模型

PTQ最大的优势就是快。我最快的一次,从拿到模型到量化完只花了10分钟。但缺点也很明显——精度损失比QAT大,尤其是对小的模型或者对精度敏感的模型。

// PTQ中计算scale的常用方法(KL散度校准)
// 1. 收集激活值直方图
// 2. 尝试不同的截断阈值
// 3. 计算原始分布和量化后分布的KL散度
// 4. 选择KL散度最小的阈值

float find_optimal_threshold(float* hist, int num_bins) {
    float min_kl = FLT_MAX;
    float best_threshold = 0;
    
    for (int i = 128; i < num_bins; i++) {
        float threshold = (float)i / num_bins * max_val;
        float kl = compute_kl_divergence(hist, i);
        if (kl < min_kl) {
            min_kl = kl;
            best_threshold = threshold;
        }
    }
    return best_threshold;
}

经验之谈:PTQ的校准数据集很关键。我见过有人随便拿了几张图去校准,结果量化后的模型在真实场景下完全不能用。校准数据一定要能代表实际部署时的数据分布。如果你做的是人脸检测,就别拿风景图去校准。

3.5 如何选择:QAT还是PTQ?

这个问题我经常被问到。我的回答是:看情况。

  • 选PTQ的场景:模型比较大(ResNet50以上)、精度要求不那么苛刻、开发周期紧、没有原始训练环境
  • 选QAT的场景:模型比较小(MobileNet之类)、精度要求高、有完整的训练pipeline、对延迟有极致要求

我个人习惯的做法是:先跑PTQ看看效果。如果精度损失在可接受范围内,那就直接用PTQ,省时省力。如果损失太大,再上QAT。毕竟QAT要重新训练,成本高不少。

嗯,量化这块内容确实不少,但核心就这几个点:理解量化原理、选对量化方式、根据场景决定用QAT还是PTQ。你把这些搞明白了,嵌入式端的模型部署基本就稳了。

避坑指南:我曾经在一个项目里,量化后的模型在开发板上跑得好好的,一上量产设备就出问题。查了半天,发现是不同设备的CPU对INT8指令的支持不一样。所以,量化前一定要确认目标硬件支持哪些指令集——ARM的NEON、x86的AVX-512 VNNI,这些都得心里有数。