简介:面向医学影像分析与深度学习检测场景,这套X光片肺病数据集包含800张原始胸片及对应标注,覆盖细菌性肺炎、新冠病毒、正常肺、结核、病毒性肺炎五类,适合用于训练YOLOv5等目标检测模型,帮助研究者快速搭建肺病辅助诊断原型。压缩包共1601个文件,由800个jpg图像、800个txt标签文件及1个yaml配置组成,txt按YOLO格式记录目标类别与边界框坐标,yaml定义类别名称与路径,数据组织规范,可直接接入训练流程。资源整体约25.51MB,已有410人学习。对于初学者,可省去采集与标注环节,直接用于模型训练、验证与论文实验;对研究者,也可作为多分类肺病检测的基准数据补充。
1. 这包 X 光片肺病数据集到底能不能直接喂给 YOLOv5
做医学影像目标检测的人,最烦的不是模型调参,而是数据集的“脏”。标签错位、格式不统一、类别分布离谱,这些坑能让人在数据预处理上消耗掉比训练多三倍的时间。我拿到这包X光片肺病数据集时,第一反应是先验证它是不是“开箱即用”:800张原始图片,已用YOLOv5格式标记,覆盖细菌性肺炎、新冠病毒、正常肺、结核、病毒性肺炎五个类别,并且文件带有明显的Roboflow导出特征。这意味着坐标系已经归一化、标签和图片一一对应、分类名已经写进配置——理论上解压后改一下路径就能启动训练。这篇笔记就是我从拆包到跑通全流程的记录:目录结构怎么认、YOLOv5的标签格式长什么样、训练参数怎么设、哪些坑我替你先踩了。
适合谁看:正在做肺病分类或检测课题的学生、刚接触YOLOv5想拿真实数据集练手的开发者、以及需要快速验证检测方案可行性的算法工程师。
2. 先拆包:Roboflow 导出结构和 YOLOv5 标签格式的对应关系
2.1 文件命名里藏的信息:.rf. 后缀与数据来源
解压后你会看到一堆类似1_coronavirus-420-_jpg.rf.9a9483366b10e7935eac8e4bd2b7a0ab.jpg的文件。这个命名格式是 Roboflow 导出的典型特征。
拆解一下:1_coronavirus-420-_jpg是原始文件名和类别提示,.rf.后面那串哈希值是 Roboflow 为每个样本生成的唯一 ID,用来保证导出后文件名在各类别间不冲突。如果你用脚本重新整理数据集,这个哈希 ID 可以当作主键用,避免重命名时把训练集和验证集的对应关系搞丢。文件名里的Normal_test-3-、Normal_val-27-这类前缀还暗示了 Roboflow 切片时的原始划分逻辑——test、val、train 的痕迹在文件名里就有,后面整理目录时可以据此二次校验。
这给我们的第一个实操启示:不要一上来就自己重命名文件,先保留原始命名跑通一次训练,确认数据和标签匹配,再做任何美化操作。我见过太多人第一个小时就花在写重命名脚本上,结果脚本边界条件写错,训练时一堆 no labels 报错,得不偿失。
2.2 labels 目录与 txt 标签格式详解
Roboflow 导出 YOLOv5 格式时,图片和标签是分目录存放的。典型结构是:
dataset/ ├── train/ │ ├── images/ │ │ └── 1_coronavirus-420-_jpg.rf.9a9483366b10e7935eac8e4bd2b7a0ab.jpg │ └── labels/ │ └── 1_coronavirus-420-_jpg.rf.9a9483366b10e7935eac8e4bd2b7a0ab.txt ├── valid/ │ ├── images/ │ └── labels/ └── data.yaml每个.txt标签文件里,一行对应一个标注框,格式是:
class_id center_x center_y width height注意:坐标全部是归一化的,取值范围 0~1,不是像素值。这是 YOLOv5 训练硬性要求,如果你的原始数据是像素坐标(Pascal VOC 或 LabelMe 导出),必须先除以图片宽高做归一化,否则 loss 直接飞掉,模型什么都学不到。
我自己写过一个最小转换脚本,逻辑大概长这样:
import os from PIL import Image def voc_to_yolo(txt_path, img_path, out_path, class_map): # 读取图片尺寸用于归一化 w, h = Image.open(img_path).size with open(txt_path, 'r') as f: lines = f.readlines() out_lines = [] for line in lines: # VOC格式: xmin ymin xmax ymax class_name parts = line.strip().split() if len(parts) != 5: continue xmin, ymin, xmax, ymax = map(float, parts[:4]) class_name = parts[4] cls_id = class_map[class_name] # 中心点和宽高都按像素转归一化 cx = (xmin + xmax) / 2 / w cy = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h out_lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n") with open(out_path, 'w') as f: f.writelines(out_lines) # 用法:把VOC格式txt转为YOLO格式 # class_map 决定类别ID,要和 data.yaml 里的顺序严格一致逻辑说明:这个函数的核心工作是把 VOC 的左上右下坐标转成中心点加宽高的归一化表示。前四步换算缺一不可——中心点 X 是左右取平均再除以宽,中心点 Y 是上下取平均再除以高,宽高直接做差后除以对应尺寸。
参数说明:class_map必须手动维护,比如{'bacterial_pneumonia': 0, 'coronavirus': 1, 'normal': 2, 'tuberculosis': 3, 'viral_pneumonia': 4},这个顺序要和data.yaml里的names列表一一对应。顺序错了,训练照样跑,但推理结果是乱的,这种错误最阴。
2.3 data.yaml 配置与五分类映射
data.yaml是整个数据集的配置文件,YOLOv5 训练时第一个读的就是它。内容大致是:
train: ../dataset/train/images val: ../dataset/valid/images nc: 5 names: ['bacterial_pneumonia', 'coronavirus', 'normal', 'tuberculosis', 'viral_pneumonia']我的建议是:拿到手先手动改train和val为绝对路径或项目相对路径,不要用 Roboflow 导出时的默认相对路径。这个文件里最容易翻车的坑是nc与names长度不一致——Roboflow 偶尔会因为在标注时删过类别,导致 names 列表里残留了空类别。你可以在终端里快速验证:
python -c "import yaml; d=yaml.safe_load(open('data.yaml')); print('nc:', d['nc']); print('names:', d['names']); print('count match:', d['nc']==len(d['names']))"如果输出count match: False,说明你的数据配置有问题,训练时类别数会错位。正常情况下这包数据集的nc是 5,对应摘要里说的五类。我每次训练前都强制自己跑一遍这段检查脚本——花十秒,省三小时。
3. 把数据集跑进 YOLOv5:环境配置、训练命令与参数选择
3.1 环境版本对照与 conda 快速安装
YOLOv5 对 PyTorch 版本有隐性要求。我常用的组合是 Python 3.9 + PyTorch 1.13 + CUDA 11.7,这个组合跑官方仓库的 requirements 基本不踩坑。如果 CUDA 版本不对,会报AssertionError: CUDA unavailable之类的问题。
conda create -n yolo5 python=3.9 -y conda activate yolo5 pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --extra-index-url https://download.pytorch.org/whl/cu117 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt逻辑说明:先建独立环境避免污染已有环境,然后按 CUDA 版本安装对应 PyTorch 轮子,最后拉最新 YOLOv5 仓库并安装依赖。
参数说明:https://download.pytorch.org/whl/cu117是指定 PyTorch 版本与 CUDA 版本的配套来源,requirements.txt里包含 opencv-python、pandas、matplotlib 等依赖,如果安装失败,通常是 numpy 版本冲突,手动pip install numpy==1.23.5能解决大多数这类问题。
3.2 目录挂载与路径检查
训练前把数据集目录和 YOLOv5 仓库放到同一层级,比如:
project/ ├── yolov5/ ├── dataset/ │ ├── train/ │ ├── valid/ │ ├── data.yaml然后确认数据集里有没有 test 目录。Roboflow 导出时通常会包含 test 集,但 YOLOv5 的 train.py 只认 train 和 val,test 不会被读取。所以你要么忽略 test,要么把 test 并进 val 增强验证集多样性。对 800 张图的小数据集,我更建议把 test 合并进 val——验证集大一点,mAP 指标参考性更强。
# 在项目根目录运行,检查图片和标签数量是否匹配 python -c " import os for split in ['train', 'valid']: imgs = os.listdir(f'dataset/{split}/images') labels = os.listdir(f'dataset/{split}/labels') img_names = {x.split('.')[0] for x in imgs if x.endswith('.jpg')} label_names = {x.split('.')[0] for x in labels if x.endswith('.txt')} print(split, 'images:', len(img_names), 'labels:', len(label_names)) print('missing labels:', len(img_names - label_names)) "如果missing labels不为 0,说明有图片没有对应标注,YOLOv5 会忽略这些图片但会打印 warning,不影响整体训练,但会白白浪费样本。我一般会把这些孤儿图片挑出来单独检查,看是不是标注时漏了。
3.3 训练命令参数解读:imgsz、batch、epochs 与类别权重
训练命令我实际用的是这一条:
python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data ../dataset/data.yaml \ --cfg models/yolov5s.yaml \ --weights yolov5s.pt \ --workers 4 \ --name chest_xray_run关键参数逐个说明:
--img 640:输入图片统一缩放到 640×640。X 光片长宽比通常接近 1:1,直接缩放不会造成严重形变。如果你的显卡显存有限,降到 512 也可以,但小目标(比如早期病灶)会丢失细节。--batch 16:批量大小取决于显存。我用 8G 显存跑 640 输入,batch 16 是上限,再大会 out of memory。如果显存不足,优先降 batch,不要降 img。--epochs 100:800 张图的数据量,100 轮是起步。医学影像特征差异大,一般 60 轮后 mAP 才稳定。早停机制可以在 val 指标连续 20 轮不涨时自动截断,省训练时间。--cfg models/yolov5s.yaml:s 是 small 版本,适合小数据集,速度快、不容易过拟合。如果你发现欠拟合明显(训练集 mAP 都很低),换 m 或 l 版本前先检查标注质量,而不是盲目加大模型。--weights yolov5s.pt:用 COCO 预训练权重做迁移学习初始化。这个很重要——医学影像虽然和自然图像差异大,但底层纹理特征迁移仍然有效。不要用--weights ''从零训练,小数据集会收敛得很慢。
另外,--weights指定预训练权重后,类别数不匹配不会报错。YOLOv5 会忽略预训练权重中与当前模型输出层形状不一致的部分,只迁移 backbone 层。这是官方行为,不算 bug。
3.4 类不平衡问题:这包数据里谁多谁少
从文件名初步估算,coronavirus 类别的样本量明显多于结核和正常肺。这种不平衡在小数据集上会导致模型偏好头部类别。两个处理手段:
第一个手段是类别权重。在训练命令里加:
--cls 0.7--cls控制分类 loss 的权重系数。默认是 0.5,当你发现少数类别 recall 明显偏低时,可以加大到 0.7~1.0,代价是定位精度可能轻微下降,因为模型更努力区分类别而不是精修框。
第二个手段是数据增强。YOLOv5 内置的 mosaic 增强会把四张图拼成一张,相当于扩大了少数类别的有效样本量。--hyp hyp.scratch-low.yaml里mosaic: 1.0是默认开启的。对小数据集,我建议保留默认增强强度,不要盲目加大hsv_h、hsv_s——X 光片是灰度图,颜色抖动对灰度图几乎无效,反而浪费算力。
4. 训练与推理避坑:四类常见翻车现场
4.1 现象一:训练开始后 loss 一直是 NaN
Loss 输出显示nan,训练无法继续。常见原因有两个:学习率设置过高或标签坐标越界。YOLOv5 默认学习率是 0.01,配合 SGD 优化器一般不会爆。优先检查标签文件——用文本编辑器打开任意一个 txt,如果坐标值大于 1 或小于 0,说明归一化步骤出了问题。
解决:写个脚本过滤所有标签文件中越界的坐标,并把对应图片移出数据集,或者重新检查转换逻辑。
python -c " import os for split in ['train', 'valid']: label_dir = f'dataset/{split}/labels' for name in os.listdir(label_dir): with open(os.path.join(label_dir, name)) as f: lines = f.readlines() for line in lines: parts = line.strip().split() if len(parts) == 5: vals = list(map(float, parts[1:])) if not all(0 <= v <= 1 for v in vals): print('bad:', name, line.strip()) "4.2 现象二:valid 集的 mAP 是 0,但训练 loss 在下降
训练集和验证集表现严重脱节。我复盘过这类问题,最常见的原因是目录读取错误——data.yaml 里val路径指到了空目录或不存在目录,YOLOv5 不会报错,只会默默把验证集当作空集对待,于是 mAP 恒为 0。
解决:用--exist-ok重新指定验证集路径,训练前手动打印yaml.safe_load确认 val 路径存在且图片数量不为 0。这属于路径问题,不是模型问题,改配置就解决了。
4.3 现象三:训练时报错 exit code 135
这类错误通常是 OOM(显存不足)或系统内存不足。YOLOv5 的 dataloader 会一次性缓存图片到内存,800 张 640×640 的图片大约需要 1GB 内存,但如果同时开--workers 8,内存会被打满。
解决:把--workers降到 4 或 2,显存不足时同时降低 batch。如果你用 Windows,workers 大于 0 还会触发 DataLoader 的 spawn 模式问题,这时必须保证训练入口是if __name__ == '__main__'包裹,否则会无限递归报错。
4.4 现象四:推理时识别出的类别顺序和真实类别对不上
训练时data.yaml里 names 顺序是['bacterial_pneumonia', 'coronavirus', 'normal', 'tuberculosis', 'viral_pneumonia'],推理脚本里如果单独新建了一个类别列表,顺序写错,就会导致模型输出索引 0 被认为是 normal,但实际 0 对应的是 bacterial_pneumonia。
解决:推理时直接yaml.safe_load载入 data.yaml,用它的 names 列表做映射,不要手写第二遍。这类问题最难排查,但也是最容易预防的——统一走配置,不硬编码。
5. 模型验证与进阶检查:看到置信度之外的信息
5.1 误判模式分析:哪些类别容易混淆
训练完成后,用val.py生成混淆矩阵。对于这个数据集,常见的混淆对是「病毒性肺炎」与「细菌性肺炎」——两者在 X 光片上的磨玻璃影和实变影存在重叠区域,即使有经验的放射科医生也会借助临床指标辅助判断。另一个高频混淆是「正常肺」被误判为「早期结核」,因为早期结核病灶在 X 光片上可能只是一小片模糊影,和纹理噪声几乎无法区分。
我的做法是把混淆矩阵导出后,挑出错误样本逐张看,重点观察标注框的位置是否合理。比如结核误判为正常,很多时候不是因为模型烂,而是因为标注框只框住了病灶的一小部分,模型学到的是一个局部纹理而不是完整的病灶语义。
5.2 用滑窗推理看模型对大片 X 光片的真实表现
800 张原图中,很多是单张肺部正位片,推理时整图缩放 640 后用一次前向可以覆盖完整肺部结构。但如果你打算把模型接到真实 PACS 流程里,面对的片子可能是 3000×3000 甚至更大的原始 DICOM 转出的 JPG,直接缩放会导致小病灶在 resize 过程中直接丢失。我一般会跑一遍滑窗推理验证边界:
import cv2 import torch from PIL import Image def slide_infer(model, img_path, window=640, stride=320): img = cv2.imread(img_path) h, w = img.shape[:2] dets = [] for y in range(0, h - window + 1, stride): for x in range(0, w - window + 1, stride): crop = img[y:y+window, x:x+window] crop_rgb = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB) result = model(crop_rgb, size=window) for box in result.xyxy[0]: x1, y1, x2, y2, conf, cls = box.cpu().numpy() dets.append((x + x1, y + y1, x + x2, y + y2, conf, cls)) return dets逻辑说明:滑窗把大图拆成若干个 640×640 窗口,重叠区用 320 的步长保证目标跨窗口时至少在一个窗口内完整呈现。窗口检测结果还原到原图坐标时,要加上窗口偏移量(x, y)。
参数说明:window=640要和训练时的--img 640保持一致;stride=320是窗口步长,越小重叠越多、检测越稳但耗时越长。医学影像病灶通常不会恰好停在窗口边界,所以重叠步长是必须的——如果步长等于窗口大小,跨窗口目标会被切断,漏检率显著上升。
5.3 置信度阈值调整的两个回合
实际使用时,conf-thres默认是 0.25,iou-thres是 0.45。但医学场景的漏诊代价远高于误报代价,也就是宁可多标一个可疑区域让医生二次确认,也不要漏掉一个病灶。
第一回合:跑一遍验证集,按类别统计置信度分布。少数类别(比如结核)如果置信度中位数已经低于 0.25,说明模型对这类样本本身就缺乏自信,强行调低阈值只会引入大量噪声。第二回合:把阈值调到 0.15,重新跑val.py,看召回率的提升幅度。如果提升很小,说明问题不在阈值,而在特征学习不足,回到数据增强或加训练轮次。
这一步是最容易忽略的环节,很多项目合成后拿默认阈值就交付,实际部署到含噪声的临床影像上漏检率会比验证集高出一大截。从那以后我每次调模型都强制自己走一遍滑窗和阈值敏感度分析,确认在低置信度区间内模型输出是否仍保持稳定。X 光肺病检测的落地点不在训练指标有多漂亮,而在真实读片场景里能不能帮医生少漏一个病灶——把数据、模型、推理链路都吃透,才能离这个目标更近一点。希望这篇拆包笔记能帮你在自己的数据集上少折腾几个小时。
本文还有配套的精品资源,点击获取