news 2026/10/1 11:40:24

23种皮肤病分类数据集实战:PyTorch从数据加载到Baseline训练

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
23种皮肤病分类数据集实战:PyTorch从数据加载到Baseline训练

简介:这份资源是面向医学图像处理与深度学习入门者的23类皮肤病分类数据集,适合用于图像分类模型训练、迁移学习实验及课程设计。数据按文件夹组织,可直接用ImageFolder加载,无需额外预处理,也可作为YOLOv5分类任务的数据源。压缩包共约2000个文件,以1998张jpeg图像为主,另附1个py可视化脚本和1个json类别字典,整体约933.7MB;其中train训练集15557张、test测试集4002张,覆盖湿疹、肿瘤、真菌感染等23个类别。可视化脚本随机抽取4张图片即可展示并保存到当前目录,无需修改即可运行,便于快速检查数据分布与图像质量。目前已有531人学习下载,适合希望快速搭建皮肤病分类基线、验证数据加载流程或开展医学图像分类研究的读者参考使用。

1. 23种皮肤病分类数据集:从拿到手到跑通第一个baseline

皮肤科门诊每天产生大量临床照片,但真正能拿来训练模型的公开数据并不多。23种皮肤病分类数据集把训练集和验证集都切好了,直接省掉最耗时的清洗和划分环节。它覆盖的类别从常见的脂溢性角化、基底细胞癌,到相对少见的血管瘤、黑色素瘤等,每类都有对应的临床图像。这个数据集适合三类人:想入门医学图像分类的算法工程师、需要快速验证模型结构的科研人员、以及做皮肤辅助诊断产品原型的团队。训练集和验证集的目录结构已经按类别分好,不用自己写划分脚本,拿到就能喂给DataLoader。但要注意,医学图像和自然图像差别很大,直接套ImageNet的预处理参数大概率会翻车,后面会细说。

2. 数据集结构拆解与加载方案选型

2.1 目录结构与类别分布

常见做法是训练集和验证集各有一个根目录,下面按类别名建子文件夹。比如:

skin_dataset/ ├── train/ │ ├── melanoma/ │ ├── basal_cell_carcinoma/ │ ├── ... │ └── seborrheic_keratosis/ └── val/ ├── melanoma/ ├── basal_cell_carcinoma/ ├── ... └── seborrheic_keratosis/

这种结构天然适配torchvision.datasets.ImageFolder和tf.keras.utils.image_dataset_from_directory。但23个类别意味着类别不平衡几乎必然存在——黑色素瘤和基底细胞癌的样本量通常远多于血管瘤。我一般会先跑一遍统计脚本,把每类数量打出来,再决定要不要用WeightedRandomSampler或者Focal Loss。

