简介面向目标检测算法学习与实战的 YOLO 肺结节数据集包专为需要真实医学影像数据开展训练和验证算法的开发者、研究生及竞赛团队准备。图片采集自真实场景质量高且场景多样标注使用 LabelImg 完成框体准确标签同时提供 VOC、COCO、YOLO 三种格式并分目录存放下载后即可直接训练 YOLO 系列模型免去格式转换的额外工作量。整个压缩包共 2000 个文件对应 1000 张图片的标签与配套内容以 xml 和 txt 标签为主体附带 py 划分脚本、yaml 配置和 html 教程文档大小约 18.74MB。附赠内容包括环境搭建指引、Linux/Windows 双版本训练教程以及可灵活分割训练集、验证集、测试集并生成 ImageSets 索引的脚本方便按需调整数据划分并快速迁移到自己的检测任务。目前已有 814 人学习下载适合从入门到落地完整跑通肺结节检测项目。1. 1000张肺结节图为什么值得先跑通YOLO再想别的拿到一个.rar压缩包名字写着“YOLO肺结节目标检测数据集”解压后是1000张医学影像和配套的VOC、COCO、YOLO三份标签。很多人的第一反应是“才1000张够训什么”但反过来看肺结节标注本身就是高成本工作能拿到1000张带结节框的CT或胸片切片已经足够跑出一个可复现的baseline。真正的问题不是数据太少而是你有没有把数据划分、标签格式、训练参数这个链路一次走通。这套方案的真正价值在于三种格式标签帮你绕过了最耗时的人工转换划分脚本保证训练集和验证集不串数据训练教程则让YOLO的复杂参数有了一个可参考的起点。适合正在做医学影像检测入门、毕业设计或者算法预研的工程师——先复现再改进而不是从零造轮子。2. VOC、COCO、YOLO三种格式怎么选每个坑都在坐标和类名上2.1 三种格式的存储结构XML、JSON、TXT 各自怎么写VOC格式源于PASCAL VOC竞赛每张图片对应一个XML文件框用左上角和右下角坐标表示。一个典型的VOC标注长这样annotation filenameimg_0001.png/filename size width512/width height512/height depth3/depth /size object namenodule/name bndbox xmin128/xmin ymin96/ymin xmax180/xmax ymax145/ymax /bndbox /object /annotation这段XML里xmin,ymin,xmax,ymax是像素坐标注意不是归一化值也不能直接把它当成中心点坐标喂给YOLO。VOC格式的优势是直观缺点是每张图一个文件读起来啰嗦而且类别名靠字符串匹配大小写不一致就会漏检。COCO格式把所有标注聚合在一个JSON文件里结构分images、annotations、categories三块。annotations里的bbox是[x, y, width, height]同样基于像素坐标表示框左上角坐标和宽高。要特别注意COCO里图片ID是从1开始的而YOLO的分类索引是从0开始。转换COCO到YOLO时类别索引要减1否则第一个类会被当成第二个类训练到推理时你对着图片看半天也找不到正确结果。YOLO格式是最简洁的每张图片对应一个txt文件每一行是一个目标格式是class_id x_center y_center width height四个坐标全部归一化到[0,1]区间。拿上面那个XML举例如果图片宽高都是512结节框的中心点是((128180)/2/512, (96145)/2/512)宽高是((180-128)/512, (145-96)/512)。这里只要漏了除法训练Loss直接飘到NaN这是新手最常见的翻车原因。2.2 为什么要用脚本转换而不是手工改一份可用的VOC转YOLO逻辑既然三种格式各有生态工程上最稳的办法是写脚本批量转换。绝大多数目标检测标注工具比如LabelImg导出的是VOCRoboflow能导出COCO和YOLO但当你拿到的数据集三种格式都给了第一步不是急着训练而是先挑一种作为“唯一事实来源”再用脚本生成另外两种。我一般会把VOC当作基准因为XML的坐标不经过归一化最容易人眼核查。下面是我常用的VOC转YOLO脚本参数化设计适合丢到命令行直接跑import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_file, class_names, img_width512, img_height512): tree ET.parse(xml_file) root tree.getroot() width int(root.find(size/width).text) height int(root.find(size/height).text) yolo_lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in class_names: continue class_id class_names.index(name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 做一次边界裁剪避免坐标溢出 xmin max(0, min(xmin, width)) xmax max(0, min(xmax, width)) ymin max(0, min(ymin, height)) ymax max(0, min(ymax, height)) x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height box_width (xmax - xmin) / width box_height (ymax - ymin) / height yolo_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}) return \n.join(yolo_lines)这个脚本把XML里的字符串坐标转成float然后除以图片宽高做归一化。注意size/width的路径写法依赖XML结构不同标注工具导出的字段名可能略有差异比如有的用sizewidth有的用width做根节点跑之前先打印一个XML确认字段。我见过最玄学的错误就是XML字段名一样但图片实际尺寸和XML里写的不一致导致框全部偏移这种情况需要以images目录里的真实尺寸为准重新读取图片。2.3 三种格式不是给你选的而是要对照着排查的很多人误以为数据集提供了三种格式是让你挑一种顺手的。实际上三种格式最大的价值是交叉校验。你可以写一段简单脚本把VOC的框画到原图上再对比COCO和YOLO转换后的框是否一致。这个操作花不了5分钟但能提前暴露坐标偏移、归一化错误、类别索引错位三类问题。以肺结节数据集为例结节通常只有十几到几十像素框稍微偏移一个像素都可能把病灶中心移出框外训练出来的模型自然学不到正样本。我吃过这个亏当时偷懒直接用COCO标签训练跑完mAP只有0.3后来把三种格式的框画出来叠加对比才发现COCO的bbox被某次脚本写成了[x_center, y_center, w, h]模型实际学了一堆错位框。这种问题用肉眼在几十张图上能快速发现靠跑训练曲线反推会浪费好几天。所以拿到这个rar包后我强烈建议先做一步“标签体检”随机抽20张图把三种格式各自的标注框画出来保存成对比图。YOLO格式可以直接用OpenCV画归一化坐标COCO画像素坐标VOC画XML坐标。三张图叠在一起框的位置必须一致。这一步做完再进入划分脚本和训练后面遇到的每个问题都有明确的排查方向。3. 划分脚本的工程细节按病人切片、随机种子与目录结构3.1 按病人维度划分否则验证集mAP是假的有了干净标签下一步是划分训练集、验证集和测试集。这里有个医学影像数据集特有的坑同一患者的多个切片图像高度相似如果随机划分同一个肺结节既出现在训练集又出现在验证集模型等于见过答案再考试验证集mAP虚高。等到换一批新患者实测精度直接掉下去这就是典型的数据泄漏。正确的做法是先用文件名或目录结构识别出患者ID。这个packet里的图片命名通常会有规律比如patient001_slice03.png或者按subjects/patient001/slices/...组织。如果命名规律不明确可以先把所有文件名去重抽前缀人工看一下再分组。划分享受的就是这一层“先分组再切分”的操作。我写过一个按前缀分组的划分脚本核心思路是先建一个patient_id - [image_path,...]的映射再按患者为单位随机分配到训练、验证、测试集合。这样做保证同一患者的剪影全落在一个集合里。import os import random from collections import defaultdict def split_by_patient(image_list, train_ratio0.8, val_ratio0.1, seed42): random.seed(seed) patient_groups defaultdict(list) for img_path in image_list: filename os.path.basename(img_path) # 按文件名下划线第一部分作为患者ID按实际规律调整 patient_id filename.split(_)[0] patient_groups[patient_id].append(img_path) patients list(patient_groups.keys()) random.shuffle(patients) n_train int(len(patients) * train_ratio) n_val int(len(patients) * val_ratio) train_patients patients[:n_train] val_patients patients[n_train:n_train n_val] test_patients patients[n_train n_val:] train_images [] for p in train_patients: train_images.extend(patient_groups[p]) ... return train_images, val_images, test_images这段代码第一行的随机种子决定了整个划分的可复现性。如果你今天跑一遍和明天跑一遍结果不一致先检查是不是没固定种子。split(_)[0]只是最朴素的患者ID提取方式具体过滤逻辑要结合数据命名来改有的用patient001_20230101_slice5.png那就得取前两段或按正则patient\d匹配。分组划分的代价是训练集图片数不可控可能略微偏离你设的比例这在医学小样本场景下完全正常优先保证无泄漏。3.2 比例、随机种子和三个集合的职责边界对于1000张这种规模我建议训练集、验证集、测试集按 6:2:2 或 8:1:1 划分。验证集用来调参和早停测试集只允许在最终评估时碰一次。很多初学者把这俩混用同一个集合既做早停又做最终评估结果就是模型在验证集上性能好但换到新数据就露馅。你现在辛苦一点把测试集锁死后面写论文或向同事汇报时才有底气。划分脚本除了生成三个集合的图片列表还应该顺手输出YOLO需要的train.txt、val.txt、test.txt每个txt一行图片绝对路径对应标签文件用同名.txt放在labels目录下。注意路径里不要有中文和空格YOLO在Windows上读带中文的路径经常黑匣子报错换成英文目录名能省掉一天人生。随机种子建议固定为42但划分完成后把每个集合的文件数量、患者数量、正样本框数量打印出来。这一步是质量闸门如果某个集合里正样本框数为0或者某个患者ID同时出现在两个集合脚本必须报错退出不能静默通过。3.3 参数化划分脚本一个好的划分工具应该能反复用不要只在命令行里手动跑把划分脚本写成可复用工具会更省心。我一般会加argparse接收数据根目录、输出目录、比例、种子和划分方式五个参数。这样换数据集时不用改代码只需要改参数python split_dataset.py \ --image_dir ./images \ --label_dir ./labels \ --output_dir ./splits \ --train_ratio 0.8 \ --val_ratio 0.1 \ --seed 42 \ --by_patient true--by_patient true这个参数很关键它告诉脚本是按病人划分还是按单张图随机划分。如果后续你只是做纯视觉预训练、不涉及医学场景可以切到单图随机划分但只要还做肺结节检测就强制按病人划分。脚本输出后下一步就可以生成data.yaml了这个文件会直接交给YOLO训练流程。另外输出目录里我建议同时存一份split_records.csv记录每个图片路径落在哪个集合以及它的患者ID。日后写论文要声明数据划分方法或者怀疑某个集合有泄漏时这份CSV是唯一的“后悔药”不用重新解压数据集再跑一遍。4. 用YOLOv8跑通肺结节训练最小命令和必调参数4.1 yolo环境配置与预训练模型下载YOLO训练教程最怕的就是环境配半天。直接走Ultralytics这条线Python 3.9-3.11环境下执行一次安装pip install ultralytics opencv-python装完后确认版本然后用命令下载预训练权重。这里特别要说千万不要从头训练。肺结节的语义复杂度不高但数据量只有1000张从零训起来的模型几乎不收敛。正确做法是下载YOLOv8在COCO上的预训练权重把它作为特征提取的起点。下载方式很简单yolo detect train modelyolov8s.pt datayour_data.yaml epochs1这个命令会触发ultralytics自动下载yolov8s.pt权重文件大约几十MB。如果只想下载不训练可以yolo predict modelyolov8s.pt source...。选 s 还是 m 取决于GPU显存。我自己的经验是显存小于8G用yolov8s大于12G可以上yolov8m。肺结节属于小目标检测参数过多的模型容易过拟合这个阶段先求稳定性别盲目上大模型。4.2 先把data.yaml写好再谈训练YOLOv8对数据集的描述全靠一个data.yaml文件。路径可以写绝对路径也可以写相对于yaml所在目录的相对路径。对肺结节数据集类别数通常只有一类写上nodule。如果你拿到的三种格式里COCO有多个类别需要先确认是不是把良性/恶性分开了否则训练时类别数和标签索引对不上Loss照样NaN。path: /your/absolute/path/to/dataset train: splits/train.txt val: splits/val.txt test: splits/test.txt nc: 1 names: 0: nodule注意train/val/test的值是相对于path的路径也可以直接填txt文件的绝对路径。我倾向于填相对路径因为这样整个项目文件夹迁移到别的机器时不用逐个改。而splits/train.txt里的图片路径必须是绝对路径YOLO会按照这个txt里的路径去读图。很多人在这一步踩坑data.yaml写对了但train.txt里的路径写的是相对路径训练时显示“no labels found”其实只是路径拼接问题。4.3 训练命令与参数imgsz、batch、mosaic的取舍最小可用训练命令长这样yolo detect train \ data./data.yaml \ modelyolov8s.pt \ epochs200 \ imgsz640 \ batch16 \ patience30 \ project./runs \ namenodule_v1 \ cacheTrue几个参数在这类医学影像场景里需要重点解释。imgsz我建议至少640因为肺结节在原图里经常只有几个像素到几十像素缩到320会直接丢目标。显存允许的话用768或896会对小目标更友好但要配合batch调小。batch16在单卡上是个合理起步值数值太大训练快但显存爆数值太小BN统计量不准。patience30是早停策略验证集指标连续30轮不提升就停止避免训练时间浪费。还有一个容易被忽略的参数是mosaic。YOLOv8默认开启马赛克数据增强把四张图拼在一起训练。这对自然场景有利但对肺结节这种小目标且背景高度相似的医学图像过强的马赛克可能让正样本位置变得不稳定。我一般的做法是设置mosaic0.5后10轮用close_mosaic10自动关闭马赛克让模型在最后阶段看到完整图像收敛更干净。命令里加这两项yolo detect train \ data./data.yaml \ modelyolov8s.pt \ epochs200 \ imgsz640 \ batch16 \ mosaic0.5 \ close_mosaic104.4 评估指标别只看mAP混淆矩阵和召回率才是医学检出的关键训练结束后runs/nodule_v1/weights/best.pt就是验证集上表现最好的权重。用它对测试集做评估yolo detect val \ model./runs/nodule_v1/weights/best.pt \ data./data.yaml \ splittest终端会输出mAP0.5、mAP0.5:0.95、precision和recall。对肺结节任务我会更关注recall也就是模型到底有没有漏检。医疗场景漏掉一个恶性结节比误报一个良性结节更严重所以哪怕precision低一点也要优先保证recall不塌。这时可以手动把置信度阈值往低调比如conf0.3以下再看recall变化。这一步还会生成confusion_matrix.png但注意里面“总合不唯一”的观感混淆矩阵的数值不是必须加起来等于样本总数因为YOLO本身是在不同confidence阈值下做多尺度预测一个目标可能被多个框预测背景类也可能吸收大量负样本。你看到对角线外的数字很大不要慌先确认它是在验证集还是在测试集下生成的再结合recall曲线判断模型是不是真的偏爱某个类别。5. 训练避坑小样本医学检测最容易翻车的5个现场5.1 翻车现场一验证集mAP高得离谱换一批患者就原形毕露现象训练时验证集mAP冲到0.9以上心里美滋滋结果找几张没见过的CT切片一测一个结节都没框出来。原因采集或划分脚本没按患者分组直接按图片随机划分同一个病人的不同slice同时出现在训练和验证集合。模型实际是“背”下了那个结节的位置而不是学会了“找结节”。解决回到第三章用--by_patient true重新划分。如果原始图片命名没有明显患者ID可以先看目录名很多数据集是按病例子目录组织的。重新划分后mAP掉到0.6左右是正常的那才代表模型真的在泛化。这个坑最大的麻烦是它不报错只会让结果虚高所以划分脚本里必须打印患者重合格检查。5.2 翻车现场二Loss直接变成NaN罪魁祸首是归一化坐标越界现象训练第一个epoch Loss正常第二个epoch直接从1.2飞到nan进程崩了。原因YOLO标签txt里出现了负数、大于1或宽高为0的归一化坐标。常见于手工转换脚本时把像素坐标当成归一化值直接写进去了或者标签框原本就画出了图像边界转换时没裁剪。解决写脚本扫描所有txt检测x_center, y_center, width, height是否落在[0,1]区间。发现问题坐标需要回到标注工具重新看一眼图片尺寸。标签文件里长宽出边界的情况通常可以直接按边界裁剪但如果是中心点错位那就不能裁了得删掉重新标注。这个扫描脚本就是你最便宜的后悔药。5.3 翻车现场三batch size太小BN层统计量飘忽不定现象训练曲线震荡幅度非常大loss下降后又在某个区间来回弹跳验证集的指标也跟着抖。原因肺结节数据集小为了塞进显存你很可能把batch调成了4甚至2。但YOLO主干网络里的BatchNorm层在小batch下统计量不稳定每批的均值和方差差异过大训练后期直接导致BN崩溃也就是热词里说的“bn崩溃”。解决至少把batch提到16。显存不够就降低imgsz到512或者用梯度累积例如batch8加上accumulate2模拟一次16的batch再更新梯度。训练曲线稳定后再慢慢调其它参数。小样本训练翻车十次有八次是batch太小引起的这不是玄学是统计规律。5.4 翻车现场四结节只有十几个像素下采样之后特征彻底消失现象训练结束召回率不错但实际推理时漏掉很多小结节单个结节宽度不到10像素的几乎全漏。原因YOLOv8的下采样倍数从640输入到输出特征图是1/8也就是一个10像素的结节在特征图上只有1个点多一点目标太小特征被背景淹没。医学影像里小目标检测比自然场景的“小汽车”还要极端。解决先把imgsz提高到896甚至1216代价是显存吃紧可以考虑关掉Mosaic并减小batch。再用多尺度预测、TTA后面第六章会讲也可以借鉴遥感图像目标检测里的切图思路把大图切成512的小块再训练。切图时要保证结节不被切破通常用重叠裁切的方式另一半保留在相邻图块里。5.5 翻车现场五混淆矩阵数字怎么都对不上现象打开confusion_matrix.png发现几个数字加起来不等于样本总数甚至对角线上的数字和检测目标总个数对不上。原因YOLO的混淆矩阵是在特定置信度阈值下统计的同时背景类吸收了大量未匹配的预测框。多个锚点对同一个目标产生多个预测NMS会把一部分被抑制的框计入背景所以矩阵元素不是简单的“真实类别 vs 预测类别”一一对应。解决不要纠结矩阵元素总和重点看漏检率矩阵里真实结节类被预测成背景的数量那才是你要压下去的指标。如果这个数字大优先调低conf阈值、提高imgsz、或检查验证集的标注是否齐全。混淆矩阵是诊断工具不是成绩单用它定位问题而不是证明效果。6. 把1000张数据榨出更大价值k-fold、冻结微调与TTA训练教程跑通之后你才刚开始真正调优。第一招是放弃单次划分改用5折交叉验证把患者分5组每次用4组训练1组验证循环5次。每一折都保留一个 best.pt最后把5个模型在测试集上的结果取平均。这个做法会让你的mAP估计可信得多论文里也更有说服力。实现方式可以复用第三章的--by_patient脚本手动循环5次或者用ultralytics的交叉验证配置。不要嫌麻烦小样本医学检测的精度波动很大一次划分的结果可能就是运气。第二招是冻结backbone微调。下载到yolov8s.pt之后先冻结前10层跑30个epoch只训练检测头和颈部网络然后再解冻全部层继续微调。这样做可以防止小数据集下预训练特征被破坏Loss不会一开始就乱跳。用代码实现也很简单训练参数里设置freeze10表示冻结前10层或者用ultralytics的API按层名手动冻结。我习惯这个方法作为所有小样本医学检测的默认起点效果比从头训练稳得多。第三招是推理阶段的测试时增强TTA。yolo detect predict命令里直接加ttaTrue模型会对同一张图做多尺度预测然后融合结果。在肺结节场景下TTA能救回一部分被漏掉的小结节。代价是推理时间增加但对离线分析场景完全可接受。如果你部署到实时设备再用标准模式。TTA不能替代数据质量但它是你最后一层兜底。这三招做完1000张数据的利用率才能说真的被榨干了。我自己的习惯是每调一个参数都记下当时测试集的精确率、召回率和阈值避免调了几天却说不清楚哪个改动起了作用。这已经不是天赋问题而是工程习惯问题。肺结节检测容不得玄学每一步可复现才敢往临床合作方向走。希望这些血泪经验能帮到你少走一段我走过的弯路。本文还有配套的精品资源点击获取