简介:YOLOv8标记乳房X光检查数据集是一份面向医学影像目标检测任务的专业标注资源,适合需要训练YOLOv8检测模型的开发者与研究人员。数据集整合自RSNA、MINI-DDSM-PNG-16、MIAS、INBREAST四个公开来源,共900张图像,已调整为416×416像素并完成YOLOv8格式标记,可直接用于模型训练与验证。压缩包内共1908个文件,其中包含1000张png图像、901个txt标签文件、4个pt预训练模型、2个cache缓存文件及1个yaml配置文件,整体大小约496.76MB;yaml文件方便配置训练参数,预训练权重可加速收敛。配套目录区分训练与测试数据,结构清晰,便于快速上手迁移学习。目前已有764人浏览学习,适合医学影像AI入门及乳腺X光检测落地实践,可省去数据收集、清洗与标注的大量时间。
1. 900 张乳腺 X 光标记数据能直接喂给 YOLOv8 吗:这份资源先替你踩平了格式坑
做乳房 X 光(乳腺钼靶)病灶检测的人,第一道坎往往不是 YOLOv8 参数调优,而是没有一份能直接喂给 YOLOv8 的标注数据。RSNA、MIAS、INBREAST、MINI-DDSM-PNG-16 都是公开数据集,但一个给 DICOM 加 CSV 坐标框,一个给 PGM 加圆心半径,标签体系互不通用,光转换和清洗就能耗掉一两个晚上。这份资源把四个来源筛成 900 张 416x416 的 PNG,配好 YOLO 格式标注和训练时生成的 labels.cache,还带 yolov8n / yolov8m / yolov8l / yolov8x 四档预训练权重,下载后可以直接进入训练流程。适合两类人:想把 YOLOv8 用于乳腺 X 光检测、但没有现成 YOLO 格式数据的从业者和研究生,以及想先跑通基线、再扩充数据的预研团队。
2. 四个数据源合并的细节:看起来都是乳腺片,标签体系并不相通
2.1 来源差异:DICOM、PGM、16 位 PNG 的转换与筛选痕迹
这份资源号称「YOLOv8 标记乳房 X 光检查数据集」,但不是从某一个源搬过来的,而是由四个公开数据集合并而成。RSNA 出自 Kaggle 2018 乳腺癌检测挑战赛,原始图像是 DICOM,标注框在配套 CSV 里,字段是 xmin/ymin/xmax/ymax 像素坐标;MIAS 是英国曼彻斯特的经典小数据集,原始约 102 张 PGM 灰度图,ground truth 是文本行里的病灶圆心与半径;MINI-DDSM-PNG-16 是 DDSM 的 PNG 重制版,16 位位深保留更细灰度层次,但标注是链码轮廓,转成 YOLO 的 xywh 框需要自己算外接矩形;INBREAST 原数据有 115 例、约 410 张钼靶图,带 BI-RADS 分级,这里只挑了 62 张。
提原始格式不是考古,是因为 YOLO 训练时只认每个 PNG 同名的 txt,txt 里第一列是类别号、后四列是归一化 xywh。所以拿到任何「下载后直接训练」的数据集,第一反应都该是怀疑三件事:作者合并时有没有把坐标系统一到归一化坐标;有没有在筛选子集时把类别标签改坏;有没有同一病例的多视角图像混进不同数据划分。下面这张表把四个源的差异和最容易翻车的点列出来,照着查就行。
| 数据源 | 资源中数量 | 原始格式 | 原始标注形态 | 合并时最容易翻车的点 |
|---|---|---|---|---|
| RSNA | 538 张 | DICOM | CSV 像素坐标框 | 坐标未归一化、同一病例多张图 |
| MINI-DDSM-PNG-16 | 200 张 | 16bit PNG | 链码轮廓 | 轮廓转矩形框丢失方向信息 |
| MIAS | 100 张 | PGM | 圆心 + 半径 | 半径换算矩形后需归一化 |
| INBREAST | 62 张 | DICOM / 导出 PNG | BI-RADS + 人工框 | 类别号与其余源不一致 |
合并逻辑里能看出筛选痕迹:MIAS 全数据集 102 张,这里给 100 张;INBREAST 原数据集 115 例约 410 张图,这里只选 62 张。这基本可以推断作者把「没有病灶框」「标注无法对齐」的图像移除了,剩下 900 张都是带正样本框的图。这个动作本身很关键:YOLOv8 的监督信号完全来自阳性框,若训练集混进大量无框负样本而不做特殊处理,正负比失衡会让 loss 前期抖动更明显。这份资源帮你把负样本事先筛掉了,所以跑起来 loss 会比「原图直接转格式」干净很多。
2.2 文件命名与 labels.cache:训练之前先读懂目录
从资源列表看,目录主力是两类文件:一类是 RSNA__39816__996130280.png 这种图像文件,另一类是训练时生成的 labels.cache。YOLO 格式下每张 PNG 应该有一个同名 txt,txt 每行对应一个目标框,格式是class x_center y_center width height,全部是 0~1 归一化值。目录里没把每个 txt 单独列出来不表示不存在,ultralytics 训练时按同名文件在 images 和 labels 两个目录间查找。下载后第一步我会先写一个小脚本,把文件名的规律拆出来,顺便确认病例数量:
import re from pathlib import Path root = Path("datasets/breast-yolo") imgs = sorted(root.rglob("*.png")) pat = re.compile(r"^(RSNA|MINI-DDSM|MIAS|INBREAST)__(.+?)__(\d+)\.png$") stats = {} for p in imgs: m = pat.match(p.name) if not m: print("命名不满足约定,训练时要留意:", p.name) continue # 分组:数据源、疑似病例编号、流水号 stats.setdefault(m.group(1), set()).add(m.group(2)) for src, pids in stats.items(): print(f"{src}: 共 {len(pids)} 个不同病例编号")这段代码的逻辑是:文件名前两段分别是数据源和疑似病例编号,无论这个编号在原始数据集里是不是严格的患者 ID,把它当分组键已经比随机划分安全。打印出来的病例数如果和图像数相等,说明每个病例只有一张图;如果病例数明显小于图像数,说明同一病例存在双视角(CC / MLO)图像,划分 train/val 时就要小心串组。图像统一放 images/train 和 images/val,对应 labels 目录保持一致,ultralytics 会根据 data.yaml 自动找 sibling 目录,不要自己乱改层级。
labels.cache 是 ultralytics 首次加载数据集时用 torch.save 序列化出来的二进制缓存,内容大致是图像路径到标签 tensor 的映射、每张图的长宽、整个数据集的 hash。第二次训练直接读取缓存,省掉反复解析 txt 的 IO。坏处是:一旦图像或标注被改动,旧 cache 里的 hash 对不上,会出现两种诡异表现——要么报 stale cache 相关错误,要么训练时静默使用旧标签。所以我拿到任何别人的数据集,第一个动作都是先清理 cache 再开训:
find /path/to/datasets/breast-yolo -name "labels.cache" -delete如果后续改了类别名、换了 data.yaml,也要回到这一步重新删。这不是可选项,是必做项,第 4 章我会再展开讲它怎么坑人。
2.3 416x416 的分辨率:先跑通基线,再谈病灶分辨率
乳腺钼靶原图通常 2000x3000 像素级别,资源作者统一 resize 到 416x416。好处是显存占用小,yolov8n 在 GTX 1660 Ti 级别显卡也能 batch=16 跑起来,迭代速度快;坏处是超过 5 倍的降采样。YOLOv8 的最小检测特征图尺寸是 imgsz/32,416 输入对应 13x13,意味着小于 32 像素的结构在下采样链上基本丢失。对 mass(肿块)、asymmetry(局灶不对称)这类直径 50~200 像素的大目标,416 够用;对微钙化簇这种只有 10~30 像素的目标,416 下基本糊掉。按病灶类型选分辨率的建议如下:
| 病灶类型 | 原图典型尺寸 | 416x416 下表现 | 建议输入 |
|---|---|---|---|
| mass 肿块 | 50~200 px | 可检出但边缘不锐 | 416 或 640 |
| asymmetry / 结构扭曲 | 30~150 px | 勉强可检 | 640 起 |
| microcalcification 簇 | 10~30 px | 基本丢失 | 896 且需数据增强 |
我的建议是把这份 416 数据集当低成本 baseline 用。先用它把数据管线跑通、把类别和划分策略调好,然后再把原图按同样标注重导出成 640 甚至 896 训一轮,对比 mAP 提升幅度。如果你冲着边缘部署去,416 反而是优势:训练时用 416,部署到 rk3588 这类板子做 int8 量化后延迟很低,检测小病灶时再把输入切回 640,相当于在推理资源和精度之间做一个折中方案。
3. 训练 YOLOv8:从标签校验到看 loss 曲线的完整流程
3.1 环境准备与数据自检
环境配置是老生常谈,但版本要锁住。我用 python 3.10 建独立环境,ultralytics 锁 8.2.x 而不是最新版,因为 8.3 之后部分回调接口有调整,照着老教程抄作业容易踩版本差异:
conda create -n yolo8 python=3.10 -y conda activate yolo8 pip install ultralytics==8.2.* opencv-python pandas装完环境先别急着训,数据自检比环境更重要。YOLO 训练最常见的问题不是模型跑不起来,而是标签和图像对不上、坐标越界、空 txt 一堆,训练日志还不直接报错。我习惯先把 images 和 labels 全部扫一遍:
import numpy as np from pathlib import Path root = Path("datasets/breast-yolo") for split in ["train", "val"]: img_dir = root / "images" / split lbl_dir = root / "labels" / split problems = 0 for img_path in sorted(img_dir.glob("*.png")): txt_path = lbl_dir / (img_path.stem + ".txt") if not txt_path.exists(): print(f"缺失标注: {img_path} -> {txt_path}") problems += 1 continue lines = np.loadtxt(txt_path, ndmin=2) if lines.size == 0: print(f"空标注: {txt_path}") problems += 1 continue if lines[:, 1:].max() > 1.0 or lines[:, 1:].min() < 0.0: print(f"坐标越界: {txt_path} 最大值 {lines[:, 1:].max():.3f}") problems += 1 print(f"{split}: 检查完成,共 {problems} 个问题")三类最常见问题:一是缺同名 txt,训练时图像会被静默跳过;二是空 txt,YOLOv8 会跳过但大量存在就意味着「看着在训、实际没学到」;三是归一化坐标越界,多半是 RSNA 像素坐标转过来时忘了除以原图宽高。这个脚本很小但建议别跳过,因为 labels.cache 的存在让损坏的标注有时不会立刻暴露,等训完才发现就晚了。
3.2 写 data.yaml 并核对类别号
自检通过后写数据配置。path 建议直接写绝对路径,相对路径在 ultralytics 8.2 里会因为工作目录不同而解析失败,这种报错最容易让人误以为是数据问题:
path: /data/breast-yolo train: images/train val: images/val names: 0: mass 1: calcificationnames 的类别号必须和 txt 第一列严格对应。有些作者只标了一类,这时 names 要缩成0: lesion;如果你想复现作者的完整实验,先看 labels 目录里 txt 第一列到底有几个值,一条命令就能统计:
find labels/train -name "*.txt" -exec awk '{print $1}' {} + | sort | uniq -c输出如果同时有 0 和 1,说明原始标注包含两类;如果只有 0,data.yaml 就要改成单类。names 写错不会让训练崩掉,但后面 mAP 曲线、混淆矩阵、热力图的类别名全是错的,排查起来非常绕。
3.3 训练命令、参数速查与 loss 曲线判断
数据集确认没问题,就可以正式开训。我一般用 yolov8m.pt 起步,而不是最小的 n,因为乳腺病灶相对小且背景复杂度高,n 级模型容易欠拟合,m 级是性价比最稳的一档:
yolo detect train \ data=breast.yaml \ model=yolov8m.pt \ epochs=60 \ imgsz=416 \ batch=8 \ device=0 \ patience=15 \ project=runs/breast \ name=mass_416几个参数随时会改,先记清楚:
| 参数 | 值 | 说明 |
|---|---|---|
| model | yolov8m.pt | 从预训练权重起步,比随机初始化收敛快;首次调试可以先用 yolov8n |
| imgsz | 416 | 沿用资源作者口径;显存够就改 640 |
| batch | 8 | 按显存调整,416 下 8 约占用 6~8GB |
| patience | 15 | 连续 15 个 epoch 无提升就停,防过拟合烧时间 |
| device | 0 | 无 GPU 改 device=cpu,但 60 epoch 会非常慢 |
训练过程中看两样东西:终端打印的 loss 和 val 指标。更直观的是直接画 results.csv 里的损失曲线:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/breast/mass_416/results.csv") fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(df["epoch"], df["train/box_loss"], label="train box") axes[0].plot(df["epoch"], df["val/box_loss"], label="val box") axes[0].set_title("box loss") axes[0].legend() axes[1].plot(df["epoch"], df["metrics/precision(B)"], label="precision") axes[1].plot(df["epoch"], df["metrics/recall(B)"], label="recall") axes[1].legend() plt.show()判断标准不复杂:val/box_loss 前 10 个 epoch 快速下降、之后缓慢下降属于正常;train loss 一直降而 val loss 中途反弹,就是过拟合,把 patience 调小,取 best.pt;如果前 5 个 epoch 的 val mAP 一直是 0,先别急着调参,回头查类别号有没有对、目标是不是太小。补充一句:YOLOv8 默认在最后 10 个 epoch 关闭 mosaic 增强,loss 曲线最后一两轮会有个小跳变,那是正常现象,不是训崩了。
4. 避坑:乳腺 X 光训练集最常见的五个翻车点
4.1 类别不平衡:肿块框把钙化框吞了
现象:训练时 loss 曲线很漂亮,但测试时钙化类别的 recall 极低,混淆矩阵里某一类预测几乎为空。
原因:RSNA 和 DDSM 这两个源都以肿块为主,微钙化样本占比通常不到 10%。YOLOv8 按图像采样,大头类别主导 loss,小类别即便有框也很难被优化起来。900 张图里如果钙化只有几十张,模型基本学不到稳定的钙化特征。
解决:先统计类别分布,awk '{print $1}' labels/train/*.txt | sort | uniq -c看一眼比例。如果超过 5:1,常见做法是把少数类的图像在同目录复制几份,相当于过采样。900 张的小数据集里复制图像会带来一定过拟合,但这是性价比最高的手段。等数据扩充到几千张再考虑更精细的采样策略。
4.2 RSNA 坐标未归一化:框直接画出图外
现象:训练时出现坐标大于 1 的报错,或者 val 预测框全部堆在图像边缘。
原因:RSNA 原 CSV 给的是像素坐标,如果合并脚本直接把这些值写进 txt,没有除以原图宽高,坐标范围完全错掉。RSNA 每张 DICOM 原始尺寸还不完全一样,不能拿一个固定分辨率去归一化。
解决:写脚本把越界坐标先夹回 0~1 兜底:
import numpy as np from pathlib import Path for txt in Path("labels/train").glob("*.txt"): lines = np.loadtxt(txt, ndmin=2) if lines.size == 0: continue lines[:, 1:] = np.clip(lines[:, 1:], 0.0, 1.0) np.savetxt(txt, lines, fmt="%.6f")这个 clip 只能防训练崩掉,不能修正原本就歪的框。正确做法是在源头转换时用每张 DICOM 的 columns 和 rows 做归一化。所以第 3 章的自检脚本要放在 clip 之前跑,先知道哪些文件有问题,再决定是修脚本还是手动改标注。
4.3 labels.cache 过期:训练在偷偷用旧标签
现象:修改标注后重新训练,mAP 几乎不变;或者明明删了某些图像文件,训练依然不报错正常跑完。
原因:labels.cache 把标签 tensor 和数据集 hash 一起缓存了。ultralytics 会校验 hash 决定是否重建缓存,但如果改动发生在「同一路径下的文件内容更新」这种场景,或者工作目录里有多个 cache 副本,就可能复用旧结果。更隐蔽的情况是:你改了 txt,但 cache 里存的是改之前的 tensor,训练时根本不会重新解析 txt。
解决:开训前强制清一次 cache,命令第 2 章给过,把路径换成你的数据集根目录即可。我现在的习惯是训练脚本开头固定加三步:删 cache、跑自检、再启动训练。同一个目录反复实验时,这一步能省掉大量「为什么改了没用」的排查时间。
4.4 按图像划分 train/val:同一病人的两张图串组
现象:val mAP 高达 0.85,看起来效果极好,换一批新病人测试时漏检严重,模型泛化能力明显不如指标。
原因:同一个 patient 的 CC 和 MLO 两个视角图像高度相似。如果按文件随机划分,同一个病人的两张图可能一张进 train 一张进 val,模型等于提前见过答案。乳腺 X 光这种小数据集上,这种数据泄漏特别致命,mAP 虚高但实际不可用。
解决:按病例分组划分,用第 2 章从文件名解析出的疑似病例编号作为 group:
from sklearn.model_selection import GroupShuffleSplit imgs = [...] # 图像路径列表 patient_ids = [...] # 与 imgs 一一对应的病例编号 gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(imgs, groups=patient_ids))GroupShuffleSplit 保证同一个病人整体进 train 或整体进 val。这是四个数据源里最容易忽略的坑。RSNA 一个病例通常有双视角图像,文件名里 pid 相同;按 image 划分会让同一人的两张图分到两边,训练指标全面失真。改成分组划分后 mAP 数字通常会掉 0.05~0.1,但那个「变差」的数值才是真实水平。
4.5 模型尺寸不是越大越好:900 张图喂不动 yolov8x
现象:换上 yolov8x 后显存直接撞墙,训练速度骤降,val loss 降一段就开始反弹,最终 mAP 反而不如 m 级模型。
原因:900 张图对 68M 参数量的 x 级模型来说太小了。乳腺 X 光图像同质化高,背景结构相似,大模型更容易把训练集细节背下来,过拟合来得比自然图像数据集快得多。GTX 1660 Ti 这种 6GB 显存跑 m 级 batch=8 imgsz=416 没问题,x 级大概率 OOM。
解决:先用 yolov8n 把数据管线和训练流程跑通,再用 yolov8m 做正式 baseline。等数据扩充到 3000 张以上,再考虑 l 或 x。资源里带的四档预训练权重不是让你一上来就全试一遍,n、m、l、x 的关系是递进的:用 m 训出第一个可信 baseline 后,把它的 best.pt 当作后续更大模型的初始化权重,比每次从头烤 GPU 更省时间。
5. 进阶:冻结 backbone 与 Grad-CAM 热力图验证
5.1 用冻结层把 416 权重迁移到 640
第 3 章训出的 best.pt(416 输入)不是终点。把它当「预训练权重」接着训 640 输入,比从头训 640 收敛快得多:
yolo detect train \ data=breast.yaml \ model=runs/breast/mass_416/weights/best.pt \ imgsz=640 \ epochs=40 \ batch=4 \ freeze=10freeze=10 表示冻结 model.model 前 10 个模块的权重。YOLOv8 的前 10 层基本覆盖 backbone 的卷积和 C2f 结构,提取的是通用的乳腺组织纹理特征,冻结它可以保留 416 阶段学到的特征,只让检测头和后半部分适应新分辨率。跑完这 40 个 epoch,再把 freeze 去掉全量微调 10~20 个 epoch,效果通常比直接训 640 更稳。
5.2 Grad-CAM 判断模型到底在看什么
乳腺 X 光背景高度相似,模型很容易学到「看胸大肌边缘」这种伪特征。mAP 高不保证泛化好,所以训练结束后我习惯做热力图而不是只看指标。YOLOv8 没有分类模型那种 classifier 输出,常见做法是 hook backbone 最后一个 C2f 的输出做 Grad-CAM。
import cv2 import numpy as np import torch from ultralytics import YOLO from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image model = YOLO("runs/breast/mass_416/weights/best.pt").model model.eval() # 打印层清单,确认 backbone 最后一个 C2f 的索引 for i, layer in enumerate(model.model): print(i, type(layer).__name__) target_layers = [model.model[8]] # 以打印结果为准,n/s/m/l/x 有微小差异 img = cv2.imread("val_sample.png") rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) input_tensor = torch.from_numpy(rgb.transpose(2, 0, 1)).unsqueeze(0).float() / 255.0 cam = GradCAM(model=model, target_layers=target_layers) grayscale_cam = cam(input_tensor)[0] vis = show_cam_on_image(rgb.astype(np.float32) / 255.0, grayscale_cam, use_rgb=True) cv2.imwrite("heatmap_416.png", vis)hook 层选得太浅,热力图只反映低层边缘纹理;选得太深,缺失空间信息,高亮区域糊成一片。backbone 最后一个 C2f 通常就在 model.model[8] 或 [9] 附近,以打印结果为准。热力图实验的意义很直接:高亮集中在肿块周围,说明模型学到的是病灶;高亮在乳房边缘或胸肌,说明它抄了近路,需要回数据里加难样本或调整归一化。把同一张图在 416 和 640 下各出一张热力图对比,还能直观看到小病灶在高分辨率下唤醒区域是否更聚焦。
我拿到任何别人整理好的数据集,固定步骤都是:先删 labels.cache,再按病例分组划分,跑完训练最后做一次热力图抽查。第一次用这份资源时,我花了两天才定位到 val mAP 虚高是患者串组导致的,换成 GroupShuffleSplit 重新训练后,mAP 从 0.83 掉到 0.74,但换新病人测试反而更稳。那个「变差」的数字才是可信的。从那以后我每次完成乳腺 X 光数据管线,都强制走一遍「删 cache → 查类别比例 → 按病例划分 → 热力图抽查」四连,这类数据结构太相似,不这样做很难分清模型到底学会了什么。希望帮到你。
本文还有配套的精品资源,点击获取