ARTICLE DETAIL

资讯详情

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

猴痘皮肤病变二分类数据集实战:从数据加载到迁移学习与阈值调优

猴痘皮肤病变二分类数据集实战:从数据加载到迁移学习与阈值调优 简介这是一份面向计算机视觉初学者与医学图像分析研究者的猴痘皮肤病变二分类数据集可直接用于深度学习模型训练与算法验证省去繁琐的数据清洗环节。压缩包内共2000个文件以1999张jpg图像为主另附1个json类别字典文件整体约24.6MB体积轻便易于下载与本地部署。数据按训练集与测试集分文件夹组织训练集含2555张图像测试集含637张图像每类图像存放于以类别命名的子目录中涵盖猴痘与非猴痘两类样本便于直接接入主流框架的数据加载流程。目前已有149人学习使用。借助该数据集读者可快速搭建二分类基线模型开展迁移学习、数据增强与模型对比实验并利用json字典核对类别映射关系适合作为课程作业、论文实验或医学图像分类入门练手的实用素材。1. 猴痘皮肤病变二分类数据集从图像分类任务说起猴痘Mpox皮肤病变图像的二分类是这两年在医学图像方向被问得比较多的一个入门级计算机视觉任务。它的设定很直接给一张皮肤病变区域的照片判断它属于猴痘还是非猴痘其他类似皮疹、水痘、麻疹等。听起来像是个简单的图像分类问题但真正动手做过的人都知道医学图像分类的坑从来不在模型结构上而在数据本身——样本量小、类别不均衡、图像质量参差、背景干扰严重。这份猴痘皮肤病变二分类数据集就是围绕这个任务整理出来的图像集合通常按类别分目录存放配合标准的 ImageFolder 式加载方式就能直接跑通训练流程。它适合谁适合正在找计算机视觉大作业题目的学生、想练手医学图像分类的算法工程师以及需要一个小规模二分类数据集来验证模型改动是否有效的研究者。你不需要自己爬图、清洗、标注拿到就能进入建模环节这在图像分类项目里省掉的往往是整个流程里最耗时的部分。2. 数据集结构与加载把目录读成张量2.1 目录组织与类别划分逻辑这类二分类数据集最常见的组织方式是按类别分文件夹根目录下两个子目录一个放正类猴痘一个放负类其他。这种结构的好处是能直接对接 PyTorch 的ImageFolder或 TensorFlow 的image_dataset_from_directory不需要自己写解析逻辑。但拿到手第一件事不是急着写模型而是先确认三件事两个类别的实际文件数量、图像格式是否统一jpg/png 混用很常见、有没有损坏文件。我一般会先跑一段统计脚本把每个类别的数量、尺寸分布、通道模式摸清楚再决定后续的预处理策略。import os from PIL import Image from collections import Counter root dataset # 根目录下面应有 monkeypox / others 两个子目录 for cls in os.listdir(root): cls_dir os.path.join(root, cls) if not os.path.isdir(cls_dir): continue sizes, modes, bad Counter(), Counter(), [] files [f for f in os.listdir(cls_dir) if f.lower().endswith((.jpg, .jpeg, .png))] for f in files: try: with Image.open(os.path.join(cls_dir, f)) as im: sizes[im.size] 1 modes[im.mode] 1 except Exception as e: bad.append((f, str(e))) print(f[{cls}] 总数{len(files)} 尺寸分布{sizes.most_common(5)} 通道{modes}) if bad: print(f 损坏文件 {len(bad)} 个示例{bad[:3]})这段脚本做的是数据体检。sizes统计图像分辨率分布如果发现尺寸差异极大比如有的 200×200有的 2000×1500说明后续必须统一 resizemodes看是 RGB 还是灰度或 RGBA灰度图混进 RGB 模型会导致通道数不匹配bad记录打不开的文件这类文件在训练时会直接抛异常中断提前清掉比训练到一半崩掉强。参数上root换成你实际的解压路径即可类别目录名按数据集实际命名调整。2.2 构建 Dataset 与 DataLoader确认数据干净之后就可以搭标准的加载管线。医学图像分类里训练集和验证集的划分要特别注意如果同一张原图经过增强产生了多个副本划分时必须以原图为单位否则会出现训练集和验证集“看到同一张图”的泄漏验证指标虚高。常见做法是先按文件名或患者 ID 分组再做 GroupShuffleSplit而不是简单随机切分。import torch from torch.utils.data import DataLoader, random_split from torchvision import datasets, transforms train_tf transforms.Compose([ transforms.Resize((224, 224)), # 统一到骨干网络输入尺寸 transforms.RandomHorizontalFlip(0.5), # 医学图像左右翻转通常安全 transforms.RandomRotation(15), # 小角度旋转模拟拍摄角度差异 transforms.ColorJitter(0.2, 0.2, 0.2), # 缓解不同设备白平衡差异 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) full datasets.ImageFolder(dataset, transformtrain_tf) n_val int(len(full) * 0.2) n_train len(full) - n_val train_set, val_set random_split(full, [n_train, n_val], generatortorch.Generator().manual_seed(42)) val_set.dataset datasets.ImageFolder(dataset, transformval_tf) # 验证集换增强 train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_set, batch_size32, shuffleFalse, num_workers4) print(f训练 {n_train} 张验证 {n_val} 张类别映射 {full.class_to_idx})这里有几个参数值得说清楚。Resize((224,224))是为了匹配 ResNet、EfficientNet 这类骨干的默认输入如果你换 ViT 系列通常也是 224 或 384。Normalize用的 ImageNet 均值方差是因为绝大多数预训练权重是在 ImageNet 上训的保持同一套归一化能让微调更稳。batch_size32在小数据集上偏大如果显存吃紧或样本只有几百张降到 16 甚至 8 更合适。num_workers在 Windows 上如果报错先设成 0 排查。注意random_split这里只是演示真实项目里如果图像来自同一批采集务必按来源分组划分别偷懒。3. 模型选型与训练迁移学习为什么是默认答案3.1 小样本二分类的骨干选择猴痘病变数据集通常规模不大几百到几千张的量级。这种体量下从零训练一个 CNN 基本等于自杀——参数太多、样本太少模型会把训练集背下来验证集准确率原地踏步。迁移学习是这里的默认答案拿 ImageNet 预训练权重做初始化冻结底层卷积只微调最后的分类头或者解冻最后几个 stage 一起小学习率微调。骨干选择上ResNet50 是最稳的基线EfficientNet-B0 在参数量和精度之间平衡得不错如果追求更轻量MobileNetV3 也能跑出可用的结果。选哪个不是玄学取决于你的算力和部署目标要发论文刷指标就上 EfficientNet 或 ConvNeXt要落地到边缘设备就 MobileNet。import torch.nn as nn from torchvision import models def build_model(archresnet50, num_classes2, freeze_backboneTrue): if arch resnet50: model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) if freeze_backbone: for p in model.parameters(): p.requires_grad False model.fc nn.Linear(model.fc.in_features, num_classes) elif arch efficientnet_b0: model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.IMAGENET1K_V1) if freeze_backbone: 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(resnet50, num_classes2, freeze_backboneTrue) trainable sum(p.numel() for p in model.parameters() if p.requires_grad) print(f可训练参数{trainable:,})freeze_backboneTrue时只训练最后的全连接层可训练参数从两千多万降到几千训练速度快、过拟合风险低适合样本极少的情况。等分类头收敛后可以解冻最后一个 stage 用更小的学习率继续微调这一步往往能再涨几个点。weights参数指定预训练权重版本不同版本对最终精度有细微影响但别在这上面纠结太久。3.2 训练循环与类别不均衡处理二分类数据集最常见的暗坑是类别不均衡。猴痘正类样本往往远少于负类如果直接训练模型会倾向于全预测成多数类准确率看着有 80%但召回率惨不忍睹。处理方式有两种一是给损失函数加类别权重二是用 WeightedRandomSampler 在采样层面平衡。我一般两个都上先看混淆矩阵再调。import torch.optim as optim from sklearn.metrics import classification_report device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) # 按类别频率计算权重频率越低权重越高 counts torch.tensor([len(os.listdir(fdataset/{c})) for c in full.classes], dtypetorch.float) class_weights (1.0 / counts) class_weights class_weights / class_weights.sum() * len(counts) criterion nn.CrossEntropyLoss(weightclass_weights.to(device)) optimizer optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4) for epoch in range(15): model.train() for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() loss criterion(model(x), y) loss.backward() optimizer.step() model.eval() preds, labels [], [] with torch.no_grad(): for x, y in val_loader: out model(x.to(device)) preds out.argmax(1).cpu().tolist() labels y.tolist() print(fEpoch {epoch1}) print(classification_report(labels, preds, target_namesfull.classes, digits3))class_weights的计算逻辑是频率的倒数再归一化让少数类在损失里占更大比重。AdamW的weight_decay1e-4是常用的正则强度小数据集上可以适当加大到 1e-3。每个 epoch 打印classification_report而不是只看准确率是因为二分类任务里 precision、recall、f1 才是真正反映模型有没有学到东西的指标。如果发现某一类 recall 长期为 0基本就是权重没起作用或者学习率太大把分类头带崩了。4. 避坑与排查那些让指标虚高或训练崩掉的细节4.1 数据泄漏验证集准确率 99% 的假象现象训练几个 epoch 后验证集准确率飙到 98% 以上但拿新图一测完全不准。原因同一张原始图像经过不同增强或裁剪后同时进了训练集和验证集模型实际上在“背答案”。解决划分数据集前先按图像来源文件名前缀、患者 ID、采集批次分组用 GroupShuffleSplit 或手动按组切分确保同一来源的图只出现在一个集合里。这个坑在医学图像里极其常见血泪经验就是——先分组再划分别用 random_split 一把梭。4.2 图像通道与格式不一致导致训练中断现象训练跑到某个 batch 突然报RuntimeError: expected 4D input或通道数不匹配。原因数据集中混入了灰度图单通道或 RGBA 四通道 PNG而 transform 里没做统一转换。解决在 Dataset 的__getitem__里强制Image.open(path).convert(RGB)或者在预处理阶段批量转换一遍。别指望数据源是干净的自己加一道 convert 成本极低。4.3 类别权重过大导致训练震荡现象加了 class weight 之后 loss 剧烈震荡准确率不升反降。原因权重按倒数计算时如果两类样本量差距是 10:1少数类权重会非常大梯度被少数类主导模型在两类之间反复横跳。解决对权重做平滑比如用sqrt(1/freq)代替1/freq或者把权重上限截断在 5 以内。也可以先不加权训练几轮看基线再决定要不要加。4.4 验证集指标好看但推理结果全是一类现象验证集 f1 有 0.85但实际部署时所有输入都被判成同一类。原因验证集和真实推理数据的分布不一致比如验证集里负类占了 90%模型只要全预测负类就有高准确率f1 被多数类拉高。解决看混淆矩阵而不是单看 f1确认少数类的 recall 是否真的可用同时在推理阶段调整决策阈值不要直接用 argmax。4.5 图像尺寸统一后细节丢失现象把高分辨率病变图 resize 到 224 后模型学不到关键纹理精度上不去。原因皮肤病变的判别信息往往在局部纹理上粗暴缩小会抹掉这些细节。解决改用 RandomResizedCrop 保留局部区域或者用更高输入分辨率384配合相应骨干再或者先做病变区域裁剪再分类。这个取舍取决于你的算力预算但别默认 224 就是最优。5. 进阶技巧用混淆矩阵和阈值调优把二分类榨干到这一步模型能跑通了但二分类任务真正的价值在于把决策边界调到符合业务需求。医学筛查场景里漏检假阴性的代价远高于误报假阳性所以默认的 0.5 阈值往往不合适需要往召回率方向偏。具体做法是在验证集上拿到所有样本的正类概率画 ROC 曲线和 PR 曲线找到满足目标召回率的最低阈值再把这个阈值固化到推理代码里。import numpy as np from sklearn.metrics import roc_curve, precision_recall_curve, confusion_matrix model.eval() probs, labels [], [] with torch.no_grad(): for x, y in val_loader: p torch.softmax(model(x.to(device)), dim1)[:, 1] # 正类概率 probs p.cpu().tolist() labels y.tolist() probs, labels np.array(probs), np.array(labels) fpr, tpr, thr roc_curve(labels, probs) # 找召回率 0.95 的最低阈值 idx np.argmax(tpr 0.95) best_thr thr[idx] print(f目标召回率 0.95 对应阈值 {best_thr:.3f}此时假阳性率 {fpr[idx]:.3f}) pred (probs best_thr).astype(int) print(confusion_matrix(labels, pred))这段代码的核心是roc_curve返回的阈值数组和对应的 tpr、fpr。np.argmax(tpr 0.95)找到第一个召回率达到 0.95 的位置取出那个阈值。之后推理时用probs best_thr代替argmax就能把模型行为对齐到“宁可误报不可漏检”的策略上。混淆矩阵用来确认调整后假阴性确实降下来了代价是假阳性上升——这个 trade-off 没有标准答案取决于你的应用场景能承受多少误报。还有一个容易被忽略的点验证集上的阈值调优做完后最好再留一个独立的测试集确认一遍别在验证集上既调阈值又报最终指标那样数字会偏乐观。我现在的习惯是数据一到手就先切出 train/val/test 三份test 集锁死不动所有调参都在 val 上做最后只在 test 上跑一次出报告。从那以后我每次拿到新的图像分类数据集都强制先做分组划分和独立测试集隔离再开始写模型——这个顺序反了后面所有指标都不可信。希望帮到你。本文还有配套的精品资源点击获取
返回列表