
简介基于深度学习的舌象诊断项目面向毕业设计、课程设计与期末大作业场景适合希望将目标检测与医学图像分割技术用于中医辅助诊断的学生。项目结合YOLO完成舌质、舌苔等关键特征识别并以U-Net构建分割流程覆盖数据预处理、模型搭建、训练评估与优化等环节技术路线完整。压缩包共184个文件大小42.65MB其中61张jpg/jpeg样本图像用于训练与测试54个py脚本和40个pyc文件对应核心功能及编译缓存另有txt/json配置、ui界面、md说明与docx学习路线文档目录结构清晰便于按模块查阅。已有67人学习下载适合直接作为毕业设计或课程设计的整体参考。通过学习代码与配套文档可以快速理解舌象诊断系统的工程实现思路掌握图像增强、模型训练与鲁棒性调优的实用方法。1. 舌象诊断不是玄学深度学习把它变成了可复现的工程任务打开一个名为“基于深度学习的舌象诊断.zip”的压缩包之前先别急着解压看代码。这个zip背后不是一套神秘算法而是一条已经被验证过的技术路线用图像分类和分割模型把中医望诊里的舌色、苔色、齿痕等主观指标变成可量化的输出。它对两类人最有用一类是做医学影像落地的算法工程师想拿一个小而完整的项目练手另一类是中医信息化方向的学生需要从数据、训练到部署的完整样例。这篇文章不替你读包而是告诉你这类工程包拿回来后按什么顺序拆、哪些参数必须改、模型为什么学不好、最后怎么证明它真的可用。2. 拆开“基于深度学习的舌象诊断.zip”后先理清目录与运行入口再碰训练2.1 解压不是双击完事先看清目录结构再动手我建议用命令行解压尽量避免图形界面在处理中文文件名时的编码问题。把压缩包放到干净目录然后列出内容。mkdir ~/tongue_ai cd ~/tongue_ai mv ~/Downloads/基于深度学习的舌象诊断.zip ./ unzip 基于深度学习的舌象诊断.zip -d tongue_ai_project cd tongue_ai_project find . -maxdepth 2 -type f | sort-d tongue_ai_project指定解压目录避免文件散落find是在tree可能没安装的服务器上最稳的目录查看方式。如果你看到-bash: tree: command not found就用find。解压后先不要碰代码先找三个东西配置文件、权重文件、数据目录。我经手过的这类工程包目录组织基本是下面几种常见形式的混合路径作用data/train/按类别分文件夹的训练图像data/val/验证集同样按类别分文件夹configs/train.yaml标签类别、图像尺寸、学习率、epoch 数models/网络结构定义文件weights/训练好的模型权重.pth或.ptscripts/预处理、训练、推理脚本requirements.txtPython 依赖清单拿到包后先打开configs/train.yaml不需要急着看模型结构。你首先要确认labels和img_size这两个值因为它们决定了后面所有代码怎么改。标签顺序尤其重要训练时的类别索引和labels列表顺序必须一致否则加载权重后推理结果会张冠李戴。如果包内没有train.yaml通常在某个.py文件里也有class_names [...]作用一样。2.2 跑通最小推理流程加载权重、喂一张图、输出概率推理优先于训练。你先验证这个 zip 里的模型权重是完整的再去考虑重训。下面这段代码是加载一个 ResNet 分类器的最简骨架适配configs/train.yaml中的配置。import torch import yaml from torchvision import transforms, models from PIL import Image with open(configs/train.yaml, r, encodingutf-8) as f: cfg yaml.safe_load(f) labels cfg[labels] img_size cfg[img_size] # 通常是 224 或 256 preprocess transforms.Compose([ transforms.Resize((img_size, img_size)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) model models.resnet18(num_classeslen(labels)) model.load_state_dict(torch.load(weights/tongue_classifier.pth, map_locationcpu)) model.eval() def infer(path): img Image.open(path).convert(RGB) x preprocess(img).unsqueeze(0) with torch.no_grad(): prob torch.softmax(model(x), dim1).squeeze() return dict(zip(labels, prob.tolist())) sample infer(data/val/淡红舌/001.jpg) for k, v in sorted(sample.items(), keylambda item: -item[1]): print(f{k}: {v:.2%})注意第10行用的是models.resnet18(num_classes...)而不是pretrainedTrue。加载 zip 包自带的权重时pretrainedTrue会先从公网下载一套 ImageNet 权重再被你本地的.pth覆盖掉既浪费流量又可能因为网络问题失败所以直接只留结构。map_locationcpu的意义是即使这个权重是在 GPU 机器上训的你也可以在没有 GPU 的机器上先跑通流程。如果load_state_dict报缺少键或尺寸不匹配绝大多数原因是num_classes和配置里的labels数量不一致少数情况是模型结构不是 ResNet18需要打开models/下的结构定义确认。2.3 安装依赖别用全局环境虚拟环境和 requirements.txt 才是正解很多 zip 工程自带requirements.txt但不会带 Python 环境。直接pip install -r requirements.txt一般会踩到 PyTorch 版本和 CUDA 不匹配的坑。我一般会分两步装。conda create -n tongue python3.10 -y conda activate tongue pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install -r requirements.txt这里故意先用 CPU 版 PyTorch 打底。原因很简单先让代码跑起来之后再按 GPU 情况替换。requirements.txt里除了 torch通常还有opencv-python、albumentations、scikit-learn、matplotlib、pyyaml。如果直接一次性安装PyTorch 体积大容易因为网络中断导致整个安装失败。分开装之后即使后面几个包失败PyTorch 已经可用排查范围更小。注意如果你的机器有 NVIDIA GPU并且nvidia-smi显示驱动支持的 CUDA 版本是 12.x可以把第一行换成--index-url https://download.pytorch.org/whl/cu121。这一步的细节在“避坑”章节还会再提现在先保证 CPU 能跑通目标是把 zip 包里的东西变成一个能用的模型而不是先纠结性能。3. 训练舌象分类模型ResNet 迁移学习的参数怎么调才算没白跑3.1 数据预处理舌象图像裁剪、颜色校正与增强舌象照片和常规 ImageNet 图片有一个大差别颜色就是特征。同一个舌头用不同手机、在不同色温的灯光下拍出来RGB 均值可能相差很大。所以预处理环节先做颜色校正是必要的。常见做法是一个简单的白平衡把 RGB 三个通道的均值拉到同一个水平抵消整体偏色。import cv2 import numpy as np def white_balance_avg(img): out img.copy().astype(np.float32) for c in range(3): mean_val img[:, :, c].mean() out[:, :, c] np.clip(out[:, :, c] * (128.0 / (mean_val 1e-6)), 0, 255) return out.astype(np.uint8) def center_crop(img, ratio0.8): h, w img.shape[:2] side int(min(h, w) * ratio) y (h - side) // 2 x (w - side) // 2 return img[y:y side, x:x side]这种白平衡假设图像的平均色是灰色对舌象采集场景来说够用但如果背景里有一大块深色衣物或深蓝色隔帘假设会被破坏。想更严谨就得在采集时放一张标准灰卡用灰卡像素做校正。中心裁剪的ratio0.8是为了把嘴唇和下巴裁掉大部分因为这类区域和舌体的颜色接近会干扰分类器。增强这一步我一般用albumentations而不是torchvision.transforms原因是后续如果做舌体分割它支持 mask 和 bbox 同步变换一个库就够了不用维护两套代码。import albumentations as A train_transform A.Compose([ A.HorizontalFlip(p0.5), A.ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit10, p0.5), A.ColorJitter(brightness0.15, contrast0.15, saturation0.1, hue0.02, p0.3), ])rotate_limit10而不是 30 或 90因为舌象有很强的空间先验舌根在下舌尖在上。旋转过大模型会学到无意义的旋转不变性。hue0.02很克制因为色相稍微飘一点淡红舌和红舌的边界就乱了。如果你的数据集只有几百张增强可以开大一点如果过拟合还明显后面可以加随机遮挡或 Mixup但不要第一步就把增强拉满否则验证集准确率会显得很挣扎。3.2 用 ResNet 做迁移学习训练脚本与关键参数选 ResNet18 而不是 ResNet50是因为舌象诊断项目的数据集规模通常很小几十到几百张。ResNet50 参数多在几千张图上的表现可能更好但在几百张图上很容易过拟合。ResNet18 在 ImageNet 上学到的基础纹理特征已经足够只需要把最后的全连接层换成你的类别数。先定义一个按文件夹名自动生成标签的数据集类。import os from PIL import Image from torch.utils.data import Dataset class TongueDataset(Dataset): def __init__(self, root, transformNone): self.classes sorted([d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))]) self.class_to_idx {c: i for i, c in enumerate(self.classes)} self.pairs [] for cls in self.classes: cls_dir os.path.join(root, cls) for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): self.pairs.append((os.path.join(cls_dir, fname), self.class_to_idx[cls])) self.transform transform def __len__(self): return len(self.pairs) def __getitem__(self, idx): path, label self.pairs[idx] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) return img, label这里有个小坑文件夹名如果是中文“淡红舌”Python 能处理但跨平台传到 Windows 或 macOS 时编码容易出问题尤其是 zip 包已经是第二次传递的时候。我习惯在训练阶段把文件夹重命名为danhong、danbai这样的拼音或英文训练完再映射回中文标签。训练循环的核心参数集中在下面这段。这里用evaluate函数先说明后面训练循环里直接调用。def evaluate(model, loader, device): model.eval() correct total 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) correct (outputs.argmax(1) labels).sum().item() total labels.size(0) return correct / total import torch import torch.nn as nn from torchvision import models from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(pretrainedTrue) model.fc nn.Linear(model.fc.in_features, len(train_dataset.classes)) model model.to(device) num_epochs 30 best_acc 0 patience, wait 10, 0 criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxnum_epochs) for epoch in range(num_epochs): model.train() total_loss, correct, total 0.0, 0, 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) correct (outputs.argmax(1) labels).sum().item() total labels.size(0) scheduler.step() train_acc correct / total val_acc evaluate(model, val_loader, device) print(fepoch {epoch1:02d} | loss {total_loss/total:.4f} | train_acc {train_acc:.3f} | val_acc {val_acc:.3f}) if val_acc best_acc: best_acc val_acc wait 0 torch.save(model.state_dict(), weights/tongue_best.pth) print( save new best model) else: wait 1 if wait patience: print(fearly stop at epoch {epoch1}) breakpretrainedTrue在这里是对的训练阶段用迁移学习正是要 ImageNet 上预训练权重作为起点让模型一开始就具备纹理和边缘提取能力。lr1e-4适合对整个网络做微调如果你只想训练最后的全连接层、冻结前面所有层可以改成lr1e-3但舌象分类任务通常全量微调效果更好。weight_decay1e-4就是 PyTorch 里 L2 正则化的等价实现对几百张的小样本数据集来说这比把模型换成更大的更好使。CosineAnnealingLR会在 30 个 epoch 内把学习率从 1e-4 平滑降到接近 0适合固定 epoch 的训练方案。配合早停当你设不住最优 epoch 时它能避免模型在训练后期因为学习率过大而跳过最优解。3.3 训练曲线与早停策略怎么判断模型没学歪训练跑完不要只盯着最后一个 epoch 的准确率把 loss 和 val_acc 曲线画出来。import matplotlib.pyplot as plt plt.figure(figsize(8, 5)) plt.plot(range(1, len(train_losses) 1), train_losses, labeltrain_loss) plt.plot(range(1, len(val_accs) 1), val_accs, labelval_acc) plt.xlabel(epoch) plt.ylabel(value) plt.legend() plt.grid(True, alpha0.3) plt.savefig(training_curves.png, dpi150)舌象项目最常见的曲线形态是train_loss 一路向下val_acc 在某个 epoch 后开始震荡甚至下滑。这是典型的过拟合信号此时早停已经生效保存的是 val_acc 最高的权重而不是最后一次迭代的权重。反过来如果训练从一开始 loss 就降不下去先别调模型结构回去看数据。检查是不是类别文件夹里有损坏图片、增强后图像是不是黑块、标签顺序是不是和配置文件不一致。这类问题在数据只有几十张的小项目里出现的概率远比模型结构不先进高。4. 舌体分割与特征提取把分类器从“看整张图”改成“只看舌头”4.1 舌体分割U-Net 与二值 mask 推理分类器直接吃整张图很容易把嘴唇、牙齿、甚至照片背景里的桌子当成预测依据。如果模型的热区落在这些地方准确率再高也不能上线。所以成熟方案是先分割舌体得到二值 mask再让分类器只看到舌头区域。U-Net 是医学图像分割的默认选择原因很简单它用跳层连接把编码器的高层语义和低层边界细节拼在一起小数据集下比深卷积网络更好训练。推理时你需要一个能输入原图、输出同尺寸 mask 的函数。import torch import torch.nn.functional as F import cv2 import numpy as np def segment_tongue(seg_model, image_bgr, devicecpu): img cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) img cv2.resize(img, (256, 256)) / 255.0 x torch.from_numpy(img).permute(2, 0, 1).float().unsqueeze(0).to(device) with torch.no_grad(): pred torch.sigmoid(seg_model(x))[0, 0].cpu().numpy() mask cv2.resize(pred, (image_bgr.shape[1], image_bgr.shape[0])) mask (mask 0.5).astype(np.uint8) kernel np.ones((3, 3), np.uint8) mask cv2.erode(mask, kernel, iterations1) return maskU-Net 最后的输出通常是未过 sigmoid 的 logits所以推理时手动加一个torch.sigmoid。threshold0.5是默认值如果 mask 偏大或偏小可以在验证集上调到 0.4 或 0.6。腐蚀 1 像素是为了消掉舌体边缘半透明过渡区域这类区域颜色往往混合了嘴唇和舌头的特征保留它们会把噪声传给后续的颜色统计。如果包里的训练代码是 U-Net你可以看到它的 loss 常用 Dice Loss因为 mask 里背景像素远多于前景交叉熵会让模型偏向预测背景。Dice Loss 的写法有很多核心是把预测概率图和真实 mask 的像素重叠程度作为指标公式里加 1 防止分子分母同时为 0。4.2 颜色与纹理特征量化舌色、苔色、齿痕不再“只可意会”分割出舌体之后下一步是量化舌色和舌苔占比。常见做法是把图像转到 HSV 颜色空间因为 HSV 的色相、饱和度、亮度分得更开比直接用 RGB 阈值更符合人眼对舌色的判断。hsv cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV) mask_region mask 0 white_m (hsv[:, :, 1] 30) (hsv[:, :, 2] 160) mask_region yellow_m (hsv[:, :, 0] 40) (hsv[:, :, 0] 20) mask_region red_m ((hsv[:, :, 0] 10) | (hsv[:, :, 0] 170)) (hsv[:, :, 1] 80) mask_region tongue_pixels mask_region.sum() print(f白色舌苔占比: {white_m.sum() / tongue_pixels:.2%}) print(f黄色舌苔占比: {yellow_m.sum() / tongue_pixels:.2%}) print(f红色舌质占比: {red_m.sum() / tongue_pixels:.2%})这些阈值来自经验不是标准答案。不同项目的光照条件不同你需要准备 20 到 30 张已经标注好舌色类别的小图用滑块调阈值手动看到大多数图结果和标注一致再定下来。注意red_m的条件HSV 里红色分布在 0 到 10 度以及 170 到 180 度两端所以用了or把两端包进来。齿痕检测比舌色更麻烦。快速原型里可以用轮廓和凸包之间的凹陷区域来估计。contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnt max(contours, keycv2.contourArea) hull cv2.convexHull(cnt) hull_img cv2.drawContours(np.zeros_like(mask), [hull], -1, 255, 2) diff cv2.absdiff(mask, hull_img) bite_count max(1, (diff 0).sum() // 200)这段逻辑是启发式的有齿痕的舌体边缘会有波浪形凹陷凹陷区域会让轮廓和凸包之间出现明显差异。bite_count的计算非常粗糙想让它真正可用更可靠的是人工标注齿痕位置训练一个关键点回归模型或者用形态学开运算后统计边缘的凹点数量。但作为从 zip 包跑通流程的第一步它已经能给出一个数值让你知道“齿痕多不多”。4.3 串成一条推理 pipeline分割、特征、分类一步到位两个模型单独跑只是验证效果实际使用时要串成一条命令。入口脚本接收一张图片路径输出一份包含分割面积、颜色占比、分类结果的 JSON。def run_pipeline(image_path): image cv2.imread(image_path) mask segment_tongue(seg_model, image) hsv cv2.cvtColor(image, cv2.COLOR_BGR2HSV) mask_region mask 0 white_ratio float(((hsv[:, :, 1] 30) (hsv[:, :, 2] 160) mask_region).sum() / mask_region.sum()) yellow_ratio float(((hsv[:, :, 0] 40) (hsv[:, :, 0] 20) mask_region).sum() / mask_region.sum()) ys, xs np.where(mask 0) x_min, x_max xs.min(), xs.max() y_min, y_max ys.min(), ys.max() pad 10 crop image[max(0, y_min-pad):y_maxpad, max(0, x_min-pad):x_maxpad] cls_result classify_tongue(crop) return { mask_area: int(mask.sum()), white_tongue_ratio: round(white_ratio, 4), yellow_tongue_ratio: round(yellow_ratio, 4), tongue_color: cls_result }分类前先根据 mask 的包围盒裁剪往外扩 10 个像素是为了避免把舌头贴边裁掉。这个 pipeline 是稳定的基础版本之后无论做 Web API 还是打包成桌面工具核心逻辑都不会变。5. 舌象诊断项目避坑数据、环境、小样本三大翻车点5.1 中文文件名乱码数据集索引直接失败现象在 Ubuntu 上解压 zip 后训练脚本报FileNotFoundError看目录里文件名是或乱码。 原因Windows 压缩 zip 时中文文件名按 GBK 编码保存而 Linux 默认按 UTF-8 解码两边对不上。 解决不要用图形工具解压改用命令unzip -O gbk。如果系统的 unzip 不支持-O参数用 Python 重新解压。import zipfile import os with zipfile.ZipFile(基于深度学习的舌象诊断.zip, r) as z: for info in z.infolist(): raw info.filename.encode(cp437) try: decoded raw.decode(gbk) except UnicodeDecodeError: decoded info.filename target os.path.join(out, decoded) os.makedirs(os.path.dirname(target), exist_okTrue) with z.open(info) as src, open(target, wb) as dst: dst.write(src.read())zipfile读取非 UTF-8 文件名时会把原始字节按 cp437 解码成字符串。所以先用encode(cp437)还原字节再用gbk解码得到正确的中文名。注意如果原 zip 是 macOS 压缩的编码可能是 UTF-8 或 Big5解码失败时保留原名即可不要直接抛异常。这个坑几乎每个中文 zip 工程包都会遇到处理掉它后面才顺。5.2 CUDA 版本不匹配训练直接“黑匣子”现象训练脚本里torch.cuda.is_available()返回 False或者训练到一半报CUDA out of memory看不到任何有效报错。 原因最常见是 pip 默认装了 CPU 版 PyTorch或者 PyTorch 要求的 CUDA 版本比当前驱动支持的最高版本更高导致运行时找不到可用的 CUDA 设备。 解决分两步排查。import torch print(torch.__version__) print(torch.version.cuda) print(torch.cuda.is_available())如果is_available()为 False先跑nvidia-smi看右上角显示的最高 CUDA 版本。比如驱动支持最高到 12.1就用 cu121 的 PyTorch。pip uninstall -y torch torchvision pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121不要盲目装最新的 cu128。驱动版本不够PyTorch 装得再新也白搭。如果只是显存不够把 batch size 从 32 降到 16同时把DataLoader的pin_memoryTrue改成 False通常能缓解。遇到这类问题先看环境和版本不要怀疑模型结构。5.3 类别不均衡导致准确率虚高现象训练日志显示 val_acc 到了 90%但看每个类别的召回率发现“淡白舌”只有 20%“淡红舌”接近 98%。 原因数据集里“淡红舌”占了大半模型把大多数样本都预测成“淡红舌”就能拿到很高准确率但少数类根本没学会。 解决用WeightedRandomSampler让少数类在同一个 epoch 里被多采样几次。from torch.utils.data import WeightedRandomSampler labels [label for _, label in train_dataset.pairs] counts torch.bincount(torch.tensor(labels)) weights 1.0 / counts[labels].float() sampler WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplersampler)注意weights是每个样本的权重而不是每个类别的权重。1/count会把极端少的类采样频率拉得很高容易让模型反复看同一张图。如果少数类图像不足 20 张单纯加重采样很容易过拟合这时要配合更强的数据增强或者干脆把极端稀有类合并到上一级类别比如把“淡白舌”和“淡红舌”合并成“舌色浅”。准确率是平均指标单类召回率才是上线前要盯的。5.4 分割标注边界不一致模型学不到干净边缘现象同一张舌象图两个人标注出的舌体 mask重合率只有 70%训练后的 U-Net 分割结果边缘抖动时大时小。 原因舌体与下唇之间没有明显突变边界标注人员各自的判断标准不同。 解决在标注规范里约定“舌体到口腔内唇侧边缘为止”不把嘴唇包进来。训练时对 mask 做 1 像素腐蚀让边界附近的错误预测承担更低的梯度。另外Dice Loss 比 BCE Loss 更适合这种前景远小于背景的任务因为 Dice 直接优化重叠率不受背景像素数量影响。如果标注数据是外包或多人协作做的还要建一个“交叉复核”流程随机抽 10% 的图像由第二个人重新标注算一次 Dice 系数低于 0.9 就退回。舌象分割的模型上限基本由标注一致性决定模型结构反而是次要因素。6. 模型验证与落地用混淆矩阵和 Grad-CAM 给诊断结果一个“后悔药”6.1 混淆矩阵比整体准确率诚实分类模型的 val_acc 能到 95%听起来不错但它不告诉你哪两类在互相打架。舌象诊断里最常见的是“淡红舌”和“红舌”被混淆因为颜色本来就是渐变采集时偏一点色温就跨类了。用混淆矩阵把这个问题逼到台面上。from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt cm confusion_matrix(all_labels, all_preds, labelsrange(len(class_names))) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labelsclass_names) disp.plot(cmapBlues) plt.xticks(rotation45) plt.savefig(confusion_matrix.png, bbox_inchestight, dpi150)all_labels和all_preds是在验证集上累积出来的数组。看混淆矩阵时我优先看对角线之外哪一格最大而不是看整体准确率。如果某两类几乎分不开先去回看它们对应的原始图片确认是不是拍摄光线差异造成的。如果是优先做颜色校正如果是标注本身的灰色地带就考虑把两个类别合并别让模型硬学。6.2 Grad-CAM 看模型是不是在“看舌头”准确率达标只是开始。你还需要知道模型做决定时眼睛盯着哪块区域。Grad-CAM 能生成热区图可视化模型对输入图像的注意力位置。from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image import cv2 target_layers [model.layer4[-1]] cam GradCAM(modelmodel, target_layerstarget_layers) rgb_img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) / 255.0 cam_mask cam(input_tensorx, targetsNone)[0] visual show_cam_on_image(rgb_img, cam_mask, use_rgbTrue) cv2.imwrite(grad_cam.png, visual)target_layers选模型最后一个残差块因为它的特征图保存了最多的高层语义。如果你用的是 ResNet50同样取model.layer4[-1]。如果热区集中在舌体内部说明模型学到了真实特征如果热区一直落在嘴唇、牙齿或背景桌面上那说明分类器找到了数据中的捷径比如用背景颜色判断类别。遇到这种场景加分割 mask 再训练比继续调学习率有意义得多。我现在做完一个舌象诊断模型最后一步永远是这两件事先打印混淆矩阵看哪两个类分不开再跑一批 Grad-CAM 图排除背景伪相关。如果这两关过不了我不会把模型放进服务里。这种谨慎不是怕翻车而是医学影像的模型一旦上线用户会拿它当真你总得给自己留一颗后悔药。希望帮到你。本文还有配套的精品资源点击获取