news 2026/9/26 1:57:21

2.5万张皮肤病变图像:黑色素瘤分类数据集实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
2.5万张皮肤病变图像:黑色素瘤分类数据集实战指南

简介:本资源为面向医学图像分类任务的大型皮肤病变数据集,适用于黑色素瘤识别、皮肤癌筛查等深度学习研究场景,适合具备一定CNN基础、希望开展医学图像分类实验的学生与算法工程师。数据集已按8个类别完成标注,涵盖黑色素瘤、鳞状细胞癌、光化性角化病、良性角化病等,并预先划分训练集与测试集,各类别图片分别存放,便于直接构建数据加载流程。压缩包共约2000个文件,以1997张jpg图像为主,另含1个json标注文件、1个py可视化脚本与1个txt说明文件,整体约372MB,运行show脚本即可快速浏览样本分布与图像质量。目前已有88人学习下载。借助该数据集,读者可完成从数据读取、类别统计到CNN模型训练与评估的完整流程,并对照作者主页中图像分类、医学分割及YOLO目标检测等改进项目,进一步拓展网络结构优化与实验对比思路。

1. 大型黑色素瘤分类数据集:2.5 万张皮肤病变图像怎么用才不白跑

皮肤病变图像分类这个方向,最劝退人的从来不是模型结构,而是数据。你兴冲冲搭好 ResNet,准备跑一个黑色素瘤二分类,结果发现公开数据集要么只有几百张、要么类别极度倾斜、要么标注质量参差到连良恶性都分不清。这个标题里的数据集,约 25,000 张已标注皮肤病变图像,正好卡在一个「够训练、够验证、够做消融」的体量上——它解决的不是「有没有数据」的问题,而是「能不能稳定复现一个可用的黑色素瘤分类基线」的问题。

它适合三类人:一是想入门医学图像分类、但被数据卡住的算法工程师;二是手里有模型、缺一个中等规模标注集做迁移学习验证的从业者;三是需要做类别不平衡、图像增强、阈值调优等实验的研究者。25,000 张这个量级,单卡 24G 显存就能跑通主流 CNN 和 ViT 的微调,不需要集群。下面我按「先搞懂数据长什么样 → 怎么切分和预处理 → 怎么训出可用基线 → 坑在哪 → 怎么把指标再往上抬」的顺序,把这条链路讲透。

2. 先看清数据:25,000 张皮肤病变图像的类别分布与标注结构

拿到一个标注数据集,第一件事不是写模型,而是把数据「摸一遍」。黑色素瘤分类的难点,八成藏在类别分布和图像质量里,而不是网络深度。这一章先把数据的结构、类别、以及为什么它值得用讲清楚,再给出可执行的统计脚本。

2.1 黑色素瘤分类任务的类别定义与标签体系

皮肤病变分类最常见的标签体系是二分类(黑色素瘤 / 非黑色素瘤)和多分类(黑色素瘤、基底细胞癌、鳞状细胞癌、良性痣等)。标题说的是「黑色素瘤分类」,落地时你要先确认手里的标签是哪种粒度。常见做法是:如果标签只有良恶性两列,就做二分类;如果带具体病变名称,就先做多分类再合并成二分类评估。

这里有个反直觉的点:黑色素瘤分类的评估指标不能只看准确率。因为黑色素瘤通常是少数类,一个把所有样本都判成「非黑色素瘤」的模型,准确率可能高达 85% 以上,但临床意义为零。所以从第一天起,你就要盯召回率(Recall / Sensitivity)和AUC,而不是 accuracy。

我一般会先建一个标签映射表,把原始标签统一成模型能吃的整数,同时保留一份原始标签用于回溯:

import os import pandas as pd # 假设标注文件是 CSV,含 image_id 和 label 两列 df = pd.read_csv("labels.csv") # 查看原始标签分布,先别急着映射 print(df["label"].value_counts(dropna=False)) # 建立映射:把具体病变名归到二分类 # 注意:映射规则要写死并留档,否则复现时对不上 label_map = { "melanoma": 1, "mel": 1, "basal_cell_carcinoma": 0, "squamous_cell_carcinoma": 0, "nevus": 0, "benign_keratosis": 0, } df["target"] = df["label"].map(label_map) # 检查有没有映射失败的样本 unmapped = df[df["target"].isna()] print("未映射样本数:", len(unmapped)) print(unmapped["label"].unique())

