ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

YOLOv7训练X光片肺病五分类:数据集标注、训练调参与避坑全流程

YOLOv7训练X光片肺病五分类:数据集标注、训练调参与避坑全流程 简介面向肺炎影像识别与目标检测任务的X光片数据集内含800张标注过的原始胸片基于YOLOv7格式整理覆盖细菌性肺炎、新冠病毒、正常肺、结核及病毒性肺炎五类场景适合医学图像分类、目标检测模型训练及毕业设计或科研实验使用。压缩包共1601个文件由800个jpg图像、800个txt标注文件及1个yaml配置文件组成标注文件对应YOLOv7所需的边界框与类别信息yaml用于定义类别名称与训练路径整体仅25.51MB便于快速下载与部署。已有410人学习使用数据按原始图片一一配对无需额外转换即可接入现有YOLO训练流程。附带的txt标注可直接用于模型微调结合五类标签能帮助初学者熟悉医学影像中的目标检测标注规范也可为肺炎辅助诊断研究提供基础数据支撑。整体结构简明方便按需筛选不同类别进行实验。1. 拿到这个X光片肺病数据集先想清楚识别任务和检测任务不是一回事很多人看到“X光片肺病数据集800张原始图片yolov7标记可识别细菌性肺炎、新冠病毒、正常肺、结核、病毒性肺炎”这个标题第一反应是“五分类图像识别拿个CNN跑一下”。真打开压缩包才发现不是这么回事这包给的是检测标注不是分类目录。它更适合用来做目标检测路线而不是直接套分类模型。适合谁有GPU、想用YOLOv7把五类任务落地、并且不排斥自己复核标签质量的人。八百张数量不大但配上合理的划分、预训练权重和验证流程足够跑通一版能用的检测原型。下面的内容就按这条路线展开把数据清洗、训练、推理和踩坑一次讲清楚。2. 拆包看结构800张X光片的五类标签与YOLO格式的对应关系拿到压缩包先别急着拷进训练脚本里。第一步是弄清楚里面的标注是什么格式、类别编号是什么顺序、有没有空标签和缺标签。YOLOv7的标注不是一个大文件而是每张图片同名的txt一张图对应一个txt文件内容按行组织每一行代表一个框。2.1 五类标签的YOLO格式class编号从0开始不是Excel行号打开任意一个txt标签常见内容长这样0 0.5000 0.5000 1.0000 1.0000 3 0.5640 0.4310 0.2230 0.2900每行五个数顺序是class_id x_center y_center width height。x_center、y_center是框中心点坐标除以图片宽高后的归一化浮点数取值范围0到1width和height也是归一化后的框宽高。class_id从0开始不是从1开始。也就是说如果包里的classes.txt写的是“细菌性肺炎、新冠病毒、正常肺、结核、病毒性肺炎”那编号顺序通常就是0、1、2、3、4除非作者在txt里改过顺序。这个细节看起来基础却是后面大多数翻车现场的总根源。X光片数据集的作者经常在Excel里整理类别导出时顺手从1编号结果txt里第一列全是1到5而YOLOv7只认0到4。拿到数据后第一件事就是把所有txt扫一遍确认最大class_id是4而不是5是0而不是1。另外要注意不同作者的标注策略可能完全不一样。一类作者会把“正常肺”这类整图标签做成一个大框框住整个肺野坐标近似0.5 0.5 0.98 0.98另一类作者认为正常肺没有病灶就不写框留下空白txt。这两种策略对训练的影响在第5章会细讲这里先留个心眼。2.2 解压后的目录怎么摆同名txt绑定图片的约定YOLOv7训练时找标签的逻辑很简单图片叫img_001.jpg它就去同一个labels根目录下找img_001.txt不关心图片和txt是否在同一层只看文件名前缀。所以目录摆放要干净强烈建议按这种结构整理xray_dataset/ ├── images/ │ ├── train/ │ │ ├── img_0001.jpg │ │ └── ... │ └── val/ │ └── ... └── labels/ ├── train/ │ ├── img_0001.txt │ └── ... └── val/ └── ...如果解压出来的原始文件名带中文、空格或特殊符号先批量改掉避免在训练脚本里因为路径编码中断。批量改名的时候要图片和txt同步改只改图片不改txt会让标签丢失。常见做法是用python脚本统一处理from pathlib import Path img_dir Path(raw_images) label_dir Path(raw_labels) for i, img_path in enumerate(sorted(img_dir.glob(*.jpg))): new_name fimg_{i:04d}{img_path.suffix.lower()} img_path.rename(img_dir / new_name) label_path label_dir / (img_path.stem .txt) if label_path.exists(): label_path.rename(label_dir / (Path(new_name).stem .txt))这段代码的逻辑是按原图顺序从0001开始编号图片改了新文件名对应txt也跟着改。注意改成img_{i:04d}会让文件名按字典序排列时和真实时间顺序没关系但训练不关心文件名只关心不重名。如果你手里的包已经带classes.txt不要动它先拿着它和txt第一列的class_id核对一遍。2.3 先统计再训练一张表看清800张样本的类别与空标签训练前花三分钟跑个统计脚本能省下后面几小时的排查时间。我一般会统计四件事图片总数、缺标签图片数、空标签图片数、每个类别的框数分布。from pathlib import Path import numpy as np img_dir Path(xray_dataset/images) label_dir Path(xray_dataset/labels) all_images sorted(img_dir.glob(*)) stats {} missing_label [] empty_label [] for img in all_images: txt label_dir / (img.stem .txt) if not txt.exists(): missing_label.append(img.name) continue with open(txt) as f: lines [l.split() for l in f if l.strip()] if not lines: empty_label.append(img.name) continue for line in lines: cls int(line[0]) w float(line[3]) h float(line[4]) stats.setdefault(cls, []).append((w, h)) print(f图片总数: {len(all_images)}) print(f缺标签: {len(missing_label)} {missing_label[:5]}) print(f空标签: {len(empty_label)} {empty_label[:5]}) cc 0 for cls in sorted(stats): wh np.array(stats[cls]) print(fclass {cls}: 框数 {len(wh)}, 平均框宽高比 {wh[:, 0].mean():.3f} / {wh[:, 1].mean():.3f})这段代码里关键判断在if not lines这个分支txt文件存在但里面没有任何行就会归入empty_label。正常肺如果大量出现在empty_label里说明作者的标注策略是“没有病灶就不标框”后面训练前必须处理。class编号直接读txt第一列不依赖文件名、不依赖Excel里的类名这是为了避免类别错位。跑完统计把每个类的框数记下来你大概就知道这个包是重检测还是重分类如果五个类里三个类的平均框宽高比接近0.9以上说明多数是整图语义标签如果有一类框宽高比只有0.2左右说明这一类标的是局部病灶比如结核空洞。这两类标注混合在一起训练时模型学的东西会分裂第4章会给出处理思路。3. 用YOLOv7把五类跑通数据yaml、分层划分与最小训练命令数据包看明白之后真正动手跑。这里用原版YOLOv7仓库不改代码、不魔改网络先把一版能出结果的流程跑通。标题说“使用yolov7标记”那就直接用YOLOv7的标注格式和训练链路比强行转成YOLOv8省掉一次格式转换。3.1 环境选择原版YOLOv7仓库与PyTorch版本怎么配YOLOv7对PyTorch版本要求不苛刻2.x都能跑但我建议用1.13到2.0之间的版本最稳。先建独立的conda环境避免把系统Python环境弄乱conda create -n yolo7 python3.9 -y conda activate yolo7 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 git clone https://github.com/WongKinYiu/yolov7 cd yolov7 pip install -r requirements.txt这套命令的逻辑是Python3.9兼容性最好torch选用cu118索引是为了配合CUDA 11.8及以上驱动如果你显卡驱动是CUDA 12把索引里的cu118换成cu121也不会出问题。克隆的是官方原版yolov7仓库不是魔改分支这样后续遇到问题搜得到答案。有个实际建议Windows上也能跑但WSL2或Linux服务器会省掉很多折腾尤其是文件路径带中文、路径分隔符、以及Cython编译扩展这一类问题。如果你用的是Windows遇到报错先看是不是路径和编码问题别急着重装环境。3.2 写一个xray五类的data yaml字段与路径规则YOLOv7训练前要准备一个数据描述文件后缀一般是yaml里面告诉训练脚本“训练集在哪、验证集在哪、一共几类、类名分别是什么”。新建xray_5cls.yaml# xray_5cls.yaml train: /home/user/xray_dataset/train.txt val: /home/user/xray_dataset/val.txt nc: 5 names: [normal, bacterial_pneumonia, covid, tuberculosis, viral_pneumonia]train和val两行可以写目录也可以写txt文件列表。写成文件列表时txt文件里每一行是一张图片的绝对路径。推荐用绝对路径因为训练时当前工作目录一变相对路径就容易找错图片。names顺序必须和txt标注里的class_id一一对应第0个名字对应txt第一列里的0第1个对应1以此类推。类名里不要写中文不要带空格。X光片数据集难免有人直接把“细菌性肺炎”写进类名YOLOv7在生成日志和检测输出时遇到中文编码会出各种奇奇怪怪的问题统一用下划线拼英文。部署时再在业务层把英文名映射回中文显示。3.3 划分train/val按标签分层防止某个类一整批被漏在val里800张图如果直接shuffle切成train和val按5%概率某个小类可能全部进val或者全部进train模型训练出来就废了。按标签分层划分是标准做法我一般用sklearn的train_test_splitfrom pathlib import Path from sklearn.model_selection import train_test_split img_dir Path(/home/user/xray_dataset/images) label_dir Path(/home/user/xray_dataset/labels) imgs sorted(img_dir.glob(*)) target [] for img in imgs: first_cls 0 txt label_dir / (img.stem .txt) if txt.exists(): with open(txt) as f: line f.readline().split() if line: first_cls int(line[0]) else: first_cls 5 # 空标签单独归为一层 target.append(first_cls) train_imgs, val_imgs train_test_split( imgs, test_size0.2, stratifytarget, random_state42 ) with open(/home/user/xray_dataset/train.txt, w) as f: f.write(\n.join(str(p) for p in train_imgs)) with open(/home/user/xray_dataset/val.txt, w) as f: f.write(\n.join(str(p) for p in val_imgs))分层划分的关键是每一个样本都要有一个用于分层的类别标签所以这里取每张图第一个框的class_id作为该图的类别。空标签的图没有类别单独归为第5层避免stratify报错。如果你的一张图同时含有结核和细菌性肺炎两种框按第一个框算还是按所有框算会有偏差但这种多标签图在800张X光片里通常很少影响有限。random_state固定成42方便以后复现同一份划分结果。3.4 最小训练命令参数逐项怎么调数据划分完成后开始训练。训练命令里我不会把一堆增强参数堆上去先用最小可跑版本cd yolov7 python train.py \ --data /home/user/xray_dataset/xray_5cls.yaml \ --img-size 640 \ --batch-size 8 \ --epochs 120 \ --device 0 \ --workers 4 \ --weights weights/yolov7.pt \ --name xray_5cls_v1几个关键参数的作用和取舍参数值说明--img-size640X光片原始分辨率差异大统一到640是精度和显存的平衡点显存8G以下降到416--batch-size8约需11G左右显存8G显卡建议4--epochs120800张小样本不需要300轮后段基本在过拟合--workers4Linux下4到8都行Windows下建议2--weightsweights/yolov7.pt用COCO预训练权重做迁移学习尽量别从头训weights/yolov7.pt需要从YOLOv7官方release页面下载并放到仓库的weights目录下。这步不可省X光片医学图像虽然和COCO自然图像差很多但预训练权重提供的底层特征提取能力还是能加速收敛。全程用默认hyp增强参数就可以不要一开始就上mixup和mosaic拉满小样本下增强太猛会让模型学不到真实肺纹理。训练过程中盯两个东西一是终端日志里的val mAP二是runs/train/xray_5cls_v1/weights/目录下的best.pt和last.pt。best.pt是验证集mAP最高时保存的权重部署用这个last.pt是最后一轮权重一般不用。如果训练到第80轮val mAP还在爬升可以适当加到150轮如果第40轮val mAP就开始震荡不涨说明模型把训练集背下来了提前CtrlC也不亏。4. 推理验证best.pt在X光片上的置信度调参与五分类输出训练完不是拿best.pt一放就完事。先跑一次验证集推理看看预测框是不是落在肺野上、类别是不是对得上、正常肺图是不是误检了一堆细菌性肺炎框。4.1 用detect.py跑val最小命令与输出结构YOLOv7自带detect.py直接调用cd yolov7 python detect.py \ --weights runs/train/xray_5cls_v1/weights/best.pt \ --source /home/user/xray_dataset/images/val \ --img-size 640 \ --conf-thres 0.25 \ --iou-thres 0.45 \ --device 0 \ --save-txt --save-confdetect.py会自动把结果写到runs/detect/exp目录下如果已经存在同名目录会生成exp2、exp3。--save-txt会把每个预测框写成txt文件内容格式是class_id conf x_center y_center width height--save-conf是让conf值写进txt里不写的话txt里只有框坐标后面做统计脚本会少一个维度。跑完先打开runs/detect/exp下的图片目录直接看可视化结果。重点看结核类800张X光片里如果结核病灶是被当作局部小框标注的那么小框在640分辨率下可能只有十几个像素这类框最容易漏。如果可视化图里结核框基本都能画在病灶上再往下做量化。4.2 conf与iou怎么调X光片小病灶的灵敏度取舍conf-thres是置信度阈值iou-thres是非极大值抑制NMS的IoU阈值。这两个参数在X光片上的调法和自然场景不太一样。X光片病灶和背景对比度低尤其新冠早期磨玻璃影置信度天然低阈值拉到0.25经常漏检一大批。我常用的做法是做两次推理先用0.25看常规效果再用0.1跑一遍看敏感度。比较两次结果如果0.1版本里多检出的框大部分是肺野内的真实病灶那就把部署阈值定在0.15左右如果多出来的全是大片背景噪点说明模型没学好阈值提高反而更稳。iou-thres影响的是重叠框的合并。新冠和病毒性肺炎都常表现为肺野外带多发磨玻璃影一个区域里可能同时有两个类别的预测框置信度都很高。这时候把iou-thres从0.45提到0.5可以让同类密集框少被合并但跨类别重叠框NMS不会管最终决策要靠类别置信度排序。4.3 无框即正常把检测结果归并成五分类输出X光片五分类业务里用户的最终诉求是“这张片子属于哪一类”而不是“框在哪”。所以要把检测输出转成图级判断。转换逻辑里最关键的一条是正常肺图在推理时大概率没有任何框输出如果强行要求每个预测框都对应一个类别正常肺class_id就不会出现在结果里。常见做法是把“无框”视为normal类的信号from pathlib import Path pred_dir Path(runs/detect/exp/labels) def decide_class(result_txt, class_names): lines [l.strip().split() for l in open(result_txt) if l.strip()] if not lines: return normal, 1.0 lines.sort(keylambda x: float(x[1]), reverseTrue) cls_id int(lines[0][0]) conf float(lines[0][1]) return class_names[cls_id], conf class_names [normal, bacterial_pneumonia, covid, tuberculosis, viral_pneumonia] for txt in sorted(pred_dir.glob(*.txt))[:10]: cls, conf decide_class(txt, class_names) print(txt.stem, cls, conf)这段代码对每张图取置信度最高的框作为整图类别没有框就归为normal。这么做的前提是知道normal类在训练集里没有局部病灶框如果有局部框反而要小心有可能一个正常肺因为某个伪影被错误判成其他类。这里要提醒一个格式局限如果包里的正常肺、细菌性肺炎、病毒性肺炎是整图大框那大框只表达“这张图属于哪一类”并没有表达病灶具体在哪。模型学到的是整张肺野的纹理分布不是局部病灶特征。业务上想要的是五分类识别率这种用法没问题但如果以后想回答“病灶在左肺上叶还是右肺下叶”这套标注就撑不住了需要重新补局部框。4.4 导出preds.csv给复盘留证据推理结果散在txt里不好看导成CSV方便在Excel里做透视表import csv from pathlib import Path pred_dir Path(runs/detect/exp/labels) class_names [normal, bacterial_pneumonia, covid, tuberculosis, viral_pneumonia] with open(preds.csv, w, newline) as f: writer csv.writer(f) writer.writerow([image, class_id, class_name, conf, bbox]) for txt in sorted(pred_dir.glob(*.txt)): for line in open(txt): parts line.split() if len(parts) 5: continue cls_id int(parts[0]) conf float(parts[1]) writer.writerow([ txt.stem, cls_id, class_names[cls_id], conf, .join(parts[2:]) ])导出后的CSV里class_name是英文编码中文名留到业务前端映射。原因还是那个老掉牙的编码问题CSV里直接写中文Excel打开经常乱码而英文类名不会。有了这张表你可以按class_id分组看平均置信度也可以单独拉出结核类的所有bbox检查是不是挤在图像角落这些都能快速发现训练阶段没暴露的问题。5. 避坑800张X光片喂给YOLOv7时会卡住的5个地方训练和推理过程中有几个坑几乎是这个数据集的标配翻车点。按“现象、原因、解决”三件套逐个过一遍很多问题不用等报错就能提前堵住。5.1 标签编号整体错位全图框把正常肺标成细菌性肺炎现象训练时loss下降正常val mAP也不低但可视化图里正常肺片子的预测框类别全是bacterial_pneumonia结核全预测成covid整体偏移一格。原因数据作者整理类别时从Excel第1行开始编号导出YOLO格式时忘了减1。于是正常肺写成1而不是0细菌性肺炎写成2而不是1全部跟yaml里的names错位。解决先跑一遍统计看txt里class_id的最大值是不是超过了nc-1。如果最大编号是5而nc是5训练脚本会直接报错如果最大是4但类别整体偏移则不会报错只能人工核对。快速核对脚本from pathlib import Path label_dir Path(xray_dataset/labels) cls_count {} for txt in label_dir.glob(*.txt): with open(txt) as f: for line in f: parts line.split() if not parts: continue cls_id int(parts[0]) cls_count[cls_id] cls_count.get(cls_id, 0) 1 print(sorted(cls_count.items()))跑完后把cls_count和包的classes.txt对照。如果你确定整体偏移最常见的后缀做法是把txt第一列全部减1。这里有个隐含风险如果某个类别本来就没有框减1后这个类直接消失需要先确认所有类别都有框再动手。5.2 800张小样本过拟合val mAP虚高测试集漏检现象训练到第40轮后验证集mAP0.5还在攀升最后best.pt在val上mAP到0.9但拿到一批新X光片测试五类识别正确率明显不如val表现。原因800张图对医学影像检测来说样本量偏小。模型可能记住了训练集的放大尺度、噪声纹理和边框位置而没有真正学到肺野病变的共性特征。val集参与了早停和权重选择mAP虚高是必然的。解决一个做法是给训练用上K折交叉验证把800张分成5折每次用4折训练1折验证最后看5折平均mAP去掉了“某一折运气好”的因素。另一个更快的做法是留出20张左右完全不参与训练的外部图单独当测试集训练全过程不碰它最后只看这20张的正确率。闭坑的核心不是调参是别把best.pt的val mAP当最终结论。之前用yolov8训练自己的数据集时也踩过同样的坑小样本场景下val指标会骗人必须回到图级正确率上做评估。5.3 正常肺标注为空文件训练时这个类彻底消失现象训练日志里loss正常下降但在最后的每类mAP报告里找不到normal类的AP或者normal类AP是0。推理时正常肺片子要么任意输出一个框要么完全无框。原因作者对正常肺采取了“无病灶不标注”的策略所有normal图片的txt是空文件。YOLOv7会把无框图当作背景负样本而不是normal类正样本。模型从头到尾没看到过一个normal框自然学不会“这是normal”。解决对normal类强行生成整图框写入txtfrom pathlib import Path label_dir Path(xray_dataset/labels) class_id 0 # normal对应的编号 box_line 0 0.5 0.5 0.98 0.98\n for txt in label_dir.glob(*.txt): if txt.stat().st_size 0: txt.write_text(box_line)整图框的x和y中心取0.5宽高取0.98而不是1.0留出2%边距避免框紧贴图像边界导致letterbox缩放后坐标轻微越界。生成后重新跑一下第2.3节的统计脚本确认normal类有框、框数等于normal图片数。如果normal图原本有框则跳过这步。5.4 灰度与分辨率不一致训练时没爆推理时漏检现象训练和验证都用同一个包的图片效果不错把另一台设备导出的X光片丢进同一个detect.py大量漏检甚至正常肺被识别成细菌性肺炎。原因X光片的原始来源可能是DICOM也可能是手机翻拍的屏幕图位深有8位、12位、16位之分有的图保存成单通道灰度有的保存成三通道伪RGB。YOLOv7训练前只做letterbox和归一化不做窗宽窗位处理数据分布稍微一变模型的边界就崩了。解决统一把训练和推理素材转成8位JPG固定长边不超过1024或2048。转换时用letterbox等比缩不要直接resize拉伸否则肺野比例变形。训练和推理走同一个预处理路径不要训练用PIL、推理用OpenCV两者读灰度图装进RGB通道的顺序不一样推理结果会莫名变差。提示如果素材里混了DICOM先转成PNG或者JPG再进YOLO管线。直接把DICOM读进detect.pyOpenCV读不了还会把后面的处理逻辑全部带偏。5.5 显存不足调小batch后训练震荡可能是AMP在背锅现象8G显存下--batch-size 8直接OOM改成2后loss曲线忽高忽低前几轮val mAP就锁死不再上涨。原因YOLOv7默认开AMP混合精度batch特别小的时候每个batch的梯度统计噪声很大AMP的loss scaling会反复震荡导致训练不稳定。这不是模型问题是显存限制和混合精度的组合不良反应。解决把--img-size从640降到416batch-size提到4或6workers降到2先跑20轮看loss是否平滑下降。如果还是震荡训练命令上加--amp手动关掉混合精度用FP32跑速度慢一点但稳定。X光片五分类对速度不敏感稳定优先。另外要注意batch2时BN层的统计量也很不靠谱所以800张小样本环境下宁可减分辨率也别把batch压到2。6. 验收技巧把best.pt转成图级混淆矩阵再谈部署训练完成后别急着谈部署。先把best.pt的检测结果和真实标签做一个图级混淆矩阵这个动作能直接回答“这个数据集到底能不能满足五分类识别需求”。图级统计的思路是每张图只看置信度最高的那个预测框把它当作整图类别然后和这张图的真实类别对比。对normal这类没有病灶框的图如果模型无框输出直接视为预测normal和训练策略保持一致。脚本很短from pathlib import Path GT_DIR Path(xray_dataset/labels/val) PRED_DIR Path(runs/detect/exp/labels) names [normal, bacterial_pneumonia, covid, tuberculosis, viral_pneumonia] matrix {} for gt_txt in sorted(GT_DIR.glob(*.txt)): first open(gt_txt).readline().split() if not first: continue gt_cls int(first[0]) pred_cls None pred_txt PRED_DIR / gt_txt.name if pred_txt.exists(): first open(pred_txt).readline().split() if first: pred_cls int(first[0]) # 无框输出映射为 normal保持和第四章决策逻辑一致 if gt_cls 0 and pred_cls is None: pred_cls 0 key (gt_cls, pred_cls) matrix[key] matrix.get(key, 0) 1 for gt_cls in range(5): row [] total sum(matrix.get((gt_cls, p), 0) for p in range(5)) correct matrix.get((gt_cls, gt_cls), 0) for pred_cls in range(5): row.append(matrix.get((gt_cls, pred_cls), 0)) acc correct / max(total, 1) print(f{names[gt_cls]:20s} 正确率 {acc:.2f} 分布 {row})这段代码的输出会把五个类各自被识别成谁列成一行。如果某两类的分布相互交错比如covid大量被识别成viral_pneumonia那问题大概率出在标注本身而不是训练参数因为这两个类别在X光片上本身就有视觉重叠。这时候要做的是去翻原始图片确认这两类是否有可区分的纹理特征再决定是合并类别还是改进标注。我自己的习惯是这类X光片数据集在正式投入前先用预训练权重跑一版、做一次图级混淆矩阵、再决定要不要在标注上投入更多精力。800张的规模决定了它很难支撑真正的多病灶检测但支撑五分类识别率是完全可行的。希望帮到你。本文还有配套的精品资源点击获取
返回列表