ARTICLE DETAIL

资讯详情

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

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

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, keytrain_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还是自定义DatasetImageFolder最省事但它要求所有图像都能被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类皮肤病分类baseline3.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(p0.5), transforms.RandomVerticalFlip(p0.3), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1, hue0.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, transformtrain_tf) val_ds datasets.ImageFolder(skin_dataset/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(类别到索引:, train_ds.class_to_idx)参数说明Resize((300, 300))是折中值皮肤病图像里有些病灶很小224可能丢细节384又太吃显存。RandomHorizontalFlip和RandomVerticalFlip对皮肤镜图像是安全的因为病灶没有固定方向。RandomRotation(15)模拟拍摄角度变化。ColorJitter的幅度要克制医学图像的颜色本身有诊断意义饱和度调太狠可能把关键特征改掉。Normalize先用ImageNet参数如果训练loss震荡厉害再换成自己算的。3.2 模型选择ResNet50还是EfficientNet23类分类不算特别多但类间差异可能很细比如不同亚型的黑色素瘤。ResNet50是稳妥起点EfficientNet-B3在同等精度下参数量更少。我一般先用ResNet50跑通确认流程没问题再换EfficientNet对比。import torch.nn as nn from torchvision import models def build_model(num_classes23, archresnet50, pretrainedTrue): if arch resnet50: model models.resnet50(weightsmodels.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(weightsmodels.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_classes23, archresnet50).to(device)关键改动是把原始分类头换成Dropout Linear。Dropout率0.3是经验值如果训练集很小每类不到200张可以加到0.5。pretrainedTrue加载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)], dtypetorch.float32 ) weights weights / weights.sum() * num_classes weights weights.to(device) criterion nn.CrossEntropyLoss(weightweights) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) 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(fEpoch {epoch1:02d} | train loss {tr_loss:.4f} acc {tr_acc:.4f} | val loss {va_loss:.4f} acc {va_acc:.4f})权重计算逻辑先取每类样本数的倒数再乘以类别数做归一化这样权重均值约为1不会把loss尺度整体放大。AdamW的lr1e-4适合微调weight_decay1e-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_workers4、pin_memoryTrue是安全组合。如果用了混合精度训练确认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, normalizetrue) plt.figure(figsize(14, 12)) sns.heatmap(cm, annotFalse, cmapBlues, xticklabelsval_ds.classes, yticklabelsval_ds.classes) plt.xlabel(Predicted) plt.ylabel(True) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150) print(classification_report(y_true, y_pred, target_namesval_ds.classes, digits3))归一化混淆矩阵的对角线是每类召回率非对角线是误判比例。重点看两类召回率低于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, dim1).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, sizenum_classes) preds (probs bias).argmax(1) f1 f1_score(labels, preds, averagemacro) 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个点代价是推理时间线性增加。如果部署环境算力有限可以用知识蒸馏把集成模型压到单模型。最后说一个我自己的习惯每次跑完实验把验证集里预测错误的样本单独存到一个文件夹按“真实类_预测类”命名。攒够几百张后翻一翻经常能发现标注错误或者图像质量太差导致的脏数据。这个习惯帮我省过好几次“模型明明没问题但指标就是上不去”的纠结。希望帮到你。本文还有配套的精品资源点击获取
返回列表