4. 训练后量化(PTQ):不重新训练,也能让模型跑得更快

说到模型量化,很多同学第一反应就是「又要重新训练了吧?」。其实不然。训练后量化(Post-Training Quantization,PTQ)就是那个让你不用动训练流程,直接拿训练好的模型做手脚的技术。我个人习惯把它叫做「懒人量化法」——当然,这不是贬义,而是说它效率高、门槛低。

我在项目里遇到过好几次这样的情况:模型在 PC 上跑得飞快,一部署到嵌入式设备上就卡成幻灯片。重新训练?时间不允许。这时候 PTQ 就是我的救命稻草。

核心结论:PTQ 不需要重新训练模型,只需要少量校准数据,就能把 FP32 模型压缩成 INT8,推理速度提升 2-4 倍,模型体积缩小 75%。代价是精度通常下降 1-3%。

训练后量化(PTQ)知识体系 PTQ 核心流程 量化原理 对称 / 非对称量化 量化参数:scale & zero_point 校准:收集激活值范围 校准数据集 100-500 张代表性样本 覆盖各类场景 / 光照 / 角度 避免单一类别偏置 TFLite 实战 Converter 配置 代表性数据集 API 精度评估与回退策略 PTQ 的核心:用少量校准数据估算激活值分布,确定量化参数,无需反向传播

4.1 PTQ 原理:说白了就是「找范围,做映射」

量化这件事,本质上就是把连续的浮点数映射到离散的整数上。FP32 有约 40 亿个取值,INT8 只有 256 个。你想想看,要把 40 亿个值塞进 256 个格子里,关键就是找到每个格子该放什么范围。

PTQ 的原理可以拆成三步:

  1. 收集统计量:用校准数据集跑一遍推理,记录每一层激活值的 min/max 或者分布直方图。
  2. 计算量化参数:根据收集到的范围,算出 scale(缩放因子)和 zero_point(零点偏移)。
  3. 转换权重和激活值:把 FP32 的权重和激活值映射到 INT8 范围。

这里有个关键点——权重量化是静态的,模型训练完权重就固定了,直接算 min/max 就行。但激活值量化是动态的,它依赖输入数据,所以才需要校准数据集来「摸清」激活值的脾气。

我的经验:激活值的分布往往不是均匀的。比如 ReLU 之后的值集中在 0 附近,尾巴拖得很长。直接用 min/max 量化会把大量精度浪费在极少数离群点上。我建议用 KL 散度或者 均方误差最小化 的方法来找最优截断阈值,效果比硬切 min/max 好不少。

4.2 校准数据集的选择:别随便拿几张图就上

校准数据集是 PTQ 里最容易翻车的环节。我曾经在一个项目中,随手拿了 50 张训练集图片做校准,结果量化后的模型在测试集上直接掉了 5 个点的精度。后来排查发现,那 50 张图全是正面光照良好的样本,模型在暗光下完全崩了。

选校准数据集,记住三个原则:

原则 说明 常见错误
代表性 覆盖推理时可能遇到的各种场景 只用训练集,忽略部署环境差异
多样性 光照、角度、背景、目标类别都要有 单一类别占 80% 以上
适量性 100-500 张足够,不是越多越好 拿几千张图跑校准,耗时且收益递减

具体到图像识别任务,我个人习惯这样选:

  • 从验证集中随机抽取 200 张,确保每个类别至少 5 张。
  • 如果部署环境已知(比如安防摄像头是俯拍),额外加 100 张对应角度的图。
  • 如果模型要处理不同分辨率,校准集里也混入不同尺寸的图。

注意:校准数据集绝对不能包含训练集里没有的类别。否则量化参数会被「带偏」,模型在未见过的类别上表现会异常。我曾经踩过这个坑,校准集里混了一张「猫」的图,但模型只训练过「狗」和「车」,结果量化后对猫的激活值范围估计完全错误。

4.3 PTQ 实战:以 TensorFlow Lite 为例

好了,理论说完了,咱们直接上手。TensorFlow Lite 的 PTQ 实现非常成熟,几行代码就能搞定。我以 MobileNetV2 为例,演示完整的量化流程。

4.3.1 环境准备

确保你安装了 TensorFlow 2.x,并且有训练好的模型。我这里假设你已经有一个 .h5 或 SavedModel 格式的模型。

import tensorflow as tf
import numpy as np

# 加载训练好的模型
model = tf.keras.models.load_model('mobilenet_v2_fp32.h5')