这段代码的逻辑是:先看原始标签长什么样,再做显式映射,最后检查漏网之鱼。参数上,label_map必须覆盖你数据里所有出现的标签,任何NaN都意味着映射表不全,直接训练会把这类样本当异常值处理。value_counts(dropna=False)里的dropna=False很关键,否则空标签会被静默吞掉。

2.2 用统计脚本摸清图像尺寸、通道与损坏文件

25,000 张图,手工看是不可能的,但你可以用脚本在几分钟内摸清尺寸分布、通道数、以及有没有打不开的坏图。这一步能帮你决定统一 resize 到多少、要不要做灰度转 RGB、以及要不要剔除异常样本。

from PIL import Image from collections import Counter import os img_dir = "images/" sizes = Counter() modes = Counter() bad_files = [] for fname in os.listdir(img_dir): path = os.path.join(img_dir, fname) try: with Image.open(path) as im: sizes[im.size] += 1 # (宽, 高) modes[im.mode] += 1 # RGB / L / RGBA except Exception as e: bad_files.append((fname, str(e))) print("尺寸分布 Top10:", sizes.most_common(10)) print("通道模式分布:", modes.most_common()) print("损坏文件数:", len(bad_files))

逻辑说明:Image.open只读文件头,不解码全图,所以 25,000 张跑下来很快。sizes统计的是原始分辨率,如果分布很散(比如从 200px 到 4000px 都有),统一 resize 到 224 或 384 是合理选择;如果大部分集中在某个区间,就按众数附近取。modes里如果出现L(灰度)或RGBA,训练前必须统一转成RGB,否则ToTensor之后通道数对不上,会在第一个 batch 就报错。bad_files里的文件直接移出训练集,别指望 DataLoader 帮你兜底。

提示:统计脚本跑完把结果存成 JSON 或 CSV,后面每次换数据版本都对比一次,能提前发现数据被替换或损坏。

2.3 类别不平衡到底有多严重:先算再决定怎么补

黑色素瘤数据集的类别不平衡是常态。你要先量化它,再决定用加权损失、重采样还是数据增强。算不平衡比(imbalance ratio)= 多数类样本数 / 少数类样本数,一般超过 3:1 就要处理,超过 10:1 就必须处理。

counts = df["target"].value_counts() print(counts) imbalance_ratio = counts.max() / counts.min() print(f"不平衡比: {imbalance_ratio:.2f}") # 计算类别权重,供 CrossEntropyLoss 使用 import numpy as np weights = len(df) / (len(counts) * counts) print("类别权重:", weights.to_dict())

参数说明:weights的计算方式是总样本数 / (类别数 × 每类样本数),这是 sklearn 风格的 balanced 权重。把它传给nn.CrossEntropyLoss(weight=torch.tensor([w0, w1])),少数类的损失会被放大,模型不会一味偏向多数类。注意权重别手动拍脑袋,一定用数据算出来,否则调参时你分不清是权重问题还是模型问题。

3. 切分与预处理:让 25,000 张图在训练时不掉链子

数据摸清之后,下一步是切分和预处理。这两步做错,后面模型再强也白搭。黑色素瘤分类里最常见的翻车就是:同一患者的多个病灶图被分到训练集和验证集,导致验证指标虚高。这一章讲怎么切得干净、怎么增强得合理。

3.1 训练/验证/测试切分:按患者切还是按图像切

如果数据里带患者 ID,必须按患者切分,不能按图像随机切。同一个患者的皮肤往往有相似的色素沉着和拍摄条件,随机切会让模型「记住」这个患者,验证集 AUC 虚高 5 到 10 个点都很常见。如果数据里没有患者 ID,那就只能按图像切,但要在论文或报告里注明这个局限。