import os from collections import Counter def count_per_class(root): counts = {} for cls in sorted(os.listdir(root)): cls_dir = os.path.join(root, cls) if os.path.isdir(cls_dir): counts[cls] = len([ f for f in os.listdir(cls_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png')) ]) return counts train_counts = count_per_class('skin_dataset/train') val_counts = count_per_class('skin_dataset/val') print('类别数:', len(train_counts)) print('训练集总数:', sum(train_counts.values())) print('验证集总数:', sum(val_counts.values())) for cls in sorted(train_counts, key=train_counts.get): print(f'{cls:35s} train={train_counts[cls]:5d} val={val_counts.get(cls, 0):5d}')

这段脚本遍历训练集和验证集的每个类别文件夹,统计图像文件数量。参数上只认.jpg、.jpeg、.png三种后缀,如果你的数据里有.bmp或.tif,需要自己加进去。输出结果里重点看两个东西:最大类和最小类的比例,以及验证集里有没有某个类样本数为零。如果比例超过10:1,后面训练时就要考虑重采样;如果验证集某类为零,那这个类在验证阶段等于没测。

2.2 加载方式:ImageFolder还是自定义Dataset

ImageFolder最省事,但它要求所有图像都能被PIL正常打开。医学图像里偶尔会有灰度图、CMYK图或者损坏文件,直接跑会在某个batch突然报错。我一般会先写一个快速校验脚本,把打不开的文件列出来,要么修复要么剔除。

from PIL import Image import os def validate_images(root): bad = [] for dirpath, _, filenames in os.walk(root): for fn in filenames: if not fn.lower().endswith(('.jpg', '.jpeg', '.png')): continue fp = os.path.join(dirpath, fn) try: with Image.open(fp) as im: im.verify() except Exception as e: bad.append((fp, str(e))) return bad bad_files = validate_images('skin_dataset') print(f'损坏或无法打开的文件数: {len(bad_files)}') for fp, err in bad_files[:20]: print(fp, '->', err)

im.verify()只检查文件头,不真正解码像素,速度快。发现坏文件后,常见处理是直接删掉或者用OpenCV重新保存一遍。如果坏文件占比超过1%,建议查一下数据来源,可能是传输过程中损坏。

如果后续要做更强的数据增强(比如同时变换图像和分割掩码),那就得写自定义Dataset。但对纯分类任务,ImageFolder加transforms足够。选型理由很简单:23类分类不是检测或分割,不需要读标注文件,ImageFolder的目录即标签机制正好匹配。

2.3 训练集与验证集的划分逻辑

这个数据集已经给了训练集和验证集,但你要确认一件事:验证集是不是从训练集里随机切出来的,还是按患者ID切的。如果是按图像随机切,同一个患者的不同角度照片可能同时出现在训练和验证里,导致验证指标虚高。医学图像里这叫患者级泄漏,是血泪教训级别的坑。

检查方法:看文件名里有没有患者ID前缀。如果有,按患者ID重新划分;如果没有,至少确认验证集里没有和训练集完全相同的图像(可以用感知哈希查重)。

import hashlib from PIL import Image import os def file_md5(fp): with open(fp, 'rb') as f: return hashlib.md5(f.read()).hexdigest() train_hashes = {} for dirpath, _, filenames in os.walk('skin_dataset/train'): for fn in filenames: if fn.lower().endswith(('.jpg', '.jpeg', '.png')): fp = os.path.join(dirpath, fn) train_hashes[file_md5(fp)] = fp dup = 0 for dirpath, _, filenames in os.walk('skin_dataset/val'): for fn in filenames: if fn.lower().endswith(('.jpg', '.jpeg', '.png')): fp = os.path.join(dirpath, fn) if file_md5(fp) in train_hashes: dup += 1 print('重复:', fp, '<->', train_hashes[file_md5(fp)]) print(f'验证集中与训练集完全相同的图像数: {dup}')

MD5只能查完全相同的文件。如果图像经过缩放或压缩,MD5会变,但内容几乎一样。更严格的做法是用pHash,但MD5作为第一道筛查已经能拦住大部分低级错误。

3. 用PyTorch跑通23类皮肤病分类baseline

3.1 数据增强与预处理参数怎么定

医学图像的色彩分布和自然图像差异很大。ImageNet的均值方差是[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225],直接拿来用不是不行,但归一化后的数值范围会偏。我一般会先算一下自己数据集的均值和方差,再决定用哪套参数。

import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf = transforms.Compose([ transforms.Resize((300, 300)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomVerticalFlip(p=0.3), transforms.RandomRotation(degrees=15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1, hue=0.05), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) val_tf = transforms.Compose([ transforms.Resize((300, 300)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_ds = datasets.ImageFolder('skin_dataset/train', transform=train_tf) val_ds = datasets.ImageFolder('skin_dataset/val', transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4, pin_memory=True) print('类别到索引:', train_ds.class_to_idx)

参数说明:Resize((300, 300))是折中值,皮肤病图像里有些病灶很小,224可能丢细节,384又太吃显存。RandomHorizontalFlip和RandomVerticalFlip对皮肤镜图像是安全的,因为病灶没有固定方向。RandomRotation(15)模拟拍摄角度变化。ColorJitter的幅度要克制,医学图像的颜色本身有诊断意义,饱和度调太狠可能把关键特征改掉。Normalize先用ImageNet参数,如果训练loss震荡厉害,再换成自己算的。

3.2 模型选择:ResNet50还是EfficientNet

23类分类不算特别多,但类间差异可能很细(比如不同亚型的黑色素瘤)。ResNet50是稳妥起点,EfficientNet-B3在同等精度下参数量更少。我一般先用ResNet50跑通,确认流程没问题,再换EfficientNet对比。

import torch.nn as nn from torchvision import models def build_model(num_classes=23, arch='resnet50', pretrained=True): if arch == 'resnet50': model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT if pretrained else None) in_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) elif arch == 'efficientnet_b3': model = models.efficientnet_b3(weights=models.EfficientNet_B3_Weights.DEFAULT if pretrained else None) in_features = model.classifier[1].in_features model.classifier = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return model device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = build_model(num_classes=23, arch='resnet50').to(device)

关键改动是把原始分类头换成Dropout + Linear。Dropout率0.3是经验值,如果训练集很小(每类不到200张),可以加到0.5。pretrained=True加载ImageNet权重,医学图像上微调通常比从头训练收敛快得多,除非你的数据量超过10万张。

3.3 训练循环与类别不平衡处理

类别不平衡时,CrossEntropyLoss的weight参数是最简单的补救。权重按类别频率的倒数算,再归一化。

import numpy as np from collections import Counter train_targets = [s[1] for s in train_ds.samples] class_counts = Counter(train_targets) num_classes = len(train_ds.classes) weights = torch.tensor( [1.0 / class_counts[i] for i in range(num_classes)], dtype=torch.float32 ) weights = weights / weights.sum() * num_classes weights = weights.to(device) criterion = nn.CrossEntropyLoss(weight=weights) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total = 0.0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) 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 @torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0.0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) loss = criterion(outputs, labels) 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(30): tr_loss, tr_acc = train_one_epoch(model, train_loader, criterion, optimizer, device) va_loss, va_acc = evaluate(model, val_loader, criterion, device) scheduler.step() print(f'Epoch {epoch+1:02d} | train loss {tr_loss:.4f} acc {tr_acc:.4f} | val loss {va_loss:.4f} acc {va_acc:.4f}')

权重计算逻辑:先取每类样本数的倒数,再乘以类别数做归一化,这样权重均值约为1,不会把loss尺度整体放大。AdamW的lr=1e-4适合微调,weight_decay=1e-4抑制过拟合。CosineAnnealingLR的T_max设成总epoch数,让学习率平滑降到接近零。训练时重点看验证loss有没有在后期反弹,如果反弹明显,说明过拟合,要么加数据增强,要么早停。

4. 皮肤病分类训练避坑与排查清单

4.1 验证集准确率虚高但实际推理一塌糊涂

现象:训练时验证集准确率能到0.9以上,但拿几张新图片测试,预测结果完全不对。

原因:最常见的是患者级泄漏——同一患者的图像同时出现在训练和验证集。其次是验证集做了和训练集相同的数据增强,尤其是随机翻转和旋转,导致验证指标被“增强”了。

解决:先查文件名里有没有患者ID,按ID重新划分。验证集的transform只保留Resize和Normalize,不要加任何随机变换。如果数据来源允许,最好留出一个独立的测试集,从头到尾不参与任何训练和调参。

4.2 某些类别永远预测不对

现象:混淆矩阵里某几个类互相混淆严重,比如基底细胞癌和脂溢性角化总是分错。

原因:这两类在临床上本来就容易混,图像特征重叠度高。另外如果这两类样本量都很少,模型学不到足够判别信息。

解决:先看这两类的样本数,如果都少于100,考虑用数据增强扩充或者用Focal Loss降低易分类样本的权重。如果样本量够但依然混淆,可以引入类别间的层次结构,或者用度量学习(比如Triplet Loss)拉大类间距离。实际项目中,我一般会单独把混淆严重的类拎出来,训练一个二分类器做二次判别。

4.3 训练loss震荡不收敛

现象:loss曲线上下跳动,验证准确率长时间卡在随机水平附近。

原因:学习率太大、batch size太小、或者归一化参数不匹配。医学图像如果用了ImageNet的归一化参数,但实际像素分布差异大,梯度会不稳定。

解决:先把学习率降到1e-5试几个epoch。如果还不行,算一下自己数据集的均值和方差,替换掉ImageNet参数。batch size如果因为显存限制只能设8或16,可以开梯度累积,累积4步等效batch size 32。

4.4 图像读取报“cannot identify image file”

现象:训练到某个batch突然报错,提示PIL无法识别图像文件。

原因:数据集中混入了损坏文件、非图像文件(比如.DS_Store或Thumbs.db),或者图像格式是PIL不支持的(比如某些医学专用的DICOM格式被直接改了后缀)。

解决:跑一遍前面给的validate_images脚本,把坏文件列出来。如果是DICOM改后缀,需要用pydicom读取后转成PNG。如果是系统隐藏文件,在Dataset里过滤掉非图像后缀即可。

4.5 显存溢出但batch size已经很小

现象:batch size降到8还是OOM。

原因:图像分辨率太高,或者模型用了EfficientNet-B7这种大模型。另外如果num_workers设太大,每个worker都会复制一份数据到内存,也可能间接导致问题。

解决:先把图像Resize到256或224。如果还不行,换EfficientNet-B0或ResNet18。检查pin_memory和num_workers,一般num_workers=4、pin_memory=True是安全组合。如果用了混合精度训练,确认torch.cuda.amp的GradScaler用法正确,否则可能因为梯度缩放导致显存异常。

5. 把验证集用出花:混淆矩阵、阈值调优与模型集成

跑通baseline只是第一步。验证集的价值不只是算一个准确率,它能告诉你模型到底在哪些类上翻车。我习惯在每个epoch结束后画混淆矩阵,归一化后看每一行的误判流向。

import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import confusion_matrix, classification_report import numpy as np @torch.no_grad() def get_predictions(model, loader, device): model.eval() all_preds, all_labels = [], [] for imgs, labels in loader: imgs = imgs.to(device) outputs = model(imgs) preds = outputs.argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) return np.array(all_labels), np.array(all_preds) y_true, y_pred = get_predictions(model, val_loader, device) cm = confusion_matrix(y_true, y_pred, normalize='true') plt.figure(figsize=(14, 12)) sns.heatmap(cm, annot=False, cmap='Blues', xticklabels=val_ds.classes, yticklabels=val_ds.classes) plt.xlabel('Predicted') plt.ylabel('True') plt.tight_layout() plt.savefig('confusion_matrix.png', dpi=150) print(classification_report(y_true, y_pred, target_names=val_ds.classes, digits=3))

归一化混淆矩阵的对角线是每类召回率,非对角线是误判比例。重点看两类:召回率低于0.6的类,以及被大量误判到某一类的那些。classification_report会给出每类的precision、recall、f1-score,比整体准确率有用得多。

如果发现某些类召回率特别低,可以调整预测阈值。默认是取softmax最大值的索引,但对不平衡数据,可以对少数类降低阈值。具体做法是给每个类一个偏置项,在验证集上网格搜索最优偏置。

from sklearn.metrics import f1_score # 获取softmax概率 @torch.no_grad() def get_probs(model, loader, device): model.eval() all_probs, all_labels = [], [] for imgs, labels in loader: imgs = imgs.to(device) outputs = model(imgs) probs = torch.softmax(outputs, dim=1).cpu().numpy() all_probs.append(probs) all_labels.extend(labels.numpy()) return np.vstack(all_probs), np.array(all_labels) probs, labels = get_probs(model, val_loader, device) num_classes = probs.shape[1] best_f1 = 0 best_bias = np.zeros(num_classes) for _ in range(200): bias = np.random.uniform(-0.3, 0.3, size=num_classes) preds = (probs + bias).argmax(1) f1 = f1_score(labels, preds, average='macro') if f1 > best_f1: best_f1 = f1 best_bias = bias print(f'最佳macro F1: {best_f1:.4f}') print('各类偏置:', np.round(best_bias, 3))

这个随机搜索虽然粗糙,但比默认argmax通常能提升1到3个点的macro F1。偏置为正的类相当于降低了预测门槛,偏置为负的类提高了门槛。搜索范围[-0.3, 0.3]是经验值,如果概率分布很集中,可以缩小范围。

模型集成是另一个提分手段。训练3到5个不同初始化的ResNet50或EfficientNet,推理时对softmax概率取平均。集成通常能稳定提升2到5个点,代价是推理时间线性增加。如果部署环境算力有限,可以用知识蒸馏把集成模型压到单模型。

最后说一个我自己的习惯:每次跑完实验,把验证集里预测错误的样本单独存到一个文件夹,按“真实类_预测类”命名。攒够几百张后翻一翻,经常能发现标注错误或者图像质量太差导致的脏数据。这个习惯帮我省过好几次“模型明明没问题但指标就是上不去”的纠结。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 11:40:18

ResNet18动物图像分类工程实践:从训练到Flask部署

简介&#xff1a;这是一份面向Python深度学习初学者与图像分类实践者的ResNet动物图像分类项目源码包&#xff0c;聚焦于使用PyTorch或TensorFlow框架实现端到端的模型训练与预测。资源完整覆盖数据预处理、ResNet18模型构建、训练调优、权重保存&#xff08;含已训练的resnet1…

作者头像 李华
网站建设 2026/10/1 11:39:53

Redis 接入 AI 实战:向量检索、语义缓存与 Agent 记忆层设计

1. Redis 接入 AI 到底意味着什么Redis 这个名字&#xff0c;做后端开发的人基本没有不知道的。它常年霸占“缓存中间件”的头把交椅&#xff0c;从最早的纯内存键值存储&#xff0c;一路演化出 Stream、JSON、Search、TimeSeries 等模块&#xff0c;早就不只是“缓存”两个字能…

作者头像 李华
网站建设 2026/10/1 11:39:23

物流包裹与条码实例分割数据集实战指南

简介&#xff1a;本资源是面向物流自动化、计算机视觉算法研发及高校科研人员的轻量级实例分割数据集&#xff0c;聚焦包裹识别与条码定位两大核心任务&#xff0c;专为YOLO系列模型训练优化。数据集共160张真实场景JPEG图像&#xff0c;配套160个YOLO格式多边形标注TXT文件&am…

作者头像 李华
网站建设 2026/10/1 11:38:08

红杉破例押注AI大模型:基础设施投资背后的逻辑与启示

1. 风投圈里的那件“破例”事&#xff0c;到底在投什么 这些年我常年蹲在AI创投和产业观察的第一线&#xff0c;见过不少热钱涌向大模型赛道的名场面。但前阵子听到红杉资本打破自身禁忌、押注人工智能企业Anthropic的消息时&#xff0c;我还是愣了一下。倒不是觉得这家机构不该…

作者头像 李华
网站建设 2026/10/1 11:37:53

AI智能体安全实战:提示词注入与自主入侵防御指南

1. 这不是科幻片&#xff0c;是正在发生的攻防现场“AI智能体安全&#xff1a;提示词注入到自主入侵&#xff0c;企业如何设防&#xff1f;”——这句话里藏着的不是未来预警&#xff0c;而是过去三个月我帮六家客户做安全评估时&#xff0c;亲眼看到的真实攻击链。所谓“提示词…

作者头像 李华