简介:面向计算机相关专业毕业设计及Python项目实战学习者,这份基于卷积神经网络的蘑菇识别微信小程序源码包,提供从模型训练、后端接口到小程序前端的完整可运行方案。压缩包共264个文件,涵盖47个Java后端源码、32个Vue前端页面、98张JPG与49张PNG图像数据集,以及SQL、YML配置、Maven打包脚本等,整体大小60.27MB,目录结构清晰,便于直接导入调试。项目采用MobileNetV2轻量化网络作为蘑菇图像分类的特征提取网络,参数量减少60%,前端采用Taro框架、后端基于SpringBoot,并通过Nginx进行请求转发;同时利用@ControllerAdvice统一异常处理,对蘑菇学名字段建立索引优化查询。教程文档、训练好的模型和全部数据一并打包,下载后可按文档完成环境配置与部署,适合作为毕业设计、课程设计或期末大作业。目前已有186人学习下载,是一份经过严格调试、即下即用的高分项目参考。
1. 这个“蘑菇识别微信小程序”,难点根本不在小程序外层壳
我把标题拆开看,真正值分的是“基于卷积神经网络”这条链路:图像能不能被正确分类,决定了你这个微信小程序是演示玩具还是可答辩作品。很多毕业设计做起来觉得工作量不够,是因为把精力花在界面动画和按钮皮肤上,CNN 部分却直接用预训练模型跑个 demo,导师问一句“为什么用这个卷积核”就答不上来。实际上,只要你把蘑菇数据集、MobileNetV2 微调、推理接口、小程序上传图片这几个环节串成闭环,整套源码、全部数据、训练好的模型自然就成了“高分项目”的底气。适合谁读?拿到这种打包资源后想自己跑通、敢在答辩时讲清楚原理,也需要复用到其他图像分类场景的准毕业生和初级算法工程师。适合从零搭一套,而不是买一个看不懂的 black box。
2. 蘑菇识别卷积神经网络:迁移学习比从零搭 VGG 更实际
2.1 为什么选 MobileNetV2 而不是自己搭 VGG
卷积神经网络的基本单位不外乎卷积、池化、步长、核和填充,但面向蘑菇识别这种有限类别的分类任务,最稳的方案是迁移学习。我一般不会从 Conv->ReLU->Pool 手写十几层,因为你的集数据(尤其蘑菇这种野外拍摄数据集)往往只有几千到一两万张,从头训练一个深层网络,收敛慢还容易过拟合。MobileNetV2 的深度可分离卷积(depthwise separable convolution)让模型参数量远小于 VGG16,同时 ImageNet 预训练的权重已经学会了纹理、边缘、颜色区域这些底层特征。
蘑菇识别的难点在于同一科属下形态接近,比如白毒鹅膏菌和可食用的草菇,可能只差菌盖边缘和菌褶颜色。这类细微差别需要网络的高层特征,底层卷积核反而不需要重新学。因此我的做法是:保留 MobileNetV2 的 features 部分,替换最后的分类器,微调最后两到三个残差块。这样既避免了被预训练特征带偏,又不会因为完全冻结导致模型对蘑菇的特定纹理不敏感。
2.2 蘑菇图像数据集的目录结构与预处理
先看一下我推荐的数据集组织方式,它同时兼容 PyTorch 的ImageFolder和后面要写的 FastAPI 接口:
mushroom_dataset/ ├── train/ │ ├── Agaricus_bisporus/ │ ├── Amanita_muscaria/ │ ├── Cantharellus_cibarius/ │ ├── Lentinula_edodes/ │ └── Pleurotus_ostreatus/ ├── val/ │ └── ...(与 train 的类别文件夹一致) └── test/ └── ...(单张图片或者保留类别文件夹,用于最终评估)类别文件夹的名字就是蘑菇学名,排序时必须保持一致。
from torchvision import datasets, transforms train_transform = transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(0.5), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_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]) ]) train_dataset = datasets.ImageFolder('mushroom_dataset/train', train_transform) val_dataset = datasets.ImageFolder('mushroom_dataset/val', val_transform) print(train_dataset.classes) # 类别列表,注意后面要保存成 JSON print(train_dataset.class_to_idx)ImageFolder会自动读文件夹名作为标签,class_to_idx按字母序编号。训练时所有图片被缩放到 256 再随机裁剪到 224;验证集用中心裁剪,保证评估结果稳定。注意这里隐含的顺序问题:训练和推理的预处理必须用同一套Normalize参数,否则模型推理概率会是乱的,这是最容易翻车的点。
2.3 用 PyTorch 微调卷积神经网络的完整代码
如果你拿到的是一个已经训练好的模型文件,第 2.2 节的数据增强主要是让你理解数据来源;如果你想重新训练,下面这段可以直接抄,跑在 Colab 或者带 GPU 的机器上都行:
import torch import torch.nn as nn import torch.optim as optim from torchvision import models from torch.optim.lr_scheduler import ReduceLROnPlateau from tqdm import tqdm num_classes = len(train_dataset.classes) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 加载 ImageNet 预训练的 MobileNetV2,Api 较新的改名成 weights model = models.mobilenet_v2(weights=models.MobileNet_V2_Weights.IMAGENET1K_V1) model.classifier[1] = nn.Linear(model.classifier[1].in_features, num_classes) model = model.to(device) # 冻结 features 的前 14 层,微调最后的特征块和分类器 for name, param in model.features.named_parameters(): if int(name.split(".")[0]) < 14: param.requires_grad = False criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4, weight_decay=1e-2) scheduler = ReduceLROnPlateau(optimizer, mode="min", factor=0.5, patience=2) train_loader = torch.utils.data.DataLoader( train_dataset, batch_size=32, shuffle=True, num_workers=4) val_loader = torch.utils.data.DataLoader( val_dataset, batch_size=32, shuffle=False, num_workers=4) best_acc = 0.0 for epoch in range(1, 31): model.train() running_loss = 0.0 for images, labels in tqdm(train_loader, desc=f"Epoch {epoch}"): images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) model.eval() correct, total = 0, 0 val_loss = 0.0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) val_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() val_acc = correct / total val_loss_avg = val_loss / len(val_dataset) scheduler.step(val_loss_avg) print(f"Epoch {epoch}: loss={running_loss/len(train_dataset):.4f}, " f"val_loss={val_loss_avg:.4f}, val_acc={val_acc:.4f}") if val_acc > best_acc: best_acc = val_acc torch.save({ "model_state_dict": model.state_dict(), "class_to_idx": train_dataset.class_to_idx, "classes": train_dataset.classes, "input_size": 224, "best_acc": best_acc }, "mushroom_mobilenet_v2.pth")为什么要用AdamW而不是Adam?蘑菇数据集噪声大,使用权重衰减能抑制边界分布过大的权重,对细粒度分类更稳。学习率 1e-4 是微调 MobileNetV2 的常见值,如果你冻结层数少,学习率可以降到 5e-5。ReduceLROnPlateau在验证损失连续两个 epoch 不降后把学习率砍半,避免后期在最优解附近震荡。
2.4 训练时的核心参数对照与失败信号
下面这张表是我做蘑菇分类时反复调整的参考值,新手可以直接按左列起步:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 输入分辨率 | 224x224 | 与 MobileNetV2 预训练保持一致,过大增加延迟 |
| batch_size | 16-32 | 显存不够就 16,同时可配合梯度累积 |
| 学习率 | 1e-4 | 主干微调用 1e-4,分类头可以用 1e-3 |
| 冻结层数 | features 前 14 层 | 冻结越多训练越快,但 acc 可能上限低 |
| 迭代数 | 30 epoch | 配合早停,通常 15-20 轮收敛 |
| 数据增强 | 随机裁剪+翻转+颜色抖动 | 蘑菇拍摄环境光照和角度变化大 |
| 损失函数 | CrossEntropyLoss | 类别不均衡时加weight参数 |
如果训练集 loss 下降但验证 loss 快速回升,优先检查是不是数据增强强度不够,或者类别数太少导致网络过拟合。另一个常见错误是ImageFolder的排序和你预训练的类别文件不一致,保存模型时一定要把classes列表一并存进 pth 文件,同时单独导出label_map.json给小程序后端复用。
{ "0": "Agaricus_bisporus", "1": "Amanita_muscaria", "2": "Cantharellus_cibarius", "3": "Lentinula_edodes", "4": "Pleurotus_ostreatus" }3. 微信小程序端识别链路:从拍照到拿到 CNN 结果
3.1 构造推理接口:FastAPI 接收图片返回蘑菇标签
微信小程序不能直接加载pytorch,所以要把训练好的模型包成一个 HTTP 服务。我用 FastAPI 最多,因为python-multipart能直接处理文件上传,响应是 JSON,小程序端不需要自己造协议。下面是一个完整的推理服务:
import io import json import torch import torch.nn as nn from fastapi import FastAPI, File, UploadFile from PIL import Image from torchvision import transforms, models app = FastAPI() device = torch.device("cpu") weights_path = "mushroom_mobilenet_v2.pth" # 读取训练时保存的类别列表 with open("label_map.json", "r", encoding="utf-8") as f: label_map = json.load(f) num_classes = len(label_map) model = models.mobilenet_v2() model.classifier[1] = nn.Linear(model.classifier[1].in_features, num_classes) ckpt = torch.load(weights_path, map_location=device) model.load_state_dict(ckpt["model_state_dict"]) model.eval().to(device) infer_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]) ]) @app.post("/predict") async def predict(file: UploadFile = File(...)): image_bytes = await file.read() image = Image.open(io.BytesIO(image_bytes)).convert("RGB") tensor = infer_transform(image).unsqueeze(0) with torch.no_grad(): logits = model(tensor) probs = torch.nn.functional.softmax(logits, dim=1)[0] top_prob, top_idx = torch.topk(probs, 1) label = label_map[str(top_idx.item())] return { "label": label, "probability": round(top_prob.item(), 4), } if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8080)这里的model = models.mobilenet_v2()不传预训练权重,因为推理时直接加载自己的 state_dict。接口只做两件事:把图片字节流转换成 PIL Image,再走与验证集完全一致的infer_transform。topk取概率最高的类别,注意概率值要round成四位小数,小程序端显示会更干净,也避免浮点过长导致布局错乱。
提示:如果你用的是训练好的完整模型文件而不仅是 state_dict,下面是兼容写法。
# 兼容 torch.save(model.state_dict()) 和 torch.save(model) 两种方式 state = torch.load(weights_path, map_location="cpu") if "model_state_dict" in state: model.load_state_dict(state["model_state_dict"]) else: model.load_state_dict(state)3.2 小程序页面收集图片:wxml 与 wx.chooseMedia 配合
小程序端不需要前端算法,只负责把照片传给后端再展示结果。一个能跑通的页面至少要有图片容器、按钮、识别结果三个部分:
<!-- pages/index/index.wxml --> <view class="container"> <image wx:if="{{imagePath}}" src="{{imagePath}}" mode="aspectFit"></image> <button type="primary" bindtap="chooseMushroom">拍摄或选择蘑菇</button> <block wx:if="{{result}}"> <text class="label">识别结果:{{result.label}}</text> <text class="prob">置信度:{{result.probability}}</text> </block> <view wx:if="{{loading}}" class="loading">识别中...</view> </view>对应的index.js:
// pages/index/index.js Page({ data: { imagePath: '', result: null, loading: false }, chooseMushroom() { wx.chooseMedia({ count: 1, mediaType: ['image'], sourceType: ['album', 'camera'], success: (res) => { const tempFilePath = res.tempFiles[0].tempFilePath; this.setData({ imagePath: tempFilePath, result: null }); this.uploadImage(tempFilePath); } }); }, uploadImage(filePath) { this.setData({ loading: true }); wx.uploadFile({ url: 'http://192.168.1.101:8080/predict', filePath: filePath, name: 'file', timeout: 15000, success: (res) => { const data = JSON.parse(res.data); this.setData({ result: data }); }, fail: (err) => { wx.showToast({ title: '识别失败,请检查服务', icon: 'none' }); }, complete: () => { this.setData({ loading: false }); } }); } });wx.chooseMedia是新版本基础库推荐代替wx.chooseImage的 API,返回的tempFilePath可以直接作为wx.uploadFile的filePath。name: 'file'必须和 FastAPI 的UploadFile = File(...)参数名对应,后端如果写的是image,这里就要改成name: 'image'。
3.3 wx.request 和 wx.uploadFile 的核心差异
这里的接口只用了wx.uploadFile,没有用wx.request,是因为上传文件时表单格式不一样:
| 对比项 | wx.uploadFile | wx.request |
|---|---|---|
| 数据格式 | multipart/form-data | application/json |
| 适用场景 | 图片、音视频文件 | 纯文本 JSON 交互 |
| 返回类型 | res.data 是字符串 | res.data 可直接是对象 |
| 控制超时 | timeout 参数 | 同样支持 timeout |
如果后端需要额外传 token,可以在wx.uploadFile的formData字段加,比如formData: { userId: 'stduent001' }。后端用UploadFile接收图片文件,其余文本字段从Form(...)取,而不是File(...),这个区别写接口时最容易漏。
4. 训练好的模型落地:类别映射、图片压缩和真机调试
4.1 服务端加载模型后,先跑一张确定的多类别图片验证
真正被调给小程序之前,先不要启动 uvicorn 就盲目测。我有一次遇到的诡异问题是:识别准确率在测试集是 96%,接口一跑几乎全部分到同一个类别,最后发现是torch.load加载到了 GPU 的state_dict键名带了module.后缀(因为多卡训练时包了nn.DataParallel)。解决办法要么训练时torch.save前加model.module.state_dict(),要么加载后手动去掉前缀:
state = torch.load(weights_path, map_location="cpu") new_state = {} for key in state: new_key = key[7:] if key.startswith("module.") else key new_state[new_key] = state[key] model.load_state_dict(new_state)加载之后,在本地用一张测试图片调用model输出概率,确认 softmax 最高类是否符合直觉。如果概率分布几乎均匀,优先怀疑三个原因:图片预处理没对齐、类别索引顺序不对、模型输入通道用了 BGR 而不是 RGB。PIL 读图默认 RGB,OpenCV 读图是 BGR,统一以训练时的读取方式为准。
4.2 小程序端图片压缩和预览的边界
手机拍照一张图常是 3~8 MB,直接上传会拖慢 FastAPI 的推理链路。wx.chooseMedia拿到临时文件后,可以先用wx.compressImage压到 1280px 左右再上传:
compressImage(src) { wx.compressImage({ src, quality: 80, success: (res) => { const compressed = res.tempFilePath; this.uploadImage(compressed); } }); }Resize(256)接受任意尺寸的图片,压缩不会直接挂掉,但压缩过度会影响菌盖纹理细节。经验值是quality: 80对蘑菇这种非极端场景足够。还有一点:小程序的image组件默认mode="scaleToFill",会把正方形照片拉伸适配,但这只是前端展示,不会改变上传到接口的原始文件,不需要在mode上过度纠结。
4.3 真机请求 localhost 失败的三个排错顺序
开发工具里能跑通,真机一上传就报request:fail,这是微信小程序最常见的坑。顺序排查:
- 后端不能只监听
127.0.0.1,必须监听0.0.0.0,否则外部设备访问不到,对应启动命令是uvicorn run:app --host 0.0.0.0 --port 8080。 - 真机和电脑必须在同一局域网,
url里写电脑内网 IP,不能写localhost。 - 微信公众平台需要配置 request 合法域名,并且域名必须备案、支持 HTTPS。纯本地演示阶段可以打开开发者工具右上角“详情-本地设置-不校验合法域名”,这个设置只对当前项目开发有效,真机体验版仍会受限,答辩展示时最好准备一个内网穿透或云服务器。
提示:如果地址栏使用 HTTP 协议,普通小程序生产环境不允许调用;如果只是开发调试,用“开发版”+“不校验合法域名”即可。
4.4 置信度阈值:比单一“蘑菇种类”更让导师认可的设计
训练好的模型返回的probability不是万能的。在蘑菇识别这种安全敏感场景中,如果最高概率小于 0.85,我建议不要返回具体种类,而是返回“无法确认,请勿食用”。这个逻辑不属于 CNN 训练,但属于智慧。在后端加一个阈值判断:
if top_prob.item() < 0.85: return { "label": "unrecognized", "probability": round(top_prob.item(), 4), "message": "图片模糊或不在模型覆盖范围内,请勿按此结果食用" }阈值自然会影响“正确率”指标。你在项目文档里写明“不确定性拒绝机制”,能直接体现你对模型的校准理解,这是很多模板项目中看不到的细节。
5. 用混淆矩阵和错题本给这个蘑菇识别项目“收口”
5.1 一个脚本生成混淆矩阵热力图
在 test 集上统计各类别之间的混淆,是毕业设计答辩提问的高频落点。用 sklearn 就能把真实标签和模型预测对起来:
import numpy as np import torch from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt all_preds, all_labels = [], [] model.eval() with torch.no_grad(): for images, labels in test_loader: images = images.to(device) labels = labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm = confusion_matrix(all_labels, all_preds) disp = ConfusionMatrixDisplay( confusion_matrix=cm, display_labels=test_dataset.classes ) disp.plot(xticks_rotation=45) plt.savefig("mushroom_confusion_matrix.png", bbox_inches="tight")输出图片放到论文或小程序项目文档里,比一张系统的截图更有说服力。混淆矩阵的横轴是预测类别,纵轴是真实类别,对角线上数值越大越健康。蘑菇项目里常见的是“可食用菇”被误判为“有毒鹅膏菌”,这种错误方向会影响结论。
5.2 把错误样本单独切片看
光有混淆矩阵还不够,把预测错误的具体图片抽出来,做成一个errors/文件夹。步骤是遍历 test_loader,当predicted != labels时,把原始 PIL 图片和预测概率一起存下。这些错题可以作为模型迭代方向:如果错误集中在某个角度、某种光线条件,说明训练集缺少同类样本,后续补数据才有根据。
5.3 给源码包里的“高分项目”加一个可运行的自检脚本
毕业设计打包里通常会放训练好的模型和教程文档。为了不让老师按文档跑的时候踩环境坑,我习惯在项目根目录放一个quick_test.py,读一张examples/下的测试图片,直接输出类别和置信度。这个小脚本能验证模型文件是否损坏、标签映射是否缺失,很多下载来的源码跑不起来的原因就是路径写死成作者本机的绝对路径。把路径全部改成相对当前文件,再用os.path.join(os.path.dirname(__file__), ...)处理,整个项目才能跟别人一碰就通。
最后一件事:把label_map.json、class_to_idx和训练脚本里的类别顺序做个完整性校验,清空缓存后再跑一遍quick_test.py。模型文件大,但别压缩成多层嵌套文件夹,微信小程序端拦截的不是模型大小,而是请求超时;这步做完了,你的“基于卷积神经网络的蘑菇识别微信小程序”才真正从 zip 里的静态文件变成可演示的完整工程。
本文还有配套的精品资源,点击获取