from sklearn.model_selection import GroupShuffleSplit # 有患者 ID 的情况:按患者分组切分 gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(df, groups=df["patient_id"])) train_df = df.iloc[train_idx] val_df = df.iloc[val_idx] # 再从训练集里切出测试集,同样按患者 gss2 = GroupShuffleSplit(n_splits=1, test_size=0.25, random_state=42) tr_idx, te_idx = next(gss2.split(train_df, groups=train_df["patient_id"])) test_df = train_df.iloc[te_idx] train_df = train_df.iloc[tr_idx] print(len(train_df), len(val_df), len(test_df))

逻辑说明:GroupShuffleSplit保证同一patient_id只出现在一个集合里。random_state固定,保证每次切分一致,方便复现。最终比例是训练 60%、验证 20%、测试 20%,这是中等规模数据集的稳妥配置。如果数据没有患者 ID,就把groups换成图像 ID,退化成普通随机切分,但心里要清楚指标会偏乐观。

3.2 图像增强:哪些增强对皮肤病变有效,哪些是帮倒忙

皮肤病变图像的增强有讲究。水平翻转、垂直翻转、小角度旋转(±15°)、颜色抖动(亮度/对比度小幅调整)是安全且有效的。但你要小心两件事:一是大角度旋转会引入黑边,二是强烈的颜色变换会破坏黑色素瘤的色素特征,反而降低召回。

