☰
Python神经网络CNN舌诊系统:从图像分类到食疗建议的完整实现
2026/9/27 23:07:32 网站建设 项目流程

简介:这是一套面向计算机相关专业学生与开发者的舌诊系统课程设计资源,基于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 matplotlib

torch负责建模和训练,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(brightness=0.1, contrast=0.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', transform=train_tf) val_ds = datasets.ImageFolder('dataset/val', transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=2) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=2) 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 里替代了 pretrained=True model = models.mobilenet_v2(weights=models.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(), lr=1e-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(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) acc = correct / total print(f"epoch {epoch+1}, 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), dim=1)[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_location=device)) label, conf, all_probs = predict('test.jpg', model, train_ds.classes, device) print(f"诊断结果:{label},置信度:{conf:.2%}")

map_location=device保证在 CPU 机器上也能加载 GPU 训出来的权重。convert('RGB')这行别省,用户上传的图可能是 PNG 带透明通道或灰度图,不转直接喂给模型会报维度错误。

4.2 置信度阈值与多标签组合查询

单张图只取最高概率有时不够稳。我一般设一个阈值,比如 0.6,超过的标签都拿出来做组合查询:

import json def get_diet_advice(probs, class_names, threshold=0.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', encoding='utf-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), dim=1)[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(host='0.0.0.0', port=5000)

接口收图片、返回 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():里,否则会构建计算图。模型在服务初始化时加载一次,全局复用。单张推理不需要 batch,unsqueeze(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 掉了」,光看一个总数根本发现不了。我吃过这个亏,后来养成习惯,不看混淆矩阵就不算训完。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询