如果你正在为毕业设计选题发愁,同时又希望做出一个“看起来有技术含量、实际工作量可控制、演示效果也拿得出手”的系统,那基于 Spring Boot + 深度学习的蘑菇种类识别系统,绝对可以放进你的备选清单里。这个题目既能体现完整的 Web 后端开发能力,又能展示深度学习的工程落地能力,更重要的是,它有一个非常直观的业务场景:用户上传一张蘑菇照片,系统自动判断属于哪个种类,并给出该蘑菇是否可食用的提示。无论是做本科毕设还是硕士项目,这个选题的性价比都相当高。
很多人一听到“深度学习 + Spring Boot”就觉得难度爆炸,实际上拆分之后并没有想象中那么可怕。我当初选择这个方向时,核心思路就是“把复杂问题拆成一个可以逐步验证的流程”:先解决数据集和模型训练问题,再解决模型怎么和 Web 后端集成的问题,最后才是页面展示和历史记录这些常规功能。今天这篇文章,我就把整个项目的选题逻辑、技术架构、模型训练、后端集成、踩坑调试过程全部摊开来说,希望给准备做类似毕设的同学一个清晰可复现的路线。
1. 为什么我最终选了蘑菇识别这个毕设方向
1.1 选题背后的真实需求:安全食用蘑菇是个痛点
每年到了雨季,不少地方都会有人因为误采误食野生蘑菇中毒。普通用户很难仅凭外观分辨蘑菇种类,而手机端的植物识别类应用大多数也只覆盖常见植物,蘑菇这种细分类别往往识别精度不高。做一个蘑菇种类识别系统,不仅能解决实际生活中的安全需求,而且图像识别在这个场景下有很强的说服力——用户拍一张照片,系统给出“可食用/有毒/未知”的提示,这比单纯做一个抽象的分类 Demo 更容易在答辩时讲清楚价值。
从毕设角度来说,选题最好具备三个特征:有真实应用场景、有技术深度、有可展示的成果。蘑菇识别恰好三点都占。场景上对应食品安全;技术上覆盖图像分类模型训练、后端接口设计、前后端交互;成果上是一个能上传图片并返回识别结果的完整系统。
1.2 毕设的工作量是如何拆解的
我当时把整个项目拆成四个并行推进的模块,这样心里会比较有底,不会觉得一团乱麻:
- 数据模块:采集并整理蘑菇图片数据集,完成类别标签标注,划分训练集和验证集
- 算法模块:使用预训练卷积神经网络做迁移学习,训练蘑菇分类模型,评估准确率
- 后端模块:基于 Spring Boot 搭建 Web 服务,实现图片上传、模型推理、历史记录等接口
- 前端模块:实现上传页面、识别结果展示、识别历史列表等界面
从工作量来看,数据清洗和模型训练大概占 30%,Spring Boot 后端集成大概占 30%,前端页面占 15%,论文文档和系统测试占 25%。整体难度属于中等偏上,但每一步都有明确的完成标准,不会出现“做一半不知道怎么收尾”的情况。
2. 技术架构与集成方案的选型对比
2.1 Spring Boot 在这个项目里扮演什么角色
Spring Boot 在整个系统里负责 Web 服务层,它并不参与深度学习模型的训练过程,而是负责接收客户端上传的图片、调用推理模块、把识别结果返回给前端。也就是说,用户看到的整个产品形态是 Java Web 应用,但背后真正做图像分类的,是一个脱离了训练框架的模型推理引擎。
选择 Spring Boot 的好处很明显:首先是毕业设计里的 Web 管理系统通常都要求用户登录、历史记录、权限控制这些功能,Spring Boot 有非常成熟的生态;其次是部署方便,一个可执行的 Jar 包丢到服务器上就能跑,配合 Vue 或 Thymeleaf 都能很快搭建出完整系统。如果你只做一个 PyTorch 或 TensorFlow 的脚本,导师会觉得你只是在“调库”,但如果你把它包装成 Web 服务,就能体现完整的系统工程能力。
2.2 深度学习模型与后端通信的三种常用方式
当时我调研了三种把深度学习模型接入 Spring Boot 的方案,各有优劣,这里直接做一个横向对比:
| 集成方式 | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|
后端通过ProcessBuilder调用 Python 推理脚本 | Python 生态方便,模型完全兼容 PyTorch | 每次推理都要启动 Python 进程,服务间耦合重,性能差,部署时还要同时装 Python 环境 | 不推荐做正式系统 |
| 单独部署 FastAPI 推理服务,Spring Boot 通过 HTTP 调用 | 模型和 Java 后端解耦,便于后续扩展,性能可控 | 需要维护两个进程,部署复杂度提高,答辩时容易说不清楚 | 适合并发要求高的场景 |
| 把 PyTorch 模型导出为 ONNX,使用 ONNX Runtime Java 库直接集成到 Spring Boot | 单个 Java 进程即可完成推理,部署最简单,速度也不慢 | 导出 ONNX 时部分算子可能存在兼容性问题,需要额外转换一次模型 | 强烈推荐用于毕设 |
我自己最终采用的是第三种方案。原因很直接——毕业答辩现场需要用一台普通电脑演示,你不可能让老师先帮你启动一个 Python 服务再启动 Java 服务,两个进程的部署方式听着就不够优雅。直接通过 ONNX Runtime 集成进 Spring Boot,Java 程序启动后模型就已经加载到内存里,用户上传图片后走一次纯 Java 的推理链路,干净利落。
2.3 我的最终系统架构
系统整体是前后端分离结构,但为了降低部署成本,前后端打包在一起部署。
大致请求链路是这样的:
- 前端 Vue 页面(或 Thymeleaf 页面)上传蘑菇图片
- Spring Boot Controller 接收图片,校验文件类型和大小
- 调用模型推理服务,服务内部使用 ONNX Runtime 加载
.onnx模型文件 - 对输入图片做缩放、归一化等预处理,执行推理
- 推理得到各个类别的概率,取最高的 Top-K 结果
- 后端将识别结果封装为 JSON,同时存入数据库记录历史
- 前端拿到结果后展示蘑菇名称、置信度以及可食用性提示
开发环境我使用的是 JDK 8 或 JDK 17(取决于你习惯的版本),依赖管理用 Maven。训练部分使用 Python 3.8、PyTorch 2.x,显卡如果没有的话,用 CPU 训练一些小的迁移学习模型也是可行的,只是时间会稍长一些。
3. 数据集整理与模型训练实录
3.1 蘑菇图像数据集怎么准备才好用
蘑菇识别的训练数据是项目成功的地基。如果数据质量不过关,后面模型精度再高也是白搭。当前公开的蘑菇图像数据集有不少,但不同数据集的类别划分方式不太一样,有的按“可食用/不可食用”二分,有的按具体蘑菇物种细分类。毕设系统里建议按具体物种分类,比如“牛肝菌”“鸡油菌”“毒蝇伞”“口蘑”等,因为这样更贴近真实识别场景,展示时也更有说服力。
我整理数据集时做了这么几件事:
- 合并多个公开来源的蘑菇图片,保留类别数量较多的前 20 类左右,确保每个类别至少有 100 张图片
- 去掉模糊、带水印、非自然背景干扰太大的图片
- 把图片统一重命名为
类别编号_序号.jpg的格式,方便写数据加载器 - 划分训练集和验证集,比例控制在 8:2,并且保证每个类别在验证集里都有足够样本
如果你找不到足够多的公开数据,另一个可行方案是先用网络爬虫抓取常见蘑菇图片,然后人工清洗。但这个方案很费时间,建议时间有限的同学优先考虑公开数据集或者用现成数据集做筛选合并。
3.2 迁移学习在蘑菇识别里为什么是王道
蘑菇种类之间的差别往往集中在菌盖颜色、菌褶形态、菌柄纹理等局部细节,这些特征需要较强的图像特征提取能力。如果从零开始训练一个深层卷积网络,不仅数据量可能不够,训练时间也是个问题。比较合理的做法是使用在 ImageNet 上预训练过的 CNN 模型做迁移学习,把预训练模型当作一个已经会看图片基础特征的特征提取器,只需要替换最后用于分类的全连接层,再用蘑菇数据集微调参数。
我当时选择的是 ResNet18,原因是模型体积适中,推理速度也能接受,在 CPU 环境下同样可以流畅运行。当然也有同学用 MobileNetV3,那个更轻量,部署到服务器时内存占用也小。如果你的服务器配置很低,MobileNetV3 是更好的选择。
核心训练代码结构如下:
import torch import torchvision.models as models import torch.nn as nn # 加载预训练 ResNet18 model = models.resnet18(pretrained=True) # 替换最后一层全连接,20 表示蘑菇类别数 model.fc = nn.Linear(model.fc.in_features, 20) # 将模型转为训练模式并配置优化器 optimizer = torch.optim.Adam(model.parameters(), lr=0.0001) loss_fn = nn.CrossEntropyLoss()训练时有一些细节直接影响最终精度。首先是学习率不要设太大,迁移学习的全连接层参数是随机初始化的,但主干部分的预训练权重已经很合理,如果你把学习率设成 0.001 以上,很容易把预训练权重震荡坏。我当时把主干部分和全连接层分别设置学习率,全连接层用 0.001,主干用 0.0001,效果会稳很多。
数据增强也是很关键的一环。我使用了随机水平翻转、随机旋转 10 度、随机亮度对比度调整。增强的目的不是让模型看到更多假数据,而是让模型对蘑菇拍摄角度、光线变化更鲁棒。下面是简单的训练脚本:
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=32, shuffle=True) val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=32, shuffle=False) for epoch in range(30): model.train() for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = loss_fn(outputs, labels) loss.backward() optimizer.step()我训练了 30 个 epoch,大概在 12 个 epoch 之后验证准确率从 82% 逐步爬升到 92% 左右。即便不使用更多数据增强技巧,最终在分离的测试图上也能达到 88% 左右。作为毕设系统,这个精度足够支撑演示,而且答辩时可以把训练曲线、混淆矩阵放出来作为量化结果。
3.3 从 PyTorch 模型到 ONNX 模型导出
训练结束后,需要把model.state_dict()保存下来,但不能直接把 PyTorch 的.pt/.pth模型丢给 Spring Boot。这里就需要把模型导出成 ONNX 格式。
ONNX 是开放神经网络交换格式,用于在 PyTorch 和 Java 之间搭桥,它保留了网络结构和权重,ONNX Runtime 可以高效执行推理。导出代码如下:
model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "mushroom_model.onnx", input_names=["input"], output_names=["output"], opset_version=12 )这里要注意opset_version,如果你用新版 PyTorch,默认的 ONNX 算子集版本可能很高,但 Java 版本的 ONNX Runtime 不一定完全支持。遇到算子不兼容时,把版本降低到 12 或 13 通常能解决大多数问题。另外,导出前一定要调用model.eval(),关闭 Dropout 和 BatchNorm 的随机行为,否则导出后的模型推理结果会不稳定。
4. Spring Boot 集成 ONNX Runtime 的详细过程
4.1 引入依赖和项目基本结构
新建一个 Spring Boot 工程,我使用的是 Maven 构建方式。在pom.xml中除了 Spring Boot 基础和 MyBatis-plus 依赖外,还需要加入 ONNX Runtime 的 Java 依赖:
<dependency> <groupId>com.microsoft.onnxruntime</groupId> <artifactId>onnxruntime</artifactId> <version>1.14.0</version> </dependency>如果项目里用了 MyBatis-Plus,就加上对应的 starter,不需要手工写太多 SQL。
项目结构大致如下:
controller:接收前端请求,处理上传service:业务逻辑,包括用户服务、识别记录服务inference:模型加载和推理服务utils:图像预处理工具entity和mapper:数据库表对应的实体类和持久层config:全局配置,比如跨域处理、文件大小配置
4.2 模型加载与单例保存
ONNX Runtime 的OrtSession在使用时需要确保是单例的。如果每个请求都重新加载一次模型,服务端会因为模型加载耗时太长而超时,还会占用大量内存。正确做法是在 Spring Boot 启动时完成模型加载:
@Service public class OnnxModelService { private OrtSession session; private Set<String> labels; @PostConstruct public void init() throws OrtException { String modelPath = "classpath:models/mushroom_model.onnx"; resource = new ClassPathResource(modelPath); byte[] modelBytes = resource.getInputStream().readAllBytes(); OrtEnvironment env = OrtEnvironment.getEnvironment(); session = env.createSession(modelBytes, new OrtSession.SessionOptions()); labels = loadLabels(); } }把模型文件放到src/main/resources/models/目录下,打成 Jar 包后依然可以读取到。如果你模型文件特别大,也可以把模型放在外部配置文件指定的磁盘路径,这样后续更新模型不需要重新打包项目。
4.3 图片预处理与推理接口实现
ONNX Runtime 接收的输入是一个四维张量,形状是[1, 3, 224, 224],表示 1 张图片、3 个通道(RGB)、224 行、224 列。而用户上传的图片尺寸五花八门,必须提前统一处理。
我写了一个图像预处理工具类,主要步骤:
- 使用
ImageIO.read读取上传图片 - 使用
BufferedImage缩放至 224×224 - 从
BufferedImage中提取像素 RGB 值 - 将 RGB 值按模型要求做归一化,ResNet 系列的归一化一般是 mean 和 std 固定的值
- 把像素数组排成
CHW排列的浮点数组,即通道在前,行和列在后
核心代码示意:
float[] chw = new float[3 * 224 * 224]; int idx = 0; for (int c = 0; c < 3; c++) { for (int i = 0; i < 224; i++) { for (int j = 0; j < 224; j++) { int rgb = image.getRGB(j, i); float val = ((rgb >> (16 - c * 8)) & 0xff) / 255.0f; val = (val - mean[c]) / std[c]; chw[idx++] = val; } } }推理时创建OnnxTensor:
OrtEnvironment env = OrtEnvironment.getEnvironment(); OnnxTensor tensor = OnnxTensor.createTensor(env, chw, new long[]{1, 3, 224, 224}); OrtSession.Result result = session.run(Collections.singletonMap("input", tensor)); float[][] output = (float[][]) result.get(0).getValue();输出矩阵的第i行第j列代表第i张图片预测为第j类的概率。因为我们只输入一张图,所以取output[0],然后遍历所有类别,找到概率最高的类别,以及前三个高概率类别用于前端展示。这种输出 Top-3 的方式比只给一个结论更有可信度,老师看到页面时会觉得你的系统不只是一个黑盒。
4.4 接口合法性和异常处理
用户上传的文件不一定是图片,也可能是损坏文件。开始时我只判断了文件扩展名,结果有次测试传了个改名后的文本文件,程序直接报错弹出异常页面。后来我在上传接口一开始就做了三件事:限制文件类型为 jpg、jpeg、png;限制文件大小最大 5MB;尝试用ImageIO.read读取文件,如果读出来是null,直接返回“图片解析失败”的异常信息。
使用 Spring Boot 的全局异常处理能让系统更完整。定义@RestControllerAdvice,捕获业务异常和文件异常,返回统一 JSON 结构。前端只要拿到code和message字段就能判断当前请求是否成功,这种设计在答辩时很加分,毕竟一个优秀的毕设系统不只是功能齐全,接口设计也要规范。
5. 前端页面与整体联调
5.1 页面层次与用户操作流
前端我没有选择非常复杂的项目,直接使用 Vue 3 + Element Plus 搭建页面,然后打包成静态资源放入 Spring Boot 的src/main/resources/static目录下。这样的部署方式最简单,Spring Boot 启动后直接访问http://localhost:8080就能打开页面。
整个前端页面分成三个核心区域:
- 首页推荐区域:展示一些常见蘑菇的科普卡片,比如“毒蝇伞”“鸡油菌”等,顺便说明识别结果仅供辅助参考,不构成食用建议
- 识别区域:中央上传组件,支持点击或拖拽上传图片,上传后立即展示原图和识别结果卡片
- 历史记录区域:表格展示当前用户的历史识别记录,包括蘑菇名称、置信度、识别时间
5.2 前端上传图片与展示结果
上传图片用的是 FormData:
const formData = new FormData(); formData.append("file", file); const res = await axios.post("/api/mushroom/identify", formData, { headers: { "Content-Type": "multipart/form-data" } });后端返回的数据结构约定如下:
{ "code": 200, "message": "success", "data": { "predictClassName": "鸡油菌", "confidence": 0.93, "edible": true, "description": "鸡油菌为可食用蘑菇,常见于林地,颜色呈杏黄色。", "topResults": [ {"name": "鸡油菌", "score": 0.93}, {"name": "牛肝菌", "score": 0.05}, {"name": "口蘑", "score": 0.01} ] } }前端拿到数据后,可以把置信度转成一个横向进度条,展示前三位可能类别。用户会明显感觉到系统不是“猜一个名字”,而是“综合判断”。
5.3 联调中的几个调整细节
由于页面部署在同一个 Spring Boot 服务下,本身没有跨域问题。如果你想用前后端分离的开发模式,比如前端跑在 5173 端口,后端跑在 8080 端口,那必须在后端加入跨域配置,否则浏览器会拦截请求。最直接的方法是在 Controller 上增加@CrossOrigin注解,或者使用全局 CorsFilter。这一点如果你是前端先写好的情况,特别容易忽略,因为后端单独用 Postman 测试接口都是通的,但浏览器里会报跨域错误。
另一个细节是文件上传大小限制。Spring Boot 默认单文件上传大小上限是 1MB,手机拍出来的图片经常超过这个值。当时我测试时上传一张 4MB 的图片,直接被后端拒绝。需要在application.yml里配置:
spring: servlet: multipart: max-file-size: 5MB max-request-size: 10MB6. 实测阶段踩过的坑和完整排查链路
6.1 模型加载极其缓慢,第一次请求直接超时
最开始我把模型加载写在OnnxModelService的构造函数里,但那时候为了图方便,只在接口被第一次调用时才执行初始化。结果第一次上传图片等了几十秒,前台报超时。
排查思路是:先确认耗时发生在哪一段。我在加载模型前后加了时间戳打印,发现光是createSession就用了 3 秒,再加上模型初始化,整体大概 4-5 秒。解决办法很简单:把模型加载放到@PostConstruct启动阶段,并在日志里打印“模型加载完成”的时间点。这样系统启动时慢一点可以接受,但接口调用时就不再需要等待初始化了。
这里也想提醒一个隐藏问题:如果模型放在classpath的 Jar 包里,每次从 Jar 中读取模型文件到内存再创建 Session 是可行的做法。但如果模型是几百 MB 的大模型,Jar 包启动会明显变慢,更合理的做法是把模型放到服务器磁盘上,通过配置项指定路径。
6.2 上传的图片被旋转或压缩,识别率明显下降
用户用手机横竖屏拍摄时,某些设备的图片会带 EXIF 旋转信息,直接用ImageIO.read读取会出现图片方向不对的问题。一开始我没处理,结果一张横着拍的蘑菇照片识别结果错得离谱。
解决方向有两个:一是读取原图携带的 EXIF 信息,根据旋转角度把图像转正;二是在前端上传前使用 Canvas 把图片统一转成 JPEG 并限制最长边。我当时选择后者,因为前端处理图片更简单,只要拿到File后画到 Canvas 上,转成 Blob 再上传。这样做还能顺便压缩大图片,后端处理起来也更快。注意压得太狠也不行,我尝试把图片压到 200px,识别率就明显下降了,因为蘑菇细节丢失严重。最合适的策略是限制长边为 600-800px,既可以保证细节,又能把图片控制在合理大小。
6.3 ONNX Runtime 并发推理时的内存问题
一开始写推理代码时,我是每次请求都创建了一个新的OrtSession,压力测试时内存飙升到快 2GB。后来优化为单例 Session 后,内存问题解决了,但并发请求时又发现可能报错。
其实 ONNX Runtime 官方文档里提到过,单个OrtSession在同一时间只允许一个推理请求执行,如果你直接并发调用session.run(),会出现线程安全问题。解决办法是添加一个读写锁或信号量,限制同时只有一个线程执行推理。虽然对于个人毕设来说并发量不会很大,但这个设计体现了对并发控制的理解。
private final Semaphore semaphore = new Semaphore(1); public float[][] infer(float[] input) { semaphore.acquire(); try { // run session } finally { semaphore.release(); } }6.4 中文字符串乱码,类别名称变成问号
这个问题非常经典,我在本地运行没事,打包成 Jar 后在新的 Windows 服务器上运行,返回的识别结果类别名变成了“???”。第一次排查时以为是数据库编码问题,但检查后不是。最后发现是读取类别标签文件时用了默认字符集,和 Windows 系统默认编码不一致。
解决办法是在读取类标签文件时明确指定 UTF-8:
BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream(labelsFile), StandardCharsets.UTF_8 ) );同时,Spring Boot 项目打包时也要确保资源文件不被 Maven 重新转码处理。在pom.xml里设置project.build.sourceEncoding为 UTF-8。
7. 项目文档组织、答辩演示的经验
7.1 毕设论文的目录怎么写才容易过
开题和中期报告是按部就班写,但最后的论文如果只是罗列代码是没有意义的。我的经验是,论文应该围绕“系统如何解决蘑菇识别需求”这条主线展开,而不是从 Spring Boot 的 Hello World 讲起。
我的论文目录大致是这样的:
- 绪论:背景、国内外蘑菇识别研究现状、选题意义
- 相关技术介绍:Spring Boot、ONNX Runtime、卷积神经网络、迁移学习
- 需求分析:功能性需求(上传识别、历史记录)和非功能性需求(准确率、响应时间)
- 系统设计:总体架构、功能模块设计、数据库设计、模型训练方案设计
- 系统实现:关键代码和算法流程、模型集成过程、前端展示
- 系统测试:功能测试、性能测试、准确率对比试验
- 总结与展望
这个目录最大的好处就是每个章节都有具体内容可写,不至于无病呻吟。测试部分可以画表格列出上传正常图片、损坏图片、超大图片的预期结果和实际结果,一份完整的测试表格比满篇代码更有说服力。
7.2 答辩演示怎么提前准备才不出洋相
答辩现场最尴尬的情况就是模型没加载、图片上传失败、识别结果不对。我建议演示前做几项准备:
- 准备 10 到 20 张测试图片,放在一个单独的文件夹里,图片命名要有规律,比如按类别区分,方便演示时快速找到
- 提前在演示机器上完整跑一遍系统,确认 Java 环境、端口占用、数据库连接都没问题
- 准备一张运行数据截图作为备用,假如现场网络不好或者模型加载异常,可以用截图直接说明成果
- 演示时长控制在 8 分钟以内,前 2 分钟讲背景,中间 4 分钟展示核心识别功能,最后 2 分钟展示管理功能和数据库存储
答辩老师通常关注三个点:项目是你自己完成的吗、技术选型是否合理、遇到问题怎么解决。因此你在准备材料时,一定要把自己踩过的坑、排查过程、优化对比写进 PPT。比如“从 PyTorch 导出 ONNX 时遇到算子不兼容,最终通过调整 opset 版本解决”这类经验,比单纯贴首页截图漂亮得多。
8. 我做完这个项目后的几点切身体会
如果让我再选一次,我依然会觉得蘑菇识别这个方向非常合适。它不像人脸识别、车牌识别那样已经烂大街,也不像纯算法研究那样难以落地,属于“既有真实需求,又能在有限时间内完成”的类型。在完整走完数据、训练、部署、文档全流程之后,你对 Spring Boot 的掌控感会强很多,也能真正理解深度学习中“迁移学习”“模型转换”“推理性能”这些抽象概念是怎么落到实处的。
出于毕设分享的考虑,我将这个项目的源码、数据库脚本、论文框架和演示图片打包整理成了一份完整资源,包含详细的项目结构说明和启动文档,方便需要的同学直接参考。拿到源码后不建议完全不改就提交,一是要保证查重逻辑合理,二是尽量把项目中的分类模型换成自己的训练结果,这样答辩被问到底层细节时,你不会一点说不出来。只要理解了文中讲到的模型导出、Spring Boot 集成、图像预处理三个核心链路,就算替换为其他数据集,也能轻松复现整个系统。