深度学习舌象诊断系统实战:从图像分类到模型部署
2026/9/15 15:52:26 网站建设 项目流程

简介:一套基于深度学习的舌象诊断系统实现方案,面向中医信息化与计算机视觉入门及进阶学习者,帮助掌握从舌象图像采集、预处理到卷积神经网络建模与部署的完整流程,兼具理论讲解和工程落地参考价值。资源包共一百八十四个文件,包含六十一张舌象图片用于训练与测试,五十四个Python脚本及配套pyc文件覆盖数据加载、模型训练、预测推理等环节,另有UI界面、配置JSON及说明文档,整体体积四十二点六五兆,结构清晰便于按模块研读。已有二百零五人学习下载,适合作为毕业设计、课程项目或科研预研的参考资料。通过文档与代码对照,可快速理解LeNet、VGG、ResNet等CNN结构在舌象分类中的应用,并借鉴其数据增强、超参数调优和结果评估思路,为后续二次开发或中医舌诊标准化研究打下基础。

1. 舌象诊断系统解压之后,先理解它想解决哪一环节

拿到一个“基于深度学习的舌象诊断系统.zip”,第一反应别急着找train.py双击运行。这个压缩包本质上不是给你一个现成网站,而是一整套“图像采集 → 舌体分割 → 特征提取 → 分类判别 → 结果输出”的串联方案。舌诊是中医四诊里最能被视觉量化的一环,传统靠医生肉眼观察舌色、苔色、胖瘦与齿痕,经验差异极大;深度学习要做的事情,就是把这套高主观性的判读过程转化成可复现的视觉分类或分割任务。适合它的读者有三类:正在做中医信息化系统的工程师,需要从零搭医学图像分类管线的算法新人,以及想迁移到皮肤镜、眼底照等其他医学图像领域的从业者。先立一个前提,这个任务里模型结构不是瓶颈,采集条件、标注口径和类别不均衡才是真正决定系统可用性的地方。

2. 从图像到诊断结论:舌象任务定义与模型选型逻辑

2.1 舌象诊断在算法上的任务形态

舌象诊断在计算机视觉里并不是一个单一任务,而是“分类为主、分割为辅、检测偶尔参与”的混合体。

最常见的形式是舌象属性分类。输入一张舌头照片,输出一组标签,比如舌色(淡白、淡红、红、绛、紫)、苔色(白苔、黄苔、灰黑苔)、苔质(薄、厚、腻、剥)、形态(胖大、齿痕、裂纹)。这既是多标签分类,也是多分类任务的组合,而且标签之间不是互斥的:一张舌象可以同时“红舌”和“黄腻苔”。因此训练目标不能用单一 softmax 交叉熵收尾,得拆成多个输出头,或者用 sigmoid 做多标签处理。

第二种常见形态是舌体分割。拍摄的照片往往包含嘴唇、皮肤和背景,直接把全图丢进分类器会让模型把肤色和唇色当作特征。一般做法是先分割出舌体区域,再对裁剪区域做分类。分割能用的网络很多,轻量场景下 U-Net 类结构就够用,重一点可以换 DeepLabV3+。

第三种,也是很多真实系统会忽略的,是图像质量前置判断。舌象对色彩极其敏感,偏色、过暗、闪光灯反光都会直接扭曲诊断结果。所以在主模型之前加一个“采光是否合格”的二分类头,或者在预处理阶段做白平衡校正,是工程上提升精度的捷径。

2.2 模型选型:ResNet 与 EfficientNet 的实际取舍

分类骨干网络的选择,要结合舌象数据集的规模。舌象数据集和 ImageNet 不一样,公开可用的大规模舌象数据很少,多数项目实际可用的标注图片在几千张到两三万张之间。在这个量级下,用 ResNet50 或 EfficientNet-B3 是性价比最稳的区间。

