简介:面向皮肤癌智能识别任务的医学目标检测数据集,按YOLO标准格式整理训练集与验证集,共1,570张高分辨率图片,覆盖基底细胞癌、黑色素瘤、银屑病、日光性角化病、交界痣、脂溢性角化病等9类常见皮肤疾病,兼顾恶性病变与良性病灶,适用于医疗影像方向的算法工程师、科研人员及目标检测初学者开展模型训练与效果评估。压缩包共计2000个文件,含1,570个txt标注文件、428个jpg原始图像、1个yaml配置文件及1个docx数据集说明文档,整体大小68.16MB,无需额外预处理即可直接读取,训练集与验证集已预先划分完毕,分别包含1,256张与314张图片,免去手动拆分的繁琐步骤。目前已有142人浏览学习,对于需要标准医学目标检测数据集验证YOLO系列模型性能的场景具有明确的实用参考价值。
1. 皮肤癌目标检测数据集:拿 1570 张标注图跑通 YOLO 病灶检测
皮肤科门诊里,医生看一眼皮损照片就能说个大概,但把这件事交给算法远没那么轻松:黑色素瘤和良性痣长得太像,类间差异极小,标注稍微糊一点,模型就分不清。这套「皮肤癌目标检测数据集.zip」解开后是 1570 张高分辨率医学图片,训练集 1256 张、验证集 314 张,覆盖基底细胞癌、复杂痣、皮内痣、交界痣、扁平苔藓、黑色素瘤、银屑病、脂溢性角化病、日光性角化病 9 类病灶。文件是 Roboflow 导出的标准结构,每张图带 YOLO 格式标注,解压改一下数据路径就能直接开训。适合手上缺医学标注数据、想先跑通 YOLO baseline 的算法工程师,也适合做医学影像 AI 课题、需要一份干净数据集验证思路的研究生。
2. 数据集结构与标签体系:9 类病灶的医学含义和检测难点
2.1 九类病灶怎么分:良性、恶性与易混淆组
做目标检测第一件事是搞清楚标签语义,尤其医学数据集,类别名不是随便起的。这套数据的 9 个类别横跨良性、恶性、癌前病变三类性质,我按训练时的实际感受整理成一张表:
| 类别名 | 中文含义 | 性质 | 检测难点 |
|---|---|---|---|
| Basal_cell_carcinoma | 基底细胞癌 | 恶性 | 与脂溢性角化病外观重叠 |
| Complex_moles | 复杂痣 | 良性需随访 | 形态不规则,与黑色素瘤难分 |
| Intradermal_nevus | 皮内痣 | 良性 | 结构相对简单,较易学 |
| Junctional_nevus | 交界痣 | 良性/潜在恶变 | 边界模糊,标注框易偏 |
| Lichen_planus | 扁平苔藓 | 良性慢性 | 丘疹密集,单个目标小 |
| Melanoma | 黑色素瘤 | 高度恶性 | 类别内形态差异大 |
| Psoriasis | 银屑病 | 良性慢性 | 斑块带鳞屑,尺度变化大 |
| Seborrheic_keratosis | 脂溢性角化病 | 良性 | 蜡样表面,边界不规则 |
| Solar_keratosis | 日光性角化病 | 癌前病变 | 浅表病灶,对比度低 |
从检测算法角度看,这份数据最麻烦的不是类别多,而是「易混淆对」多。Melanoma 与 Complex_moles 这对,一个是高度恶性黑色素瘤,一个是形态不规则的良性复杂痣,二者都可能出现不对称、边缘锯齿、颜色不均,训练时 loss 往往集中在这两类的边界上;Basal_cell_carcinoma 与 Seborrheic_keratosis 这对也一样,基底细胞癌和老年斑的蜡样表面在照片里很难一眼区分。验证集的混淆矩阵里基本是这两块最热。我一般把这个先验记下来:训练结束后单独看这两对的混淆情况,而不是只盯着总 mAP 一个数。
另一个值得注意的点是尺度差异。银屑病和扁平苔藓的病灶往往是一小片密集分布,单颗皮损只有几十个像素;基底细胞癌、脂溢性角化病则是单颗大病灶,同一张图里目标尺寸能差 5 倍以上。这对锚框策略和小目标检测有直接影响,第 4 章会细说。简单讲,这份对照数据的检测难点不在「找不找得到」,而在「分不分得清」,标注框质量直接决定模型上限。
2.2 文件名里藏着什么:Roboflow 导出结构与哈希命名
数据集文件名不是乱起的,拿其中一张举例:
intradermal-nevus-INN-7-_jpg.rf.0aa3ba9e55ba2e53ba22be752483ad4b.jpg从左往右拆:intradermal-nevus是类别名,INN-7是图片原始 ID,_jpg表示原图格式,.rf.后面那串 32 位哈希是 Roboflow 平台给每张图片生成的唯一标识,防止不同来源图片重名。这种命名在 Roboflow 导出的目标检测数据集里很常见,.rf.后面的哈希可以直接当唯一键用,以后合并别的公开数据集也不会撞名。
解压之后目录一般是这种结构:
skin-cancer/ ├── train/ │ ├── images/ # 训练图片 .jpg │ └── labels/ # 同名 .txt,YOLO 格式标注 └── valid/ ├── images/ └── labels/标签文件和图片同名不同后缀,每行 5 个数字:
class_id x_center y_center width height坐标全部是相对图片宽高的归一化小数,范围 0~1。这是 YOLO 系列的通用标签格式,Ultralytics YOLOv8、YOLOv11 直接读这种 txt,不需要再转 COCO 或 VOC。如果你这份 zip 解开后标注和图片混放在一起,按 3.2 节的脚本重排即可,不影响数据本身。
2.3 训练集验证集划分:1256/314 的 8:2 比例怎么用
训练集 1256 张、验证集 314 张,合计 1570 张,正好是 8:2 划分。目标检测里这个量级属于「小样本但能跑通」:不像 COCO 那种几十万张的大数据集,但也足够训出像样的 baseline,关键看怎么组织数据。
拿到数据我建议先做一次类别分布体检。医学数据集普遍不均衡,黑色素瘤、日光性角化病的样本通常远少于银屑病,先知道分布才知道后面要不要做特殊处理。统计脚本如下:
from collections import Counter from pathlib import Path for split in ["train", "valid"]: label_dir = Path(f"skin-cancer/{split}/labels") counter = Counter() for txt in label_dir.glob("*.txt"): for line in txt.read_text().strip().splitlines(): if line: counter[int(line.split()[0])] += 1 print(split, dict(sorted(counter.items())))这段代码遍历每个标签文件,取每行第一列(类别 ID)做计数,输出类似{0: 142, 1: 98, ...}的字典。如果 Melanoma 只有几十张,后面必须单独处理,否则模型大概率把它当背景漏检。顺手还要计算每个类别在 train 和 valid 里的占比是否一致——train 里占 5% 的类在 valid 里占 20%,验证指标就会虚高。
提示:Roboflow 导出时类别 ID 通常按字母序从 0 开始,与 YAML 中 names 顺序一一对应。统计输出的 key 是数字 ID,务必对照 names 看,别把 ID 和类别名搞错位。
3. 用 YOLOv8 复现皮肤癌检测:环境、数据与训练一条龙
3.1 环境准备与依赖安装
先讲环境。Ultralytics YOLOv8 是目前最省事的目标检测框架,pip 装完就能跑,不用自己编译 Darknet 或折腾 C++ 源码。我习惯先用 conda 隔离一个干净环境,避免和系统里其他项目的 torch 版本打架:
conda create -n skin python=3.10 -y conda activate skin pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install ultralytics python -c "import torch; print(torch.cuda.is_available())"最后一行输出True说明 GPU 可用,训练走 CUDA;输出False也没关系,1570 张图用 CPU 也能训,只是慢。cu121 是 CUDA 12.1 对应的 torch 版本,显卡驱动较老就换成 cu118,两个版本对最终精度没有实质差别。先装 torch 再装 ultralytics 是有讲究的:ultralytics 默认会顺手拉一个 torch,装的很可能不是 GPU 版,把已经配好的 CUDA 环境覆盖掉,这个坑我踩过一次,所以顺序反过来了。
3.2 目录整理与 YAML 配置
数据集解压后如果目录不是 images/labels 分离的结构,先整理再谈训练。最常见的坑是 jpg 和 txt 混在同一个文件夹里,YOLO 找不到标签直接报错。整理脚本如下:
cd ~/datasets/skin-cancer for split in train valid; do mkdir -p "$split/images" "$split/labels" find "$split" -maxdepth 1 -name "*.jpg" -exec mv {} "$split/images/" \; find "$split" -maxdepth 1 -name "*.txt" -exec mv {} "$split/labels/" \; done脚本对 train 和 valid 分别建 images、labels 子目录,把同目录下的 jpg 和 txt 各归各位。执行完用ls train/images | wc -l和ls train/labels | wc -l对比数量,图片数和标签数必须一致,这个一致性检查比任何配置文件都重要——少一个标签,那张图就会被当空样本跳过,模型等于白看一张图。
目录就绪后写数据配置文件,文件名随意,我用skin_cancer.yaml:
path: /home/yourname/datasets/skin-cancer train: train/images val: valid/images nc: 9 names: 0: Basal_cell_carcinoma 1: Complex_moles 2: Intradermal_nevus 3: Junctional_nevus 4: Lichen_planus 5: Melanoma 6: Psoriasis 7: Seborrheic_keratosis 8: Solar_keratosispath填数据集的绝对路径,train和val是相对 path 的图片目录,YOLO 会自动到同级的 labels 目录找标注。nc是类别数,必须和 names 长度一致。names 顺序不能乱,它对应标签文件每行的第一列数字;Roboflow 默认按字母序排类,所以这里 0~8 正好和导出一致。如果你自己动过标签,先跑一遍 2.3 节的统计脚本,核对最大 ID 是不是 8。
3.3 训练参数与启动训练
配置没问题就可以开训了。baseline 命令长这样:
yolo detect train \ model=yolov8n.pt \ data=skin_cancer.yaml \ epochs=120 \ imgsz=640 \ batch=16 \ patience=25 \ lr0=0.003 \ project=runs/skin \ name=baseline参数说明:model=yolov8n.pt用 nano 版预训练权重做迁移学习,医学图和 ImageNet 分布差异大,但底层的边缘、纹理特征仍可用,比从零训练收敛快得多。imgsz=640是默认输入尺寸,显存小于 16G 就别往上加。patience=25表示连续 25 个 epoch 验证指标不提升就提前停,医学小数据集通常 60~80 epoch 就收敛了,不用真跑满 120。lr0=0.003比默认的 0.01 保守,原因在第 5 章避坑里细说。
训练结束后去runs/skin/baseline/看结果,重点看results.png里的 loss 曲线和weights/best.pt。best.pt 是按验证集指标保存的最优权重,后面验证、推理都只用它,不要用last.pt——那是中途或结束时的保存,指标通常更差。
3.4 验证与推理:看 mAP 也要看具体错在哪
训练完先跑验证,拿到量化指标:
yolo detect val \ model=runs/skin/baseline/weights/best.pt \ data=skin_cancer.yaml输出里两个数最要紧:mAP50是 IoU 阈值 0.5 下的平均精度,mAP50-95是 0.5 到 0.95 每隔 0.05 的平均。医学检测场景我优先看 mAP50,病灶框允许一定误差,框大一点小一点不影响判读;mAP50-95 低说明框边界不精,多半是标注本身就糊。之后再抽几张验证图跑推理:
yolo detect predict \ model=runs/skin/baseline/weights/best.pt \ source=/home/yourname/datasets/skin-cancer/valid/images \ conf=0.25 \ save=Trueconf=0.25表示置信度低于 0.25 的预测框直接丢弃,save=True把带框的结果图保存下来。这一步是给「人眼」看的——指标再漂亮,都要直接看图判断模型学的是病灶本身还是皮肤纹理背景。我习惯把误检图单独收一个目录,后面调阈值、调增强时反复对比。
4. 训练效果调优:从 mAP 到临床可用性的参数调整
4.1 输入尺寸与数据增强:医学图像不能照搬自然图像的套路
baseline 跑通只是第一步,要让它接近临床可用,参数得重调。第一个动的是输入尺寸。皮肤镜照片里病灶往往只占整图的 10%~20%,imgsz=640 时小病灶缩到几十个像素,特征基本被压没了。显存够的话,把 imgsz 提到 960 试一轮,mAP50 通常能涨 3~5 个点,代价是训练时间翻倍、batch 要跟着降。如果显存只有 8G,imgsz=960 配 batch=4 是极限,再往上就得换 640。
增强策略也要改,这里最容易翻车。YOLOv8 默认开 mosaic、hsv 变换等一堆增强,在自然图像上很有效,但皮肤癌数据要慎用。mosaic 把 4 张图拼成一张,病灶被切碎再缩放,等于给模型生成现实中不存在的样本;hsv 色相扰动直接改变皮肤颜色,而皮肤病诊断极度依赖颜色信息,红色斑块被改成紫色,模型学到的颜色特征就废了。我实际用的配置:
yolo detect train \ model=yolov8n.pt data=skin_cancer.yaml \ imgsz=960 batch=8 epochs=120 \ hsv_h=0.01 hsv_s=0.2 hsv_v=0.2 \ mosaic=0.3 close_mosaic=10hsv_h=0.01把色相扰动压到几乎不变,mosaic=0.3降低 mosaic 的参与概率,close_mosaic=10让最后 10 个 epoch 完全关闭 mosaic,让模型在真实分布上做最后收敛。这几项改动在色彩敏感的医学数据上,收益比调学习率还明显。从那以后,我在任何医学数据集上都会先问一句:默认增强会不会把类别关键特征改掉,再决定开多大。
4.2 类别不均衡:先统计,再决定用哪种手段
医学数据集几乎没有类别均衡的,黑色素瘤样本少、良性痣样本多,模型天然偏向多数类。先跑 2.3 节的统计脚本拿分布,样本量差距在 3 倍以内不用大动干戈,正常训练;超过 5 倍就要干预了。
轻量做法是复制少数类样本,让模型每个 epoch 多看到它们几遍。写个简单脚本按类别 ID 复制:
import shutil from pathlib import Path base = Path("skin-cancer/train") target_class = "5" # Melanoma dup_count = 2 # 每张图复制 2 份 for txt in (base / "labels").glob("*.txt"): lines = txt.read_text().strip().splitlines() if any(line.split()[0] == target_class for line in lines): img = base / "images" / (txt.stem + ".jpg") for i in range(dup_count): shutil.copy(txt, base / "labels" / f"{txt.stem}_dup{i}.txt") shutil.copy(img, base / "images" / f"{txt.stem}_dup{i}.jpg")脚本逻辑很简单:遍历 train 的标签,只要某张图包含 Melanoma,就把它的图片和标签各复制两份,文件名加_dup0、_dup1后缀。注意只复制 train,val 绝对不能动,否则验证指标失真。更稳的做法是两阶段训练:第一轮全量数据训出 baseline,第二轮针对 mAP 接近 0 的难分类别单独微调。样本太少时,任何高级采样技巧都不如让模型多看几遍数据管用,这是我做过几个小样本医学项目后的朴素结论。
4.3 迁移学习与骨干网络:nano 起步,m 版本收尾
医学数据量小,从头训练基本不可行,必须迁移。但预训练权重的使用要分层看:底层卷积学到的边缘、纹理特征对皮肤镜照片仍然有效,深层特征偏向自然物体,所以训练时要控制骨干网络的更新幅度。
我习惯先用yolov8n.pt跑通全流程,确认数据和配置没问题,再换yolov8m.pt加冻结策略冲精度:
yolo detect train \ model=yolov8m.pt data=skin_cancer.yaml \ imgsz=960 batch=8 epochs=100 \ freeze=10 lr0=0.001freeze=10冻结前 10 层,让它们在前几个 epoch 保持 ImageNet 学到的通用特征,只更新检测头;lr0=0.001比 baseline 更低,因为 m 版本参数更多,学习率大容易震荡。这一轮跑完 mAP50 通常能从 nano 的 0.7 上下涨到 0.8 以上。显存不够跑 m 版本的话,yolov8s.pt是次优选择,参数量介于 n 和 m 之间,8G 显存也能跑。
注意:换模型前把
runs/skin/baseline整个目录改名或备份,别让两次训练的曲线混在一起,后面对比全是脏数据。
5. 医学目标检测避坑指南:5 个真实踩坑记录
这章全是血泪经验,下面的坑我基本都亲手踩过。每条按「现象 → 原因 → 解决」写,直接对照排查。
5.1 现象:训练 loss 前 20 轮不降反升,验证 mAP 一直是 0
原因:学习率过大加 batch 太小。默认lr0=0.01在 COCO 那种大数据集上没问题,但在 1256 张图的小样本场景里梯度噪声大,0.01 直接震荡,loss 完全压不住。batch 小于 8 时尤其明显。
解决:把lr0降到 0.003,同时设warmup_epochs=3让学习率前 3 个 epoch 线性爬升。改完之后 loss 前 10 轮就能稳定下降,mAP50 在第 20 轮左右开始出现非零值。如果降学习率还压不住,检查 batch:8G 显存跑 imgsz=640 时 batch=8 是底线,再小梯度就没法收敛。
5.2 现象:验证集 mAP 0.85,换真实照片却大量误检
原因:数据同源泄露。这套皮肤癌数据如果来自少数病例拍摄批次,同一来源的多张照片可能同时进了 train 和 val,模型记住的是拍摄背景和光照条件,而不是病灶本身的特征,验证指标虚高,一上真实场景立刻露馅。
解决:按文件名的来源前缀分组切分。比如INN-7这组的图片属于同一来源,组内所有图片要么全进 train、要么全进 valid,按来源而不是按单张图片随机切分。另外准备 20~30 张外部来源的皮肤镜照片做冒烟测试,训练完先跑一遍,掉多少分一眼可见。医学数据集最容易中招的就是这一类问题,因为病例拍摄批次天然成组。
5.3 现象:预测框整体偏移,框和病灶边缘对不齐
原因:标签坐标没归一化,或尺度不匹配。Roboflow 导出时如果原图被自动 resize 过,而标签文件里的坐标仍基于原图尺寸,框就会整张图偏移。另一种情况是标签里混入了绝对像素坐标,YOLO 当成归一化坐标来读,框直接飘到画面边缘。
解决:抽查一个标签文件看数值范围:
import numpy as np p = "skin-cancer/train/labels/intradermal-nevus-INN-7-_jpg.rf.0aa3ba9e55ba2e53ba22be752483ad4b.txt" data = np.genfromtxt(p) print(data.shape, data[:, 1:].min(), data[:, 1:].max())正常归一化坐标的所有值都应该在 0~1 之间。如果最大值大于 1,说明是像素坐标,需要除以原图宽高重新归一化;如果值都在 0~1 但框仍然偏小,多半是标注本身画得紧,无法靠代码修复,推理时把 IoU 阈值从默认的 0.45 放宽到 0.35 稍微缓解。坐标范围检查成本极低,我现在把这件事列为拿到新数据集后的第一个动作。
5.4 现象:训练中途报错,提示 label class 超出范围
原因:标签文件的 class_id 和 YAML 的 nc 对不上。典型的场景是 Roboflow 导出后某个类别被单独删改过,ID 不连续;或者你写 YAML 时 names 的顺序和导出时的 ID 顺序不一致,模型读到 ID=8 的标签但 names 只给了 8 个名字,直接越界崩溃。
解决:训练前全量扫描 train 和 valid 标签的最大类别 ID:
awk '{print $1}' skin-cancer/train/labels/*.txt | sort -n | uniq awk '{print $1}' skin-cancer/valid/labels/*.txt | sort -n | uniq两条命令的输出应该包含 0 到 8,且没有大于 8 的数字。出现 9 或更大的数字,说明有别的类混进来了,先把对应样本挑出来看图,再决定删除还是归并到正确类别。这个检查 30 秒做完,却能省掉一次训练中断浪费的几个小时。
5.5 现象:Melanoma 的 mAP 接近 0,其他类别都正常
原因:类别易混淆加样本过少。黑色素瘤和复杂痣在外观上高度相似,而 Melanoma 的样本量又少,模型把绝大部分黑色素瘤都预测成了 Complex_moles。这不是 bug,而是数据分布问题,属于医学目标检测最典型的 failure mode。
解决:先看混淆矩阵确认误判去向,再按 4.2 节的方式复制 Melanoma 样本。如果复制后还是压不住,把这两类单独拎出来训练一个二分类模型,在检测框的基础上做二次分类。二分类模型用同一份数据只保留这两个类的标签,epochs 减半就能收敛,效果往往比硬调检测头好。医学检测里「先检测后分类」是很实用的拆法,把 9 分类问题拆成「框出病灶」和「判断性质」两步,每一步都简单很多。
6. 进阶验证技巧:混淆矩阵与置信度阈值校准
6.1 从验证集自动导出混淆矩阵
训练结束别只看 mAP 数字,把混淆矩阵拉出来看一遍,能省下大量盲目调参的时间。Ultralytics 在验证时会自动产出confusion_matrix.png,也可以直接用 Python 读取:
from ultralytics import YOLO model = YOLO("runs/skin/baseline/weights/best.pt") metrics = model.val(data="skin_cancer.yaml") cm = metrics.confusion_matrix.matrix # (nc+1, nc+1) print(cm)矩阵每一行是真实类别,每一列是预测类别,最后一行一列是背景。我拿到手只看三个位置:对角线亮不亮(各类别识别率)、Melanoma 那一行里哪一列的数值最大(被误判成了谁)、背景行有没有大片非零(漏检严不严重)。三个问题对应的调法完全不同——分别对应加数据、做二分类、降阈值。
6.2 置信度阈值:宁可多检,不可漏检
医学检测和安防、交通场景不一样,漏检一个黑色素瘤的代价远高于多框一个良性痣。默认conf=0.25在验证集上 f1 最高,但真正落地使用我会降到一个更保守的值:
yolo detect predict \ model=runs/skin/baseline/weights/best.pt \ source=/home/yourname/datasets/skin-cancer/valid/images \ conf=0.08 \ iou=0.35 \ save=Trueconf=0.08会放出大量低置信度框,iou=0.35让重叠框的抑制更宽松,避免两个相邻病灶被合并成一个。代价是误检变多,但这些误检框在人工复核阶段可以被快速筛掉,漏检则完全没有补救机会。如果是做辅助诊断工具,我一般会把置信度做成可调滑杆,而不是写死一个值,让操作医生根据场景自己权衡。
这套皮肤癌数据集我已经放进自己的数据集清单里,作为皮肤癌检测 benchmark 的默认起步数据。从那以后,我每次拿到新的医学检测数据集,都强制走一遍固定流程:先统计类别分布和标签坐标范围,再按来源分组切分数据,最后看完混淆矩阵才敢谈调优。这套习惯是从皮肤癌数据集的几次翻车里总结出来的,希望帮到你。
本文还有配套的精品资源,点击获取