☰
YOLO乳腺癌检测实战:单类别数据集训练避坑指南
2026/10/22 3:17:41 网站建设 项目流程

简介:本资源为面向医学图像目标检测的乳腺癌检测数据集,适用于深度学习入门者、医学影像算法研究者及需要开展癌症病灶检测实验的学生与工程师。数据集采用YOLOv5标准文件夹结构保存,无需额外预处理即可直接投入训练,图像统一为640×640分辨率RGB格式,边界框标注清晰完整,前景丰富,每张图像均含多个目标。包内共1845个文件,以922个txt标签文件、921张jpg图像为主,另附1个可视化py脚本与1个png预览图,压缩包约12.87MB。数据划分为训练集778张图像及对应778个标签、验证集143张图像及对应143个标签,类别为breast cancer单类。可视化脚本无需修改即可运行,随机传入一张图片便能绘制边界框并保存至当前目录,便于快速核验标注质量。目前已有1074人学习下载,适合作为乳腺癌检测模型训练与验证的实用数据基础。

1. 乳腺癌检测数据集为什么让一批 YOLO 模型集体翻车

拿到「YOLO 医学图像数据集:乳腺癌检测(1类别,包含训练集、验证集)」这个标题,很多人第一反应是:单类别检测,任务够简单,拿现成 YOLO 权重微调几轮就能出结果。真跑起来才发现,mAP 卡在 0.5 上下不动,验证集 loss 震荡,模型把病灶和正常腺体组织混成一团。问题不在 YOLO 本身,而在这类数据集的结构和医学图像的成像特性——单类别意味着没有类别间竞争来帮模型收敛,病灶区域小、对比度低、边界模糊,训练集和验证集的分布差异往往比自然图像数据集大得多。

这篇笔记面向的是手里已经有一份乳腺癌检测数据集、想用 YOLO 跑通检测链路的从业者。我会按「数据集结构怎么读 → 标注格式怎么转 → 训练参数怎么设 → 验证指标怎么看 → 坑在哪」的顺序,把单类别医学检测从数据到推理的完整路径拆开讲。不涉及任何具体医疗机构或真实病例,所有示例均为模拟数据。读完你应该能判断:这份数据集值不值得投入训练,以及第一轮 baseline 该怎么搭。

2. 单类别乳腺癌检测数据集的结构与标注格式拆解

2.1 训练集/验证集目录长什么样,先做一次数据体检

医学图像数据集常见的交付形态是一个压缩包,解压后得到 images 和 labels 两个平行目录,各自再分 train 和 val。但实际拿到的数据往往更乱:有的把标注写成 CSV,有的用 COCO JSON,有的直接给 XML。第一步不是急着转格式,而是先做数据体检——统计图像数量、分辨率分布、标注框数量和面积分布。

import os import cv2 import numpy as np from pathlib import Path from collections import Counter def dataset_health_check(root_dir): """对 YOLO 格式数据集做基础体检""" stats = {"train": [], "val": []} for split in ["train", "val"]: img_dir = Path(root_dir) / "images" / split lbl_dir = Path(root_dir) / "labels" / split if not img_dir.exists(): continue for img_path in img_dir.glob("*.png"): img = cv2.imread(str(img_path)) if img is None: continue h, w = img.shape[:2] lbl_path = lbl_dir / (img_path.stem + ".txt") n_boxes = 0 areas = [] if lbl_path.exists(): with open(lbl_path) as f: for line in f: parts = line.strip().split() if len(parts) == 5: _, _, bw, bh = map(float, parts[1:]) n_boxes += 1 areas.append(bw * bh) # 归一化面积 stats[split].append({ "file": img_path.name, "w": w, "h": h, "boxes": n_boxes, "mean_area": np.mean(areas) if areas else 0 }) for split in ["train", "val"]: data = stats[split] if not data: print(f"{split}: 无数据") continue res = Counter([d["w"] for d in data]) print(f"{split}: {len(data)} 张图, " f"分辨率分布 top3={res.most_common(3)}, " f"平均框数={np.mean([d['boxes'] for d in data]):.2f}, " f"平均归一化面积={np.mean([d['mean_area'] for d in data]):.4f}") return stats dataset_health_check("./breast_dataset")

