YOLOv5训练X光片肺炎检测模型:数据验证与避坑指南
简介800张胸部X光片原始图片与YOLOv5格式标注共同构成一份肺病数据集可直接用于目标检测模型训练、验证与迁移学习主要面向医学影像分析与深度学习初学者内容覆盖细菌性肺炎、新冠病毒、正常肺、结核、病毒性肺炎五类场景可支撑科研实验、课程设计及肺炎智能筛查项目的前期验证。压缩包共1601个文件包含800个jpg图片、800个txt标注文件和1个yaml配置文件整体大小25.51MBtxt文件记录归一化后的边界框坐标与类别编号yaml文件定义数据集路径与类别名称结构清晰、标注统一接入YOLOv5训练流程几乎无需额外清洗。目前已有410人学习说明其在教学演示与实战练习中具备一定参考价值。借助该数据集读者能快速搭建肺炎病灶检测实验、复现多类别分类结果并可结合数据增强、超参数调优等方式继续优化模型省去从零采集图像和手动标注的繁琐环节。1. 这个zip里的X光片肺病数据集能不能用yolov5训练出能用的肺炎检测模型如果让我一句话说清这个zip的价值800张原始胸部X光片用yolov5格式做了目标检测标记覆盖细菌性肺炎、新冠病毒、正常肺、结核、病毒性肺炎五个类别。这种规模在医学影像深度学习里属于典型的小样本但好在yolov5的迁移学习路径足够成熟配一台普通N卡就能跑出一个能演示、能写论文基线、能验证想法的检测模型。它最合适的用户有三类做医学影像课题的学生、想给院内系统做预研的工程师、以及需要快速上手yolov5数据规范和训练流程的算法新人。但先泼一盆冷水它不能作为临床诊断依据800张图连做个靠谱的AI辅助筛查都偏少只能当科研和教学用的结构与套件。拿到包后别急着训练先解压验数据因为这种标注包的坑往往不在模型而在标签本身。2. 拆开zip先验证数据目录结构、标签映射与类别均衡性2.1 解压后先看目录images 和 labels 要一一对应常见做法是解压到独立目录然后用命令行看两层目录结构。yolov5的标准数据布局是images/放jpg或pnglabels/放同名txt每行五个数类别id、归一化中心点x、归一化中心点y、归一化框宽、归一化框高。这个包名字里写了yolov5标记理论上应该就是这个布局但实际拿到手还是要确认。unzip -q X光片肺病数据集.zip -d lung_dataset find lung_dataset -maxdepth 2 -type d | sortunzip -q是安静解压避免刷屏find -maxdepth 2只列两层目录能快速看出是平铺结构images、labels直接在根目录下还是已经分好train/val/test子集。如果看到images和labels平铺在最外层后面需要自己划分如果看到images/train、labels/train同层存在说明原包就已经按yolov5约定的方式组织好了。接下来核对图片和标签数量# 统计图片与标签文件数量数字应接近或相等 ls lung_dataset/images/*.jpg | wc -l ls lung_dataset/labels/*.txt | wc -l如果labels数量比images少很多说明有图片没标注训练时yolov5会过滤掉这些图并打警告如果labels数量反而多说明有冗余txt多数是转换脚本生成的残留。数量对上了还要看标签内容是否正常。我一般会用Python快速统计类别id和空标注文件数import os from collections import Counter label_dir lung_dataset/labels cls_cnt Counter() empty_files 0 for f in os.listdir(label_dir): if not f.endswith(.txt): continue lines [ln.strip() for ln in open(os.path.join(label_dir, f), encodingutf-8).readlines() if ln.strip()] if not lines: empty_files 1 for ln in lines: cls_cnt[int(ln.split()[0])] 1 print(空标注文件数:, empty_files) print(类别id出现次数:, dict(cls_cnt))这段代码能一次性暴露两个问题空文件有多少、类别id是否集中或缺失。如果某个id一次都没出现要么是该类别图片确实没有标注要么是标注格式错误。后面会专门讲“正常肺”这类图片为什么会制造空文件。2.2 类别映射classes.txt 的顺序就是训练时的names顺序yolov5不认标签名只认数字id。一个标准的labels目录下应该有一个classes.txt内容是类别名列表一行一个行号就是id。以这个数据集为例通常是bacterial_pneumonia covid-19 normal tuberculosis viral_pneumonia那么id0是细菌性肺炎id1是新冠病毒id2是正常肺id3是结核id4是病毒性肺炎。训练配置文件data.yaml里的names列表必须和这个顺序完全一致这是整个训练流程里最容易错、错了还不好发现的环节。我见过有人把names写成[normal, bacterial_pneumonia ...]训练时loss照降mAP看起来正常但推理画框时类别名全错位。一个实用的校验办法随便挑一张已知类别的图片把labels里的txt第一行第一列数字打印出来对照classes.txt确认。如果原包已经带了classes.txt就信任这个文件数据yaml按它写如果没带就根据标注内容反推id顺序。这里不建议用字典排序因为标注工具保存顺序和实际类别顺序未必一致手动确认比猜靠谱得多。2.3 800张图五个类别先数一数再决定训练策略800张图平均到五个类别是每类160张但实际包几乎不可能这么均衡。正常肺作为对照类往往会多一些某些肺炎类别可能只有几十张。所以训练前要有一个判断哪些类需要补样本哪些类可以靠增强扛过去。常见的三类做法是对少样本类别做重复采样让每个类别参与训练的次数接近做离线增强比如水平翻转或小角度旋转但增强后的图片不要再加入验证集或者先用原始比例跑一轮看每类的recall再决定补什么。yolov5本身有--hyp参数可以调增强强度但那是训练前的全局配置不解决类别不均衡。如果原包是平铺结构需要先划分train和val。这里注意三个原则固定随机种子保证可复现按类别比例分层抽样避免某一类全掉进训练集划分完成后把labels同步过去不能只移动图片。下面是个简洁的划分脚本import random, os, shutil all_imgs [f for f in os.listdir(lung_dataset/images) if f.endswith((.jpg, .png))] random.seed(42) # 固定种子复现划分结果 random.shuffle(all_imgs) val_imgs set(all_imgs[:int(len(all_imgs) * 0.15)]) # 15%作为验证集 for split in (train, val): os.makedirs(flung_dataset/images/{split}, exist_okTrue) os.makedirs(flung_dataset/labels/{split}, exist_okTrue) for img in all_imgs: split val if img in val_imgs else train name os.path.splitext(img)[0] shutil.move(flung_dataset/images/{img}, flung_dataset/images/{split}/{img}) txt flung_dataset/labels/{name}.txt if os.path.exists(txt): shutil.move(txt, flung_dataset/labels/{split}/{name}.txt) else: # 没有标签也补一个空txt避免yolov5找不到对应文件而跳过 open(flung_dataset/labels/{split}/{name}.txt, w).close()划分完再看一眼images/train和images/val的数量比以及每个子集里是否有空txt。空txt对应的通常是normal类图片这在检测任务里是合法的背景样本但需要确认是“故意留空”而不是“转换漏标”。3. 用yolov5训练这个数据集环境、数据配置与命令3.1 环境准备克隆yolov5仓库并安装依赖拿到标注数据后我不建议自己从头写检测网络直接用yolov5官方仓库是最省事的路径。环境上优先选LinuxWindows也能跑但会遇到一些文件和路径大小写的小毛病。显卡不是硬性要求没有GPU用CPU训这个尺寸的数据集也能跑只是慢一个epoch可能十分钟以上有N卡就先装好CUDA版PyTorch。# 克隆官方仓库并安装依赖建议在虚拟环境里做 git clone https://github.com/ultralytics/yolov5 cd yolov5 python -m venv venv source venv/bin/activate pip install -r requirements.txtrequirements.txt会装好torch、torchvision、opencv、pandas等。如果机器上已经装了torch要留意版本匹配yolov5 6.x及以上通常要求torch1.8。装完检查一下python -c import torch; print(torch.__version__, torch.cuda.is_available())输出里torch.cuda.is_available()如果是True说明可以走GPU训练如果是False后面所有命令都还能跑但--batch要调小--cache ram也慎用。注意不要追最新的yolov5 master分支选官方仓库里一个稳定release版本社区反馈多踩坑资料全。3.2 准备data.yaml用绝对路径或规范性相对路径yolov5训练时通过一个yaml文件告诉模型数据在哪、有几类、类别叫什么。核心字段是path、train、val、nc、names。这里有一个很多人栽跟头的点train和val写的是图片目录但yolov5会自动在同级目录找labels所以图片目录必须叫images标注目录必须叫labels且目录层级保持一致。path: /home/user/lung_dataset train: images/train val: images/val nc: 5 names: [bacterial_pneumonia, covid-19, normal, tuberculosis, viral_pneumonia]path建议写绝对路径。因为yolov5的命令行里--data路径是相对当前工作目录的如果yaml里的路径也是相对路径两个基准叠加在一起很容易找不到图片。改机器或换目录时只需要改这一行的绝对路径其他不用动。names的顺序和章节2.2里classes.txt的顺序必须一模一样这里不建议用中文因为绘图和日志输出时中文可能乱码用英文短命名即可。训练时命令行用--data指向这个yaml文件即可。如果图片已经在images/train下面yolov5会自己去labels/train找对应txt不需要在yaml里写labels目录。3.3 训练命令先跑一个小的验证流程再全量第一次训练不要上大分辨率。yolov5s是这个系列里参数量最小的对这个数据规模足够img640是速度与精度的平衡点batch要看着显存设8G显存跑batch16在640分辨率下可能OOM降到8比较稳。epochs设200配合早停实际可能七八十轮就停。python train.py \ --data lung_dataset.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 200 \ --patience 50 \ --freeze 10 \ --cache ram参数含义--freeze 10冻结前10层骨干权重这是小数据集上防止过拟合的关键操作让预训练特征不被少量X光片破坏--patience 50表示连续50轮验证集mAP没有提升就提前结束800张图不需要硬跑满200轮--cache ram把图片载入内存加快读取但如果内存不够就改成--cache disk或去掉。如果显存不足优先把--img降到512再降batch。训练过程中终端会打印每轮的box_loss、cls_loss、P、R、mAP50等指标。小数据集最典型的sign是训练loss一直降、验证mAP先升后降这种过拟合信号出现时靠的就是patience早停。最终权重在runs/train/exp/下best.pt是验证集上mAP最高的那一轮last.pt是最后一轮权重部署和推理默认都用best.pt。3.4 超参数调优的三个经验值yolov5自带一堆超参数文件data/hyps/hyp.scratch-low.yaml是比较适合小数据集的起点。对X光片这种图像内容高度统一的场景我一般会改动三个地方第一把hsv_h、hsv_s、hsv_v调低X光片是灰度图像颜色抖动基本没用反而可能让模型学到不存在的色彩特征第二mosaic不要开满mosaic1.0会把四张图拼在一起医学图像中病灶区域小拼接后容易被裁掉或扭曲改成0.5比较稳第三flipud关闭胸部X光片的上下方向有解剖学意义上下翻转会让模型学到错误的方位特征。如果不想手写hyp文件也可以在命令行临时改几个关键值python train.py \ --data lung_dataset.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 200 \ --patience 50 \ --freeze 10 \ --hyp data/hyps/hyp.scratch-low.yaml改完hyp之后第一次训练还是用默认参数跑通流程确认能稳定收敛再去调一上来就调参很容易陷入训练十几次全在玄学的循环浪费时间也看不出哪个参数真正起作用。4. 自己补标注的三个拦路虎标注工具、VOC转YOLO和正常肺怎么处理4.1 用labelimg补标注安装、快捷键和YOLO模式如果原标注质量差或者你只想补充某几个类别的样本用labelimg补标注是最常见的做法。它支持PascalVOC和YOLO两种格式切换格式的开关在菜单栏里切到YOLO后再保存就是直接生成txt不需要二次转换。pip install labelimg labelimg lung_dataset/images lung_dataset/labels启动命令里第一个参数是图片目录第二个是标签目录。如果已存在同名txtlabelimg会加载并显示已有框。常用的快捷键是W画框D下一张A上一张CtrlS保存。画框之前确认一下当前模式否则保存出来是XML而不是txt后面还要做格式转换。补标注要注意一个容易被忽略的操作labelimg的类别列表最好提前写到predefined_classes.txt否则每次画新框都要手动输入类别名手误概率极高。这个文件放在labelimg安装目录下的data/里一行一个类别顺序不要乱。自己补的框如果和原包类别id不一致后面训练时会出现id错位模型会把细菌性肺炎当成新冠。4.2 已有XML标注怎么转成YOLO txt归一化坐标转换脚本另一种常见情况是你手上已经有PascalVOC格式的XML标注需要转成yolov5格式。转换核心是坐标换算把左上角(x1, y1)和右下角(x2, y2)的像素坐标替换成中心点坐标加宽高并全部除以图片宽高归一化。import xml.etree.ElementTree as ET def voc2yolo(xml_path, img_w, img_h, class_map): 读VOC XML输出YOLO格式的标签行列表 root ET.parse(xml_path).getroot() out_lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: continue # 跳过不在映射表里的类别 bndbox obj.find(bndbox) x1 float(bndbox.find(xmin).text) y1 float(bndbox.find(ymin).text) x2 float(bndbox.find(xmax).text) y2 float(bndbox.find(ymax).text) # 裁剪到图像边界防止坐标越界 x1 max(0, min(x1, img_w)); x2 max(0, min(x2, img_w)) y1 max(0, min(y1, img_h)); y2 max(0, min(y2, img_h)) if x2 - x1 1 or y2 - y1 1: continue # 过滤掉尺寸小于1像素的框 x_c ((x1 x2) / 2) / img_w y_c ((y1 y2) / 2) / img_h box_w (x2 - x1) / img_w box_h (y2 - y1) / img_h out_lines.append(f{class_map[name]} {x_c:.6f} {y_c:.6f} {box_w:.6f} {box_h:.6f}) return \n.join(out_lines) # class_map 顺序建议和后续data.yaml保持一致 class_map { bacterial_pneumonia: 0, covid-19: 1, normal: 2, tuberculosis: 3, viral_pneumonia: 4, }这段代码里最容易写错的是分母中心点x要除以图片宽度img_w中心点y要除以图片高度img_h框的宽高也是同样的对应关系。有人习惯把所有坐标统一用宽度归一化结果框的y坐标全是错的训练时mAP永远起不来。另外调用这个函数之前一定要从XML的size节点里读实际的width和height不要用猜测的固定分辨率否则换一组图片就全面翻车。4.3 正常肺类别的空标注不要用框把整张肺框进去这是整个数据集里最需要想清楚的一个问题。正常肺怎么标有人直接框住整个肺野告诉模型这个区域属于正常肺。但这样做的实际效果是yolov5学到的不是病变长什么样而是肺野在图像上的位置和形状。一旦换一台X光机拍出来的肺野位置变了模型立刻失灵。更合理的做法是正常肺图片保留空txt不做任何目标框。检测模型只负责找出四类病变区域如果一张图没有框它就相当于一个背景图告诉模型这里没有需要报警的目标。但yolov5对这种空标注图片没有分类损失模型不会专门学习正常肺这个概念。如果你明确需要一个正常/异常的二分类结果我的建议是分两步走第一步用yolov5检测四类病变第二步在检测区域使用一个轻量分类器判断该区域是否真的异常。不要把normal硬塞进yolov5的框里。还有另一种做法是把正常肺标注成整个胸腔区域类别为normal。这种方案在图像水平上能跑通但会带来一个严重问题一张图上既有正常区域又有病灶区域时框会重叠模型会confuse。所以拿到这个zip时最好先检查normal类的txt内容。如果发现normal类的框宽和框高都接近0.8甚至1.0基本就是框整肺了训练前必须把这类框删掉或者干脆移除normal类别。4.4 标注质量检查把标签画回原图看一眼训练前一定要做可视化验证。这一步花十分钟能省掉后面几小时的排查。写一个简单的OpenCV脚本随机抽几张图把txt里的框画回图片上保存出来人工看。import cv2, random, os img_dir lung_dataset/images/train label_dir lung_dataset/labels/train names [bacterial_pneumonia, covid-19, normal, tuberculosis, viral_pneumonia] for img_name in random.sample(os.listdir(img_dir), 5): img cv2.imread(os.path.join(img_dir, img_name)) h, w img.shape[:2] base os.path.splitext(img_name)[0] txt_path os.path.join(label_dir, base .txt) if not os.path.exists(txt_path): print(f跳过 {img_name}没有txt) continue for line in open(txt_path): parts line.strip().split() if len(parts) ! 5: continue cid, x_c, y_c, bw, bh map(float, parts) # 归一化坐标换算回像素坐标 x1 int((x_c - bw / 2) * w) y1 int((y_c - bh / 2) * h) x2 int((x_c bw / 2) * w) y2 int((y_c bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, names[int(cid)], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(check_ img_name, img)画出来的图如果发现框不在病灶区域、类别名明显不对、或者normal类的框把整张图都框住了先改标注再训练。不要指望模型能修正错误的标签。标注质量的优先级高于模型结构和超参数这一点在医学影像任务里几乎是铁律。5. 避坑小样本医学图像训练最常见的5个翻车现场5.1 现象loss降了但mAP一直是0训练日志里box_loss和cls_loss都正常下降但每轮验证集的mAP50一直卡在0。多数情况下问题不在模型而在标签和配置。最常见的原因是data.yaml的names顺序和labels里的id对不上比如原本id0是bacterial_pneumonia但yaml里第0个写成了covid-19模型学到的是标签错位后的映射关系验证时自然算不出真阳性。另一个常见原因是坐标转换脚本写错分母把y坐标也除以了宽度导致框实际落在图像外。解决方法是先按4.4的可视化脚本画框框位置和类别都对再检查yaml。还有一个不算少见的情况训练时--data指向了旧的yaml或旧数据集路径模型其实在训练另一个数据集这时候不需要调参改路径重跑即可。5.2 现象训练日志一直打 WARNING: No labels found这句警告出现时要分清是正常还是异常。正常对应的是normal类图片它们没有目标框只有空txtyolov5会把它们当作背景图参与损失计算异常情况是某张图本来有目标但转换脚本漏生成了txt或者图片文件名和txt文件名主名不一致导致yolov5找不到标签。解决方法是先按2.1的数量统计和4.4的可视化检查确认空标注是故意为之。如果正常类空标注数量占比太高比如超过总数的一半模型会学到大部分图没有目标推理时倾向于输出低置信度甚至什么都不框。我一般会把背景图比例控制在20%~30%多的正常样本放到验证集里单独测误检率。5.3 现象mAP50很高但换个X光机拍就崩800张图训练出来的模型在同一个数据源上可能mAP50接近0.9但拿到其他设备、不同曝光参数的胸片上就垮掉。这在小样本数据集上非常常见本质是模型记住了图像的亮度分布、对比度、纹理等设备特征而不是病变特征。原因通常有两个划分数据集时没有按病人或设备分组同一病人的多张图同时出现在train和val里相当于验证集作弊或者增强强度不够学不到跨设备泛化能力。解决办法是在划分时优先按病人ID分组文件名如果带病人编号先把同一病人的图放进同一个子集。增强上提高hsv_v亮度扰动的范围模拟不同曝光条件但不要开上下翻转。5.4 现象显卡内存不足或OOM8G显存跑--img 640 --batch 16很容易OOM。原因不一定是显存不够有时候是--cache ram把大量图片缓存到内存导致整体占用过高。解决的优先级依次是先降batch到8或4再把--img降到512或416最后关闭--cache ram或改成--cache disk。Windows上如果DataLoader一直卡住加上--workers 0可以绕开多进程问题。需要注意img降到416后X光片里的微小病灶可能缩到几个像素模型很难抓住。所以我一般先用小尺寸跑通流程确认无bug后再用640重新训练一次。5.5 现象训练曲线剧烈震荡最后几轮还不如中间某轮小数据集上LOSS曲线像锯齿一样说明学习率对当前数据量来说偏大。yolov5默认学习率0.01带warmup对几百张图来说可能太激进。解决方法是把hyp文件里的lr0从0.01改到0.005lrf保持0.1或者直接加--cos-lr让学习率按余弦曲线下降。patience不要设太大小数据集设30~50比较合适既能避免浪费算力也能防止模型在过拟合后继续震荡。最后说一句如果训练已经完成不要因为best.pt在训练集上不是最高分数就怀疑它它是验证集上泛化性能最好的一个权重正是部署要用的。6. 模型出来之后怎么验证单图推理、混淆矩阵和ONNX导出训练好的best.pt先别急着部署还要做三个验证步骤。第一步是用没参与过训练的新图推理看看实际输出效果# 用独立的新图目录跑推理不要再用训练集 python detect.py \ --weights runs/train/exp/weights/best.pt \ --source ../test_xray/ \ --conf 0.3 \ --iou 0.45 \ --save-txt \ --save-conf--conf 0.3是置信度阈值医学场景下宁可漏检也不要误检太低会框出一堆无关区域--save-txt会输出每个框的类别、坐标和置信度--save-conf把置信度写进txt。结果在runs/detect/exp/里。如果输出的框把肺野边缘、肋骨、文字水印都框了出来说明模型学偏了回头检查训练集标注是否包含了这些干扰项。第二步是在验证集上计算每类的精确率和召回率yolov5的val.py会生成混淆矩阵图python val.py \ --data lung_dataset.yaml \ --weights runs/train/exp/weights/best.pt \ --conf 0.3 \ --iou 0.45 \ --save-conf \ --save-jsonval.py输出在runs/val/exp/下其中confusion_matrix.png能看到五个类别哪些经常互相混淆。肺病数据集里最常见的是细菌性肺炎和病毒性肺炎混淆因为阴影形态相似。如果混淆严重首要手段是补这两个类别的样本而不是调模型结构。第三步是想部署到边缘设备时导出ONNX。热词里常提到的RK3568量化部署前置步骤就是先导出onnxpython export.py \ --weights runs/train/exp/weights/best.pt \ --include onnx \ --img 640 \ --batch 1导出后可以用onnxruntime在CPU上跑验证导出的模型和PyTorch输出一致。如果目标是瑞芯微RK3568这类板子还需要把onnx转成rknn再做INT8量化。这里必须提醒一句量化对医学小目标检测的精度损伤比自然图像更大量化前后要在同一个验证集上对比mAP50。如果掉点超过5个百分点建议保留FP32并考虑用边缘盒子里的NPU做混合量化不要贪图省事。我自己在这个项目上吃过最大的亏就是把normal类框成整肺。当时训练完mAP50挺好看拿到真实场景一测模型把胸片上的L字母水印也当成肺野识别出来。后来把normal框全部删掉保留空txt作为负样本再单独加一个图像分类头判断正常与异常效果才稳定下来。从那次之后我做任何检测训练都会先花半小时验证标注语义因为这决定了模型的世界观。标注如果错了模型越努力越离谱——这个教训比任何超参数调优都值钱。希望帮到你。本文还有配套的精品资源点击获取