脸部皮肤病检测数据集:VOC与YOLO格式解析及YOLOv8训练全流程
简介脸部皮肤病检测数据集以 YOLO 与 VOC 双格式提供面向需要训练皮肤病灶识别模型的开发者与学生。压缩包整体约 35.93MB共 2000 个文件其中 1590 个 XML 标注文件与配套 TXT 标签文件构成主体JPEGImages、Annotations、labels 三目录规则清晰便于直接接入 YOLO 或 Faster R-CNN 等目标检测流程。数据覆盖粉刺、丘疹、结节、脓疱 4 类矩形框总数 8827各框数分别为粉刺 3875、丘疹 3041、脓疱 1297、结节 614类别分布不均非常适合练习类别不平衡处理、数据增强与模型调参同时附有图片清单和说明文本可辅助完成训练集/验证集划分与样本核对。图片均为清晰原图未做增强标注采用矩形框可复用于多种检测框架YOLO 与 VOC 两种格式可对比使用降低格式转换成本。目前已有 103 人学习下载适合希望获得规范标注数据并开展皮肤病灶检测实验的个人学习者仅限学习交流使用请勿商用。1. 脸部皮肤病检测数据集一套标注好的病灶检测学习样本做皮肤影像相关的检测任务时最卡人的往往不是模型而是数据。手里没有标注好的病灶图YOLO 再强也跑不起来。这份脸部皮肤病检测数据集正好补上这段路它同时提供 YOLO 和 VOC 两种标注格式图片是真实场景下的脸部皮肤照片每张图都带病灶框和类别标注具体分几类、各有多少张解压后打开 classes 文件就能看到。对正在做毕业设计、个人学习目标检测或者想跑通「数据集→训练→推理」全流程的人来说它把最脏最累的标注环节约掉了剩下的是你真正该练的模型调参与训练部分。本文按我拆数据集的习惯来写先讲两种标注格式的字段构成再给 VOC 转 YOLO 的脚本和参数接着是 YOLOv8 的训练配置最后是几类高频踩坑记录你可以直接按章节顺序复现。2. 数据集结构先看明白YOLO 与 VOC 两套标注的字段映射拿到任何检测数据集我第一件事不是急着训练而是把目录结构和标注文件翻一遍。这个习惯帮我避开了大部分「训练半天不收敛最后发现是标签读错了」的翻车现场。这份数据集的典型布局是图片、VOC 标注、YOLO 标注三个目录分开放下面给出常见组织方式。2.1 目录结构与文件组织解压后一般能看到类似下面的结构路径内容说明images/脸部皮肤照片jpg 格式训练与验证共用的原始图片annos/ 或 VOC/每张图对应的 xml 标注标签名是图片同名扩展名 .xmllabels/ 或 YOLO/每张图对应的 txt 标注标签名是图片同名扩展名 .txtclasses.txt 或 names.txt类别清单每一行一个类别名顺序决定类别 ID我一般会先确认图片和标注文件的数量是否一一对应。在 Linux 或 macOS 上执行ls images | wc -l和ls labels | wc -l两边数量必须一致。如果发现某个目录多文件或少文件多半是前面某一步数据整理时漏掉了这种情况直接拿来训练会出现训练时报「找不到标签」或图片被静默跳过的问题。数量核对通过后再随机打开一张图片和一个标注文件对照着看坐标是否真的贴合病灶区域。这一步虽然花五分钟但能省掉后面排查脏数据的几个小时。2.2 VOC 标注的字段构成VOC 格式来源于 PASCAL VOC 检测任务是一个用 XML 描述目标框的标准结构。一个典型的标注文件长这样annotation folderJPEGImages/folder filenameskin_001.jpg/filename size width640/width height480/height depth3/depth /size object nameacne/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin100/xmin ymin120/ymin xmax300/xmax ymax360/ymax /bndbox /object /annotation这段 XML 里真正决定检测结果的是三组字段filename负责把标注和图片关联起来size里的width和height是后续做坐标归一化的分母object里的name和bndbox则是模型要学的东西。一个文件里有几个 object 节点就代表这张图有几个病灶框。truncated和difficult这两个字段在转换时一般可以忽略difficult1通常表示目标太小或遮挡严重实际使用中大部分数据集的难例已经被人工筛掉了。用 Python 去读这种 XML 有标准库xml.etree.ElementTree就够不需要上 BeautifulSoup。下节转换脚本里我会用root.findall(object)遍历所有框这也是处理 VOC 最常见的做法。有一点值得注意bndbox里的四个值是像素坐标左上角为原点xmin/ymin是框左上角xmax/ymax是右下角坐标值不是归一化的直接喂给 YOLO 之前必须做换算。2.3 YOLO 标注的归一化坐标换算YOLO 的 txt 标注格式和 VOC 完全不同每行对应一个目标框包含五个数字类别 ID、中心点 x、中心点 y、框宽、框高其中后四个值全部是相对图片宽高的比例取值在 0 到 1 之间。拿上面 XML 里的数字算一次图片宽 640、高 480框从 (100, 120) 到 (300, 360)。中心点 x 是 (100 300) / 2 / 640 0.3125中心点 y 是 (120 360) / 2 / 480 0.5框宽 (300 - 100) / 640 0.3125框高 (360 - 120) / 480 0.5。所以对应的 YOLO 行就是0 0.3125 0.5000 0.3125 0.5000。你可以在数据集里随便找一张图手算几行验证自己的理解这个五分钟的小练习比看十篇博客都有用。YOLO 采用归一化坐标不是为了省存储而是为了让模型对不同分辨率的图片有天然的尺度不变性。训练时 YOLO 会把输入图缩放到统一尺寸如果标注还是原始像素缩放之后坐标就全错了。这个换算逻辑在从 VOC 转 YOLO 时是必须处理的拿到手的数据集如果已经帮你转好了你也要能看懂这些数字的含义否则后面排查错检时根本无从下手。3. 把 VOC 转成 YOLO 格式转换脚本与四个边界坑很多数据集只给一种标注这份数据集同时给了两套省了转换的麻烦。但实际场景里「VOC 转 YOLO」仍然是你大概率会遇到的操作比如你自己标了一批数据、或者从其他开源项目拿到 VOC 标注都需要转。这一章给出一个我常用的转换脚本并解释四个容易出现问题的边界情况。3.1 VOC 转 YOLO 的完整脚本以下脚本假设 xml 放在voc_annos/目录图片在images/目录转换结果输出到labels/。类别清单通过classes列表传入顺序决定 YOLO 的类别 ID这个顺序转换后就不能再变训练配置里必须和它保持一致。import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_path, classes, out_dir): tree ET.parse(xml_path) root tree.getroot() size root.find(size) width int(size.find(width).text) height int(size.find(height).text) base os.path.splitext(os.path.basename(xml_path))[0] out_path os.path.join(out_dir, base .txt) with open(out_path, w) as f: for obj in root.iter(object): name obj.find(name).text if name not in classes: continue class_id classes.index(name) 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) / width y_center ((ymin ymax) / 2) / height w (xmax - xmin) / width h (ymax - ymin) / height x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) f.write(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n) classes [acne, blackhead, pigmentation] os.makedirs(labels, exist_okTrue) for xml_file in os.listdir(voc_annos): if not xml_file.endswith(.xml): continue convert_voc_to_yolo(os.path.join(voc_annos, xml_file), classes, labels)脚本的逻辑是逐层解析 XML先从size节点拿图片宽高再遍历每个object节点取类别名和框坐标最后把bndbox的左上角右下角换算成归一化的中心点与宽高。换算公式和上一节手算的完全一致。参数上有几个地方值得调整classes列表的顺序是全局约定我习惯按字母序排列避免后来加类别时插队导致 ID 全部错位clamp那两行把结果约束在 0 到 1 之间这是防止标注超出图片边界的保险f{class_id} {x_center:.6f}保留六位小数精度足够文件体积也不会膨胀。如果你的数据集里存在没有object节点的 XML脚本会输出一个空 txt训练时 YOLO 会把它视为背景图这本身没问题但建议你单独统计一下这种图的数量如果占比超过 5%说明标注漏检严重会影响模型对病灶的召回率。提示转换之前务必备份原始 xml或至少保证voc_annos是只读的。别问我为什么强调这个覆盖了源标注再想找回是真的没有后悔药。3.2 数据划分先分后转还是先转后分这个问题没有标准答案我的习惯是先划分再转换。因为 VOC 的 xml 本身就是完整可读的中间格式万一划分后某类标注出问题重新从 xml 生成 txt 比反过来容易得多。下面这段脚本按 8:1:1 划分训练集、验证集和测试集import random import shutil from pathlib import Path random.seed(42) images sorted(list(Path(images).glob(*.jpg))) random.shuffle(images) n len(images) train_split images[:int(n * 0.8)] val_split images[int(n * 0.8):int(n * 0.9)] test_split images[int(n * 0.9):] for split, files in [(train, train_split), (val, val_split), (test, test_split)]: img_dir Path(fsplit/{split}/images) label_dir Path(fsplit/{split}/labels) img_dir.mkdir(parentsTrue, exist_okTrue) label_dir.mkdir(parentsTrue, exist_okTrue) for img in files: shutil.copy(str(img), str(img_dir / img.name)) label_file Path(labels) / (img.stem .txt) if label_file.exists(): shutil.copy(str(label_file), str(label_dir / label_file.name))这里random.seed(42)是保证每次运行划分结果一致方便复现实验结果。split目录生成三个子集每个子集内部图片和标签保持同名同目录结构。划分时最好按整张图切而不是按病灶框切避免同一张图出现在训练集和验证集里导致验证指标虚高。数据集规模不大时8:1:1 是比较保守的比例如果你的图片总数上了一万可以适当放宽到 9:0.5:0.5验证集就算只留几百张也够用。3.3 转换结果校验别直接开训转换完不能直接训练至少要做两步校验。第一步是统计每个类别的样本数量这能同时发现类别标签错位和类别不平衡问题import glob from collections import Counter counter Counter() for txt_path in glob.glob(labels/*.txt): for line in open(txt_path): class_id int(line.split()[0]) counter[class_id] 1 print(counter)把打印出来的计数和classes列表对照比如acne在列表里是第 0 位那counter[0]应该对应痤疮框的总数。如果发现数量对不上十有八九是某个类别名在 XML 里和classes列表不一致脚本里if name not in classes: continue静默跳过就会导致漏转。第二步是用 OpenCV 把标注画回图片上随机抽几张目视检查框的位置是否贴合病灶区域。这个画框脚本很简短核心就是cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, 2)。我每次转换后都会跑这两个步骤加起来不到十分钟却是训练能否收敛的第一道关卡。4. 基于 YOLOv8 训练自定义皮肤病检测参数选型与损失调优数据集准备好了接下来是训练环节。本章以 YOLOv8 为例讲参数配置因为它是目前入门成本最低的框架一行命令就能训练同时保留了足够的调参空间。先建一个 data.yaml 描述数据集路径和类别然后选择合理的训练参数再讨论损失函数中类别不平衡的应对。4.1 用 data.yaml 挂载数据集data.yaml 是 YOLOv8 读取数据集的核心配置路径建议用绝对路径避免在不同目录下训练时报文件找不到的错误。以下是一个模板path: /home/you/facial-skin-dataset train: images/train val: images/val test: images/test names: 0: acne 1: blackhead 2: pigmentationnames的顺序必须和 VOC 转 YOLO 时classes列表的顺序完全一致这是最容易出问题的地方。如果你在转换时把acne放在第 0 位yaml 里也必须是第 0 位一旦错位模型会拿着痤疮的框去学黑头训练出来基本是废的。path后面如果写相对路径YOLO 会从当前工作目录去找我见过不少人在服务器上换了个目录执行训练命令就说数据集加载失败最后发现是 path 写得太随意。还有一点train和val的值是相对于path的路径不要再加images/train/images这样的重复嵌套保持目录结构与 data.yaml 一一对应即可。4.2 训练参数选型小目标场景怎么设脸部皮肤病检测属于典型的小目标场景。病灶区域往往只占整张脸的几个百分点直接按默认参数训练精度会比较勉强。我从实际应用中整理了一份参数起点表参数推荐值说明imgsz640 或 960病灶小就提分辨率显存够就上 960batch16按显存调整不够用梯度累积epochs100配 early stop不必硬跑满lr00.01预训练模型微调可降到 0.005optimizerauto让框架自己选或显式指定 AdamWpatience15验证集指标连续不提升就提前停cos_lrTrue余弦退火后期收敛更平缓训练命令如下yolo detect train \ datafacial_skin.yaml \ modelyolov8n.pt \ imgsz640 \ batch16 \ epochs100 \ lr00.01 \ optimizerauto \ patience15 \ cos_lrTrueimgsz是这张表里最值得动手调的参数。从 640 提到 960小目标的 AP 通常能涨三到五个点代价是显存占用和训练时间几乎翻倍。如果你跑在 8G 显存的卡上960 配 batch 16 大概率 OOM可以降到 batch 8 或者退回 640。lr0用预训练权重微调时不用给太大0.01 是个安全的起点如果你发现训练前期 loss 震荡得厉害直接降一半到 0.005。cos_lrTrue会让学习率在训练后期逐渐衰减到接近零对小数据集来说能明显减少过拟合现象我一般都会开。4.3 损失函数与类别不平衡处理YOLOv8 的损失函数由三部分组成回归框的 box loss、分类的 cls loss、以及 DFL 损失其中 DFL 负责细化边界框的分布。训练日志里显示的cls_loss和box_loss就是前两部分dfl_loss比较小但不要因为它小就忽略它的作用体现在最终推理时边界框的精确定位上。类别不平衡在皮肤病数据集里几乎是必然的。某种病灶特别常见另一种只有几十个框模型会偏向学样本量大的类。应对方式有两种数据层面做难例过采样把样本少的类别对应图片在训练时重复喂几次通过repeat参数控制或者在损失层面给稀有类别更高的权重。YOLOv8 里可以给不同类别设置 loss 权重具体在 model 配置里修改不同类别的 cls loss 系数。我一般先看训练完的混淆矩阵再决定要不要动手——如果只是轻微偏斜优先加训练轮次和调注意力如果某个类 AP 低于 0.3就必须处理数据了。这里也顺带提一句很多人纠结 COCO 的 80 个类别在 YOLO 里怎么读其实类别顺序完全由自己的classes列表定义COCO 权重预训练只是借用了特征提取能力分类头会被替换所以不要被 80 类这个数字带偏。5. 避坑指南类别错位、小目标漏检与训练不收敛这部分是血泪经验汇总。以下每条都是我在类似数据集上真实遇到过的坑按「现象→原因→解决」的方式记录你可以逐条对照。5.1 训练完模型把所有图都检出同一个类现象模型训练 100 轮验证集 mAP 看着还行但推理时不管什么病灶图片输出结果全是同一个类别框的位置还特别粗犷。原因这是最典型的类别 ID 错位。VOC 转 YOLO 时classes列表顺序和 data.yaml 的names顺序不一致导致模型学到的是「第 0 类 黑头」而你的标注里「第 0 类 痤疮」的错误映射。还有一种情况是多个类别在 XML 里名字有细微差异比如acne和acne带了个空格脚本里classes.index(name)直接跳过所有带空格的类别全部落到后台。解决回到第 3.3 节用统计脚本把每个类别的标注框数量拉出来和classes.txt逐项核对。转换脚本里对name做一次strip()把首尾空格清掉。如果你已经训了一半才发现赶紧停掉修好映射重新转标签别硬着头皮继续跑。5.2 训练到一半 loss 弹出 NaN现象训练日志前几十个 step 都正常某个 epoch 开始box_loss直接变成nan后面全部是nan验证集的 mAP 也跟着崩掉。原因标注框出现越界值。比如 XML 里xmax大于图片宽度或者 txt 里的归一化坐标大于 1。数据集中标注工具偶尔会留下这类脏数据尤其是手工标注后批量修改图片尺寸时坐标没有同步更新。解决转换脚本里把 clamp 操作加上把越界值强制压回 0 到 1这能兜住大部分问题。同时写一个扫描脚本找出所有「宽高小于 0」或「中心点加半宽大于 1」的标注行输出图片名和行号人工确认后从标注文件里删掉或修正。这类脏数据占比通常极低删掉对整体精度影响可以忽略。5.3 小目标病灶的整体漏检率居高不下现象验证集上正常尺寸的病灶检测得不错但很小的病灶一个也没检出PR 曲线里 recall 在低置信度区间断崖式下跌。原因脸部的痘痘、黑头这些病灶在 640x640 的输入下可能只有十几个像素。YOLOv8 下采样倍数高小目标的特征图分辨率不足经过几层卷积后细节信息基本丢失。这是模型结构层面的限制不是调参能完全解决的。解决第一选择是提imgsz640 提到 960小目标的像素量几乎翻两倍AP 提升明显。第二选择是训练时开启多尺度训练YOLOv8 支持scale参数在训练中随机缩放输入让模型见不同大小的目标。还有更进阶的方案是拿训练好的模型做 SAHI 切片推理把大图切成重叠小块分别检测再合并这个方案在推理阶段对小目标召回率提升非常明显代价是推理速度变慢。5.4 显存溢出导致训练中断现象训练命令跑了几分钟报错CUDA out of memory进程直接退出。改小 batch 后进到训练又报同样的错。原因显存分配不只是 batch 和 imgsz 两项决定的。数据集里图片分辨率远超imgsz时瓶颈在数据加载阶段开启 Mosaic 增强会额外占用显存做拼接这也是隐性开销。很多情况下你以为改小 batch 够了实际是worker数和缓存设置的问题。解决先按显存减半 batch比如 batch 16 改成 8 再跑。如果还 OOM把cacheTrue改成cacheFalse让数据不缓存到显存中占用存量空间。再往下是检查workers数据加载进程数设成 4 到 8 通常足够开太多反而占内存。真不行就换小模型yolov8n 换 yolov8s 显存开销差距接近一倍精度损失可控。5.5 训练 loss 一直降不下去现象训练从第一个 epoch 起cls_loss就在 2 以上跑了 30 轮几乎没有变化验证集 mAP 一直贴着 0。原因排除标签错位和脏数据后最常见的是学习率设置问题。数据集很小的情况下 lr00.01 可能偏大模型在损失曲面震荡始终跳不进收敛区。其次是没有开启cos_lr训练后期步长固定loss 卡在底部下不去。还有一种情况是类别特别不均匀多数的那个类把 loss 主导了小类几乎学不到东西。解决把 lr0 从 0.01 降到 0.005 或 0.001 再训。开cos_lrTrue让学习率后期平滑衰减。检查第一个 epoch 结束时各类别的 loss 贡献如果某类占比过高回到数据层面做重采样。还有一种玄学但有效的手段把模型换成预训练权重继续微调而不从随机初始化开始预训练特征对小数据集收敛帮助很大。6. 验证环节用混淆矩阵和实际推理确认模型可用训练完不是看个 mAP 就完事了最终要回答的问题是「模型在我自己的真实照片上靠不靠谱」。我会分两步做验证先看指标文件里的混淆矩阵再做一次脱离数据集的实拍推理。6.1 从混淆矩阵定位系统性错检YOLOv8 训练结束后runs/detect/train/目录下会生成confusion_matrix.png和results.png。混淆矩阵的每一行是真实类别每一列是预测类别对角线上的数字越高越好。拿到这张图先看两类位置一是某个类别被大量预测成另一类说明这两个类在视觉上高度相似比如痤疮和黑头在红肿期颜色接近模型区分困难二是背景列的值偏高说明模型把大量非病灶区域当成了目标需要调高置信度阈值。另一个文件results.png里包含val/box_loss和val/cls_loss两条曲线正常情况应该在训练后期趋于平缓如果验证集曲线在某个 epoch 后开始向上翘就是过拟合信号。过拟合时优先考虑加数据增强、减小模型尺寸、或者提前结束训练不要硬加 epoch。6.2 真实图片推理验证指标是抽象的最终还得看推理结果。我习惯准备几张没参与过训练的数据集外照片用训练好的权重跑一遍from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourcetest_images/, conf0.25, iou0.45, imgsz640, saveTrue, save_txtTrue )conf0.25是置信度阈值低于这个值的框会被丢弃对小目标场景可以降到 0.15 观察漏检情况但会出现大量误检找一个平衡点要靠多跑几张图对比。iou0.45是 NMS 的 IoU 阈值值越大保留的框越少。saveTrue保存可视化图save_txtTrue额外输出标注文件方便你对坐标精度。看了推理图之后我会顺手把save_txtTrue生成的 txt 和图片放在一起逐张检查重点看三个点框有没有漏掉明显的病灶框边缘是否贴住病灶边界有没有把眼睛、鼻孔这类正常器官误检成皮肤病。这三类问题在 mAP 指标上未必能直接反映但直接影响实际使用者的观感。从那以后我拿到任何新数据集都会强制先跑一遍「类别统计 → 转换校验 → 小样本训练 → 真实图推理」这四步再谈正式训练和调参。这套流程已经帮我避开了至少三种会在后期返工的坑希望帮到你。本文还有配套的精品资源点击获取