这段代码做三件事:遍历 train/val 两个 split,读取每张图的宽高,解析对应的 YOLO 标注文件,统计每张图的标注框数量和归一化面积。输出里最需要关注的是「平均归一化面积」——如果这个值低于 0.01,说明病灶在整张图中占比极小,YOLO 默认的 640 输入尺寸下目标可能只有几十个像素,这是后续 mAP 上不去的核心原因之一。分辨率分布则决定了你该不该统一 resize,还是用 letterbox 保持长宽比。

2.2 标注格式转换:从 COCO/XML 到 YOLO txt 的四个边界坑

如果数据集原始标注不是 YOLO txt,就需要转换。COCO JSON 转 YOLO 是最常见的一种,转换逻辑本身不复杂,但边界处理容易翻车。

import json from pathlib import Path from PIL import Image def coco_to_yolo(coco_json, img_dir, out_dir): """COCO JSON -> YOLO txt,单类别 class_id 固定为 0""" with open(coco_json) as f: data = json.load(f) # 建立 image_id -> 文件信息 的映射 img_map = {img["id"]: img for img in data["images"]} # 按 image_id 聚合标注 ann_by_img = {} for ann in data["annotations"]: ann_by_img.setdefault(ann["image_id"], []).append(ann) out_dir = Path(out_dir) out_dir.mkdir(parents=True, exist_ok=True) for img_id, anns in ann_by_img.items(): info = img_map[img_id] w, h = info["width"], info["height"] lines = [] for ann in anns: x, y, bw, bh = ann["bbox"] # COCO: 左上角 x,y + 宽高 # 转 YOLO: 中心点 + 宽高,全部归一化 cx = (x + bw / 2) / w cy = (y + bh / 2) / h nw = bw / w nh = bh / h # 边界裁剪,防止浮点误差导致越界 cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) nw = min(max(nw, 0.0), 1.0) nh = min(max(nh, 0.0), 1.0) # 过滤掉宽高为 0 的无效框 if nw <= 0 or nh <= 0: continue lines.append(f"0 {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") if lines: stem = Path(info["file_name"]).stem with open(out_dir / f"{stem}.txt", "w") as f: f.write("\n".join(lines)) coco_to_yolo("./annotations.json", "./images", "./labels_all")

四个边界坑分别是:第一,COCO 的 bbox 是左上角坐标加宽高,YOLO 是中心点坐标加宽高,转换时中心点要加半个宽高,这一步写错会导致框整体偏移;第二,浮点运算后归一化坐标可能略微超出 [0,1],必须裁剪,否则训练时数据增强会报错;第三,宽高为 0 的退化框要过滤,医学标注里偶尔出现;第四,图像文件名和标注文件名的 stem 必须严格对应,大小写敏感,Windows 和 Linux 之间迁移时最容易出问题。

2.3 单类别数据集的划分策略:验证集不能随便切

训练集和验证集的划分不是随机切就完事。医学图像有个特点:同一批数据可能来自同一台设备、同一个扫描参数,随机切分会导致训练集和验证集高度同分布,验证指标虚高,换一批数据就崩。常见做法是按来源分组切分——如果数据集里隐含了设备批次或扫描序列信息,尽量让验证集覆盖不同的来源。

另一个问题是单类别数据集的正负样本比例。如果验证集里有一部分图像完全没有标注框(纯负样本),这些图对 mAP 没有贡献,但对误检率评估很重要。建议在划分时保证验证集中至少包含 10% 到 20% 的纯负样本,否则你无法判断模型在正常组织上的误报情况。

# 按 8:2 划分,同时保证验证集包含负样本 python split_dataset.py \ --images ./images_all \ --labels ./labels_all \ --out ./breast_dataset \ --val-ratio 0.2 \ --neg-ratio 0.15 \ --seed 42

--neg-ratio控制验证集中纯负样本的最低比例,--seed固定随机种子保证可复现。划分完成后,训练集和验证集的图像文件分别复制到images/train和images/val,标注文件同理。这一步做完,数据集才算真正可用。

3. 用 YOLO 在本地跑通乳腺癌检测的最小训练链路

3.1 环境准备与数据配置文件

