news 2026/9/16 16:01:26

基于深度学习与图像分类的舌苔识别完整工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于深度学习与图像分类的舌苔识别完整工程实践

简介:一份整合了Python源码、PyQt5图形界面、训练模型与毕业论文的深度学习舌苔识别检测系统,适合计算机视觉或医学图像处理方向的毕业设计及项目实践者。压缩包共110个文件,主要包含Python脚本、pyc编译文件、PyQt5界面ui与ttc字体、模型pth权重、训练日志及数据集图片,另有json配置与docx论文文档,整体约104.93MB,目录结构清晰,便于按模块查阅。目前已有536人学习下载。资源完整覆盖舌苔识别检测全流程,从舌象数据集构建与图像增强扩充,到DCGAN生成舌象图片和卷积神经网络设计训练,再到体质辨识需求与功能实现均有涉及;配套毕业论文详细阐述了研究背景、机器学习理论、需求分析、数据集构建及网络设计等内容,同时提供TensorBoard训练日志可供复盘调参,适合希望快速复现系统、理解模型训练与界面联调细节的学习者。

1. 为什么舌苔识别值得用深度学习做

中医舌诊讲究望闻问切,但舌苔颜色、厚度、腻度全靠医生肉眼归类,主观性很强。同一张舌象照片,不同医生可能给出不同结论,这种不一致恰好是深度学习能解决的问题。你手里这套系统,做的就是舌苔分类和体质辨识:输入一张舌面照片,由卷积神经网络推断苔色、苔质以及对应的体质倾向。更难得的是,它不是一个孤立的算法demo,而是把数据集标注、图像增强、DCGAN数据扩充、ResNet训练和PyQt5桌面界面串成了完整闭环,里面还带了训练事件文件和可以直接跑的Python源码。对想入门图像分类的从业者或者做毕业设计的学生来说,最有价值的点在于你能完整看到数据怎么整理、模型怎么训练、模型怎么被GUI程序调用,这三个环节正好是工程落地的全部。

2. 数据集先于模型:舌象标注、图像增强与DCGAN扩充

2.1 舌象标注:先定义分类轴

舌苔识别首要问题不是网络结构,而是标签体系。直接使用连续症状描述没法训练分类器,绝大多数项目会把舌象映射到固定类别。常见做法是沿着苔色和苔质两个维度打标:苔色分为白苔、黄苔、灰苔、黑苔,苔质分为薄、厚、腻、剥。落到体质辨识场景里,还要把这些特征组合映射成体质类型,这样输出结果对普通用户更直观。

我在类似舌象项目里常用下面这套标签表,实际项目中建议把label存成csv,让训练脚本和界面共用一份映射文件,避免两边各写一个顺序。

类别标签舌象特征对应体质倾向
0舌淡红、苔薄白平和质
1舌红、苔黄腻湿热质
2舌淡胖、苔白腻痰湿质
3舌红少津、苔少裂纹阴虚质

标注时有几个容易踩的坑:一是同一张图尽量由两个人独立标注,标签冲突的样本协商或者剔除;二是图片命名不要用中文和空格,目录结构提前分成train、val、test三份;三是原始舌象通常是手机拍摄,环境光照参差不齐,后续增强策略要覆盖亮度变化。

2.2 基础图像增强:为什么用这几组参数

数据量小是这类课题共同的痛点,几百张原始舌象不可能直接喂给深度网络。最直接的扩充手段是几何变换和颜色变换。我一般只对训练集做增强,验证集和测试集保持原图,否则在线增强会让验证集失去参考意义。

from PIL import Image import random def basic_augment(img): random.seed(123) # 左右翻转,模拟舌头左右摆放偏移 if random.random() > 0.5: img = img.transpose(Image.FLIP_LEFT_RIGHT) # 旋转限制在±15度,避免舌面空间关系被破坏 angle = random.uniform(-15, 15) img = img.rotate(angle, fillcolor=(255, 255, 255)) # 亮度调整,覆盖不同拍摄光照场景 factor = random.uniform(0.8, 1.2) img = img.point(lambda p: min(255, max(0, int(p * factor)))) return img

