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量化,而且有自动校准功能。它的工作流程是:
- 把模型转成ONNX格式
- 用TensorRT的trtexec工具或Python API构建引擎
- 指定精度模式(FP32/FP16/INT8)
- 提供校准数据集(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~3个点
- 再量化:FP16是首选,INT8看精度要求
- 最后优化:TensorRT或ONNX Runtime做推理加速
为什么是这个顺序?因为蒸馏后的模型更鲁棒,量化时精度损失更小。我试过先量化再蒸馏,结果学生模型学到的都是量化噪声,效果很差。
嗯,最后提醒一句:量化不是万能的。如果你的模型本身精度就不高(比如只有80%),量化后可能掉到78%,这时候应该先提升模型精度,而不是指望量化来救场。
下一章我们讲模型部署的完整流程,从训练到推理,一步不落。到时候我会拿一个实际项目来演示,敬请期待。