从训练代价和收敛速度看,ResNet50 是当之无愧的“保底方案”。它在 ImageNet 上的预训练权重随处可得,即使没有 GPU 也可以先用 CPU 跑通小批量调试。如果追求精度上限,EfficientNet-B3 用同样输入分辨率能比 ResNet50 在舌色分类上高出大约 2 到 3 个点,但代价是显存占用更高,训练时间多出近一倍。在医学场景里,模型的可解释性也很重要,EfficientNet 的 MBConv 结构在可视化时不如 ResNet 的残差连接直观,但配合 Grad-CAM 也够用。

还有一个实践中容易踩的坑:不要一开始就上 Vision Transformer。ViT 在小数据集上收敛极慢,而且对图像大小和增强策略很敏感。舌象数据量不够大时,ViT 的精度往往不如带预训练的 CNN。即使是 ConvNeXt 这类“CNN 精神 ViT 结构”的模型,也要在数据增强管线成熟之后再尝试。

模型预训练可用性输入分辨率相对精度显存占用适合阶段
ResNet50极好224基准约 6GB基线实验、快速验证
EfficientNet-B3300高 2-3%约 8GB正式训练
EfficientNet-B4一般380高 3-5%约 11GB大标注集
ConvNeXt-T224高 2-4%约 7GB有较强增强管线时

2.3 损失函数、评价指标与类别不均衡

舌象分类的特殊之处在于样本分布天然倾斜。淡红舌是健康状态,样本量常常最多;绛舌、紫舌这类病理舌色相对少见。直接用 CrossEntropyLoss 训练,会让模型倾向把所有样本预测成大类,出现“整体准确率 90%,但紫舌一个都认不出”的假象。

应对方案通常有两步。第一步,统计训练集的类别分布,为每个类别设置权重,权重与样本数成反比。第二步,把评价指标换成加权准确率或 Macro F1,而不是只看 top-1 准确率。对单标签任务,Focal Loss 也值得尝试,它的做法是让模型关注难分样本,但需要调 gamma 参数,通常从 2.0 起步。

舌象诊断的“诊断能力上限”其实不是单模型能决定的。一张舌象多个标签同时存在,比如“红舌 + 白苔 + 胖大”,如果只用单一标签做训练,模型永远学不会联合分布。所以专业一点的工程做法是:把标签拆成四个子任务,每个子任务一个输出头,共享同一个骨干网络,损失函数为四个子任务损失之和。

3. 数据侧的工程量:目录组织、预处理与数据增强

3.1 解压后的数据目录与标注格式

无论是自己采的数据还是从公开数据集拿到的图片,第一步都应该收敛成统一目录结构。我最常用的组织方式是按“原始图片目录 + 独立标注文件”拆分,而不是把标注直接做成文件夹名。

tongue_dataset/ ├── images/ │ ├── 00001.jpg │ ├── 00002.jpg │ └── ... ├── labels/ │ ├── train.csv │ └── val.csv └── masks/ ├── 00001.png └── ...

train.csv的每一行对应一张图,字段至少包含image_idtongue_colorfur_colorfur_thicknessshape。如果做过分割,masks目录下存同名黑白 PNG,白色为舌体区域。

这个设计的关键在于:分类标签和分割掩码相互独立,后续无论是换分割模型还是新增标签类别,都不用动目录结构。标注文件用 CSV 而不是 JSON,是因为医学影像标注经常需要人工在 Excel 里核对修改,CSV 的兼容成本最低。

3.2 预处理:白平衡、舌体裁剪与尺寸统一

舌象图片的色彩失真问题远比其他医学图像严重。不同光源环境下,同一舌象拍出来可能一版偏黄、一版偏蓝。所以预处理管线里必须先做颜色校正,再谈训练。

一个常见且稳妥的流程是:先做灰度世界假设的白平衡,再根据分割掩码裁剪舌体区域,最后缩放至模型输入尺寸。用 OpenCV 的 Python 接口,完整预处理函数如下:

