资讯动态

927张痤疮JPG图训练YOLOv8检测模型:小样本数据实战指南

发布时间:2026/10/9 15:52:04 来源:尧图企业网站定制
简介痤疮数据集专为YOLOv8目标检测模型训练而设计包含九百二十七张JPG格式皮肤影像及配套标注面向计算机视觉开发者、医疗AI研究者和皮肤科智能诊断相关项目。资源共一千八百五十五个文件其中九百二十七张原始图像对应九百二十七个txt标签另有yaml配置文件整体压缩包约33.93MB轻量易用。数据已按机器学习标准划分为训练集、验证集与测试集可直接导入YOLOv8训练流程用于痤疮位置识别与定位也可支撑皮肤科辅助诊断、远程医疗影像分析等场景。目前已有三百二十九人学习下载适合需要快速验证检测模型、开展皮肤影像实验或构建智能诊断原型的读者。随包还可获得图像与标签一一对应的目录组织方式省去手工转换格式与整理目录的时间便于将精力集中在模型调优与效果评估上。1. 痤疮数据集 JPG YOLOv8 格式927 张图够训练一个检测模型吗你手里可能正握着一个 927 张 JPG 的痤疮图像集标注已经转成了 YOLOv8 的 txt 格式目录结构也像模像样。于是你准备yolo train一把梭。我见过太多这种开场数据集小、目标密集、类别分布悬殊直接开训的结果是 loss 曲线像过山车验证集 mAP 也不低但把权重丢到一张没见过的真实照片里立刻现原形。问题不在你训练得不够久而在于你把「数据集是 YOLOv8 格式」误当成了「数据集可以直接喂给 YOLOv8 训练」。927 张图做不了从零训练但做迁移学习完全够用前提是你把数据组织、标签校验、增强策略和验证方式按小样本的规矩来。这篇文章就沿着这个数据集规格把从目录规范到转换脚本、从训练参数到排错清单的整条路径讲清楚适合准备做皮肤病辅助检测、目标检测小样本实验或者只是想把这份数据用起来的开发者。先别急着跑训练把前两章看完你能省下至少一晚上的调参时间。2. 从 JPG 裸图到 YOLOv8 训练集目录规范与标签格式2.1 YOLOv8 对数据集目录的硬性要求images 与 labels 的对应关系YOLOv8 的训练入口是data.yaml它不关心你的原始图片放在哪个犄角旮旯只认配置里写的路径。但路径指向的目录结构有一个硬性约定图片目录和标签目录必须同名且标签目录名固定为labels图片目录名可以是images也可以是train、val这类按用途划分的目录名。按官方默认约定常见的组织方式是这样的acne_dataset/ ├── data.yaml ├── train/ │ ├── images/ │ │ ├── acne_001.jpg │ │ ├── acne_002.jpg │ │ └── ... │ └── labels/ │ ├── acne_001.txt │ ├── acne_002.txt │ └── ... └── val/ ├── images/ └── labels/如果你拿到的 927 张图是平铺在一个目录里的先别急着写划分脚本——先搞清楚两件事标注文件是跟图片同名的 txt还是集中在某个标注文件里原始标注格式是 VOC XML、COCO JSON还是 LabelMe 导出的 JSON。不同来源决定转换脚本的解析逻辑。目录整理这一步常见做法是把 927 张图按 8:1:1 或 8:2 划分成 train 和 val测试集可以先不单独划从 val 里抽。# 在数据集根目录下执行假设原始图片在 raw_images/标签在 raw_labels/ mkdir -p train/images train/labels val/images val/labels # 用 Python 随机划分保证同源图片不被切散 python - EOF import os, random, shutil random.seed(42) images sorted(os.listdir(raw_images)) random.shuffle(images) split int(len(images) * 0.8) for idx, name in enumerate(images): src_img os.path.join(raw_images, name) src_lab os.path.join(raw_labels, name.replace(.jpg, .txt)) if idx split: dst_img os.path.join(train/images, name) dst_lab os.path.join(train/labels, name.replace(.jpg, .txt)) else: dst_img os.path.join(val/images, name) dst_lab os.path.join(val/labels, name.replace(.jpg, .txt)) shutil.copy(src_img, dst_img) shutil.copy(src_lab, dst_lab) print(划分完成:, split, train /, len(images) - split, val) EOF这个脚本的逻辑很简单random.seed(42)保证每次运行划分结果一致random.shuffle打乱顺序避免同一部位连续照片全落在训练集或验证集。注意这里用的是copy而不是move原因是原始文件建议保留一份万一划分出问题还能重来。划分比例按 8:2 是 YOLOv8 文档里的常见值但如果你手里的图像来自不同设备、不同光线建议按来源分组划分否则验证集会混入训练集的同源数据mAP 虚高。2.2 标签文件不是 XMLtxt 里每行五个数字的含义YOLOv8 的标签文件是纯文本每行代表一个目标框固定五个字段class_id center_x center_y width height。注意这里 center_x、center_y 是归一化到 [0,1] 的相对坐标width 和 height 是归一化后的框宽高不是像素值。这个格式化跟 COCO 的左上角加宽高表示法完全不同也是新手最容易翻车的地方。# 标签文件示例 acne_001.txt 0 0.523437 0.612500 0.078125 0.093750 1 0.734375 0.448958 0.045000 0.052083第一行的含义类别 0假设 0 是粉刺中心点位于图像宽度 52.34%、高度 61.25% 的位置框宽占图像宽度 7.81%框高占图像高度 9.38%。这些数字看着像乱码但 YOLOv8 训练时就是这么读的。如果你转换时把中心坐标写成了像素值或者忘了做归一化模型能训练但框会飞表现为 loss 降不下去或者预测框全部挤在图像角落。为了确认手里的标签不是脏数据训练前先跑一段校验脚本这是血泪教训换来的习惯# validate_labels.py import os label_dir train/labels img_dir train/images errors [] for txt_name in sorted(os.listdir(label_dir)): base txt_name.replace(.txt, .jpg) if not os.path.exists(os.path.join(img_dir, base)): errors.append(f缺少图片: {base}) continue with open(os.path.join(label_dir, txt_name), r) as f: lines f.read().strip().split(\n) if not lines or lines []: errors.append(f空标签: {txt_name}) continue for line_no, line in enumerate(lines, 1): parts line.split() if len(parts) ! 5: errors.append(f{txt_name} 第{line_no}行字段数异常) continue try: cls, cx, cy, w, h map(float, parts) except ValueError: errors.append(f{txt_name} 第{line_no}行含非数字) continue if not (0 cx 1 and 0 cy 1 and 0 w 1 and 0 h 1): errors.append(f{txt_name} 第{line_no}行坐标越界) if errors: print(发现 %d 个问题: % len(errors)) for e in errors[:20]: print( -, e) else: print(标签校验通过共 %d 个文件 % len(os.listdir(label_dir)))这段脚本做三件事检查标签与图片是否同名对应、检查每行是否恰好五个字段、检查坐标是否在归一化边界内。跑完没有报错基本可以排除格式层面的低级错误接下来再进入转换和训练才有意义。3. 把原始标注转成 YOLOv8 格式一个能直接用的转换脚本3.1 转换脚本的完整实现与逐段说明如果你的原始标注是 JSON 或 XML直接手动改格式是不现实的927 张图对应几千个框手工改必错。最可靠的做法是写一个转换脚本把原始标注统一转成 YOLOv8 的 txt。下面这个脚本适用于常见的目标检测标注格式假设每张图对应一个 JSON 文件结构类似 LabelMe 导出的格式# convert_to_yolo.py import json import os from glob import glob def convert_json_to_yolo(json_path, img_width, img_height, class_map): 将单张图像的标注 JSON 转为 YOLOv8 格式的字符串。 class_map: dict原始类别名到 id 的映射如 {粉刺: 0, 炎性丘疹: 1} with open(json_path, r, encodingutf-8) as f: data json.load(f) lines [] # 兼容两种常见结构shapes 列表或 annotations 列表 shapes data.get(shapes, data.get(annotations, [])) for shape in shapes: label shape.get(label, shape.get(category_name)) if label not in class_map: print(f跳过未知类别: {label} in {json_path}) continue # 取多边形或矩形的外包框 points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) # 边界裁剪防止标注框超出图像范围 x_min max(0, x_min) y_min max(0, y_min) x_max min(img_width, x_max) y_max min(img_height, y_max) if x_max x_min or y_max y_min: print(f忽略空框: {json_path}) continue box_width x_max - x_min box_height y_max - y_min cx x_min box_width / 2 cy y_min box_height / 2 # 归一化到 [0, 1] cx / img_width cy / img_height box_width / img_width box_height / img_height cls_id class_map[label] lines.append(f{cls_id} {cx:.6f} {cy:.6f} {box_width:.6f} {box_height:.6f}) return \n.join(lines)这段代码的核心在于把原始标注的多边形顶点转成外接矩形然后做归一化。cx / img_width这一步是把像素中心坐标变成相对坐标box_width / img_width同理。cx:.6f保留六位小数是经验值精度够用且文件不会太冗余。class_map需要你手动维护一个类别名到 id 的映射比如痤疮数据集常见的分类有粉刺、炎性丘疹、脓疱、结节分别映射到 0、1、2、3。注意类别 id 必须从 0 开始连续编号YOLOv8 不认 1~4 这种编号方式。转换后的 txt 文件名必须与 JPG 同名且扩展名替换为.txt。完整的转换流程还要把图片尺寸读出来不能从 JSON 里拿因为 JSON 里的尺寸可能和实际 JPG 分辨率不一致。读取图片尺寸最稳的方式是直接用 Python 的imagesize库或者用 PIL 打开图片获取width, height不推荐从文件名或 JSON 元数据猜。3.2 归一化坐标与边界框裁剪两个最容易算错的地方第一个坑JSON 里的坐标可能是浮点但单位是像素也可能标注工具直接存成了归一化坐标。判断方法很简单——看坐标数值是否大于 1。如果 json 里所有坐标都在 0~1 之间说明标注工具已经做了归一化那img_width和img_height就不能参与第二次归一化直接用原值。上面脚本默认像素坐标如果你手里的数据是归一化的把cx / img_width这行注释掉即可。第二个坑边界裁剪不是可选项。痤疮图像往往来自医生在移动端拍摄标注时手一抖就可能在图像边缘留下越界框。如果 x_max 大于 img_width归一化后 w 会超过 1YOLOv8 训练时会把这种框当异常处理轻则警告重则 loss 爆炸。上面脚本里x_max min(img_width, x_max)就是为了把框拉回有效范围。如果裁剪后的宽高为 0说明该标注本身就是脏数据直接跳过并打印提示别硬转。还有一个常被忽略的点转换完成后要统计一下类别分布。927 张图里可能 90% 的框都是粉刺类结节只有十几个框。这种情况先记录在案后面第 4 章的增强和第 5 章的类别不平衡处理都会用到这个统计。统计脚本非常简单# count_classes.py from collections import Counter import os counter Counter() label_dir train/labels for txt_name in os.listdir(label_dir): with open(os.path.join(label_dir, txt_name), r) as f: for line in f: cls_id line.split()[0] counter[cls_id] 1 print(各类别框数统计:, dict(counter))这个统计数字直接决定你要不要做类别重采样。如果某一类别的框数少于总框数的 5%模型大概率学不到这个类别这不是训练技巧能解决的问题需要从数据层面干预。4. 小样本训练的保命配置数据增强与训练参数怎么调4.1 927 张图该用多大的输入尺寸和 batch先给结论输入尺寸 640 起步如果你的显存允许8GB 以上用 960 或 1024 效果更好。原因很简单——痤疮目标是典型的小目标。一张 1920×1080 的照片里一个粉刺的标注框像素宽高往往只有 30~80 像素输入缩到 640 后框变得更小模型的下采样倍数让它几乎丢失细节。YOLOv8 的检测头在 P3、P4、P5 三个尺度上输出P3 层对应 80×80 的特征图如果目标在 640 输入下小于 8×8 像素P3 层也救不回来。所以分辨率不够一切白搭。下面是针对 927 张小样本数据集的基础训练参数建议参数建议值说明imgsz640显存不足或 960小目标优先大分辨率batch16小 batch避免梯度过于平滑epochs300配合早停实际跑到 200 左右lr00.005迁移学习不建议用默认 0.01optimizerAdamW小样本收敛更稳SGD 容易震荡patience50验证集 50 轮不提升就停seed42固定随机种子保证可复现pretrainedTrue必须用 COCO 预训练权重迁移batch 设 16 而不是 32是因为小样本数据集本身多样本梯度噪声就大batch 太大容易过早收敛到局部最优。epochs 设 300 是给模型留足学习空间但加了patience50如果验证集 mAP 连续 50 轮不涨会自动停所以实际训练时间取决于数据难度。lr0 用 0.005 而不是默认的 0.01是因为迁移学习时 backbone 的权重已经比较成熟学习率太大会破坏预训练特征。如果你用的是 YOLOv8 的命令行方式训练命令长这样yolo train \ modelyolov8s.pt \ dataacne_dataset/data.yaml \ imgsz960 \ batch16 \ epochs300 \ lr00.005 \ optimizerAdamW \ patience50 \ seed42 \ pretrainedTruemodelyolov8s.pt表示从 s 规模的预训练权重开始微调。为什么选 s 而不是 m 或 l927 张图的数据量撑不起大模型s 模型参数量约 1100 万在医学小目标上已经够用m 或 l 容易过拟合而且训练时间翻倍。如果 960 分辨率在你的显卡上 OOM降到 640 就行但后面评估时如果 mAP 不理想优先怀疑分辨率而不是模型容量。4.2 离线增强与在线增强最有效的六个增强组合数据增强是 927 张小数据集能不能用的分水岭。YOLOv8 内置了在线增强配置写在data.yaml旁边或训练代码里。先说结论对小目标检测颜色类增强有用几何类增强要克制mosaic 必须调低概率。# data.yaml 增强配置段YOLOv8 通过 ultralytics 配置启用 train: train/images val: val/images nc: 4 names: [粉刺, 炎性丘疹, 脓疱, 结节] # 增强参数 hsv_h: 0.015 # 色相扰动幅度医学图像不宜太大 hsv_s: 0.5 # 饱和度扰动 hsv_v: 0.4 # 亮度扰动 fliplr: 0.5 # 水平翻转 scale: 0.3 # 缩放扰动 mosaic: 0.3 # 拼接增强概率默认 1.0 必须调低 mixup: 0.1 # 混合增强小样本可用但不宜过高这几个参数的逻辑hsv_h调到 0.015 是因为痤疮的颜色特征红肿、脓白是诊断关键信息色相扰动太大会让模型学到错误的颜色不变性fliplr0.5是安全的人脸左右对称这个增强不会引入语义错误scale0.3模拟不同拍摄距离但不要超过 0.5否则小目标缩得更小直接消失mosaic0.3是最关键的一处——默认值是 1.0即每张训练图都做拼接四张图拼一起后每个目标的尺寸又缩小一半对痤疮这种本就小的目标来说是灾难必须调低到 0.3 以下。mixup0.1用很低概率做两张图融合能增加背景多样性但概率高了模型学到的特征会模糊。离线增强是在转换阶段做的补充常见做法是对占比少的类别做复制粘贴增强copy-paste augmentation从标注里把某一类的小目标抠出来随机贴到其他图像的背景区域。这个方法对小样本目标检测特别有效但注意粘贴位置不要盖住已有的标注框# copy_paste_augment.py 示例把稀有类别的目标复制到训练图像中 import cv2 import numpy as np import random def copy_paste(src_img, src_box, dst_img, dst_boxes, max_attempts20): 把 src_box 区域从 src_img 复制到 dst_img 的空白区域。 x1, y1, x2, y2 map(int, src_box) patch src_img[y1:y2, x1:x2].copy() ph, pw patch.shape[:2] for _ in range(max_attempts): # 随机生成粘贴位置 nx random.randint(0, dst_img.shape[1] - pw) ny random.randint(0, dst_img.shape[0] - ph) # 检查是否与已有框重叠 overlap False for bx1, by1, bx2, by2 in dst_boxes: ix1, iy1 max(nx, bx1), max(ny, by1) ix2, iy2 min(nx pw, bx2), min(ny ph, by2) if ix2 ix1 and iy2 iy1: overlap True break if not overlap: dst_img[ny:nyph, nx:nxpw] patch dst_boxes.append([nx, ny, nxpw, nyph]) return True return Falsemax_attempts20是尝试次数上限防止死循环重叠检测用简单的交并比判零实现ix2 ix1 and iy2 iy1表示两框有交集。离线增强的收益在稀有类别上立竿见影但别做过头——视觉上重复的样本如果太多模型会把背景纹理也当成特征一般建议稀有类别扩充后不超过总框数的 30%。5. 训练痤疮检测模型的常见问题与避坑记录5.1 现象验证集 mAP 很高但放到真实照片上几乎全漏检这是小样本目标检测最典型的翻车现场。验证集 mAP0.5 到了 0.85你满怀信心地把模型丢到新拍摄的一张照片上结果一个框都没画出来。原因通常是三种一是训练集和验证集划分时同源图像没有隔开同一患者的连续帧分散在两边模型相当于做过「开卷考试」二是背景负样本太少模型只学会了在有痤疮的图片里找目标遇到新场景的复杂背景就懵三是增强参数把颜色分布改得太狠模型学到的特征在真实照片上不具备泛化性。解决思路重新按来源分组划分数据集确保同一拍摄序列的全部图像进入同一侧增加背景无目标的负样本图可以从原始数据里挑出没有标注框的 JPG 作为背景样本把hsv_h和hsv_s调回保守值。验证 mAP 只能作为训练监控指标真正的评估要留一部分完全不参与训练的测试图像。5.2 现象训练 loss 不降反升曲线像锯齿振荡训练两三轮后 loss 不但不降反而一路往上爬这种曲线的背后通常不是模型问题是学习率或增强强度的问题。小数据集上AdamW 对学习率的敏感度比 SGD 更高lr00.01 的默认值对迁移学习来说偏激进。另一种常见原因是mosaic1.0下拼接出来的图像边界处出现半截目标模型被大量不完整的框搞糊涂了。解决把lr0降到 0.003~0.005或者改成 SGD 配合 Warmup将mosaic调低到 0.3如果还震荡直接设为 0同时检查标签里有没有大量超小框归一化宽度小于 0.01这种框对 loss 的贡献是纯噪声建议在转换阶段过滤掉。5.3 现象小痤疮目标几乎全部漏检大框检得还行这属于典型的小目标问题漏检原因是多尺度的。YOLOv8 的 P3 特征图负责小目标检测但它的感受野在 640 输入下对应原图约 8×8 像素区域一个直径 5 像素的痤疮在这个尺度下信号太弱而 P5 层感受野大只能看到大目标。如果 val 集里小目标框占比超过一半mAP 会被大目标拉高掩盖小目标的失败。解决首选提高输入分辨率到 960 或 1024这能让 P3 层看到的目标从 8 像素变成 12 像素效果立竿见影如果显存不允许可考虑用切片推理SAHI把原图切成 4 块分别推理再合并结果再不行就在增强里加一点scale0.4的随机放大让模型见过不同尺度的目标。5.4 现象模型把所有框都预测成类别 0稀有类别完全学不到这种情况几乎可以断定是类别严重不平衡。假设 927 张图里类别 0粉刺有 3000 个框类别 3结节只有 40 个框模型的 loss 会被粉刺类别主导预测时永远输出先验概率最高的类别。这不是模型笨是损失函数天然偏向多数类。解决统计各类别框数后对占比低于 10% 的类做三种干预——复制粘贴增强把少数类的目标贴到更多图上给 loss 按类别加权或者只保留少数类的训练轮数先在全类别上训 50 轮再单独用小学习率微调少数类。第三种方法实操中效果最直接但要注意微调时 val 集不能变否则对比不公平。5.5 现象同一份数据和配置换台机器结果差异巨大这是个让人崩溃的玄学问题。YOLOv8 默认使用 GPU 上的非确定性算法某些 CUDA 操作在不同硬件上的原子性不同导致同样的随机种子产出不同结果。如果你要复现或做实验对比这是必须先解决的问题。解决在训练脚本里强制确定性模式设置环境变量CUBLAS_WORKSPACE_CONFIG:4096:8并用torch.use_deterministic_algorithms(True)同时关闭可能导致非确定性的增强如随机裁剪的填充方式统一为常数填充。如果换了机器后结果仍不一致检查 CUDA 版本和 PyTorch 版本是否一致版本差异也会带来微小数值变化。这是不追求像素级复现的设备跑通即可不用过于焦虑。6. 验证模型与进阶技巧把 927 张图的价值榨干6.1 用 PR 曲线和混淆矩阵定位失败模式训练结束后别只看 val 的 mAP。YOLOv8 训练过程中会保存PR_curve.png和confusion_matrix.png到 runs 目录这两张图的价值远大于一个 mAP 数字。PR 曲线的右下角会在接近召回率 0.9 处出现一个骤降的拐点那个位置对应的置信度阈值就是你的模型在小目标上的实际能力边界。混淆矩阵的行是真实类别列是预测类别看对角线之外最密集的格子——如果类别 0 被大量预测成类别 1说明这两种痤疮表型在视觉特征上高度相似模型学到的是颜色相近的表面特征而不是结构差异。这时候不要盲目调参回到数据里检查这两类的标注是否准确标注本身有歧义的样本比模型误差更值得处理。一个实用的验证习惯从 val 集中挑出 10 张模型预测效果最差的图手动标出漏检和误检的位置按失败模式分类。如果 80% 的失败是漏检输入分辨率优先如果 60% 是误检背景负样本数量和增强多样性优先如果两者各半优先解决类别不平衡。6.2 类别重平衡与置信度阈值调优的实操最后一步是调预测阈值。YOLOv8 的默认置信度阈值是 0.25对医学辅助检测这种宁可误报不可漏报的场景这个阈值偏高。用验证集跑一次预测统计不同阈值下的精确率和召回率变化曲线找一个满足「召回率不低于 0.8 且精确率不低于 0.4」的交叉点。公式很简单precision TP / (TP FP)你要的是在可接受的误报数量下尽量抓住每一个真实目标。常见做法是写一段小脚本遍历阈值把结果导成 CSV 对比yolo predict \ modelruns/detect/train/weights/best.pt \ sourceval/images \ conf0.15 \ save_txtTrue \ save_confTrueconf0.15是更宽松的阈值它会让预测结果里多出很多低置信度框然后你根据实际场景决定哪些能接受。这个阈值调优没有通用最优值完全取决于你的业务容忍度——如果做辅助筛查宁可多画框让医生看如果做自动分级阈值就得提高。最后提醒一件事927 张图的模型只能算可行性验证真正要落地至少还要补充几百张不同设备、不同光线下的负样本和难例。我自己的习惯是每次训练完把 best.pt 单独复制一份按日期命名存档数据和模型版本对不上是最常见的后悔药场景——你永远不知道半年后重跑实验时丢的是哪一份权重。希望这篇文章能帮你把 927 张图真正用起来少走几步弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