简介:皮肤癌目标检测数据集是一个面向医学影像场景的YOLO格式数据集,共约1570个样本(训练集1256个、验证集314个),覆盖基底细胞癌、黑色素瘤、银屑病等9类皮肤病变。资源包共2000个文件,其中txt标注文件1570个,jpg图片428张,另有1个yaml配置和1份docx数据集介绍,整体68.16MB,目录按训练/验证划分,便于直接导入项目。数据集可帮助目标检测方向的学习者和研究者快速开展皮肤癌病灶定位、多分类识别等实验,尤其适合黑色素瘤早期筛查相关任务。标注格式与YOLO训练管线衔接顺畅,省去手工整理和格式转换步骤,可用于课程设计、论文验证或行业模型预研。目前已有142人学习下载。
1. 皮肤癌目标检测数据集:比通用数据集难啃的三个原因
下载一份皮肤癌目标检测数据集.zip之前,先做好心理准备:这个zip解压后,大概率没有你熟悉的整齐目录,标注框也未必是规规矩矩的矩形。皮肤病灶边界不规则、病灶与周围肤色对比度低、同一患者的多个病灶图像高度相似——这三件事叠在一起,决定了它和你之前玩过的猫狗、车牌、车辆检测数据集的训练体验完全不同。很多人第一次跑训练,mAP能过0.3就算运气不错,不是模型不行,是数据这个环节先埋了雷。这篇文章要讲的,就是从解压、清洗、划分、训练到避坑的完整落地路径。适合两类人:准备把YOLO系列迁移到皮肤病灶检测的工程师,以及做医学影像课题、需要判断这份数据值不值得投入的研究生。
2. 拆开zip看门道:皮肤癌数据集的目录结构、标注格式与质量筛查
2.1 一份皮肤癌目标检测数据集里通常有哪些文件
不管这个zip是从开放数据集整理来的,还是别人二次打包的,你解压后大概率会看到这几样东西:images目录、标注目录(名字可能是labels、annotations或xmls)、一个meta.csv或README,偶尔附带train.txt/val.txt。皮肤镜图像数据集常见来源是 ISIC、HAM10000 这类公开集合,文件名也常带着ISIC_0000000.jpg这种原始ID,但经过二次整理后,命名可能被改成000001.jpg配000001.txt,所以第一步永远是看 README 或 meta 表,而不是猜。
meta 表是这份数据里最值钱的文件。它通常记着 image_id、患者ID、诊断类别、病灶在身体上的解剖部位。后面做数据划分、类别统计、查重复都要靠它。没有 meta 表的数据集要格外小心,意味着你无法按患者隔离数据,也很难判断类别标签是医生标注的还是自动生成的。
2.2 标注格式与类别设定:从VOC/COCO到YOLO的差异
皮肤癌检测的类别不是随意定的,常见标签就那几个:melanoma(黑色素瘤)、nevus(痣)、basal cell carcinoma(基底细胞癌)、squamous cell carcinoma(鳞状细胞癌)、seborrheic keratosis(脂溢性角化病)。如果你看到的是八分类版本,多半是在这几个基础上加了 actinic keratosis、dermatofibroma 之类。类别之间长得非常像,黑色素瘤和良性痣在早期几乎靠颜色和边缘的细微差异区分,这也是为什么皮肤癌检测比通用检测更依赖数据质量。
标注格式方面,医学标注工具导出的多是 VOC 的 xml 或 COCO 的 json,而训练 YOLO 需要 txt,每行是class x_center y_center width height,坐标归一化到 0-1。转换公式不复杂,唯一要小心的是坐标系的坑:VOC 的 xmin/ymin 是从左上角算的,转成中心坐标时要除以图像宽高:
import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_dir, classes): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in classes: continue box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) x_center = (xmin + xmax) / 2 / w y_center = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{classes.index(name)} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") out_path = os.path.join(out_dir, os.path.splitext(os.path.basename(xml_path))[0] + '.txt') with open(out_path, 'w') as f: f.write('\n'.join(lines))这段脚本的逻辑是遍历 xml 里的object节点,过滤掉 classes 列表之外的类别,再把 bndbox 的四点坐标换算成 YOLO 需要的中心点加宽高。classes.index(name)决定了类别ID,所以 classes 列表的顺序就是你 yaml 里 names 的顺序,这里顺序错,后面训练出来的类别全错位。皮肤病灶的边缘往往不规整,矩形框会把大量正常皮肤包进来,这是标注格式的天然缺陷,后续靠增强和 loss 设计去弥补。
2.3 数据质量筛查:训练前必须先做的三件事
不要一解压就开训。先花 20 分钟做三件事,能省下后面几周的排查时间。第一件事:检查图片能否正常打开。网盘转存、断点续传都会让 zip 里的个别文件损坏,最典型的表现是 png 图片被改成 jpg 后缀,或者文件头损坏,PIL 一打开就报错。第二件事:检查标注是否越界。YOLO 训练对越界坐标很敏感,轻则丢掉目标,重则 loss 变 nan。第三件事:统计类别分布和重复图片。皮肤癌数据集的比例天然失衡,黑色素瘤通常只占一小部分,如果训练集里某个类别只有几十个框,那基本可以预见这个类别的 AP 会挂零。
这一步的产出是一份简单的筛查报告,不需要画图,命令行就能看:统计 images 和 labels 的文件数差、逐行扫描 label 文件里坐标是否在 0-1 之间、用sha256sum或 pHash 查重。等到第五章我会把典型的翻车现象和排查方法展开,这里只需要记住一个结论:数据集类的 zip,质量参差不齐,筛查环节不能跳。
3. 按患者划分训练集:从清洗到完成的预处理脚本
3.1 为什么必须按患者ID划分而不是随机切分
通用目标检测的数据划分是随机打乱,图片之间相对独立,怎么切都行。皮肤癌数据不行。同一个患者的病灶图像,在肤色、纹理、拍摄条件、光照上高度相似,甚至同一颗痣被不同角度拍了两张。如果你按图片随机划分,这两张高度相似的图很可能一张进训练集、一张进验证集,模型等于提前看到了答案,val mAP 虚高到 0.8 以上,一上真实场景直接崩盘。
正确做法是按患者ID分组,同一个患者的所有图像必须落在同一个集合里。这需要 meta.csv 里有 patient_id 字段。如果 zip 里没有 meta 表,一个退而求其次的办法是用文件名前缀推断,比如ISIC_0001234前几位作为患者近似ID,但这是下策,只能减少泄露,不能保证隔离。数据划分比例一般用 70% / 15% / 15%,测试集也要按患者隔离,否则你后面做的所有评估都是自欺欺人。
3.2 图像尺寸与增强策略:颜色增强要克制
皮肤镜图像的分辨率通常很高,常见的是 2048x1536 甚至更高,但 YOLO 训练时一般不会直接吃原图。imgsz 设 640 是起步,如果你的数据里有大量小病灶——在整张图里占比不到 5% 的那种——建议直接上 960。imgsz 越大,小目标保留的像素越多,但显存占用和训练时间也线性上涨,6GB 显存的卡跑 960 会很吃力,需要配合小 batch。
增强策略上,mosaic、随机翻转、旋转、平移都可以开,这些对皮肤病灶检测帮助明显。但有一条边界要守住:不要做剧烈的颜色增强。皮肤癌诊断非常依赖颜色信息,黑色素瘤之所以能被识别,很大程度上是它在色调上异于周围痣。hsv 增强的 h 通道如果调太强,病灶颜色被扭曲,等于把关键特征直接洗掉。我一般把 h 的幅度压到 0.01 以内,s 和 v 可以开到 0.2 左右,保留一定光照鲁棒性但不破坏色相。
3.3 预处理与校验脚本:把zip变成yaml能用的干净数据
这里把一个可落地的清洗加划分流程串起来。整体逻辑是:先读 meta 建立患者到图片的映射,按患者划分出三个集合,再逐张检查图片完整性和标注格式,最后输出 YOLO 需要的目录结构和 train.txt / val.txt / test.txt。
import os import random import shutil from collections import defaultdict random.seed(42) image_dir = "images" label_dir = "labels" meta_path = "meta.csv" patient_to_images = defaultdict(list) with open(meta_path, encoding="utf-8") as f: header = f.readline().strip().split(",") for line in f: parts = line.strip().split(",") row = dict(zip(header, parts)) patient_to_images[row["patient_id"]].append(row["image_id"]) patients = list(patient_to_images.keys()) random.shuffle(patients) n_train = int(len(patients) * 0.7) n_val = int(len(patients) * 0.15) for split, split_patients in [ ("train", patients[:n_train]), ("val", patients[n_train:n_train + n_val]), ("test", patients[n_train + n_val:]), ]: os.makedirs(f"{split}/images", exist_ok=True) os.makedirs(f"{split}/labels", exist_ok=True) with open(f"{split}.txt", "w") as f: for pid in split_patients: for img_id in patient_to_images[pid]: src_img = os.path.join(image_dir, img_id + ".jpg") src_lbl = os.path.join(label_dir, img_id + ".txt") if os.path.exists(src_img) and os.path.exists(src_lbl): shutil.copy(src_img, f"{split}/images/") shutil.copy(src_lbl, f"{split}/labels/") f.write(os.path.abspath(f"{split}/images/{img_id}.jpg") + "\n")这段脚本的关键在第一个循环:按患者分组而不是按图片分组,确保同一患者的图不会散落到 train 和 val 两个集合里。random.seed(42)固定打乱顺序,保证同一份数据多次划分结果一致,方便对照实验。复制文件而不是移动文件,保留原始数据做备份,后面清洗出问题还有后悔药。如果你的磁盘紧张,可以把shutil.copy换成硬链接或直接写相对路径,ultralytics 支持读绝对路径列表,目录结构可以保持扁平。
数据校验的脚本可以独立跑,逻辑是扫描每个 label 文件,检查对应的图片是否存在、能否打开、标注行数是否合法、坐标是否越界:
import os from PIL import Image label_dir = "labels" image_dir = "images" for label_file in os.listdir(label_dir): img_id = os.path.splitext(label_file)[0] img_path = os.path.join(image_dir, img_id + ".jpg") if not os.path.exists(img_path): print(f"[missing] {img_id}") continue try: with Image.open(img_path) as im: w, h = im.size im.verify() except Exception: print(f"[broken] {img_id}") continue with open(os.path.join(label_dir, label_file)) as f: for idx, line in enumerate(f): parts = line.strip().split() if len(parts) != 5: print(f"[bad_line] {img_id} line {idx}") continue _, xc, yc, bw, bh = parts x1 = float(xc) - float(bw) / 2 y1 = float(yc) - float(bh) / 2 x2 = float(xc) + float(bw) / 2 y2 = float(yc) + float(bh) / 2 if x1 < 0 or y1 < 0 or x2 > 1 or y2 > 1: print(f"[out_of_range] {img_id} line {idx}")注意im.verify()只检查文件头,不保证整张图能解码,所以有时校验通过了训练时还会报Image decompression error。遇到这种情况,把 PIL 换成 OpenCV 的cv2.imread再扫一遍,两个库的解码容错不一样,交叉验证更稳。越界坐标的处理,不能只报错不修,我一般直接 clip 到 [0, 1] 区间,YOLO 的 loss 对越界标签会算出 nan,clip 是最省事的修复手段。
4. 用YOLO系列跑通皮肤癌检测:环境配置、模型选型与关键参数
4.1 最小环境配置:从零到能跑yolo train
先解决环境,再谈调参。ultralytics 的安装对新手已经足够友好,不需要手工编译 CUDA 算子,pip 装完就能跑。如果你之前没配过,按这个顺序来,大概十分钟就能到第一个训练命令:
conda create -n skin python=3.10 -y conda activate skin pip install ultralytics nvidia-smipython=3.10不是强制的,3.9 到 3.11 都行,只是 3.10 的兼容性最稳。装完ultralytics会自动带上 torch 的 CPU 版本,如果你的机器有 NVIDIA 显卡,需要用pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121先装 CUDA 版,再装 ultralytics,否则训练时只能看到 CPU,慢到怀疑人生。nvidia-smi的作用是确认驱动和显存,训练前看一眼显存余量,心里有个底。
环境配好之后,验证能不能跑通:随便找一张图,执行yolo detect predict model=yolov8n.pt source=test.jpg,能输出框就说明链路通畅。这一步虽然只花一分钟,但能把环境问题提前暴露,不用等到训练报错才排查。
4.2 模型选型:从v8n起步还是直接上v11
模型选型没有绝对答案,但有一条务实的路线:数据量小、机器配置一般,从 YOLOv8n 或 v8s 起步。皮肤癌公开数据集的规模普遍不大,几千张图就算不错了,n 和 s 级别的模型参数少,不容易把训练集背下来,跑一轮也快,适合快速验证数据质量。如果你的数据已经清洗干净、标注质量高、图片上千张且显存有余量,再考虑 v8m 或 v11m。
YOLOv11 是 ultralytics 仓库里的新系列,训练方式和 v8 完全兼容,数据配置、命令行参数、权重文件格式都通用,从 v8 切到 v11 几乎没有迁移成本。v11 在小模型上的精度比 v8 同规格略有提升,但提升幅度不是换模型就能吃到的——前提还是你的数据经得起推敲。我见过太多人把时间花在对比 v8 和 v11 的零点几个点 mAP 上,忽略了数据划分泄漏这个更大的问题。
另外提一个皮肤病灶场景的特定方向:皮肤病灶边界不规则,有人尝试用旋转框检测,参考 BBAVectors 的思路去预测带角度的框。但这个方向的前提是数据集的标注本身就是旋转框或多边形,如果 zip 里只有轴对齐的矩形框,硬转 OBB 只会引入噪声,得不偿失。标注是矩形的,就按矩形的玩法来做;想要精细边界,那是分割模型的事,不是检测任务该背的锅。
4.3 关键训练参数:imgsz、batch、epochs怎么定
训练命令本身不长,难点在参数值怎么定。一张参数表把最常见的配置列出来,然后逐一解释:
| 参数 | 建议值 | 说明 |
|---|---|---|
| data | skin.yaml | path、train、val、names 四项必须齐 |
| model | yolov8s.pt 或 yolov11s.pt | 数据量小从 s 起步 |
| imgsz | 640 或 960 | 小病灶多优先 960 |
| batch | 16(6GB显存)/ 32(12GB+) | OOM 就减半 |
| epochs | 150-300 | 必须配合 patience 早停 |
| patience | 20-30 | val mAP 连续不涨就停 |
| lr0 | 0.01(迁移)/ 0.005(从头训) | 迁移学习用默认就好 |
| scale | 0.3 左右 | 增强里缩放幅度要克制 |
yolo detect train \ data=skin.yaml \ model=yolov8s.pt \ imgsz=640 \ batch=16 \ epochs=200 \ patience=20 \ scale=0.3这条命令里最容易被忽略的是scale=0.3。ultralytics 默认的 scale 增强是 0.5,意味着训练时图像会被随机缩放到原图的 50%-150%。皮肤病灶本来就小,再被缩到一半,小目标直接变成几个像素,模型学不到任何特征。把 scale 压到 0.3,相当于限制了缩放下限,对小目标友好很多。
关于patience,早停是必须开的。皮肤癌数据量小,模型很容易在 80 个 epoch 左右就过拟合,后面全在浪费算力。patience 20 表示 val mAP 连续 20 个 epoch 没刷新就停,既不会过早截断,也不会在过拟合区间反复横跳。
类别的处理单独说。ultralytics 的检测头没有直接暴露class_weight参数,很多教程说在 yaml 里加 weight 字段,实际并不生效。常见做法是离线过采样:把少数类图片复制几分放到训练集里,让模型多看到几次。另一种是改 loss,给少数类的 cls loss 加权重,但需要改源码,维护成本高。我一般先用过采样跑一版,效果不够再动 loss。评估时不要只看总 mAP,每个类别的 AP 单独打印出来看,黑色素瘤的 AP 才是这个任务真正要关心的指标。
5. 皮肤癌目标检测避坑清单:六条踩坑记录与排查方法
5.1 解压报错与文件残缺:先查zip再谈训练
现象:zip 解压到一半提示CRC failed,或者解压后 images 目录里只有 80% 的图片,标注文件倒是齐的。原因:源文件在网盘转存或下载中断时损坏,压缩包本身不完整。解决:重新下载,下载完成后不要直接双击解压,用命令行unzip -t file.zip先测试完整性;解压后跑一遍 3.3 节的校验脚本,坏文件提前过滤。另外,偶尔会遇到包内文件命名带了空格或中文,YOLO 读路径会直接崩,解压后顺手rename成纯英文小写命名,省得后面手动改 yaml。
5.2 空标注与越界坐标:第一个epoch就nan的元凶
现象:训练在第一个 epoch 就报nan,或提示all labels are empty。原因:部分图片的标注文件是空的,或者 txt 里的坐标越界,归一化后不在 0-1 区间,loss 计算时出现无效值。解决:把空标注对应的图片从训练集移除,越界坐标用 clip 修复。不要天真地以为空标注就是背景样本,YOLO 的 detect 模式里每张图至少要有一个前景框,纯背景图请直接删掉。越界坐标有一种隐蔽情况:VOC 转 YOLO 时算错了分母,用的不是图像宽高而是标注里的某个错误尺寸,转换脚本要自己写一遍或仔细核查,不要拿网上没验证过的轮子直接用。
5.3 类别不平衡:黑色素瘤AP长期为0
现象:总 mAP 看着还行,但打印每个类别的 AP 时,melanoma 那一行永远是 0。原因:黑色素瘤样本太少,整个数据集里可能只有几十个框,模型训练时这类样本的梯度贡献被多数类淹没。解决:先统计类别分布,确认少数类的框数量;做过采样,把少数类图片复制 3-5 份进训练集;评估时按类别看 AP,而不是被总 mAP 骗过去。注意过采样不要复制到验证集和测试集,否则又引入泄露。
5.4 小病灶漏检:imgsz和增强scale的博弈
现象:mAP@0.5 有 0.6 以上,但小目标类别的 AP 几乎为零,皮肤镜图里占比很小的早期病灶一个都检测不到。原因:imgsz 设了 640,原图里 10 像素量级的病灶被压缩到 3-4 个像素,特征图上下采样几轮后直接消失;或者 scale 增强把目标缩得更小。解决:imgsz 提到 960,显存不够就降 batch;把 scale 从默认 0.5 压到 0.3;如果小目标仍然漏,检查标注里小框的比例,过小的框(比如小于 5 像素)直接过滤掉也算一种取舍,虽然会丢样本,但至少不会让模型花精力学噪声。
5.5 验证集虚高:同源图像泄露怎么查
现象:val mAP 高达 0.8,换一批外部图片测试 mAP 掉到 0.3。原因:数据划分没按患者隔离,同一个患者的相似图像同时出现在训练和验证集。解决:回到 3.1 节按 patient_id 重新划分;如果 meta 表没有患者ID,用图片哈希做一次查重,把相似度极高的重复图片从验证集剔除。我自己的血泪经验是:第一次做皮肤癌数据时按图片随机划分,val 跑出 0.82,兴奋了三天,换数据一测原形毕露,白调了两周参数。数据划分这个环节,再谨慎都不为过。
5.6 许可与隐私:医疗数据不能当普通图片处理
现象:有人把二次标注后的皮肤镜图像直接传到公开平台,或者拿一份标注"仅供科研"的数据集去做商业产品。原因:没读 README 里的数据使用许可,忽略了皮肤镜图像属于敏感健康数据。解决:动数据之前先读许可声明,确认能否二次分发、能否商用;内部处理时做去标识化,去掉 meta 里的患者ID;二次标注产物不要公开分享,除非许可明确允许。这不是形式主义,医疗数据的使用边界是这类项目能不能走远的前提,掉进这个坑不是调参能救回来的。
6. 从mAP到临床可用:分类别评估与部署兜底技巧
6.1 分类别分尺寸评估:别被总mAP骗了
训练跑完,输出一堆指标,先别看那个总的 mAP。把每个类别的 AP 单独打印,再按目标面积分成小、中、大三档分别看。具体做法是用 val 模式带split=test跑一遍我们划分好的独立测试集:
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") metrics = model.val(data="skin.yaml", split="test", plots=True) print(metrics.box.map) # 总 mAP@0.5:0.95 print(metrics.box.maps) # 每个类别的 APplots=True会生成混淆矩阵和 P-R 曲线,混淆矩阵在医疗场景里比 mAP 更直观:它告诉你哪些类别之间被搞混了——最常见的混类是黑色素瘤和痣。分类别 AP 如果某类挂零,检查是不是训练样本太少,还是标注本身有问题。
6.2 从固定类别到开放词汇与多模态分析:扩展方向怎么试
如果你的业务需求不只是识别这几个固定类别,而是想覆盖更多病变类型,固定类别的检测器就需要重新训练,成本很高。开放词汇目标检测的思路是让模型根据文本描述去定位目标,遇到新类别不用重训。但开放词汇模型在皮肤病灶上的表现没有在通用物体上好,医学词汇的语义空间和自然图像差异大,迁移时要做好效果打折的准备。另一个方向是把检测结果和病灶的颜色、纹理特征接起来做多模态分析,检测只是第一步,后续的判断建议参考组织病理,而不是只看模型输出。我的建议很直接:如果目标明确就是识别那几类常见病变,老老实实训练固定类别检测器,稳定且可控;如果想做探索性研究,再去试开放词汇,别把它当生产方案的默认选项。
6.3 部署端的输入规范与低置信度兜底
模型训练好了,部署时还有一个容易翻车的环节:拍摄设备的图像和训练集分布不一致。手机拍的临床照片和皮肤镜图像的成像方式完全不同,直接丢给模型,效果很难保证。部署端要做的三件事:统一缩放并保持长宽比,用灰色填充短边,不要拉伸变形;关闭自动白平衡,或者说至少不要在预处理里主动改颜色模式,否则模型学到的颜色特征全部失效;置信度阈值设在 0.25 而不是默认的 0.5,皮肤癌漏检的代价远高于误检,低阈值配合人工复核才是医疗场景的合理姿态。
我自己的教训是:早期做皮肤病灶检测,总想着把模型调到能端到端自动出结论,后来发现临床场景里模型的作用应该是筛出可疑病灶让医生复核,而不是替医生下诊断。把置信度阈值调低、增加人工兜底环节之后,整个系统的实用价值反而高了很多。数据清洗、按患者划分、分类别评估这些基础工作,枯燥且不性感,但它们决定了模型能不能真正走出训练集。希望这些踩坑记录和参数设定能帮你在皮肤癌目标检测这条路上少走几步弯路。
本文还有配套的精品资源,点击获取