4、模型量化(上):量化原理(对称/非对称)、PTQ(训练后量化)实战、校准数据集的选择

各位同学,欢迎来到第四章。今天咱们聊聊模型量化。

说实话,量化是我在嵌入式部署中最常用的手段,没有之一。你想想看,一个模型从FP32变成INT8,体积直接缩到四分之一,推理速度翻倍甚至更多,这种好事上哪找去?但量化也不是万能的,搞不好精度掉得你怀疑人生。我刚开始做量化的时候,就踩过不少坑,今天把这些经验分享给你。

4.1 量化原理:对称 vs 非对称

量化的本质是什么?说白了,就是用更少的比特数去表示一个数值。比如FP32的0.123456,我们用INT8的42来表示。那怎么映射呢?这就涉及到量化参数了。

每个量化操作都有两个核心参数:缩放因子(scale)零点(zero_point)。公式很简单:

q = round(r / scale) + zero_point

其中r是原始浮点数,q是量化后的整数。反量化就是:

r = (q - zero_point) * scale

嗯,这里要注意,scale决定了精度,zero_point决定了偏移。

4.1.1 对称量化

对称量化,就是zero_point固定为0。公式变成:

q = round(r / scale)

它的特点是量化范围关于原点对称。比如INT8的范围是[-128, 127],那映射的浮点范围就是[-128*scale, 127*scale]。

我在项目中遇到过一个问题:当模型的权重分布比较对称时,比如经过BatchNorm后的特征图,对称量化效果很好。但如果分布严重偏斜,比如ReLU后的输出全是正数,对称量化就浪费了一半的表示范围。

对称量化的优缺点:

  • 优点:实现简单,计算效率高(没有zero_point的加减操作)
  • 缺点:对非对称分布的数据不友好,精度损失较大

4.1.2 非对称量化

非对称量化就灵活多了。它允许zero_point不为0,可以自由调整映射范围。比如ReLU后的输出范围是[0, 6.0],我们可以把0映射到INT8的0,6.0映射到255,这样整个表示范围都用上了。

公式就是最前面那个:

q = round(r / scale) + zero_point

scale和zero_point的计算方式:

scale = (r_max - r_min) / (q_max - q_min)
zero_point = round(q_min - r_min / scale)

我个人习惯对激活值用非对称量化,对权重用对称量化。为什么?因为权重通常分布比较对称,而且对称量化在推理时少一次减法,速度快那么一丢丢。激活值嘛,ReLU之后全是正的,非对称量化能更好地利用表示范围。

我的经验:如果你不确定选哪种,可以先试试对称量化。如果精度掉得厉害,再换成非对称。大部分情况下,非对称量化能挽回0.5-1%的精度损失。

4.2 PTQ(训练后量化)实战

PTQ,全称Post-Training Quantization,就是训练完模型之后再做量化。不需要重新训练,省时省力。我90%的项目都用PTQ,除非精度实在过不去,才会考虑QAT(量化感知训练)。

PTQ的流程其实很简单:

  1. 加载预训练模型(FP32)
  2. 准备校准数据集
  3. 运行校准,收集每层的激活值分布
  4. 计算量化参数(scale和zero_point)
  5. 量化模型并导出

来,看个PyTorch的实战代码:

import torch
import torch.quantization as quant

# 1. 加载预训练模型
model = torch.load('resnet18_fp32.pth')
model.eval()

# 2. 设置量化配置
model.qconfig = quant.default_qconfig  # 对称量化,per tensor
# 或者用这个:quant.get_default_qconfig('fbgemm')  # 针对x86优化

# 3. 准备量化
model_prepared = quant.prepare(model, inplace=False)

# 4. 校准:用校准数据跑一遍前向
def calibrate(model, calib_loader, num_batches=100):
    model.eval()
    with torch.no_grad():
        for i, (inputs, _) in enumerate(calib_loader):
            if i >= num_batches:
                break
            model(inputs)

calibrate(model_prepared, calib_loader, num_batches=200)

# 5. 转换量化模型
model_quantized = quant.convert(model_prepared, inplace=False)

# 6. 保存
torch.save(model_quantized.state_dict(), 'resnet18_int8.pth')

代码看着简单吧?但坑都在细节里。我曾经因为校准数据没处理好,量化后的模型精度从85%掉到30%,排查了半天才发现是校准数据分布和真实数据不一致。

警告:校准阶段一定要用torch.no_grad(),不然梯度计算会占用大量显存。我第一次跑校准的时候忘了加,结果16G的显存直接爆了。

4.3 校准数据集的选择

校准数据集,说白了就是用来「观察」模型激活值分布的数据。它不参与训练,只是让模型跑一遍前向,我们好知道每层的数值范围。

那校准数据集怎么选?我总结了三条原则:

  • 代表性:校准数据要能代表真实场景的数据分布。比如你做人脸识别,校准集里全是猫狗图片,那量化后的模型肯定拉胯。
  • 多样性:覆盖尽可能多的输入模式。不要只用晴天图片,阴天、雨天、夜晚都得有。
  • 适量性:不是越多越好。我一般用100-500张图片就够了,太多反而浪费时间。

你可能会问:为什么不用全部训练集做校准?

嗯,这个问题我刚开始也想过。后来发现,用全部训练集做校准,效果反而不好。为什么?因为训练集里有很多「极端样本」,比如噪声很大的图片、标注错误的样本,这些会让激活值的范围变得特别大,导致量化精度下降。

我个人习惯从验证集里随机抽200张图片做校准。如果验证集也没有,就从训练集里均匀采样,每类抽几张,保证类别平衡。

校准数据集大小参考:

模型类型 推荐校准样本数 说明
图像分类(如ResNet) 100-500 每类至少5-10张
目标检测(如YOLO) 200-1000 覆盖不同尺寸、角度
语义分割(如UNet) 50-200 场景多样性更重要
NLP模型(如BERT) 500-2000 覆盖不同句式

还有一点,校准时的batch size要和推理时保持一致。为什么?因为BatchNorm层在推理时用的是running mean和running var,但校准过程中有些框架会重新计算这些统计量。batch size变了,统计量也会变,影响量化效果。

我曾经在这个问题上栽过跟头。有一次量化一个YOLOv5模型,校准用batch size=32,推理时用batch size=1,结果精度掉了3个点。后来改成一致,精度就回来了。

避坑指南:校准完成后,一定要用验证集评估一下量化模型的精度。如果精度下降超过1%,可以考虑:

  • 增加校准样本数
  • 换成非对称量化
  • 对敏感层不做量化(保留FP32)
  • 实在不行就上QAT

好了,这一章的内容就到这里。量化原理和PTQ实战是基础,校准数据集的选择是经验活。下一章我们会讲量化推理的加速技巧和QAT(量化感知训练),到时候再聊。

记住一句话:量化不是银弹,但用好了,绝对是嵌入式部署的利器。