资讯动态

Python深度学习舌象诊断系统:从数据采集到Grad-CAM可解释性实战

发布时间:2026/10/1 16:46:22 来源:尧图企业网站定制
简介这份资源是Python实现的基于深度学习的舌象诊断系统完整源代码面向计算机相关专业学生与深度学习入门者可直接用于毕业设计、期末大作业或课程设计。系统功能完善、界面美观、操作简单代码注释详尽新手也能看懂并快速部署运行。压缩包共183个文件约42.7MB其中54个py源码文件承载模型训练与界面逻辑61张jpg与1张jpeg为舌象样本及界面素材40个pyc为编译缓存另有14个txt、7个json、2个ui界面文件及docx学习路线文档结构清晰便于按模块查阅。资源内附《基于深度学习的舌象诊断系统学习路线》文档可帮助读者理解数据预处理、模型构建与诊断流程。目前已有165人学习下载适合需要完整项目方案、可运行代码与排错参考的读者能有效降低从零搭建舌象诊断系统的门槛。1. 从一张舌头照片到诊断结论舌象诊断系统到底在做什么很多人第一次听到「Python实现基于深度学习的舌象诊断系统源代码」这个题目脑子里浮现的是中医老先生看舌头的画面觉得这东西离工程很远。其实把它拆开看本质就是一个标准的图像分类任务输入是一张舌头照片输出是若干类别标签比如舌质颜色偏淡还是偏红、舌苔是薄白还是厚腻。深度学习在这里干的事和你用 CNN 识别猫狗、识别恶意软件流量没有本质区别只是数据集换成了舌象类别定义换成了中医证候维度。这个方向适合两类人一类是计算机视觉大作业、课设需要找一个有辨识度又不烂大街的题目舌象诊断比手写数字识别、猫狗分类更能写出东西另一类是想把深度学习真正落到一个垂直领域理解从数据采集、标注、训练到推理部署完整链路的从业者。它不需要 GPU 集群一张消费级显卡甚至 CPU 都能跑通最小闭环但里面的坑一点不少——数据不平衡、颜色失真、标注主观性每一个都能让你训练出来的模型变成玄学。我下面讲的这套方案是我自己按这个标题从头搭过一遍的路径不依赖任何特定仓库你照着能复现出一个可用的版本。2. 舌象数据从哪来采集、标注与预处理的三道关2.1 为什么公开舌象数据集几乎不能直接用你如果去搜「舌象数据集」能找到的多是几百张量级、类别定义模糊、拍摄设备五花八门的小数据集。直接拿来训练模型学到的往往是「这台相机拍的舌头」而不是「这种舌质」。常见做法是自己攒数据用手机在固定光源下拍或者从中医教材、公开图谱里裁剪。我一般会先定一个最小可用规模——每个类别至少 80 到 100 张类别数控制在 4 到 6 个比如「淡白舌 / 红舌 / 绛舌 / 紫舌」加「薄白苔 / 厚腻苔」两套标签体系先跑通一套再扩展。采集时最容易翻车的是光源。同一根舌头暖光下拍出来偏红冷光下偏白模型会直接把这些当成类别特征。血泪经验是要么全程用同一盏环形补光灯要么在预处理阶段做白平衡校正。我一般会在拍摄时放一张标准色卡在画面角落后期用它做颜色归一化。2.2 用 OpenCV 做舌体分割与颜色校正拿到原始照片后第一步是把舌头从嘴唇、牙齿、背景里抠出来。舌体在 HSV 空间里通常落在红色到品红的一段区间可以用颜色阈值加形态学操作做粗分割再用轮廓面积筛掉噪声。import cv2 import numpy as np def segment_tongue(img_path): img cv2.imread(img_path) img cv2.resize(img, (512, 512)) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 舌体在HSV中的大致范围H在0-10和170-180两段 lower1 np.array([0, 40, 50]) upper1 np.array([10, 255, 255]) lower2 np.array([170, 40, 50]) upper2 np.array([180, 255, 255]) mask cv2.inRange(hsv, lower1, upper1) cv2.inRange(hsv, lower2, upper2) # 形态学闭运算填补空洞开运算去噪点 kernel np.ones((7, 7), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations2) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations1) # 只保留最大连通区域去掉嘴唇等干扰 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None, None largest max(contours, keycv2.contourArea) clean_mask np.zeros_like(mask) cv2.drawContours(clean_mask, [largest], -1, 255, -1) result cv2.bitwise_and(img, img, maskclean_mask) return result, clean_mask这段代码的逻辑是先把图缩到 512×512 统一尺寸转 HSV 后用两段红色阈值提取候选区域闭运算把舌苔造成的孔洞补上开运算去掉零散噪点最后只留面积最大的轮廓。参数里lower1/upper1的 S、V 下限是关键S 太低会把灰白背景也框进来V 太低会把暗部阴影算进去。如果你的图偏暗把 V 下限从 50 降到 30 试试。颜色校正我一般用简单版的灰度世界算法假设整幅图的 RGB 均值应该接近灰色算出三个通道的增益系数再乘回去。这一步能显著降低不同设备之间的色差但注意别在分割前做否则背景会干扰均值。2.3 数据增强要克制别把舌象转出医学上不存在的颜色图像分类常用的随机裁剪、翻转、旋转在舌象上大部分可以用但颜色抖动ColorJitter要非常小心。你把饱和度调高 0.5可能把「淡白舌」变成「红舌」标签直接错了。我的做法是几何增强放开颜色增强只做极小幅度的亮度调整±10%色相和饱和度一律不动。from torchvision import transforms train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees10), transforms.ColorJitter(brightness0.1), # 只动亮度不动色相饱和度 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])Normalize用的是 ImageNet 的均值和方差因为后面要拿预训练模型做迁移学习这一步必须对齐。旋转角度我控制在 10 度以内舌象的方向本身有语义转太多反而不合理。3. 模型选型与训练从 ResNet 迁移学习到类别不平衡处理3.1 为什么小数据集上别自己搭 CNN舌象数据通常就几百到几千张你从零搭一个五六层的 CNN大概率欠拟合或者过拟合调参调到怀疑人生。常见做法是用 torchvision 里的预训练模型做迁移学习ResNet18 或 EfficientNet-B0 是性价比最高的两个选择。ResNet18 参数量约 1100 万在 224×224 输入下推理快适合课设演示EfficientNet-B0 精度略高但结构复杂一点看你要不要写进报告里显得高级。我一般会冻结前面的卷积层只训练最后的全连接层等 loss 稳定后再解冻最后两个 block 做微调。这样在小数据上收敛稳不容易一开始就把预训练权重带偏。import torch import torch.nn as nn from torchvision import models def build_model(num_classes4, freeze_backboneTrue): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) if freeze_backbone: for param in model.parameters(): param.requires_grad False # 替换最后的全连接层 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return modelweights参数指定加载 ImageNet 预训练权重freeze_backboneTrue时只有新的fc层参与训练。Dropout(0.3)是我在小数据上常用的正则化强度数据量上千后可以降到 0.2。注意替换fc后新层的参数默认requires_gradTrue所以冻结循环要写在替换之前否则会把新层也冻上。3.2 类别不平衡舌象数据里「淡白舌」总是最多真实采集的舌象数据淡白舌和薄白苔这类常见证候能占到一半以上稀有类别可能只有几十张。直接训练模型会倾向于全预测成多数类准确率看着有 70%但少数类召回率接近零。处理方式有三种我一般组合用第一种是加权损失函数给少数类更高的权重。权重按类别频率的倒数算再归一化。from collections import Counter def compute_class_weights(labels, num_classes): counter Counter(labels) total len(labels) weights [] for i in range(num_classes): count counter.get(i, 1) weights.append(total / (num_classes * count)) return torch.tensor(weights, dtypetorch.float32) # 用法 criterion nn.CrossEntropyLoss(weightclass_weights)weight参数会让损失函数对少数类的错误惩罚更重。注意权重别设得太极端否则模型会对少数类过拟合验证集上反而抖得厉害。我一般把最大权重限制在 5 倍以内。第二种是重采样对少数类做带替换的过采样或者对多数类做欠采样。第三种是数据增强只对少数类做。实际项目里我通常先上加权损失效果不够再叠加过采样。3.3 训练循环里必须盯住的三个量训练脚本本身不复杂但有几个量你不盯就会白跑。第一个是每个 epoch 的训练 loss 和验证 loss如果训练 loss 一直降、验证 loss 先降后升就是过拟合该加正则或早停。第二个是验证集上的混淆矩阵光看准确率会骗你得看少数类有没有被完全忽略。第三个是学习率迁移学习阶段我一般用 1e-3 配 Adam解冻微调时降到 1e-4。def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) preds outputs.argmax(dim1) correct (preds labels).sum().item() total imgs.size(0) return total_loss / total, correct / totalloss.item() * imgs.size(0)是为了按样本数加权平均最后一个 batch 不满时不会拉偏均值。argmax(dim1)取每个样本得分最高的类别。验证阶段记得加model.eval()和torch.no_grad()否则 BatchNorm 和 Dropout 的行为会和训练时不一致指标会飘。4. 推理部署与界面把模型变成一个能演示的系统4.1 用 Gradio 十行代码搭出可交互界面课设和大作业最后都要演示你不可能让老师去命令行敲 Python。Gradio 是最省事的方案不用写前端一个函数就能出网页界面。import gradio as gr from PIL import Image def predict(img: Image.Image): img_tensor val_tf(img).unsqueeze(0).to(device) model.eval() with torch.no_grad(): logits model(img_tensor) probs torch.softmax(logits, dim1)[0] classes [淡白舌, 红舌, 绛舌, 紫舌] return {classes[i]: float(probs[i]) for i in range(len(classes))} demo gr.Interface( fnpredict, inputsgr.Image(typepil), outputsgr.Label(num_top_classes4), title舌象诊断系统 ) demo.launch()val_tf是验证集的预处理管道必须和训练时一致否则输入分布对不上预测会乱。softmax把 logits 转成概率gr.Label直接渲染成条形图。demo.launch()默认起在本地 7860 端口演示够用。4.2 推理速度与模型导出如果你要在报告里写「系统响应时间」得实测。ResNet18 在 CPU 上单张 224×224 推理大概 30 到 50 毫秒GPU 上 5 毫秒以内。想再快可以导出成 ONNX 或者用 TorchScript但课设阶段没必要除非老师明确要求部署优化。# 导出 TorchScript方便脱离源码部署 model.eval() example torch.randn(1, 3, 224, 224).to(device) traced torch.jit.trace(model, example) traced.save(tongue_model.pt)torch.jit.trace会记录一次前向传播的计算图导出的模型不依赖 Python 类定义。注意 trace 对含控制流的模型不友好ResNet 这种纯前馈结构没问题。5. 避坑与排查舌象诊断系统最容易翻车的五个地方5.1 现象训练准确率 95%一换手机拍就全错原因模型过拟合到了训练设备的色彩分布和拍摄角度没有学到真正的舌质特征。解决训练时加入不同设备、不同光照的样本或者强制做颜色归一化验证集必须留一部分来自不同设备的图不能全用同一批数据随机切分。5.2 现象验证集 loss 突然变成 NaN原因学习率太大或者某个 batch 里出现了全黑、全白的异常图梯度爆炸。解决先把学习率降一个数量级再检查数据管道里有没有损坏图片加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)能兜住大部分情况。5.3 现象混淆矩阵里两个类别永远互相错分原因这两个类别的视觉差异本来就小比如「淡白舌」和「薄白苔」在某些图上颜色接近标注本身可能就不一致。解决回头检查标注规范把边界样本单独拿出来讨论如果确实难分考虑合并类别或者改成多标签任务而不是硬做单标签分类。5.4 现象Gradio 界面传图后报尺寸错误原因Gradio 传进来的 PIL 图尺寸不固定而模型要求 224×224。解决在predict函数里先过val_tftransforms.Resize会处理尺寸如果还报错检查是不是漏了unsqueeze(0)模型要的是 4 维张量[batch, channel, H, W]。5.5 现象换了台机器跑结果和之前对不上原因随机种子没固定或者 PyTorch 版本、CUDA 版本不同导致数值精度差异。解决在脚本开头固定torch.manual_seed(42)、np.random.seed(42)并在报告里写清楚环境版本。跨设备复现时允许小数点后几位的差异但类别预测应该一致。6. 让模型可信用 Grad-CAM 看它到底在看舌头哪里课设答辩最容易被问的一句话是「你怎么知道模型学的是舌象而不是背景」这时候你需要一个可解释性工具。Grad-CAM 能生成热力图标出模型做决策时关注了图像的哪些区域。如果热力图集中在舌体上说明模型学到了正确的东西如果集中在嘴角或者背景那你的模型就是个黑匣子得回去查数据。from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image # ResNet18 最后一个卷积层 target_layers [model.layer4[-1]] cam GradCAM(modelmodel, target_layerstarget_layers) input_tensor val_tf(img).unsqueeze(0).to(device) grayscale_cam cam(input_tensorinput_tensor)[0] visualization show_cam_on_image(np.array(img) / 255.0, grayscale_cam, use_rgbTrue)target_layers指定要可视化的卷积层一般选最后一个 stage 的最后一层分辨率够用又保留语义信息。grayscale_cam是 0 到 1 的热力值show_cam_on_image把它叠加到原图上。我一般会挑几张验证集里预测正确的和预测错误的图各跑一遍对比热力图分布——预测错的那些热力图往往落在舌体边缘或者嘴唇上这就是你下一步该补数据的方向。一个我踩过的坑Grad-CAM 对 BatchNorm 的 running stats 敏感如果你在推理时忘了model.eval()热力图会完全乱掉。所以跑可解释性之前先确认模型处于 eval 模式。这套东西做下来从数据采集到可解释性大概两三天能跑通最小闭环剩下的时间都花在数据质量和调参上。我的习惯是每改一次数据管道就重新跑一遍 Grad-CAM确认模型关注区域没跑偏再去看指标。指标好看但热力图乱飘的模型答辩时经不起追问。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