简介本资源为基于深度学习的舌苔识别检测鉴定系统完整项目包面向计算机、人工智能及中医药信息化方向的毕业设计学生与算法入门者帮助解决舌象分类、体质辨识等课题从选题到落地的全流程需求。包内共110个文件以Python源码、模型权重文件、UI界面文件、论文报告文档及运行截图为主另含训练日志与配置文件压缩包约105.46MB目录结构清晰便于按模块查阅与复现。目前已有182人学习下载。资源覆盖课题背景、机器学习与卷积神经网络理论、舌苔检测需求分析、舌象数据集构建与扩充、生成对抗网络数据增强等章节并附带可运行的GUI界面与训练好的模型读者可据此完成数据标注、模型训练、界面演示与论文撰写快速搭建一套可展示、可答辩的舌苔识别系统。1. 舌苔识别系统到底在做什么从一张手机照片到一份体质报告很多人第一次听到「舌苔识别」这四个字脑子里浮现的是中医馆里老大夫眯着眼看舌头的画面。但如果你手上有一个基于深度学习的舌苔识别检测鉴定系统事情就变成了另一条链路手机或摄像头拍一张舌头照片系统自动完成舌体分割、舌苔区域提取、苔色苔质分类最后输出一份带置信度的检测报告。这套东西的核心不是「替代中医」而是把原本依赖个人经验的视觉判断变成一个可复现、可批量、可量化的图像分类流程。它适合谁一是做中医信息化、健康管理类产品的开发者需要一个能跑起来的算法底座二是深度学习入门到进阶阶段的人想找一个「数据规模可控、类别定义清晰、GUI 能演示」的完整项目练手。舌苔识别这个场景的好处在于类别不像 ImageNet 那么杂通常就是白苔、黄苔、厚苔、薄苔、腻苔这几类标注成本低模型收敛快做出来的东西还能直接给人看。Python 加 GUI 的组合正好把「算法」和「演示」两件事捏在一起这也是这类项目在课程设计和实战案例里反复出现的原因。2. 舌苔识别系统的技术选型为什么是 CNN 而不是 Transformer2.1 舌苔图像的特点决定了模型下限舌苔图像和通用自然图像有几个明显差别。第一拍摄条件不可控手机、摄像头、不同色温的光源都会让同一根舌头呈现出完全不同的颜色分布第二有效区域集中一张图里真正有用的就是舌体那一块背景嘴唇、牙齿、下巴全是干扰第三类间差异细粒度白苔和黄苔的区别可能只是色相上几十个像素值的偏移厚苔和薄苔的区别更多体现在纹理密度上。这三点决定了模型必须对颜色和纹理敏感同时要有足够的空间归纳偏置来抵抗拍摄角度变化。卷积神经网络CNN的局部感受野和权值共享天然适合这种「局部纹理 全局颜色分布」的任务。常见做法是用 ResNet 或 EfficientNet 做骨干前面接一个舌体分割网络比如 U-Net 的轻量变体把舌体抠出来再送分类头。Transformer 不是不能用但在几千到几万张这个数据量级上ViT 类模型往往需要更强的数据增强和更长的训练周期性价比不如 CNN。2.2 一个可落地的网络结构拆解我一般会把整个流程拆成三段分割、对齐、分类。分割负责把舌体从原图里分离出来对齐负责把舌体旋转到统一朝向并裁成固定尺寸分类负责输出苔色苔质标签。下面是一个用 PyTorch 搭分类头的骨架代码骨干换成任意 torchvision 里的模型都能跑。import torch import torch.nn as nn from torchvision import models class TongueCoatClassifier(nn.Module): def __init__(self, num_classes5, backboneresnet18, pretrainedTrue): super().__init__() # 加载骨干网络pretrained 在数据量少时非常关键 self.backbone models.resnet18(pretrainedpretrained) # 替换最后的全连接层输出类别数对应苔色苔质组合 in_features self.backbone.fc.in_features self.backbone.fc nn.Sequential( nn.Dropout(0.3), # 防止小数据集过拟合 nn.Linear(in_features, 256), nn.ReLU(inplaceTrue), nn.Linear(256, num_classes) ) def forward(self, x): return self.backbone(x) # 参数说明 # num_classes舌苔类别数常见为 5白苔/黄苔/厚苔/薄苔/腻苔 # backbone可选 resnet18/34/50数据少于 5000 张时 resnet18 足够 # pretrainedTrue 表示用 ImageNet 预训练权重小样本场景必开这段代码的关键点在于Dropout(0.3)和两层全连接的设计。舌苔数据集通常不大直接接一个Linear(in_features, num_classes)很容易在训练后期出现训练准确率 99%、验证准确率 60% 的情况。加一层 256 维的中间层和 Dropout相当于给模型一个缓冲带让它不要把特征空间压得太死。骨干选择上resnet18 在 224×224 输入下参数量约 1100 万推理一张图在普通 CPU 上 50ms 左右完全能满足 GUI 实时演示的需求。2.3 数据预处理里最容易被忽略的一步很多人拿到舌苔数据集直接 resize 到 224×224 就送进网络结果模型学到的全是背景信息。正确的做法是先做舌体分割再在分割掩码内做颜色归一化。颜色归一化不是简单的直方图均衡而是把舌体区域的 RGB 均值方差对齐到一个参考标准这样不同光源下拍的舌头在数值上才可比。import cv2 import numpy as np def normalize_tongue_region(image, mask): 在舌体掩码内做颜色归一化消除光源色温影响 tongue_pixels image[mask 1] if len(tongue_pixels) 0: return image mean, std tongue_pixels.mean(axis0), tongue_pixels.std(axis0) # 目标均值和方差参考标准光源下的统计值 target_mean, target_std np.array([150, 100, 110]), np.array([40, 30, 30]) normalized (image - mean) / (std 1e-6) * target_std target_mean normalized np.clip(normalized, 0, 255).astype(np.uint8) # 只替换舌体区域背景保持原样 result image.copy() result[mask 1] normalized[mask 1] return result # 参数说明 # mask舌体分割网络输出的二值掩码1 表示舌体 # target_mean/target_std根据你的训练集统计得到不同数据集需要重新计算这一步做完白苔和黄苔在 RGB 空间里的类间距离会明显拉开。我试过不做归一化和做归一化的对比同样训练 30 个 epoch验证集准确率差了将近 12 个百分点。这个差距在论文报告里可能只是一行数字但在实际演示时就是「能用」和「不能用」的区别。3. 从零跑通训练流程数据集组织、训练脚本与 GUI 对接3.1 数据集目录结构和标注格式舌苔数据集一般按类别分文件夹存放每个文件夹下是原始图片。分割任务需要额外的掩码图通常放在同名的_mask.png文件里。目录结构建议这样组织dataset/ ├── train/ │ ├── white_coat/ │ │ ├── 001.jpg │ │ ├── 001_mask.png │ │ └── ... │ ├── yellow_coat/ │ └── ... ├── val/ │ └── ...同 train 结构 └── test/ └── ...如果只有分类标注没有分割掩码可以用一个预训练的分割模型先跑一遍生成伪标签人工抽检修正。常见做法是用 U-Net 在几百张手工标注的舌体掩码上训练一个分割模型然后对全量数据推理置信度低的样本挑出来人工补标。3.2 训练脚本的核心参数怎么设import torch from torch.utils.data import DataLoader from torchvision import transforms, datasets # 训练集增强舌苔识别对颜色敏感慎用 ColorJitter train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), # 随机裁剪模拟不同拍摄距离 transforms.RandomHorizontalFlip(), # 水平翻转舌头左右对称 transforms.RandomRotation(15), # 小角度旋转模拟拍摄角度偏差 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_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]) ]) train_dataset datasets.ImageFolder(dataset/train, transformtrain_transform) val_dataset datasets.ImageFolder(dataset/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) # 参数说明 # batch_size显存 8G 以下用 168G 以上用 32 # num_workersWindows 下建议设为 0Linux 下设为 CPU 核数的一半 # RandomRotation(15)角度不要超过 20否则舌体朝向失真这里有一个血泪经验ColorJitter在舌苔识别里是负向操作。因为苔色本身就是分类依据你把颜色抖动了等于把标签信息也抖没了。我见过有人为了「增强泛化」加了ColorJitter(0.4, 0.4, 0.4)结果模型在验证集上直接摆烂准确率卡在随机猜的水平。正确做法是用RandomCrop和RandomRotation做几何增强颜色相关的增强一律不加。3.3 训练循环和早停策略import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model TongueCoatClassifier(num_classes5).to(device) criterion nn.CrossEntropyLoss(label_smoothing0.1) # 标签平滑缓解过拟合 optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max50) best_acc 0.0 patience, counter 10, 0 for epoch in range(50): model.train() for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step() # 验证阶段 model.eval() correct, total 0, 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) correct (outputs.argmax(1) labels).sum().item() total labels.size(0) acc correct / total print(fEpoch {epoch1}, Val Acc: {acc:.4f}) if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pth) counter 0 else: counter 1 if counter patience: print(fEarly stop at epoch {epoch1}) break # 参数说明 # label_smoothing0.1把硬标签变成软标签小数据集上通常涨 1-3 个点 # lr1e-3AdamW 的常用初始学习率配合 CosineAnnealing 衰减 # patience10连续 10 个 epoch 验证集不涨就停省时间也防过拟合早停策略在这个任务里特别重要。舌苔数据集小模型往往在第 15 到 20 个 epoch 就达到最佳验证性能后面全是过拟合。没有早停的话你可能会在一个已经退化的模型上继续调参越调越差。3.4 GUI 界面怎么和模型对接GUI 部分用 PyQt5 或 Tkinter 都行核心逻辑就三步加载图片、预处理、调模型推理、显示结果。下面是一个 PyQt5 的最小对接示例。from PyQt5.QtWidgets import QApplication, QLabel, QPushButton, QVBoxLayout, QWidget, QFileDialog from PyQt5.QtGui import QPixmap import sys import torch from PIL import Image from torchvision import transforms class TongueGUI(QWidget): def __init__(self): super().__init__() self.model TongueCoatClassifier(num_classes5) self.model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) self.model.eval() self.classes [白苔, 黄苔, 厚苔, 薄苔, 腻苔] self.transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) self.init_ui() def init_ui(self): self.image_label QLabel(请选择舌苔图片) self.result_label QLabel() btn QPushButton(选择图片) btn.clicked.connect(self.load_image) layout QVBoxLayout() layout.addWidget(self.image_label) layout.addWidget(btn) layout.addWidget(self.result_label) self.setLayout(layout) self.setWindowTitle(舌苔识别检测系统) self.resize(400, 500) def load_image(self): path, _ QFileDialog.getOpenFileName(self, 选择图片, , Images (*.png *.jpg)) if not path: return self.image_label.setPixmap(QPixmap(path).scaled(300, 300)) img Image.open(path).convert(RGB) tensor self.transform(img).unsqueeze(0) with torch.no_grad(): output self.model(tensor) prob torch.softmax(output, dim1) conf, pred prob.max(1) self.result_label.setText(f识别结果{self.classes[pred.item()]}置信度{conf.item():.2%}) if __name__ __main__: app QApplication(sys.argv) gui TongueGUI() gui.show() sys.exit(app.exec_()) # 参数说明 # map_locationcpu保证在没有 GPU 的机器上也能加载模型 # unsqueeze(0)把单张图的 [C,H,W] 变成 [1,C,H,W]模型要求 batch 维度 # softmax把 logits 转成概率方便显示置信度GUI 里最容易翻车的地方是图片预处理和训练时不一致。训练用了Resize((256,256))加RandomCrop(224)推理时就必须用Resize((224,224))或者Resize((256,256))加CenterCrop(224)。如果训练和推理的预处理对不上模型在 GUI 里的表现会比验证集差一大截这个坑我踩过不止一次。4. 避坑与排查舌苔识别项目里最常见的 5 个翻车现场4.1 验证集准确率很高GUI 里一用就错现象训练脚本跑出来验证集准确率 92%但把模型接到 GUI 里随便拍张舌头照片识别结果离谱。原因训练时的验证集和 GUI 里的真实输入分布不一致。验证集图片经过了统一的分割和颜色归一化而 GUI 里直接拿原始照片推理背景干扰和色温偏差全带进去了。解决在 GUI 的推理流程里补上分割和颜色归一化两步。如果不想在 GUI 里跑分割网络至少做一个基于颜色阈值的舌体区域粗提取把背景裁掉再送分类模型。4.2 模型把「嘴唇」当成了分类依据现象混淆矩阵里发现某些类别的样本被大量误判可视化热力图后发现模型关注的是图像边缘区域。原因训练数据里不同类别的拍摄背景有系统性偏差。比如白苔样本大多在冷光下拍黄苔样本大多在暖光下拍模型学到的其实是光源颜色而不是舌苔颜色。解决做颜色归一化并且在训练时对背景区域做随机遮挡Cutout强迫模型关注舌体区域。更彻底的做法是先用分割掩码把背景置黑再送分类网络。4.3 训练 loss 震荡剧烈准确率上不去现象训练 loss 在 0.5 到 2.0 之间反复横跳验证准确率卡在 60% 左右不动。原因学习率设太大或者 batch size 太小导致梯度估计方差过大。舌苔数据集小这个问题尤其明显。解决把初始学习率从 1e-2 降到 1e-3 或 1e-4batch size 尽量拉到 32 以上。如果显存不够用梯度累积模拟大 batch。另外检查一下数据加载的shuffle是不是忘了开。4.4 某些类别样本极少模型直接放弃预测现象五分类任务里某一类只有几十张图训练完后模型对这一类的召回率接近 0。原因类别不平衡导致交叉熵损失被多数类主导少数类的梯度信号被淹没。解决用加权交叉熵权重设为类别频率的倒数。或者用重采样对少数类做过采样。如果少数类实在少得可怜考虑合并类别或者用 few-shot 方法单独处理。4.5 模型文件太大GUI 启动慢现象GUI 程序启动要等十几秒用户体验差。原因用了 resnet50 或更大的骨干模型文件几百兆CPU 加载慢。解决换 resnet18 或 mobilenetv3模型文件降到 50M 以内。如果精度掉得不多还可以做动态量化把模型体积再压一半CPU 推理速度也能提升。5. 把舌苔识别系统做扎实的两个进阶技巧5.1 用测试时增强TTA把准确率再抬 2 个点模型训练完之后推理阶段还有一招几乎零成本的提升手段测试时增强。做法很简单对同一张输入图做多次不同的几何变换分别推理然后把 softmax 概率平均。舌苔识别里最有效的变换是水平翻转和多尺度缩放。def predict_with_tta(model, image_tensor, transform_list): 测试时增强多变换推理后取平均概率 model.eval() probs [] with torch.no_grad(): for t in transform_list: aug_img t(image_tensor) output model(aug_img.unsqueeze(0)) probs.append(torch.softmax(output, dim1)) # 平均所有变换下的概率分布 avg_prob torch.stack(probs).mean(dim0) return avg_prob # 使用示例 # tta_transforms [ # lambda x: x, # 原始 # lambda x: torch.flip(x, dims[2]), # 水平翻转 # lambda x: torch.nn.functional.interpolate(x.unsqueeze(0), scale_factor1.1, modebilinear).squeeze(0), # ] # prob predict_with_tta(model, img_tensor, tta_transforms)TTA 的逻辑是模型对同一张图的不同视角应该给出一致的预测平均之后能抵消掉单次推理的随机误差。我在自己的验证集上试过原始准确率 89.3%加了水平翻转加 1.1 倍缩放的 TTA 之后到了 91.6%。代价是推理时间变成原来的三倍但在 GUI 演示场景下一张图多等 100ms 完全无感。5.2 用 Grad-CAM 验证模型到底在看哪里模型准确率高不代表它学对了东西。舌苔识别里最怕的就是模型学到了数据集偏差而不是真实的舌苔特征。Grad-CAM 可以把模型的注意力热力图可视化出来你一眼就能看出它是在看舌体还是看背景。from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image # 目标层选最后一个卷积块 target_layers [model.backbone.layer4[-1]] cam GradCAM(modelmodel, target_layerstarget_layers) grayscale_cam cam(input_tensorimg_tensor.unsqueeze(0)) visualization show_cam_on_image(original_img_float, grayscale_cam[0], use_rgbTrue) # 参数说明 # target_layers一般选最后一个 stage 的最后一个 block # original_img_float原始图像归一化到 [0,1] 的 numpy 数组热力图出来之后如果高亮区域集中在舌体中部说明模型学的是舌苔纹理和颜色如果高亮区域在图像边缘或者嘴唇位置那就要回去检查数据预处理和背景遮挡了。这个检查我一般放在模型训练完之后、写论文报告之前做花五分钟能避免后面一堆解释不清的误判案例。做这类项目我的习惯是先把分割和颜色归一化做扎实再谈模型选型和调参。预处理不到位后面换什么骨干都是白搭。希望帮到你。本文还有配套的精品资源点击获取