import torchvision.transforms as T train_tf = T.Compose([ T.Resize((384, 384)), # 比 224 保留更多病灶边缘细节 T.RandomHorizontalFlip(p=0.5), T.RandomVerticalFlip(p=0.5), T.RandomRotation(degrees=15), # 小角度,避免黑边 T.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.02), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) val_tf = T.Compose([ T.Resize((384, 384)), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])

参数说明:Resize到 384 是我在 25,000 张量级上的常用选择,比 224 多保留病灶边界,显存也扛得住。RandomRotation(15)是经验值,超过 20 度黑边明显,模型会学到黑边这个伪特征。ColorJitter的hue只给 0.02,因为色相大幅变化会让黑色素瘤的棕黑色特征失真。Normalize用的是 ImageNet 统计量,做迁移学习时保持一致。

注意:验证集和测试集只做 Resize + ToTensor + Normalize,绝不做随机增强,否则指标不可比。

3.3 用 DataLoader 把 25,000 张图喂满 GPU

数据管道最后一步是 DataLoader。25,000 张图不算大,但如果你用机械硬盘、num_workers又设成 0,GPU 会大量空转。合理配置能让训练速度差出两三倍。

from torch.utils.data import DataLoader, Dataset from PIL import Image class SkinDataset(Dataset): def __init__(self, df, img_dir, transform): self.df = df.reset_index(drop=True) self.img_dir = img_dir self.transform = transform def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.iloc[idx] img = Image.open(os.path.join(self.img_dir, row["image_id"])).convert("RGB") return self.transform(img), int(row["target"]) train_loader = DataLoader( SkinDataset(train_df, "images/", train_tf), batch_size=32, shuffle=True, num_workers=8, pin_memory=True, persistent_workers=True, )

逻辑说明:convert("RGB")兜底灰度图和 RGBA 图,避免通道数不一致。num_workers=8适合 8 核以上 CPU,pin_memory=True加速 CPU 到 GPU 的拷贝,persistent_workers=True避免每个 epoch 重建 worker。batch_size=32在 384 分辨率、24G 显存下比较稳,显存小就降到 16 并配合梯度累积。

4. 训练一个可用的黑色素瘤分类基线:从损失函数到阈值调优

数据管道通了,接下来是训练。这一章给一个能跑通、能出合理指标的基线,重点讲损失函数、学习率、以及最容易被忽略的阈值调优。

4.1 损失函数与类别权重:别让多数类主导梯度

二分类黑色素瘤任务,CrossEntropyLoss配合类别权重是最省事的起点。如果你想要更好的概率校准,可以试Focal Loss,但先用加权 CE 跑通基线。

import torch import torch.nn as nn # weights 来自 2.3 节的计算结果 class_weights = torch.tensor([1.0, 3.5]).to(device) # 示例值,按实际算 criterion = nn.CrossEntropyLoss(weight=class_weights) # 优化器:微调用小学习率 optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)

参数说明:class_weights必须用 2.3 节算出的真实值,示例里的 3.5 只是占位。lr=1e-4是微调预训练模型的常用起点,从头训练可以到 1e-3。weight_decay=1e-4抑制过拟合。CosineAnnealingLR的T_max设成总 epoch 数,让学习率平滑降到接近 0。

4.2 训练循环与验证:盯住召回率和 AUC

训练循环本身不复杂,关键是每个 epoch 结束在验证集上算对指标。黑色素瘤分类我只看三个数:验证集 AUC、少数类召回、以及混淆矩阵。

from sklearn.metrics import roc_auc_score, recall_score, confusion_matrix def evaluate(model, loader): model.eval() all_probs, all_labels = [], [] with torch.no_grad(): for imgs, labels in loader: imgs = imgs.to(device) logits = model(imgs) probs = torch.softmax(logits, dim=1)[:, 1] all_probs.extend(probs.cpu().numpy()) all_labels.extend(labels.numpy()) auc = roc_auc_score(all_labels, all_probs) preds = (np.array(all_probs) > 0.5).astype(int) rec = recall_score(all_labels, preds, pos_label=1) cm = confusion_matrix(all_labels, preds) return auc, rec, cm for epoch in range(20): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() auc, rec, cm = evaluate(model, val_loader) print(f"Epoch {epoch}: AUC={auc:.4f}, Recall={rec:.4f}") print(cm)

逻辑说明:softmax取第 1 列作为黑色素瘤概率,roc_auc_score直接用概率,不受阈值影响。recall_score用 0.5 阈值先看个大概,真正部署时阈值要单独调。混淆矩阵能告诉你模型是漏诊多还是误诊多,黑色素瘤场景下漏诊(假阴性)代价更高。

4.3 阈值调优:0.5 不是最优,验证集上扫一遍

这是最容易被跳过、但收益最直接的一步。模型输出的概率,0.5 只是默认阈值,黑色素瘤分类里往往要把阈值调低来提高召回。做法是在验证集上扫一遍阈值,选召回满足要求时误诊可接受的那个点。

thresholds = np.arange(0.1, 0.9, 0.05) best_thr, best_score = 0.5, -1 for thr in thresholds: preds = (np.array(all_probs) > thr).astype(int) rec = recall_score(all_labels, preds, pos_label=1) # 这里用 F2 分数,更看重召回 from sklearn.metrics import fbeta_score f2 = fbeta_score(all_labels, preds, beta=2, pos_label=1) if f2 > best_score: best_score, best_thr = f2, thr print(f"最佳阈值: {best_thr:.2f}, F2={best_score:.4f}")

参数说明:beta=2的 F2 分数给召回更高权重,适合医学筛查场景。阈值扫描范围 0.1 到 0.9、步长 0.05 是常用配置。选出的阈值要固定下来,测试集和部署时都用它,不能每次重新扫。

5. 避坑与排查:黑色素瘤分类里最容易翻车的 5 件事

这一章是我自己踩过和见别人踩过的坑,按「现象 → 原因 → 解决」写。每一条都真实影响过指标,不是凑数。

坑一:验证集 AUC 高得离谱,测试集一塌糊涂。现象:验证集 AUC 0.95,测试集掉到 0.80。原因:按图像随机切分,同一患者的图同时进了训练和验证,模型记住了患者特征。解决:回到 3.1 节,按患者 ID 用GroupShuffleSplit重新切分,重新训练。

坑二:模型把所有样本判成多数类,召回为 0。现象:训练 loss 正常下降,但少数类召回一直是 0。原因:类别权重没加,或者加了但权重算错(比如把多数类权重设得更大)。解决:检查class_weights是否来自 2.3 节的真实计算,确认少数类权重更大;同时把评估指标从 accuracy 换成召回和 AUC。

坑三:训练到第 3 个 epoch 就过拟合,验证 loss 反弹。现象:训练 loss 持续降,验证 loss 先降后升。原因:25,000 张对大型模型来说不算多,全量微调容易过拟合。解决:冻结 backbone 前几层、降低学习率到 1e-5、加大 weight_decay、增强里加RandomErasing,或者换更小的模型。

坑四:DataLoader 报通道数错误或图片打不开。现象:训练中途RuntimeError: expected 3 channels或UnidentifiedImageError。原因:数据里有灰度图、RGBA 图或损坏文件,2.2 节的统计没做全。解决:__getitem__里强制convert("RGB"),并在 2.2 节脚本里把bad_files移出数据集。

坑五:换了随机种子指标波动 3 个点以上。现象:同样代码,换个 seed,AUC 从 0.88 变 0.85。原因:数据量中等、类别不平衡时,切分和初始化的随机性影响被放大。解决:固定所有 seed(Python、NumPy、PyTorch),用 3 到 5 折交叉验证报告均值和方差,而不是单次结果。

提示:这五条里,坑一和坑二最致命,前者让指标虚高,后者让模型无效。每次开新实验前,先花十分钟确认这两件事。

6. 把指标再抬一截:迁移学习选型与 Grad-CAM 验证的实操技巧

基线跑通之后,想再往上抬,最划算的两件事是换更强的预训练 backbone,以及用 Grad-CAM 看模型到底在关注哪里。这一章给具体做法。

backbone 选型上,25,000 张这个量级,我一般从ConvNeXt-Tiny、EfficientNet-B3、ViT-Base里选。经验是:数据量偏小、病灶边界重要时,ConvNeXt 和 EfficientNet 往往比 ViT 稳;如果做过多轮增强、数据多样性够,ViT 微调后上限更高。切换 backbone 时,只改模型定义和输入分辨率,数据管道和训练循环不动,这样对比才公平。

import timm # 换 backbone 只需改这一行 model = timm.create_model("convnext_tiny", pretrained=True, num_classes=2) model = model.to(device) # 分层学习率:backbone 小,分类头大 params = [ {"params": model.parameters(), "lr": 1e-5}, ] # timm 模型的分类头名字因模型而异,先打印确认 print(model.get_classifier())

逻辑说明:timm.create_model一行切换 backbone,pretrained=True加载 ImageNet 权重。分层学习率能让 backbone 微调更温和、分类头学得更快。get_classifier()先确认分类头名字,再决定要不要给它单独设更大学习率。

Grad-CAM 是我每次训练完必看的一步。它能告诉你模型是在看病灶本身,还是在看图像边缘、标尺、甚至水印。如果热力图集中在病灶区域,说明模型学到了有意义的特征;如果集中在角落,那指标再高也不可信。

from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image target_layers = [model.stages[-1]] # ConvNeXt 的最后一层 cam = GradCAM(model=model, target_layers=target_layers) grayscale_cam = cam(input_tensor=img_tensor, targets=None) visualization = show_cam_on_image(img_float, grayscale_cam[0], use_rgb=True)

参数说明:target_layers要选最后一个有空间信息的卷积/特征层,不同 backbone 名字不同,用model.named_modules()找。targets=None表示用预测类别作为目标。看热力图时重点看少数类样本,模型对黑色素瘤的关注区域是否合理,比整体 AUC 更能说明问题。

我自己的习惯是:每换一次 backbone 或增强策略,先跑 5 个 epoch 看 Grad-CAM,热力图不对就直接停,别浪费算力。这个习惯帮我省下的 GPU 时间,比任何调参技巧都多。希望帮到你。

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

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

FPGA MIPI多路视频聚合方案:从协议到调试全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 1:56:36

yolov8行人检测毕设实战:从环境搭建到ONNX部署

简介:基于YOLOv8的行人检测系统,是一套面向计算机专业本科毕业设计的高分完整项目,曾获导师指导与评审认可,代码完整、可直接运行,特别适合正在筹备毕业设计的学生,以及需要课程设计、期末大作业或项目实战…

作者头像 李华
网站建设 2026/9/26 1:56:32

Dynamo packages.zip 正确解压与部署指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 1:56:21

Visual Studio 2026是假消息?真相与VS 2022实战升级指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华