4. 训练后量化(PTQ):从原理到实战调优
训练后量化,圈里人常叫 PTQ。说白了,就是模型训练完了,我们直接拿它做量化,不再动训练过程。这招在端侧部署里特别实用——你想想看,很多场景下我们只有训练好的权重,没有原始训练环境,PTQ 就成了唯一选择。
我个人习惯把 PTQ 看作「模型瘦身手术」。手术做得好,模型跑得快还几乎不掉精度;做得不好,那掉点起来真是让人头疼。这一章,我把 PTQ 的完整流程、校准数据怎么选、误差怎么分析调优,全部分享给你。
4.1 PTQ 标准流程
PTQ 的流程其实不复杂,但每一步都有坑。我画了个流程图,你先看个全貌:
流程看着简单,但每个环节都有讲究。我一个个拆开说。
4.2 校准数据集的选择
校准数据集,是 PTQ 里最容易被低估的一环。很多人随便拿几百张图就跑,结果量化后模型在某些场景下崩得一塌糊涂。
校准集的核心要求:要能代表真实推理时的数据分布。不是越多越好,而是越「像」越好。
| 校准集类型 | 适用场景 | 推荐数量 | 注意事项 |
|---|---|---|---|
| 训练集子集 | 通用场景,分布稳定 | 500~2000 张 | 需随机采样,避免类别偏差 |
| 验证集 | 精度验证,分布已知 | 全部或随机子集 | 注意不要过拟合到验证集 |
| 真实场景采集 | 特定部署场景(如夜间、雨天) | 200~500 张 | 覆盖极端情况,如过曝、欠曝 |
| 合成数据 | 隐私受限或数据获取困难 | 1000+ 张 | 需验证分布一致性 |
校准集数量怎么定?我见过有人用 100 张图就跑通了,也见过用 5000 张反而效果更差的。关键在于:校准集要能「激活」模型的所有量化敏感路径。太少,统计量不稳定;太多,边际收益递减,还浪费时间。
一般经验值:分类任务 500~1000 张,检测任务 1000~2000 张,超分或 ISP 任务 500~1000 张就够了。
4.3 量化误差分析
量化误差,说白了就是「信息丢失」。FP32 的精度被压缩到 INT8,那丢失的信息去哪了?我们得找到它。
误差主要来自三个地方:
- 截断误差:超出量化范围的值被硬截断。比如某个激活值跑到 200,但 INT8 最大只能表示 127,那 73 就丢了。
- 舍入误差:浮点数映射到离散整数时的精度损失。这个一般影响小,但累积起来也够呛。
- 跨层误差传播:前一层的误差被后一层放大。这是最隐蔽的,也是调优的重点。
怎么分析?我常用的方法是 逐层对比:
# 伪代码:逐层余弦相似度对比
for layer in model.layers:
fp32_output = forward_fp32(layer, input)
int8_output = forward_int8(layer, input)
cos_sim = cosine_similarity(fp32_output, int8_output)
if cos_sim < 0.99:
print(f"警告:{layer.name} 相似度 {cos_sim:.4f},需要关注")
我记得有一次做 ISP 去噪模型的量化,发现前面几层卷积的相似度都在 0.98 以上,但到了中间一个残差块,突然掉到 0.89。查了半天,原来是那个层的激活值分布特别宽,最大值跑到 300 多,INT8 根本装不下。这就是典型的截断误差问题。
4.4 量化调优实战
找到问题层之后,怎么调?我总结了几个实战技巧:
4.4.1 调整校准策略
不同的校准策略,对极端值的处理方式不同:
| 校准策略 | 原理 | 适用场景 | 精度表现 |
|---|---|---|---|
| MinMax | 取实际最小/最大值 | 分布均匀,无极端值 | 中等,易受 outlier 影响 |
| Percentile | 取指定百分位(如 99.9%) | 存在少量极端值 | 较好,鲁棒性高 |
| KL 散度 | 最小化量化前后分布差异 | 分布不对称,有长尾 | 优秀,但计算稍慢 |
| MSE | 最小化均方误差 | 对精度要求极高 | 优秀,需调参 |
4.4.2 混合精度量化
不是所有层都适合 INT8。有些层对精度特别敏感,强行量化反而得不偿失。这时候可以「网开一面」:
- 敏感层保留 FP16 或 FP32:比如第一层卷积、最后的全连接层、注意力机制中的 softmax。
- 逐层指定量化位宽:有些框架支持 INT8 + INT4 混合,进一步压缩。
- 按通道量化:每个输出通道独立算 scale,精度比逐层量化高不少。
# 示例:在 TensorRT 中指定混合精度
import tensorrt as trt
# 创建配置
config = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30)
# 设置精度约束
config.set_flag(trt.BuilderFlag.INT8)
config.set_flag(trt.BuilderFlag.FP16) # 允许回退到 FP16
# 逐层精度控制(通过 inspector 分析后手动指定)
precision_constraints = {
"conv_1": trt.DataType.FLOAT, # 第一层保留 FP32
"attention_softmax": trt.DataType.FLOAT, # softmax 用 FP32
"res_block_3": trt.DataType.HALF, # 敏感残差块用 FP16
}
# 实际通过 per-layer precision override API 设置
4.4.3 量化感知训练(QAT)兜底
如果 PTQ 调了半天,精度还是差 1~2 个点,那就别硬撑了。这时候该上 QAT 了。但 QAT 不是本章重点,我只说一句:PTQ 调优的终点,就是 QAT 的起点。先用 PTQ 找到问题层,再用 QAT 微调那几个层,效率最高。
4.5 实战案例:ISP 去噪模型 PTQ 调优
拿我之前做的一个 ISP 去噪模型举例。模型不大,8 层卷积加 2 个残差块,FP32 的 PSNR 是 38.5dB。
第一轮 PTQ:直接用 MinMax 校准,500 张训练集子集。结果 PSNR 掉到 36.2dB,掉了 2.3 个点。逐层分析发现,第 5 层和第 7 层卷积的余弦相似度只有 0.93 和 0.91。
第二轮调优:换成 Percentile(99.9%) 校准,校准集加了 200 张真实夜间场景。PSNR 回升到 37.8dB,还差 0.7dB。
第三轮:把第 5 层和第 7 层改成 FP16 混合精度。PSNR 到了 38.3dB,只差 0.2dB。这个精度损失在端侧完全可以接受。
4.6 本章小结
PTQ 的核心就三件事:选对校准集、找对问题层、用对调优策略。校准集要覆盖真实分布,别偷懒;误差分析要逐层做,别只看最终精度;调优时先改校准策略,再考虑混合精度,最后才上 QAT。
嗯,这一章的内容就到这里。PTQ 是端侧部署的入门功夫,但入门功夫练好了,后面 QAT 和量化算子优化才能走得顺。你自己动手跑一遍流程,踩几个坑,就全明白了。