简介:这份PDF聚焦轻量化ShuffleNetV2与MobileNet-V3融合模型,面向农业病虫害识别与嵌入式部署方向的研究者、算法工程师及PyTorch学习者。文档完整覆盖融合模型设计动机、特征融合策略、剪枝量化优化、数据集构建、训练评估以及嵌入式平台部署全流程,并配有清晰目录,适合快速定位模型对比、部署适配和性能测试章节。资源为1个PDF文件,共41页,包体仅2.14MB,轻便易存。文档图表与目录显示正常,内容完整,已有124人学习。借助PyTorch的动态计算图特性,读者可更灵活地复现和调试融合模型代码,理解ShuffleNetV2与MobileNet-V3在准确率、计算量和嵌入式资源占用之间的平衡;同时文中对剪枝、量化和特征融合层的分析,也为在低成本设备上进行实时推理提供了具体思路。适合作为课程设计、毕业课题或轻量化视觉方案落地的参考资料。
1. 为什么农业病虫害识别要选轻量化融合模型
农业生产现场的设备往往不是机房里的 GPU 服务器,而是几百毫瓦到几瓦功耗的嵌入式边缘盒子。把病虫害识别算法放上去,最大的矛盾是叶片病斑区域小、背景复杂,模型不能太浅;但板端算力有限,模型又不能太厚。只靠单条主干网络,要么像 ShuffleNetV2 那样计算量很低但细纹理特征偏弱,要么像 MobileNet-V3 那样注意力机制到位但整网算子偏多。这套方案把两者在特征层做模型融合,用 ShuffleNetV2 保留高效的全局结构感知,用 MobileNet-V3 强化病斑局部响应,再通过量化、剪枝和板端性能调优,落到嵌入式边缘 AI 部署中。适合正在做农业视觉识别、边缘推理加速或者轻量网络落地的工程师参考。
2. ShuffleNetV2 与 MobileNet-V3 的选型逻辑与计算特征
2.1 两类轻量化骨干的互补性
农业病虫害图像和通用 ImageNet 图像差异很大。病害叶片上经常是几个毫米级的病斑,比如稻瘟病初期在叶片上只是针尖大小的褐色点,整张图里占比不到 2%。这类任务要求网络在浅层保留局部纹理,在深层又不丢失全局上下文。
ShuffleNetV2 的分组卷积和通道混洗把计算量压得很低,但通道混洗的本质是通道间信息交换,对空间细粒度特征的聚合能力不如注意力机制。MobileNet-V3 在深度可分离卷积上叠加了 SE 注意力,能够自适应地对通道做重标定,在抗背景干扰上更有优势。两者互补:ShuffleNetV2 提供高效下采样结构,MobileNet-V3 提供特征重标定能力。采用特征级融合,而不是简单把两个模型的最终打分做加权,可以让分类层在融合后的中间特征上重新学习决策边界。
2.2 计算量与参数量对比
在做融合之前,我先对比了两个骨干在 224×224 输入下的常见公开数据。这个对比的意义在于确定融合模型的规模上限,避免两边都取完整大模型,最后参数超过 8M,嵌入式端根本跑不动。
| 模型变体 | 参数量 | 乘加运算量 | 特征表达特点 |
|---|---|---|---|
| ShuffleNetV2 0.5x | 约 1.4M | 约 41M | 通道混洗,计算结构规整 |
| ShuffleNetV2 1.0x | 约 2.3M | 约 146M | 性价比均衡,适合 CPU |
| MobileNetV3-Small | 约 2.5M | 约 56M | SE 注意力 + Hard-Swish |
| MobileNetV3-Large | 约 5.4M | 约 219M | 精度更高,但算子偏重 |
实际部署中参数量不是唯一指标。如果板端 NPU 对深度可分离卷积做了专门加速,MobileNetV3 的运行帧率可能比 FLOPs 看起来更低廉;如果只能跑 ARM CPU,ShuffleNetV2 的分组卷积因为内存搬运频繁,反而没有数字上那么理想。所以我通常取 ShuffleNetV2 1.0x 的浅层和 MobileNetV3-Small 的特征层,把融合模型参数控制在 4M 左右,推理延迟控制在单帧 30ms 到 80ms 之间。
2.3 融合节点怎么选:从 stage 输出取特征
两个模型的下采样步长不完全一致,融合时不能直接拿各自最后一层特征拼接。常见的做法是在 stage3 或 stage4 之后取特征,此时特征图分辨率在 14×14 左右,空间位置信息保留得比较好,对小病斑更友好。PyTorch 里最方便的方式是用中间层 hook 观察形状。
import torch from torchvision.models import shufflenet_v2_x1_0, mobilenet_v3_small backbone_s = shufflenet_v2_x1_0(weights=None) backbone_m = mobilenet_v3_small(weights=None) feat_shape = {} def make_hook(name): def hook(module, input, output): feat_shape[name] = output.shape return hook # 挂到目标 stage 输出位置,节点名以实际 torchvision 版本为准 backbone_s.stage4.register_forward_hook(make_hook("shuffle_stage")) backbone_m.features[16].register_forward_hook(make_hook("mobilenet_stage")) dummy = torch.randn(1, 3, 224, 224) backbone_s(dummy) backbone_m(dummy) print(feat_shape)这段代码的作用是明确两个主干特征图的通道数和分辨率。backbone_s.stage4是 ShuffleNetV2 最后一个下采样 stage,输出通道通常为 192;backbone_m.features[16]是 MobileNetV3-Small 的一个中间层,具体索引需要先打印模型结构确认。如果两侧分辨率不一致,需要在融合层里进行插值对齐,或者换一个 stride 相同的节点。选节点时不要只看通道数,还要看该位置是否处于注意力模块前后,SE 模块后面的特征更适合做融合。
3. 融合模型结构设计与 PyTorch 实现
3.1 特征级融合还是决策级融合
病害类别之间的视觉差异很小,很多早期病斑和健康叶片的色差不到 10 个灰度级。决策级融合只对两个模型最终的分类概率做平均或投票,在浅层网络遇到这种难样本时,两个模型的错误可能同时发生,融合后仍然分错。特征级融合则不同:它在空间分辨率还算完整的地方把两路特征拼起来,让后续卷积自己学会选择哪一路信息更可信。
特征级融合有两种常见形式:拼接和相加。拼接会让通道数翻倍,但保留的信息完整;相加节省内存,却要求两路特征的通道语义基本一致。在农业病虫害识别中,ShuffleNetV2 和 MobileNetV3 的通道语义并不同源,直接相加可能互相抵消,所以我选择拼接后再用 1×1 卷积压缩。这个 1×1 卷积本质上是做通道间的线性组合,可以理解成可学习的加权融合。
3.2 主干裁剪与融合层实现
两个主干原本都带着全连接分类头,融合前需要把它们裁剪掉。裁剪位置可以放在某个 stage 之后,不一定非要用完整主干。下面是一个可运行的融合模型骨架,使用create_feature_extractor抽取中间特征。
import torch import torch.nn as nn import torch.nn.functional as F from torchvision.models import shufflenet_v2_x1_0, mobilenet_v3_small from torchvision.models.feature_extraction import create_feature_extractor class FeatureFusionLayer(nn.Module): def __init__(self, in_ch1, in_ch2, proj_ch=128): super().__init__() # 拼接后先用 1x1 卷积压缩通道,减少后续计算量 self.proj = nn.Sequential( nn.Conv2d(in_ch1 + in_ch2, proj_ch, 1, bias=False), nn.BatchNorm2d(proj_ch), nn.Hardswish(inplace=True) ) def forward(self, x1, x2): # 空间分辨率不一致时先插值到相同大小 if x1.shape[-2:] != x2.shape[-2:]: x2 = F.interpolate(x2, size=x1.shape[-2:], mode="bilinear", align_corners=False) fused = torch.cat([x1, x2], dim=1) return self.proj(fused) class ShuffleMobileFusion(nn.Module): def __init__(self, num_classes=10, proj_ch=128): super().__init__() # 抽取节点名要按实际 torchvision 版本调整 self.extractor_s = create_feature_extractor( shufflenet_v2_x1_0(weights=None), return_nodes={"stage4": "s"} ) self.extractor_m = create_feature_extractor( mobilenet_v3_small(weights=None), return_nodes={"features.16": "m"} ) self.fusion = FeatureFusionLayer(in_ch1=192, in_ch2=96, proj_ch=proj_ch) self.head = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.3), nn.Linear(proj_ch, num_classes) ) def forward(self, x): feat_s = self.extractor_s(x)["s"] feat_m = self.extractor_m(x)["m"] fused = self.fusion(feat_s, feat_m) return self.head(fused)代码里in_ch1和in_ch2必须和实际抽取节点的输出通道对应。ShuffleNetV2 1.0x 的stage4输出一般是 192 通道,MobileNetV3-Small 在相近深度的特征层输出一般是 96 通道。create_feature_extractor返回的是字典,key 是return_nodes里设置的名称,这样才能保证 forward 里取到的不是整网输出。融合层中的插值分支只在分辨率不匹配时才生效,正式部署时最好保证两个节点 stride 一致,避免引入额外的 resize 算子,因为板端工具链对动态插值的支持通常较差。
3.3 输出层与损失函数设计
融合后的特征图经过全局平均池化后变成一维向量,再接全连接层。病虫害数据集普遍存在类别不平衡,健康叶片样本远多于发病样本,因此 dropout 之后可以再加 label smoothing。输出层的proj_ch要和融合层的压缩通道保持一致,否则全连接输入维数对不上。
3.4 轻量化优化:剪枝与量化
融合模型虽然有轻量骨干,但两路特征拼接后仍有不少冗余卷积核。常用做法是结构化剪枝,按卷积核 L1 范数排序,剪掉贡献小的滤波器,这样不会产生不规则稀疏,硬件加速也更友好。
import torch def prune_conv_by_l1(conv, prune_ratio=0.2): weight = conv.weight.data # out_c, in_c, kh, kw l1_norm = weight.abs().sum(dim=(1, 2, 3)) keep_num = int(conv.out_channels * (1 - prune_ratio)) keep_idx = torch.argsort(l1_norm, descending=True)[:keep_num] conv.weight.data = weight[keep_idx] conv.out_channels = keep_num return conv剪枝时要注意,当前层输出通道变了,下一层卷积的输入通道也必须同步裁剪,否则 tensor shape 对不上。我一般对融合层里的 1×1 卷积做剪枝收益最高,因为它直接压缩两侧拼接后的通道。剪枝后必须做 10 到 20 个 epoch 的微调,否则精度会掉 3 到 5 个点。量化方面,PyTorch 官方提供了 post-training quantization,嵌入式 Linux 上的 ARM CPU 推理可以用qnnpack后端。
import torch.ao.quantization as quant model.eval() model.qconfig = quant.get_default_qconfig("qnnpack") quant.prepare(model, inplace=True) # 用约 200 张训练集图片做校准,只 forward 不 backward with torch.no_grad(): for img, _ in calib_loader: model(img) quant.convert(model, inplace=True)qnnpack是面向 ARM 设备的量化后端,量化后模型权重变成 int8,内存占用大约下降到原来的四分之一。校准集不能太少,我通常取验证集中覆盖每个类别的图片各 10 张,这样量化时每个通道的数值范围不会偏。如果目标板子是 NPU,一般不会直接部署 PyTorch 量化权重,而是导出 ONNX 后交给厂商工具链转成专用格式。
4. 从数据集到训练:病虫害识别训练细节
4.1 数据采集与标注
农业病虫害识别效果好不好,数据质量比网络结构更关键。公开数据集里 PlantVillage 使用最普遍,但它是实验室背景,叶片边缘干净。真正田间采集的图有泥土、水滴、遮挡和其他叶片干扰,训练前需要加入田间负样本,否则模型会把粗糙背景纹理误判成病斑。
标注分类图像时,我推荐一张图只保留一个主导类别。如果一张叶片上同时有稻瘟病和胡麻斑病,建议把这张图放到一个单独的混合类别里,而不是强行归到某一类。这样模型输出层的类别语义更清晰,部署后报警也更可靠。标注工具用 LabelImg 做检测框,用 X-AnyLabeling 做多边形分割,分类任务直接按目录命名即可。
4.2 预处理与增强策略
训练时增强不能只用水平翻转。田间叶片姿态随机,光照变化剧烈,还需要加入旋转、裁剪、颜色扰动和随机遮挡。下面是常用的训练增强配置。
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.6, 1.0)), transforms.RandomRotation(30), transforms.ColorJitter(hue=0.1, saturation=0.3, brightness=0.3), transforms.RandomErasing(p=0.3, scale=(0.02, 0.15)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ])这里的RandomResizedCrop把 crop 比例下限设为 0.6,是为了让模型看到叶片局部放大的病斑区域。RandomErasing模拟叶片被遮挡的情况,避免模型只依赖整片叶子的形状做判断,这个操作对嵌入式场景很重要,因为摄像头视野里经常有其他叶片挡过来。归一化均值方差使用 ImageNet 标准值,因为两个骨干的预训练权重都是基于 ImageNet 分布训练的。
4.3 训练超参数与优化器选择
融合模型两个骨干的梯度量级不同,训练时不能只靠 Adam 一路跑到低学习率。我通常先冻结两个骨干,只训练融合层 5 个 epoch,再解冻全部网络,用 SGD 配合 CosineAnnealing 训练。
| 超参数 | 推荐值 | 说明 |
|---|---|---|
| 优化器 | SGD momentum=0.9 | 比 Adam 泛化更稳定 |
| 初始学习率 | 0.001 | 解冻后使用 |
| 学习率策略 | CosineAnnealingLR | 搭配 10 epoch warmup |
| 损失函数 | CrossEntropyLoss(label_smoothing=0.1) | 缓解过拟合 |
| Batch Size | 32 | 输入 224×224 时显存占用适中 |
| Epoch | 60 | 小数据集 60 足够 |
训练循环里注意加梯度裁剪。ShuffleNetV2 分支的梯度尺度通常比 MobileNetV3 分支更小,融合后整体 loss 对两个分支的敏感度不同,梯度裁剪可以避免某一侧更新过快。
import torch import torch.nn as nn from torch.optim.lr_scheduler import CosineAnnealingLR model = ShuffleMobileFusion(num_classes=10).to(device) criterion = nn.CrossEntropyLoss(label_smoothing=0.1) optimizer = torch.optim.SGD( model.parameters(), lr=0.001, momentum=0.9, weight_decay=5e-4 ) scheduler = CosineAnnealingLR(optimizer, T_max=60) for epoch in range(60): model.train() for images, labels in train_loader: logits = model(images.to(device)) loss = criterion(logits, labels.to(device)) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 10.0) optimizer.step() scheduler.step()label_smoothing=0.1会让模型输出不再过分自信,对病害类别间特征重叠的情况有帮助。clip_grad_norm_的 max norm 设为 10.0,只是把异常大的梯度拉回来,不会干扰正常收敛。
4.4 评估指标与常见坑
训练时监控 accuracy 还不够。农业病虫害识别里最常见的问题是模型把所有叶片都预测为健康类别,因为健康样本占比太高。这时候要看每个类别的召回率和 F1,而不是只看总体准确率。评估代码里至少打印每个类别的 precision、recall,再看混淆矩阵确定哪些类别互相混。
如果某个早期病斑类别召回率低,优先调整RandomResizedCrop的 scale 下限从 0.6 降到 0.4,让训练时更多看到病斑局部放大图。如果精确率低,说明模型把健康叶片的正常纹理误判为病斑,这通常是训练集里病斑照片光照偏暗导致的,可以在增强里加入亮度调整,并补充自然光下拍摄的健康叶片负样本。
5. 嵌入式部署:模型转换、量化与板端性能调优
5.1 从 PyTorch 到板端运行时
PyTorch 模型最终要转成板端工具链识别的格式。常见路径是导成 ONNX,再用 NCNN、RKNN、OpenVINO 或 TensorRT 转换。导出前要固定输入尺寸和动态轴。嵌入式 NPU 对动态 shape 支持不稳定,静态模型部署更保险。
model.eval() dummy = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy, "pest_fusion.onnx", input_names=["input"], output_names=["logits"], opset_version=11, dynamic_axes={"input": {0: "batch"}, "logits": {0: "batch"}}, )opset_version=11兼容大多数工具链。如果板端 SDK 支持更新版本,再提高到 13。MobileNetV3 里的 HardSwish 激活函数在老工具链上容易报不支持,可以提前把nn.Hardswish改写成ReLU6(x + 3) / 6的等价形式,再导一次。
5.2 板端性能调优与系统裁剪
ONNX 转成板端 runtime 之后,性能调优不在模型层,而在数据流和系统层。我会先关掉 Linux 桌面服务,用systemd只保留推理进程依赖的服务,把系统空载 CPU 占用压到 2% 以下。嵌入式 Linux 部署时还要裁剪设备树,关闭用不到的 USB、串口、HDMI 节点,相关的驱动模块不加载,可以释放几十 MB 内存和一部分中断开销。
推理侧采用多线程流水线:采集线程只抓帧,推理线程用双 buffer 交替处理。图像从摄像头 buffer 直接送到 NPU 输入,不做 CPU 拷贝。如果是 RKNN,开启 zero-copy 后帧率能提升 5 到 8 帧。
5.3 验证推理耗时的一个可靠方法
性能测试别信单次耗时,要用 500 帧统计 P99 延迟。板端设备偶发频率降级时,平均延迟看不出问题,P99 会暴露丢帧风险。
import time latencies = [] for _ in range(500): image = camera_read() t0 = time.perf_counter() logits = infer(image) latencies.append(time.perf_counter() - t0) latencies.sort() avg_ms = sum(latencies) / len(latencies) * 1000 p99_ms = latencies[int(len(latencies) * 0.99)] * 1000 print(f"avg: {avg_ms:.1f}ms, p99: {p99_ms:.1f}ms")测耗时期间要关掉日志打印和画面叠加,否则 I/O 会明显拉高延迟。如果 P99 超过 100ms,优先检查是不是摄像头采集被阻塞,再考虑输入分辨率从 224 降到 192。模型融合虽然提高识别率,但两路主干始终有串行开销,在边缘 AI 部署中若延迟压不住,可以把 MobileNetV3 分支的 SE 模块替换成全局平均池化加 1×1 卷积的简化注意力,算子种类更少,NPU 上的延迟通常会降得更明显。
本文还有配套的精品资源,点击获取