简介马行为识别数据集是一套面向计算机视觉与深度学习场景的标注资源主要用于马匹行为自动识别覆盖站立、吃草、躺下等常见动作对应不同的标注类别整体识别准确率约为89.8%。压缩包共2000个文件均为PASCAL VOC格式的XML标注文件总大小约331.49MB每个XML文件内含目标边界框与类别标签可直接与原始图片配套用于目标检测或行为分类任务。数据集源自7112张原始图片的标注整理文件命名与原始图像一一对应适合作为训练集、验证集或数据增强的辅助素材对智慧养殖、动物行为分析及目标检测研究者而言这套标准化的标注数据能够减少人工标注成本便于快速搭建行为识别模型并验证算法效果。已有101人学习适合需要高质量马匹行为标注数据的研究者直接取用。1. 为什么选马行为识别7112张图和四个行为标签背后的价值做畜牧智能化或者动物行为学实验的人大概率遇到过同一个尴尬公开数据集里牛、羊、猪的检测数据一抓一把马的行为识别却很难凑齐一套能直接训练的资源。这套马行为识别数据集7112张原始图片标注格式是PASCAL VOC XML标签体系只有horse、horse-eating、horse-laying、horse-standing四类分别对应无动作、吃草、躺下、站立。官方给出的正确识别率在89.8%对单类动物行为识别来说属于够用的水平。它的价值不在于标签数量多而在于行为状态被拆得干净同一匹马在吃草和站立时的姿态差异很大框的宽高比、中心点位置都有明显区别比通用目标检测数据集更容易让模型学到行为层面的特征。适合正在做动物姿态分析、牧场无人化巡检或者想用一套干净数据跑通检测训练流程的人。2. PASCAL VOC格式解析XML文件字段结构与批量读取脚本2.1 VOC标注的目录约定和XML最小结构这套数据集的标注文件命名很规律类似168bdff81f037fcc_jpg.rf.ea38855086be3363bb64b450c421ee63.xml这种格式。.rf是Roboflow导出时留下的标记前面是图像文件名后面是唯一ID同一个ID在对应图片名里也会出现用于建立图片和XML的对应关系。标准的PASCAL VOC XML标注文件核心字段是annotation根节点下的几组信息。folder是图片所在目录名filename是图片文件名source标注来源size里记录图片的width、height、depth三个参数。真正决定检测框的是object节点一个object对应一个目标实例内部包含name标签类别名、pose拍摄角度、truncated目标是否被截断、difficult目标是否难识别以及bndbox下的xmin、ymin、xmax、ymax四个坐标。这里有个细节值得注意difficult字段在VOC官方定义里表示这个目标“难以辨认”训练时标准做法是忽略掉。这套数据里的马行为识别场景吃草的马经常被草或栏杆遮挡容易出现truncated或difficult被标记的情况。跑训练之前这个字段最好单独统计一下决定是过滤掉还是保留。2.2 用Python批量解析7112个XML读坐标、统计类别分布拿到数据第一步不是直接喂给训练脚本而是先做一次全量解析搞清楚每个类别到底有多少实例、每张图片尺寸是否一致、坐标有没有越界。下面是常用的批量解析脚本import os import xml.etree.ElementTree as ET from collections import Counter xml_dir annotations class_counter Counter() total_boxes 0 error_files [] for xml_name in os.listdir(xml_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_name) try: tree ET.parse(xml_path) root tree.getroot() except Exception as e: error_files.append((xml_name, str(e))) continue size root.find(size) width int(size.find(width).text) height int(size.find(height).text) for obj in root.iter(object): name obj.find(name).text.strip() difficult obj.find(difficult) diff_flag int(difficult.text) if difficult is not None else 0 bndbox obj.find(bndbox) xmin int(float(bndbox.find(xmin).text)) ymin int(float(bndbox.find(ymin).text)) xmax int(float(bndbox.find(xmax).text)) ymax int(float(bndbox.find(ymax).text)) if xmax xmin or ymax ymin: error_files.append((xml_name, finvalid bbox: {xmin},{ymin},{xmax},{ymax})) continue if xmin 0 or ymin 0 or xmax width or ymax height: error_files.append((xml_name, fbbox out of range: {xmin},{ymin},{xmax},{ymax})) class_counter[name] 1 total_boxes 1 print(类别统计:, dict(class_counter)) print(总实例数:, total_boxes) for item in error_files[:20]: print(异常文件:, item)这段代码做的事情很直白遍历annotations目录下所有XML文件逐个解析object节点提取类别名和坐标。ET.parse是Python标准库的XML解析方式不需要装第三方包读VOC格式足够用。root.iter(object)会递归遍历所有名为object的子节点比findall更稳妥因为有些标注工具生成的XML层级会略有差异。坐标提取时用了int(float(...))双重转换是因为有的标注工具把坐标写成123.0这种浮点字符串直接int()会报错。这里统计类别分布的目的是为后面的训练集划分和类别权重设置提供依据。如果四个类别实例数差距过大比如躺下样本只有几百张训练时需要做类别重加权或者数据增强补偿否则模型会偏向多数类。异常文件列表要重点查看越界的坐标框如果在YOLO格式转换时不处理轻则训练Loss异常重则程序崩溃。2.3 标注完整性校验空标签与缺失文件的处理XML文件解不出来或者某个XML里没有任何object节点训练时就会出问题。实际操作中常见两种情况一是XML文件是空壳只有annotation根节点内部没有目标信息二是图片名和XML名对不上导致数据加载器找不到对应标注。import os def check_empty_annotations(xml_dir): empty_list [] for xml_name in os.listdir(xml_dir): xml_path os.path.join(xml_dir, xml_name) tree ET.parse(xml_path) root tree.getroot() objects root.findall(object) if len(objects) 0: empty_list.append(xml_name) return empty_list def check_image_xml_mapping(image_dir, xml_dir): xml_names [f.split(.xml)[0] for f in os.listdir(xml_dir) if f.endswith(.xml)] image_names [f.split(.jpg)[0] for f in os.listdir(image_dir) if f.endswith(.jpg)] missing [name for name in image_names if name not in xml_names] orphan [name for name in xml_names if name not in image_names] return missing, orphan empty check_empty_annotations(annotations) missing, orphan check_image_xml_mapping(images, annotations) print(空标注文件数:, len(empty)) print(缺少XML的图片数:, len(missing)) print(缺少图片的XML数:, len(orphan))这套校验逻辑建议在拿到数据集后第一时间跑一遍不要跳过。缺图或者缺XML的文件很可能导致训练过程中报FileNotFoundError到时候再排查来源问题比一开始发现麻烦得多。3. 从VOC到YOLO格式数据整理、归一化与训练集划分3.1 为什么要转YOLO格式现在主流检测框架YOLOv5、YOLOv8、YOLOv11的训练接口原生支持的数据标注格式是每张图片对应一个同名txt文件每行记录类别id 中心点x 中心点y 宽度w 高度h坐标都归一化到0到1之间。PASCAL VOC的XML坐标是绝对值格式上差着一层转换。更重要的是归一化坐标让模型不依赖输入图片分辨率。同一张图缩放到640×640还是1280×1280归一化后的标注值不变训练时省去很多对尺寸的额外处理。YOLO系列在DataLoader里做letterbox缩放时归一化标注直接作用于缩放后的坐标不会有分辨率不一致带来的偏差。3.2 坐标转换脚本归一化公式与目录生成VOC坐标转YOLO格式的公式很简单中心点x等于(xmin xmax) / 2 / width中心点y等于(ymin ymax) / 2 / height宽度w等于(xmax - xmin) / width高度h等于(ymax - ymin) / height。import os import xml.etree.ElementTree as ET CLASS_MAP { horse: 0, horse-eating: 1, horse-laying: 2, horse-standing: 3, } def convert_voc_to_yolo(xml_path, output_dir): tree ET.parse(xml_path) root tree.getroot() filename root.find(filename).text size root.find(size) width int(size.find(width).text) height int(size.find(height).text) txt_lines [] for obj in root.iter(object): name obj.find(name).text.strip() difficult obj.find(difficult) diff_flag int(difficult.text) if difficult is not None else 0 if diff_flag 1: continue if name not in CLASS_MAP: print(f未知类别 {name} 出现在 {xml_path}) continue 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) # 防止越界坐标导致归一化值超过1 xmin max(0, min(xmin, width)) xmax max(0, min(xmax, width)) ymin max(0, min(ymin, height)) ymax max(0, min(ymax, height)) cx (xmin xmax) / 2.0 / width cy (ymin ymax) / 2.0 / height bw (xmax - xmin) / width bh (ymax - ymin) / height # 过滤掉宽或高为0的无效框 if bw 0 or bh 0: continue txt_lines.append(f{CLASS_MAP[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_name filename.replace(.jpg, .txt).replace(.png, .txt) with open(os.path.join(output_dir, out_name), w) as f: f.write(\n.join(txt_lines)) xml_dir annotations label_dir labels os.makedirs(label_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if xml_file.endswith(.xml): convert_voc_to_yolo(os.path.join(xml_dir, xml_file), label_dir)转换脚本里有几个参数值得展开说。CLASS_MAP把四个标签映射成数字类别IDhorse对应0horse-eating对应1horse-laying对应2horse-standing对应3。这个顺序会直接写进YOLO训练配置的names列表后面如果改了映射关系训练出来的模型类别含义就全乱了所以第一步就要定死。坐标裁剪用了max(0, min(xmin, width))确保坐标不会因为标注工具导出误差出现负数或超过图片宽高的情况。归一化坐标如果超过1训练时计算Loss会报错裁剪是最省事的兜底方案。difficult1的过滤是在转换成txt的时候就做掉而不是等到训练时才处理。原因很简单txt格式没有地方记录difficult属性过滤动作前置后面训练流程就不用纠结。这段代码跑完labels目录下每个XML会对应生成一个同名txt文件。接着做一次反向检查统计每个txt文件的实例数和XML里的object数量是否一致防止转换过程中丢框。3.3 训练集和验证集划分固定随机种子与类别均衡划分数据集时很多人在这一步吃过亏直接用random.shuffle全部打乱结果验证集里某些行为类别一个样本都没有训练出来的模型在val上指标虚高换到真实场景就露馅。import os import random from collections import Counter random.seed(42) image_files [f for f in os.listdir(images) if f.endswith(.jpg)] random.shuffle(image_files) val_ratio 0.15 val_count int(len(image_files) * val_ratio) val_files image_files[:val_count] train_files image_files[val_count:] # 检查验证集中行为类别的分布 def check_distribution(file_list): counter Counter() for img in file_list: txt_name img.replace(.jpg, .txt).replace(.png, .txt) txt_path os.path.join(labels, txt_name) if not os.path.exists(txt_path): continue with open(txt_path) as f: for line in f: cls_id line.split()[0] counter[cls_id] 1 return counter train_dist check_distribution(train_files) val_dist check_distribution(val_files) print(训练集类别分布:, dict(train_dist)) print(验证集类别分布:, dict(val_dist))固定seed42是为了让每次划分结果可复现这一点在复现89.8%识别率时很关键——官方指标是在特定训练集和验证集划分下测出来的随机打乱后复现出的指标会有几个百分点的浮动这不是模型问题是数据划分问题。划分比例15%做验证集对7000多张图来说够用验证集大约有1000张图实例数足够评估。验证集的类别平衡建议目测一下。如果horse-eating在验证集里只有几十个框评估结果随机性会很大同一套权重跑两次可能差好几个点。遇到这种情况把val_ratio提高到0.2或者手动保证每个类别在验证集中至少有一批固定数量的样本。4. 行为类别与标注质量分析四个标签的边界和识别难点4.1 四个标签的判定逻辑horse类与具体行为的区别这套数据集的标签设计有一个容易误解的点horse标签代表“无任何动作的马”而horse-standing标注的是“站立的马”。看起来两者都是站立状态实际区别在于目标的行为状态是否明确。从标注规范角度理解horse更像是兜底类别马既没有吃草、也没有躺下处于普通站立或走动的状态都可以标成horse。horse-standing则特指稳定的站立姿态前腿和后腿同时着地身体没有明显位移。这里存在主观判断空间也是模型在这两个类别上容易混淆的原因——同一张图不同标注人员可能给出不同的标签。horse-eating是最有辨识度的行为类别特征是马低头、嘴部接近地面或草料区域颈部弯曲明显。但正因如此它的检测难度也最高许多吃草画面里马的头部被草遮挡或者只有背部和臀部露出标注框内包含大量背景。horse-laying则完全不同目标框宽高比接近1:1甚至横向拉长和站立的细长框有明显几何区别。标签行为含义典型姿态特征检测难点horse无明确动作站立或慢走身体完整与horse-standing混淆horse-eating吃草低头、颈部弯曲、嘴部贴近地面头部被遮挡目标不完整horse-laying躺下身体横卧框宽高比偏大样本量相对少角度变化大horse-standing站立四腿着地身体直立与horse边界模糊4.2 行为难易度差异吃草检测为什么容易漏检从目标检测的角度拆解吃草行为的漏检主要来自三个因素。第一个因素是姿态变形。马低头吃草时脖子和头部在图像中的形状与站立状态完全不同检测器如果依赖躯干特征做判断容易在头部区域给出低置信度。第二个因素是遮挡。草料区域通常位于画面底部马的头颈部经常被栏杆、草丛或其他马匹遮挡标注框的实际可见区域可能只有完整目标的60%。第三个因素是类内差异。不同品种的马体型差异大矮马和重型马在低头时的高度、宽度比例完全不同单一anchor尺寸难以覆盖全部情况。实测中常见的情况是horse-eating的置信度普遍低于horse和horse-standing如果后处理阈值设在0.5以上会丢掉一批吃草检测结果。解决方案有两种一是把模型的置信度阈值降到0.3左右二是增加吃草类别的样本权重让模型在训练时更关注这类样本的Loss。4.3 89.8%识别率的评价协议与复现要点拿到一个数据集第一反应应该是确认这个89.8%是在什么评价协议下测出来的。不同协议之间的数值差异很大mAP0.5、mAP0.5:0.95、整体准确率三者不能直接对比。按常见做法理解这个指标大概率是在一定数量的验证集上检测框与标注框的IoU达到0.5以上且类别判断正确时算作识别成功。要复现这个数值需要注意三个变量训练集和验证集的划分方式、模型架构与输入分辨率、训练超参数。YOLOv8s输入分辨率640×640、训练300个epoch是复现这类单类动物行为识别数据集的稳妥起点实际跑出来的mAP可能在85%到92%之间浮动取决于前两个变量的选择。5. 避坑手册VOC转YOLO与训练期的七个高频问题5.1 训练崩溃某个txt文件为空或缺失现象训练跑到某个epoch突然报错AssertionError: label shape或者FileNotFoundError中断后重启训练错误从头再来一遍。原因数据集里存在空标注文件没有object节点的XML转出来的txt为空或者图片文件名与XML文件名不一致导致转换后图片找不到对应txt。YOLO的DataLoader默认要求每张图片都有标注文件空文件会直接触发断言失败。解决训练前跑一遍完整性校验脚本检查每个txt文件是否有内容。发现空文件后要么删除对应的图片要么用--skip-empty参数处理。我的习惯是直接把空标注对应的图片从训练列表里剔除避免影响Loss计算。5.2 类别漂移horse-eating被解析成两个类别现象训练日志里类别数量变成5个预期是0到3实际出现了4和5或者horse-eating这个类别完全没被识别出来。原因XML里的name字段有细微差异比如horse-eating写成Horse-eating或者末尾带空格、换行符。name.text.strip()能处理空格但大小写不一致的情况处理不了。解决写代码时强制做一次类别名归一化把所有标签统一成小写再去空格然后映射到CLASS_MAP。如果类别不在映射表里打印出实际内容——很多隐藏问题就是这么暴露的。5.3 坐标越界导致Loss变为NaN现象训练Loss值在某个epoch突然变成nan之后再也回不来。原因XML标注里有超出图片边界的框比如xmax1920但图片宽度只有1280归一化后宽度大于1模型预测的边界框回归会出数值问题。解决转换脚本里对坐标做clip操作把所有坐标限制在图片范围内。这不是可选步骤而是必做步骤。标注工具导出时偶尔会把坐标多写出几个像素不裁剪迟早会遇到。5.4 躺卧行为检测效果差样本不平衡现象horse-laying的mAP明显低于其他三个类别或者验证时完全检测不到躺着的马。原因数据集里躺卧样本数量少加上躺卧姿态的宽高比与站立差异大模型见过的正样本不够学不到稳定的特征。解决先统计类别分布确认差距。如果躺卧样本确实少优先做法是复制样本配以轻度几何增强如水平翻转、小角度旋转而不是直接加大量Mosaic因为躺卧姿态在Mosaic里很容易被缩小到看不清。训练配置里把horse-laying的Loss权重调高1.2到1.5倍也有实际效果。5.5 吃了difficult标记的亏现象模型在验证集上指标正常但一到真实场景就大量误检尤其是在有遮挡的环境里。原因数据集官方标注中difficult1的框没有在训练时被过滤掉模型强行去拟合这些困难样本导致特征学习的重心偏移。解决在VOC转YOLO阶段直接丢弃difficult1的实例。严格来说difficult标记的框在VOC评估时不计入metric所以它们从训练阶段消失反而是干净的。5.6 数据增强策略不当导致行为特征丢失现象训练集loss降得很快验证集loss反而上升出现过拟合迹象。原因旋转角度设置过大马的姿态被增强成非自然的形状模型学到的是增强后的伪特征而不是真实行为特征。行为识别数据集与通用物体检测不同马吃草、躺下的姿态受生物力学约束不能像检测杯子那样随意旋转。解决degrees参数控制在5度以内translate控制在0.1以内关闭mosaic或将其概率降到0.5以下保留fliplr0.5、scale0.5这类对行为影响较小的增强。5.7 图片分辨率不一致导致标注脱离实际位置现象推理阶段用真实场景图片时检测框总是偏移但训练集上指标完全正常。原因数据集的图片尺寸不完全一致有的1920×1080有的1280×720如果转换时没有用每张图的真实宽高而用了固定值归一化坐标就错了。解决转换脚本中width和height必须从XML的size节点读取不能假设所有图片一样大。这一点在预处理时很容易忽略但影响是全局性的。6. 进阶验证技巧用混淆矩阵定位行为误判再谈参数调优6.1 用验证集混淆矩阵定位类别纠缠训练完成后不要只看mAP就收工。对行为识别来说更重要的信息是哪个类别和哪个类别在互相误判。YOLOv8训练结束后会在runs/detect/train目录下生成confusion_matrix.png直接打开看。如果horse和horse-standing之间存在明显的互相误判说明这两个类别的边界定义对模型来说太接近了。这种情况优先检查标注规范看有没有一批图的标签本身标错了如果标注没问题再考虑把horse定义为“移动中”的马与horse-standing的“静止站立”拉开姿态差异。行为识别的数据集类别定义的质量比数量更影响训练上限。6.2 推理参数与场景化调优训练好的模型导出并部署时有几个参数值得根据使用场景微调yolo detect predict modelruns/detect/train/weights/best.pt \ source./test_images \ conf0.3 \ iou0.5 \ imgsz640 \ saveTrueconf0.3是置信度阈值吃草行为本身检测难度高默认0.25到0.3之间比较合理设到0.5会漏掉大量目标。iou0.5控制NMS去重的IoU阈值场景中马匹密集时建议降低到0.4避免相邻的框被错误合并。imgsz640是输入分辨率马在画面中占比较小时提升到768或960能改善小目标检测但推理速度会明显下降需要权衡。6.3 从“能跑通”到“能交付”的一个小习惯处理完这套马行为数据集后我养成了一个习惯每次拿到新的VOC格式数据集先跑一遍完整校验——统计类别分布、检查坐标越界、确认图片和XML映射关系、过滤difficult样本全部通过后再进训练流程用时不到十分钟但能省掉后面数小时的排查时间。从那以后我每次处理VOC转YOLO都强制走一遍这个流程再没出现过训练中途因为标注问题崩掉的情况。希望帮到你。本文还有配套的精品资源点击获取