3、模型量化原理:量化基本概念、对称量化与非对称量化、量化精度损失分析、量化感知训练介绍

聊到边缘计算部署,量化是绕不开的话题。我刚开始接触这玩意儿时,也觉得不就是把32位浮点数砍成8位整数嘛,能有多复杂?结果第一次动手就踩了坑——模型精度直接掉了5个点,推理结果完全没法看。后来才明白,量化这件事,门道深着呢。

3.1 量化基本概念

说白了,量化就是把模型里那些高精度的参数(比如FP32),用更低精度的数据类型(比如INT8)来表示。你想想看,一个FP32的权重占4个字节,换成INT8只要1个字节,内存占用直接缩到四分之一。在边缘设备上,这差别可太大了。

量化的核心公式其实很简单:

q = round(r / S) + Z

其中:

  • r 是原始的浮点数值
  • q 是量化后的整数值
  • S 是缩放因子(scale),决定了浮点数和整数之间的映射关系
  • Z 是零点偏移(zero point),用来对齐浮点数的零点

反量化就是反过来:

r = (q - Z) * S

嗯,这里要注意,round操作会引入舍入误差,这是量化精度损失的主要来源之一。我在项目中遇到过,有些层对舍入误差特别敏感,稍微偏一点,整个输出就歪了。

关键点:量化不是简单的截断,而是通过缩放和偏移,把浮点数分布映射到整数范围内。映射得越准,精度损失越小。

3.2 对称量化与非对称量化

这两种量化方式,区别就在于零点Z怎么处理。

对称量化

对称量化把零点Z固定为0。也就是说,浮点数的0,量化后还是0。公式简化成:

q = round(r / S)

这样做的好处是计算简单,硬件实现也方便。但有个问题——如果浮点数分布不对称(比如ReLU后的激活值全是正数),那量化范围就浪费了一半。

我记得有一次部署一个MobileNet模型,激活值全是正的,用对称量化时,INT8的负数范围全浪费了。说白了,就是拿8位整数的表达能力只用了7位,亏得很。

非对称量化

非对称量化允许Z不为0。这样就能把浮点数的分布完整地映射到整数范围里,不浪费任何表达空间。

公式就是最前面那个:

q = round(r / S) + Z

你想想看,对于ReLU后的激活值(全是非负数),非对称量化可以把[0, 6.0]映射到[0, 255],而对称量化只能映射到[-128, 127],有效范围只有一半。哪个更优?一目了然。

对比项 对称量化 非对称量化
零点Z 固定为0 可调节
计算复杂度 稍高
硬件友好度 一般
适用场景 权重(分布近似对称) 激活值(分布可能不对称)

我的习惯:权重用对称量化,激活值用非对称量化。这样既保证了计算效率,又最大化了精度保留。当然,具体还得看硬件支持情况。

3.3 量化精度损失分析

量化一定会损失精度,这是物理规律决定的。但损失多少、能不能接受,得具体分析。

精度损失主要来自三个方面:

  1. 舍入误差:round操作把浮点数映射到离散的整数点上,必然有舍入。
  2. 截断误差:超出量化范围的数值会被截断(clamp),这部分信息直接丢失了。
  3. 累积误差:每一层的量化误差会向后传播,层层叠加,最后可能被放大。

我曾经部署过一个语义分割模型,量化后mIoU掉了3个点。排查了半天,发现是某个中间层的激活值分布特别宽,最大值和最小值差了20多倍。量化时为了覆盖整个范围,缩放因子S被拉得很大,导致大部分数值的量化分辨率极低。

怎么解决?我用了逐通道量化(per-channel quantization)。每个通道单独计算S和Z,而不是整个张量共用一个。这样每个通道都能得到最优的映射,精度损失从3个点降到了0.5个点以内。

避坑指南:我曾经以为量化后精度掉得少就万事大吉,结果发现某些边缘case(比如小目标检测)的精度掉了10个点以上。所以评估量化模型时,不能只看平均指标,还得关注极端情况。

3.4 量化感知训练介绍

前面说的都是训练后量化(Post-Training Quantization, PTQ)。但PTQ有个硬伤——它没有让模型去适应量化带来的误差。说白了,模型训练时用的是FP32,推理时突然换成INT8,模型当然会"不舒服"。

量化感知训练(Quantization-Aware Training, QAT)就是来解决这个问题的。它在训练过程中模拟量化操作,让模型学会"忍受"量化误差。

QAT的核心思路是:

  • 在前向传播时,插入伪量化节点(fake quantization nodes)
  • 这些节点模拟量化和反量化的过程(round + clamp)
  • 反向传播时,使用直通估计器(Straight-Through Estimator, STE)来近似梯度

为什么需要STE?因为round操作的梯度几乎处处为0,没法直接反向传播。STE的做法是:前向传播用round,反向传播假装round不存在,直接把梯度传过去。

代码实现大概长这样:

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

# 前向传播用伪量化,反向传播用STE
class FakeQuantize(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x, scale, zero_point):
        return fake_quantize(x, scale, zero_point)
    
    @staticmethod
    def backward(ctx, grad_output):
        # STE:直接把梯度传回去
        return grad_output, None, None

我在实际项目中发现,QAT通常需要额外训练几个epoch,但效果立竿见影。同样是INT8量化,QAT比PTQ能多保留1-2个点的精度。对于精度敏感的任务(比如人脸识别、医疗影像),QAT几乎是必选项。

总结一下:

  • PTQ适合快速部署、精度要求不高的场景
  • QAT适合精度敏感、有充足训练资源的场景
  • 如果时间允许,我建议先跑PTQ看看精度损失,再决定要不要上QAT

嗯,量化这块内容就讲到这里。下一章我们会聊具体的量化工具和实战流程,到时候拿一个真实模型来走一遍,你就全明白了。