首页 / 资讯中心 / 文章详情

舌象诊断系统实战:基于ResNet50的中医望诊图像分类与部署

舌象诊断系统实战:基于ResNet50的中医望诊图像分类与部署 ★ FEATURED ARTICLE
简介这是一套面向中医数字化与计算机视觉研究者的舌象诊断系统源码包基于深度学习方法完成舌象图像的分类识别与辅助诊断适合具备一定编程和深度学习基础的学生、工程师用于复现实验、课题研究或二次开发。压缩包共182个文件大小42.52MB包含60张jpg/jpeg/png舌象图像样本、54个py源码及pyc编译文件并配有文本说明、配置数据、界面文件、项目文档与字体文件等辅助内容数据、代码、配置与文档层次分明便于按需查阅。目前已有40人学习下载除了完整的模型源码和训练脚本之外还附带了可直接观察的舌象样本图像、界面配置与说明文档可帮助使用者理解数据预处理、模型构建、界面交互等环节对正在做课程设计、毕业论文或相关课题的研究者而言是一套结构完整、便于复现和二次开发的实践资料。1. 舌象诊断系统不玄深度学习落地中医望诊的第一关拿到「舌象诊断系统」这套源码的第一反应它解决的是一件很具体的事给一张舌头照片模型自动输出舌色、舌苔、舌形三个维度的分类判断并给出分析结果。这类系统这两年热度高本质是医学影像分类的一个垂直变体把经典的CNN链路套到中医望诊场景。真正复现下来最反直觉的结论是模型不是瓶颈数据才是。标注不一致、拍照光源不统一、样本类严重倾斜随便一个坑都比修改网络结构对准确率的影响大。适合两类人一类是拿它当深度学习实战项目来拆的在校学生另一类是评估中医数字化诊断能不能上生产的从业者。下文按我实际跑这套源码的顺序展开参数、边界、坑都写在对应位置。2. 系统架构与数据链路从舌图录入到诊断输出的完整闭环2.1 四层架构划分解压源码之后第一件事不是跑训练而是先梳理目录搞清楚各个模块的职责边界。这套系统从工程结构上可以拆成四层数据层、预处理层、模型层、服务层。每一层对应源码里一个相对独立的脚本集合替换任何一个环节都不影响其他层。层级职责对应源码动作数据层把原始舌头照片整理成带标签的数据集标签解析、数据集划分、样本统计预处理层从原图中裁剪舌体、校正颜色、做数据增强ROI裁剪、灰度世界白平衡、增强管线模型层训练与推理核心网络网络定义、损失计算、训练循环、ONNX导出服务层把训练好的模型包装成可用入口单图预测脚本、批量评测脚本这四层的边界划分是有讲究的数据层可以单独换成医院里重新采集的批量数据服务层可以换成一个Flask接口或者桌面端模型层不需要动。对想改造这套系统做毕设或产品原型的人来说这种可替换性意味着每一层都有独立的实验空间。2.2 标签体系与采集规范舌象诊断本质上是多标签分类任务。一张舌头照片要同时输出舌色、舌苔、舌形三个维度。源码里标签文件的组织方式通常是每行一条样本路径加上三个标签字段取值的组合决定了分类头的数量。舌色维度常见取值淡白、淡红、红、绛、紫。舌苔维度常见取值薄白、白腻、黄腻、剥苔。舌形维度常见取值胖大、瘦薄、齿痕。这三个维度不是独立变化的红舌常伴黄腻苔脾虚湿盛多表现于胖大舌加白腻苔。模型能不能学到这层关联取决于标签质量。文档说明里花了相当篇幅强调采集规范——固定色温、固定光源角度、干净背景、舌头自然伸出状态。这些条件不控好收集几千张照片也可能是在给模型喂噪声。标注一致性是另一个容易被低估的问题。医生A把偏红的舌标成红舌医生B可能会标成绛舌同一个人不同状态下拍的舌象也存在差异。常见的做法是双人标注、第三方仲裁标签存疑的样本不进训练集这是保证模型上限的操作比调参优先级高得多。2.3 环境依赖与源码目录结构复现的第一步是搭环境这一步翻车率其实是最高的。源码要求的运行环境大概是 Python 3.8 PyTorch 1.13 torchvision 0.14 opencv-python onnxruntime。建议新建独立的conda环境不要直接装到系统Python里。conda create -n tongue_diag python3.8 -y conda activate tongue_diag pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cu117 pip install opencv-python onnxruntime numpy pandas albumentations版本锁定的原因在于torchvision版本与PyTorch版本必须严格匹配否则预训练权重加载会直接报错onnxruntime在Python 3.10以上部分wheel存在兼容问题3.8环境最省事。如果机器没有NVIDIA GPU可以把第一行的cu117换掉、装CPU版本先跑通流程再上GPU。源码目录结构大致是data目录放原始图和标签文件preprocess目录放ROI裁剪和增强脚本models目录放网络定义scripts目录放训练和推理入口docs目录放文档说明。README里给的是一整套从数据整理到训练评估的流程对着走一遍比看任何二次讲解都直接。3. 舌象预处理从ROI裁剪到数据增强的参数设置3.1 舌体定位与ROI区域划分舌头照片不能整张直接喂进网络。背景、嘴唇、牙齿对舌色判断是纯干扰网络很容易学到错误特征。源码里的做法是先用目标检测框出舌体或者直接读取标注文件里已有的边界框做裁剪。拿到舌体之后还需要做分区舌尖、舌中、舌根。颜色信息主要集中在中段舌尖受环境光和唇色影响最大舌根则容易被遮挡。import json import cv2 import numpy as np # 读取标注文件中的舌体边界框 with open(data/labels/sample_001.json, r) as f: ann json.load(f) bbox ann[tongue_bbox] # [x1, y1, x2, y2] img cv2.imread(data/images/sample_001.jpg) h, w img.shape[:2] # 对边界框做向内收缩剔除嘴唇和背景边缘 margin_x int((bbox[2] - bbox[0]) * 0.05) margin_y int((bbox[3] - bbox[1]) * 0.05) x1, y1 max(0, bbox[0] margin_x), max(0, bbox[1] margin_y) x2, y2 min(w, bbox[2] - margin_x), min(h, bbox[3] - margin_y) roi img[y1:y2, x1:x2]边界框向内收缩5%这个操作是为了剔除舌尖和舌根边缘的唇色干扰。这里的参数不是死的——如果标注框本身已经标注得很紧收缩比例可以降到2%如果标注粗糙、框外带了大量背景则需要提高到10%。缩小ROI后模型更容易把注意力放在舌面纹理和颜色分布上。3.2 白平衡与色彩校正舌象最有诊断价值的信息是颜色偏偏颜色最容易失真。同一台手机自动白平衡在不同光源下拍出的舌头色差肉眼可见。校正常用的方法是灰度世界假设把R、G、B三个通道的均值拉平到同一个水平抵消光源色温的影响。def gray_world_white_balance(img): 灰度世界白平衡假设图像中RGB三通道均值应相等 result img.astype(np.float32) mean_r np.mean(result[:, :, 2]) mean_g np.mean(result[:, :, 1]) mean_b np.mean(result[:, :, 0]) # 以G通道均值为基准等比例缩放R和B通道 scale_r mean_g / (mean_r 1e-6) scale_b mean_g / (mean_b 1e-6) result[:, :, 2] * scale_r result[:, :, 0] * scale_b # 截断到合法像素范围 result np.clip(result, 0, 255).astype(np.uint8) return result灰度世界假设对舌象这种红色占比很大的场景其实偏保守处理完的画面会偏灰、偏中性一些但对分类任务来说影响是正向的——它把不同拍摄设备之间的色差压缩了。源码里如果遇到极端偏色的样本还会结合标准色卡做更细的校正但灰度世界是性价比最高的第一步。3.3 数据增强参数与注意事项源码默认的数据增强参数值得单独拿出来分析因为它们直接决定模型能不能泛化、会不会把颜色学歪。核心增强操作包括随机旋转15度、水平平移10%、缩放范围0.9到1.1、HSV空间的色相扰动±15、饱和度扰动20%、亮度扰动20%每个操作的概率设为0.5。import albumentations as A train_transform A.Compose([ A.Rotate(limit15, p0.5), A.ShiftScaleRotate( shift_limit0.1, scale_limit0.1, rotate_limit0, p0.5 ), A.HueSaturationValue( hue_shift_limit15, sat_shift_limit20, val_shift_limit20, p0.5 ), A.Resize(224, 224), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])色相扰动只给±15是经过权衡的。舌色分类对色相对应关系极其敏感扰动大了会把淡红舌变成红舌等于在造虚假样本。亮度扰动同理过大的亮度扰动会把正常舌变成淡白舌直接把类别边界搅浑。数据增强在这里是调节模型泛化能力和颜色判断准确率之间的杠杆幅度必须比通用图像分类小一个量级。3.4 数据集切分按样本主体分组训练集、验证集、测试集按8:1:1切分但切分粒度必须按样本主体而不是按单张图片。同一患者的舌头照片如果同时出现在训练集和验证集模型在验证集上的表现会虚高——它在训练时已经见过同一个人的舌色了。这属于数据泄漏是验证集准确率接近99%但线上表现惨淡的最常见原因。from sklearn.model_selection import GroupShuffleSplit # sample_id 是样本主体的唯一标识 gss GroupShuffleSplit(n_splits1, train_size0.8, random_state42) train_idx, val_idx next(gss.split(file_list, groupspatient_ids)) train_files [file_list[i] for i in train_idx] val_files [file_list[i] for i in val_idx]这里用GroupShuffleSplit而不是普通的train_test_split核心差异就在于groups参数。如果源码里自带的划分脚本没有按主体分组建议改成这种写法。同一患者多个时间点采样的照片在临床上很常见不按主体分组等于验证集直接失效。4. 训练与部署要点ResNet50微调、损失设计与ONNX导出4.1 网络结构双分类头设计网络选型上源码用的是ResNet50作为backbone。选择这个结构不是因为它最新而是因为它有预训练权重、足够深、在中小规模医学图像数据集上泛化表现稳定。关键改造在分类头部分去掉原生的ImageNet分类层换成针对舌色、舌苔、舌形三个维度的独立分类头。import torch.nn as nn class TongueDiagnosisNet(nn.Module): def __init__(self, num_colors5, num_coatings4, num_shapes3): super().__init__() import torchvision.models as models backbone models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) self.features nn.Sequential(*list(backbone.children())[:-1]) # 三个分类头共用一个特征提取器 self.color_head nn.Linear(2048, num_colors) self.coating_head nn.Linear(2048, num_coatings) self.shape_head nn.Linear(2048, num_shapes) def forward(self, x): feat self.features(x) feat feat.flatten(1) return self.color_head(feat), self.coating_head(feat), self.shape_head(feat)三个分类头共享同一个特征提取器的好处是计算量小且舌色、舌苔、舌形之间是有相关性的共享底层特征能让三个任务互相增强。但要注意梯度回传的时候三个头的loss会同时影响backbone的权重如果某个维度的标签噪声特别大会拖累整体特征质量。遇到这种情况可以退一步让三个头各自接独立的FC层组合代价是参数量略增。4.2 超参数设置与损失函数源码文档里给的超参数组合优化器AdamW初始学习率1e-4batch size 32训练40到60个epoch。微调阶段的关键经验是backbone的学习率要比新加的分类头低一般是分类头的十分之一避免预训练权重被大幅破坏。超参数数值调整建议优化器AdamW比Adam多一个解耦的权重衰减初始学习率1e-4分类头/ 1e-5backbone验证集不降就减半batch size32显存不够就降到16weight decay1e-4过拟合时加大到5e-4训练轮数40~60以验证集最优为准类别不平衡是舌象诊断绕不开的问题。淡红舌、薄白苔这类正常态在数据集中占比可能超过一半其他类别加在一起才占另一半。直接用交叉熵损失模型会倾向把一切预测成多数类。源码给出了两个解决层面一是给交叉熵加class weight二是用Focal Loss让模型把注意力放到难分类的少数类上。import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, gamma2.0, alphaNone): super().__init__() self.gamma gamma self.alpha alpha def forward(self, logits, targets): ce_loss F.cross_entropy(logits, targets, reductionnone) pt torch.exp(-ce_loss) focal_loss (1 - pt) ** self.gamma * ce_loss if self.alpha is not None: alpha_t self.alpha[targets] focal_loss alpha_t * focal_loss return focal_loss.mean()gamma取2.0是Focal Loss论文里的默认值alpha是各类别权重的张量从训练集标签分布统计出来即可。注意alpha的取值不要直接按频率倒数算对于占比特别小的类别权重拉得太高会导致训练震荡建议上限控制在5倍以内。4.3 训练循环与模型保存策略训练主循环本身不复杂重点是模型保存时机和学习率衰减策略。源码里常见做法是监控验证集准确率连续3个epoch不提升就把学习率乘以0.5同时保存验证集指标最优的那一次权重。best_acc 0.0 patience 0 for epoch in range(num_epochs): model.train() for batch_x, batch_labels in train_loader: batch_x batch_x.to(device) labels [lab.to(device) for lab in batch_labels] optimizer.zero_grad() pred_colors, pred_coatings, pred_shapes model(batch_x) loss (loss_color(pred_colors, labels[0]) loss_coating(pred_coatings, labels[1]) loss_shape(pred_shapes, labels[2])) / 3 loss.backward() optimizer.step() val_acc evaluate(model, val_loader) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) patience 0 else: patience 1 if patience 3: for g in optimizer.param_groups: g[lr] * 0.5 patience 0三个分类头的loss取平均是常见做法没有哪个维度应该被优先保证。如果发现训练后期某个维度始终学不好可以把对应loss的权重加大到2但不要三个头各自调一套lr会让优化过程变得不可控。4.4 ONNX导出与部署对齐训练结束后的落地动作是导出ONNX。PyTorch模型直接部署有几个麻烦依赖库重、推理速度慢、C端不好接。导出ONNX后可以用onnxruntime推理依赖轻、跨平台、速度接近原生。import torch import onnxruntime as ort model TongueDiagnosisNet() model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, tongue_model.onnx, input_names[input], output_names[color, coating, shape], opset_version12, dynamic_axesNone ) # 用onnxruntime验证导出结果 sess ort.InferenceSession(tongue_model.onnx) input_data preprocess_single_image(test.jpg) # 返回 [1,3,224,224] float32 outputs sess.run(None, {input: input_data})导出时固定输入尺寸dynamic_axes不打开。打开动态轴表面看灵活实际会让部署端的预处理代码多出一条分支纯属给自己挖坑。推理端用OpenCV读图时记得OpenCV读进来是BGR顺序必须先转到RGB再走训练时的预处理流程这一条做错能让部署版准确率直接掉回随机水平。5. 避坑指南复现这套舌象诊断系统的5个真实踩坑记录5.1 验证集准确率接近95%部署后错判一堆现象训练阶段验证集准确率很漂亮把模型接到推理脚本后对真实场景图片的判断乱七八糟。原因训练时的预处理流水线和推理时的预处理流水线不一致。训练用的是ROI裁剪白平衡归一化推理时直接用原图resize就喂进去了输入分布完全对不上。解决把训练脚本里从原始图片到张量的所有步骤抽成一个独立的preprocess函数训练和推理共用。尤其检查归一化的mean和std是否一致、颜色通道顺序是否一致、是否做了ROI裁剪。5.2 损失一直震荡训练曲线像心电图现象loss曲线上下剧烈跳动验证集准确率也跟着抖模型根本没有收敛趋势。原因学习率偏高或者数据增强过强。舌象数据集不像ImageNet那样千万级规模直接复用通用图像分类的学习率容易震荡。解决先用0.1、0.01、0.001、0.0001四个学习率各跑几十个batch看初始loss下降情况再定。这个小数据集规模一般1e-4起步分类头1e-4、backbone 1e-5是相对稳的组合。5.3 预测结果清一色输出正常舌少数类完全不出现在结果里现象模型对所有测试图片都输出淡红舌、薄白苔没有任何例外。原因类别不平衡叠加交叉熵损失模型发现全部预测成多数类就能获得极低的loss。少数类样本量太少对梯度贡献微乎其微。解决训练前统计标签分布给多数类降权或少数类升权切换到Focal Loss。同时检查验证集里少数类样本有没有被数据划分脚本丢到训练集里导致验证集根本看不到少数类。5.4 同一张舌象换台手机拍摄诊断结果截然不同现象模型对同一舌头在不同设备、不同光源下拍的照片给出完全不同的分类稳定性不可接受。原因色彩未做标准化处理。模型学到的颜色分布与训练时的色温绑定测试时换了光源色温输入分布整体偏移结果自然漂移。解决推理前强制走一遍灰度世界白平衡训练时也保持同样的处理流程。如果对颜色准确性要求更高可在采集时放置标准色卡按色卡做映射校正这是临床采集场景的标配操作。5.5 conda环境按要求装好了跑训练脚本还是报import error现象torch、torchvision、opencv都装成功了运行train.py提示找不到某个模块或版本冲突。原因大概率是torch和torchvision版本不匹配或者是训练脚本里用了更高版本torchvision才有的预训练权重注册接口。解决严格按requirements.txt列出的版本重装。如果装的是预发布版或镜像源版本卸载后重新指定版本装一次先装CPU版跑通全流程再换CUDA版能省去非常多排查时间。6. 用留出集加典型样本做一次更可信的系统验证把模型跑出准确率之后真正要验证的是它从黑匣子变成可信工具。我的做法是额外加三层验证而不是只看总准确率。第一层是用留出集做分维度混淆矩阵。统计舌色、舌苔、舌形在测试集上的整体准确率再看每个类别之间的混淆分布。舌色里红舌和绛舌最容易互混舌苔里黄腻和白腻边界模糊这些是模型的实际短板。第二层是Top-2命中率检测如果Top-1准确率不高但Top-2能把真实标签覆盖住说明模型学到了颜色远近关系实际问题没那么大。第三层是典型样本覆盖从验证集里抽淡红舌薄白苔、红舌黄腻苔、胖大舌这三个典型组合以及模型最容易翻车的临界样本逐一跑推理、打印三个维度的概率输出。python scripts/evaluate.py --model_path best_model.pth \ --test_dir data/test_images/ \ --label_file data/test_labels.json \ --top_k 2 \ --output report.csv评估脚本会输出每张测试图三个维度的Top-1和Top-2结果以及一个按类别汇总的混淆统计表。我一般会把report.csv里模型输出和人工判断不一致的行单独拉出来看原图人工再核一遍到底是谁错了。这一步对判断「模型是学会了还是背住了」极有价值。评估指标也不能只看一个。舌色准确率、舌苔准确率、舌形准确率分别看是三件事整体准确率最高不代表每个维度都可靠。实际使用场景里如果舌色可靠而舌苔不稳定产品上的取舍就是侧重输出舌色结果舌苔结果标注为参考。从那以后我每次拿到开源医疗AI项目都会先把训练预处理和推理预处理两条流水线逐行对齐再查一遍标签分布和增强参数确认少数学不会被「洗掉」最后才会信那个准确率数字。这套动作花不了多少时间但能过滤掉一大半表面光鲜的项目。希望帮到你。提示舌象诊断模型的输出结果为辅助分析参考不能替代执业医师的诊断结论。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站