YOLO 训练需要一个 YAML 配置文件描述数据集路径和类别信息。单类别数据集的配置非常简洁,但路径写错是最常见的翻车点。

# breast_data.yaml path: /home/user/breast_dataset # 数据集根目录,绝对路径最稳 train: images/train # 相对 path 的训练集图像目录 val: images/val # 相对 path 的验证集图像目录 nc: 1 # 类别数,乳腺癌检测只有 1 类 names: 0: lesion # 类别名,用 lesion 比用 breast_cancer 更中性

path建议写绝对路径,相对路径在不同工作目录下启动训练时容易找不到文件。nc必须和标注文件里的 class_id 最大值一致,单类别就是 1,class_id 从 0 开始。names里的名称只影响可视化标签显示,不影响训练,但建议用简洁的英文名。

环境方面,YOLO 系列通常依赖 PyTorch、OpenCV、NumPy 等。如果用的是较新的 YOLO 实现,一条 pip 安装命令即可。显存方面,单类别检测在 640 输入尺寸下,8GB 显存足够跑 batch size 16。

pip install ultralytics opencv-python numpy pyyaml # 验证环境 python -c "import torch; print(torch.__version__, torch.cuda.is_available())"

3.2 训练命令与关键参数怎么设

最小训练链路就是一条命令,但参数设不对,结果差很多。

yolo detect train \ data=breast_data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.001 \ lrf=0.01 \ patience=20 \ augment=True \ mosaic=0.5 \ scale=0.3 \ fliplr=0.5 \ flipud=0.0 \ cache=True \ device=0 \ project=runs/breast \ name=exp01

逐参数说明:model选 s 级别而不是 n 级别,是因为医学图像特征弱,n 级别容量不够,容易欠拟合;lr0=0.001比默认的 0.01 小一个量级,医学数据量通常不大,学习率太大会震荡;patience=20表示 20 轮验证指标不提升就早停,避免过拟合;mosaic=0.5把默认的 1.0 降下来,因为医学图像拼接后解剖结构不合理,太强的 mosaic 会引入噪声;flipud=0.0关闭上下翻转,乳腺图像有明确的方位语义,上下翻转会破坏这一先验;cache=True把图像缓存到内存,小数据集能显著加速。

注意:如果显存不够,优先降 batch 而不是降 imgsz。医学图像里小目标多,imgsz 从 640 降到 320 会让病灶区域缩到十几个像素,mAP 直接掉一截。

3.3 训练过程监控:看哪些曲线,什么时候该停

训练启动后,runs/breast/exp01 目录下会生成 results.csv 和若干可视化文件。重点看三条曲线:box_loss、cls_loss、mAP50。单类别检测没有 cls_loss 的类别竞争,cls_loss 通常下降很快,如果它一直不降,说明标注文件里的 class_id 有问题。

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/breast/exp01/results.csv") df.columns = df.columns.str.strip() fig, axes = plt.subplots(1, 3, figsize=(15, 4)) axes[0].plot(df["epoch"], df["train/box_loss"], label="train") axes[0].plot(df["epoch"], df["val/box_loss"], label="val") axes[0].set_title("box_loss"); axes[0].legend() axes[1].plot(df["epoch"], df["metrics/mAP50(B)"], label="mAP50") axes[1].plot(df["epoch"], df["metrics/mAP50-95(B)"], label="mAP50-95") axes[1].set_title("mAP"); axes[1].legend() axes[2].plot(df["epoch"], df["metrics/precision(B)"], label="precision") axes[2].plot(df["epoch"], df["metrics/recall(B)"], label="recall") axes[2].set_title("P/R"); axes[2].legend() plt.tight_layout() plt.savefig("training_curves.png", dpi=150)

判断标准:如果 train/box_loss 持续下降但 val/box_loss 在某个 epoch 后开始上升,就是过拟合,早停该触发;如果 mAP50 在 0.3 以下就平了,大概率是学习率或数据增强参数不对;如果 precision 高但 recall 很低,说明模型太保守,可以适当降低置信度阈值或增加正样本增强。

4. 验证集评估与推理:mAP 之外还要看什么

4.1 验证集评估命令与指标解读

训练结束后,用验证集做一次完整评估。