逻辑说明:旋转角度范围取±15度,因为真实采集时舌头倾斜幅度不会太大,角度过大产生的黑色填充区域会干扰模型;fillcolor设置成白色是为了保持舌面背景的自然感。亮度因子放在0.8到1.2之间,相当于在同一张图上叠加了4种亮度环境。point操作是对每个像素做乘法,再用min、max做颜色截断,防止数值溢出。如果采集环境整体偏暗,可以把factor下限降到0.7、上限升到1.3,但不能继续扩大,否则舌色从淡红变成暗红,标签语义就被破坏了。

除了上面的基本操作,还可以把HSV空间里的H通道做小幅偏移,比如偏移±5度,以及随机裁剪后再缩放到统一尺寸。随机裁剪的采样中心最好居中偏下,因为舌面主体通常出现在取景框下半部分,裁剪到牙齿反而会引入无关纹理。

2.3 DCGAN扩充舌象样本:结构、损失与不稳定判断

当基础增强手段用尽仍然不够时,就要考虑生成对抗网络。DCGAN相比StyleGAN训练成本低,生成图像分辨率不高,但用于扩充分类模型的训练样本基本够用。生成器和判别器通常按下面的方式构造。

import torch.nn as nn class Generator(nn.Module): def __init__(self, z_dim=100, out_channels=3): super().__init__() self.net = nn.Sequential( nn.ConvTranspose2d(z_dim, 256, 4, 1, 0), nn.BatchNorm2d(256), nn.ReLU(True), nn.ConvTranspose2d(256, 128, 4, 2, 1), nn.BatchNorm2d(128), nn.ReLU(True), nn.ConvTranspose2d(128, 64, 4, 2, 1), nn.BatchNorm2d(64), nn.ReLU(True), nn.ConvTranspose2d(64, out_channels, 4, 2, 1), nn.Tanh() ) def forward(self, z): return self.net(z) class Discriminator(nn.Module): def __init__(self, in_channels=3): super().__init__() self.net = nn.Sequential( nn.Conv2d(in_channels, 64, 4, 2, 1), nn.LeakyReLU(0.2), nn.Conv2d(64, 128, 4, 2, 1), nn.BatchNorm2d(128), nn.LeakyReLU(0.2), nn.Conv2d(128, 256, 4, 2, 1), nn.BatchNorm2d(256), nn.LeakyReLU(0.2), nn.Conv2d(256, 1, 4, 1, 0) ) def forward(self, x): return self.net(x)

逻辑说明:生成器从100维高斯噪声z上采样出64x64三通道图像,转置卷积加BatchNorm加ReLU是DCGAN的标准配置;输出层用Tanh把像素压到-1到1,因此训练前真实舌象图片要同步归一化到[-1,1]区间。判别器用LeakyReLU,斜率取0.2,可以避免生成图像梯度消失。实际训练中判别器很容易压过生成器,典型信号是D loss快速跌到接近0而G loss飙升。这时生成样本对判别器来说太假了,常见对策是把生成器学习率调到0.0004、判别器保持0.0002,或者每训练两步判别器再训练一步生成器。

DCGAN生成的图片需要人工抽检,剔除带网格伪影或舌头形状畸变的样本,再与真实数据混合。生成样本比例不要超过真实数据的30%,否则分类模型会对生成分布产生偏好,遇到真实拍摄图反而掉点。项目论文第四章专门写了GAN相关概念,这里的结构可以直接承接那一章内容。

3. 舌苔分类网络设计:从输入尺寸到训练策略

3.1 主干选ResNet而不是ViT的理由

舌苔识别属于细粒度图像分类,类别差异集中在颜色和纹理变化,对局部特征敏感。ResNet通过残差连接让梯度可以跨层传播,在几千张级别的数据上就能训出稳定效果。现在不少文章把transformer模型详解讲得很彻底,于是总想上ViT,但ViT需要超大训练集和大规模预训练才能发挥优势,几百到几千张舌象直接训练容易过拟合。即便使用加载了ImageNet预训练权重的ViT,也要配更强的数据增强和更长的训练策略。这个项目里使用ResNet系列,是医疗小样本图像里的稳妥选择。

