
简介本资源是一套面向计算机科学与人工智能专业本科生的毕业设计级舌苔图像识别系统聚焦深度学习在中医舌诊辅助分析中的落地实践解决舌象特征提取、病理体征分类与可视化诊断等核心问题。压缩包共131个文件含26个Python主程序含模型训练、推理及PyQt5界面逻辑、6个.pth预训练权重、2个.ui界面文件、7个JPG/PNG测试样本、5个JSON配置与标注数据以及万字学术论文与技术文档整体大小为119.77MB。已有42人学习下载适用于毕业设计开题、中期实现与答辩全流程参考。读者可直接运行GUI程序进行舌苔图像上传与实时识别复现完整训练日志含TensorBoard events文件并基于模块化代码结构快速理解数据预处理、ResNet迁移学习、注意力机制融合及 PyQt5信号槽交互等关键技术实现细节。1. 项目拆解与整体设计思路1.1 这个系统到底解决什么问题舌苔识别这事放在中医里叫舌诊是望闻问切里望的核心环节。传统做法是老中医肉眼观察靠经验判断舌色、苔色、苔质再结合其他症状给出诊断。这套方法本身没问题但它有几个天然的痛点主观性强同一个舌头不同医生看可能结论不一样、经验依赖重年轻医生需要多年积累才能准确判断、不可量化没法用数据记录和回放。而深度学习图像分类技术成熟到今天这个程度恰好能在图像特征提取这件事上做到稳定、可复现。把舌苔照片交给一个训练好的卷积神经网络CNN模型能自动提取颜色、纹理、分布这些特征然后输出一个分类结果。这就是舌苔识别系统最核心的技术逻辑图像输入到分类输出中间全部由模型自动完成。我在做这个毕业设计的时候给自己定的目标很明确不搞那些花里胡哨的东西就是把一条完整的链路跑通——图像采集 → 预处理 → 模型推理 → 结果展示。前面三个环节用Python的深度学习生态解决最后一个环节用PyQt5搭一个桌面应用来完成。整个系统分两块一块是模型的训练与评估另一块是带图形界面的推理系统。两块独立开发最后通过模型文件对接起来。1.2 为什么选PyQt5而不是Web前端很多做深度学习项目的同学第一反应是做一个Web端演示系统Flask或FastAPI起个服务前端页面调接口。这也是一种方案但如果你做过就知道Web方案的坑其实不少环境依赖重、部署复杂、摄像头调用需要处理浏览器权限、还要考虑跨域问题。而作为毕业设计评审老师最看重的不是系统多么互联网化而是你真正理解并实现了一个完整的软件系统。PyQt5的优势在于它是Python生态里最成熟的桌面GUI框架之一基于Qt的C底层性能可靠界面组件齐全文件对话框、按钮、标签、表格、绘图控件都有现成的最关键的是它和OpenCV、PyTorch的配合非常顺滑——摄像头采集到的图像是numpy数组格式PyQt5的QImage可以直接从这个数组构建显示中间几乎不需要格式转换的开销。而且桌面应用在医助工具这个场景下本来就更合适。想象一下实际使用场景医生在诊室里打开软件摄像头对准患者舌头点击采集模型马上给出判断整个过程不需要打开浏览器、不需要网络请求、不需要担心数据上传到哪台服务器。这种本地化、单机化的体验恰恰是医疗辅助工具最看重的隐私和稳定性。1.3 系统总体架构与模块划分整个系统我拆成了五个模块每个模块职责单一、边界清晰模块职责关键技术点数据准备模块舌苔图像的采集、清洗、增强OpenCV、NumPy、数据增强策略模型训练模块训练图像分类模型并保存权重PyTorch、ResNet/MobileNet、迁移学习图像预处理模块推理前的图像标准化处理OpenCV、尺寸调整、归一化模型推理模块加载模型执行分类输出结果PyTorch、模型加载与推理UI交互模块桌面界面、控件逻辑、结果展示PyQt5、信号槽机制、QThread多线程模块之间的调用关系是单向的UI模块调用预处理模块和推理模块推理模块依赖训练好的模型文件。这样设计的好处有两点一是训练和推理解耦我可以在不打开界面的情况下用脚本测试模型效果二是如果以后想换模型架构只需要替换训练模块和模型文件UI层完全不用动。这里有个很关键的设计决策我单独说一下模型推理必须放到子线程里执行绝对不能直接写在UI主线程里。原因很简单——深度学习模型推理需要几百毫秒到几秒不等如果放在主线程里界面会直接卡死用户体验极差。PyQt5提供QThread或者更现代的QRunnableQThreadPool两种方式我下面会详细讲如何实现。2. 数据集处理与模型选型详解2.1 舌苔数据集的现实困境与应对策略做图像分类项目第一个绕不开的问题就是数据。公开的舌苔图像数据集非常少这一点和ImageNet那种动辄上千万张的大数据集完全没法比。原因也好理解舌苔图像涉及个人健康隐私采集需要专业医疗人员的参与而且标注工作必须有中医背景才能做。我当时用的是网上能找到的一个舌象数据集大概有3000多张图分为几种苔色和舌色类别比如白苔、黄苔、灰苔、红舌、淡红舌等。3000多张图对于一个深度学习分类任务来说属于小规模数据。怎么办两条路第一条路是数据增强。把每张图做随机旋转±15度、水平翻转、亮度/对比度调整、小范围缩放和裁剪。这样数据集能扩充到原来的10倍以上。这里要特别注意舌苔图像的增强策略不能乱用。比如你不能做垂直翻转舌头倒过来不符合实际采集情况、不能做极端的颜色扭曲舌色是诊断依据颜色偏了标注就废了、不能加太强的噪声会破坏苔质纹理。增强的目的是模拟真实采集中的光照变化、角度偏移、距离远近而不是无中生有制造新样本。第二条路是迁移学习。用小数据集从头训练一个CNN基本是死路很容易过拟合准确率也上不去。但如果你用ImageNet上预训练好的模型作为特征提取器只训练最后的全连接分类层数据量需求就大幅下降了。这就是迁移学习在中小规模数据集上的核心价值——你不需要几千张图就能拿到一个能用的模型。2.2 模型选型ResNet与MobileNet的取舍我对比了两个主流模型架构ResNet18和MobileNetV2。ResNet18是深度学习里的经典方案引入了残差连接解决深层网络退化问题18层结构在分类任务上效果稳定MobileNetV2是移动端优化的轻量级网络用深度可分离卷积大幅压缩参数量推理速度更快。如果你也在做这个项目我的建议是课题演示优先用ResNet18实际部署可以考虑MobileNetV2。原因很简单ResNet18的准确率通常更高一些在论文里写结果更好看MobileNetV2的优势在于模型体积小大约10MB左右、推理速度快但精度会略低一点点。选择模型之后要做一件关键操作修改最后的全连接层。预训练模型的输出类别是1000类ImageNet的类别数需要改成我们自己的类别数。在PyTorch里只需一行代码import torchvision.models as models # 加载预训练模型同时修改全连接层的输出维度 model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) num_features model.fc.in_features # ResNet18的fc层输入是512 num_classes 5 # 你自己的舌苔类别数 model.fc torch.nn.Linear(num_features, num_classes)这里有个容易踩的坑修改model.fc之后如果你直接加载之前保存的模型权重会因为维度不匹配报错。所以正确做法是先修改模型结构再加载预训练权重或者只加载权重中匹配的部分。2.3 训练策略与关键参数解读训练部分的代码框架不复杂真正的难点在于参数调试。我分享一下我的经验和理解一是优化器选择。我用了Adam优化器初始学习率设在1e-4。为什么是Adam而不是SGD因为Adam自适应调整每个参数的学习率收敛速度更快对学习率的敏感度更低比较适合像我这样没有大量时间做精细调参的场景。如果你想让精度再高一点可以在训练后期换用SGDmomentum或者对学习率做余弦退火。二是学习率调度。这里我强烈建议加一个ReduceLROnPlateau——当验证集的loss连续若干个epoch不下降时学习率自动降低一个数量级。这个技巧真的能救回很多训练跑偏的情况。代码就三行scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3, verboseTrue ) # 在每个epoch结束的验证阶段后调用 scheduler.step(val_loss)三是损失函数。多分类问题用交叉熵损失nn.CrossEntropyLoss()这个没什么好说的PyTorch的实现在内部已经做好了softmax和log的融合直接输入模型原始输出logits就行。四是迭代次数。我设了30个epoch每个epoch结束跑一次验证集。理论上loss会在15-20个epoch后开始收敛如果你发现训练集准确率很高但验证集准确率一直上不去那就是过拟合了需要加Dropout或者数据增强强度。关于训练效率我不建议在CPU上跑ResNet18不然一个epoch可能要十几分钟。我当时在实验室用一张GTX 1660Ti一个epoch只要1分钟左右30个epoch大概半小时结束。没有独立显卡的同学可以考虑用云GPU平台或者退而求其次用更小的MobileNetV2在CPU上跑。3. 核心代码实现与训练流程全记录3.1 环境配置从零搭建开发环境这个项目涉及PyTorch、OpenCV、PyQt5三个核心库环境配置看似简单其实有些细节需要注意。Python版本我推荐3.8或3.9太新的版本比如3.12可能会有部分库还没有预编译的wheel包安装时容易出问题。安装顺序建议为# 1. 先建虚拟环境避免污染全局Python python -m venv tongue_env # Windows下激活 tongue_env\Scripts\activate # Linux/Mac下激活 source tongue_env/bin/activate # 2. 安装PyTorchCPU版和GPU版的命令不同根据需要选择 # GPU版需要先确认你的CUDA版本 pip install torch torchvision # CPU版 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 3. 安装其他依赖 pip install opencv-python pillow numpy matplotlib pip install pyqt5 pyqt5-tools几个容易出问题的点一是PyQt5安装后Qt Designer界面设计工具在哪如果你装了pyqt5-tools在虚拟环境目录的Lib\site-packages\pyqt5_tools下能找到。不过我后来发现手写代码比拖控件更快——尤其是界面布局不复杂的时候手写布局代码更直观、更好控制。二是OpenCV的cv2.imread默认按BGR通道读取图像而PyTorch训练时用的是RGB顺序这个颜色通道问题会在显示和推理时造成颜色不对后面我会专门说。3.2 图像预处理管线的关键细节舌苔图像的预处理比通用图像分类要更讲究因为颜色信息是诊断的核心依据。我总结的预处理管线包括以下几个环节import cv2 import numpy as np def preprocess_tongue_image(image, target_size(224, 224)): # 1. 转换为RGB如果是用OpenCV读的 if image.ndim 3 and image.shape[2] 3: # OpenCV默认是BGR转换到RGB image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 2. 缩放并居中裁剪 h, w image.shape[:2] # 先等比缩放让短边等于target_size scale target_size[0] / min(h, w) new_w, new_h int(w * scale), int(h * scale) resized cv2.resize(image, (new_w, new_h), interpolationcv2.INTER_LINEAR) # 再居中裁剪到目标尺寸 start_x (new_w - target_size[1]) // 2 start_y (new_h - target_size[0]) // 2 cropped resized[start_y:start_y target_size[0], start_x:start_x target_size[1]] # 3. 转为float并归一化到[0,1] cropped cropped.astype(np.float32) / 255.0 # 4. 标准化使用ImageNet的均值和标准差 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) normalized (cropped - mean) / std # 5. 调整维度顺序HWC - CHW并增加batch维度 # PyTorch的输入是 (N, C, H, W) tensor np.transpose(normalized, (2, 0, 1)) tensor np.expand_dims(tensor, axis0) return tensor这个管线里有几个细节必须说明。第一训练和推理必须用完全相同的预处理方式。很多人训练时做了数据增强推理时忘了做基本的缩放和标准化结果模型效果一塌糊涂。第二标准化的均值和标准差要用训练集统计的用ImageNet的默认值也可以但一定要保持一致。第三不要用双线性插值以外的重采样方法最近邻插值会有锯齿双三次插值速度慢双线性是平衡之选。3.3 训练脚本与结果评估训练脚本的核心结构如下。我不贴全部代码太长只放关键框架# train.py 核心部分 import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms, models from torchvision.datasets import ImageFolder # 定义数据增强和预处理 train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) # 加载数据 train_dataset ImageFolder(data/train, transformtrain_transform) val_dataset ImageFolder(data/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) # 构建模型 model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) model.fc nn.Linear(model.fc.in_features, len(train_dataset.classes)) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) # 训练循环 criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-4) for epoch in range(30): model.train() running_loss 0.0 for inputs, labels in train_loader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 验证 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) _, predicted torch.max(outputs, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() val_acc val_correct / val_total print(fEpoch {epoch1}: loss{running_loss/len(train_loader):.4f}, fval_acc{val_acc:.4f}) # 保存模型 torch.save(model.state_dict(), tongue_model.pth)训练完成后我把验证集结果整理成混淆矩阵这是毕业设计答辩里非常好的展示材料。它能直观看出模型在哪些类别之间容易混淆。比如我的结果显示模型容易把薄白苔和淡红舌搞混——后来想了想因为这两类在训练数据里样本量不太均衡而且薄白苔和淡红舌在图像上确实有很多相似区域。针对这个问题我给样本量少的类别增加了过采样和更强的数据增强再训练一轮后混淆率明显下降了。3.4 推理代码与模型加载的注意事项训练好模型后推理端的代码很简单但是有一个容易踩坑的点模型定义的代码必须和训练时一致。比如如果你在训练脚本里写了models.resnet18(weights...)然后修改了fc层保存了state_dict那么在推理脚本里也必须用同样的方式构建模型再加载权重否则会报size mismatch的错误。# inference.py import torch import torchvision.models as models class TongueClassifier: def __init__(self, model_pathtongue_model.pth, num_classes5): # 构建与训练时完全一致的模型结构 self.model models.resnet18(weightsNone) self.model.fc torch.nn.Linear(self.model.fc.in_features, num_classes) # 加载训练好的权重 self.model.load_state_dict(torch.load(model_path, map_locationcpu)) self.model.eval() # 切换为评估模式 self.class_names [淡红舌, 红舌, 白苔, 黄苔, 灰苔] def predict(self, input_tensor): with torch.no_grad(): outputs self.model(input_tensor) probs torch.softmax(outputs, dim1) confidence, pred_idx torch.max(probs, dim1) return self.class_names[pred_idx.item()], confidence.item()注意self.model.eval()这行不能省。PyTorch的模型在训练和推理模式下Dropout和BatchNorm的行为是不同的。如果你忘了切到eval模式同样的输入每次推理结果都可能不一样而且准确率会下降。这是网上提问最多的坑之一。4. PyQt5界面开发与交互逻辑实现4.1 界面布局设计与功能拆解整个桌面应用的主窗口我设计成三块区域左侧图像显示区。用于显示摄像头实时画面或用户选择的图片也是系统最直观的输出部分。右上控制面板。包含打开摄像头拍照采集从文件选择图片开始识别保存报告等按钮。右下识别结果区。显示预测类别、置信度以及一个简单的诊断参考文本区域可以写一些中医知识的说明让系统看起来更完整。界面布局用PyQt5的QHBoxLayout和QVBoxLayout组合实现。这是我最推荐的方式而非用Qt Designer拖控件——手写布局代码在版本管理、代码审查和后期修改上的体验都更好。核心代码结构如下from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTextEdit, QFileDialog) from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt, QTimer import cv2 class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(基于深度学习的舌苔识别系统) self.setMinimumSize(900, 600) self._init_ui() self.classifier TongueClassifier() self.capture None self.timer QTimer() self.timer.timeout.connect(self.update_frame) def _init_ui(self): # 图像显示区域 self.image_label QLabel() self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setMinimumSize(480, 360) self.image_label.setStyleSheet(border: 1px solid #ccc; background: #f8f8f8;) self.image_label.setText(图像区域) # 控制按钮 self.btn_camera QPushButton(打开摄像头) self.btn_capture QPushButton(拍照采集) self.btn_open QPushButton(打开图片) self.btn_predict QPushButton(开始识别) self.btn_save QPushButton(保存报告) # 结果展示 self.result_label QLabel(识别结果) self.result_label.setWordWrap(True) self.confidence_label QLabel(置信度) self.detail_text QTextEdit() self.detail_text.setReadOnly(True) # 布局 # ...省略布局代码4.2 摄像头实时采集与图像显示摄像头实时采集是演示环节的加分项代码却并不复杂。核心思路是用OpenCV的VideoCapture打开摄像头用一个QTimer定时器每几十毫秒抓取一帧在界面上刷新显示。def open_camera(self): if self.capture is None: self.capture cv2.VideoCapture(0) if not self.capture.isOpened(): self.detail_text.append(摄像头打开失败请检查设备连接) return self.timer.start(30) # 每30ms刷新一帧约33fps self.btn_camera.setText(关闭摄像头) else: self.timer.stop() self.capture.release() self.capture None self.btn_camera.setText(打开摄像头) def update_frame(self): ret, frame self.capture.read() if ret: # BGR转RGB再转为QImage显示 rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb_frame.shape bytes_per_line ch * w q_img QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888) # 显示时缩放保持长宽比 pixmap QPixmap.fromImage(q_img) pixmap pixmap.scaled(self.image_label.size(), Qt.KeepAspectRatio) self.image_label.setPixmap(pixmap) # 保存当前帧供后续识别使用 self.current_frame frame.copy()这里需要注意一个细节QImage构造时要传入bytes_per_line每行字节数。如果你忘了这个参数图像可能会显示成扭曲的、带斜条纹的乱码。因为numpy数组的内存布局里每行末尾可能有填充字节不告诉QImage的话它就会用默认值导致错位。另外一个容易遇到的问题Qt.KeepAspectRatio缩放后图像显示区域的空白部分默认是黑色背景。如果你想让背景看起来更协调可以在QLabel的样式表里设置背景色比如background: #f8f8f8;。有些同学这里没设置演示截图时背景是黑块视觉效果差很多。4.3 模型推理集成与多线程处理模型推理放到子线程这是我在第三节末尾提到的关键点。PyQt5在子线程和主线程之间传递结果最方便的方式是自定义信号。我的实现思路如下from PyQt5.QtCore import QThread, pyqtSignal import numpy as np class InferenceThread(QThread): result_ready pyqtSignal(str, float) # 类别名, 置信度 def __init__(self, classifier, image): super().__init__() self.classifier classifier self.image image # numpy数组 def run(self): try: # 预处理在子线程中执行 input_tensor preprocess_tongue_image(self.image) # 推理 class_name, confidence self.classifier.predict(input_tensor) self.result_ready.emit(class_name, confidence) except Exception as e: self.result_ready.emit(识别失败, 0.0)在主线程里点击开始识别按钮时启动这个线程并通过result_ready信号连接一个槽函数来更新界面def on_predict_clicked(self): if self.current_frame is None: self.detail_text.append(请先采集图像或打开图片) return self.detail_text.append(正在识别中请稍候...) self.thread InferenceThread(self.classifier, self.current_frame) self.thread.result_ready.connect(self.on_result_received) self.thread.start() def on_result_received(self, class_name, confidence): self.result_label.setText(f识别结果{class_name}) self.confidence_label.setText(f置信度{confidence*100:.2f}%)使用多线程有几个好处值得在论文里强调一是界面不会卡顿用户体验提升二是在模型推理期间用户仍然可以操作其他控件比如打开另一张图片三是避免系统未响应被Windows杀掉。这个设计思路体现了软件工程中主线程负责UI工作线程负责耗时任务的经典原则答辩时能讲到这个点老师会认为你考虑得很周全。4.4 图片选择、保存报告与中医知识展示从文件选择图片功能用QFileDialog.getOpenFileName实现配合cv2.imread读取。读取后的图片同样要在界面上显示并且存入self.current_frame以便后续识别。def on_open_clicked(self): file_path, _ QFileDialog.getOpenFileName( self, 选择图片, , 图片文件 (*.jpg *.jpeg *.png *.bmp) ) if file_path: img cv2.imread(file_path) if img is None: self.detail_text.append(图片读取失败请检查文件格式) return self.current_frame img rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch*w, QImage.Format_RGB888) self.image_label.setPixmap( QPixmap.fromImage(qimg).scaled( self.image_label.size(), Qt.KeepAspectRatio ) )保存报告功能也值得做毕业设计系统有输出物会比纯界面识别更完整。实现很简单把识别结果和中医知识写入一个文本文件或者HTML文件。我选了HTML格式因为可以直接在浏览器里打开排版也好看。关于中医知识展示我建议做成一个字典映射每种舌苔类别对应一段中医描述包括舌象特征临床意义可能关联的症候等。这部分内容不需要太深但一定要有参考来源比如《中医诊断学》教材里的描述。体现的是你做了领域调研而不只是一个技术demo。5. 常见问题排查与实战避坑清单5.1 环境与安装类问题速查我在开发和测试过程中整理了这套项目里最常遇到的几个问题按频率排序列成表格方便你排查现象原因解决方式pip install pyqt5安装极慢或超时网络问题或源速度慢换国内镜像源pip install pyqt5 -i https://pypi.tuna.tsinghua.edu.cn/simplePyQt5安装成功但import报错Python版本过高缺少预编译包换用Python 3.8/3.9创建新虚拟环境重装打开摄像头时程序崩溃OpenCV无法访问摄像头或权限不足检查设备管理器确认摄像头正常确保没有其他程序占用摄像头macOS下需要授权终端摄像头权限推理结果一直不变可能没有切到model.eval()模式在predict方法开头加上self.model.eval()模型加载报size mismatch推理模型的类别数或结构参数与训练时不一致确认num_classes参数和训练时的值一致确认模型的backbone类型一致界面图片颜色偏蓝/偏绿OpenCV的BGR和RGB通道顺序未转换显示和推理前用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)转换识别时界面卡死推理在UI主线程中执行将推理逻辑放入QThread子线程中摄像头画面闪烁或卡顿QTimer间隔过小或处理耗时过长将QTimer间隔从10ms改为30-50ms缩小显示缩放计算的频率5.2 模型训练中常见的隐性陷阱训练阶段的坑通常不会直接报错而是表现为模型不收敛准确率上不去loss异常这类隐性现象。我实际经历过几个**第一数据增强和归一化顺序写反了。**在transforms.Compose里ToTensor()之前做Normalize()会直接报错因为此时图像还是PIL格式或numpy数组没有变成Tensor。但如果你用了transforms.ToTensor()之后再做ColorJitter之类的增强操作虽然不报错但行为就不对了——因为ToTensor()会把像素值从0-255缩放到0-1范围这时候再做亮度调整的效果和预期完全不一样。**第二类别不平衡没处理。**舌苔数据集中可能黄苔有1000张而灰苔只有180张。如果不做处理模型会倾向于把大多数样本预测为黄苔整体准确率虚高但实际上对少数类的识别几乎失效。解决方式有两个一是计算类别权重并传给CrossEntropyLoss的weight参数二是对少数类做过采样。我个人更推荐前者实现更简单而且效果足够好。# 根据每个类别的样本数计算权重 from sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight( balanced, classesnp.unique(dataset_labels), ydataset_labels ) class_weights torch.tensor(class_weights, dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)**第三验证集不能做数据增强。**有些同学图省事训练和验证共用一套transform导致训练集和验证集之间的分布不一致验证准确率忽高忽低。验证集应该只用基本的缩放、ToTensor和Normalize不要加随机旋转、翻转这些会改变样本语义的操作。我见过不少论文里的验证集还带水平翻转这在实际评估中是不严谨的。5.3 毕业设计答辩的展示技巧既然标题里带了毕业设计最后聊几句答辩和演示环节的心得。一定要准备一个演示脚本——不是念稿子而是提前设计好操作路径。比如说启动界面 → 打开一张测试图 → 点击识别 → 展示结果和置信度。整个过程最好不要超过3分钟。如果你要展示摄像头实时识别务必提前测试好光线因为摄像头在室内暗光环境下的成像质量会严重影响识别准确率。论文里务必包含混淆矩阵和训练loss曲线。这两张图能直观证明你的模型是训练过来的不是瞎蒙的。我的建议是用matplotlib保存训练过程中每个epoch的loss值最终画出曲线用sklearn的confusion_matrix生成混淆矩阵再用seaborn的heatmap画出热力图。这两个图一放答辩完整度立刻上一个台阶。讲清楚为什么用深度学习。很多同学在答辩时只说我用了一个CNN模型但老师真正想问的是为什么不能用传统方法这里可以简单说明传统计算机视觉方法比如颜色直方图、纹理特征对拍摄条件变化和个体差异非常敏感舌苔图像的颜色和纹理边界模糊传统特征工程需要大量人工设计特征而且泛化能力有限。深度学习通过自动学习特征表达在图像分类任务上显著优于传统方法。这样回答既解释了技术选型逻辑也体现你对领域的理解。别在演示时临时跑训练。答辩现场的网络、显卡环境都不确定最好提前把模型训练好生成好所有图表答辩时只演示推理和界面。如果现场网络不好pip install任何东西都会很痛苦所以重要的依赖请在答辩前装好并测试通过。我个人在实际操作中还有一个体会整套系统的核心是完整跑通而不是算法创新。尤其是本科毕业设计老师期待的是一个你真正理解并完成的软件系统而不是一个你在某个开源项目上改了改参数的东西。从头搭一遍环境、写一遍数据预处理、训一遍模型、做一遍界面这个过程走下来你对深度学习和Python工程化的理解会比刷十篇教程都深刻。哪怕效果不是最好的你也知道哪里能改、怎么改这种掌控感才是这个项目真正值回票价的地方。本文还有配套的精品资源点击获取