yolo detect val \ model=runs/breast/exp01/weights/best.pt \ data=breast_data.yaml \ imgsz=640 \ batch=16 \ conf=0.001 \ iou=0.6 \ save_json=True \ project=runs/breast_val \ name=eval01

conf=0.001是评估时的最低置信度,设低是为了计算完整的 PR 曲线,不影响最终 mAP 计算。iou=0.6是 NMS 的 IoU 阈值,医学图像病灶密集时这个值可以适当调低到 0.5,避免相邻病灶被合并。save_json=True会输出 COCO 格式的预测结果,方便后续做逐图分析。

评估输出里,mAP50 和 mAP50-95 是核心指标,但单类别检测还要看 precision-recall 曲线的形状。如果曲线在 recall 0.6 之后 precision 断崖式下跌,说明模型在高召回区间误检严重,临床上不可接受。

4.2 单张图像推理与结果可视化

验证指标是全局的,实际使用时要看单张图的推理效果。

from ultralytics import YOLO import cv2 model = YOLO("runs/breast/exp01/weights/best.pt") results = model.predict( source="./test_images", imgsz=640, conf=0.25, # 推理置信度阈值,比评估时高 iou=0.5, # NMS IoU save=True, save_txt=True, project="runs/predict", name="test01" ) # 逐图统计检出框数量 for r in results: n = len(r.boxes) if r.boxes is not None else 0 print(f"{r.path}: {n} boxes, " f"max_conf={r.boxes.conf.max().item() if n else 0:.3f}")

推理时conf=0.25是常用起点,比评估时的 0.001 高,目的是过滤掉低置信度噪声。如果发现漏检多,降到 0.15 试试;如果误检多,升到 0.4。save_txt=True会输出每张图的检测结果 txt,格式和训练标注一致,方便做错误分析。

4.3 错误分析:把漏检和误检分开看

拿到推理结果后,不要只看总数,要把漏检和误检分开统计。漏检是验证集里有标注但模型没检出的框,误检是模型检出但标注里没有的框。这两类错误的成因完全不同:漏检通常是目标太小或对比度太低,误检通常是正常组织被误判。

import numpy as np from pathlib import Path def error_analysis(pred_dir, gt_dir, iou_thresh=0.5): """简单统计漏检和误检数量""" tp, fp, fn = 0, 0, 0 for pred_file in Path(pred_dir).glob("*.txt"): gt_file = Path(gt_dir) / pred_file.name preds = np.loadtxt(pred_file, ndmin=2) if pred_file.stat().st_size else np.empty((0, 5)) gts = np.loadtxt(gt_file, ndmin=2) if gt_file.exists() and gt_file.stat().st_size else np.empty((0, 5)) # 简化匹配:按中心点距离粗匹配,实际项目建议用 pycocotools matched_gt = set() for p in preds: best_iou, best_idx = 0, -1 for i, g in enumerate(gts): if i in matched_gt: continue # 计算 IoU(此处省略具体实现,按标准公式) iou = compute_iou(p[1:], g[1:]) if iou > best_iou: best_iou, best_idx = iou, i if best_iou >= iou_thresh: tp += 1 matched_gt.add(best_idx) else: fp += 1 fn += len(gts) - len(matched_gt) print(f"TP={tp}, FP={fp}, FN={fn}, " f"precision={tp/(tp+fp+1e-6):.3f}, recall={tp/(tp+fn+1e-6):.3f}") error_analysis("runs/predict/test01/labels", "./breast_dataset/labels/val")

这段代码是简化版匹配逻辑,实际项目建议直接用 pycocotools 的 COCOeval,它处理了多框匹配、面积分组等细节。但简化版足够让你快速判断:如果 FN 远大于 FP,优先解决漏检;如果 FP 远大于 FN,优先解决误检。

5. 乳腺癌检测训练避坑:5 个血泪踩坑记录

5.1 坑一:验证集 mAP 很高,换一批数据就崩

现象:训练时验证集 mAP50 到 0.85,信心满满拿去推理新数据,结果漏检一大片。原因:训练集和验证集来自同一批数据,分布高度一致,模型记住了这批数据的成像特征而不是病灶本身的特征。解决:划分验证集时按来源分组,如果数据里隐含设备批次信息,确保验证集覆盖不同批次;同时用更强的数据增强(亮度、对比度、高斯噪声)提升泛化。

5.2 坑二:标注框归一化坐标越界导致训练中断

现象:训练启动几轮后报错,提示坐标超出 [0,1] 范围。原因:原始标注转换时浮点运算产生微小越界,或者标注本身有误。解决:转换脚本里加裁剪逻辑(见 2.2 节代码),同时在训练前跑一次标注校验,把所有越界框修正或剔除。

5.3 坑三:mosaic 增强让病灶位置变得不合理

现象:训练 loss 正常下降,但验证 mAP 始终上不去。原因:mosaic 把四张图拼成一张,医学图像的解剖结构被破坏,模型学到了拼接边缘的伪特征。解决:把 mosaic 从默认 1.0 降到 0.3 到 0.5,或者直接关闭,改用 mixup 和随机仿射变换。

5.4 坑四:图像尺寸不统一,letterbox 后目标太小

现象:数据集里图像分辨率从 512 到 2048 不等,统一 resize 到 640 后小图目标尚可,大图目标缩得太小。原因:直接 resize 改变了目标像素占比。解决:用 letterbox 保持长宽比,或者对高分辨率图像做切块训练(tiling),把大图切成 640 的子图分别标注和训练。

5.5 坑五:推理置信度阈值照搬评估值

现象:评估时 mAP 不错,实际推理时要么漏检多要么误检多。原因:评估时 conf=0.001 是为了算完整 PR 曲线,实际推理需要根据场景调。解决:在验证集上扫一遍置信度阈值,画出 precision 和 recall 随阈值变化的曲线,选一个业务可接受的平衡点。临床上通常优先保 recall,阈值可以设低一些。

6. 把单类别检测推到可用:阈值扫描与模型集成的小技巧

单类别乳腺癌检测的最后一公里,往往不是模型结构问题,而是决策阈值和结果后处理。我一般会做两件事:阈值扫描和轻量集成。

阈值扫描的做法是:在验证集上跑一次推理,保存所有预测框的置信度和匹配结果,然后从 0.05 到 0.9 以 0.05 为步长遍历,统计每个阈值下的 precision、recall 和 F1。

import numpy as np def threshold_sweep(preds, gts, iou_thresh=0.5): """preds/gts: list of (conf, x1, y1, x2, y2)""" thresholds = np.arange(0.05, 0.95, 0.05) results = [] for t in thresholds: tp = fp = fn = 0 for pred, gt in zip(preds, gts): p = [x for x in pred if x[0] >= t] matched = set() for box in p: best_iou, best_j = 0, -1 for j, g in enumerate(gt): if j in matched: continue iou = compute_iou(box[1:], g[1:]) if iou > best_iou: best_iou, best_j = iou, j if best_iou >= iou_thresh: tp += 1; matched.add(best_j) else: fp += 1 fn += len(gt) - len(matched) prec = tp / (tp + fp + 1e-6) rec = tp / (tp + fn + 1e-6) f1 = 2 * prec * rec / (prec + rec + 1e-6) results.append((t, prec, rec, f1)) # 打印 F1 最高的三个阈值 results.sort(key=lambda x: -x[3]) for r in results[:3]: print(f"conf={r[0]:.2f}, P={r[1]:.3f}, R={r[2]:.3f}, F1={r[3]:.3f}") return results

这个扫描能直接告诉你:在保 recall 的前提下,置信度最低能放到多少。临床上如果要求 recall 不低于 0.9,就选满足这个条件里 precision 最高的阈值。

轻量集成的做法更简单:用同一份数据训练两个不同规模的模型(比如 s 和 m),推理时把两个模型的预测框合并再做一次 NMS。单类别检测的集成收益通常比多类别小,但在小目标上往往能提升几个点的 recall。我自己的习惯是,如果单模型 recall 卡在 0.8 上不去,就加一个 m 级别模型做集成,通常能推到 0.85 以上,代价是推理时间翻倍。

最后说一个我踩过的坑:不要用测试集去调阈值。阈值扫描必须在验证集上做,调完直接用到测试集。我见过有人反复在测试集上试阈值,最后指标好看但模型实际不可用。验证集和测试集的边界,在医学检测里比自然图像更敏感,因为数据量小,过拟合验证集的风险更高。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询