3.2 数据加载器:归一化参数不能乱改

先看数据预处理pipeline,重点不是模型花哨,而是预处理参数要和预训练权重匹配。

from torch.utils.data import DataLoader from torchvision import transforms transform_train = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.15, contrast=0.15), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) transform_val = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True)

逻辑说明:Resize到224是ResNet的标准输入尺寸,模型最后的全连接层输出对应类别数。RandomHorizontalFlip解决舌头左右偏移,ColorJitter的幅度比第2章手工版本温和,因为ToTensor以后像素范围变成0到1,手工增强里0.8到1.2的亮度因子到这里需要按比例折算。Normalize用ImageNet统计值不是随手写的,加载预训练权重时必须配套这组mean和std,否则第一个batch的特征分布就和预训练参数不匹配,表现为loss下降缓慢。

DataLoader的batch_size设为32,显存不足时降到16,不要优先调num_workers为0,否则数据加载会拖慢训练速度。pin_memory=True让GPU拷贝更快,CPU训练时设成False,否则白白占用内存。

3.3 训练循环与超参设置

下面的训练循环保留早停和最优模型保存,适合复现论文里的实验曲线。

from torchvision import models import torch.nn as nn import torch model = models.resnet18(pretrained=True) model.fc = nn.Linear(512, 4) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.1) best_acc = 0.0 for epoch in range(60): model.train() for images, labels in train_loader: images, labels = images.cuda(), labels.cuda() outputs = model(images) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.cuda(), labels.cuda() _, predicted = torch.max(model(images), 1) total += labels.size(0) correct += (predicted == labels).sum().item() acc = correct / total print(f"epoch {epoch} acc {acc:.4f}") if acc > best_acc: best_acc = acc torch.save(model.state_dict(), "tongue_resnet.pth") scheduler.step()

逻辑说明:替换最后一层全连接,把ImageNet的1000类输出改成4类。CrossEntropyLoss内部自带Softmax,模型尾部不需要再接一层。Adam初始学习率0.001对迁移学习来说比较稳,如果loss剧烈震荡就降到0.0003。StepLR每20轮乘0.1,让后期步长变小,拟合更精细。模型保存时机是验证集准确率最高的一刻,文件名用相对路径,但要注意和PyQt5界面加载时的工作目录保持一致。

这套配置可以直接沉淀成超参表:

参数名调整建议
输入尺寸224x224数据量大可升到256,量小维持224
batch_size32显存不足时降到16
学习率0.001微调用0.001,从头训练用0.01
weight_decay1e-4过拟合明显时加大到5e-4
训练轮数60搭配早停策略

3.4 迁移学习和冷启动的细微差别

这里有个容易踩的坑:ResNet加载pretrained=True之后,如果直接冻结backbone只训练fc层,对舌象这种与ImageNet差异较大的任务效果反而一般。我一般选择不冻结,整个网络参与训练,只是把初始学习率调低,让预训练特征逐渐适应舌象分布。观察训练过程,如果训练集准确率快速升到95%以上而验证集落后超过10个点,说明已经过拟合,优先把第2章的在线增强参数加强,再调大weight_decay,最后才考虑换成更深网络。

4. PyQt5 界面、模型加载与推理链路

4.1 界面组件如何划分

PyQt5的作用是给训练好的模型套一个桌面壳。一个能交付的界面至少要有图片选择按钮、图片显示区、识别结果区、体质建议区。项目提供的是.py源码,说明界面是QtDesigner拖出来的或者完全手写的。手写UI的好处是后续调整widget布局方便,缺点是代码稍长。我习惯把模型加载和推理封装到独立类里,让界面代码和深度学习代码不互相纠缠。

控件类型变量名作用
QPushButtonbtn_open触发文件选择对话框
QLabelimage_label显示缩放后的舌象图片
QLabelresult_label显示识别结果和置信度
QPlainTextEditadvice_text显示体质倾向建议

4.2 加载本地模型与预处理一致性

