第3章:训练后量化(PTQ)——实战中的精度调优与验证
训练后量化,也就是PTQ,是我在实际项目中用得最多的模型压缩手段。为什么?因为它快。你不需要重新训练模型,拿一个训练好的浮点模型,跑一遍校准流程,就能得到一个INT8模型。说白了,就是“白嫖”推理加速。
但这里有个坑——精度掉得厉害怎么办?我遇到过不止一次,模型量化后精度从90%掉到60%,那叫一个崩溃。所以这一章,咱们就聊聊PTQ的完整流程、校准数据集怎么选,以及在TensorRT和ONNX Runtime里具体怎么落地。
3.1 PTQ流程详解
PTQ的流程其实不复杂,但每一步都有讲究。我习惯把它拆成四个阶段:
- 准备浮点模型:你得先有一个训练好的FP32模型。注意,模型必须是推理状态,不是训练状态。BatchNorm层要融合,Dropout要关掉。
- 插入量化节点:在模型的每个权重和激活值后面,插入伪量化节点(FakeQuantize)。这些节点负责记录数据的动态范围。
- 校准(Calibration):跑一批校准数据,让量化节点收集到真实的激活值分布。这一步决定了量化参数——scale和zero_point。
- 生成量化模型:根据校准结果,把FP32的权重和激活值映射到INT8,生成最终的量化模型。
嗯,这里要注意:校准不是训练。校准过程中,权重是不更新的。你只是让模型“看”一遍数据,然后统计出每个张量的最大值、最小值,或者更复杂的分布信息。
核心要点:PTQ的精度损失,主要来自激活值的量化误差。权重量化通常比较稳,因为权重分布相对固定。但激活值每层都在变,校准数据集选不好,误差就会累积。
3.2 校准数据集的选择
校准数据集,说白了就是用来“喂”模型的那几百张图片或几百条数据。我见过有人随便拿100张训练集图片做校准,结果量化后精度崩了。为什么?因为校准集没有覆盖到模型可能遇到的各种情况。
我个人习惯,校准数据集的选择要遵循三个原则:
- 代表性:校准集的数据分布,要和实际推理时的数据分布一致。比如你做的是猫狗分类,校准集里就不能全是猫,没有狗。
- 多样性:尽量覆盖不同的光照、角度、背景。我曾在项目中只用白天拍的图片做校准,结果模型在夜间场景下直接“瞎了”。
- 适量性:不是越多越好。通常500-1000张图片就足够了。太多反而会让校准过程变慢,而且收益递减。
我曾经踩过一个坑:用训练集的全部数据做校准,结果量化后精度反而比用500张子集还差。后来分析发现,训练集里有很多重复或相似的样本,导致校准统计出来的动态范围偏大,量化精度反而下降。
我的建议:从验证集中随机抽取500-1000个样本作为校准集。如果验证集也不够,那就从训练集里挑,但要确保类别均衡。
3.3 PTQ在TensorRT中的实现
TensorRT的PTQ,我用的最多的是它的INT8校准器(Int8Calibrator)。TensorRT提供了几种校准算法:
| 校准算法 | 特点 | 适用场景 |
|---|---|---|
| EntropyCalibrator | 基于KL散度,寻找最优阈值 | 大多数分类模型,精度损失小 |
| MinMaxCalibrator | 直接取最大值作为阈值 | 对精度要求极高,但可能浪费量化范围 |
| PercentileCalibrator | 取某个百分位数的值 | 存在异常值的情况 |
我个人习惯,先用EntropyCalibrator跑一遍。如果精度掉得厉害,再换成PercentileCalibrator,调一下百分位数试试。
下面是一个简单的TensorRT PTQ代码示例:
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit
# 创建builder和config
builder = trt.Builder(trt.Logger(trt.Logger.WARNING))
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
config = builder.create_builder_config()
# 设置INT8模式
config.set_flag(trt.BuilderFlag.INT8)
# 自定义校准器
class MyCalibrator(trt.IInt8EntropyCalibrator2):
def __init__(self, calibration_data, batch_size):
trt.IInt8EntropyCalibrator2.__init__(self)
self.calibration_data = calibration_data
self.batch_size = batch_size
self.current_index = 0
# 分配GPU内存
self.device_input = cuda.mem_alloc(self.calibration_data[0].nbytes)
def get_batch(self, names):
if self.current_index + self.batch_size > len(self.calibration_data):
return None
batch = self.calibration_data[self.current_index:self.current_index + self.batch_size]
self.current_index += self.batch_size
cuda.memcpy_htod(self.device_input, batch)
return [int(self.device_input)]
def get_batch_size(self):
return self.batch_size
def read_calibration_cache(self):
return None
def write_calibration_cache(self, cache):
pass
# 使用校准器
calibrator = MyCalibrator(calibration_images, batch_size=32)
config.int8_calibrator = calibrator
# 构建引擎
engine = builder.build_engine(network, config)
嗯,这里要注意:校准器的get_batch方法,每次返回一个batch的数据。数据必须是连续的,而且类型要对。我遇到过有人传了float32的数据,结果校准出来的scale全是错的。
避坑指南:TensorRT的INT8校准,默认会做层融合(layer fusion)。有时候融合后的层,量化误差会变大。你可以通过设置config.set_flag(trt.BuilderFlag.INT8)之后,再手动关闭某些层的INT8模式,来保留精度。
3.4 PTQ在ONNX Runtime中的实现
ONNX Runtime的PTQ,走的是另一种路子。它用的是ONNX的量化工具——onnxruntime.quantization。这个工具的好处是,你不需要写太多代码,几行就能搞定。
但坏处是,灵活性不如TensorRT。你没法自定义校准算法,只能用它内置的那几种。
下面是一个ONNX Runtime PTQ的示例:
import onnx
from onnxruntime.quantization import quantize_static, QuantType, CalibrationMethod
from onnxruntime.quantization import CalibrationDataReader
# 自定义校准数据读取器
class MyDataReader(CalibrationDataReader):
def __init__(self, calibration_images):
self.calibration_images = calibration_images
self.current_index = 0
def get_next(self):
if self.current_index >= len(self.calibration_images):
return None
input_name = "input"
data = self.calibration_images[self.current_index]
self.current_index += 1
return {input_name: data}
def rewind(self):
self.current_index = 0
# 执行静态量化
quantize_static(
model_input="model.onnx",
model_output="model_quant.onnx",
calibration_data_reader=MyDataReader(calibration_images),
quant_format=QuantType.QInt8,
per_channel=True,
activation_type=QuantType.QInt8,
weight_type=QuantType.QInt8,
calibrate_method=CalibrationMethod.MinMax,
extra_options = {
"ActivationSymmetric": True,
"WeightSymmetric": True,
}
)
你想想看,这个流程是不是比TensorRT简单多了?但简单也有简单的代价。ONNX Runtime的校准方法只有MinMax和Entropy两种,而且不支持自定义校准器。如果你的模型对量化特别敏感,可能得在TensorRT上多花点功夫。
我曾经在一个语义分割模型上,用ONNX Runtime的PTQ,精度掉了5个点。后来换成TensorRT的EntropyCalibrator,只掉了1个点。所以,选哪个工具,得看你的精度要求。
我的建议:如果你的模型是分类或检测这类对量化不太敏感的,ONNX Runtime的PTQ完全够用。如果是分割、关键点检测这类对细节要求高的,建议用TensorRT,并且多试几种校准算法。
3.5 精度验证与调优
量化完了,怎么验证精度?很简单,跑一遍验证集,对比FP32和INT8的精度。如果精度掉得在可接受范围内(比如1-2%),那就OK。如果掉得太多,就得调优了。
我常用的调优手段有三个:
- 换校准算法:从Entropy换成Percentile,或者反过来。
- 调整校准集:增加或减少样本,或者换一批数据。
- 混合精度:把某些敏感层保留为FP16或FP32。在TensorRT里,可以通过设置per_layer_precision来实现。
嗯,最后一条最有效,但也最麻烦。你得一层一层地试,找到哪些层对量化最敏感。我习惯的做法是,先全部量化,然后逐层恢复为FP16,看精度变化。如果恢复某一层后精度明显回升,那这层就是“敏感层”。
总结一下:PTQ不是一锤子买卖。校准集选得好,精度可能只掉1%。选得不好,掉10%都有可能。多试几种校准算法,多换几批校准数据,总能找到最优解。
好了,这一章就聊到这儿。下一章咱们聊聊量化感知训练(QAT),那才是真正能“救”模型精度的手段。