☰
23类皮肤病图像分类数据集:从ImageFolder加载到迁移学习baseline实战
2026/10/9 6:06:25 网站建设 项目流程

简介:这份资源面向医学图像处理与深度学习入门者,提供23类皮肤病图像分类数据集,覆盖湿疹、肿瘤、真菌感染、带状疱疹等常见病种,可直接用于图像分类模型训练与验证。数据按文件夹组织,train训练集含15557张、test测试集含4002张,配合ImageFolder即可加载,无需额外清洗或标注,也能作为YOLOv5分类任务的数据源。压缩包共2000个文件,以1998张jpeg图像为主,另附1个py可视化脚本和1个json类别字典,整体约933.7MB,7z格式打包。其中json文件给出23类标签映射,py脚本随机抽取4张图片即可展示并保存到当前目录,无需修改即可运行,便于快速检查数据分布与类别质量。目前已有531人学习,适合希望快速搭建皮肤病分类基线、验证模型效果或开展课程实验的读者使用。

1. 23 类皮肤病图像分类数据集:从拿到文件夹到跑通第一个 baseline

如果你做过医学图像分类,大概率遇到过这种局面:论文里写着「在皮肤病数据集上验证」,但你手里只有几百张图,类别还不平衡,连个像样的验证集都切不出来。这份 23 类皮肤病分类数据集解决的就是这个前置问题——它把训练集和测试集按文件夹分好,train 下 15,557 张、test 下 4,002 张,总共 967 MB,23 个类别覆盖湿疹、肿瘤、真菌感染、疱疹等常见皮肤病变。目录结构直接对齐ImageFolder的约定,不需要你写额外的标注解析脚本。适合两类人:一是想快速验证分类模型(ResNet、EfficientNet、ViT 都行)的算法工程师,二是拿它做课程设计或毕设、需要一份「能直接跑」数据的学生。下面按「数据长什么样 → 怎么接进训练管线 → 坑在哪 → 怎么验证」的顺序拆开讲。

2. 数据组织与 ImageFolder 加载:先看清目录再动手

2.1 目录结构与类别字典

拿到压缩包解压后,根目录下是data,里面分train和test两个子目录,各自再按类别名建文件夹。这种「一类一文件夹」的结构是 PyTorchImageFolder的标准输入格式,也是 yolov5 分类任务默认吃的格式。文件名本身带类别前缀,比如herpes-type-1-recurrent-94.jpeg、eczema-herpeticum-3.jpeg、herpes-zoster-13.jpeg,从文件名就能大致判断归属,但真正决定标签的是它所在的文件夹名,不是文件名——这点后面避坑章节会展开。

数据集还附带一个 JSON 字典文件,记录 23 个类别的映射关系。常见做法是把它读进来生成class_to_idx,方便后续把预测结果还原成可读类别名。先确认一下目录层级和类别数量:

import os, json from pathlib import Path root = Path("data") train_dir = root / "train" test_dir = root / "test" # 列出类别文件夹,确认 23 类 classes = sorted([d.name for d in train_dir.iterdir() if d.is_dir()]) print("类别数:", len(classes)) print("前 5 类:", classes[:5]) # 统计每个类别的图片数量,检查是否严重不平衡 for c in classes: n_train = len(list((train_dir / c).glob("*.jpeg"))) + len(list((train_dir / c).glob("*.jpg"))) n_test = len(list((test_dir / c).glob("*.jpeg"))) + len(list((test_dir / c).glob("*.jpg"))) print(f"{c:40s} train={n_train:5d} test={n_test:5d}")

这段脚本做三件事:确认类别数是否为 23、打印类别名、逐类统计 train/test 图片数。参数上唯一要注意的是后缀——数据集里以.jpeg为主,但可能混有.jpg或.png,所以 glob 时把常见后缀都覆盖上,避免漏统计。跑完你会得到一张类别分布表,如果某些类别 train 只有几十张、test 只有几张,那训练时就得考虑重采样或类别权重,这是后面调参的依据。

2.2 用 ImageFolder 接进 DataLoader

确认目录没问题后,接进 PyTorch 的标准流程就是ImageFolder+DataLoader。不需要自己写Dataset子类,这也是这份数据集省事的地方:

from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集做增强,测试集只做 resize + 归一化 train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) test_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_set = datasets.ImageFolder("data/train", transform=train_tf) test_set = datasets.ImageFolder("data/test", transform=test_tf) train_loader = DataLoader(train_set, batch_size=32, shuffle=True, num_workers=4) test_loader = DataLoader(test_set, batch_size=32, shuffle=False, num_workers=4) print("train 样本数:", len(train_set), "类别:", train_set.classes) print("test 样本数:", len(test_set))