推理链路的关键是让模型处于和训练时完全一致的状态。加载模型后必须调用eval(),关闭梯度计算,否则BatchNorm层在训练和推理两种状态下输出会漂移。这里是从界面上传图片到输出完整结果的最小实现。

from torchvision import transforms, models import torch.nn.functional as F from PIL import Image import torch class TongueClassifier: def __init__(self, model_path, class_names): self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.model = models.resnet18(num_classes=len(class_names)) self.model.load_state_dict( torch.load(model_path, map_location=self.device)) self.model.to(self.device) self.model.eval() self.class_names = class_names self.transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) def predict(self, image_path): img = Image.open(image_path).convert("RGB") x = self.transform(img).unsqueeze(0).to(self.device) with torch.no_grad(): logits = self.model(x) prob = F.softmax(logits, dim=1)[0] idx = int(torch.argmax(prob).item()) probs = {c: round(p, 4) for c, p in zip(self.class_names, prob.tolist())} return self.class_names[idx], probs

逻辑说明:torch.load需要加上map_location,因为训练事件文件来自LAPTOP主机,训练产物可能在不同机器之间迁移,不指定位置容易遇到CUDA不可用错误。load_state_dict之前必须先构造结构一致的模型,这里用resnet18并传num_classes。predict方法返回类别名和所有类别的概率字典,方便界面展示百分比。F.softmax的dim=1表示在类别维度归一化,不要用torch.softmax替代,不同版本对维度语义的处理容易造成混淆。

类别名列表的顺序必须和第2章的标签顺序完全一致,顺序错一位,结果全错。

4.3 槽函数里调用推理并刷新显示

界面部分用按钮触发文件选择,选中图片后既显示在界面上,也把路径传给分类器。

from PyQt5.QtWidgets import (QWidget, QPushButton, QLabel, QFileDialog, QVBoxLayout) from PyQt5.QtGui import QPixmap class MainWindow(QWidget): def __init__(self, classifier): super().__init__() self.classifier = classifier self.btn = QPushButton("选择舌象图片") self.image_label = QLabel() self.result_label = QLabel("点击按钮选择图片") layout = QVBoxLayout() layout.addWidget(self.btn) layout.addWidget(self.image_label) layout.addWidget(self.result_label) self.setLayout(layout) self.btn.clicked.connect(self.on_click) def on_click(self): path, _ = QFileDialog.getOpenFileName( self, "选择舌象", "", "图片文件 (*.jpg *.png)") if not path: return pixmap = QPixmap(path).scaledToWidth(320) self.image_label.setPixmap(pixmap) name, probs = self.classifier.predict(path) self.result_label.setText(f"识别:{name}\n置信度:{probs}")

逻辑说明:QFileDialog把文件类型过滤为jpg和png,不放开webp是因为部分采集设备和老版本库支持不稳定。scaledToWidth只固定宽度320像素,高度按比例缩放,避免图片变形。predict方法在主线程执行,ResNet18推理一次几十毫秒,界面不会明显卡顿;如果后续换成更重网络或者需要同时处理多张图,再把推理放到QThread的run方法里,用信号把结果传回主线程。

界面和模型解耦之后,TongueClassifier替换成MobileNet或ViT实现,主窗口代码完全不用改,这个接口设计值得保留。

5. 训练日志排查:从tfevents到实测调参

5.1 直接解析events.out.tfevents文件

项目源码目录里有一组events.out.tfevents.*文件,这些是TensorFlow训练时TensorBoard的事件文件。从时间戳跨度看覆盖了多轮实验,比如1652188470和1649325615这两个编号对应不同训练阶段,直接跑TensorBoard就能看到曲线。

tensorboard --logdir . --port 6006

然后在浏览器打开localhost:6006。如果只想快速读取数值而不开浏览器,可以这样解析:

from tensorflow.python.summary.summary_iterator import summary_iterator for event in summary_iterator("events.out.tfevents.1652188470.LAPTOP-ACFSLO5L.12688.0"): if event.HasField("summary"): for value in event.summary.value: if value.tag.startswith("loss") or value.tag.startswith("acc"): print(event.step, value.tag, value.simple_value)