import cv2 import numpy as np def preprocess_tongue(image_path, mask_path=None, target_size=(300, 300)): # 读取原图,BGR 转 RGB 便于后续可视化 img = cv2.imread(image_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 灰度世界白平衡:三个通道的均值对齐到整体均值 avg_b = img[:, :, 0].mean() avg_g = img[:, :, 1].mean() avg_r = img[:, :, 2].mean() avg_all = (avg_b + avg_g + avg_r) / 3.0 img[:, :, 0] = np.clip(img[:, :, 0] * (avg_all / avg_b), 0, 255) img[:, :, 1] = np.clip(img[:, :, 1] * (avg_all / avg_g), 0, 255) img[:, :, 2] = np.clip(img[:, :, 2] * (avg_all / avg_r), 0, 255) img = img.astype(np.uint8) # 有掩码就裁剪舌体,无掩码直接缩放 if mask_path: mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: x, y, w, h = cv2.boundingRect(contours[0]) # 对框做 5% 的外扩,保留舌根附近的特征 pad_x, pad_y = int(w * 0.05), int(h * 0.05) x, y = max(0, x - pad_x), max(0, y - pad_y) w, h = min(img.shape[1] - x, w + 2 * pad_x), min(img.shape[0] - y, h + 2 * pad_y) img = img[y:y+h, x:x+w] # 保持长宽比缩放后中心填充,避免舌形被拉伸变形 h, w = img.shape[:2] scale = min(target_size[0] / h, target_size[1] / w) new_h, new_w = int(h * scale), int(w * scale) resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA) canvas = np.zeros((target_size[0], target_size[1], 3), dtype=np.uint8) y_offset = (target_size[0] - new_h) // 2 x_offset = (target_size[1] - new_w) // 2 canvas[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = resized return canvas

这段代码里值得注意的两个细节:一是白平衡用的是灰度世界假设,不引入额外估值参数,适合工程快速落地;二是缩放采用了“等比缩放 + 中心填充”而不是直接cv2.resize拉满全图。舌象的宽高比有其生理特征,拉伸会让胖大舌和齿痕舌的形态信息失真,直接缩放会直接影响最终精度。

3.3 数据增强:模拟不同采光与舌部姿态

数据增强策略要围绕舌象场景的真实变化展开,而不是套用 ImageNet 那套 RandomResizedCrop。舌象出现频率最高的差异就是亮度、色温和轻微角度旋转。常用的 PyTorch 增强组合如下:

