4. 轻量化模型在田间边缘设备上的部署验证

部署验证的任务拆解

训练好的作物病害分类模型(假设为 MobileNetV3-Small 主干,输入 224×224,输出 5 类:健康、早疫病、晚疫病、叶斑病、锈病)不能直接丢到田间边缘设备上。边缘设备通常只有 ARM CPU、有限内存、无独立 GPU,因此需要先做格式转换,再在算力受限条件下测量推理延迟与精度,最后比较不同量化策略的取舍。

本节把验证拆成三条并行关注线:格式转换链(PyTorch → ONNX → 量化 → 边缘推理引擎)、算力受限模拟(限制线程数、模拟低主频)、量化策略对比(FP32 / FP16 / INT8 动态 / INT8 静态)对模型体积与准确率的影响。三者共同决定模型能否真正落地。

田间边缘设备部署验证:转换链 · 算力模拟 · 量化对比 ① 格式转换链 PyTorch 权重 best_model.pth FP32 · 约 6.2 MB ONNX 导出 opset 13 动态 batch=1 量化处理 FP16 / INT8 校准集 200 张 边缘推理引擎 ONNX Runtime CPU EP · 1 线程 田间 设备 ARM CPU ② 算力受限模拟 线程数限制 intra_op_num_threads=1 模拟单核 ARM 场景 关闭 GPU / 加速器 测量指标 单张推理延迟 (ms) 预热 10 次后取均值 验证集 Top-1 准确率 ③ 量化策略对比维度 模型体积 FP32 基准 FP16 ≈ 1/2 INT8 ≈ 1/4 推理延迟 INT8 通常最快 FP16 需硬件支持 CPU 上 FP16 可能变慢 准确率 INT8 静态 掉点通常 ≤ 1~2% ④ 部署通过判据(示例阈值,需按实际设备标定) 延迟判据 单张 ≤ 150 ms(约 6~7 FPS) 满足田间巡检节拍 精度判据 量化后 Top-1 准确率下降 ≤ 2% 且各类别召回不塌陷 体积判据 模型文件 ≤ 2 MB 适配设备存储与 OTA 更新 转换后模型

转换与量化:可执行的关键代码

下面给出从 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 准确率结论
FP326.2 MB约 210 ms92.4%精度基准,延迟与体积不达标
FP163.1 MB约 260 ms92.3%CPU 上无加速,反而变慢,不推荐
INT8 动态1.6 MB约 95 ms91.2%体积与延迟达标,精度下降 1.2%
INT8 静态1.6 MB约 88 ms91.9%综合最优,需校准集

判读要点:FP16 在无 FP16 指令集的 ARM CPU 上会被拆成 FP32 计算,延迟不降反升,这是常见误区;INT8 动态量化无需校准、落地最快,但精度略低于静态量化;静态量化需要 200 张左右有代表性的田间图像做校准,若校准集与真实田间分布偏差大,掉点会明显放大。因此校准集应覆盖不同光照、不同病害严重程度,而不是只用清晰样本。

延迟测量必须包含预处理与后处理,否则会低估真实端到端耗时。上表仅统计推理会话调用时间,实际部署时图像解码与 resize 往往再增加 20~40 ms,需一并纳入判据。
若 INT8 静态量化后某一类别(如锈病)召回明显下降,优先检查该类别的校准样本数量是否过少,而不是直接放弃量化。可对该类别单独补充 30~50 张校准图后重新量化。