4. 模型转换与量化基础:从 FP32 到 INT8 的必经之路
说实话,模型转换和量化这块,是很多嵌入式 AI 新手的第一道坎。我见过太多人在 PyTorch 里跑得飞起的模型,一转到 NPU 上就崩了。原因很简单——你训练时用的是 FP32 的浮点世界,而 NPU 大多只认 INT8 的整数世界。这中间的桥怎么搭,就是今天要聊的核心。
4.1 模型格式转换:PyTorch → ONNX → TFLite
先说说格式转换。我个人习惯把这条链路叫做「三跳接力」:PyTorch 负责训练,ONNX 做中间翻译,TFLite 是最终给 NPU 吃的格式。每一步都有坑,咱们一个一个来拆。
4.1.1 PyTorch 导出 ONNX
PyTorch 转 ONNX 其实就一行 torch.onnx.export(),但参数设置不对,后面全白搭。我在项目中遇到过最典型的问题——动态轴没设对,导致转出来的模型只能跑固定尺寸的输入。
import torch
import torchvision.models as models
model = models.resnet18(pretrained=True)
model.eval()
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model, # 模型
dummy_input, # 示例输入
"resnet18.onnx", # 输出路径
export_params=True, # 导出训练好的参数
opset_version=12, # 算子集版本,我一般用 12 或 13
do_constant_folding=True, # 常量折叠优化
input_names=['input'],
output_names=['output'],
dynamic_axes={
'input': {0: 'batch_size'}, # 动态 batch
'output': {0: 'batch_size'}
}
)
4.1.2 ONNX 转 TFLite
ONNX 到 TFLite 这一步,官方推荐用 onnx2tf 或者 onnx-tensorflow 桥接。但我个人更倾向直接用 onnx2tf,因为它对量化支持更好。
# 安装
pip install onnx2tf
# 转换命令
onnx2tf \
-i resnet18.onnx \
-o tflite_model \
-oiqt \
-qt per-tensor \
-cind input 1,224,224,3 \
-cofn input 0.0,1.0
参数说明:-oiqt 是开启整数量化训练(Quantization-Aware Training 的替代方案),-qt per-tensor 指定量化粒度。嗯,这里要注意,-cind 后面的输入形状是 NHWC 格式,跟 PyTorch 的 NCHW 不一样,别搞反了。
4.2 量化原理:FP32 → INT8
量化说白了,就是用更少的比特数去近似表示原来的浮点数。FP32 有 32 位,INT8 只有 8 位,压缩了 4 倍。但代价是精度损失。你想想看,原来能表示 2^32 个不同的数,现在只能表示 256 个,信息量差了 1600 多万倍。
量化的核心公式其实很简单:
INT8_value = round(FP32_value / scale) + zero_point
其中 scale 是缩放因子,zero_point 是零点偏移。这两个参数决定了 FP32 到 INT8 的映射关系。
4.3 对称量化 vs 非对称量化
这是量化里最容易混淆的概念。我画了张图帮你理清思路。
4.3.1 对称量化
对称量化把 FP32 的 0 映射到 INT8 的 0,所以 zero_point = 0。计算时少一次加减法,硬件实现更简单。我最早在某个 NPU 上做量化时,发现它的硬件加速器只支持对称量化——非对称量化需要软件模拟,速度直接掉一半。
4.3.2 非对称量化
非对称量化允许 zero_point 不为 0,能更灵活地匹配数据的实际分布。比如你的激活值范围是 [2.0, 6.0],用非对称量化可以把整个 INT8 的 0~255 都用来表示这个区间,精度更高。
4.4 量化误差的来源与缓解
量化不是万能的。我总结了几种常见的精度损失来源:
| 误差来源 | 原因 | 缓解方法 |
|---|---|---|
| 截断误差 | FP32 值超出 INT8 表示范围 | 校准数据集要覆盖真实分布,避免 outlier |
| 舍入误差 | 量化公式中的 round 操作 | 使用更细粒度的 per-channel 量化 |
| 跨层误差累积 | 每层量化误差逐层放大 | 量化感知训练(QAT)微调模型 |
| 算子不匹配 | NPU 不支持某些量化算子 | 替换为 NPU 支持的等价算子 |
我曾经在一个语义分割项目里,量化后 mIoU 从 0.72 掉到了 0.58。排查了半天,发现是某个卷积层的权重分布特别宽,用 per-tensor 量化截断太狠。换成 per-channel 量化后,mIoU 回到了 0.70。
4.5 量化校准:选对数据集很重要
量化校准就是用一小部分数据跑一遍模型,统计出每层激活值的 min/max 范围。这个范围直接决定了 scale 和 zero_point 的取值。
校准数据集的选择有讲究:
- 数量: 100~500 张图片通常就够了。太多没必要,太少统计不准。
- 分布: 要覆盖真实场景。比如做自动驾驶,别只用晴天数据校准,雨天、夜间的也得放几张。
- 多样性: 每个类别至少出现几次。分类模型如果校准集里缺了某个类别,那个类别的激活值范围可能完全跑偏。
4.6 量化后的模型验证流程
模型量化完别急着部署,先跑一遍验证流程。我个人的标准流程是这样的:
- 数值对比: 用同一张输入,对比 FP32 模型和 INT8 模型的逐层输出。误差超过 5% 的层要重点排查。
- 精度验证: 在验证集上跑完整指标。分类看 Top-1/Top-5,检测看 mAP,分割看 mIoU。
- 边界测试: 找一些极端样本(过亮、过暗、遮挡严重)测试,看量化模型是否稳定。
- 性能测试: 在目标 NPU 上测延迟和吞吐量,确认满足实时性要求。