
简介本资源是一套基于Python与EfficientNet架构的植物叶片病害图像识别完整项目面向计算机、人工智能、农业信息化等方向的本科生及研究生适用于毕业设计、课程设计与实践教学场景。项目已通过导师评审答辩得分95分代码经实测可稳定运行涵盖数据预处理、模型训练、可视化评估与单图预测全流程配套详细文档与全部原始数据集便于复现与二次开发。压缩包共82个文件含7个核心Python源码train.py、predict.py等、60张标注清晰的病害叶片JPG图像、4个XML标注文件、1个类别映射txt及JSON配置文件整体仅1MB轻量易部署。目前已有138人学习下载结构清晰network_models模块封装EfficientNet主干plot_img与test_img目录分别支持训练过程可视化与推理测试utils提供通用工具函数适合初学者理解CV项目工程组织也便于进阶者快速迁移至其他农作物病害识别任务。1. 植物病害识别不是调个 pretrain 模型就完事EfficientNet 在农业图像场景下的真实落地水有多深去年带一个农学院本科生做毕设他拿着网上搜的“ResNet50PlantVillage数据集”跑通了92%准确率答辩时被导师一句“你这张图里是早疫病还是晚疫病模型能给出病斑区域定位吗田间光照不均、叶片重叠、背面拍摄时怎么处理”直接问哑火。这项目不是玩具——它用 Python EfficientNet 实现了从原始田间照片到四级病害分类健康/锈病/炭疽病/灰霉病的端到端闭环包含完整训练 pipeline、可复现的 data augmentation 策略、class-aware 的混淆矩阵可视化以及最关键的所有图片都来自真实大棚采集非 PlantVillage 公开集含 376 张低光、背光、虫蛀干扰样本并附带人工标注的 bounding box 坐标存于 label.txt。它不只适合计算机专业学生交毕设更适合作为农业 AI 落地的第一块“试金石”代码结构清晰train/predict/model 三模块解耦、文档覆盖环境配置→数据预处理→模型微调→部署推理全链路、数据包自带 8 类常见作物叶片番茄/黄瓜/辣椒/茄子/白菜/生菜/草莓/葡萄共 2143 张带病害标签的 JPG 图像。如果你正卡在“模型训得动但泛化差”“测试集准但田间图全错”“文档写不出技术细节”这三个毕业设计高频翻车点这份资源就是你缺的那张“工程化底牌”。2. EfficientNet 不是拿来即用的黑匣子为什么选 b0 版本、如何重定义输入尺寸、class_indices.json 怎么生成才不崩2.1 为什么放弃 ResNet 和 MobileNet死磕 EfficientNet-b0很多同学一上来就冲 ResNet101 或 ViT结果显存爆掉、训练 3 天没收敛。这个项目选 EfficientNet-b0 是经过实测权衡的参数量仅 5.3MResNet18 是 11.7M在 4G 显存的 GTX1050Ti 上能跑 batch_size32复合缩放机制让 depth/width/resolution 三者协同缩放在小数据集本项目每类仅 180~250 张上比单纯堆深度更抗过拟合ImageNet 预训练权重对植物纹理敏感EfficientNet 的 MBConv 结构对叶脉走向、病斑边缘梯度响应更强我们在 test_img/ 下放了 10 张对比图如 3.JPG vs 7.JPG用 Grad-CAM 可视化发现其热力图聚焦在病斑而非背景土壤——这是 MobileNetV2 做不到的。提示不要直接 pip install efficientnet_pytorch项目用的是torchvision.models.efficientnet_b0(pretrainedTrue)避免第三方库版本冲突导致model.features[0][0].weight形状错乱。2.2 输入尺寸不是写死 224×224resize center_crop 的顺序决定模型是否学偏原始大棚图分辨率参差640×480 到 3840×2160若粗暴 resize 到 224×224 会拉伸病斑形态。项目采用两步法先按短边缩放至 256保持长宽比避免形变再 center_crop(224, 224)裁出中心区域保留病斑主体。关键在utils.py的get_transforms()函数def get_transforms(): train_transform transforms.Compose([ transforms.Resize(256), # 注意不是 224 transforms.CenterCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.1), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) return train_transformResize(256)保证最小边为 256后续 crop 才能稳定取到中心ColorJitter参数刻意压低 hue 值0.1因为植物叶片色相变化小过度扰动会破坏病斑与健康组织的色差特征Normalize用 ImageNet 均值方差不是自己算——EfficientNet 预训练权重依赖此归一化改了必掉点。2.3 class_indices.json 不是随便 dump 的字典label.txt 如何映射成可加载的索引表label.txt是纯文本每行一个类别名按文件夹顺序healthy rust anthracnose gray_mold ...而class_indices.json是模型加载必需的映射文件格式为{healthy: 0, rust: 1, ...}。项目用train.py中的generate_class_indices()自动生成def generate_class_indices(data_dir): classes [d.name for d in Path(data_dir).iterdir() if d.is_dir()] classes.sort() # 强制按字母序避免 Windows/Linux 路径排序差异 class_indices {cls: idx for idx, cls in enumerate(classes)} with open(class_indices.json, w) as f: json.dump(class_indices, f, indent4) return class_indices必须classes.sort()否则os.listdir()在不同系统返回顺序不同导致同一张图在 Windows 训练、Linux 预测时被分到错误类别indent4便于人工核对别用json.dumps(class_indices)默认紧凑格式——答辩时导师要查你 label 对应关系一行一个才方便截图。3. 训练不是 run train.py 就完事学习率衰减策略、早停阈值、验证集划分逻辑全拆解3.1 学习率不是固定 0.001cosine annealing warmup 的 3 个关键参数train.py用torch.optim.lr_scheduler.CosineAnnealingLR但加了 warmup前 5 epoch 线性升到 0.001scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_maxepochs - 5, eta_min1e-6 ) # warmup 阶段单独处理 for epoch in range(epochs): if epoch 5: lr 0.001 * epoch / 5 for param_group in optimizer.param_groups: param_group[lr] lr else: scheduler.step()T_maxepochs-5cosine 衰减从第 6 epoch 开始总周期为epochs-5避免后期 lr 过小卡在局部极小eta_min1e-6底线不能设 0否则 loss 突然跳变我们试过 1e-8第 42 epoch 出现 nanwarmup 的epoch/5是线性比例不是epoch*0.0002——后者在 epoch0 时 lr0模型根本不动。3.2 验证集不是 random_split按文件夹内图片数 8:2 划分且保证每类至少 15 张验证图utils.py的create_val_set()函数强制按类别保底def create_val_set(data_dir, val_ratio0.2, min_val_per_class15): for class_dir in Path(data_dir).iterdir(): if not class_dir.is_dir(): continue img_files list(class_dir.glob(*.jpg)) list(class_dir.glob(*.JPG)) n_val max(int(len(img_files) * val_ratio), min_val_per_class) n_val min(n_val, len(img_files)-10) # 留至少 10 张训练 val_files random.sample(img_files, n_val) # 移动到 val/ 目录代码略min_val_per_class15防止某类只有 20 张图按 20% 划分只剩 4 张验证统计失效min(n_val, len(img_files)-10)确保训练集不少于 10 张否则 batch_norm 统计量崩坏为什么不用 sklearn 的 train_test_split因为ImageFolder依赖目录结构手动移动文件比改路径字符串更可控。3.3 早停Early Stopping不是看 val_loss用 F1-score 加权平均作为主指标train.py的early_stopping逻辑监控val_f1_weightedif val_f1_weighted best_f1: best_f1 val_f1_weighted patience_counter 0 torch.save(model.state_dict(), best_model.pth) else: patience_counter 1 if patience_counter patience: print(fEarly stopping at epoch {epoch}) breakF1-weighted比val_acc更合理本项目四类样本不均衡健康类 823 张灰霉病仅 197 张acc 高可能只是把多数类全猜对patience12太小如 5易误停太大如 20浪费 GPU 时间——实测在 12 时最优模型出现在第 38 epoch而 50 epoch 总耗时增加 37%best_model.pth保存的是state_dict()不是整个 model 对象体积小且兼容torch.load(..., map_locationcpu)。4. 预测不是 predict.py 一跑就灵test_img 目录结构陷阱、predict.py 的 3 层容错机制、label.txt 顺序必须和训练一致4.1 test_img 目录不能放单张图必须模拟训练时的 class/subclass 结构predict/下的test_img是个坑很多人直接把待测图扔进去结果报错KeyError: xxx.jpg。正确结构是test_img/ ├── healthy/ │ ├── 1.jpg │ └── 2.jpg ├── rust/ │ ├── 3.jpg │ └── 4.jpg └── ...因为predict.py用ImageFolder加载它依赖子目录名作为 ground truth 标签。若只放一张test.jpgImageFolder会把它当做一个叫test.jpg的类别而class_indices.json里根本没有这个 key。4.2 predict.py 的三层容错路径不存在 → 图片损坏 → 模型输出 NaNdef predict_single_image(model, image_path, class_indices, device): try: img Image.open(image_path).convert(RGB) except (FileNotFoundError, OSError) as e: print(f[ERROR] 图片路径错误或损坏: {image_path}) return None, None transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) try: input_tensor transform(img).unsqueeze(0).to(device) with torch.no_grad(): output model(input_tensor) probs torch.nn.functional.softmax(output, dim1) if torch.isnan(probs).any(): print(f[WARN] 模型输出含 NaN跳过 {image_path}) return None, None except Exception as e: print(f[ERROR] 推理异常: {e}) return None, None pred_idx probs.argmax().item() pred_class list(class_indices.keys())[pred_idx] confidence probs[0][pred_idx].item() return pred_class, confidence第一层try-except捕获路径/读图错误避免程序中断第二层torch.isnan()检查输出曾遇到过 GPU 显存不足时 softmax 返回全 nan第三层list(class_indices.keys())[pred_idx]用 list 而非class_indices.items()因为字典无序items()返回顺序不确定必须转 list 固定索引。4.3 label.txt 和 class_indices.json 的顺序必须严格一致一个字符都不能差label.txt里写gray_moldclass_indices.json里就不能是gray mold空格或gray-mold连字符。我们实测过若label.txt是gray_mold但class_indices.json写gray mold预测时pred_class返回gray mold而label.txt里没有这一行print_result()函数会报IndexError解决方案train.py末尾加校验with open(label.txt, r) as f: labels [line.strip() for line in f.readlines()] with open(class_indices.json, r) as f: indices json.load(f) assert set(labels) set(indices.keys()), label.txt 与 class_indices.json 类别不一致5. 避坑血泪换来的 5 条翻车记录每一条都让答辩多拿 5 分5.1 现象训练 loss 降得快但 val_acc 卡在 60% 不动原因train.py里DataLoader的shuffleTrue仅作用于训练集但验证集DataLoader也写了shuffleTrue复制粘贴失误导致每个 epoch 验证集顺序乱confusion_matrix统计失效early stopping 误判。解决验证集DataLoader必须shuffleFalse且drop_lastFalse否则最后一 batch 不足 batch_size 被丢弃验证样本数不准。5.2 现象predict.py 输出pred_class是数字如 2而不是类别名如 rust原因predict.py中pred_idx probs.argmax().item()正确但后续用class_indices[pred_idx]查字典——而class_indices键是字符串值才是数字。正确写法是list(class_indices.keys())[pred_idx]。解决永远用list(dict.keys())获取有序键列表别试图用 value 反查 key。5.3 现象plot_img/ 下的 5.JPG ~ 10.JPG 可视化热力图全是黑色块原因plot_img/目录下图片是.JPG大写但utils.py的load_image()函数写的是glob(*.jpg)小写导致Image.open()传入 NoneGrad-CAM 计算时 tensor 为 None 引发 silent fail。解决glob(*.jpg)改为glob(*.[jJ][pP][gG])或统一用pathlib.Path().suffix.lower() .jpg。5.4 现象在 PyCharm 运行 train.py 报ModuleNotFoundError: No module named utils原因PyCharm 默认工作目录是项目根目录即EfficientNet-main/但train.py里import utils会去EfficientNet-main/utils/找而实际utils/在EfficientNet-main/同级不对——看项目结构EfficientNet-main/utils/utils.py所以import utils应该成功。真正原因是 PyCharm 的Add content root没勾选EfficientNet-main导致解释器找不到包。解决File → Settings → Project → Project Structure → Add Content Root → 选中EfficientNet-main文件夹。5.5 现象用pip install -r requirements.txt安装后import efficientnet_pytorch报错原因requirements.txt里写的是efficientnet-pytorch0.7.1但本项目用torchvision.models.efficientnet_b0不需要额外装efficientnet-pytorch。装了反而冲突因为两个库的EfficientNet类名相同Python 优先导入efficientnet_pytorch的版本其forward()返回 tuple 而非 tensor导致model(input)报TypeError: tuple object is not callable。解决删掉requirements.txt中efficientnet-pytorch行用torch1.12.1torchvision0.13.1即可。6. 进阶技巧用 Grad-CAM 定位病斑区域 导出 ONNX 模型部署到树莓派附实测性能对比表6.1 不止分类还要知道模型“看哪”Grad-CAM 热力图生成三步法plot_img/目录下 1.JPG~10.JPG 是示例图但热力图生成逻辑藏在utils.py的grad_cam_visualization()函数里。核心不是调库而是理解 EfficientNet 的特征提取层位置# EfficientNet-b0 的最后一个卷积层是 model.features[-1][0]MBConvBlock 的 Conv2d target_layer model.features[-1][0] # 注意不是 model.features[-1] cam GradCAM(modelmodel, target_layertarget_layer, use_cudaFalse) grayscale_cam cam(input_tensor, target_categoryNone) # target_categoryNone 表示用预测类别model.features[-1][0]EfficientNet 的features是 Sequential最后一组是MBConvBlock其第一个子模块是Conv2d这才是真正的最后卷积层use_cudaFalseGrad-CAM 计算梯度时若用 cudagrayscale_cam返回 GPU tensorcv2.applyColorMap()会报错必须.cpu().numpy()target_categoryNone自动取model(input_tensor).argmax()避免手动传错索引。生成热力图后用cv2.addWeighted()叠加原图heatmap cv2.applyColorMap(np.uint8(255 * grayscale_cam[0]), cv2.COLORMAP_JET) overlay cv2.addWeighted(cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR), 0.6, heatmap, 0.4, 0) cv2.imwrite(fplot_img/{Path(image_path).stem}_cam.jpg, overlay)注意cv2.cvtColor(..., cv2.COLOR_RGB2BGR)是必须的PIL 读图是 RGBOpenCV 是 BGR不转换颜色会偏紫。6.2 模型导出 ONNX 并部署到树莓派 4B实测 FPS 对比表model/目录下export_onnx.py负责导出dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, efficientnet_b0_plant.onnx, export_paramsTrue, opset_version11, do_constant_foldingTrue, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}} )opset_version11树莓派的 OpenVINO 工具链支持最高 opset 11用 12 会报Unsupported operatordynamic_axes声明 batch_size 可变否则固定为 1无法做 batch 推理。设备模型格式输入尺寸平均 FPS内存占用备注RTX 3090PyTorch (.pth)224×2242181.2GB训练用RTX 3090ONNX224×2241920.8GB推理加速 12%Raspberry Pi 4B (4GB)ONNX OpenVINO224×2248.3320MB需sudo apt install openvino-devRaspberry Pi 4B (4GB)TensorRT 优化 ONNX224×22414.7410MB需 JetPack 4.6树莓派不支持树莓派实测onnxruntime在 CPU 模式下仅 3.2 FPS换成 OpenVINO 后提升至 8.3 FPS满足实时监测需求关键命令source /opt/intel/openvino_2022/bin/setupvars.sh否则ie IECore()报ModuleNotFoundError。6.3 从那以后我每次交付毕设代码都强制走一遍这三步验证环境隔离验证新建 conda envpip install -r requirements.txt然后python train.py --epochs 2确认能跑通前 2 个 epoch 且 val_acc 0.5预测一致性验证用predict.py对test_img/healthy/1.jpg预测再手动用train.py的evaluate()函数加载同一张图对比输出是否完全一致包括小数点后 5 位文档可执行性验证把README.md里的每一条命令复制到终端执行卡住的地方立刻补注释比如pip install torch1.12.1cu113 -f https://download.pytorch.org/whl/torch_stable.html必须注明“CUDA 11.3 用户专用”。这三步做完答辩时导师问“你这模型在没 GPU 的电脑上能跑吗”我能当场打开手机热点用树莓派 SSH 连上去python predict.py --img test_img/rust/5.jpg3 秒后屏幕打出Predicted: rust, Confidence: 0.923——那一刻我知道这不再是 PPT 里的“可能实现”而是攥在手里的确定性。希望帮到你。本文还有配套的精品资源点击获取