简介:在计算机视觉领域,图像分类是基础而核心的任务,借助卷积神经网络(CNN)能够自动提取颜色、纹理等层次化特征。针对医学图像样本少、标注成本高的痛点,迁移学习通过预训练模型微调有效缓解了过拟合。PyQt5作为成熟的桌面GUI框架,可在本地搭建交互友好、响应流畅的应用界面。从中医数字化诊断的应用场景出发,舌苔识别系统利用深度学习模型对舌部图像进行客观分类,并通过PyQt5完成数据导入、推理展示与历史记录等全链路流程,为临床辅助决策提供了可行原型。围绕系统设计、数据增强、模型训练与界面开发的关键环节,分享从毕设项目到工程实践的完整经验。 答辩结束那天晚上,我翻着电脑里这个项目从零到一的全部过程——乱糟糟的代码版本、二十多次模型训练的日志、以及最终跑起来的PyQt5桌面应用——突然觉得这段经历很值得写下来。起因其实很简单:毕设选题清单里视觉方向的题目就那么几个,目标检测烂大街,人脸识别对我这种没GPU资源的学生来说门槛不低,而我对中医数字化诊断这个方向一直有点兴趣。最后我选了“基于PyQt5的深度学习舌苔识别系统”,一个把CNN图像分类和桌面应用结合起来的小项目。这篇博文写给正在选毕设题目的同学、想快速上手PyQt5做桌面应用的开发者,以及准备做类似医学图像分类项目的朋友,我会把从选题、数据、模型、界面到答辩的全部细节和踩坑经历摊开讲。
1. 这个毕设到底在做什么:选题动机与系统边界
1.1 视觉项目那么多,为什么选了舌苔识别
先说选题逻辑。毕设题目难度要适中,太简单答辩时没东西讲,太难自己扛不住。舌苔识别这个方向有几个天然优势:第一,它是标准的图像分类问题,深度学习CNN直接对口;第二,中医舌诊在临床研究里一直有数字化需求,算是有应用价值;第三,PyQt5做桌面客户端正好补足“算法+应用”的完整链路,是老师喜欢看到的完整系统。
舌诊的医学背景大致是这样:舌头是人体健康状况的一个窗口,舌质和舌苔的变化与脏腑功能密切相关。传统中医通过观察舌色、舌形、苔色、苔质来判断病情,但这依赖医生的主观经验,不同医生看同一个舌头可能得出不同结论。用计算机视觉做舌苔识别,本质是把这种人工经验转化为可量化的客观特征,这就是项目的核心价值所在。
我当时给自己定的目标是:做一个能运行的桌面应用,用户上传舌部照片,系统自动判断舌苔类别并给出置信度,同时保存历史记录。这个目标不大,但涉及GUI开发、图像处理、深度学习模型训练、数据持久化,整个闭环做下来已经够一个毕设的分量。
1.2 任务定义:分类为主,检测作为延伸
舌苔识别严格来说有两种做法:目标检测和图像分类。目标检测需要在舌头区域画边界框,工作量更大,需要标注框数据;图像分类则假定输入图片已经是裁剪好的舌头区域,只需要判断整张图属于哪一类。
我最终选择的是图像分类,原因很实际:舌苔公开数据集极少,自己采集和标注边界框成本太高。分类任务对数据标注的要求低很多,按文件夹归类就行。而且对于毕设来说,分类结果的展示和解释更直观——上传图片、点击识别、输出“黄厚腻苔 置信度91.2%”,这个流程比画框更贴近普通用户的使用习惯。
分类的类别设置也有讲究。舌苔在中医里有苔色(白、黄、灰黑)、苔质(薄、厚、腻、腐、剥)等维度,如果一次性做十几类,每类的样本量会被摊薄,模型根本学不好。我当时压缩到5类:白薄苔、白厚腻苔、黄薄苔、黄厚腻苔、剥苔,这五类在中医里代表不同的身体状态,区分度也比较清晰。
1.3 系统功能清单和技术指标
这套系统最终要交付的东西很明确,我从一开始就列了一张功能清单:
- 用户登录和注册:用SQLite存用户表,虽然简单但毕设功能列表里好看
- 图片导入:支持jpg、png、bmp格式,通过QFileDialog选择文件
- 舌苔识别:加载训练好的CNN模型,对图片进行预处理并输出分类
- 结果展示:显示类别、置信度、推理耗时,并把识别结果保存
- 历史记录:按时间倒序展示所有识别记录,支持清空和删除
性能指标上我给自己定的及格线是:单张图片推理时间不超过2秒(CPU环境),五分类准确率不低于85%。这个指标不算高,但考虑到舌苔图片本身存在光照、角度等干扰,85%已经能说明模型学到了一些有效特征。说实话,如果数据量不足,这个指标还需要往下调。
2. 技术选型推演:为什么是PyQt5+CNN这套组合
2.1 界面层:PyQt5 vs Web前后端 vs Tkinter
技术选型是很多同学最纠结的部分,网上资料又杂又乱。我说说我的思考过程。毕设界面层有三个主流选择:PyQt5、Tkinter、Web前端(Flask+Vue或者纯Flask页面)。
Tkinter是Python自带的GUI库,学起来简单,但界面风格停留在Windows 98时代,做不出像样的视觉效果。用Web前端做界面,交互体验确实现代,但相当于写了一个Web应用,还需要处理浏览器端和Python后端的通信,开发链路更长。PyQt5正好处在中间位置——它是Qt框架的Python绑定,控件丰富、界面美观、和OpenCV/深度学习库的配合很丝滑,而且信号槽机制写事件处理非常顺手。
我之前用过一段时间的PyQt5,对这个库的脾气比较熟,所以选它没有犹豫。如果你完全没接触过PyQt5,也不要慌,这个库的写法非常套路化:创建QWidget/QMainWindow,往上面放控件,连接信号槽,写业务逻辑,整个流程一周就能上手。
2.2 模型层:自建CNN和迁移学习的取舍
深度学习模型的选择是另一个关键决策。我当时考虑过三条路:从零训练一个自定义CNN、用预训练模型做迁移学习、直接调用现成API(比如阿里云或百度的图像识别接口)。用现成API最省事,但毕设答辩时老师让你打开代码讲原理,你直接傻眼,而且舌苔属于医学图像,普通API对这类垂直领域的分类效果大概率还不如自己训的模型。
我的方案是双轨并行:先用自建CNN跑通完整流程,再用ResNet18做迁移学习提高精度。自建CNN的好处是结构完全可控,每一层的作用都能讲清楚,应对答辩“这个网络为什么这么设计”这种问题时特别有优势。ResNet18的优势则是特征提取能力强,收敛更快,最终效果上限更高。
具体的自建CNN结构我后面会展开,这里先给结论:输入224x224的RGB图片,经过三组“卷积+BatchNorm+ReLU+最大池化”的特征提取,然后展平接全连接层和Softmax分类器。
2.3 环境配置:版本组合与安装顺序
PyQt5的环境问题说大不大,说小不小,网上很多人在这上面卡了一整天才装好。我用的环境是Python 3.9 + PyQt5 5.15.9 + PyTorch 1.13(CPU版),这套组合运行稳定。
安装顺序比较推荐先装PyTorch再装PyQt5。这里说一个经验:PyQt5安装最怕的是和sip版本冲突。如果你执行pip install pyqt5之后,导入PyQt5时提示sip相关错误,解决方法很简单,先卸载再重装:
pip uninstall pyqt5 pyqt5-sip pip install pyqt5==5.15.9 pyqt5-sip==12.11.0不要直接装最新版PyQt5 6.x,除非你愿意忍受和教材教程完全不兼容的API差异。我就是用5.15版本省去了很多不必要的麻烦,后面排错章节会单独讲这部分坑。
3. 数据集准备:舌苔图片的采集、清洗与增强
3.1 数据从哪来:公开数据集与自采的组合方案
数据集是所有医学图像项目最大的痛,真实舌苔数据不是你想找就能找到的。我最终的数据来源有三个:网上能搜到的零散舌象图片、开源医学影像平台上的少量舌象样本、以及一些中医书籍电子版里扫描的舌象图。虽然这些数据很零散,但整理下来凑了600多张,每类100多张,勉强能支撑一个简单的分类实验。
如果镜像量再大一些,还可以考虑从医学图像竞赛平台找相关数据集,国内外的中文舌诊研究其实有少数学者开源过部分数据。重点提醒一下,数据来源一定要在论文里写清楚,注明出处和版权说明,避免学术不端的麻烦。
数据清洗这一步非常关键。我过了一遍所有图片后,把三类不合格的图片踢掉了:光线过暗导致舌色完全失真的、舌体被遮挡超过三分之一无法判断苔质的、清晰度太低连肉眼都看不了的。清洗后实际用于训练的有效图片大约520张,说句实话这个量偏少了,所以后面数据增强成了抢救模型的必要手段。
3.2 预处理关键操作:去光照干扰与统一尺寸
舌苔识别的核心特征包括颜色和纹理。苔色判断依赖颜色,苔质判断依赖纹理细节,因此预处理要尽量保留这两类信息,同时消除光照、角度、阴影等噪声干扰。
我的预处理流程分四步。第一步,把所有图片统一缩放到224x224,不管原图是多大,这个尺寸是输入模型的标准尺寸,也兼顾了CPU推理速度。第二步,做光照归一化,用OpenCV的直方图均衡化,把舌体区域的亮度分布拉回正常范围,这样不同环境光线下的照片在颜色上更具可比性。第三步,对图片做轻微的锐化,让苔质的纹理细节更清晰,帮助模型区分厚苔和薄苔。第四步,标准化到模型需要的格式,也就是转换为浮点数张量并按ImageNet的均值方差归一化。
完整处理函数可以参考这个框架:
import cv2 import numpy as np def preprocess_image(image_path): img = cv2.imread(image_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (224, 224)) # 光照归一化 img = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(cv2.cvtColor(img, cv2.COLOR_RGB2LAB))[:, :, 0] # 锐化 kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]]) img = cv2.filter2D(img, -1, kernel) return img / 255.0这一步的效果在实验对比里也很明确:不做光照归一化时,低光照图片的预测结果分布非常随机;做了之后,同样的图片在大多数轮次里能稳定输出正确类别。
3.3 数据增强策略:针对颜色敏感任务的控制
数据增强是力挽狂澜的一步。因为只有500多张原始图片,如果不做增强,自建CNN大概率严重过拟合——训练集准确率99%,验证集准确率60%,这种戏剧性的差距老师一眼就能看出来。
做增强要注意一个矛盾:舌苔分类对颜色极其敏感,因此幅度过大的颜色抖动会直接把样本变成另一个类别。比如“黄厚腻苔”这个类别的图像,如果把色相往绿色偏移,模型很可能会学到错误的颜色关联。我的增强组合是:随机水平翻转、-10度到10度随机旋转、10%以内的亮度调整、2%以内的饱和度调整、随机裁剪一定的比例后再缩放回224x224。综合应用后,相当于每张原图扩展出了多种不同角度的变形,模型见过的样本形态从五百多张变成了五千多张。
用PyTorch的transforms库实现,代码很简洁:
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(10), transforms.ColorJitter(brightness=0.1, saturation=0.02), transforms.RandomResizedCrop(224, scale=(0.85, 1.0)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])这里特别强调一点:val和test集的变换不要包含Random翻转、Random裁剪这类随机操作,只需要Resize、ToTensor和Normalize,确保验证时的输入是可复现的。
4. 模型训练过程:从基线CNN到ResNet微调
4.1 第一版模型:自建CNN的完整结构
自建CNN的意义在于让你彻底理解一个神经网络是怎么从输入图片一步步提取特征并输出类别概率的。这个理解在答辩中非常重要,因为老师一定会问“你的网络为什么这样设计”。
我的基线网络结构如下:
- 输入:3x224x224的RGB图像
- 第一层卷积Conv2d(3, 16, kernel_size=3, padding=1),接BatchNorm2d(16)和ReLU,然后最大池化,输出变为16x112x112
- 第二层卷积Conv2d(16, 32, kernel_size=3, padding=1),接BatchNorm2d(32)和ReLU,然后最大池化,输出变为32x56x56
- 第三层卷积Conv2d(32, 64, kernel_size=3, padding=1),接BatchNorm2d(64)和ReLU,然后最大池化,输出变为64x28x28
- 展开为642828=50176维向量,通过全连接层映射到128维,Dropout(0.5)减少过拟合,最后全连接映射到5个类别
第一层卷积负责提取边缘、纹理等低级特征,后面各层逐步组合出对类别有区分力的高级语义特征。最大池化的作用是下采样和增大感受野,同时让模型对位置不敏感。BatchNorm的作用则是稳定训练过程、加速收敛,尤其是小数据集上效果明显。
虽然这个基线网络不算深,但对五分类任务来说已经在及格线附近,训练50轮能到75%左右的测试准确率。它会作为之后迁移学习的对照组,用来证明微调后的模型性能提升。
4.2 迁移学习:ResNet18的接入方式
当基线模型准确率停在75%上不去时,我决定切到迁移学习。选择ResNet18的原因是它最小、速度最快、对CPU推理友好,而且残差结构能有效解决深层网络梯度消失问题,在小数据集上也能稳定收敛。
迁移学习的接入方式很简单,PyTorch官方早就集成好了:
import torchvision.models as models model = models.resnet18(pretrained=True) num_features = model.fc.in_features model.fc = nn.Linear(num_features, 5)关键在于微调策略:需要决定只训练最后一层分类头,还是微调全部参数。我的实测结果是:只冻结Backbone训练最后的全连接层,虽然训练飞快,但模型只学到ImageNet特征和舌苔类别之间的简单映射,准确率约80%,提升有限。全部参数微调,初始学习率用0.0001,训练20轮后准确率能爬到86%以上,效果明显更好。
需要注意的是,迁移学习的输入归一化必须使用预训练模型对应的均值方差,也就是[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]。如果忘了归一化,模型的表现会莫名其妙地差。这个坑我后来排查了很久才找到,属于典型的“看起来没错但就是不准”的隐藏错误。
4.3 训练参数、评估指标与过拟合排查
训练参数我最终确定为:优化器AdamW,weight_decay设0.0001防止过拟合,学习率0.0001,batch_size为16,训练轮数30轮,损失函数CrossEntropyLoss。学习率策略用的是ReduceLROnPlateau,验证集loss连续3个epoch不下降时学习率减半。
评估指标不能只看准确率,还要看每个类别的精确率、召回率和F1值。尤其是样本不均衡时,比如“剥苔”类图片很少,即使模型把所有样本都预测为“白薄苔”,整体准确率也可能不低,但这个模型实际是废的。我在测试集打印classification_report之后才发现了这个问题,然后通过给稀缺类别在损失函数里加类别权重来处理。
过拟合的判断标准也分享一个实用方法:训练过程中记录每轮的训练loss和验证loss。如果训练loss持续下降、验证loss先降后升,这就是典型的过拟合信号,模型开始“背诵”训练样本。这时候优先减少模型复杂度或增强正则化,而不是急着加数据。
整套训练日志我用TensorBoard保存,每一轮的准确率、loss曲线、混淆矩阵都清清楚楚。这些图表在论文和答辩PPT里直接就能用,一下子就把整个实验过程的工作量拉满了。
5. PyQt5客户端开发:界面到推理的完整链路
5.1 主窗口布局与QSS美化
界面设计上我放弃了花哨的皮肤引擎,直接用QSS(Qt样式表)实现了一套简洁的医学风格界面,白灰配色加一个深绿色主题按钮,看起来不会显得太学生气。整体布局采用左右分栏:左侧是图片选择与展示区域,右侧是识别结果和历史记录区域。
主窗口基于QMainWindow搭建,控件分工如下:中间的QSplitter分割左右两个QWidget;左半部分放QPushButton“打开图片”和一个QLabel用于显示图片;右半部分放一个QGroupBox展示识别结果,里面是类别标签、置信度、推理耗时的QLabel,下方再放一个QListWidget展示历史记录。顶部菜单栏加一个“退出”Action,底部QStatusBar显示“模型加载成功”等状态信息。
QSS美化只需要给主窗口和关键控件设置样式,例如设置按钮背景色和悬停颜色,QLabel字体大小,QListWidget的边框圆角等。整体代码量不大,却能极大提升观感。毕设答辩时,第一眼界面好看是有隐性加分的。
5.2 图像导入与格式转换的细节
PyQt5和OpenCV的图像格式互相转换是初学者最容易踩坑的点。QFileDialog选中的图片路径是字符串,你要先用cv2.imread读成OpenCV格式,之后又要把OpenCV格式转成QImage才能显示在QLabel上。这个转换有个很容易犯的错误:OpenCV默认读进来的是BGR通道顺序,而QImage显示时按RGB理解,如果直接转,显示的图片颜色就会偏蓝偏红,舌苔颜色一错,这个系统就完全失去了诊断意义。
正确做法是先用cv2.cvtColor转换通道,再构造QImage:
rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w qimage = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)这里还有一个隐蔽的坑:QImage构造时的bytesPerLine参数必须填ch * w,如果省略或填错,图片在显示时会出现奇怪的拉伸错位。因为QImage需要知道每一行像素占多少字节,默认值对连续内存的ndarray通常没问题,但经过某些resize裁剪操作后数据步长可能不同,会导致画面撕裂。同样的道理也适用于预处理后的图像,矩阵经过torchvision的ToTensor转成CHW后,你要通过permute转回HWC再转QImage显示。
5.3 QThread异步推理,避免界面卡死
如果你的系统跑推理时界面直接变成“未响应”状态,等几秒才弹出来结果,那就是典型的同步阻塞问题。推理是在主线程执行,而主线程同时还要负责窗口重绘和事件响应。我把模型推理放到QThread子线程后,界面立刻恢复流畅,用户体验完全不是一个档次。
具体做法是写一个继承QThread的推理线程类,通过信号把结果传回主线程:
from PyQt5.QtCore import QThread, pyqtSignal class InferThread(QThread): finish_signal = pyqtSignal(dict) def __init__(self, model, image): super().__init__() self.model = model self.image = image def run(self): label, confidence, cost = single_predict(self.model, self.image) self.finish_signal.emit({ "label": label, "confidence": confidence, "time": cost })在主界面里,点击“开始识别”按钮时先禁用按钮防止重复点击,然后创建并启动线程,线程结束的信号会触发一个槽函数来更新界面并恢复按钮状态。如果模型一直挂在内存里,CPU推理一次大约0.3到0.8秒,UI完全保持流畅。GPU版推理更快,几十毫秒就能完成,不过CPU版已经满足2秒的指标要求了。
5.4 SQLite历史记录与报表导出
历史记录模块我用SQLite实现,代码量不大但功能完整。数据库文件放在程序主目录下,首次运行自动建表。表结构如下:
CREATE TABLE IF NOT EXISTS history ( id INTEGER PRIMARY KEY AUTOINCREMENT, image_path TEXT, result TEXT, confidence REAL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );每次识别完成后,把图片路径、识别类别、置信度、当前时间插入到表里。主界面右侧的QListWidget每次启动时从数据库读取最近20条记录展示,用户双击某条记录可以重新查看当天识别的图片。另外还加了一个“导出报告”按钮,把所有历史记录写入csv文件,用Excel直接打开,这对毕设验收演示很有用——你可以现场演示一批图片的识别结果,再展示导出的报告表格。
6. 联调排错实录:四类高频坑与排查思路
6.1 模型权重路径与打包路径问题
这个坑几乎每个做PyQt5+深度学习的同学都会遇到:开发环境里代码运行正常,但用PyInstaller打包成exe后,程序点击识别直接崩溃,错误提示找不到模型文件。原因很简单——打包后当前工作目录变化了,程序还在用相对路径找模型权重。
后来我用的是PyInstaller的sys._MEIPASS机制处理资源路径。打包后的exe运行时,外部资源文件会被解压到一个临时目录,你需要判断程序是否处于打包状态,如果是就切换到临时目录找资源:
import sys, os def resource_path(relative_path): if hasattr(sys, "_MEIPASS"): base_path = sys._MEIPASS else: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) model_path = resource_path("models/best_model.pth")这里再提醒一个实践建议:模型文件动辄几十MB甚至上百MB,不建议打进exe包,应该在首次启动时让用户指定模型文件路径,或者把模型放在exe同级的models目录下。这样打包出来的程序体积小,模型也方便更新。
6.2 OpenCV、PyQt5、模型输入的格式不统一
三种格式混在一起真的会让人头皮发麻。OpenCV读取的图像是BGR顺序的numpy数组,PyTorch模型要求的输入是CHW顺序、归一化到0到1的RGB张量,PyQt5显示时又要转成QImage的RGB格式。牵一发动全身,一个环节错了,结果可能表现为颜色混乱、识别准确率暴跌或者直接报维度错误。
我把格式转换流程做成一个固定的pipeline,每次开发新功能都走这条流程:cv2读取BGR -> 转为RGB -> 转为CHW张量 -> Normalize -> 模型推理 -> 输出类别。任何需要显示原始图片的地方,都从最开始的RGB数据构造QImage,不要从CHW张量再转回来多绕一步。
模型加载时也要注意,model.eval()必须在推理前调用。有一次我忘了设置eval模式,结果模型保留训练时的Dropout随机性,同一张图片每次预测结果都不一样。这个问题特别隐蔽,因为代码不会报错,只有当你拿同一张图测两次发现结果不同时才能想到。
6.3 PyQt5不同版本API差异
PyQt5和PyQt6的差异按说应该在开始时避免,但网上很多教程的代码是不同版本混着写的,照抄很容易出问题。例如PyQt5里QDesktopWidget在PyQt6已经被移除,按钮点击信号在PyQt5里是button.clicked.connect(),在PyQt6里QAction的位置和构造方式也变了。这些改动虽然不大,但一报错就会浪费大量时间。
我建议锁定PyQt5 5.15系列版本。装完后验证一下:
python -c "from PyQt5.QtCore import QT_VERSION_STR; print(QT_VERSION_STR)"如果输出的Qt版本是5.x,基本就没问题了。还有PyQt5的sip版本问题和Qt插件丢失问题,后者多出现在重装后,官方库路径没刷新。这类环境问题在开发机上的解法比较粗暴:创建一个干净的虚拟环境,重新安装所有依赖,能解决85%莫名其妙的问题。
6.4 模型推理耗时与内存占用不规范
CPU推理虽然满足2秒指标,但一开始因为模型输入尺寸和线程管理没做对,推理耗时会飘到3秒以上。后来排查出两个原因:一是每次推理都重新加载模型权重,白白浪费了I/O时间;二是预处理部分为了偷懒把图像保持原尺寸输入模型,大图直接拖慢了卷积计算。
解决方案是模型常驻内存只加载一次,图片统一缩放到224x224。另一个内存问题出现在多次推理后内存占用不断上升,原因是QImage和numpy数组在转换过程中没有及时释放。这种问题用del和gc.collect能缓解,但关键是不要在循环里反复创建临时副本,尽量复用同一个numpy数组。
我还对模型做了推理速度的基准测试:自建CNN在CPU上大约0.2秒,ResNet18大约0.45秒,都在可接受范围内。如果还想更快,可以用torch的quantization做INT8量化,速度能再提升一倍,但准确率会有轻微下降,这个取舍要看项目需求。
7. 从代码到论文:毕设文档的编排与答辩准备
7.1 功能性图表的制作建议
毕设论文里,图表是老师最先看的内容之一,也是最容易拉开差距的地方。按我的经验,必须准备的图包括:系统总体架构图、系统功能模块图、程序流程图、数据库ER图、模型网络结构图、训练过程中的loss和准确率曲线图、测试集混淆矩阵图。这些图不用画得多精美,但要清晰、规范、逻辑自洽。
绘图工具我用的是draw.io,免费且在线就能用,架构图和流程图都能快速上手。网络结构图可以用PyTorch官方工具库Torchviz画出模型的计算图,把它作为论文里的网络结构图非常专业。混淆矩阵直接通过sklearn的confusion_matrix计算,再用matplotlib画热力图。所有图表在论文里的边缘要留白,统一字体和配色,整体观感会非常加分。
7.2 实验对比表格的呈现方式
实验部分论文的“含金量”主要通过对比表格来体现,我建了一张总表,横向列出所有实验方案,包括:自建CNN基线、ResNet18冻结Backbone微调、ResNet18全参数微调、带数据增强与不带数据增强的对比。纵向记录每个方案的准确率、宏平均F1值、模型参数量、单张推理耗时。这张表能让答辩老师快速看到你对实验设计的系统性思考。
每张表旁边记得配一段文字分析,比如“为什么全参数微调明显好于冻结Backbone”“数据增强对小样本任务的关键作用”。不要只罗列数据,分析才是展示能力的部分。另外,各个类别的分类效果差异也很值得写,比如“白厚腻苔”和“黄厚腻苔”之间容易混淆,因为两者在纹理上接近,主要区别体现在颜色上,这能说明硬件和光照条件对系统的影响。
7.3 答辩常被追问的几个问题
答辩前我把老师可能问到的问题列了一个清单,现场基本没有超出这个范围的。高频问题大概是这几个:
第一,“为什么用分类而不是目标检测?”回答思路:分类任务已经满足系统的核心需求,且标注成本低;检测可以作为后续扩展方向,用YOLO定位舌体再切分到分类模型,形成一个两阶段系统。
第二,“数据量这么小,模型泛化能力能保证吗?”回答思路:承认数据量确实是局限,但同时说明已经使用迁移学习、数据增强、严格的数据清洗和交叉验证来缓解这个问题,未来的方向是扩展更大规模的临床数据集。
第三,“你的系统在真实临床场景能用吗?”回答思路:这是一个研究原型,能为中医舌诊的客观量化提供辅助参考,但距离临床落地还有距离,需要更大规模的数据验证和医生的专业标注参与。顺着这条线说,老师会觉得你有边界意识,不是乱吹功能。
第四,“为什么选ResNet18而不是更深网络?”回答思路:数据量有限,深层网络容易过拟合;CPU部署上ResNet18的推理速度和内存占用更友好;ResNet18的精度已满足项目指标要求。
第五,“有哪些指标可以优化?”回答思路:优化光照校正算法、增加舌体分割预处理、更大规模数据下的多标签分类、模型轻量化部署、加入摄像头实时检测能力,这些都能体现你的思考深度。
除此之外,答辩现场还有一个特别实用的技巧:准备一段3分钟以内的现场演示脚本,从打开软件、选择图片、点击识别、展示结果到查看历史记录,一气呵成。演示时一旦出错不要说太多废话去圆,直接说明这是因为环境问题,然后在PPT里准备好结果截图作为补充。我现场演示时就遇到过一次模型加载缓慢的情况,等了将近两秒,我顺势讲解了一下模型加载机制,把尴尬转化为展示内容。
最后再分享一个我做毕设最大的体会:这类项目最忌一开始就想把功能做全,一定要走通最小闭环再逐步优化。第一周哪怕只用命令行跑通一次训练和预测,整个项目就有了确定性;接下来再叠GUI、叠历史记录、叠打包发布,每一步都是在前一步的稳定基础上往前走。先让“图片进、结果出”这条主线动起来,后面的一切都顺了。
本文还有配套的精品资源,点击获取