4. 轻量化模型在田间边缘设备上的部署验证
部署验证的任务拆解
训练好的作物病害分类模型(假设为 MobileNetV3-Small 主干,输入 224×224,输出 5 类:健康、早疫病、晚疫病、叶斑病、锈病)不能直接丢到田间边缘设备上。边缘设备通常只有 ARM CPU、有限内存、无独立 GPU,因此需要先做格式转换,再在算力受限条件下测量推理延迟与精度,最后比较不同量化策略的取舍。
本节把验证拆成三条并行关注线:格式转换链(PyTorch → ONNX → 量化 → 边缘推理引擎)、算力受限模拟(限制线程数、模拟低主频)、量化策略对比(FP32 / FP16 / INT8 动态 / INT8 静态)对模型体积与准确率的影响。三者共同决定模型能否真正落地。
转换与量化:可执行的关键代码
下面给出从 PyTorch 权重到 ONNX、再到 INT8 动态量化的完整脚本。假设模型定义与训练脚本在同一工程中,模型类名为 LeafDiseaseNet,权重文件为 best_model.pth,验证集目录结构为 val/类别名/*.jpg。
import torch, onnx, onnxruntime as ort, numpy as np, time, os
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
from model import LeafDiseaseNet # 与训练脚本共用模型定义
DEVICE = "cpu"
CKPT = "best_model.pth"
ONNX_FP32 = "leaf_fp32.onnx"
ONNX_INT8 = "leaf_int8.onnx"
# 1) 加载权重并导出 FP32 ONNX
model = LeafDiseaseNet(num_classes=5).to(DEVICE)
model.load_state_dict(torch.load(CKPT, map_location=DEVICE))
model.eval()
dummy = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model, dummy, ONNX_FP32,
input_names=["input"], output_names=["logits"],
opset_version=13, do_constant_folding=True,
dynamic_axes={"input": {0: "batch"}, "logits": {0: "batch"}},
)
# 2) INT8 动态量化(无需校准集,适合快速验证)
from onnxruntime.quantization import quantize_dynamic, QuantType
quantize_dynamic(ONNX_FP32, ONNX_INT8, weight_type=QuantType.QInt8)
# 3) 构建单线程推理会话,模拟边缘算力
so = ort.SessionOptions()
so.intra_op_num_threads = 1
so.inter_op_num_threads = 1
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
sess = ort.InferenceSession(ONNX_INT8, sess_options=so, providers=["CPUExecutionProvider"])
# 4) 验证集评估:延迟 + 准确率
tf = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
val_ds = datasets.ImageFolder("val", transform=tf)
loader = DataLoader(val_ds, batch_size=1, shuffle=False)
# 预热
for _ in range(10):
sess.run(None, {"input": np.random.randn(1, 3, 224, 224).astype(np.float32)})
correct, total, latencies = 0, 0, []
for img, label in loader:
x = img.numpy().astype(np.float32)
t0 = time.perf_counter()
logits = sess.run(None, {"input": x})[0]
latencies.append((time.perf_counter() - t0) * 1000)
pred = int(np.argmax(logits, axis=1)[0])
correct += int(pred == int(label))
total += 1
print(f"INT8 模型体积: {os.path.getsize(ONNX_INT8)/1024:.1f} KB")
print(f"平均延迟: {np.mean(latencies):.1f} ms (P95: {np.percentile(latencies,95):.1f} ms)")
print(f"Top-1 准确率: {correct/total*100:.2f}%")
把 ONNX_INT8 换成 ONNX_FP32 再跑一遍,即可得到 FP32 的对照数据。若需要 INT8 静态量化(精度通常更好),把第 2 步替换为 quantize_static,并提供一个 CalibrationDataReader,用验证集中随机抽取的 200 张图像做校准。
示例结果与判读
以下数据为教学示例,用于说明如何组织对比表与判读,不代表任何真实设备实测值。实际部署时必须用目标设备重新测量。
| 策略 | 模型体积 | 单张延迟(1 线程) | Top-1 准确率 | 结论 |
|---|---|---|---|---|
| FP32 | 6.2 MB | 约 210 ms | 92.4% | 精度基准,延迟与体积不达标 |
| FP16 | 3.1 MB | 约 260 ms | 92.3% | CPU 上无加速,反而变慢,不推荐 |
| INT8 动态 | 1.6 MB | 约 95 ms | 91.2% | 体积与延迟达标,精度下降 1.2% |
| INT8 静态 | 1.6 MB | 约 88 ms | 91.9% | 综合最优,需校准集 |
判读要点:FP16 在无 FP16 指令集的 ARM CPU 上会被拆成 FP32 计算,延迟不降反升,这是常见误区;INT8 动态量化无需校准、落地最快,但精度略低于静态量化;静态量化需要 200 张左右有代表性的田间图像做校准,若校准集与真实田间分布偏差大,掉点会明显放大。因此校准集应覆盖不同光照、不同病害严重程度,而不是只用清晰样本。