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

资讯详情

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

深度学习舌象诊断系统实战:从图像分类到模型部署

深度学习舌象诊断系统实战:从图像分类到模型部署 简介一套基于深度学习的舌象诊断系统实现方案面向中医信息化与计算机视觉入门及进阶学习者帮助掌握从舌象图像采集、预处理到卷积神经网络建模与部署的完整流程兼具理论讲解和工程落地参考价值。资源包共一百八十四个文件包含六十一张舌象图片用于训练与测试五十四个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-B3好300高 2-3%约 8GB正式训练EfficientNet-B4一般380高 3-5%约 11GB大标注集ConvNeXt-T好224高 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_id、tongue_color、fur_color、fur_thickness、shape。如果做过分割masks目录下存同名黑白 PNG白色为舌体区域。这个设计的关键在于分类标签和分割掩码相互独立后续无论是换分割模型还是新增标签类别都不用动目录结构。标注文件用 CSV 而不是 JSON是因为医学影像标注经常需要人工在 Excel 里核对修改CSV 的兼容成本最低。3.2 预处理白平衡、舌体裁剪与尺寸统一舌象图片的色彩失真问题远比其他医学图像严重。不同光源环境下同一舌象拍出来可能一版偏黄、一版偏蓝。所以预处理管线里必须先做颜色校正再谈训练。一个常见且稳妥的流程是先做灰度世界假设的白平衡再根据分割掩码裁剪舌体区域最后缩放至模型输入尺寸。用 OpenCV 的 Python 接口完整预处理函数如下import cv2 import numpy as np def preprocess_tongue(image_path, mask_pathNone, 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:yh, x:xw] # 保持长宽比缩放后中心填充避免舌形被拉伸变形 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), interpolationcv2.INTER_AREA) canvas np.zeros((target_size[0], target_size[1], 3), dtypenp.uint8) y_offset (target_size[0] - new_h) // 2 x_offset (target_size[1] - new_w) // 2 canvas[y_offset:y_offsetnew_h, x_offset:x_offsetnew_w] resized return canvas这段代码里值得注意的两个细节一是白平衡用的是灰度世界假设不引入额外估值参数适合工程快速落地二是缩放采用了“等比缩放 中心填充”而不是直接cv2.resize拉满全图。舌象的宽高比有其生理特征拉伸会让胖大舌和齿痕舌的形态信息失真直接缩放会直接影响最终精度。3.3 数据增强模拟不同采光与舌部姿态数据增强策略要围绕舌象场景的真实变化展开而不是套用 ImageNet 那套 RandomResizedCrop。舌象出现频率最高的差异就是亮度、色温和轻微角度旋转。常用的 PyTorch 增强组合如下from torchvision import transforms train_transforms transforms.Compose([ transforms.ToTensor(), # 亮度与对比度扰动模拟不同环境光 transforms.ColorJitter(brightness0.3, contrast0.2, saturation0.2, hue0.02), # 小角度旋转模拟舌头伸出的角度差异 transforms.RandomRotation(degrees8), # 轻微水平翻转舌象左右对称 transforms.RandomHorizontalFlip(p0.5), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])参数上brightness0.3对应 ±30% 亮度扰动这是模拟手机拍照时环境光差异的保守值hue一定不要设大舌色的病理差异本身就在色相上hue0.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, backboneefficientnet_b3, num_classes_per_task(8, 5, 4, 4)): super().__init__() # timm 加载预训练权重drop_rate0.2 做分类头正则 self.backbone timm.create_model(backbone, pretrainedTrue, features_onlyFalse) 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) }注意代码里timm的features_onlyFalse表示直接用分类网络的特征输出部分而不是返回多尺度特征图。四个 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 内迅速收敛。这里要特别提醒一点pretrainedTrue在 PyTorch 新版本里会从网络下载权重。如果运行环境无法访问外网要在代码里带上本地的pretrained_cfg参数或者直接用state_dict手动加载否则卡在权重下载阶段很难排查。训练过程中最值得盯的指标是每个任务的 Macro F1。日志里每轮打印一次Epoch 12/30 | loss2.151 | tongue_color F10.683 | fur_color F10.771 | fur_thickness F10.542 | shape F10.624 | lr3.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(backboneefficientnet_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_version17, 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 验证模型在真实环境中的表现离开实验环境之前建议再做一次“设备迁移验证”。用三台不同品牌的手机拍摄同一受试者同一时段的舌象分别跑一遍推理观察输出标签是否一致。舌象诊断在实验室数据集上的精度再高如果换一台手机就变结果在医院场景里就是不可用的系统。遇到偏差较大时优先在预处理管线里增加设备相关的颜色校准矩阵而不是重新训练模型。最终验证通过的标准只有一条不同设备、不同光线条件下同一位受试者的输出标签保持稳定。本文还有配套的精品资源点击获取
返回列表