007-YOLO11训练前统计大中小目标-判断数据集难度

007-YOLO11训练前统计大中小目标-判断数据集难度
007-YOLO11 训练前统计大中小目标判断数据集到底难在哪里本文面向 YOLO11 自定义数据集训练和后续模型改进实验仍沿用 Ultralytics8.3.253作为系列版本。正式改网络之前先统计数据集中的小目标、中目标、大目标、类别分布和异常标签可以帮助我们判断问题到底来自数据还是来自模型结构。摘要很多 YOLO 改进文章会直接从注意力机制、Neck 融合或检测头开始但如果不先分析数据集后面的改进很容易变成盲试。比如数据集中 70% 都是小目标就不能只看普通 P3、P4、P5 检测层如果某个类别只有几十个框模型检测不好也不一定是结构问题。本文基于 YOLO 标注格式给出一份可直接运行的数据集统计脚本。脚本会读取data.yaml统计 train、val、test 中的目标数量、类别分布、大中小目标比例、平均框宽高、长宽比和异常标签情况。统计结果可以作为 YOLO11 后续改进路线的依据例如是否需要小目标检测层、是否需要调整输入尺寸、是否优先修标注或补数据。关键词YOLO11、目标检测、数据集统计、小目标检测、YOLO 标签、Ultralytics、data.yaml一、为什么训练前要先统计目标尺度模型训练效果不好时很多人第一反应是换模块。但在真实项目中问题经常藏在数据里数据现象可能导致的问题小目标占比很高mAP50-95 偏低漏检明显某些类别样本极少类别间指标差距大标注框过松或过紧定位质量不稳定长条形目标很多普通卷积特征可能不够适配背景和目标相似Precision 降低误检增加空标签或坏标签较多训练日志异常指标波动大所以在做 YOLO11 改进之前先统计数据集很有必要。它能帮助我们决定后面的技术路线小目标多优先考虑 P2 检测层、SPD-Conv、小目标增强或更大输入尺寸类别不均衡先补数据或做采样策略不要马上堆复杂模块长条形目标多后面可以关注大核卷积、条带池化、方向感受野模块标注异常多先修标签再谈模型结构。这一步不直接提高指标但能减少很多无效实验。二、大中小目标怎么定义目标检测中常见的大中小目标划分可以参考 COCO 的面积思路。为了方便和 YOLO 训练输入尺寸对应本文脚本会把标注框换算到imgsz640的缩放尺度下再按面积分类类型面积范围小目标面积小于32 x 32中目标面积在32 x 32到96 x 96之间大目标面积大于等于96 x 96这里要注意两点这只是训练前的数据分布分析不等于 COCO 官方完整评测如果你训练时使用imgsz960或其他尺寸统计脚本里的--imgsz也要同步修改。YOLO 标签格式为class_id x_center y_center width height其中x_center、y_center、width、height都是归一化坐标范围通常在 0 到 1 之间。脚本会结合原图宽高把归一化宽高换算成像素尺度再根据训练输入尺寸估算目标大小。三、数据集目录要求脚本默认读取 YOLO 常见目录结构datasets/ my_dataset/ images/ train/ val/ test/ labels/ train/ val/ test/ data.yaml图片和标签需要同名对应images/train/000001.jpg labels/train/000001.txtdata.yaml示例path:G:/datasets/my_datasettrain:images/trainval:images/valtest:images/testnames:0:class_a1:class_b如果你的train或val写的是.txt图片列表脚本也会尽量读取列表中的图片路径。四、安装需要的依赖统计脚本只依赖几个常用库pipinstallpyyaml pillow pandas如果你已经安装了 Ultralytics大多数环境里这些库可能已经存在。缺哪个再补哪个即可。五、完整统计脚本新建脚本analyze_yolo_boxes.py内容如下importargparseimportcsvfromcollectionsimportCounter,defaultdictfrompathlibimportPathimportyamlfromPILimportImage IMAGE_SUFFIXES{.jpg,.jpeg,.png,.bmp,.webp}defresolve_path(value,yaml_dir,dataset_root):pathPath(value)ifpath.is_absolute():returnpath root_pathdataset_root/pathifroot_path.exists():returnroot_pathreturnyaml_dir/pathdefcollect_images(split_value,yaml_dir,dataset_root):ifsplit_valueisNone:return[]ifisinstance(split_value,(list,tuple)):images[]foriteminsplit_value:images.extend(collect_images(item,yaml_dir,dataset_root))returnimages split_pathresolve_path(str(split_value),yaml_dir,dataset_root)ifsplit_path.is_file()andsplit_path.suffix.lower().txt:images[]forlineinsplit_path.read_text(encodingutf-8).splitlines():lineline.strip()ifnotline:continueitem_pathPath(line)ifnotitem_path.is_absolute():item_pathsplit_path.parent/item_path images.append(item_path)returnimagesifsplit_path.is_dir():returnsorted(pforpinsplit_path.rglob(*)ifp.suffix.lower()inIMAGE_SUFFIXES)return[]deflabel_path_for_image(image_path):partslist(image_path.parts)foriinrange(len(parts)-1,-1,-1):ifparts[i].lower()images:parts[i]labelsreturnPath(*parts).with_suffix(.txt)returnimage_path.with_suffix(.txt)defnormalize_names(names):ifisinstance(names,dict):return{int(k):str(v)fork,vinnames.items()}ifisinstance(names,list):return{i:str(v)fori,vinenumerate(names)}return{}defsize_bucket(area):ifarea32*32:returnsmallifarea96*96:returnmediumreturnlargedefanalyze(data_yaml,imgsz):yaml_pathPath(data_yaml)yaml_diryaml_path.parent datayaml.safe_load(yaml_path.read_text(encodingutf-8))dataset_rootdata.get(path,yaml_dir)dataset_rootresolve_path(dataset_root,yaml_dir,yaml_dir)namesnormalize_names(data.get(names,{}))total_images0missing_label_images0empty_label_images0bad_label_lines0object_count0class_counterCounter()size_counterCounter()split_counterCounter()class_size_counterdefaultdict(Counter)class_sumsdefaultdict(lambda:{w:0.0,h:0.0,area:0.0,aspect:0.0})forsplitin[train,val,test]:imagescollect_images(data.get(split),yaml_dir,dataset_root)split_counter[f{split}_images]len(images)total_imageslen(images)forimage_pathinimages:label_pathlabel_path_for_image(image_path)ifnotlabel_path.exists():missing_label_images1continuelines[line.strip()forlineinlabel_path.read_text(encodingutf-8).splitlines()ifline.strip()]ifnotlines:empty_label_images1continuetry:width,heightImage.open(image_path).sizeexceptException:bad_label_lineslen(lines)continuescalemin(imgsz/width,imgsz/height)forlineinlines:partsline.split()iflen(parts)5:bad_label_lines1continuetry:cls_idint(float(parts[0]))box_wfloat(parts[3])*width*scale box_hfloat(parts[4])*height*scaleexceptValueError:bad_label_lines1continueifbox_w0orbox_h0:bad_label_lines1continueareabox_w*box_h aspectmax(box_w/box_h,box_h/box_w)bucketsize_bucket(area)object_count1split_counter[f{split}_objects]1class_counter[cls_id]1size_counter[bucket]1class_size_counter[cls_id][bucket]1class_sums[cls_id][w]box_w class_sums[cls_id][h]box_h class_sums[cls_id][area]area class_sums[cls_id][aspect]aspectprint(images:,total_images)print(objects:,object_count)print(missing label images:,missing_label_images)print(empty label images:,empty_label_images)print(bad label lines:,bad_label_lines)print(split summary:,dict(split_counter))print(size summary:,dict(size_counter))output_csvyaml_path.with_name(box_size_summary.csv)withoutput_csv.open(w,newline,encodingutf-8-sig)asf:writercsv.writer(f)writer.writerow([class_id,class_name,count,small,medium,large,avg_w_at_imgsz,avg_h_at_imgsz,avg_area_at_imgsz,avg_aspect,])forcls_idinsorted(class_counter):countclass_counter[cls_id]sumsclass_sums[cls_id]writer.writerow([cls_id,names.get(cls_id,fclass_{cls_id}),count,class_size_counter[cls_id][small],class_size_counter[cls_id][medium],class_size_counter[cls_id][large],round(sums[w]/count,2),round(sums[h]/count,2),round(sums[area]/count,2),round(sums[aspect]/count,2),])print(csv saved:,output_csv)defmain():parserargparse.ArgumentParser()parser.add_argument(--data,requiredTrue,helppath to YOLO data.yaml)parser.add_argument(--imgsz,typeint,default640,helptraining image size)argsparser.parse_args()analyze(args.data,args.imgsz)if__name____main__:main()这段脚本的核心逻辑是从data.yaml中读取path、train、val、test和names找到每张图片对应的 YOLO 标签文件把归一化框宽高换算到训练输入尺寸附近按小目标、中目标、大目标统计数量按类别输出 CSV 汇总表。六、运行脚本在数据集根目录或任意工作目录下运行python analyze_yolo_boxes.py--dataG:/datasets/my_dataset/data.yaml--imgsz640运行后会在data.yaml同级目录生成box_size_summary.csv终端也会输出类似下面的信息结构images: 数据集图片总数 objects: 标注框总数 missing label images: 缺少标签文件的图片数 empty label images: 空标签图片数 bad label lines: 格式异常的标签行数 split summary: train、val、test 的图片和目标数量 size summary: small、medium、large 的目标数量 csv saved: 汇总表保存位置这里的数字需要以你自己的数据集为准。文章或实验记录中不要直接使用别人的统计结果。七、CSV 汇总表怎么看生成的box_size_summary.csv中每一行对应一个类别字段含义class_id类别编号class_name类别名称count该类别标注框总数small该类别小目标数量medium该类别中目标数量large该类别大目标数量avg_w_at_imgsz缩放到训练输入尺寸后的平均框宽avg_h_at_imgsz缩放到训练输入尺寸后的平均框高avg_area_at_imgsz缩放后的平均框面积avg_aspect平均长宽比越大说明越细长如果某个类别small占比很高后续模型改进时就要重点观察这个类别的召回情况。如果某个类别count很低不建议直接归因于网络结构。先补数据、修标注或合并过细类别往往比换模块更有效。八、根据统计结果规划改进方向统计完成后可以按下面思路判断统计结果后续优先方向小目标占比高增加 P2 检测层、提高输入尺寸、尝试小目标增强中大目标为主先保持 P3、P4、P5重点看特征融合和定位质量类别长尾明显补充少样本类别保持验证集类别分布稳定长宽比很大关注方向感受野、条带池化、大核卷积等结构空标签很多确认是否为真实负样本避免误删坏标签较多先修复标签格式再训练模型这一步的价值在于避免盲目改网络。例如数据里小目标并不多就没必要第一时间上 P2 检测层如果类别极度不均衡复杂注意力模块也不一定能解决少样本问题。九、统计结果如何写进实验台账建议在 baseline 记录表旁边增加一张数据集分析表项目记录方式图片总数按脚本输出填写标注框总数按脚本输出填写小目标比例small / objects中目标比例medium / objects大目标比例large / objects最少样本类别根据 CSV 中count最小的类别填写平均长宽比最高类别根据 CSV 中avg_aspect最大的类别填写异常标签数量missing label images、bad label lines后续文章中如果要解释为什么尝试某个小目标模块可以引用这张分析表。例如从数据统计可以看到本数据集中小目标占比较高因此后续实验会优先关注 P2 检测层、SPD-Conv 和小目标特征增强方法。这种写法比直接说“该模块适合小目标”更有说服力因为它和自己的数据集联系起来了。十、常见问题1. 空标签图片要不要删除不一定。空标签图片如果是真实背景图可以作为负样本保留如果是漏标就必须补标。关键是区分“没有目标”和“忘记标注”。2. 小目标占比高就一定要加 P2 吗不一定。P2 会增加计算量和显存占用。建议先建立 YOLO11n baseline再根据小目标类别的 Recall 和 mAP 表现决定是否增加 P2。3. 统计脚本里的 small、medium、large 是最终评测指标吗不是。这里是训练前的数据分布统计用来帮助判断数据难点。正式 mAP 指标仍然以 Ultralytics 的验证结果为准。4. 标签文件里出现超过 1 的坐标怎么办YOLO 归一化坐标通常应在 0 到 1 附近。如果出现明显超过范围的值优先检查标注格式是否从 VOC、COCO 或 LabelMe 转换错误。5. 类别编号和 names 对不上怎么办需要先修正data.yaml或标签类别编号。类别编号错了模型会学习到错误映射后面换任何模块都很难得到可靠结果。十一、总结本文完成了 YOLO11 训练前的数据集目标尺度统计流程。通过读取 YOLO 标签和data.yaml我们可以得到图片数量、目标数量、类别分布、小中大目标比例、平均框宽高、长宽比和异常标签情况。这一步不是为了替代训练而是为了让训练和改进更有方向。数据集中到底是小目标多、类别不均衡、长条目标多还是标签质量有问题应该先用统计结果说话。后续继续做 YOLO11 改进时就可以把本文的统计结果和 baseline 指标结合起来先知道数据难在哪里再决定该改 Backbone、Neck、检测头、损失函数还是数据增强。这样写出来的实验路线更清楚也更不容易变成照着模块列表盲目堆结构。