简介:面向狗狗行为识别与目标检测任务,数据集提供1551张原始图片,并同时附带VOC格式的XML标注和YOLO格式的TXT标注,覆盖bark(吠叫)、default(默认)、eat(进食)、lyingDown(躺卧)等8种常见行为,矩形框总计1613个。压缩包共2000个文件,核心为1551个xml标注文件与449个txt文件,整体约57.77MB;JPEGImages、Annotations、labels三个目录分别存放图片、XML和TXT,其中labels下的txt文件按YOLO格式记录类别与归一化框坐标,Annotations下的xml遵循VOC标注规范,便于直接接入YOLO、Faster R-CNN等主流检测框架。图片清晰度良好,未经增强处理,适合行为分类、姿态估计等视觉任务的算法验证与模型训练。目前已有180人学习下载,适合计算机视觉学习者或研究人员快速获取带标注数据,节省采集与人工标注时间。数据集标注规范、类别覆盖较均衡,可直接用于训练、验证及不同检测算法的对比实验,也可作为课程设计或毕业设计的基础数据。
1. 狗狗行为检测数据集拆包:1551张图、8种行为,YOLO+VOC双格式到底能省多少事
做狗狗行为检测项目时,第一道坎通常不是模型,而是数据。自己从视频里抽帧、清洗、画框、打行为标签,攒出1551张有效图至少得折腾两周,而且行为类别的标注比普通目标检测更容易标错,因为姿态相近的“坐下”和“趴下”连人都容易看走眼。这个数据集把8种常见狗狗行为整理成YOLO和PASCAL VOC两套标注格式,解压后可以直接进入YOLOv8训练流程,老项目如果只认VOC XML也能无缝接入。对做宠物摄像头、狗狗陪伴机器人、宠物行为分析平台的工程师来说,1551张不算大样本,但足够把训练流程跑通,也足够提前暴露格式转换、类别不均衡、过拟合这些真实问题。这篇文章按我实际动手的顺序讲,从解压目录讲到训练参数,再把最容易翻车的地方单独列出来。
2. 狗狗行为检测数据集的目录、类别映射与YOLO/VOC标注换算
2.1 解压后的第一件事:确认目录结构和train/val划分
拿到压缩包,我建议别急着把图片拖进标注工具里看效果,先把整体目录拉出来。多数带YOLO格式的数据集会按下面的结构组织,图片放images、标签放labels、VOC的XML放annotations,train和val预先切好:
dogs-behavior/ ├── images/ │ ├── train/ │ │ └── dog_0001.jpg │ └── val/ │ └── dog_0001.jpg ├── labels/ │ ├── train/ │ │ └── dog_0001.txt │ └── val/ │ └── dog_0001.txt ├── annotations/ │ ├── train/ │ │ └── dog_0001.xml │ └── val/ │ └── dog_0001.xml ├── classes.txt └── data.yaml具体文件名不一定叫dog_0001,但结构逻辑基本一致:labels目录下的txt文件和images目录下的jpg文件同名,只是后缀不同。这一步要确认三件事:train和val图片数量比例是否合理、labels里有没有明显缺文件、classes.txt在不在。我一般直接在终端里跑一条命令看统计:
unzip 狗狗行为检测数据集.zip -d ./dog_dataset find ./dog_dataset -type f | awk -F. '{print $NF}' | sort | uniq -c这条命令把文件后缀统计出来,jpg、txt、xml、yaml各有多少个一目了然。如果txt数量远小于jpg数量,说明标签文件缺失,还没训练就要先补数据;如果xml数量和txt数量差不多,说明VOC标注和YOLO标注双份齐全,后续做格式转换时就有对账依据。
先打开classes.txt看类别名和排列顺序,这是整个数据集最容易被忽略的关键。YOLO标签里的类别ID就是classes.txt的行号,从0开始,第一行是0,第二行是1,以此类推。比如常见的行为划分是进食、坐下、趴下、站立、行走、奔跑、吠叫、玩耍,那么classes.txt第1行如果写的是eating,ID 0就代表eating,后面所有标注里的0都指它。顺序一旦和data.yaml里的names对不上,训练不会报错,但推理出来的行为类别名会张冠李戴。
2.2 YOLO和VOC的坐标换算:XML像素坐标转成txt归一化坐标
VOC格式的标注存放在XML文件里,用xmin、ymin、xmax、ymax表示目标框的绝对像素坐标,而YOLO的txt每行格式是“类别ID、中心点x、中心点y、框宽、框高”,而且后四个值都必须除以图片宽高做归一化。把两者对应起来的关键公式就是中心点取最小最大坐标的均值,宽高取差值,再分别除以图片宽高:
import os import xml.etree.ElementTree as ET def voc_to_yolo_annotation(xml_path, out_dir, class_names): """把单张VOC XML转成YOLO txt。 class_names的顺序决定了YOLO标签里每个框的类型ID。 """ tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) basename = os.path.splitext(os.path.basename(xml_path))[0] lines = [] for obj in root.findall("object"): cat = obj.find("name").text if cat not in class_names: continue cls_id = class_names.index(cat) box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(os.path.join(out_dir, basename + ".txt"), "w") as f_out: f_out.write("\n".join(lines)) class_names = [ "eating", "sitting", "lying", "standing", "walking", "running", "barking", "playing", ] os.makedirs("labels/train", exist_ok=True) for xml_name in os.listdir("annotations/train"): if xml_name.endswith(".xml"): voc_to_yolo_annotation( os.path.join("annotations/train", xml_name), "labels/train", class_names, )这段脚本里,class_names的顺序和classes.txt保持一致,通过class_names.index(cat)把VOC里的类别名称转成YOLO的类别ID,而不是手写数字映射,能少踩一个错位坑。中心点用(xmin + xmax) / 2计算,除以图片宽高后得到0到1之间的归一化值;框宽用xmax - xmin算绝对宽度再除以图片宽度。输出精度保留6位小数,已经足够匹配主流训练框架的读取精度。
这里要说明一点:这个数据集既然同时给了YOLO和VOC两种格式,训练时直接用现成的labels目录就行,不需要做转换。给你这个脚本的真正用途是两个:一是拿来验证包内VOC标注和YOLO标签能否对上,二是以后你拿LabelImg标了一批新图存成VOC格式时,能顺手转进现有训练集。实际跑的时候,如果遇到某个XML里name写的是中文,并且不在class_names里,脚本会直接跳过这个目标,所以扩展类别时记得同步改class_names。
2.3 训练前做一次类别分布统计:8个行为里谁多谁少必须心里有数
在正式训练前,花五分钟统计一下标签分布,能避免后面训练半天发现个别行为类别完全没有样本量支撑。我用两个口径统计:按目标框数量算一个数,按图片数量再算一个数。因为有的图里会同时出现多只狗、多个框,行为检测落地时更关心的是“包含这个行为的图片有多少张”。
from collections import Counter import glob by_box = Counter() by_image = Counter() for txt_path in glob.glob("labels/**/*.txt", recursive=True): with open(txt_path, encoding="utf-8") as f: lines = [line.strip() for line in f if line.strip()] seen_classes = set() for line in lines: cls_id = int(line.split()[0]) by_box[cls_id] += 1 seen_classes.add(cls_id) for cls_id in seen_classes: by_image[cls_id] += 1 print("按目标框统计:", dict(by_box)) print("包含该行为的图片数:", dict(by_image))这段脚本不涉及训练框架,纯Python就能跑,递归遍历labels目录下所有txt。如果某个行为只有几十张图,比如奔跑和吠叫这类瞬间动作本来就难抓拍,在1551张的规模里很容易变成冷门类,后面的训练策略就要针对性调整:要么给这些类适当做离线增强,要么接受它们的AP值偏低,而不是盲目追求八个类平均。
还有一种情况值得警惕:如果按目标框统计和按图片统计两个结果差距很大,说明很多图片里有多只狗并且行为各不相同。此时要先确认项目要的是图像级行为标签还是目标级行为标签,两种语义在训练和评估时的指标含义完全不同,做产品需求调研时经常在这里出现信息差。
3. 用YOLOv8把狗狗行为检测跑起来:data.yaml、训练命令与结果判读
3.1 用YOLOv8训练自己的数据集:路径整理和data.yaml配置
YOLOv8也就是Ultralytics框架,读取数据集靠的是data.yaml,不直接索引classes.txt,所以先把数据描述文件写对。这个数据集如果包内自带data.yaml,也要打开检查一下path和names是否与当前路径一致,因为很多打包好的数据集默认路径是标注者电脑上的绝对路径,直接复用到你的机器上一定会报找不到图片。
# data.yaml path: /home/user/datasets/dogs-behavior train: images/train val: images/val nc: 8 names: 0: eating 1: sitting 2: lying 3: standing 4: walking 5: running 6: barking 7: playingpath是数据集根目录的绝对路径,train和val的值是相对根目录的图片子目录路径。这里要特别强调:train指向的是图片目录而不是标签目录,YOLOv8会自己根据images这个名字找到同级labels目录下的txt标签,它的约定是图片在images/train,标签就在labels/train。如果你把目录结构改成别的名字,就得自己在代码里改路径匹配逻辑,所以不要轻易动目录名。
nc必须等于names里列出的类别数,写错会在训练初始化阶段报错或者静默错位。names可以写成这种字典形式,也可以直接写成列表,两种写法Ultralytics都支持,但类别顺序必须和标签文件里的ID一一对应。最稳妥的做法是把classes.txt的内容原封不动搬过来,保持顺序不变。
3.2 最小训练命令:imgsz、batch、epochs三个参数怎么定
环境装好之后,最精简的训练命令是下面这条。我用的是coco预训练权重v8s起步,1551张的数据规模不建议从零随机初始化训练,也不建议一上来就上v8l这种大模型,s尺寸在这个数据量级属于平衡点。
pip install ultralytics yolo detect train \ model=yolov8s.pt \ data=/home/user/datasets/dogs-behavior/data.yaml \ epochs=100 \ imgsz=640 \ batch=8 \ patience=20 \ device=0 \ project=./runs \ name=dog_behavior几个关键参数的考量:
| 参数 | 默认值 | 我建议怎么看 |
|---|---|---|
| imgsz | 640 | 行为检测的框不算极端小,640够用;显存紧张再降到480,但小行为如抬头、摇尾的细节会受影响 |
| batch | 8 | 以显存为第一约束,8不够就调到4;batch太大在小数据集上反而加速过拟合 |
| epochs | 100 | 1551张图通常50轮以内就到平台期,100轮配合早停足够 |
| patience | 20 | 验证集指标连续20轮没提升就停,省时间 |
yolov8n虽然更快,但在这种小数据集上容易出现欠拟合,尤其区分趴下和坐下这种细节姿态时,模型容量不够学不细。yolov8l则容易把小样本行为直接当成噪声丢掉。v8s的推理速度和精度折中,先用它跑通一轮,拿到baseline再决定要不要往上加容量。
device参数如果只有CPU就写device=cpu,epochs建议先跑一轮看看loss曲线形态,确认数据和标签没大问题后再正式跑100轮。命令行跑完会在./runs/detect/dog_behavior下生成训练产物,包括weights目录、results.csv和一组可视化图。
3.3 训练完看什么:loss曲线、mAP50和混淆矩阵
训练结束后别只盯着终端里最后一行mAP,我会按固定顺序看四样东西:results.csv里的loss曲线、验证集mAP50、混淆矩阵图、还有几张带预测框的验证图。
import pandas as pd df = pd.read_csv("runs/detect/dog_behavior/results.csv") print(df[["epoch", "train/cls_loss", "val/cls_loss", "metrics/mAP50(B)"]].tail(5))这条命令读出最后5轮的指标。正常情况是train/cls_loss和val/cls_loss同步下降,然后同时走平;如果train loss还在降但val loss掉头往上,说明模型开始背训练集了,小数据集上这是典型信号。mAP50主要看0.5这个IoU阈值下的表现,行为检测的框画得粗一点问题不大,更关心的是行为类别判断对不对,所以mAP50比mAP50-95更有参考价值。
混淆矩阵要重点看相邻行为之间的误检,比如lying和sitting互相混、standing和walking互相混,这类错误靠加大epochs解决不了,要从数据层面想办法,比如补拍姿态更标准的样本,或者干脆合并容易混淆的行为类别。最后再打开验证集预测图,确认边界框确实框在狗身上而不是落在背景物体上,这一步是真正判断标签质量的时候,指标再漂亮,框偏了也是白搭。
4. 避坑清单:1551张数据集训练中常踩的5个坑与排查方法
4.1 坑一:标签和图片对不上号,训练时一多半图片是背景
现象:训练过程不报错,loss也在下降,但验证时的边界框大量漂移,mAP50低到离谱,打开预测图发现模型在背景区域乱框。
原因:很常见的坑。图片目录和标签目录里的文件名对不上,可能某个jpg的对应txt缺失,也可能一个图片名出现了多个后缀版本,jpg和jpeg混在一起,导致YOLOv8在找同名txt时漏掉一部分标注,直接把没标签的图当背景图训练。
解决:训练前先做一次严格的对应关系检查,把缺失标签的图片全部列出来:
import glob import os for phase in ("train", "val"): img_files = glob.glob(f"images/{phase}/*.jpg") missing = [] for img in img_files: stem = os.path.splitext(os.path.basename(img))[0] txt_path = f"labels/{phase}/{stem}.txt" if not os.path.exists(txt_path): missing.append(img) print(f"{phase} 缺失标签:{len(missing)} / {len(img_files)}") for m in missing[:5]: print(" ", m)如果图片有jpg和png混合的情况,上面这个脚本要把后缀都匹配一遍,最稳的办法是只取文件名主体做比对,不写死后缀。找到缺失图片后,用2.2节给的VOC转YOLO脚本重新从xml生成缺失的txt,因为双格式数据集里VOC标注通常是全的,转一下就能补回来。
4.2 坑二:VOC转YOLO时坐标算错,验证AP全白
现象:训练过程一切正常,生成的混淆矩阵也有内容,但验证集的precision和recall全是0,或者全类别AP都一样。
原因:典型算错位置是中心点公式。有人把x_center直接写成xmin / img_w,漏掉了(xmin + xmax) / 2这一步;还有人转完忘记除以图片宽高,直接存了绝对像素坐标。这两种错误的结果都是框的位置完全错乱,模型学不到有效特征。
解决:转完不做可视化验证等于白转。写一个小函数把YOLO标签画回图上,肉眼对比原图和框是否贴合:
import cv2 def show_yolo_box(img_path, txt_path): img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path) as f: for line in f: cid, xc, yc, bw, bh = map(float, line.strip().split()) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) return img check_img = show_yolo_box("images/train/dog_0001.jpg", "labels/train/dog_0001.txt") cv2.imwrite("check.jpg", check_img)这段代码把txt里的归一化坐标还原成像素坐标时,用的公式是(xc - bw/2) * w,和转换时刚好互逆。随机挑5到10张图画出来看一遍,框能贴合再进训练,这一步花十分钟能省后面一整天的瞎调参。
4.3 坑三:8个行为类别样本量差距悬殊,个别行为AP被打穿
现象:整体mAP50看着还挺好,但拆开看单类AP,发现barking或者walking这类行为AP是0,或者低到不可用。
原因:还是那句老话,在1551张图里,少数行为类别可能只有三五十个目标框。这些小类别样本在训练时被mosaic、翻转等增强一搅和,模型更容易把它们当作背景噪声忽略。
解决:我一般分三步走。第一步用2.3的统计脚本算出每个行为类别的图片数,低于100张的单独处理。第二步给这些少样本类别做离线增强,水平翻转、亮度抖动、轻微旋转,把几十张扩到一百多张,而不是简单在代码里重复同一张图。第三步评估时单独看每个类别的AP,不要只看总mAP,总指标容易掩盖单类失效。注意,YOLOv8不像分类模型那样有一个现成的class_weight参数,真正可持续的解法还是从数据侧把少样本类补齐。
4.4 坑四:train/val按帧随机切,同源视频帧泄漏导致结果虚高
现象:训练集mAP50到了0.9以上,拿训练时没见过的真实视频去测,漏检反而严重,差距非常明显。
原因:如果这个数据集是从视频抽帧来的,同一个视频的相邻帧相似度极高。随机按帧划分train和val时,同一段视频的相似帧会同时出现在两边,模型等于提前见过答案,val指标虚高,一到真实场景就现原形。
解决:按视频片段划分而不是按帧划分。假设文件名前缀是视频标识,比如dog0011_0123.jpg里的dog0011就是视频ID,那就要整组划分:
import glob import os import random import shutil random.seed(0) def video_key(path): return os.path.basename(path).split("_")[0] imgs = glob.glob("images/*.jpg") video_ids = list({video_key(p) for p in imgs}) random.shuffle(video_ids) split = int(len(video_ids) * 0.8) train_videos = set(video_ids[:split]) val_videos = set(video_ids[split:]) for img in imgs: vid = video_key(img) dst = "images/train" if vid in train_videos else "images/val" shutil.copy(img, os.path.join(dst, os.path.basename(img)))标签文件按同样逻辑复制到labels下对应目录,确保train和val里不出现同一个视频ID。按视频划分后,val集合里的场景和train区别更明显,获得的指标才接近真实部署水平。小样本数据集上这种泄漏问题特别隐蔽,因为总mAP不降,但泛化能力已经出名地差。
4.5 坑五:类别ID从1开始写,训练正常但推理时行为名称错位
现象:训练能正常跑完,预测框的位置也对,但输出的行为类别名和实际行为对不上,比如明明检测到狗躺着,打印出来的类别却是坐着。
原因:标注工具或转换脚本里类别ID从1开始编号,而classes.txt和YOLO框架都要求从0开始。更常见的是有人中途往classes.txt中间插了一行类别,导致后面所有标签ID往后移了一位,数据本身没变,只是ID对不上了。
解决:训练前先扫描一遍所有标签文件,检查最大类别ID是否等于类别总数减1:
import glob min_id, max_id = 999, -1 for txt in glob.glob("labels/**/*.txt", recursive=True): with open(txt) as f: for line in f: cls_id = int(line.split()[0]) min_id = min(min_id, cls_id) max_id = max(max_id, cls_id) print("最小类别ID:", min_id, "最大类别ID:", max_id) assert min_id == 0 and max_id <= 7, "类别ID范围异常,请检查classes.txt"如果max_id等于8,说明确有ID从1开始的情况,把所有标签文件里的class_id统一减1就行。这个坑最容易出现在用VOC数据拼了YOLO数据的场景里,因为VOC里的name是字符串,转到YOLO时容易以1为基准从头编号。做一次全量扫描确认,比等推理阶段返工划算太多。
5. 部署前验证:视频推理和ONNX导出时的三个细节
训练完不要直接拿去部署,先拿一段真实场景视频跑一遍推理。这里有个容易被忽视的细节:训练时用的验证集图片通常是整理过的,现场视频的光照、角度、遮挡完全不一样,跑视频能发现很多静态图上不存在的漏检问题。
yolo detect predict \ model=runs/detect/dog_behavior/weights/best.pt \ source=demo.mp4 \ conf=0.25 \ save_txt=True \ save_conf=Trueconf=0.25是常规建议值,但行为检测场景我建议专门试一次0.1和一次0.1和0.1,不对,是0.1和0.5两种阈值,观察漏检和误检的平衡点。行为本身有歧义性,比如站立和行走在单帧上本来就难分,置信度阈值调低能保留更多候选框,后续再靠时序平滑做判断,这是行为检测和通用目标检测用法上最明显的区别。
确认视频推理效果没问题后,再做ONNX导出,方便后续用TensorRT或者OpenVINO在边缘设备上部署:
yolo export model=runs/detect/dog_behavior/weights/best.pt \ format=onnx \ imgsz=640 \ opset=12 \ simplify=True导出后要检查两件事:第一,输入尺寸是否是固定的640,如果部署端想用动态分辨率,需要重新导出并确认动态轴;第二,用onnxruntime跑一遍推理,对比一下和PyTorch原模型的结果差距。ONNX推理通常有微小浮点差异,但框和类别不应有大变化,如果出现明显的框偏移,多数情况是导出时预处理设置和训练时不一致。
我现在的习惯是训练完先跑一次val,再拿一段没见过的竖屏视频按真实使用场景过一遍,最后才导出ONNX,并且会刻意记录CPU上的推理耗时。1551张的小数据集本身参数上限就摆在那里,与其纠结调参,不如在验证流程上多下功夫。希望这些踩过的坑能帮你少走一段弯路。
本文还有配套的精品资源,点击获取