
简介这是一份面向医学图像分类任务的已标注数据集聚焦肾脏结节和肿瘤的自动识别包含正常、结节、肿瘤三个分类整体已按训练集、验证集、测试集拆分并存放于独立文件夹内可直接作为分类网络或yolov5分类任务的数据输入。压缩包共2000个文件其中1998个为JPG格式的肾脏影像1个Python脚本用于数据集可视化1个JSON文件记录类别字典与索引映射压缩包大小约151.5MB。目前已有299人浏览学习。数据集结构清晰训练、验证、测试三个目录分别存放对应图像便于直接开展模型训练与效果评估JSON文件可帮助快速查看类别对应关系可视化脚本支持批量展示样本适合在训练前检查数据质量与类别分布。对于医学图像分析学习者、算法工程师以及需要进行肾脏结节、肿瘤识别研究的团队这是一份开箱即用的标准分类数据集。1. 拿到一份划分好的肾脏结节数据最先该做什么医学图像分类看起来是个标准的深度学习任务但真拿到一份“肾脏结节、肿瘤数据图集”并开始训练时很多人才发现卡壳的地方根本不在模型而在数据组织、类别定义和评估口径上。常见的痛点是数据是文件夹保存的但文件夹排序和训练时的类别 id 对不上验证集准确率高达 99%到了医院新数据上却完全失效训练时 Loss 不降你以为是网络结构问题其实只是预处理里灰度图复制成了三通道却不配套。这篇笔记按一线工程师的流程来拆先讲怎么把“划分好的文件夹 类别字典文件”这套结构用到位再讲 ResNet 迁移学习的完整训练配置最后落在敏感度、特异度和 Grad-CAM 可视化这些医生真正关心的输出上。每一步都写成可以直接照着跑的代码。2. 数据组织与类别字典先把训练集、验证集目录和 class_to_idx 定死2.1 先定类别边界单标签还是多标签结节、良性恶性囊肿怎么分医学图像分类的第一步不是写训练脚本而是建立一套让影像科医生认可、程序员又能落地的类别体系。肾脏数据常见的有三种划分思路最简单的二分类是“肾脏肿瘤”和“正常组织”适合快速做分诊临床上更常用的是三类即正常、良性病变比如血管平滑肌脂肪瘤、单纯囊肿和恶性病变如肾细胞癌这需要病理结果做标注基准再细一点还可以按病理亚型分比如透明细胞癌、乳头状细胞癌、嫌色细胞癌类间形态差异更细微对数据量和图像分辨率的要求也更高。这里要特别提醒一件事“结节”和“肿瘤”不是同一个维度。结节是影像学术语凡是超声或 CT 上发现的局灶性占位都能叫结节而肿瘤是病理或临床诊断术语标注时必须依赖活检或手术病理作为金标准。如果你手上的数据只有影像没有病理标签那只能算“检测或者分类”任务不能声称是“良恶性诊断”。我在做类别字典时一般会把类别命名写成可读性强的英文短词比如kidney_normal、kidney_benign、kidney_tumor而不是用 0、1、2 直接命名文件夹后续写可视化代码一眼就能看懂。类别体系确定后还要考虑数据合规。公开的肾脏影像数据集如 KiTS19、TCGA-KIRC 可以用于学术研究但下载前要仔细阅读使用许可注意是否允许重新分发和用于商用院内自采数据则必须过伦理审批和脱敏流程训练时只处理脱敏后的 PNG/JPG不直接碰原始 DICOM 中的患者身份信息。2.2 数据划分用分层抽样按类别比例划分不要随手 shuffle拿到划分好的数据第一件事是验证它划分的是否合理。很多人习惯直接把所有图片读完再train_test_split但医学数据里最常见的错误是随机划分导致类别比例漂移。比如肿瘤样本只占 5%随机分 80% 进训练集验证集里可能只剩 1% 的肿瘤样本模型压根没见到足够多样的病灶形态评估结果自然不可信。正确做法是分层抽样也就是按类别比例把数据分到训练、验证、测试三个文件夹。import os import json import shutil from sklearn.model_selection import train_test_split data_root data/all_images train_target data/train val_target data/val test_target data/test # 读取全部类别按字母序排序保证可复现 categories sorted([name for name in os.listdir(data_root) if os.path.isdir(os.path.join(data_root, name))]) # 扫描所有文件记录文件名与类别 files, labels [], [] for cls in categories: cls_dir os.path.join(data_root, cls) for fname in os.listdir(cls_dir): if fname.lower().endswith((.png, .jpg, .jpeg)): files.append(os.path.join(cls, fname)) labels.append(cls) # 第一步按类别比例分出 80% 训练20% 临时 train_files, tmp_files, train_labels, tmp_labels train_test_split( files, labels, test_size0.2, stratifylabels, random_state42) # 第二步临时集对半分得到 val 和 test val_files, test_files, val_labels, test_labels train_test_split( tmp_files, tmp_labels, test_size0.5, stratifytmp_labels, random_state42) # 第三步复制文件到目标文件夹 for split_name, split_files in [(train, train_files), (val, val_files), (test, test_files)]: for rel_path in split_files: src os.path.join(data_root, rel_path) dst os.path.join({train: train_target, val: val_target, test: test_target}[split_name], rel_path) os.makedirs(os.path.dirname(dst), exist_okTrue) shutil.copy2(src, dst) # 第四步生成类别字典 class_to_idx {cls: i for i, cls in enumerate(categories)} with open(class_to_idx.json, w) as f: json.dump(class_to_idx, f, indent2) print(类别字典:, class_to_idx) print(各划分数量:, len(train_files), len(val_files), len(test_files))这段代码里有几个参数值得说明。stratifylabels是分层抽样的关键它保证训练集、验证集、测试集中每个类别的占比和原始数据总体保持一致在肿瘤样本只占 5% 时特别重要。random_state42固定随机种子让每次划分结果一致方便复现实验。test_size0.2是在总量中预留 20% 给验证和测试再把这 20% 对半分得到 10% 验证、10% 测试。如果数据集总量很小比如每个类别只有几百张可以考虑把验证和测试各留 5%剩余 90% 训练或者用 K 折交叉验证代替单次划分。生成class_to_idx.json用到了遍历文件夹名称的方式这里有个易错点os.listdir返回的顺序不保证稳定所以我用sorted()强制按字母序排列保证每次运行生成的 id 一致。这一点看起来小但一旦你重新生成字典文件时顺序变了之前保存的模型权重就作废了。这个细节我会在避坑章节再展开。2.3 类别字典文件ImageFolder 的天然搭档推理时必须复用同一份PyTorch 的torchvision.datasets.ImageFolder天然支持“文件夹保存”的数据组织方式它会把每个子文件夹当作一个类别并按字母序给类别排序自动生成class_to_idx属性。我一般会在数据准备脚本里主动把class_to_idx导出成 JSON 文件而不是依赖 ImageFolder 每次现场计算。原因在于训练时用它保证数据加载顺序一致验证时用它重排标签推理时直接读取它反查预测类别全流程用的都是同一份 id 映射谁也不会错位。注意class_to_idx.json是训练产物也是模型的一部分。你要把它和权重文件放在同一个目录下最好命名带版本号例如class_to_idx_v2.json对应resnet18_kidney_v2.pt不要后期手工去改 JSON 内容。如果新增了一个类别正确的做法是重新生成整个数据目录和字典文件从头训练而不是在旧 JSON 里追加一条。因为新增类别会导致模型输出维度变化旧权重无法用于新模型。文件命名规范上我建议直接用英文和数字命名图片比如kidney_tumor_001.png避免中文文件名在跨平台传输时出现乱码。同时文件名里不要带空格和特殊符号否则某些数据加载库解析时可能报错或者在 torchvision 的DatasetFolder扩展逻辑里触发不可预期的过滤规则。这些是数据版图里最小的坑但往往是进场第一周就卡住人的地方。3. 模型与训练配置ResNet 迁移学习在肾脏影像上的完整动作3.1 为什么选 ResNet 而不是一上来就上 ViT肾脏结节和肿瘤图像分类的数据规模通常不大公开数据集加上自采数据也就几千张到几万张这个量级对 Vision Transformer 这类大模型并不友好容易过拟合而且预训练权重的分布和医学灰度图差异很大训练起来很玄学。ResNet18 或 ResNet50 是更稳的选择预训练权重在 ImageNet 上充分训练过迁移到医学影像可以显著降低对数据量的需求torchvision 自带实现没有额外依赖Grad-CAM 等可解释性工具对 ResNet 的卷积特征图支持成熟。ResNet18 更适合快速验证ResNet50 在计算资源允许时能带来少量提升。如果数据量很少建议用 ResNet18 加冻结主干权重只训练最后的全连接层。# 检查 GPU 可用性和显存 nvidia-smi # 预期输出里能看到显卡型号和显存占用CUDNN 版本要跟 PyTorch 匹配3.2 数据增强方法在医学影像上的取舍先翻转旋转再考虑对比度扰动医学图像的数据增强和自然图像不太一样核心原则是“不能改变病灶的形态学语义”。随机裁剪、缩放这类增强在自然图像里很常见但在肾脏 CT 中如果裁剪区域偏移过多等于把肾脏和周边组织混在一起破坏了解剖结构模型学到的特征反而是噪声。我一般用全局 Resize 加轻度翻转旋转以及轻微的亮度和对比度扰动。from torchvision import transforms # 训练集增强 transform_train transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.1, contrast0.1), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证集和测试集只做拉伸和归一化不做任何增强 transform_eval transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里有一处医学图像特有的细节很多 CT 数据是单通道灰度图但 ResNet 预训练权重要求三通道 RGB 输入。常规做法是在数据读取阶段把灰度图复制三份拼成三通道。注意ColorJitter里不要动hue和saturation这两个参数对灰度图或伪彩图没有意义反而可能引入不存在的色彩分布干扰模型对纹理特征的判断。如果你手上的数据是从 DICOM 转出来的 PNG还要确认一下是窗口化后的数值还是原始像素值尽量统一预处理流程。3.3 模型微调替换全连接层、冻结主干、分层设置学习率import torch import torch.nn as nn from torchvision import models # 加载预训练 ResNet18 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 替换最后一层全连接输出改为本任务的类别数 num_classes len(class_to_idx) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) # 冻结主干参数在小数据集上先用固定特征训练分类头 for name, param in model.named_parameters(): if fc not in name: param.requires_grad False # 只把 fc 层参数送入优化器降低显存占用和过拟合风险 optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) # 选择 GPU 或 CPU device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device)这段代码的关键在于param.requires_grad False。冻结主干意味着卷积层不参与梯度更新只训练最后的全连接层数据量不足时这是最稳妥的方案。如果你数据量超过一万张可以解冻最后一两个残差块参与微调让模型适应医学图像的纹理细节但要小心过拟合。fc的输入维度来自model.fc.in_features不要硬编码成 512因为不同版本的 ResNet 输出维度不一样硬编码会在换模型时翻车。训练循环本身和通用分类任务没有太大区别但建议加两样东西按类别权重加权损失函数以及早停机制。from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_set ImageFolder(data/train, transformtransform_train) val_set ImageFolder(data/val, transformtransform_eval) # 类别不平衡时给损失函数加权重比如肿瘤样本是 5%权重就按比例调 labels [train_set.targets[i] for i in range(len(train_set))] class_counts torch.bincount(torch.tensor(labels)).float() class_weights 1.0 / class_counts class_weights class_weights / class_weights.sum() loss_fn nn.CrossEntropyLoss(weightclass_weights.to(device)) train_loader DataLoader(train_set, batch_size16, shuffleTrue, num_workers4) val_loader DataLoader(val_set, batch_size16, shuffleFalse, num_workers4) best_val_acc 0.0 patience 10 bad_epochs 0 for epoch in range(50): model.train() train_loss 0.0 for images, targets in train_loader: images, targets images.to(device), targets.to(device) optimizer.zero_grad() outputs model(images) loss loss_fn(outputs, targets) loss.backward() optimizer.step() train_loss loss.item() * images.size(0) model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, targets in val_loader: images, targets images.to(device), targets.to(device) outputs model(images) preds outputs.argmax(dim1) val_correct (preds targets).sum().item() val_total targets.size(0) val_acc val_correct / val_total print(fEpoch {epoch1:02d}, Train Loss: {train_loss / len(train_set):.4f}, Val Acc: {val_acc:.4f}) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), resnet18_kidney_best.pt) bad_epochs 0 else: bad_epochs 1 if bad_epochs patience: print(Early stopping at epoch, epoch 1) breakclass_weights的计算用的是1 / class_counts再归一化让少数类样本的损失贡献相对更大。早停的 patience 设为 10意味着连续 10 个 epoch 验证准确率不提升就停止避免在训练集上过拟合后验证指标一路恶化。保存模型时只存state_dict()而不是整个模型对象方便后续迁移到不同定义环境。模型文件名带上任务名和最优验证指标这是我自己踩过找不到权重对应数据版本的亏之后养成的习惯。3.4 训练参数参考表按数据量调整参数小数据集每类 1000中等数据集每类 1000~5000说明模型ResNet18ResNet18 / ResNet50数据量太小不要用大模型输入尺寸128x128 或 224x224224x224128 提速明显但细节较差batch size8 / 1616 / 32显存不够先减 batch别动图像尺寸学习率1e-3仅 fc1e-4解冻部分层微调阶段用小学习率优化器AdamAdam / SGDSGDMomentum 收敛更稳但需要调整周期训练轮数30~5050~80配合早停数据增强轻量翻转旋转常规增强增强过重会让小数据集更难拟合4. 评估与可解释性敏感度、特异度和 Grad-CAM 才是给医生看的证据4.1 为什么准确率在医学分类里不够用影像科医生和算法工程师关注点不一样。工程师习惯用准确率衡量模型但肾脏肿瘤在筛查人群中占比可能不到 5%一个全预测“正常”的模型也能轻松获得 95% 的准确率在指标上好看在临床上却毫无价值。肿瘤被漏掉会造成严重延误正常肾脏被误报又会带来不必要的穿刺和检查。所以医学评估必须看敏感度和特异度。敏感度等于真阳性除以真阳性加假阴性衡量的是“肿瘤患者中有多大比例被正确发现”特异度等于真阴性除以真阴性加假阳性衡量的是“正常人有 多大比例没有被误报”。只有两个指标一起用才能判断一个模型是否真正可用。4.2 评估脚本混淆矩阵、分类报告和 ROC-AUCimport torch from torchvision.datasets import ImageFolder from torchvision import transforms from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score import numpy as np # 加载测试集 transform_eval transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) test_set ImageFolder(data/test, transformtransform_eval) # 加载最优权重 model.load_state_dict(torch.load(resnet18_kidney_best.pt, map_locationcpu)) model.eval() all_labels [] all_probs [] all_preds [] with torch.no_grad(): for images, targets in DataLoader(test_set, batch_size32, shuffleFalse): outputs model(images) probs torch.softmax(outputs, dim1) preds probs.argmax(dim1) all_probs.extend(probs[:, 1].numpy()) # 假设类别 1 是肿瘤 all_preds.extend(preds.numpy()) all_labels.extend(targets.numpy()) print(混淆矩阵:) print(confusion_matrix(all_labels, all_preds)) print(\n分类报告:) print(classification_report(all_labels, all_preds, target_namestest_set.classes)) print(ROC-AUC:, roc_auc_score(all_labels, all_probs))注意all_probs里取的是probs[:, 1]这是假设类别 1 是肿瘤适用于二分类。如果你的类别超过两类AUC 需要做 OvR一对多处理比如roc_auc_score传入multi_classovr。classification_report会输出每一类的精确率、召回率和 F1其中召回率就是敏感度。医生一般关心肿瘤这一行的召回率如果这个数字低于 90%模型基本不能进入辅助诊断流程。4.3 用 Grad-CAM 让模型说出“模型看的是肾还是肾周脂肪”模型表现好还不够医生要看到模型“为什么这样判断”。Grad-CAM 是最常用的可视化手段思路是对最后一个卷积层的特征图按梯度加权求和生成反映模型关注区域的热力图。肾周脂肪和肿瘤在 CT 上可能亮度接近模型有时会把脂肪误学成关键特征热力图能帮你快速发现这种错误。import cv2 import numpy as np def grad_cam(model, input_tensor, device, target_layer): 简化版 Grad-CAM只支持单张图像输入 grads {} activations {} def forward_hook(module, input, output): activations[value] output.detach() def backward_hook(module, grad_input, grad_output): grads[value] grad_output[0].detach() hook1 target_layer.register_forward_hook(forward_hook) hook2 target_layer.register_full_backward_hook(backward_hook) output model(input_tensor.unsqueeze(0).to(device)) model.zero_grad() class_idx output.argmax(dim1).item() output[0, class_idx].backward() act activations[value].squeeze(0) # [C, H, W] grad grads[value].squeeze(0) # [C, H, W] weights grad.mean(dim(1, 2), keepdimTrue) # 全局平均池化得到权重 cam (weights * act).sum(dim0).cpu().numpy() cam np.maximum(cam, 0) # ReLU 截断负值 cam cv2.resize(cam, (224, 224)) # 缩放到与原图一致 cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) return camregister_forward_hook和register_full_backward_hook分别捕获目标层的输入输出和反向传播梯度。weights * act是加权求和的过程最后归一化到 0~1 区间。这里用的目标层一般是model.layer4[-1]也就是最后一个残差块的输出特征图空间分辨率 7x7能保留足够的细节又不过于碎片化。可视化时把热力图叠加到原图上用半透明效果展示医生一眼就能看出模型到底是在看病灶还是看旁边的血管。register_full_backward_hook是 PyTorch 1.8 之后推荐使用的接口老代码里的register_backward_hook在 1.8 之后会告警在某些版本里还不生效。如果你用的是旧版本 PyTorch建议先升级到 2.x。5. 避坑与排查肾脏分类项目里最常翻车的五个场景5.1 推理时预测结果全乱类别 id 完全错位现象训练集上验证准确率正常但拿到独立测试文件夹做推理时预测出来的类别名和图像内容完全对不上。明明看起来是肾脏肿瘤的图预测结果却显示 normal。原因训练时用的是ImageFolder自动按字母序生成的class_to_idx但推理脚本里手工写了一个类别顺序列表比如把kidney_tumor放在了索引 0 的位置。两个 id 映射不一致模型输出的 logits 索引和现实类别自然对不上。解决把class_to_idx.json当成模型的必要组成部分和权重文件放在一起。推理脚本启动时用json.load()读入字典然后用{v: k for k, v in class_to_idx.items()}生成反向映射来显示类别名。顺带养成的检查习惯是训练结束后打印一次train_set.class_to_idx推理前打印一次读入的字典两个必须完全一致。这个排查只需要 30 秒但能省下半天。5.2 验证集准确率虚高一上医院数据就失效现象本地验证集准确率 98%医生拿一批新扫描的图像来测试准确率掉到 70% 以下而且错误集中在漏检肿瘤。原因数据泄漏。很多肾脏数据集来自 CT 或 MRI 的连续层切同一个病人的几十张切片之间高度相似。如果只是按图片随机划分部分层切会同时进入训练集和验证集模型相当于提前看到了答案。另外有些数据集做过增强增强版本混进了验证集也会导致虚高。解决划分数据时按病人 ID 分组保证同一个病人的所有切片只出现在一个划分里不能既在训练集又在验证集。做法是在文件名里带上病人编号比如patient_001_slice_023.png按下划线第二位作为 group key用GroupShuffleSplit代替train_test_split。如果原始文件名没有病人信息需要先找数据提供方补齐这一步没有捷径。5.3 训练 Loss 不降先检查预处理而不是调整网络结构现象模型训练了十几个 epochLoss 几乎不动准确率徘徊在 30%~40%和瞎猜差不多。原因两个高发问题。一是灰度图复制成三通道后没有重新统计本数据集的均值和标准差仍然沿用 ImageNet 的mean[0.485, 0.456, 0.406]。CT 图像数值分布和自然图像差异极大归一化后很多信息被压缩掉了梯度没法有效传播。二是RandomRotation角度设得过大比如 90 度导致肾脏本身的解剖方位被翻转特征失去意义。解决先用一小批数据可视化检查送入模型之前的 tensor 长什么样。具体做法是取几张图做transform然后torchvision.utils.save_image导出肉眼确认图像没有变成全黑或者细节被抹掉。更严谨的做法是计算自己数据集的 mean 和 std用全量数据扫描一遍再填进Normalize里。这两步做完再考虑是不是学习率的问题。5.4 肿瘤一个都抓不到模型全预测为正常现象训练结束后验证集的整体准确率看着有 90% 以上但单独看肿瘤那一类的混淆矩阵真阳性几乎为零敏感度只有个位数。原因类别不平衡。肿瘤样本占比太低模型发现全预测正常能得到较低 loss就走上了偷懒路线。更麻烦的是如果你按照整体准确率来做早停那些只优化多数类的模型反而被保留下来越训练越偏。解决除了在损失函数里加类别权重早停的监控指标要从准确率改成少数类的敏感度或者用 ROC-AUC 这类对类别不平衡更稳健的指标。另外可以在每次 epoch 结束后打印每一类的召回率肉眼监控少数类的变化趋势而不是只看一个整体准确率。类别权重调大后如果训练集 loss 下降到接近 0 但验证集变差说明模型过拟合了少数类此时应减少训练轮数并配合增强。5.5 换了一台电脑预测结果崩溃图像读取和预处理不一致现象训练机上预测正常把权重、字典和推理脚本拷到同事的 Windows 电脑上同样的图片预测结果完全不同。原因图像读取路径不一致。训练时用 PIL 读取 RGB 图像推理脚本用 OpenCV 的cv2.imread读取OpenCV 默认返回 BGR 通道顺序这个顺序不一致会彻底改变模型输入还有一种情况是训练时图像后缀是.png推理时拿到的是同名.jpg两种格式的压缩损耗不一样也会带来微小偏差。解决训练和推理的transforms用同一段代码复制粘贴而不是重写所有图像统一用 PIL 配合Image.open(...).convert(RGB)读取不混用 OpenCV 和 PIL。这两条做好跨机器的输入差异基本能消除。再进一步可以把 transform 序列化到训练权重同目录的 JSON 配置里推理脚本直接读配置从根本上避免两边各自维护一份预处理代码。6. 从训练到落地推理脚本与下一步怎么升级6.1 把模型、类别字典和预处理打包成一个推理脚本训练完成不等于交付。我一般会写一个独立的推理脚本把模型权重、类别字典、图像预处理全部在脚本内部装配好确保新机器上也能直接从单张图片得到结果。import json import torch from PIL import Image from torchvision import transforms from torchvision import models class KidneyPredictor: def __init__(self, weight_path, dict_path, devicecpu): self.device device with open(dict_path, r) as f: self.class_to_idx json.load(f) self.idx_to_class {v: k for k, v in self.class_to_idx.items()} num_classes len(self.class_to_idx) self.model models.resnet18(weightsNone) self.model.fc torch.nn.Linear(self.model.fc.in_features, num_classes) self.model.load_state_dict(torch.load(weight_path, map_locationdevice)) self.model.to(device) self.model.eval() def predict(self, image_path): img Image.open(image_path).convert(RGB) img_tensor transform_eval(img).unsqueeze(0).to(self.device) with torch.no_grad(): probs torch.softmax(self.model(img_tensor), dim1) pred_idx probs.argmax(dim1).item() return self.idx_to_class[pred_idx], probs[0, pred_idx].item() transform_eval transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) predictor KidneyPredictor(resnet18_kidney_best.pt, class_to_idx.json) cls, conf predictor.predict(data/test/kidney_tumor/sample.png) print(f预测类别: {cls}, 置信度: {conf:.3f})这个脚本有个容易被忽略的点num_classes是从类别字典长度推导出来的而不是硬编码。如果未来类别数量变化脚本依然能跑。transform_eval必须和训练时完全一致特别是Resize的尺寸和Normalize的参数。如果你想做更严谨的结果报告可以把输出改成按置信度排序的 Top-3 列表对医生来说比单一结果更有参考价值。6.2 从分类走向分割和更细粒度诊断这个方向的价值不止于二分类和三类分类。我见过不少团队把这套章节流程跑通后又往前迈了一步先用 YOLOv8 检测肾脏区域再把检测框裁剪出来送入 ResNet 分类相当于做了一个两阶段诊断流水线能有效过滤肾脏周围组织的干扰。更进一步是训练 U-Net 分割模型把肿瘤区域分割出来再统计体积、直径等定量特征这些指标是临床报告里真正被采纳的内容。当前大模型和医学多模态模型也在加速这个领域与其换一个大模型重新做迁移学习我更建议把数据质量、标签规范、评估口径这些基础设施先打扎实这是任何算法都绕不开的地基。我现在拿到任何一份新的分类数据第一步永远是打印一遍class_to_idx和类别分布而不是急着跑训练脚本这个习惯让我在所有项目里都少走了很多弯路。希望帮到你。本文还有配套的精品资源点击获取