ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

痤疮检测数据集YOLOv8格式全解析:从标签转换到训练避坑

痤疮检测数据集YOLOv8格式全解析:从标签转换到训练避坑 简介面向深度学习与计算机视觉研究者的痤疮检测数据集包含927张JPG图像及对应YOLOv8格式标注txt文件并附yaml配置可直接用于训练、验证与测试目标检测模型。数据集已按机器学习标准流程划分训练集、验证集和测试集标注采用YOLOv8规范的边界框格式便于在Ultralytics YOLOv8框架中快速启动实验对于皮肤科辅助诊断、远程医疗影像分析等场景可作为模型训练与评估的基础语料。整个资源压缩包共1855个文件其中927个jpg图像、927个txt标注文件和1个yaml配置文件整体大小约33.93MB结构简洁无需额外格式转换。目前已有329人学习下载适合具备一定YOLO使用基础的研究者或开发者用于痤疮定位、检测模型调优及泛化能力验证。借助该数据集使用者可复现目标检测训练流程围绕小目标、遮挡等难点开展针对性优化积累医疗影像AI落地的实战经验。1. 痤疮检测数据集改成YOLOv8格式927张JPG到底能干什么一个只有927张图片的痤疮数据集放在今天动辄几万张的公共数据集面前确实不起眼但它把“从皮肤照片到可训练检测模型”这条链路压缩到了最短。图像是JPG标签是YOLOv8标准txt类和坐标一一对应框架拿到手就能直接开训。对算法工程师来说这套数据的核心价值不是规模而是格式干净、场景聚焦能用来快速验证痤疮检测可行性也能当迁移学习的地基。常见应用诉求其实非常具体皮肤科门诊拍一张面部照片自动标出粉刺、炎性丘疹和脓疱的位置与数量护肤类App对用户自拍做即时皮肤评估皮肤影像科研里做病灶密度统计。这些需求都落在目标检测范畴而YOLOv8几乎是目前最容易上手的选择。适合读这篇内容的人大致有三类刚接触YOLO数据格式的开发者想拿真实皮肤数据练手的算法实习生以及做医疗AI预研但还没有自采数据的小团队。2. 从JPG到YOLOv8格式文件组织、标签坐标与类别定义拿到数据集后先别急着训练。第一件事是搞清楚文件组织方式和标签格式。YOLOv8对数据格式的要求非常严格标签文件里多一个空格、少一个字段训练时要么直接报错要么模型学着学着就跑偏。这一章把格式层面的三个关键点拆开讲对应的是你拿到手之后第一步要做的三件检查。2.1 一张JPG配一个txt这套数据的文件组织方式YOLOv8数据集的常规组织方式是images/和labels/两个根目录下各自按train、val、test划分子目录。图像文件名与标签文件名必须完全相同只是一张是.jpg另一个是.txt。以927张图为例常见目录结构是这样dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/打开任意一个txt标签文件内容格式是每行五个字段类别id、归一化中心点x、归一化中心点y、归一化框宽、归一化框高。与VOC的x_min、y_min、x_max、y_max像素坐标写法不同YOLO标签统一采用相对坐标这也是它能在不同分辨率图像间通用而不需要额外处理的原因。# 读取一个标签文件检查YOLO标签是否合法 label_path labels/train/acne_001.txt with open(label_path, r) as f: lines f.readlines() for line in lines: parts line.strip().split() if len(parts) ! 5: print(非法标签行:, line.strip()) continue cls_id int(parts[0]) x_c, y_c, w, h map(float, parts[1:]) print(f类别{cls_id}, 中心({x_c:.4f}, {y_c:.4f}), 宽{w:.4f}, 高{h:.4f})是不是所有图片都平铺在一个目录下也行理论上可以YOLOv8的train和val字段可以指向同一目录运行时不会报错但那样验证集和训练集会重叠mAP没有任何参考意义。所以拿到数据集后要做的事情就是确认images和labels下面是否已经按split分好没有的话用第3章的脚本自己分。2.2 归一化坐标YOLOv8标签公式与边界值处理标签里的坐标都落在0到1之间这是YOLO格式的关键设计。训练时无论输入图像是600像素宽还是4000像素宽标签都在同一个尺度上模型不需要关心分辨率带来的坐标漂移。如果原始标注是像素坐标到自己转换时用下面这个公式# 从像素坐标转YOLO归一化坐标 # 四个像素值分别表示目标框的左、上、右、下边界 x_min, y_min, x_max, y_max 120, 80, 240, 190 img_w, img_h 640, 480 x_center ((x_min x_max) / 2) / img_w y_center ((y_min y_max) / 2) / img_h width (x_max - x_min) / img_w height (y_max - y_min) / img_h # 写入txt的四个数值依次是 x_center, y_center, width, height print(x_center, y_center, width, height)这套公式本身不难但有一个经常踩坑的细节某些标注工具导出的YOLO标签目标框边界会正好落在图像边缘比如框顶边贴住上边缘。这时height或y_center计算出来可能等于0甚至因为浮点误差变成负值。训练时这种非法坐标不一定报错但会让loss在对应样本上变成NaN然后整轮训练直接翻车。遇到坐标越界的标签稳妥处理是把越界值clip到0和1之间前提是宽高仍为正数。如果校验后发现大量标签越界多半是标注工具的导出设置不对建议重新导出靠clip掩盖问题会污染模型对边界目标的判断。2.3 先看清类别分布再决定单类还是多类训练类别数量直接决定模型输出层的设计。有的痤疮数据集只标了一个类就叫acne有的细分了粉刺、炎性丘疹、脓疱等多个类。打开任意一个txt文件看第一列取值的集合就清楚了。要统计整个数据集的类别分布可以跑这段# 统计所有分片里出现过的类别id和数量 import os from collections import Counter all_cls Counter() for split in [train, val, test]: label_dir flabels/{split} for fname in os.listdir(label_dir): with open(os.path.join(label_dir, fname), r) as f: for line in f: parts line.strip().split() if len(parts) 5: all_cls[int(parts[0])] 1 print(类别分布:, dict(all_cls))如果统计结果只有一个类别0训练配置就相当简单nc1。如果有多个类别需要重点观察各类别间的目标数量差距。比如类别0有3000个目标类别1只有200个目标模型训练后期几乎会忽略类别1因为多数样本的梯度都被类别0主导。处理方法一般是按类别数量加权损失或者对少数类图像做重复采样。反过来如果927张图里不同类别的目标数量差距在5倍以内直接用默认配置就好不必额外干预。3. 把927张图拆成可复现的训练集划分、校验与数据增强格式确认无误后下一步是把图像拆成训练集、验证集、测试集顺带处理少量坏标签。这个步骤看起来简单但它决定了后续所有指标的可信度。训练集和验证集内容一旦重叠模型最终的mAP就是自欺欺人。3.1 划分训练集/验证集/测试集一个脚本解决按8:1:1拆分927张图大约得到741张训练图、93张验证图、93张测试图。验证集和测试集各90多张虽然不算充裕但已经足够给出统计意义上可参考的mAP。拆分一定要在训练之前完成不要训练完再回去补划分否则你无法确认验证集里有没有混进训练图像。# 按 8:1:1 划分图片和同名标签 import os import random import shutil random.seed(42) # 固定随机种子保证结果可复现 image_dir images/all label_dir labels/all images [f for f in os.listdir(image_dir) if f.endswith(.jpg)] random.shuffle(images) n len(images) train_imgs images[:int(n * 0.8)] val_imgs images[int(n * 0.8):int(n * 0.9)] test_imgs images[int(n * 0.9):] for split, split_imgs in [(train, train_imgs), (val, val_imgs), (test, test_imgs)]: os.makedirs(fimages/{split}, exist_okTrue) os.makedirs(flabels/{split}, exist_okTrue) for img_name in split_imgs: shutil.copy(os.path.join(image_dir, img_name), fimages/{split}/{img_name}) label_name img_name.replace(.jpg, .txt) shutil.copy(os.path.join(label_dir, label_name), flabels/{split}/{label_name}) print(ftrain{len(train_imgs)}, val{len(val_imgs)}, test{len(test_imgs)})脚本的逻辑是先把所有jpg文件名打乱再按前80%、80%到90%、最后10%切成三段对应复制图片和同名txt到目标目录。random.seed(42)这行值得多说一句固定种子保证每次运行结果一致你可以把同一组划分用在多轮实验中避免因为数据集不同导致对比不公平。如果你的数据量更大可以把shuffle换成基于文件名哈希的确定性划分效果相同。3.2 标签合法性校验坐标越界与空标签排查927张图说多不多人工逐张核对不现实。我习惯在训练前跑一遍全量标签校验检查三类问题列数不为5、坐标不在0到1之间、宽高为0或负数。任何一个问题都可能让训练过程出现诡异现象。# 检查所有标签文件是否合法 import os bad_labels [] for split in [train, val, test]: label_split_dir flabels/{split} if not os.path.exists(label_split_dir): print(f缺少目录: {label_split_dir}) continue for fname in os.listdir(label_split_dir): path os.path.join(label_split_dir, fname) with open(path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: bad_labels.append((fname, 列数不为5)) continue cls_id int(parts[0]) vals list(map(float, parts[1:])) if any(v 0 or v 1 for v in vals): bad_labels.append((fname, 坐标越界)) if vals[2] 0 or vals[3] 0: bad_labels.append((fname, 宽高为0)) if bad_labels: print(存在问题的标签:) for fname, reason in bad_labels[:50]: print(fname, reason) else: print(全部标签坐标在0~1之间宽高为正数量正常)注意这里还有一个容易被忽略的情况空标签文件。有的图片确实没有任何痤疮目标txt文件是0字节属于正常样本但如果你发现本来应该有目标的图片对应txt是空的那大概率是标注导出漏了。空文件在统计时不会报错但它会作为背景样本参与训练如果数量异常多会拉低模型对痤疮的召回率。所以校验时顺便统计每个txt的行数行数为0的单独列出来人工确认是“真背景”还是“漏标注”。3.3 数据增强针对痤疮小目标的参数设置927张图训练一个目标检测模型数据增强不是可选项是必需品。痤疮病灶通常只占图像很小面积比如一张640x640的训练图里单个粉刺可能只有15x15像素。这种情况下模型很容易过拟合到背景纹理而不是学会识别病灶本身。YOLOv8训练时默认开启mosaic增强和HSV扰动但对皮肤图像来说有些默认参数需要调。# augment参数示例适合皮肤病灶的保守增强配置 # 关键旋转和透视关掉颜色扰动保持温和 yolo detect train \ dataacne.yaml \ modelyolov8s.pt \ epochs300 \ imgsz640 \ batch16 \ degrees0 \ translate0.1 \ scale0.3 \ fliplr0.5 \ hsv_h0.015 \ hsv_s0.4 \ hsv_v0.3 \ mosaic1.0为什么把degrees设成0人脸照片旋转90度或180度在真实采集场景里几乎不会出现强行旋转反而让模型学到不真实的病灶形态。肤质纹理对光照方向敏感过强的HSV饱和度扰动会让原本偏红的炎性丘疹变成暗紫色这不符合临床图像的颜色分布。scale设0.3是因为痤疮目标本身小过度缩放会让目标变成几个像素模型根本学不到有效特征。fliplr保持0.5是因为面部左右对称性真实存在水平翻转不会破坏病灶语义。mosaic增强默认开启把四张图拼接成一张对小目标检测特别有帮助因为拼接后每张子图的尺度被压缩相当于模型看到了更多小尺寸目标。927张图的规模下建议全程开启mosaic不要中途关闭。如果发现训练后期loss波动明显可以在最后50个epoch把mosaic关掉让模型在接近真实分布的图像上微调这是YOLOv8训练里一个实用的小技巧。4. 用YOLOv8把痤疮检测跑起来训练命令、关键参数与结果判定数据准备好接下来进入训练环节。这一章给的是一条能直接复现的训练路径同时把几个决定成败的参数讲透。4.1 先写acne.yaml路径、类别数与名称训练前要准备一个数据集配置文件用来告诉YOLOv8去哪里找图片、一共有几个类别。这个文件是YOLOv8训练的唯一入口写错路径会直接报错写错类别数则会让模型输出维度对不上标签。# acne.yaml path: /data/acne_dataset # 数据集根目录 train: images/train # 相对于path val: images/val test: images/test # 可留空 nc: 1 # 类别数量由第2.3节统计结果决定 names: [acne] # 类别名称顺序与类别id对齐path字段建议写绝对路径特别是你在不同机器之间拷贝项目时。相对路径在本地能跑换一台机器或者换一个工作目录就可能找不到文件。train和val字段填的是相对于path的路径不是完整绝对路径。names列表里的顺序必须和标签里的类别id一一对应id为0对应names[0]如果你把names顺序写反训练不报错但推理时类别名会错位。4.2 一条可复现的训练命令与关键参数YOLOv8的训练入口是ultralytics的CLI最简命令如下yolo detect train \ dataacne.yaml \ modelyolov8n.pt \ epochs300 \ imgsz640 \ batch16 \ patience50modelyolov8n.pt的意思是从预训练权重继续训练不是从头训练。对927张图的小数据集来说用预训练权重做初始化几乎必选因为ImageNet或COCO上预训练得到的特征提取能力可以迁移到皮肤纹理上训练收敛更快最终精度也更高。如果不想用预训练可以改成modelyolov8n.yaml但小数据集从头训练的效果通常明显差一截。batch16在907张图规模下是一个合理起点。显存不够就降到8但不要低于4否则梯度估计的噪声太大会让训练不稳定。imgsz640是YOLOv8默认输入尺寸对痤疮这种小目标场景可以尝试imgsz960代价是训练时间增加约一倍。patience50的意思是验证集指标连续50个epoch没有提升就提前停止省时间的同时也能防止过拟合。单卡训练时建议加上batch-1让框架自动探测显存能容纳的最大batch但自动探测的结果可能偏大导致训练后期loss震荡。实际使用时手动设置通常更可控。# 正式训练带完整增强参数 yolo detect train \ dataacne.yaml \ modelyolov8s.pt \ epochs300 \ imgsz640 \ batch16 \ patience50 \ degrees0 \ translate0.1 \ scale0.3 \ fliplr0.5 \ hsv_h0.015 \ hsv_s0.4 \ hsv_v0.3 \ projectacne_runs \ nameexp1从yolov8n升级到yolov8s参数量从约300万增加到约1100万。对痤疮检测来说这个量级在GTX 1060级别的显卡上仍然可训但精度提升明显。如果你的显存小于6GB还是建议在n和s之间做选择优先把batch保住。4.3 训练结果怎么判定loss曲线、mAP50与PR曲线训练结束后到runs/detect/exp1/目录下看结果。重点是三个文件results.png、confusion_matrix.png、PR_curve.png。results.png里包含训练loss、验证loss、precision、recall、mAP50、mAP50-95六条曲线。前50个epoch内loss快速下降是正常的如果loss在初期就出现尖刺或NaN回头查标签。mAP50是IoU阈值0.5时的平均精度对痤疮这种目标小且密集的场景它是最直观的参考指标。mAP50-95则更严格它的数值通常只有mAP50的六七成两个指标一起看才能判断模型是“大概框对”还是“框得很准”。# 查看训练指标曲线 ls runs/detect/exp1/ # 预期看到 weights/best.pt, results.png, PR_curve.png 等怎么判断模型可用经验标准是mAP50达到0.5以上模型已经能稳定找出大部分明显病灶达到0.65以上具备实际应用潜力。但mAP50高不代表直接能上临床第5章会专门讲从指标到实际效果的落差。5. 痤疮数据集训练避坑指南4个高频问题与排查方案YOLOv8训练流程跑通不难真正折磨人的是那些指标看起来正常、实际效果却不尽如人意的情况。下面是四类我反复遇到的高频问题每一条都给到具体排查思路。5.1 现象一loss稳定下降验证集mAP却一直为0训练过程没有任何报错loss曲线很漂亮但val/mAP50始终是0。最可能的原因有三个第一训练集和验证集存在图片重叠模型在验证集上“作弊”的条件不成立指标反而失真第二标签文件名与图片名对不上验证集大部分txt是空的导致所有预测都按漏检处理第三标签坐标全是0或全是1模型学到了错误的框位置。排查顺序建议是先用脚本比对train和val的文件名集合确认没有交集再随机挑几张验证集图片把它对应的txt标签画到原图上人工确认边框是否包住病灶。如果标签画出来明显不对问题出在标注导出环节而不是训练参数。# 检查训练集和验证集是否有图片名重叠 import os train_imgs set(os.listdir(images/train)) val_imgs set(os.listdir(images/val)) duplicates train_imgs val_imgs print(重叠图片数量:, len(duplicates)) if duplicates: print(list(duplicates)[:10])5.2 现象二mAP50很高实际检测效果却不可用模型在验证集上mAP50达到0.6以上拿去拍一张真实手机照片结果满屏误检。这是医疗影像场景最常见的落差。原因通常是验证集和训练集来自同一批采集设备、同一光照条件而实际场景的光线、角度、肤色差异让模型暴露在分布外输入下。另一个隐蔽原因是训练时数据增强过度尤其是HSV饱和度扰动太大模型学到的是“颜色不对也是目标”这种错误映射。解决思路有两个方向。一是推理时收紧置信度阈值从默认的0.25提到0.35甚至0.4误检会减少但召回也会下降需要根据业务容忍度去试。二是采集少量真实场景图片用训练好的模型做伪标注人工修正后混合进数据集再微调一轮这种增量训练对实际场景提升通常非常显著。5.3 现象三粉刺和炎性丘疹总是漏检痤疮检测最大的痛点是目标小。一张640x640的图像里单个粉刺可能只有10x10像素人眼都需要凑近屏幕才能看清模型漏检完全正常。漏检集中在小目标时优先做三件事把imgsz从640提高到960或1280小目标在更大输入尺寸下能保留更多特征把模型从yolov8n换成yolov8s或yolov8m更大模型对细节的表征能力更强调整推理时的conf阈值到0.15到0.2区间小目标通常置信度偏低阈值太高会把它们过滤掉。# 推理时针对小目标降低置信度阈值 yolo detect predict \ modelruns/detect/exp1/weights/best.pt \ sourcetest_face.jpg \ imgsz1280 \ conf0.15 \ iou0.45注意1280输入会显著增加推理耗时边缘设备上不一定跑得动。如果模型最终要部署到手机端建议训练时就用1280推理时用640配合TTA做补偿而不是反过来。5.4 现象四验证集mAP波动剧烈每轮结果忽高忽低927张图按8:1:1划分后验证集只有约93张。这个体量下几张图上的检测结果就能让mAP上下浮动五六个百分点。模型本身没问题是验证集太小统计波动太大。这是小数据集训练的常态不是bug。处理方式有三种。第一改成K折交叉验证把927张图分成5折每次用4折训练1折验证最终取5次的平均指标这是小数据集上最可靠的评估方式。第二固定一个seed版本在多次实验中使用完全相同的train/val划分保证对比基准一致。第三把最终评估放到test集上test集全程不参与训练也不参与调参只在最终报告时跑一次这样即使val有波动test指标仍然可信。6. 把训练好的模型接到真实皮肤检测场景推理脚本与后处理调优训练只是第一步模型最终要跑在真实照片上。真实场景和训练集的最大差异在于图像来源门诊相机、手机前置摄像头、护肤品App内嵌拍照不同设备拍出来的肤色、光照、清晰度完全不同。推理侧的后处理决定了模型在真实场景里到底是“能用”还是“摆设”。# 推理脚本加载best.pt并对单张图片预测 from ultralytics import YOLO model YOLO(runs/detect/exp1/weights/best.pt) results model.predict( sourcepatient_face.jpg, conf0.25, # 置信度阈值 iou0.45, # NMS IoU阈值 imgsz640, # 与训练尺寸保持一致 saveTrue, # 保存可视化结果 save_txtTrue, # 保存txt检测结果 ) for r in results: boxes r.boxes for box in boxes: x1, y1, x2, y2 box.xyxy[0].tolist() conf float(box.conf[0]) cls int(box.cls[0]) w x2 - x1 h y2 - y1 # 过滤过小目标框宽度小于图像宽度2%的框大概率是噪声 if w 640 * 0.02 or h 640 * 0.02: continue print(f痤疮目标: 置信度{conf:.2f}, 位置({x1:.0f},{y1:.0f})-({x2:.0f},{y2:.0f}))这段代码做完两件事运行标准推理然后在后处理里把小于图像宽度2%的框过滤掉。为什么要过滤YOLOv8对密集小目标容易产生多个重叠的碎片框过滤掉小尺寸预测框能明显减少视觉噪音。实际调参时我习惯把conf先设到0.2看看输出里误检多不多再逐步上调到误检率可接受的临界点。皮肤科场景里漏检一个丘疹比多框一个背景更严重所以阈值不宜拉太高0.25到0.35区间通常比较合适。还有一个实战技巧值得单独说说先做人脸区域检测再做痤疮检测。完整的面部照片里背景、头发、衣物占了大量面积这些区域会产生大量假阳性。先用YOLOv8自带的人脸检测模型框出人脸区域然后只在这个区域内跑痤疮检测误检率通常能下降一半以上。处理流程是两张图输入第一张原图做人脸检测得到裁剪后的面部区域第二张把裁剪区域缩放后送进痤疮模型。这个策略牺牲一点推理速度换来的精度提升非常值得。我自己用927张图训练的小模型最后并没有直接接到临床系统里而是把它当作预标注工具帮医生把门诊照片先框一遍再由医生修正。这个过程让我意识到小数据集模型最务实的应用方式是“辅助人工”而不是“替代人工”。如果你也打算拿这套数据训练模型做类似的事建议把输出格式做成带置信度标注的可视化结果让使用方一眼看出来哪些框是模型高置信度的哪些是低置信度待复核的。这样模型的价值不是“做决定”而是“帮人更快地做决定”落地的阻力会小很多。希望上面这些从格式检查到推理调优的细节能帮你在痤疮检测这条路上少走几步弯路。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进