# 准备校准数据集(假设你有 200 张图片)
calib_images = np.load('calib_data.npy')  # shape: (200, 224, 224, 3)
calib_labels = np.load('calib_labels.npy')

4.3.2 定义校准数据生成器

这是 PTQ 的关键步骤。TFLite 需要你提供一个生成器,每次 yield 一批数据。注意,这里只需要输入数据,不需要标签。

def representative_dataset():
    for i in range(len(calib_images)):
        # 每次给一张图,TFLite 会自动收集激活值分布
        yield [calib_images[i:i+1].astype(np.float32)]

# 如果你有 GPU,可以一次给多张
def representative_dataset_batch():
    batch_size = 32
    for i in range(0, len(calib_images), batch_size):
        batch = calib_images[i:i+batch_size].astype(np.float32)
        yield [batch]

我的习惯:先用单张图的方式跑一遍,确保流程没问题。正式量化时用 batch 方式,速度快很多。但注意 batch 不要太大,否则显存容易爆。我一般设 32。

4.3.3 配置转换器并量化

核心代码就这几行:

# 创建转换器
converter = tf.lite.TFLiteConverter.from_keras_model(model)

# 开启 INT8 量化
converter.optimizations = [tf.lite.Optimize.DEFAULT]

# 指定校准数据集
converter.representative_dataset = representative_dataset_batch

# 强制所有操作都转成 INT8(如果不设,部分操作可能保留 FP32)
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]

# 设置输入输出类型为 INT8
converter.inference_input_type = tf.uint8
converter.inference_output_type = tf.uint8

# 执行转换
tflite_model = converter.convert()

# 保存量化后的模型
with open('mobilenet_v2_int8.tflite', 'wb') as f:
    f.write(tflite_model)

转换完成后,你会得到一个 .tflite 文件。对比一下大小:FP32 的 MobileNetV2 大约 14MB,INT8 版本只有 3.5MB 左右。压缩了 75%。

4.3.4 验证精度

量化完了别急着部署,先跑一下精度验证。我一般用验证集对比量化前后的 Top-1 和 Top-5 准确率。

# 加载量化模型
interpreter = tf.lite.Interpreter(model_path='mobilenet_v2_int8.tflite')
interpreter.allocate_tensors()

input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

# 验证集评估
correct = 0
total = len(val_images)

for i in range(total):
    # 输入需要转成 uint8
    input_data = val_images[i].astype(np.uint8)
    interpreter.set_tensor(input_details[0]['index'], [input_data])
    interpreter.invoke()
    
    output = interpreter.get_tensor(output_details[0]['index'])
    pred = np.argmax(output)
    
    if pred == val_labels[i]:
        correct += 1

print(f'INT8 模型准确率: {correct/total:.4f}')

实战结果参考:在我之前的一个车牌识别项目中,FP32 模型准确率 97.2%,INT8 量化后 96.1%,只掉了 1.1 个点。但推理速度从 45ms 降到了 12ms(在树莓派 4B 上测试)。这个 trade-off 非常划算。

4.3.5 常见问题与回退策略

如果量化后精度掉得太多(超过 3%),别慌。试试这几个方法:

  • 检查校准数据集:是不是太偏了?换一批图试试。
  • 使用混合量化:把敏感层保留 FP32,其他层用 INT8。TFLite 支持通过 converter.target_spec.supported_ops 指定。
  • 尝试不同的校准方法:TFLite 默认用 min/max,你可以试试用 KL 散度(需要自己实现校准逻辑)。
  • 增加校准数据量:从 100 张加到 500 张,有时候能改善。

避坑指南:我曾经遇到一个 case,量化后模型在某个特定类别上准确率从 95% 掉到 60%。排查了半天,发现是校准数据里这个类别的样本太少(只有 2 张)。加了 20 张该类别的图重新校准,精度就恢复到了 92%。所以校准集的类别平衡非常重要。

4.4 小结:PTQ 的适用场景

PTQ 不是万能的,但它适合绝大多数边缘部署场景。我个人总结的适用判断标准:

场景 推荐 PTQ? 原因
模型较大(>50MB),部署在资源受限设备 ✅ 强烈推荐 体积和速度收益明显
模型精度已经很高(>95%),允许掉 1-2% ✅ 推荐 精度损失可接受
模型精度刚好卡在业务线(如 90% 要求) ⚠️ 谨慎使用 掉点可能导致不达标,建议先评估
模型非常小(<1MB),对速度不敏感 ❌ 不推荐 收益不大,反而增加复杂度