
简介痤疮数据集是面向计算机视觉开发者与医疗AI研究者的目标检测资源基于YOLOv8框架构建包含927张JPG图像及对应边界框标注可用于训练痤疮识别模型辅助皮肤科医生进行病情诊断与远程医疗监测免去人工标注成本。压缩包共1855个文件其中含927个jpg图像、927个txt标注文件与1个yaml配置文件压缩包大小约为33.93MBtxt文件已按YOLO格式保存标注坐标yaml文件用于定义类别与路径配置。目前已有329人浏览学习。数据集已按训练、验证、测试集标准划分可直接导入YOLOv8训练流程验证集用于监控过拟合测试集用于评估泛化能力。该数据适用于目标检测模型的训练、验证与评估支持数据增强实验在医学影像分析、智能诊断系统等方向上具有较高的实用价值与研究意义。1. 痤疮数据集 JPG YOLOv8927 张标注图开箱能训吗拿到这套痤疮数据集时我第一反应是927 张 JPG 图像标注已经做成了 YOLOv8 格式是不是解压完就能直接yolo detect train跑起来答案是「可以但别急着跑」。这套资源的价值在于它把图像整理成了标准 YOLOv8 训练目录结构病灶区域用边界框方式标好适合做皮肤影像目标检测的起步数据。如果你之前只用过 VOC 或 COCO 格式的通用数据集会发现 YOLOv8 的 txt 标签有完全不同的组织逻辑——图像和标注文件靠同名绑定类别信息写在data.yaml里任何一个文件名对不上训练时标注就会被静默丢掉。这篇文章按我实际拆包的顺序把目录结构、标签解析、训练配置、翻车点、验证导出和增量训练一次讲完。2. 先拆目录YOLOv8 数据集结构与标签逐行解析2.1 标准目录结构images 与 labels 的配对关系解压这份资源后顶层目录应该是下面这种形状这是 ultralytics 官方推荐的布局也是 YOLOv8 训练时data.yaml最低成本对接方式dataset/ ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ ├── 0002.jpg │ │ └── ... │ └── val/ │ ├── 0501.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 0001.txt │ │ ├── 0002.txt │ │ └── ... │ └── val/ │ ├── 0501.txt │ └── ... └── data.yaml先记住一个关键规则images/train/0001.jpg的标注必定在labels/train/0001.txt扩展名不同、文件名 stem 必须完全一致。训练器加载时就是按这个匹配逻辑去找标签的如果同名 txt 不存在那张图会被当成「无目标」样本损失函数直接跳过它——结果往往是模型对这张图什么都学不到你却以为它训练了。这份资源标称 927 张图常见的划分习惯是 8:2 或 9:1即 742 张训练加 185 张验证或者 834 张训练加 93 张验证。拿到手先确认train和val的实际数量跟 927 对不上也没关系做一次重划分就行。我见过不少人把全部图塞进 trainval 目录空着ultralytics 也能跑但几乎无法判断过拟合不建议这么干。2.2 标签 txt 每行含义类别 ID 加归一化坐标随便打开一个.txt文件内容长这样0 0.485156 0.533333 0.218750 0.288889 1 0.714844 0.468889 0.126563 0.200000每行五个数值从左到右分别是类别 ID、归一化中心点 x、归一化中心点 y、归一化宽度 w、归一化高度 h。这里的cx, cy, w, h全部除以了图像自身宽高取值必须在 0 到 1 之间。如果哪个坐标大于 1说明标注工具导出时用了像素坐标而没有归一化训练时框会歪到图像外面去这是我见过最常见的 YOLO 格式翻车原因。想把归一化坐标转回像素坐标做可视化检查可以写个简单脚本from pathlib import Path label_path Path(labels/train/0001.txt) img_w, img_h 640, 640 # 用你实际图像的宽高替换 for line in label_path.read_text().strip().splitlines(): parts line.split() cls int(parts[0]) cx, cy, w, h map(float, parts[1:]) x1 int((cx - w / 2) * img_w) y1 int((cy - h / 2) * img_h) x2 int((cx w / 2) * img_w) y2 int((cy h / 2) * img_h) print(fclass{cls}, bbox({x1}, {y1}, {x2}, {y2}))这里把中心点坐标减去宽高一半得到左上角(x1, y1)加上宽高一半得到右下角(x2, y2)。注意 YOLOv8 的标签里存的是中心点和宽高不是左上角右下角很多从 VOC 转过来的人会在这一步写错导致画出来的框整体偏移。2.3 类别分布与划分核对先数一遍再动手在写data.yaml之前先用一行命令统计每个类别 ID 出现的次数确认类的 ID 是连续的cat labels/train/*.txt | awk {print $1} | sort | uniq -c输出大概是213 0 187 1 96 2这表示类别 0 出现了 213 次类别 1 出现了 187 次类别 2 出现了 96 次。如果最后一行awk输出的 ID 不是从 0 开始的而是 1、2、3说明标注工具导出时从 1 开始编号需要在前处理里做一次class_id - 1。如果 ID 之间有跳号比如 0、1、3也要先补齐或重映射否则模型输出的类别维度会和data.yaml里的names对不上。我的习惯是统计完再找一个 txt 画一次框肉眼确认框落在病灶上而不是整张图乱飘。这一步花不了两分钟能省掉后面训练完才发现标签错的后悔药。顺便说一句如果发现某些 txt 是空文件优先怀疑标图工具导出了「背景图」——这类图要么删掉要么单独放一个负样本目录处理。3. 跑通训练data.yaml、检查脚本与关键参数3.1 data.yaml 的完整写法与路径陷阱YOLOv8 训练时唯一必须自己写的配置文件是data.yaml。内容不复杂但路径写错会导致训练器直接报错或静默跳过数据。这份资源如果自带 yaml先打开确认如果没有按下面这种写法建一个path: /data/acne_dataset train: images/train val: images/val names: 0: blackhead 1: papule 2: pustulepath指向数据集根目录train和val填相对于path的路径。names列表的顺序就是类别 ID 的顺序names的第 0 项对应标签里的 0第 1 项对应 1以此类推。具体类别名称以你解压后的data.yaml里的定义为准不要照抄我这里的示例。这里有两个坑第一path不要写相对路径因为 ultralytics 在训练时会切换工作目录相对路径经常找不到数据我习惯直接写绝对路径第二Windows 下路径分隔符用反斜杠但 YAML 里反斜杠是转义符稳妥做法是把路径里的\换成/Linux 服务器上则直接用正斜杠。如果path配的是数据集根目录而根目录本身带中文或空格也尽量改成纯英文路径避免编码问题。3.2 训练前最后一道检查坏图、空标签和扩展名很多人解压完数据集直接开训等到训练中途发现 loss 不对才回头查数据浪费时间。我一般会先跑一段快速检查脚本把坏图和缺失标签一次筛出来import cv2 from pathlib import Path bad_imgs [] missing_labels [] for img_path in sorted(Path(images/train).glob(*.*)): if img_path.suffix.lower() not in (.jpg, .jpeg, .png): continue img cv2.imread(str(img_path)) if img is None: bad_imgs.append(str(img_path)) continue txt_path Path(labels/train) / (img_path.stem .txt) if not txt_path.exists(): missing_labels.append(str(txt_path)) continue lines txt_path.read_text().strip().splitlines() if not lines: # 空标签 missing_labels.append(str(txt_path)) print(坏图/无法解码:, len(bad_imgs), bad_imgs[:5]) print(缺标签或空标签:, len(missing_labels), missing_labels[:5])cv2.imread返回None说明文件头损坏或不是完整 JPEG 编码检查标签的存在性和空文件是避免「有图无标注」的静默样本。Path.glob在这里匹配所有扩展名再通过suffix.lower()做过滤是为了照顾*.JPG、*.jpeg这类大小写不一致的情况。如果你的检查结果显示有几百张图找不到标签优先怀疑是images里文件名带_copy、(1)这类后缀或者是图片被重命名过而标签没同步。这种问题靠手动改是不现实的直接用脚本做一次全量重命名更省事具体做法在下一章的 4.1 里给出。3.3 启动训练imgsz、batch、epochs 怎么定检查没问题后就可以跑训练了。以本资源为例命令如下yolo detect train \ dataacne.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ projectacne_work \ namerun1modelyolov8n.pt表示加载 COCO 预训练权重作为起点对 927 张图的小数据集来说用性能最轻的 n 模型起步比直接用 s 或 m 更不容易过拟合。imgsz640是训练时的输入分辨率痤疮病灶在图像里通常占比较小640 不够时可以试 1280但显存占用会翻好几倍。batch要根据显存调6GB 的 GTX1660Ti 跑 n 模型batch 建议 8 到 1612GB 以上的卡可以开到 32 或更大。epochs100作为基准值后面看 loss 曲线再决定加还是减。训练日志和权重会输出到acne_work/run1其中weights/best.pt是验证集 mAP 最高的权重weights/last.pt是最后一轮权重。我一般只用best.pt因为最后一轮往往已经过拟合。训练过程中如果想中途停下看指标直接按CtrlC中断再单独跑yolo detect val就能看当前权重的表现。4. 避坑指南JPG 图像接 YOLOv8 时的五个翻车现场4.1 图像改名而标签没同步静默吞噬标注现象训练时日志里出现WARNING提示image 1/742 ... missing或者训练结束后验证集 mAP 异常低但训练 loss 看起来正常。很多人这时候会去调参实际是数据问题。原因标图完成后为了让文件名更规范手动批量重命名了 JPG但labels下的 txt 还保留旧名字。YOLOv8 加载图片后查找同名 txt 找不到就直接把这个样本当作无目标图片处理看起来「还能训」实际上大量样本被静默丢弃。解决把图片和标签放在同一个循环里同步改名确保永远一起变import os from pathlib import Path for img in Path(images/train).glob(*.jpg): new_name facne_{img.name} new_img img.with_name(new_name) os.rename(img, new_img) txt Path(labels/train) / (img.stem .txt) if txt.exists(): new_txt txt.with_name(facne_{txt.name}) os.rename(txt, new_txt)这里用img.stem从图片名推导标签名再同步修改两个文件。.with_name()会替换文件对象的名字部分保留目录路径。改完之后再跑一次 3.2 的检查脚本确认 missing 数量归零。4.2 类别 ID 从 0 开始标图工具的 1-based 陷阱现象训练能正常跑loss 也下降但抽几张验证图看预测结果类别完全错乱比如本该是黑头的框被识别成脓疱。原因不少标注工具在界面上显示的第一个类别是 1第二类是 2导出 YOLO 格式时直接把 UI 编号写入 txt导致标签里的类别是 1-based。而 YOLOv8 要求类别严格从 0 开始错位后模型的类别语义和names列表整体平移了一位。解决先按 2.3 的方法统计类别 ID如果发现sort | uniq -c输出从 1 开始就批量把第一列减 1。类别不多时可以直接用sedsed -i s/^1 /0 / labels/train/*.txt sed -i s/^2 /1 / labels/train/*.txt sed -i s/^3 /2 / labels/train/*.txt注意执行顺序要从大到小避免先把 2 改成 1导致下一步又把原来的 2 误改。类目超过 4 个时别用一组 sed 硬凑用 Python 读第一列统一减 1 更稳。4.3 EXIF 旋转信息为什么框总是整体偏移现象训练时 loss 降得很快但验证集 mAP50 卡在 0.2 附近画出来的框上下偏移或者左右翻转而且对特定方向的图像偏移程度不一样。原因手机或数码相机拍摄的 JPG 可能会写入 EXIF Orientation 标签。OpenCV 的imread不会读取 EXIF 旋转信息图像加载后方向不变但 ultralytics 训练预处理用的图像几何变换会先读宽高如果原始图像带有 Orientation 标签读取到的宽高和实际旋转后的宽高是反的标签坐标就会偏移。解决训练前用 PIL 过滤一遍所有图像将 EXIF 旋转落地到像素同时清掉 Orientation 元数据from PIL import Image, ImageOps from pathlib import Path for img_path in Path(images/train).glob(*.jpg): im Image.open(img_path) im ImageOps.exif_transpose(im) # 应用 EXIF 旋转 im im.convert(RGB) # 去掉透明通道和多余模式 im.save(img_path, JPEG, quality95)ImageOps.exif_transpose根据 EXIF 的 Orientation 值自动旋转图像convert(RGB)会把 RGBA、灰度图统一成三通道 JPEG避免训练时通道数不一致。处理完成后可以随机抽几张图用第 2.2 节的脚本画框对比框的贴合度会明显改善。4.4 扩展名大小写.jpg 和 .JPG 在 Linux 下是两回事现象数据集在 Windows 上一切正常拷到 Linux 服务器训练后报大量缺标签或找不到图像报错集中在Assertion ... not found。原因Windows 文件系统不区分大小写.JPG和.jpg是同一个文件Linux 严格区分而你标签文件里引用的图像名是.jpg实际文件名是.JPG一旦程序按小写去匹配就落空。解决把images下所有图像扩展名统一成小写并同步修改文件名标签只跟 stem 走扩展名不影响for f in images/train/*; do lower$(basename $f | tr A-Z a-z) mv $f images/train/$lower done同样处理images/val。用tr A-Z a-z把文件名全部转小写连带着把*.JPEG也统一成了*.jpeg。虽然 YOLOv8 能识别jpeg和png但为了后续设备端部署少踩坑建议统一转成.jpg小写扩展名。4.5 空标签和纯背景图loss 震荡的隐藏来源现象训练 loss 一开始就剧烈震荡偶尔还有 NaN但数据量、batch、学习率看起来都正常。原因数据集里混入了空标签图或者图像本身是过曝/纯色背景。空标签的 txt 文件存在但没有任何行YOLOv8 在构建损失时会得到一个空的目标集合纯背景图如果标注是「无目标」一些数据加载器会把它当成负样本处理但如果目录里同时存在标签文件内容为空就会在匹配时产生奇怪行为。解决把空标签图筛出来从训练集移除from pathlib import Path for txt in Path(labels/train).glob(*.txt): content txt.read_text().strip() if not content: img txt.with_suffix(.jpg) print(空标签:, txt, 对应图像:, img) # txt.unlink() # 确认后再注释掉执行 # img.unlink()移除前建议先人工看一眼这些图确认是不是真的背景图。如果你本来就把空标签当成「负样本」用正确做法是把它们整理到一个独立目录避免跟正样本混在同一个train下混在一起会让你后期调阈值的时候根本分不清模型是学不会还是数据太乱。5. 验证与导出从 mAP 指标到 RK3588 部署链路5.1 验证集指标怎么读mAP50、mAP50-95 和混淆矩阵训练结束后用best.pt在验证集上跑一次完整验证yolo detect val \ modelacne_work/run1/weights/best.pt \ dataacne.yaml终端会输出一串指标重点看四列mAP50(B)、mAP50-95(B)、precision(B)、recall(B)。它们的含义如下指标读法mAP50IoU 阈值固定 0.5 时的平均精度均值小目标数据最常用mAP50-95IoU 从 0.5 到 0.95 每隔 0.05 取平均要求框更精准precision所有预测框中真正命中的比例高代表误报少recall所有真框中被模型找出来的比例高代表漏检少痤疮病灶尺寸小边界框通常不到图像面积的 5%这种场景下mAP50的参考价值比mAP50-95更大因为小目标对 IoU 阈值极其敏感。如果mAP50在 0.6 以上而mAP50-95只有 0.3 左右说明框大致位置对了但贴合度不够优先去调imgsz而不是换模型。验证完成后runs/val/exp目录里会生成confusion_matrix.png这个图能直接告诉你哪些类别互相混淆比如结节被识别成囊肿这类信息比单看数字更具体。想画训练过程的 loss 曲线ultralytics 训练时已经在acne_work/run1/results.csv里记录了每个 epoch 的train/box_loss、val/box_loss等字段直接用 pandas 读出来画即可import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(acne_work/run1/results.csv) plt.plot(df[epoch], df[train/box_loss], labeltrain_box) plt.plot(df[epoch], df[val/box_loss], labelval_box) plt.legend() plt.show()看曲线时注意如果 val loss 在某个 epoch 后持续上升而 train loss 还在下降就是典型的过拟合信号回头把epochs砍到那个拐点附近如果 val loss 从头到尾都是平的先怀疑标签或数据划分有问题别急着加训练轮数。5.2 导出 ONNXRK3588 上部署的朴素路径很多人的目标不是停在 PyTorch 里跑推理而是部署到边缘设备。RK3588 这类平台跑 YOLOv8 的标准链路是PyTorch 权重先转 ONNX再转 RKNN 格式加载到 NPU。第一步先用 ultralytics 自带的导出功能yolo export modelacne_work/run1/weights/best.pt formatonnx opset12导出后的best.onnx可以直接用 ONNXRuntime 做纯 CPU 推理验证确认权重没问题再往 RKNN 走import onnxruntime as ort import numpy as np sess ort.InferenceSession(best.onnx) input_name sess.get_inputs()[0].name img np.zeros((1, 3, 640, 640), dtypenp.float32) # 模拟归一化后的输入 out sess.run(None, {input_name: img}) print(out[0].shape)输出 shape 通常是(1, 4 num_classes, 8400)或(1, 8400, 4 num_classes)取决于 ONNX 导出版本。8400 是 YOLOv8 在不同尺度特征图上的预测框总数后面接的后处理要按这个维度写 NMS。到了 RK3588 这一步需要把 ONNX 转成 RKNN 格式转换工具链对算子支持有限前面导出时尽量用opset12而不是更高版本避免某些算子不被 NPU 驱动识别。另外要注意导出时不要开dynamicTrueRKNN 转换工具对动态 shape 支持很差固定成640x640最省事。这一步在 PC 上先拿 ONNX 跑通整张图确认输出坐标和 PyTorch 推理一致再上板子调 NPU排查成本会低很多。6. 增量训练冻结主干微调与数据增强的取舍用 927 张图训出初版权重后如果还想再拉一截 mAP我一般不会直接加 epoch而是做两阶段微调。第一阶段冻结 backbone只训练 head让模型先适应当前数据的类别分布第二阶段解冻全部参数小学习率微调。原因是病灶的纹理特征集中ImageNet 预训练模型在底层学到的边缘、颜色特征已经够用硬训反而容易把主干带偏。ultralytics 的freeze参数可以指定冻结前 N 层yolo detect train \ modelacne_work/run1/weights/best.pt \ dataacne.yaml \ epochs50 \ freeze10 \ imgsz640freeze10表示冻结前 10 层对 YOLOv8n 来说基本覆盖了 stem 和大部分主干阶段。这一阶段训练完后用新生成的best.pt再做一次不冻结的全参数微调学习率建议调低一个数量级常见做法是在命令里加lr00.001甚至lr00.0005避免在已经收敛的基础上把权重冲坏。数据增强这块我的建议是优先用 ultralytics 内置的线上增强而不是自己去离线扩图。Mosaic、HSV 扰动、随机仿射这些增强默认已经开启而且是在线实时作用在每张图上不会产生额外文件离线扩图一旦忘了同步标签文件就会重新踩一遍第 4 章的坑。只有一种情况我才会做离线增强检测目标太小想通过切片把大图裁成小图再放大训练这种操作必须连同标签一起裁剪并重新归一化工程量明显更高。老实说我在项目里吃过一次亏把自己裁剪的新图直接塞进原数据目录文件名前缀不一致labels里却没有对应 txt训练日志只报了几条 WARNING当时没在意结果验证集 mAP 掉了一截才排查到问题。从那以后我每次加图都强制走一遍同名同步脚本再只拿新图跑 10 个 epoch 做 sanity check确认没有 missing 标签才合并进全量训练。这个习惯帮我避掉了后面很多次的「不知道为什么 loss 不动」的玄学问题希望帮到你。本文还有配套的精品资源点击获取