逻辑说明:summary_iterator按照事件文件顺序逐条返回记录,判读HasField可以跳过没有summary的空事件。tag过滤保留loss和acc相关量,step就是训练迭代步数。把这个脚本批量跑在不同事件文件上,就能对比出哪一轮实验收敛更快。

5.2 从loss曲线判断训练状态

看loss曲线时按三个特征判断:训练loss和验证loss同步下降,说明学习率合适;训练loss持续下降但验证loss中途回升,说明过拟合,需要回看增强配置;loss曲线呈锯齿状震荡,多数情况下学习率偏大,或者batch_size太小。多个tfevents文件恰好对应不同配置的实验,比较文件大小和时间跨度可以推断出哪次训练迭代更充分。

5.3 用测试集验证留存模型

训练曲线只能说明模型在验证集上的表现,最后一步必须放到完全没参与训练的测试集上算混淆矩阵。

from sklearn.metrics import classification_report, confusion_matrix import numpy as np y_true = [] y_pred = [] for images, labels in test_loader: images = images.cuda() with torch.no_grad(): preds = torch.argmax(model(images), dim=1).cpu().numpy() y_true.extend(labels.numpy()) y_pred.extend(preds) print(classification_report(y_true, y_pred, target_names=["0", "1", "2", "3"])) print(confusion_matrix(y_true, y_pred))

逻辑说明:手动遍历测试集,把每个batch的预测值拼起来,再交给sklearn计算精确率和召回率,比在训练循环里写metric要直观。混淆矩阵对角线越集中说明分类越稳定。当某一列的数值集中而其他列为0,说明模型对这个类别过拟合,回头优先做类别平衡和数据增强。部署前用这个混淆矩阵替换掉论文里的准确率曲线,能明显提升实测结果的可信度。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 16:00:22

基于NLP的微博情感分析系统:从数据清洗到Flask部署

简介:基于NLP的微博用户情感分析系统是一套完整的毕业设计Python工程,面向计算机、通信、人工智能、自动化等专业学生及从业者,可用于课程设计、大作业或毕业设计参考,也适合用来掌握中文微博文本情感倾向自动判别的完整流程。压缩…

作者头像 李华
网站建设 2026/9/16 16:00:11

MATLAB实现粒子群优化求解电力系统最优潮流(OPF)

简介:本资源是一份面向MATLAB初学者与电力系统优化方向学习者的粒子群优化算法(PSO)基础实现包,聚焦于最优潮流(OPF)等典型工程优化问题的求解。压缩包共3个文件,含2个核心MATLAB源码文件&#…

作者头像 李华
网站建设 2026/9/16 15:59:09

三维RRT算法在无人机路径规划中的MATLAB实现

1. 项目概述:三维RRT算法在无人机路径规划中的应用在无人机自主导航领域,路径规划算法直接决定了飞行器能否安全高效地完成任务。这个MATLAB实现的三维RRT(快速随机树)算法项目,为开发者提供了一个可自定义起终点、障碍…

作者头像 李华
网站建设 2026/9/16 15:57:31

2026大理电气检测机构排名 TOP5 CMA 资质机构提供防爆设备检测+防爆安全检测 联系方式推荐

大理电气防爆检测机构鳞次栉比、鱼龙混杂,化工园区、油库加油站、矿山厂区、制药企业、危化品仓储场所开展防爆电气安全排查、生产验收时,大量无资质机构出具报告无法通过应急管理部门核查。小编实地走访筛选本地正规第三方电气防爆检测实验室&#xff0…

作者头像 李华
网站建设 2026/9/16 15:57:22

ERPNext免费开源ERP:3步搭起财务、销售、库存完整管理系统

ERPNext免费开源ERP:3步搭起财务、销售、库存完整管理系统 【免费下载链接】erpnext Free and Open Source Enterprise Resource Planning (ERP) 项目地址: https://gitcode.com/GitHub_Trending/er/erpnext 还在用Excel记账、靠脑子记库存,系统预…

作者头像 李华