十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

医学图像分类实战:19类器官细胞数据集与PyTorch加载完全指南

医学图像分类实战:19类器官细胞数据集与PyTorch加载完全指南 简介面向医学图像分类任务的19种器官细胞识别数据集涵盖肾上腺、子宫、甲状腺、食道等类别类别字典以JSON文件提供可直接用于卷积神经网络或YOLOv5分类模型训练。数据已按文件夹划分为训练集与测试集图片统一为PNG格式另附Python可视化脚本无需额外整理即可快速浏览各类别样本适合医学影像初学者快速搭建分类基准实验也能用于算法对比与论文复现。压缩包共2000个文件以1998张图片为主体辅以1个JSON类别字典和1个Python脚本整体约260.22MB目录结构清晰简洁可一键解压使用。目前已有176人学习对于需要标准医学图像分类数据集的开发者可节省大量数据收集与预处理时间直接聚焦模型设计与调优配合分类网络项目与YOLOv5分类教程可进一步延伸实践。1. 医学图像分类数据集到底值不值得用先看这 19 类细胞能帮你省掉什么做医学图像分类的课题最折磨人的往往不是模型调参而是数据集。你以为你在做深度学习图像识别实际上大部分时间在干数据处理——下载来的公开数据集要么是原始病理切片没裁剪要么几十个类别全堆在一个大文件夹里没有任何划分更别提什么类别字典文件了。这种情况我至少遇到过三次每次都是先花两周时间清洗数据实验还没开始人已经累了。所以当看到“19种器官细胞图像识别数据集”而且自带划分好的文件夹和类别字典文件时第一反应是终于有个像样的了。这个数据集解决的核心问题是两个一是把 19 类器官细胞的图像数据按训练集、验证集、测试集预先分好打开就是标准目录结构省掉了所有人都会做但所有人都不愿意做的划分工作二是提供了一个类别字典文件把文件夹名和类别 ID 对应起来这意味着你不用自己写脚本猜标签顺序直接按字典加载就能对接 PyTorch 或 TensorFlow 的训练流程。对谁最有用做医学影像分析的研究生、刚入门图像识别的算法工程师、以及需要在短时间内出一个分类 baseline 的人。数据量不大但结构完整适合把精力放在模型和实验上而不是耗在数据整理上。2. 拿到数据集先摸清家底目录结构、类别字典与标签映射2.1 文件夹划分结构的标准读法train、val、test 到底怎么组织这个数据集最良心的设计就是数据按文件夹保存且已经划分完毕。下载解压之后第一件事一定不是直接跑训练而是先把目录结构摸透。常见的组织方式有两种一种是顶层分 train、val、test 三个文件夹然后每个文件夹下再按 19 个类别各建一个子文件夹另一种是顶层直接按类别建 19 个文件夹再通过一个 split.txt 或类似文件去做划分。前者是 torch 生态的 ImageFolder 标准结构后者则需要你自己按文件列表移动或过滤。我建议你先在终端里看一眼真实结构用 tree 或者 find 命令都行# 查看数据集的顶层目录结构Linux/macOS 下用 treeWindows 可用 git bash tree -L 2 /path/to/dataset # 如果目录层级太深只看前两层就够不带 -L 参数会输出所有文件很卡这段命令会告诉你数据集的实际组织方式。如果看到的是dataset/train/class_01/xxx.png这种结构那说明它可以直接喂给 torchvision 的 ImageFolder如果看到的是dataset/train/北京市_肝脏细胞_001.png这种把类别写进文件名的形式那就要走自定义 Dataset 路线了。逻辑上前者省事但依赖文件夹命名规范后者灵活但需要你手动解析文件名。参数上-L 2的意思是只展开两层目录避免输出几千个文件路径刷屏。2.2 类别字典文件0 到 18 的数字背后是谁类别字典文件通常是一个 JSON 或 TXT里面定义了每个类别 ID 对应的类别名。这个文件是整个数据集对齐的基准任何训练脚本里的分类头、混淆矩阵横轴、测试时输出的标签都要以这个字典为准而不是以文件夹的字母排序为准。为什么强调这一点因为很多人在写数据加载代码时直接用了sorted(os.listdir())以为文件夹名字母序正好等于类别 ID 序但一旦某个类别名改动过你的标签就全错位了训练出来的模型准确率诡异得高——没错标签错位时会学出一些假特征。我拿到任何带类别字典的数据集第一件事永远是先把字典读出来看一眼import json # 读类别字典假设文件名是 label_dict.json放在数据集根目录 with open(/path/to/dataset/label_dict.json, r, ensure_asciiFalse) as f: label_dict json.load(f) # 打印出来格式通常是 { 0: 肝脏细胞, 1: 肾细胞, ... } for k, v in sorted(label_dict.items()): print(f类别ID {k}: {v}) print(f类别总数: {len(label_dict)})这段代码做了两件事第一确认字典的 key 是字符串类型的序号第二确认 19 个类别都能对应上中文或英文名称。为什么强调字符串类型——因为 JSON 的 key 本来就是字符串但有些人在后续代码里拿整数0去查字典会 KeyError这就是最常见的翻车点。参数上ensure_asciiFalse保证中文类别名正常显示而不是被转成\uXXXX乱码。逻辑上这一小步的代价是十秒钟收益是整个实验过程中标签永远不会对错。2.3 从文件夹到标签建立一份可以用到最后的类别映射表很多人忽略了类别字典文件是给人看的而模型需要的是从文件夹名到整数标号的稳定映射。正确的做法不是每次训练前临时写映射而是把映射表保存成文件整个项目共用一个版本。我的习惯是把 label_dict 反转为类别名 - 整数ID保存为一个 CSV 存到项目目录这样后续做推理时不管哪个脚本都能加载同一份映射不会出现训练脚本一个映射、测试脚本另一个映射的情况。import csv # 反转字典类别名 - 整数ID注意 int() 转换 name_to_id {v: int(k) for k, v in label_dict.items()} # 保存成 CSV 备用 with open(/path/to/project/class_map.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([class_name, class_id]) for name, cid in sorted(name_to_id.items(), keylambda x: x[1]): writer.writerow([name, cid]) print(已保存 class_map.csv后续加载 Dataset 时统一用它)这里关键点是int(k)显式转换因为 label_dict 的 key 是字符串而模型输出的是整型标号不转换就会出现0 ! 0的诡异错误。这段代码在整个项目中只用跑一次之后所有训练和推理代码都读 class_map.csv 而不是原始 JSON。我的经验是共享一份映射文件比在三个脚本里各写一份要省心得多。3. 用 PyTorch 把文件夹数据喂进模型两个可复用的加载方案3.1 最省事方案ImageFolder 直接读5 分钟跑通 baseline如果数据集是标准的train/类别名/图片结构torchvision 的ImageFolder是最快路径。它自动扫描文件夹下的所有子目录按字母序或目录创建顺序生成类别索引并返回(图像张量, 类别索引)对。这里有个隐性要求文件夹名的排序必须和类别字典一致否则标签就错了。我的做法是先读取 label_dict 生成一个 name_to_id 映射再用is_valid_file或者classes参数做强制对齐。from torchvision import datasets, transforms # 图像预处理统一尺寸 归一化 transform_train transforms.Compose([ transforms.Resize((224, 224)), # 全部缩放到 224x224省心但会损失比例 transforms.RandomHorizontalFlip(), # 简单数据增强医学图像要慎用翻转 transforms.ToTensor(), # HWC - CHW并归一化到 [0,1] transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) # ImageNet 统计值 ]) # 直接用 ImageFolder 加载训练集 train_dataset datasets.ImageFolder( root/path/to/dataset/train, transformtransform_train ) print(类别索引映射:, train_dataset.class_to_idx) # 打印并和 label_dict 对比逻辑上ImageFolder返回的class_to_idx字典就是数据集自带的映射但这个映射是它自己扫出来的不保证和 label_dict 一致。所以打印出来对比是必须的不一致的话就用os.rename调整文件夹名或者干脆走下面自定义 Dataset 的路。参数上Resize((224, 224))是为了匹配常见预训练模型的输入这里直接用正方形填充代价是细胞图像的宽高比失真医学图像通常背景占比大轻度形变对分类结果影响不大可以接受。RandomHorizontalFlip是否该用是个争议点医学图像不像自然图像那样具有不变的翻转语义某些器官细胞左右翻转后类别不变但染色体的左右就不该翻转我在后面避坑章节会细说。3.2 自定义 Dataset当目录结构不规整时的兜底方案更常见的情况是数据集并不是标准 ImageFolder 结构或者你已经把数据从原始文件夹挪到了别处。这时就要写自定义 Dataset。这里的原则是不要依赖os.listdir()的返回顺序来定标签因为文件系统返回顺序随机今天排序可能和明天不一样。分类标签只从固定映射表来这才是可复现的。import os import pandas as pd from PIL import Image from torch.utils.data import Dataset class CellDataset(Dataset): def __init__(self, root_dir, class_map_csv, transformNone): # 读取类别映射文件 self.df pd.read_csv(class_map_csv) # 正向和反向映射类名转 ID、ID 转类名 self.name_to_id dict(zip(self.df[class_name], self.df[class_id])) self.id_to_name {v: k for k, v in self.name_to_id.items()} # 遍历所有图片记录 (图片路径, 类别ID) self.samples [] for class_name in self.name_to_id.keys(): class_dir os.path.join(root_dir, class_name) if not os.path.isdir(class_dir): continue for img_name in os.listdir(class_dir): img_path os.path.join(class_dir, img_name) if img_path.lower().endswith((.png, .jpg, .jpeg, .bmp)): self.samples.append((img_path, self.name_to_id[class_name])) self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] image Image.open(img_path).convert(RGB) # 统一转成 3 通道 if self.transform: image self.transform(image) return image, label # 使用示例 train_dataset CellDataset( root_dir/path/to/dataset/train, class_map_csv/path/to/project/class_map.csv, transformtransform_train )这段代码的核心设计是类别映射只读 class_map.csv文件夹遍历只是用来收集文件路径不参与标签决策。这样即使某个文件夹多了一个无关文件甚至某个类别文件夹没被扫到标签映射也不会乱。convert(RGB)这一步是防坑用的——医疗图像有些是灰度 PNG 单通道直接 PIL 打开不进 transform 会报通道数错误提前转换让所有输入统一为三通道。samples列表在__init__里构建意味着每次重新实例化都要遍历一次文件夹文件数量大的时候会慢几秒但 19 类细胞这个规模完全无压力。3.3 DataLoader 参数怎么设worker 数、shuffle 与 pin_memory 的工程取舍数据集加载写好了DataLoader 的参数选择直接关系到训练速度与稳定性。这里最常见的误区是盲目加大num_workers以为 worker 越多加载越快结果在 Windows 上直接崩掉或者内存爆掉。医学图像数据集通常图片不大几百 KB 到几 MB 不等2 到 4 个 worker 足够打满 GPU 的喂数据速度更多的 worker 只会增加 CPU 上下文切换开销。from torch.utils.data import DataLoader train_loader DataLoader( train_dataset, batch_size32, shuffleTrue, # 训练集必须打乱否则模型会学到类别顺序 num_workers4, # Windows 下建议设 0 或 2Linux 可设 4~8 pin_memoryTrue, # 显存充足时开启加速 CPU-GPU 传输 drop_lastTrue # 防止一个批次里全是尾部的某一类 ) val_loader DataLoader( val_dataset, batch_size32, shuffleFalse, # 验证集不打乱保证评估结果可复现 num_workers2, pin_memoryTrue, drop_lastFalse # 验证集不能丢样本落一个都可能影响指标 )shuffle的区别是关键训练集每个 epoch 都要打乱防止模型记忆样本顺序验证集和测试集保持顺序这样每次评估结果完全一致方便对比实验。pin_memory设置为 True 的代价是锁定一块主机内存换取 GPU 拷贝加速显存紧张的小卡上可以关掉。drop_last在训练集上建议开启因为最后一个 batch 往往样本数不足 batch_size对 BatchNorm 层的数据分布统计会产生一次扰动虽然影响很小但没必要留着。4. 从训练结果反推数据质量类别不均衡、误标注与预处理边界4.1 19 个类别的样本数可能天差地别先统计再决定要不要加权19 类器官细胞图像数据集中很多情况下不同类别的样本数量差异可以到 10 倍以上。比如常见的正常细胞种类多、图像多而罕见病变类型可能只有几十张。如果直接拿原始数据训练模型会天然偏向样本量大的类别对少数类学习不足。所以拿到数据后第一件事不是调模型而是统计每个类别的样本数看看是不是一个长尾分布。import collections # 统计每个类别的样本数 count_by_class collections.Counter() for img_path, label in train_dataset.samples: count_by_class[label] 1 for cid in sorted(count_by_class.keys()): print(f类别ID {cid} ({train_dataset.id_to_name[cid]}): {count_by_class[cid]} 张)这段代码直接遍历samples列表统计不需要重新加载图像速度快且不会出错。注意到我在CellDataset里已经把id_to_name保存成属性了否则这里还得再读一次映射表。统计之后的做法分几档如果最少的类别有 200 张以上可以不做重采样单纯用加权损失函数如果最少类别只有几十张建议先考虑用WeightedRandomSampler做有放回采样如果连 20 张都不到那这个类别就不该硬训练要么加数据增强要么换数据集。加权损失函数的做法也顺手给出来这是处理类别不均衡的常规手段import torch import torch.nn as nn # 根据样本数反比计算权重然后传入 CrossEntropyLoss total sum(count_by_class.values()) weights torch.tensor([total / count_by_class[i] for i in sorted(count_by_class.keys())], dtypetorch.float32) criterion nn.CrossEntropyLoss(weightweights)权重计算的逻辑是样本越少的类别每个样本的 loss 贡献权重越大。这里直接用总样本数除以类别样本数得到的是一个相对权重不需要额外归一化——因为 CrossEntropyLoss 会自动对 batch 内的 loss 做平均。这个修改只加了一行但对少数类的分类精度影响极大我见过不少项目仅靠这一行把少数类的 F1 从 0.5 拉到了 0.75 以上。4.2 类别字典文件可能和文件夹内容对不上推理阶段的跨界坑这个坑尤其隐蔽训练时用的是训练集文件夹 label_dict但当你在测试或推理阶段加载数据时如果直接从文件夹名生成标签而不是用同一个映射表就会出现标签错位。最经典的现象是训练准确率 98%测试准确率只有 60%而且混淆矩阵对角线是乱的——这不是模型不行是推理时的标签顺序和训练时不一样。我的建议是把映射表和推理脚本绑定在一起推理阶段加载数据时强制读取同一个 class_map.csv而不是用 ImageFolder 重新扫描。import torch import torch.nn.functional as F # 训练完后保存模型与映射表在同一个目录 torch.save(model.state_dict(), /path/to/project/best_model.pth) # 推理时加载映射表 df_map pd.read_csv(/path/to/project/class_map.csv) name2id dict(zip(df_map[class_name], df_map[class_id]))这里的核心是让模型权重和类别映射永远在一起不要移动任何一个单独的文件。我吃过一次亏把模型拷到另一台机器上做部署忘了带 label_dict结果生成的预测结果全部错位排查了两天才发现是类别顺序的问题。从那以后我的规则就是模型和映射表永远放同一个目录缺一个都不算一套完整交付物。4.3 迁移学习的预处理边界ImageNet 均值和医学图像能不能共用现在大家都是拿在 ImageNet 上预训练好的 ResNet 或 EfficientNet 来迁移学习这套流程默认使用 ImageNet 的 mean 和 std 做归一化。但医学图像和自然图像在像素分布上差得很远——细胞染色切片往往背景是淡色、细胞核是深紫色或蓝色像素分布集中在一个很窄的区间用 ImageNet 的均值归一化后图像会整体偏暗或者偏亮模型第一层学到的低层特征可能会失真。你不想训练一次深度模型从零开始。常见做法是保留预训练模型仍然使用 ImageNet 均值。我的经验是初期直接用 ImageNet 均值跑一个 baseline等模型收敛后再跑一组“用训练集计算 mean 和 std”的对照实验通常在 19 类细胞分类这种任务上两者差距不超过 1 到 2 个百分点说明影响不大。如果想要更贴近数据分布用下面的代码统计训练集的均值和标准差然后在 transform 里替换即可。import numpy as np from tqdm import tqdm pixel_sum np.zeros(3) pixel_sq_sum np.zeros(3) pixel_count 0 # 只遍历一遍训练集不带增强统计均值和标准差 for img_path, _ in tqdm(train_dataset.samples): img np.array(Image.open(img_path).convert(RGB)).astype(np.float32) / 255.0 pixel_sum img.sum(axis(0, 1)) pixel_sq_sum (img ** 2).sum(axis(0, 1)) pixel_count img.shape[0] * img.shape[1] mean pixel_sum / pixel_count std np.sqrt(pixel_sq_sum / pixel_count - mean ** 2) print(训练集均值:, mean, 标准差:, std)这段代码用了pixel_sum和pixel_sq_sum两个累计量避免了把所有图片全部读入内存再算的一次性开销。astype(np.float32)是为了避免 uint8 溢出——像素值都在 0 到 255平方后直接用 uint8 存会溢出成 0。如果计算出的 mean 和 ImageNet 均值差别在 0.1 以下直接用 ImageNet 参数即可如果差超过 0.15建议替换成训练集的统计量。5. 医学图像分类避坑5 条数据使用者的血泪踩坑记录5.1 同一患者的多张切片被分进训练集和验证集指标虚高现象验证集准确率 99%模型表现好得可疑换了模型还是 99%不禁让人怀疑是不是代码写错了。原因医学图像数据集中同一个病人的多个切片往往在文件夹里紧挨着划分 train/val 时如果不做人级别的分组这些高度相似的切片会同时出现在训练集和验证集。模型记住了切片级纹理而不是器官细胞的真实特征导致验证指标虚高。这本质上是数据泄露的一种只是泄露发生在样本间相似性而不是标签。解决如果原始数据文件名里能识别出病人 ID比如patient_001_xxx.png用GroupKFold按病人划分确保同一个病人的切片只在训练集和验证集中的一边。from sklearn.model_selection import GroupKFold # 假设你已经有一个列表每个元素是 (图片路径, 标签, 病人ID) groups [patient_id for _, _, patient_id in all_samples] gkf GroupKFold(n_splits5) for train_idx, val_idx in gkf.split(all_samples, groupsgroups): # train_idx 和 val_idx 的病人集合完全不相交 train_samples [all_samples[i] for i in train_idx] val_samples [all_samples[i] for i in val_idx]GroupKFold 的逻辑是先按 group 分组再划分保证任意 split 中同一组只出现在一侧。代价是如果某个病人的切片特别多划分后训练集和验证集的类别分布会偏移需要多折交叉验证取平均来降低方差。如果你拿到的 19 类数据文件命名没有病人 ID那就只能祈祷划分是合理的——这也是为什么我建议先看文件名是否有规律再动手训练。5.2 数据扩增不分类型乱加翻转异常类型被翻转成正常类型现象训练集加了RandomHorizontalFlip之后训练 loss 正常下不来验证准确率比不加还低 2 到 3 个点。原因医学图像有结构性语义不是所有图像都可以翻转。某些细胞类型例如有极性、有方向性的组织结构左右翻转后形态本身就变了但这并不代表它是一个新的合理样本。19 类器官细胞中有些类别翻转后语义可能不变但有些翻转后语义边界模糊。解决建议初期训练只加小角度的旋转±15 度和轻微的仿射抖动不要用翻转如果非要翻转先把模型训一版不加翻转的 baseline再对比加翻转的版本差别小于 0.5% 就保留翻转做增强。血泪经验是“不加增强的模型不会骗你乱加增强的模型会给你一个惊喜”。5.3 读图时强制 RGB 导致灰度图像上出现伪影现象单独查看某张灰度图片时很正常但训练出的模型对该类图像的预测结果不稳定抖动巨大。原因有些格式如 PNG是灰度单通道或者带 alpha 通道的 RGBA直接convert(RGB)会把灰度值复制到三个通道看起来没事但某些库的ToTensor()不做通道对齐或者预处理时 alpha 通道被当成了第四个通道直接截断导致像素分布异常。另一个常见现象是灰度图被错误转换成三通道后部分灰度值在 RGB 转 HSV 等色彩空间变换时被放大出伪色。解决在所有读图代码之前统一走一次 PIL 的 RGB 转换并且不依赖ToTensor()做通道数检查。如果你确定数据集中全是灰度图可以用Image.open(...).convert(L)读取单通道然后自定义 transform 复制成三通道保证后续模型输入通道数为 3class GrayToRGB: def __call__(self, img): # img 是 PIL 灰度图转成 3 通道 return img.convert(RGB) # 在 transform 里使用 transform transforms.Compose([ GrayToRGB(), transforms.Resize((224, 224)), transforms.ToTensor(), # 不再依赖 convert(RGB) 隐式处理 ])这样做的价值是让通道转换成为一个显式的、可排查的环节而不是隐藏在Image.open()的默认行为里。当你确认所有输入都是灰度图后可以把模型第一层的输入通道改为 1重新初始化 conv1 的权重通常能省一点显存但效果上和复制三通道差别不大不要期待精度提升。5.4 num_workers 设置过大Windows 上直接引发死锁和内存爆炸现象训练脚本在 Linux 上跑得好好的到 Windows 上一运行DataLoader就卡死或者直接报BrokenPipeError甚至电脑蓝屏。训练到一半程序突然死掉怎么也复现不了。原因Windows 下多进程数据加载使用 spawn 方式启动子进程每个子进程都需要重新导入主模块。如果脚本没有if __name__ __main__守卫子进程会重复执行参数初始化和 DataLoader 创建逻辑形成递归轻则内存翻倍重则直接死锁。Linux 下 fork 方式没有这个问题所以你在 Linux 上开 8 个 worker 没事Windows 下开 8 个就崩。解决Windows 上把num_workers设为 0 或 2并且整个训练代码包在if __name__ __main__:里如果确实要跑多进程主进程里不要在 import 之外执行任何数据集初始化逻辑。if __name__ __main__: # 所有的 DataLoader、训练循环、模型初始化都放在这里面 train_loader DataLoader(train_dataset, batch_size32, num_workers2, shuffleTrue) # ... 训练循环num_workers0意味着数据加载完全在主进程内完成速度慢但稳定Windows 上 Debug 用足够num_workers2是性能和稳定性的折中实测大部分医学图像数据集 2 个 worker 已经能跑满一张中端 GPU 的吞吐。另一个隐藏坑是 pin_memory 在 Windows 下偶尔会有兼容性问题如果程序在训练中途莫名崩掉先试pin_memoryFalse。5.5 验证集和测试集不做严格分离反复调参调到过拟合测试集现象模型在验证集上的指标越来越高但发布后用户反馈效果糟糕或者比赛提交分数大幅低于本地验证分数。原因你在实验过程中反复使用同一个验证集调参本质上已经把这个集合的信息泄漏进了模型选择过程。验证集的作用是一次性的调参超过三次后验证集与模型的隐含相关性就会增加最后选出的模型是“对验证集过拟合的模型”测试集上自然翻车。19 类器官细胞数据集的规模不大验证集样本少噪声高这个问题被放大得更明显。解决把数据严格三分训练集训权重验证集做小规模调参测试集只在最后跑一次。这个数据集已经划分好了 train/val/test所以你的职责是定死“测试集只用一次”的纪律。如果 val 指标和 test 指标差异超过 5%说明 val 太小或者和 train 太像建议用 5 折交叉验证评估稳定性取平均和方差作为模型真实水平的估计。6. 验证数据集的可用性用混淆矩阵和 t-SNE 做一次快速健康检查拿到一个数据集先别急着上大模型我的习惯是先跑一个轻量级模型做健康检查同时用混淆矩阵和 t-SNE 可视化判断这 19 类是否真的可分。健康检查的意义在于用最廉价的实验暴露数据集的问题——比如某两类图像长得几乎一样、某个类别样本质量差、标签有错位等这些问题用 ResNet 训练十分钟在混淆矩阵上一眼就能看出来省得你花三天训完大模型才发现数据本身有硬伤。import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import confusion_matrix from torch.utils.data import DataLoader # 假设 model 是训练过的分类模型val_loader 是验证集 model.eval() all_preds, all_labels [], [] with torch.no_grad(): for images, labels in val_loader: outputs model(images.cuda()) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) # 计算混淆矩阵并可视化 cm confusion_matrix(all_labels, all_preds) plt.figure(figsize(14, 12)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[train_dataset.id_to_name[i] for i in range(19)], yticklabels[train_dataset.id_to_name[i] for i in range(19)]) plt.xlabel(预测类别) plt.ylabel(真实类别) plt.savefig(/path/to/project/confusion_matrix.png, dpi200, bbox_inchestight)注释参数里xticklabels和yticklabels用id_to_name保证矩阵坐标不是数字而是可读的类别名否则对角线上的错位根本看不出是哪两类在混淆。如果发现某种混淆集中在某几类比如肾细胞和肾上腺细胞互相分不清大概率不是模型不行而是这两类图像本身在外观上高度相似需要更强的纹理特征或者更高分辨率输入。这个结论直接影响你下一步的模型选择而不是在调参的泥潭里打转。t-SNE 是另一个快速判断可分的工具它把图像特征降维到二维平面用肉眼看聚类情况。需要注意的是 t-SNE 的随机性很强同一次训练 run 出来的点位置每次都会变但簇的相对关系是稳定的from sklearn.manifold import TSNE import numpy as np # 从模型倒数第二层抽特征假设叫 feature_extractor all_features, all_labels [], [] with torch.no_grad(): for images, labels in val_loader: feats feature_extractor(images.cuda()).cpu().numpy() all_features.append(feats) all_labels.extend(labels.numpy()) features np.concatenate(all_features, axis0) # n_components2 意味着降到二维perplexity 控制在 5~50 之间 tsne TSNE(n_components2, perplexity20, n_iter1000, random_state42) embedding tsne.fit_transform(features) # 按标签着色绘图 plt.figure(figsize(12, 10)) scatter plt.scatter(embedding[:, 0], embedding[:, 1], call_labels, cmaptab20, s10, alpha0.7) plt.colorbar(scatter) plt.title(t-SNE 可视化验证集特征分布) plt.savefig(/path/to/project/tsne_val.png, dpi200, bbox_inchestight)参数上perplexity与样本量相关样本量在 1000 左右时设 20 到 30 是稳妥区间n_iter1000是默认值如果你看到图很乱没有明显簇结构可以提高到 2000。这里要提醒一句t-SNE 本质是保留邻居关系的非线性降维它告诉你的“可分性”是相对指标别拿它当成严格的可分性证明——但如果是同一类的点完全没有聚在一起的趋势那这个类别的特征提取大概率是失败的。我现在的习惯是拿到任何新数据集先花一小时做三件事统计分布、跑一个 10 epoch 的 ResNet18 baseline、画混淆矩阵和 t-SNE。三件事做完就知道这个数据集值不值得深挖、坑在哪、模型要从哪个方向调。这套流程救了我不止一次有一次就是靠 t-SNE 发现某两类细胞几乎完全重叠后来一查是数据标注本身把两个亚型混标了你再怎么调模型都是白费力气。希望帮到你少走点我走过的弯路。本文还有配套的精品资源点击获取
返回列表