逻辑说明:ImageFolder会自动扫描子目录名作为类别、图片作为样本,class_to_idx按文件夹名字母序生成。归一化用的 mean/std 是 ImageNet 的统计值,如果你从零训练而非加载预训练权重,可以改成自己算的均值方差,但用预训练权重时保持一致更稳。batch_size=32是 224 分辨率下的常见起点,显存不够就降到 16 或 8。num_workers在 Windows 上如果报多进程错误,先设成 0 排查。

提示:train 和 test 的class_to_idx必须一致。ImageFolder 各自扫描各自的目录,只要两边文件夹名完全相同,映射就一致;一旦某类在 test 里缺失,索引就会错位,务必用train_set.class_to_idx == test_set.class_to_idx校验一次。

2.3 可视化脚本怎么用

数据集自带一个可视化 py 文件,随机抽 4 张图展示并保存到当前目录。这类脚本的价值在于快速肉眼确认「图和标签对不对得上」——医学图像里标签错配是常见脏数据问题,训练前扫一眼能省掉后面调半天的冤枉功夫。常见做法是直接运行,它会读data/train下的图片,用 matplotlib 拼成 2x2 网格。如果你要改抽样数量或保存路径,改脚本里的n和save_path两个变量即可。运行前确认 matplotlib 已装,且当前工作目录在数据集根目录下,否则相对路径会找不到图。

3. 训练配置与迁移学习:把 23 类跑出一个能看的准确率

3.1 选 backbone 与冻结策略

23 类、1.5 万张训练图,这个规模不适合从零训大模型,迁移学习是性价比最高的路线。常见选择是 ResNet50 或 EfficientNet-B0:前者稳、社区资料多,后者参数少、推理快。加载预训练权重后,把最后的全连接层换成 23 维输出:

import torch import torch.nn as nn from torchvision import models def build_model(num_classes=23, backbone="resnet50", freeze=True): if backbone == "resnet50": model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT) if freeze: # 冻结除 fc 外的所有层,先只训分类头 for p in model.parameters(): p.requires_grad = False model.fc = nn.Linear(model.fc.in_features, num_classes) elif backbone == "efficientnet_b0": model = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.DEFAULT) if freeze: for p in model.parameters(): p.requires_grad = False model.classifier[1] = nn.Linear(model.classifier[1].in_features, num_classes) return model model = build_model() device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device)

参数说明:freeze=True时只训练新换的分类头,适合数据量偏小、怕过拟合的阶段;等分类头收敛后,再解冻后面几个 block 做微调,学习率调小一个量级。num_classes=23必须和实际类别数一致,写错会在 loss 计算时直接报维度不匹配。backbone 选哪个不用纠结,先跑通 ResNet50,有精力再对比 EfficientNet。

3.2 训练循环与关键超参

一个最小可用的训练循环,包含训练和验证两个阶段:

import torch.optim as optim criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1) def run_epoch(loader, train=True): model.train() if train else model.eval() total_loss, correct, total = 0.0, 0, 0 with torch.set_grad_enabled(train): for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) loss = criterion(outputs, labels) if train: optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) correct += (outputs.argmax(1) == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total for epoch in range(15): tr_loss, tr_acc = run_epoch(train_loader, train=True) va_loss, va_acc = run_epoch(test_loader, train=False) scheduler.step() print(f"epoch {epoch+1:02d} | train {tr_loss:.4f}/{tr_acc:.4f} | val {va_loss:.4f}/{va_acc:.4f}")

逻辑说明:CrossEntropyLoss是单标签多分类的标准损失,内部含 softmax,所以模型输出不要自己再加 softmax。优化器只传requires_grad=True的参数,冻结阶段就只更新分类头。StepLR每 7 个 epoch 把学习率乘 0.1,是迁移学习的常用节奏。验证阶段用model.eval()关掉 dropout 和 BN 的统计更新,否则验证结果会飘。跑 15 个 epoch 后看 val 准确率曲线,如果 train 一路涨、val 早早就平甚至下降,就是过拟合,回头加增强或加 dropout。

3.3 类别不平衡的处理

23 类皮肤病数据里,湿疹、疱疹这类常见病样本多,罕见病变可能只有几十张。直接训练会让模型偏向多数类。两种常见做法:一是给CrossEntropyLoss传weight参数,按类别频率的倒数加权;二是用WeightedRandomSampler在采样阶段就平衡。前者改动小,后者对少数类更友好:

from torch.utils.data import WeightedRandomSampler import numpy as np targets = [s[1] for s in train_set.samples] class_count = np.bincount(targets) class_weight = 1.0 / class_count sample_weight = class_weight[targets] sampler = WeightedRandomSampler(sample_weight, num_samples=len(sample_weight), replacement=True) train_loader = DataLoader(train_set, batch_size=32, sampler=sampler, num_workers=4)

参数说明:class_weight取频率倒数,样本越少的类权重越高;replacement=True表示有放回采样,保证少数类能被反复抽到。注意用了sampler就不能再设shuffle=True,两者冲突会报错。评估时别只看总体准确率,少数类的 recall 才是关键,建议打印每类的混淆矩阵。

4. 避坑与排查:这几处翻车我踩过

4.1 文件名带类别前缀,但标签只认文件夹

现象:看到herpes-zoster-13.jpeg就以为标签能从文件名解析,自己写了个按文件名切分的 Dataset,结果和 ImageFolder 的标签对不上。原因:这份数据的标签由文件夹决定,文件名前缀只是命名习惯,两者在个别样本上可能不一致。解决:统一用 ImageFolder,别自己解析文件名;如果确实要用文件名,先抽样比对文件名前缀和文件夹名是否一致。

4.2 train/test 类别数不一致导致索引错位

现象:训练时 loss 正常下降,但验证准确率始终在随机水平附近。原因:某个类别只在 train 有、test 没有(或反过来),两个 ImageFolder 的class_to_idx长度不同,预测索引和真实标签错位。解决:训练前强制校验train_set.class_to_idx == test_set.class_to_idx,不一致就手动对齐或剔除缺失类。

4.3 图像尺寸和通道不统一

现象:DataLoader 报stack expects each tensor to be equal size或通道数错误。原因:数据里混有灰度图或尺寸差异极大的图,Resize只统一了空间尺寸,没处理单通道。解决:在 transform 里加transforms.Grayscale(num_output_channels=3)把灰度转三通道,或自定义 transform 做兜底;Resize放在ToTensor之前。

4.4 验证集当测试集反复调参

现象:调了很久,test 准确率很高,换个新数据就崩。原因:把 test 当验证集用,超参和模型选择都在这 4002 张上做的,等于变相过拟合。解决:从 train 里再切一份验证集(比如 10%),test 只在最后评估一次,别拿它指导训练。

4.5 归一化参数照搬导致收敛慢

现象:从零训练时 loss 下降很慢。原因:直接用了 ImageNet 的 mean/std,但这份数据的亮度分布和自然图像差别大。解决:统计训练集的像素均值和方差替换掉默认值,几行代码的事,收敛会明显改善。

5. 进阶验证与一个收尾习惯

跑通 baseline 之后,别急着上更复杂的模型,先把评估做扎实。医学图像分类里,总体准确率会骗人——多数类占大头时,全预测成多数类也能有不错的 accuracy。我一般会强制走一遍这套验证流程:先出混淆矩阵看哪些类互相混,再逐类算 precision/recall/F1,最后看 top-3 准确率(临床上给候选诊断比只给一个更有意义)。

from sklearn.metrics import classification_report, confusion_matrix import numpy as np model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in test_loader: preds = model(imgs.to(device)).argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_names=test_set.classes, digits=3)) cm = confusion_matrix(all_labels, all_preds) # 找出最容易混的两类 off_diag = cm.copy() np.fill_diagonal(off_diag, 0) i, j = np.unravel_index(off_diag.argmax(), off_diag.shape) print(f"最易混淆: {test_set.classes[i]} -> {test_set.classes[j]}, 次数={off_diag[i, j]}")

这段脚本输出逐类指标和混淆矩阵,并自动定位混淆最严重的一对类别。参数上target_names直接取test_set.classes,保证顺序和标签索引一致。拿到结果后,如果某两类反复混,先回去看可视化脚本抽出的样本,确认是不是标注本身就有歧义——医学图像里湿疹和疱疹性湿疹本来就难分,这种混淆未必是模型的问题。

还有一个习惯:每次换 backbone 或改超参,固定随机种子(torch.manual_seed(42)、np.random.seed(42)),否则两次结果没法比。我早期图省事不设种子,同一份配置跑出两个差 3 个点的准确率,排查了半天才发现是随机性,从那以后每次实验都强制走一遍种子固定和类别分布校验。这份数据集结构干净、开箱即用,把上面这些验证做扎实,它足够撑起一个可信的分类 baseline。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询