4、模型轻量化技术(下):知识蒸馏与量化

上一章我们聊了剪枝和轻量化结构设计,今天继续往下走。知识蒸馏和量化,这两个东西在实际部署中几乎是必选项。我个人的习惯是:先蒸馏,再量化,最后用TensorRT或者ONNX Runtime做推理优化。顺序别搞反了,否则精度损失会让你头疼。

4.1 知识蒸馏:让大模型当老师

知识蒸馏说白了就是「师夷长技以制夷」。你有一个大模型(老师),它学得很好,但跑不动。你有一个小模型(学生),它跑得快,但学得差。蒸馏就是让老师教学生。

核心思想很简单:学生不仅要学真实标签,还要学老师的「软标签」。软标签里包含了老师对各类别的判断倾向,比如一张猫的图片,老师可能输出「猫0.9,狗0.08,兔子0.02」——这个分布信息比单纯的「猫」标签丰富得多。

关键公式:

蒸馏损失 = α × 学生与真实标签的交叉熵 + (1-α) × 学生与老师软标签的KL散度

其中软标签需要经过温度T软化:softmax(z/T),T越大,分布越平滑。

我在项目中遇到过一个问题:温度T设成1,学生学得跟老师一模一样,但精度反而下降了。后来发现,T设成3~5比较合适,让软标签的「暗知识」充分暴露出来。你想想看,如果T=1,软标签和硬标签差别不大,蒸馏就没意义了。

我的经验:

  • α一般取0.7~0.9,老师权重别太高,否则学生学不到真实分布
  • T从5开始调,逐步降低到3,观察验证集精度
  • 学生模型结构可以跟老师完全不同,但输入输出尺寸要一致

4.2 量化:INT8与FP16

量化就是把模型从FP32变成FP16或者INT8。说白了就是用更少的比特数表示权重和激活值。FP16精度损失很小,INT8则可能掉点,但速度提升明显。

4.2.1 FP16量化

FP16其实就是半精度浮点数。大部分GPU都原生支持,转换起来几乎无痛。我建议你直接上FP16,因为:

  • 显存占用减半
  • 计算速度提升1.5~2倍
  • 精度损失通常小于0.5%

嗯,这里要注意:有些算子对FP16不友好,比如LayerNorm、Softmax。我在部署BERT模型时就踩过坑,FP16下Softmax的数值稳定性变差,导致推理结果全乱套。解决办法是让这些算子回退到FP32。

4.2.2 INT8量化

INT8才是真正的「轻量化利器」。它把每个权重从32位压缩到8位,模型体积直接缩到1/4。但代价是精度可能掉1~3个点。

INT8量化有两种方式:

方式 原理 精度 速度
训练后量化(PTQ) 用少量校准数据统计激活值范围 中等
量化感知训练(QAT) 在训练中模拟量化误差 慢(需重新训练)

我曾经在一个目标检测项目里用PTQ,结果mAP掉了4个点。后来换成QAT,只掉了1.2个点。所以如果你的精度要求很高,别偷懒,上QAT。

避坑指南:

我曾经在MobileNetV3上做INT8量化,发现精度直接崩了。原因是MobileNetV3用了h-swish激活函数,它对量化特别敏感。解决办法是:要么换成ReLU6,要么用QAT并仔细调校准数据集。

4.3 TensorRT与ONNX Runtime的量化工具

这两个工具是部署界的「倚天剑」和「屠龙刀」。我个人的选择是:NVIDIA显卡用TensorRT,其他平台用ONNX Runtime。

4.3.1 TensorRT量化

TensorRT支持INT8和FP16量化,而且有自动校准功能。它的工作流程是:

  1. 把模型转成ONNX格式
  2. 用TensorRT的trtexec工具或Python API构建引擎
  3. 指定精度模式(FP32/FP16/INT8)
  4. 提供校准数据集(INT8需要)

代码示例:

import tensorrt as trt

# 创建builder
builder = trt.Builder(trt.Logger(trt.Logger.WARNING))
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))

# 解析ONNX模型
parser = trt.OnnxParser(network, trt.Logger(trt.Logger.WARNING))
with open("model.onnx", "rb") as f:
    parser.parse(f.read())

# 配置INT8量化
config = builder.create_builder_config()
config.set_flag(trt.BuilderFlag.INT8)
config.int8_calibrator = MyCalibrator()  # 自定义校准器

# 构建引擎
engine = builder.build_serialized_network(network, config)

这里要注意:校准数据集必须覆盖真实场景的分布。我见过有人用ImageNet的100张图做校准,结果部署到工业场景时精度崩了。因为工业场景的图片分布跟ImageNet完全不同。

4.3.2 ONNX Runtime量化

ONNX Runtime的量化工具更友好,支持动态量化和静态量化。动态量化只在推理时量化权重,激活值保持FP32;静态量化则连激活值也量化了。

代码示例:

from onnxruntime.quantization import quantize_dynamic, quantize_static, QuantType

# 动态量化(推荐快速验证)
quantize_dynamic(
    "model.onnx",
    "model_int8.onnx",
    weight_type=QuantType.QInt8
)

# 静态量化(精度更高)
from onnxruntime.quantization import CalibrationMethod
quantize_static(
    "model.onnx",
    "model_int8.onnx",
    calibration_data_reader=my_calibration_reader,
    quant_format=QuantFormat.QDQ,
    per_channel=True,
    activation_type=QuantType.QInt8,
    weight_type=QuantType.QInt8,
    calibrate_method=CalibrationMethod.MinMax
)

我的建议:

  • 先用动态量化快速验证,精度OK就收工
  • 如果精度不够,上静态量化,校准数据至少500张
  • 如果还不行,考虑QAT或者混合精度(部分层INT8,部分层FP16)

4.4 总结与实战建议

好了,这一章的内容就这些。我最后给你一个实战路线图:

  1. 先蒸馏:用大模型教小模型,能提升1~3个点
  2. 再量化:FP16是首选,INT8看精度要求
  3. 最后优化:TensorRT或ONNX Runtime做推理加速

为什么是这个顺序?因为蒸馏后的模型更鲁棒,量化时精度损失更小。我试过先量化再蒸馏,结果学生模型学到的都是量化噪声,效果很差。

嗯,最后提醒一句:量化不是万能的。如果你的模型本身精度就不高(比如只有80%),量化后可能掉到78%,这时候应该先提升模型精度,而不是指望量化来救场。

下一章我们讲模型部署的完整流程,从训练到推理,一步不落。到时候我会拿一个实际项目来演示,敬请期待。