4、训练后量化(PTQ):PTQ原理、校准数据集、KL散度与熵校准、实战:使用TensorRT进行PTQ。
各位同学,欢迎来到第四章。今天咱们聊聊训练后量化,也就是PTQ。
说实话,在嵌入式平台上做模型部署,PTQ是我最常用的手段。为什么?因为它省事啊。你不需要重新训练模型,拿一个训练好的浮点模型,跑一遍校准,就能得到一个量化模型。我刚开始接触这个的时候,总觉得“哪有这么好的事”,后来发现,只要校准数据集选得好,精度损失真的可以控制得很小。
4.1 PTQ原理:说白了就是找映射关系
PTQ的核心思想,就是把浮点数映射到定点数。比如一个32位浮点数的权重,我们想把它变成8位整数。怎么变?
公式其实很简单:
q = round(r / scale) + zero_point
其中r是原始浮点值,q是量化后的整数值,scale是缩放因子,zero_point是零点偏移。
嗯,这里要注意。scale和zero_point怎么确定?这就引出了校准过程。我个人习惯把校准理解为“找一把合适的尺子”。这把尺子要能量尽所有的数据,但又不能浪费刻度。
举个例子。假设你的权重范围是[-1.0, 1.0],那scale就是2.0/255≈0.0078。但如果你的权重范围是[-0.5, 0.5],还用同样的scale,那有一半的整数刻度就浪费了。所以校准的目的,就是找到最适合当前数据分布的scale和zero_point。
4.2 校准数据集:别小看它,坑很多
校准数据集,说白了就是用来“教”量化算法如何选择scale和zero_point的数据。
我在项目中遇到过一个问题。有个同学拿训练集直接当校准集,结果量化后的模型在测试集上精度掉得一塌糊涂。为什么?因为训练集和测试集的分布有差异,校准出来的尺子只适合训练集。
我个人建议,校准数据集应该满足几个条件:
- 代表性:能覆盖模型在实际部署中可能遇到的各种情况
- 多样性:不要全是同一类样本,比如全是猫的图片
- 数量适中:一般500-2000张图片就够了,太多反而过拟合
我曾经踩过一个坑。用100张图片做校准,结果量化后模型在某些场景下完全失效。后来换成500张,问题就解决了。你想想看,100张图片能代表多少种情况?
4.3 KL散度与熵校准:量化界的“黄金搭档”
说到校准方法,KL散度绝对是绕不开的话题。很多框架,包括TensorRT,默认用的就是KL散度校准。
KL散度衡量的是两个分布之间的差异。在量化中,我们用它来比较原始浮点值的分布和量化后整数值的分布。我们希望这两个分布尽可能接近。
具体怎么做?
- 统计原始浮点值的直方图
- 尝试不同的阈值,把直方图截断
- 对截断后的直方图进行量化
- 计算量化后的分布和原始分布的KL散度
- 选择KL散度最小的那个阈值
说白了,就是找一个“最佳截断点”。为什么要截断?因为8位整数只有256个刻度,而浮点数的范围可能很大。如果硬要把所有浮点数都映射到256个刻度上,那每个刻度代表的区间就很大,精度损失就大。截断掉一些极端值,反而能让大部分数据的量化更精细。
我记得有一次,一个模型量化后精度掉了5个点。我检查了一下,发现是某个层的权重分布有很长的尾巴。用KL散度校准后,精度只掉了0.5个点。这就是截断的好处。
核心要点:KL散度校准的本质,是在“精度损失”和“动态范围”之间找一个平衡点。截断得越多,量化越精细,但丢失的信息也越多。
4.4 实战:使用TensorRT进行PTQ
好了,理论讲完了,咱们来点实际的。TensorRT做PTQ,其实就几步。
首先,你需要一个训练好的浮点模型。我这里以ONNX格式为例。
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit
import numpy as np
# 创建builder和config
TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(TRT_LOGGER)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
config = builder.create_builder_config()
# 设置量化标志
config.set_flag(trt.BuilderFlag.INT8)
# 设置校准器
class MyCalibrator(trt.IInt8EntropyCalibrator2):
def __init__(self, calibration_files, batch_size):
trt.IInt8EntropyCalibrator2.__init__(self)
self.calibration_files = calibration_files
self.batch_size = batch_size
self.batch_idx = 0
self.device_input = cuda.mem_alloc(self.batch_size * 3 * 224 * 224 * 4)
def get_batch_size(self):
return self.batch_size
def get_batch(self, names):
if self.batch_idx < len(self.calibration_files):
# 读取并预处理图片
batch = self._load_batch()
cuda.memcpy_htod(self.device_input, batch)
self.batch_idx += 1
return [int(self.device_input)]
else:
return None
def read_calibration_cache(self):
return None
def write_calibration_cache(self, cache):
with open('calibration.cache', 'wb') as f:
f.write(cache)
# 创建校准器实例
calibrator = MyCalibrator(calibration_files, batch_size=32)
config.int8_calibrator = calibrator
# 解析ONNX模型
parser = trt.OnnxParser(network, TRT_LOGGER)
with open('model.onnx', 'rb') as f:
parser.parse(f.read())
# 构建引擎
engine = builder.build_engine(network, config)
这段代码看着长,其实核心就几行:
config.set_flag(trt.BuilderFlag.INT8):告诉TensorRT我们要做INT8量化config.int8_calibrator = calibrator:设置校准器builder.build_engine():构建量化后的引擎
小技巧:校准过程会生成一个calibration.cache文件。下次再构建同样的模型时,可以直接读取这个缓存,省去重新校准的时间。我一般会把这个文件保存下来,方便团队其他成员使用。
校准完成后,你会得到一个INT8精度的TensorRT引擎。这个引擎的大小大约是FP32模型的1/4,推理速度能提升2-4倍。
注意事项:不是所有层都适合量化。比如一些对精度特别敏感的层(如检测头的最后一层),我建议保留为FP16或FP32。TensorRT支持逐层精度设置,你可以通过set_precision()来控制。
最后,验证一下量化后的精度。我习惯的做法是:
- 用FP32模型跑一遍测试集,记录精度
- 用INT8模型跑一遍同样的测试集,记录精度
- 对比两个精度,如果差距在1%以内,就算合格
如果精度掉得太多,怎么办?别急,有几个排查方向:
- 检查校准数据集是否足够有代表性
- 尝试不同的校准算法(比如MinMax校准)
- 对某些敏感层进行回退(保留为FP16)
好了,这一章的内容就到这里。PTQ是个实用技术,但也不是万能的。下一章我们会聊聊量化感知训练(QAT),那才是真正能“榨干”模型性能的手段。