尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python神经网络CNN舌诊系统:从图像分类到食疗建议的完整实现

Python神经网络CNN舌诊系统:从图像分类到食疗建议的完整实现 简介这是一套面向计算机相关专业学生与开发者的舌诊系统课程设计资源基于Python与CNN卷积神经网络实现可对舌头图像进行症状诊断并给出相应食疗方案适合用作毕业设计、课程设计或深度学习入门练手项目。压缩包共26个文件约225.85MB包含11个py源码文件、5个pyc编译文件、3个keras模型文件以及xml、json、md等配置与说明文档涵盖舌体检测、齿痕检测、颜色检测等核心模块模型文件可直接加载使用。已有95人学习下载。资源提供完整可运行代码与说明文档结构清晰、难度适中便于快速上手读者可据此理解CNN在医学图像分类中的应用思路掌握模型调用与推理流程并在此基础上进行二次开发或功能扩展遇到问题还可与作者交流获取指导。1. 从一张舌头照片到一份食疗建议这套 CNN 舌诊系统到底能跑出什么把手机凑近伸舌头拍一张。几秒钟后屏幕上跳出「齿痕舌偏脾虚湿盛建议薏米赤小豆汤」——这就是「基于 Python 神经网络 CNN 实现的舌诊系统」想做的事。它本质是一个图像分类项目用卷积神经网络CNN对舌象图片做特征提取输出舌质、舌苔、舌形等类别再根据类别组合去匹配一份食疗方案库。整套东西通常包含三块训练好的模型文件、推理脚本、以及一个把「诊断结果」翻译成「食疗建议」的映射表。适合谁正在做课程设计的学生、想找一个完整 CNN 落地案例的 Python 入门者、以及需要快速搭一个图像分类 demo 的开发者。它不替代医生但作为一套能跑通「数据→模型→推理→业务输出」全链路的工程样本价值很实在。下面我按自己复现这类项目的顺序把选型、训练、推理、踩坑一次讲透。2. 拆开这套舌诊系统CNN 分类器加食疗映射表的两段式结构2.1 为什么是 CNN 而不是全连接或 SVM舌象识别的输入是图片核心难点在于「舌体区域的颜色、纹理、边缘形态」这些局部特征。全连接网络把图片拉平成一维向量空间信息直接丢失相邻像素的关系被抹掉效果通常很差。SVM 配合手工特征比如颜色直方图、LBP 纹理在样本少时能凑合但特征工程全靠人调换一批拍摄设备就崩。CNN 的卷积核天然在局部感受野上滑动能自动学到「舌边有没有齿痕」「舌苔是薄是厚」这类模式权值共享又让参数量可控。常见做法是用一个轻量骨干网络做迁移学习而不是从零训一个大模型——课程设计级别的数据量几百到几千张根本喂不饱 ResNet50 这种量级。选型上我一般这么定数据量小于 2000 张用 MobileNetV2 或自己搭一个 4 层卷积的小网络数据量上万再考虑 ResNet 系列。舌诊这个场景类别不多舌质、舌苔、舌形各几类小网络足够推理还快部署到普通笔记本 CPU 上都能跑。2.2 数据组织目录结构决定你能不能少写一半代码CNN 图像分类最省事的做法是让数据加载器直接读文件夹名当标签。目录长这样dataset/ ├── train/ │ ├── 齿痕舌/ │ ├── 裂纹舌/ │ ├── 薄白苔/ │ ├── 黄腻苔/ │ └── 正常/ └── val/ ├── 齿痕舌/ ├── 裂纹舌/ ├── 薄白苔/ ├── 黄腻苔/ └── 正常/每个类别一个文件夹图片丢进去即可。torchvision.datasets.ImageFolder或tf.keras.utils.image_dataset_from_directory会自动把文件夹名映射成 0、1、2… 的整数标签省掉手写标签映射的麻烦。注意训练集和验证集要按类别分层切分别让某个类别在验证集里一张都没有否则评估指标会失真。2.3 食疗方案映射诊断结果怎么变成建议模型输出的是类别概率业务要的是「吃什么」。中间需要一张映射表把「一个或多个舌象标签的组合」对应到食疗条目。常见做法是用一个 JSON 或 CSV 维护舌象标签组合食疗方向示例建议齿痕舌 薄白苔健脾祛湿薏米赤小豆汤裂纹舌 少苔滋阴润燥银耳百合羹黄腻苔清热化湿冬瓜荷叶茶正常日常调理均衡饮食推理时取概率最高的标签或者取超过阈值的多个标签做组合查询。这里有个工程细节映射表要允许「单标签命中」和「组合命中」两种模式否则用户拍出来的舌头同时有齿痕和黄苔时系统会不知道返回哪条。我一般先查组合查不到再降级到单标签。3. 用 PyTorch 把 CNN 舌诊模型训起来从数据加载到保存模型文件3.1 环境准备与依赖安装先确认 Python 版本3.8 到 3.10 兼容性最好太新的版本有时会和某些 torch 轮子打架。装依赖# 建议用虚拟环境隔离避免污染全局 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装核心依赖CPU 版够用有显卡就换成对应 CUDA 版本 pip install torch torchvision pip install numpy pillow matplotlibtorch负责建模和训练torchvision提供预训练骨干和图像变换Pillow读图matplotlib看训练曲线。如果机器有 NVIDIA 显卡去 PyTorch 官网查对应 CUDA 版本的安装命令别直接pip install torch装成 CPU 版否则训练慢到怀疑人生。3.2 数据增强与加载器配置舌象图片受拍摄光线影响极大同一根舌头在暖光和冷光下颜色差很多。数据增强是必须的但要注意颜色抖动ColorJitter别开太猛否则会把「黄苔」抖成「白苔」标签就废了。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集增强翻转、轻微旋转、小幅颜色扰动 train_tf transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸和骨干网络输入对齐 transforms.RandomHorizontalFlip(), # 舌头左右翻转不改变语义 transforms.RandomRotation(10), # 小幅旋转模拟拍摄角度偏差 transforms.ColorJitter(brightness0.1, contrast0.1), # 只做轻微扰动 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], # ImageNet 均值 [0.229, 0.224, 0.225]) # ImageNet 标准差 ]) # 验证集只做尺寸统一和归一化不做随机增强 val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds datasets.ImageFolder(dataset/train, transformtrain_tf) val_ds datasets.ImageFolder(dataset/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers2) print(train_ds.classes) # 打印类别名确认和文件夹一致归一化用的均值和标准差是 ImageNet 的统计值因为我们要用预训练权重输入分布必须对齐。batch_size设 32 是通用起点显存不够就降到 16 或 8。num_workers在 Windows 上有时会报错设成 0 用主进程加载即可。3.3 搭建迁移学习模型不从头训拿 MobileNetV2 的预训练权重把最后的分类头换成我们的类别数import torch.nn as nn from torchvision import models def build_model(num_classes): # 加载预训练骨干weights 参数在新版 torchvision 里替代了 pretrainedTrue model models.mobilenet_v2(weightsmodels.MobileNet_V2_Weights.DEFAULT) # 冻结骨干参数先只训分类头防止小数据集把预训练特征带偏 for param in model.parameters(): param.requires_grad False # 替换分类头输入维度是 MobileNetV2 的 1280 model.classifier[1] nn.Linear(1280, num_classes) return model num_classes len(train_ds.classes) model build_model(num_classes) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device)冻结骨干是迁移学习的关键一步。小数据集直接全量微调预训练学到的通用边缘、纹理特征会被少量样本覆盖反而更差。先冻结训几轮分类头等 loss 稳定了再解冻部分层做微调这是常见做法。3.4 训练循环与模型保存import torch.optim as optim criterion nn.CrossEntropyLoss() # 只优化分类头参数骨干被冻结了不参与更新 optimizer optim.Adam(model.classifier.parameters(), lr1e-3) best_acc 0.0 for epoch in range(15): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 验证阶段 model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) preds model(imgs).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) acc correct / total print(fepoch {epoch1}, loss {running_loss/len(train_loader):.4f}, val_acc {acc:.4f}) # 保存验证集上表现最好的模型 if acc best_acc: best_acc acc torch.save(model.state_dict(), best_tongue_cnn.pth) print(f saved, best_acc{best_acc:.4f})CrossEntropyLoss内部带 softmax别在模型里再加一层 softmax。学习率 1e-3 是 Adam 的常用起点训分类头够用。保存state_dict()而不是整个模型对象文件更小、加载更灵活。15 轮是个经验值看验证准确率不再涨就可以停别死磕轮数。4. 推理脚本与食疗输出把模型文件接进业务逻辑4.1 加载模型做单张图片预测训练完拿到best_tongue_cnn.pth推理时重建同样的网络结构再加载权重from PIL import Image def predict(image_path, model, class_names, device): model.eval() tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) img Image.open(image_path).convert(RGB) # 强制转 RGB防止灰度图报错 tensor tf(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): probs torch.softmax(model(tensor), dim1)[0] idx probs.argmax().item() return class_names[idx], probs[idx].item(), probs.cpu().numpy() # 重建模型并加载权重 model build_model(len(train_ds.classes)).to(device) model.load_state_dict(torch.load(best_tongue_cnn.pth, map_locationdevice)) label, conf, all_probs predict(test.jpg, model, train_ds.classes, device) print(f诊断结果{label}置信度{conf:.2%})map_locationdevice保证在 CPU 机器上也能加载 GPU 训出来的权重。convert(RGB)这行别省用户上传的图可能是 PNG 带透明通道或灰度图不转直接喂给模型会报维度错误。4.2 置信度阈值与多标签组合查询单张图只取最高概率有时不够稳。我一般设一个阈值比如 0.6超过的标签都拿出来做组合查询import json def get_diet_advice(probs, class_names, threshold0.6): # 取出所有超过阈值的标签 hits [class_names[i] for i, p in enumerate(probs) if p threshold] if not hits: hits [class_names[probs.argmax()]] # 全低于阈值就退回最高分 with open(diet_map.json, r, encodingutf-8) as f: diet_map json.load(f) # 先查组合键再降级到单标签 combo_key .join(sorted(hits)) if combo_key in diet_map: return diet_map[combo_key] for h in hits: if h in diet_map: return diet_map[h] return {方向: 日常调理, 建议: 保持均衡饮食}sorted(hits)保证组合键顺序稳定不然「齿痕舌薄白苔」和「薄白苔齿痕舌」会被当成两个键。阈值 0.6 是起点实际调的时候看误报率阈值太高会漏诊太低会把不相关的标签也拉进来。4.3 用 Flask 包一个最小可用的接口课程设计通常要有个能演示的界面Flask 最省事from flask import Flask, request, jsonify import io app Flask(__name__) app.route(/diagnose, methods[POST]) def diagnose(): file request.files[image] img Image.open(io.BytesIO(file.read())).convert(RGB) # 复用前面的 predict 逻辑这里直接内联 tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) tensor tf(img).unsqueeze(0).to(device) with torch.no_grad(): probs torch.softmax(model(tensor), dim1)[0].cpu().numpy() label, conf, _ predict_from_probs(probs, train_ds.classes) advice get_diet_advice(probs, train_ds.classes) return jsonify({舌象: label, 置信度: round(conf, 4), 食疗: advice}) if __name__ __main__: app.run(host0.0.0.0, port5000)接口收图片、返回 JSON前端拿去做展示。注意模型要在服务启动时加载一次别每次请求都load_state_dict那样响应会慢到没法用。5. 避坑指南舌诊 CNN 项目里最容易翻车的五个地方5.1 现象训练准确率 99%验证准确率 50% 出头原因数据集太小且类别不均衡模型把训练集背下来了。舌诊图片往往「正常」类特别多其他类别就几十张模型学会全猜「正常」也能拿高准确率。解决先看混淆矩阵确认是不是某个类别全军覆没。然后做类别加权CrossEntropyLoss(weight...)传入按类别频率倒数算出的权重或者对少数类做过采样。数据量实在少就把数据增强开足但颜色相关的增强要克制。5.2 现象换一批手机拍的照片预测全乱原因训练集和测试集的光线、白平衡、拍摄距离分布不一致模型学到的是「这批图的颜色统计」而不是「舌象本身的特征」。解决训练时就加入强光照增强比如随机调整 gamma、随机改变色温。更彻底的做法是在预处理阶段做白平衡校正用舌体区域做参考。我一般会在推理前加一步自动白平衡能明显提升跨设备稳定性。5.3 现象加载模型时报RuntimeError: Error(s) in loading state_dict原因保存和加载时的网络结构不一致。常见于改了分类头维度、或者用了DataParallel保存导致 key 多了module.前缀。解决确认build_model的num_classes和训练时一致。如果是module.前缀问题加载时做 key 重映射state_dict {k.replace(module., ): v for k, v in state_dict.items()}。5.4 现象推理时显存爆掉或速度极慢原因忘了torch.no_grad()或者每次请求都重新加载模型或者 batch 维度没处理好。解决推理代码必须包在with torch.no_grad():里否则会构建计算图。模型在服务初始化时加载一次全局复用。单张推理不需要 batchunsqueeze(0)加一维即可。5.5 现象食疗建议返回空或报 KeyError原因映射表的键和模型输出的类别名对不上。比如模型类别是「薄白苔」映射表里写的是「白苔」查不到就崩。解决映射表的键必须和train_ds.classes完全一致包括空格和标点。建议在服务启动时做一次校验遍历所有类别确认映射表里都有对应条目缺的直接报警。6. 把准确率再往上推一档分层微调与置信度校准的实操技巧前面冻结骨干训完分类头模型大概能到 80% 上下。想再往上走得做分层微调解冻骨干的后几层用更小的学习率比如 1e-4继续训。为什么是后几层CNN 浅层学的是边缘、颜色块这类通用特征深层学的才是「舌苔厚薄」这种任务相关特征微调深层性价比最高。# 解冻骨干最后两个 block用更小的学习率 for name, param in model.named_parameters(): if features.17 in name or features.18 in name: param.requires_grad True # 参数分两组骨干用 1e-4分类头用 1e-3 optimizer optim.Adam([ {params: model.features.parameters(), lr: 1e-4}, {params: model.classifier.parameters(), lr: 1e-3} ])分层学习率的逻辑是预训练权重已经很好大学习率会把它们毁掉分类头是随机初始化的需要大一点的学习率快速收敛。这样再训 5 到 8 轮通常能涨 3 到 5 个百分点。另一个容易被忽略的点是置信度校准。模型输出的 softmax 概率往往偏乐观0.9 的置信度实际正确率可能只有 0.7。舌诊这种场景置信度直接决定要不要给用户建议校准很有必要。简单做法是用验证集做温度缩放Temperature Scaling在验证集上找一个温度参数 T让softmax(logits/T)的置信度和实际准确率对齐。T 大于 1 会让分布更平缓降低过度自信。# 温度缩放在验证集上网格搜索最优 T import numpy as np def calibrate_temperature(model, val_loader, device): logits_list, labels_list [], [] model.eval() with torch.no_grad(): for imgs, labels in val_loader: logits_list.append(model(imgs.to(device)).cpu()) labels_list.append(labels) logits torch.cat(logits_list) labels torch.cat(labels_list) best_t, best_nll 1.0, float(inf) for t in np.arange(0.5, 3.0, 0.1): nll nn.CrossEntropyLoss()(logits / t, labels).item() if nll best_nll: best_nll, best_t nll, t return best_t T calibrate_temperature(model, val_loader, device) print(f最优温度 T {T:.2f})推理时把 logits 除以 T 再 softmax置信度就靠谱多了。这个技巧在医疗相关的分类任务里尤其值得做因为用户看到「置信度 92%」和「置信度 65%」的反应完全不同。最后说个我自己的习惯每次改完模型或数据先跑一遍固定的测试集把混淆矩阵和各类的 precision、recall 打出来存档。舌诊项目最容易出现的问题是「整体准确率涨了但某个类别 recall 掉了」光看一个总数根本发现不了。我吃过这个亏后来养成习惯不看混淆矩阵就不算训完。希望帮到你。本文还有配套的精品资源点击获取
返回列表