多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

皮肤癌目标检测数据集实战:YOLO训练避坑与患者级划分指南

皮肤癌目标检测数据集实战:YOLO训练避坑与患者级划分指南 简介皮肤癌目标检测数据集是一个面向医学影像场景的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, encodingutf-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_okTrue) os.makedirs(f{split}/labels, exist_okTrue) 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 对越界标签会算出 nanclip 是最省事的修复手段。4. 用YOLO系列跑通皮肤癌检测环境配置、模型选型与关键参数4.1 最小环境配置从零到能跑yolo train先解决环境再谈调参。ultralytics 的安装对新手已经足够友好不需要手工编译 CUDA 算子pip 装完就能跑。如果你之前没配过按这个顺序来大概十分钟就能到第一个训练命令conda create -n skin python3.10 -y conda activate skin pip install ultralytics nvidia-smipython3.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 modelyolov8n.pt sourcetest.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怎么定训练命令本身不长难点在参数值怎么定。一张参数表把最常见的配置列出来然后逐一解释参数建议值说明dataskin.yamlpath、train、val、names 四项必须齐modelyolov8s.pt 或 yolov11s.pt数据量小从 s 起步imgsz640 或 960小病灶多优先 960batch166GB显存/ 3212GBOOM 就减半epochs150-300必须配合 patience 早停patience20-30val mAP 连续不涨就停lr00.01迁移/ 0.005从头训迁移学习用默认就好scale0.3 左右增强里缩放幅度要克制yolo detect train \ dataskin.yaml \ modelyolov8s.pt \ imgsz640 \ batch16 \ epochs200 \ patience20 \ scale0.3这条命令里最容易被忽略的是scale0.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的博弈现象mAP0.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 模式带splittest跑一遍我们划分好的独立测试集from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) metrics model.val(dataskin.yaml, splittest, plotsTrue) print(metrics.box.map) # 总 mAP0.5:0.95 print(metrics.box.maps) # 每个类别的 APplotsTrue会生成混淆矩阵和 P-R 曲线混淆矩阵在医疗场景里比 mAP 更直观它告诉你哪些类别之间被搞混了——最常见的混类是黑色素瘤和痣。分类别 AP 如果某类挂零检查是不是训练样本太少还是标注本身有问题。6.2 从固定类别到开放词汇与多模态分析扩展方向怎么试如果你的业务需求不只是识别这几个固定类别而是想覆盖更多病变类型固定类别的检测器就需要重新训练成本很高。开放词汇目标检测的思路是让模型根据文本描述去定位目标遇到新类别不用重训。但开放词汇模型在皮肤病灶上的表现没有在通用物体上好医学词汇的语义空间和自然图像差异大迁移时要做好效果打折的准备。另一个方向是把检测结果和病灶的颜色、纹理特征接起来做多模态分析检测只是第一步后续的判断建议参考组织病理而不是只看模型输出。我的建议很直接如果目标明确就是识别那几类常见病变老老实实训练固定类别检测器稳定且可控如果想做探索性研究再去试开放词汇别把它当生产方案的默认选项。6.3 部署端的输入规范与低置信度兜底模型训练好了部署时还有一个容易翻车的环节拍摄设备的图像和训练集分布不一致。手机拍的临床照片和皮肤镜图像的成像方式完全不同直接丢给模型效果很难保证。部署端要做的三件事统一缩放并保持长宽比用灰色填充短边不要拉伸变形关闭自动白平衡或者说至少不要在预处理里主动改颜色模式否则模型学到的颜色特征全部失效置信度阈值设在 0.25 而不是默认的 0.5皮肤癌漏检的代价远高于误检低阈值配合人工复核才是医疗场景的合理姿态。我自己的教训是早期做皮肤病灶检测总想着把模型调到能端到端自动出结论后来发现临床场景里模型的作用应该是筛出可疑病灶让医生复核而不是替医生下诊断。把置信度阈值调低、增加人工兜底环节之后整个系统的实用价值反而高了很多。数据清洗、按患者划分、分类别评估这些基础工作枯燥且不性感但它们决定了模型能不能真正走出训练集。希望这些踩坑记录和参数设定能帮你在皮肤癌目标检测这条路上少走几步弯路。本文还有配套的精品资源点击获取
返回列表