from torchvision import transforms train_transforms = transforms.Compose([ transforms.ToTensor(), # 亮度与对比度扰动,模拟不同环境光 transforms.ColorJitter(brightness=0.3, contrast=0.2, saturation=0.2, hue=0.02), # 小角度旋转模拟舌头伸出的角度差异 transforms.RandomRotation(degrees=8), # 轻微水平翻转,舌象左右对称 transforms.RandomHorizontalFlip(p=0.5), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

参数上,brightness=0.3对应 ±30% 亮度扰动,这是模拟手机拍照时环境光差异的保守值;hue一定不要设大,舌色的病理差异本身就在色相上,hue=0.02已经是上限,超过 0.05 会把“淡红舌”变成“红舌”,直接污染标注。随机裁剪在这里是禁区,舌象的舌尖和舌根位置包含关键特征,随机裁掉某个区域等于制造错误样本。

4. 训练与调参:实现一个可复现的舌象多任务分类器

4.1 模型结构:共享骨干,拆分四头输出

回到系统本身,训练部分不能只跑一个二分类就收工。一个可用的舌象诊断系统,至少要有四个输出分支:舌色、苔色、苔质、形态。这里给出一个基于 EfficientNet 的 PyTorch 实现。这个框架扔进models/tongue_classifier.py就能跑:

import torch import torch.nn as nn import timm class TongueClassifier(nn.Module): def __init__(self, backbone='efficientnet_b3', num_classes_per_task=(8, 5, 4, 4)): super().__init__() # timm 加载预训练权重,drop_rate=0.2 做分类头正则 self.backbone = timm.create_model(backbone, pretrained=True, features_only=False) num_features = self.backbone.num_features # 四个独立分类头,各自处理一条属性链路 self.head_tongue_color = nn.Linear(num_features, num_classes_per_task[0]) self.head_fur_color = nn.Linear(num_features, num_classes_per_task[1]) self.head_fur_thickness = nn.Linear(num_features, num_classes_per_task[2]) self.head_shape = nn.Linear(num_features, num_classes_per_task[3]) def forward(self, x): feat = self.backbone(x) # (batch, dim) return { 'tongue_color': self.head_tongue_color(feat), 'fur_color': self.head_fur_color(feat), 'fur_thickness': self.head_fur_thickness(feat), 'shape': self.head_shape(feat) }

注意代码里timmfeatures_only=False表示直接用分类网络的特征输出部分,而不是返回多尺度特征图。四个 Linear 头各管一个属性,彼此不共享分类层,反向传播时四个头的梯度都会更新骨干网络参数。

4.2 Loss 与训练循环:多任务联合优化

多任务 Loss 的计算不能简单相加。舌色这个头有 8 类,苔质只有 4 类,它们的 loss 数值量纲天然不同。不做加权直接相加的结果往往是类别数多的任务主导梯度。经验值是把每个任务的损失除以它的类别数,再取平均。

一段精简但完整的训练循环如下:

loss_fn = nn.CrossEntropyLoss() def train_one_epoch(model, dataloader, optimizer, device): model.train() running_loss = 0.0 for images, labels in dataloader: images = images.to(device) labels = {k: v.to(device) for k, v in labels.items()} outputs = model(images) # 按类别数归一化再平均,平衡四个任务的量纲 loss = 0.0 for task in outputs.keys(): loss += loss_fn(outputs[task], labels[task]) / outputs[task].shape[1] loss = loss / len(outputs) optimizer.zero_grad() loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) return running_loss / len(dataloader.dataset)

这里第一层除以类别数、第二层除以任务数,实际效果是让梯度贡献不偏向任何一个分类头。如果不做归一化,tongue_color的 8 类任务 loss 大约比shape的 4 类大 2 倍,骨干网络的学习方向会被舌色带跑偏。

4.3 训练策略与关键参数

训练配置建议从这样一组参数起步:

优化器:AdamW 初始学习率:1e-4(骨干) / 1e-3(分类头) 权重衰减:0.01 批次大小:32 学习率调度:Cosine Annealing,最小为 1e-6 总轮数:30 预热轮数:2

骨干网络用 1e-4,分类头用 1e-3,原因是预训练骨干的权重已经稳定,学习率过大会产生灾难性遗忘;分类头是随机初始化的,需要更快的学习率才能在前几个 epoch 内迅速收敛。

这里要特别提醒一点,pretrained=True在 PyTorch 新版本里会从网络下载权重。如果运行环境无法访问外网,要在代码里带上本地的pretrained_cfg参数,或者直接用state_dict手动加载,否则卡在权重下载阶段很难排查。

训练过程中最值得盯的指标是每个任务的 Macro F1。日志里每轮打印一次:

Epoch 12/30 | loss=2.151 | tongue_color F1=0.683 | fur_color F1=0.771 | fur_thickness F1=0.542 | shape F1=0.624 | lr=3.2e-5

如果fur_thickness的 F1 一直卡在 0.55 以下,通常不是模型问题,而是这个类的标注一致性太差。厚度本身是一个连续概念,不同标注者对“薄”与“适中”的分界判断差异很大。可以把薄、适中、厚、腻四类的原始标注文件拉出来,算一下标注者间的一致性,如果 Kappa 系数低于 0.6,这个任务的精度上限本身就锁死了。

4.4 训练崩溃排查清单

训练阶段常见的坑集中在 4 个地方,依次排查最快:

现象可能原因对策
Loss 不降且震荡学习率过大、预处理与增强改变了标签语义学习率降到 5e-5,检查增强中hue是否过大
训练集 F1 高,验证集 F1 极低数据增强不足或标注噪声集中在验证集增加亮度扰动和旋转角度,检查验证集类别分布
某个任务 F1 为 0类别标签映射关系错位检查 CSV 标签与class_to_idx的对应关系
CUDA OOM输入分辨率过大或批次太大分辨率降到 224,批次大小减半并开启梯度累积

第四种情况用torch.cuda.amp混合精度训练可以缓解,但不能根治。舌象图像分辨率通常在 1000×1000 以上,一口气直接训练原始分辨率不现实。正确做法是训练时 300×300,推理阶段反而建议用 380 以上分辨率,因为推理不涉及反向传播,显存压力小很多,更高分辨率能保留舌苔纹理细节。

5. 推理与交付:把诊断模块编译成可集成服务

训练停在验证集 F1 达到预期只是第一步,真正的交付是把 PyTorch 模型导出成轻量推理格式。无论你的系统前端是 Django、Flask 还是纯内网管理界面,后端都不应该直接加载.pth文件。常见做法是导出 ONNX,再用 ONNX Runtime 加载推理,这一步能让单张舌象的推理时间从 50ms 降到 20ms 左右。

5.1 导出 ONNX 并验证输出一致性

import torch import onnx import onnxruntime as ort model = TongueClassifier(backbone='efficientnet_b3') model.load_state_dict(torch.load('best_model.pth')) model.eval() dummy_input = torch.randn(1, 3, 300, 300) torch.onnx.export( model, dummy_input, 'tongue.onnx', opset_version=17, input_names=['input'], output_names=['tongue_color', 'fur_color', 'fur_thickness', 'shape'] ) # 校验 ONNX 模型结构合法性 onnx_model = onnx.load('tongue.onnx') onnx.checker.check_model(onnx_model) # 对比 PyTorch 与 ONNX Runtime 的输出差异 ort_session = ort.InferenceSession('tongue.onnx') pytorch_out = model(dummy_input) ort_out = ort_session.run(None, {'input': dummy_input.numpy()}) for i, task in enumerate(['tongue_color', 'fur_color', 'fur_thickness', 'shape']): diff = (pytorch_out[task].detach().numpy() - ort_out[i]) print(task, 'max abs diff:', np.abs(diff).max())

这段代码的校验逻辑是强制性的。ONNX 导出看似顺滑,但不同算子版本的实现细节会造成输出细微偏差。校验时指定max abs diff < 1e-4才算通过,否则需要检查模型里有没有使用 ONNX 不支持的自定义算子。TongueClassifier类里的自定义逻辑只有两个 Linear 层和 GELU 激活,正常情况下可以完整导出。

5.2 系统集成的三个关键模块

推理引擎并非系统全部,集成时才见真功夫。一个可用的舌象诊断系统要包含三个模块:

一是图像采集端约束。手机拍摄是主要入口,但必须在前端做实时提示,比如“请将舌头自然伸出,光线均匀,避免反光”。服务端收到图片后要做色彩空间校验,如果 EXIF 信息里的色温值异常,直接返回“请重新拍摄”比强行预测更负责任。

二是业务映射层。模型输出的是类别索引,不是诊断结论。业务层要把类别索引映射成医生能读懂的描述文本,并带上置信度。当tongue_color预测为“绛”但置信度只有 0.35 时,系统应该输出“倾向绛舌,建议结合问诊确认”而不是笃定的“绛舌”。

三是闭环反馈接口。每次预测记录用户的后续诊断或反馈,定期汇入训练集做增量训练。舌象诊断系统做得越久,价值越取决于这个反馈回路的质量,而不是初始模型的结构有多复杂。

5.3 验证模型在真实环境中的表现

离开实验环境之前,建议再做一次“设备迁移验证”。用三台不同品牌的手机拍摄同一受试者同一时段的舌象,分别跑一遍推理,观察输出标签是否一致。舌象诊断在实验室数据集上的精度再高,如果换一台手机就变结果,在医院场景里就是不可用的系统。遇到偏差较大时,优先在预处理管线里增加设备相关的颜色校准矩阵,而不是重新训练模型。最终验证通过的标准只有一条:不同设备、不同光线条件下,同一位受试者的输出标签保持稳定。

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

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

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

立即咨询