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的流程其实很简单:
- 加载预训练模型(FP32)
- 准备校准数据集
- 运行校准,收集每层的激活值分布
- 计算量化参数(scale和zero_point)
- 量化模型并导出
来,看个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(量化感知训练),到时候再聊。
记住一句话:量化不是银弹,但用好了,绝对是嵌入式部署的利器。