简介:本资源为基于深度学习的舌苔识别检测鉴定系统完整项目包,面向计算机、人工智能及中医药信息化方向的毕业设计学生与开发者,帮助解决舌象分类、体质辨识等课题从建模到界面落地的全流程需求。包内共110个文件,以26个Python源码、6个pth模型权重、2个ui界面文件、2个docx论文文档及json配置、jpg舌象样本、pyc缓存等为主,压缩包约104.93MB,涵盖数据标注、图像增强、DCGAN扩充、检测网络设计与GUI交互等模块。资源配套毕业论文,系统梳理了课题背景、机器学习理论、需求分析、舌象数据集构建与扩充、舌苔检测网络设计等章节,便于读者理解算法选型与实验思路。目前已有536人学习下载,适合需要完整赛题方案、可运行源码、预训练模型与论文写作参考的读者,也可作为深度学习图像分类项目的实践模板。
1. 舌苔识别系统拆包:一份能跑通的深度学习毕业设计长什么样
舌象采集卡在「拍完照没人会看」这件事上很久了。中医体质辨识里,舌苔的颜色、厚薄、润燥是绕不开的观察项,但让非专业人士对着镜子判断白苔黄苔、薄苔厚苔,基本靠猜。这份基于深度学习的舌苔识别检测鉴定系统,把「拍照 → 分类 → 体质倾向提示」串成了一条可演示的链路,交付物是 Python 源码、PyQt5 图形界面、训练好的模型权重,外加一份完整的毕业论文。它适合三类人:正在做深度学习方向毕业设计、需要一套能讲清「数据怎么来、模型怎么训、界面怎么接」的完整参考;想入门 CNN 图像分类、但不想从零搭脚手架的自学者;以及需要快速验证舌象分类思路的产品原型开发者。资源里那串events.out.tfevents.*文件是 TensorBoard 训练日志,说明模型确实被训练过,不是空壳代码——这一点比很多只给架构图的「源码包」实在得多。
2. 环境搭建与依赖安装:把 TensorFlow、PyQt5、OpenCV 装到一条线上
2.1 为什么这套组合对版本特别敏感
深度学习项目最劝退新手的不是写代码,是装环境。这份源码同时依赖 TensorFlow(训练与推理)、PyQt5(界面)、OpenCV(图像预处理)、NumPy(矩阵运算),四者之间存在版本咬合关系。TensorFlow 2.x 与 PyQt5 本身不冲突,但 TensorFlow 对 NumPy 版本有上限要求——比如 TF 2.10 要求 NumPy < 1.24,而某些新版 OpenCV 又依赖更高版本 NumPy,装错顺序就会出现numpy.dtype size changed这类二进制不兼容报错。常见做法是先锁定 TensorFlow 版本,再让 pip 自动解析其余依赖,而不是一个个手动装最新版。
我一般会建议用虚拟环境隔离,避免污染系统 Python。下面这套流程在 Windows 和 Linux 上都验证过:
# 创建虚拟环境,Python 建议 3.8~3.10,TF 2.x 对 3.11+ 支持较晚 python -m venv venv # Windows 激活 venv\Scripts\activate # Linux / macOS 激活 source venv/bin/activate # 先装 TensorFlow,让它决定 NumPy 版本 pip install tensorflow==2.10.0 # 再装界面与图像库,不指定版本让 pip 适配 pip install pyqt5 opencv-python pillow matplotlib # 最后核对关键包版本 pip list | findstr "tensorflow numpy opencv pyqt5"这段命令的逻辑是「先定大头、再补小件」。TensorFlow 是依赖链的根,先装它,pip 会把 NumPy 拉到兼容区间;随后装 OpenCV 时,如果它要求的 NumPy 与已装版本冲突,pip 会给出明确提示,而不是等到运行时才崩。参数上,tensorflow==2.10.0只是示例,实际以源码requirements.txt为准——如果包里没带这个文件,就按源码里import tensorflow后调用的 API 反推,比如用了tf.keras.applications就说明是 2.x。
提示:如果安装 TensorFlow 时卡在下载,优先换国内镜像源,命令末尾加
-i https://pypi.tuna.tsinghua.edu.cn/simple,不要反复重试默认源。
2.2 验证环境是否真的可用
装完不代表能用。TensorFlow 和 PyQt5 都有「装上了但导入报错」的经典情况。跑一段最小验证脚本,比直接启动主程序更快定位问题:
# check_env.py —— 环境自检脚本 import sys def check(): # 检查 Python 版本,低于 3.7 直接劝退 print("Python:", sys.version) try: import tensorflow as tf print("TensorFlow:", tf.__version__) # 确认 GPU 是否可见,没有 GPU 会返回空列表,属正常 print("GPU devices:", tf.config.list_physical_devices('GPU')) except Exception as e: print("TensorFlow 导入失败:", e) try: from PyQt5.QtWidgets import QApplication print("PyQt5: OK") except Exception as e: print("PyQt5 导入失败:", e) try: import cv2 print("OpenCV:", cv2.__version__) except Exception as e: print("OpenCV 导入失败:", e) if __name__ == "__main__": check()逻辑说明:把三个核心库分别 try 住,哪个失败就单独报哪个,避免一个报错掩盖其他问题。参数上不需要改任何东西,直接python check_env.py运行。如果 TensorFlow 导入失败且报DLL load failed,在 Windows 上通常是缺 Visual C++ 运行库,装一个 VC_redist 即可;如果 PyQt5 报could not find Qt platform plugin,是 Qt 插件路径没配好,重装 PyQt5 一般能解决。
3. 数据集构建与 DCGAN 扩充:舌象图片从哪来、怎么补
3.1 舌象数据的标注分类逻辑
舌苔识别本质是图像分类任务,标签体系决定了模型能输出什么。从论文目录看,这份资源把舌象按舌苔特征做了分类标注,常见维度包括苔色(白、黄、灰黑)和苔质(薄、厚、腻、剥)。标注方式一般是按文件夹分目录,一个类别一个文件夹,这是tf.keras.preprocessing.image_dataset_from_directory最省事的组织方式。如果源码里用的是自定义 Dataset,那多半在 CSV 里存了「文件名 → 标签」映射。
数据量是这类项目的命门。舌象公开数据集本来就少,自己拍又受光线、设备、伸舌姿势影响,所以论文里专门写了「使用图像增强扩充数据集」和「DCGAN 生成舌象图片」两节。图像增强是常规操作——旋转、翻转、调亮度、加噪声,用ImageDataGenerator几行就能做。DCGAN 则是用生成对抗网络造新样本,适合原始图片只有几百张、直接训 CNN 必然过拟合的情况。
# data_augment.py —— 用 ImageDataGenerator 做基础增强 from tensorflow.keras.preprocessing.image import ImageDataGenerator # 训练集增强:旋转、平移、剪切、缩放、翻转都开 train_datagen = ImageDataGenerator( rescale=1./255, # 像素归一化到 0~1 rotation_range=20, # 随机旋转 ±20 度 width_shift_range=0.1, # 水平平移 10% height_shift_range=0.1, # 垂直平移 10% shear_range=0.1, # 剪切变换 zoom_range=0.1, # 缩放 horizontal_flip=True, # 水平翻转,舌象左右对称,安全 fill_mode='nearest' # 填充边缘像素 ) # 验证集只做归一化,不能增强,否则评估失真 val_datagen = ImageDataGenerator(rescale=1./255) train_gen = train_datagen.flow_from_directory( 'dataset/train', target_size=(224, 224), # 与模型输入尺寸一致 batch_size=32, class_mode='categorical' )逻辑说明:增强只作用于训练集,验证集和测试集必须保持原图分布,否则准确率虚高。参数上target_size要和模型输入层对齐,改模型就得同步改这里;batch_size受显存限制,8G 显存跑 224×224 一般 32 没问题,报 OOM 就降到 16。horizontal_flip对舌象是安全的,因为舌头左右翻转不改变病理特征,但如果是文字类图像就不能开。
3.2 DCGAN 生成舌象图片的实操要点
DCGAN 由生成器 G 和判别器 D 组成,G 把随机噪声变成假图,D 判断真假,两者对抗训练。论文里用 DCGAN 生成舌象,目的是补充稀缺类别。实操时最容易翻车的地方是训练不稳定——G 和 D 的损失此消彼长,生成器崩掉后输出全是噪点。
# dcgan_generator.py —— 生成器结构(简化版) from tensorflow.keras import layers, models def build_generator(latent_dim=100): model = models.Sequential([ # 输入噪声向量,reshape 成 7x7x256 的特征图 layers.Dense(7 * 7 * 256, use_bias=False, input_shape=(latent_dim,)), layers.BatchNormalization(), layers.LeakyReLU(alpha=0.2), layers.Reshape((7, 7, 256)), # 上采样到 14x14 layers.Conv2DTranspose(128, (5, 5), strides=(2, 2), padding='same', use_bias=False), layers.BatchNormalization(), layers.LeakyReLU(alpha=0.2), # 上采样到 28x28 layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False), layers.BatchNormalization(), layers.LeakyReLU(alpha=0.2), # 输出 28x28x3,tanh 把像素压到 -1~1 layers.Conv2DTranspose(3, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh') ]) return model逻辑说明:latent_dim=100是 DCGAN 论文的默认噪声维度,改大改小都会影响生成多样性。Conv2DTranspose是转置卷积,负责逐层放大特征图。LeakyReLU的 alpha 设 0.2 是 DCGAN 的标准配置,用普通 ReLU 容易导致梯度死亡。输出层用tanh是因为判别器输入也归一化到 -1~1,两边必须一致。训练时如果生成器输出始终是灰蒙蒙一片,先检查判别器是不是太强——可以降低 D 的学习率,或者给 D 的标签加一点噪声。
注意:DCGAN 生成的图片只能用于扩充训练集,不能混进验证集和测试集,否则评估结果没有意义。生成样本建议人工过一遍,明显畸形的直接剔除。
4. CNN 模型训练与 TensorBoard 日志解读:那堆 tfevents 文件到底记了什么
4.1 模型结构与训练脚本的关键参数
从论文目录「深度神经网络的结构和概念」「卷积神经网络」可以推断,模型主干是标准 CNN,可能带 BatchNormalization 和 Dropout。训练脚本的核心是model.compile和model.fit两个调用,参数设错会直接导致不收敛或过拟合。
# train.py —— 训练主流程 import tensorflow as tf from tensorflow.keras import layers, models, callbacks # 构建 CNN:三层卷积 + 全连接 model = models.Sequential([ layers.Conv2D(32, (3, 3), activation='relu', input_shape=(224, 224, 3)), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu'), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activation='relu'), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(128, activation='relu'), layers.Dropout(0.5), # 丢弃一半神经元,抑制过拟合 layers.Dense(num_classes, activation='softmax') # 多分类输出 ]) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4), loss='categorical_crossentropy', metrics=['accuracy'] ) # 回调:TensorBoard 日志 + 早停 + 保存最优权重 callbacks_list = [ callbacks.TensorBoard(log_dir='./logs', histogram_freq=1), callbacks.EarlyStopping(patience=10, restore_best_weights=True), callbacks.ModelCheckpoint('best_model.h5', save_best_only=True) ] history = model.fit( train_gen, epochs=50, validation_data=val_gen, callbacks=callbacks_list )逻辑说明:learning_rate=1e-4是 Adam 在图像分类上的常用起点,太大震荡、太小收敛慢。Dropout(0.5)放在全连接层前,卷积层一般不加。EarlyStopping的patience=10表示验证损失连续 10 轮不降就停,restore_best_weights=True保证拿到的是最优轮次的权重而不是最后一轮。ModelCheckpoint只存最优模型,避免硬盘被中间权重塞满。如果训练时 loss 一直是nan,先查学习率是不是太大,再查数据里有没有标签越界。
4.2 用 TensorBoard 读训练日志
资源里那串events.out.tfevents.1652188470.LAPTOP-ACFSLO5L.12688.0就是 TensorBoard 的日志文件,文件名里的数字是时间戳和进程号。这些文件记录了每轮的 loss、accuracy、学习率等标量,有的还带直方图。启动 TensorBoard 就能把训练过程可视化:
# 指向日志所在目录,不是单个文件 tensorboard --logdir=./logs --port=6006 # 浏览器打开 http://localhost:6006逻辑说明:--logdir指向包含 tfevents 文件的文件夹,TensorBoard 会自动扫描所有子目录。如果日志分散在多个文件夹,可以指向它们的共同父目录,TensorBoard 会按文件夹分组展示。--port默认 6006,被占用就换一个。看曲线时重点看两条:训练 loss 和验证 loss。如果训练 loss 持续下降但验证 loss 先降后升,是典型过拟合,需要加数据增强或加 Dropout;如果两条都居高不下,是欠拟合,要加模型容量或降学习率。
提示:tfevents 文件是二进制格式,不要用文本编辑器打开,看到乱码是正常的,只能用 TensorBoard 解析。
5. PyQt5 界面与模型推理对接:从选图到出结果的完整链路
5.1 界面布局与信号槽机制
PyQt5 的核心是信号槽——按钮点击是一个信号,绑定的函数是槽。这份资源的 GUI 大概率包含「选择图片」「开始识别」「显示结果」三个区域。界面和推理逻辑要解耦,否则换个模型就得改界面代码。
# main_window.py —— 界面与推理对接 import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget, QFileDialog) from PyQt5.QtGui import QPixmap from predict import predict_image # 推理函数单独放一个模块 class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("舌苔识别系统") self.resize(600, 500) # 控件 self.btn_select = QPushButton("选择舌象图片") self.btn_predict = QPushButton("开始识别") self.label_img = QLabel("未选择图片") self.label_result = QLabel("等待识别") # 布局 layout = QVBoxLayout() layout.addWidget(self.btn_select) layout.addWidget(self.label_img) layout.addWidget(self.btn_predict) layout.addWidget(self.label_result) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) # 信号槽绑定 self.btn_select.clicked.connect(self.on_select) self.btn_predict.clicked.connect(self.on_predict) self.image_path = None def on_select(self): # 打开文件对话框,过滤常见图片格式 path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "Images (*.png *.jpg *.jpeg *.bmp)") if path: self.image_path = path pixmap = QPixmap(path).scaled(300, 300) self.label_img.setPixmap(pixmap) def on_predict(self): if not self.image_path: self.label_result.setText("请先选择图片") return # 调用推理函数,返回类别和置信度 label, conf = predict_image(self.image_path) self.label_result.setText(f"识别结果:{label} 置信度:{conf:.2%}") if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())逻辑说明:on_select负责选图和预览,on_predict负责调推理。把predict_image单独放模块里,界面只关心「拿到路径、显示结果」,模型换不换跟界面无关。参数上scaled(300, 300)是预览尺寸,不影响推理用的原图。QFileDialog的过滤器写成*.png *.jpg *.jpeg *.bmp,覆盖常见格式。如果点击按钮没反应,先检查clicked.connect有没有写错,再检查槽函数有没有被异常吞掉——PyQt5 默认不打印槽函数里的异常,加个 try 包一下能看到真实错误。
5.2 推理函数的预处理必须和训练一致
这是最容易翻车的地方:训练时图片归一化到 0~1,推理时忘了除 255,结果全错。预处理必须逐字对齐训练脚本。
# predict.py —— 单张图片推理 import numpy as np import cv2 from tensorflow.keras.models import load_model # 全局加载一次模型,避免每次点击都重新加载 model = load_model('best_model.h5') class_names = ['薄白苔', '厚白苔', '黄苔', '灰黑苔'] # 按训练时的顺序 def predict_image(img_path): # 读图并转 RGB,OpenCV 默认是 BGR img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 缩放到训练时的输入尺寸 img = cv2.resize(img, (224, 224)) # 归一化,必须和训练时的 rescale=1./255 一致 img = img.astype('float32') / 255.0 # 加 batch 维度,模型要求 (1, 224, 224, 3) img = np.expand_dims(img, axis=0) # 推理 preds = model.predict(img) idx = np.argmax(preds[0]) return class_names[idx], float(preds[0][idx])逻辑说明:cv2.cvtColor转 RGB 是因为 OpenCV 读进来是 BGR,而训练时用的ImageDataGenerator按 RGB 处理,不转就颜色错位。np.expand_dims加 batch 维度,模型只接受四维输入。class_names的顺序必须和训练时flow_from_directory的类别顺序一致——那个顺序是按文件夹名字母序排的,不是随便写的。如果识别结果总是同一类,先打印preds看看是不是某个值恒为 1,那多半是预处理没对齐。
6. 避坑与排查:五个让这套系统跑不起来的真实原因
6.1 模型加载报「Unknown layer」或「No model found」
现象:load_model('best_model.h5')抛异常,提示未知层类型或文件不存在。原因通常是保存模型时用了自定义层,加载时没传custom_objects;或者路径写的是相对路径,而程序的工作目录和模型不在同一层。解决:先确认best_model.h5和运行脚本在同一目录,用绝对路径最稳;如果确实有自定义层,加载时写成load_model('best_model.h5', custom_objects={'MyLayer': MyLayer})。另外 Keras 保存的.h5和 TensorFlow SavedModel 格式不通用,别混着加载。
6.2 界面能开但点识别就闪退
现象:PyQt5 窗口正常显示,点「开始识别」后程序直接退出,控制台没有明显报错。原因多半是槽函数里抛了异常,而 PyQt5 默认把槽函数异常打到 stderr 后继续,某些情况下会直接终止。解决:在on_predict里用 try/except 包住推理调用,把异常打到label_result上,这样至少能看到错误信息。常见异常是模型没加载成功、图片路径含中文导致cv2.imread返回 None。
6.3 训练准确率 99% 但实际识别全错
现象:TensorBoard 上训练集和验证集准确率都接近 1,但拿新图片测试一塌糊涂。原因通常是数据泄漏——增强后的图片同时进了训练集和验证集,或者同一张原图的不同增强版本被分到了两个集合。解决:先按原图划分训练/验证/测试,再对训练集做增强,验证集和测试集只做归一化。DCGAN 生成的图片也只能进训练集。这个坑在毕业设计里特别常见,答辩时被问「验证集怎么划分的」很容易露馅。
6.4 TensorBoard 启动后看不到任何曲线
现象:tensorboard --logdir=./logs跑起来了,浏览器打开一片空白。原因通常是logdir指错了层级,或者训练时TensorBoard回调的log_dir和启动时指定的不是同一个。解决:先确认./logs下确实有events.out.tfevents.*文件,没有就是训练时没写进去;有的话检查路径拼写,Windows 上反斜杠和正斜杠混用有时会出问题,统一用正斜杠。如果日志目录下有多个子文件夹,TensorBoard 会按子文件夹分组,曲线可能藏在折叠项里。
6.5 换自己的数据集后类别数对不上
现象:用自己拍的舌象替换原数据集,训练时提示标签维度不匹配,或者推理时class_names越界。原因:num_classes是写死在模型最后一层的,换了数据集类别数变了但没改。解决:改Dense(num_classes, activation='softmax')里的num_classes,同时更新predict.py里的class_names列表,两者长度必须一致。另外flow_from_directory会自动按文件夹数推断类别数,如果文件夹里混了非图片文件,类别数会多出来,训练前先清理。
7. 进阶技巧:用迁移学习把准确率再拉一截
原模型的 CNN 是从零训练的,数据量不大的情况下,迁移学习往往能明显提升效果。做法是拿 ImageNet 上预训练的模型(如 MobileNetV2、ResNet50)当特征提取器,只训练最后的分类层,再解冻部分卷积层做微调。这套资源里的舌象分类任务,用 MobileNetV2 替换原 CNN,通常能在同样数据量下把验证准确率提高几个百分点,而且训练更快。
# transfer_learning.py —— 用 MobileNetV2 做迁移学习 import tensorflow as tf from tensorflow.keras import layers, models # 加载预训练主干,不含顶层分类器 base = tf.keras.applications.MobileNetV2( input_shape=(224, 224, 3), include_top=False, weights='imagenet' ) # 冻结主干,先只训分类头 base.trainable = False model = models.Sequential([ base, layers.GlobalAveragePooling2D(), # 替代 Flatten,参数更少 layers.Dropout(0.3), layers.Dense(num_classes, activation='softmax') ]) model.compile( optimizer=tf.keras.optimizers.Adam(1e-3), loss='categorical_crossentropy', metrics=['accuracy'] ) # 第一阶段:只训分类头 model.fit(train_gen, epochs=10, validation_data=val_gen) # 第二阶段:解冻最后 30 层做微调,学习率调小 base.trainable = True for layer in base.layers[:-30]: layer.trainable = False model.compile( optimizer=tf.keras.optimizers.Adam(1e-5), # 微调学习率要小 loss='categorical_crossentropy', metrics=['accuracy'] ) model.fit(train_gen, epochs=20, validation_data=val_gen)逻辑说明:迁移学习分两阶段。第一阶段主干冻结,只训分类头,让随机初始化的分类层先收敛,避免一开始就把预训练权重带偏。第二阶段解冻最后 30 层,用很小的学习率(1e-5)微调,让主干特征适配舌象数据。GlobalAveragePooling2D比Flatten参数少得多,不容易过拟合。参数上1e-3和1e-5差两个数量级,这是故意的——微调阶段学习率必须小,否则预训练权重会被冲掉。如果微调后验证准确率反而下降,说明解冻层数太多或学习率太大,减少解冻层数或再降学习率。
验证迁移学习是否真的有效,不能只看最终准确率,要看验证 loss 曲线是否比原模型更平滑、收敛更快。我一般会保留原模型和迁移学习模型两套权重,在测试集上各跑一遍混淆矩阵,对比每个类别的召回率——舌苔识别里「黄苔」和「灰黑苔」容易混,迁移学习对这两个类的区分度提升最明显。
从那以后我每次拿到这类毕业设计源码,都强制先跑一遍环境自检脚本,再确认数据集划分没有泄漏,最后才动模型结构。顺序反了,后面全是无用功。希望帮到你。
本文还有配套的精品资源,点击获取