前阵子我在捣鼓一个宠物智能设备,需求看起来就一句话:识别画面里到底有没有猫或狗。可真等下手做,才发现这一句话背后全是坑。为此我干脆自己攒了一套猫狗检测数据集,总共有4300张图,用YOLO训练宠物识别模型,从标注格式到部署全流程走了一遍。这篇就毫无保留地把整套方案拆开讲,包括数据集怎么凑、标签怎么标、模型怎么选、训练参数怎么调、上线后遇到哪些问题。想入门YOLO目标检测、或是正打算做宠物识别相关项目的朋友,可以参考这套流程,省得像我一样踩一圈。
1. 为什么我决定自己整理一套猫狗检测数据集
1.1 一个看似简单的需求背后全是细节
宠物识别不是“给一张图,判断猫还是狗”这么简单。真实场景里,猫缩在沙发角落被抱枕挡了一半,黑狗在夜晚逆光下只剩一团轮廓,两只宠物凑在一起打闹,这些情况会同时出现。如果只是拿分类模型做识别,根本拿不到“宠物到底在画面哪个位置”。所以我直接锁定了目标检测方案,模型要输出bounding box加类别,这样才能进一步驱动设备动作:比如猫靠近自动锁住喂食器、狗闯入禁区触发告警。
这个需求对模型有两个硬要求。一是实时性,单帧推理速度要在可接受范围内,否则设备体验会很差;二是误检率,宠物监控类应用如果一天到晚瞎报,用户很快就会把功能关掉。综合下来,YOLO系列是性价比最高的选择,速度快、生态成熟、训练部署链路完整。而YOLO要训练好,第一步永远是数据,这也成了我决定自己整理数据集的直接原因。
1.2 公开数据集救不了你的实际场景
动手之前,我也在开源数据集里翻过一轮。很多公开的猫狗检测数据集,看缩略图似乎很美,但真拿来做项目就会发现问题。
最常见的是类别不平衡。有些数据集里狗的图片数量明显多于猫,直接训练会导致猫的召回率偏低。而在实际产品里,猫和狗的重要程度是一样的,漏检哪边都会出问题。第二是背景太干净,很多图来自图库,宠物在正中间,背景是白墙或纯色草地,摄像头设备看到的却是沙发缝、窗帘逆光、地毯花纹这种复杂环境,模型很容易不知所措。第三是标注格式不统一,有的是VOC格式XML,有的是COCO格式JSON,有些txt文件里坐标根本没归一化。就算费劲转成YOLO格式,也经常发现框偏大、框偏小、类别标错,需要大量返工。
所以我干脆决定自己整理一套。公开数据集适合做通用预训练,但不适合直接拿去做场景交付。只有自己控制采集、清洗、标注、切分的每个环节,才能知道模型表现好到底是因为什么。
2. 数据集设计与标注方案
2.1 4300张图是怎么攒出来的
最终留下的可训练图片是4300张,猫和狗基本对半,猫约2150张、狗约2150张。里面掺了大概5%~8%的多宠物图片,也就是同一画面里既有猫又有狗,这部分样本对多目标检测能力提升非常关键,没有它们,模型很容易在两只宠物同框时只检出其中一个。
图片来源我做了混合。一部分是自己拿手机拍的,大概300张,专门拍家里的猫在不同光线、不同角落下的状态;一部门是从合法的图集类素材里筛选;还有很大一部分是从视频里抽帧得到的。视频抽帧有个明显好处,同一只宠物在连续帧里有不同的姿态、不同的遮挡程度、不同的身体朝向,相当于给数据集加入了丰富的时序变化。但抽帧一定要做去重,不然训练集和验证集里出现同一段画面的连续帧,验证指标会虚高,看着mAP很漂亮,部署后立刻露馅。
清洗阶段我卡得比较狠。分辨率低于300×300的图片直接删掉,模糊到肉眼都难分辨的删掉,有严重水印或贴纸遮挡宠物主体的删掉。卡通、插画、3D渲染图也一律排除,除非你的项目本身就做卡通宠物识别。这样清洗的目的是让模型贴近真实摄像头输入,而不是训练集精美得像写真集,部署后每天对着客厅的乱糟糟环境发懵。
2.2 YOLO标签格式与归一化坐标计算
YOLO的标签格式是每张图对应一个txt文件,每行代表一个目标:
class_id x_center y_center width height
这五个数字里,x_center、y_center是目标框中心点的坐标,width和height是框的宽高。注意,这四个值全部是归一化到0~1之间的小数,不是像素值。这一点对刚入门的朋友特别容易踩坑。如果标签存成绝对像素,图片一旦被缩放,框的位置就全对不上了。归一化坐标天然能够适配任意输入尺寸,因为训练时模型总会把图缩放到固定大小。
拿一个具体例子来算。假设一张图宽度1600px、高度1200px,里面有一只猫,框的左上角坐标是(400,300),右下角坐标是(900,800)。那么框宽w=900-400=500像素,框高h=800-300=500像素,中心点x=(400+900)/2=650像素,中心点y=(300+800)/2=550像素。归一化之后:
- x_center = 650 / 1600 = 0.40625
- y_center = 550 / 1200 = 0.45833
- w = 500 / 1600 = 0.3125
- h = 500 / 1200 = 0.41667
如果猫的class_id约定为0,那这个txt文件里就是一行:0 0.40625 0.45833 0.3125 0.41667。
我标注的时候先用LabelImg做初标,再做格式转换。也有朋友改用X-AnyLabeling这类支持半自动分割的标注工具,效率确实高一些,但转换出来的坐标格式一定要再核对,尤其要检查归一化逻辑是不是和YOLO一致。
2.3 标注质量检查与类别平衡
标注完成后要通篇过两遍,这个环节千万别省。我自己检查时最看重三类问题。
第一是类别混淆。黑猫和黑狗在暗光环境下真的很容易标错,冬天深色毛发的宠物拍出来就是一团黑影,人眼都容易看走眼,标注时稍不留神就会把猫标成狗。这种错误比框偏了几个像素严重得多,因为模型会收到互相矛盾的梯度信号。
第二是框的松紧程度。框太紧会截掉耳朵、尾巴,模型会学得束手束脚;框太松会把旁边的茶几角、抱枕边包进去,等于让模型把背景当目标一起学。我的经验是,目标框包住可见的完整躯干就好,被挡住的部分不用强行外扩,毕竟你要的是“这只宠物在哪个区域”,不是“这只宠物的轮廓有几斤几两”。
第三是漏标。有些图片目标若隐若现,实在看不清就删掉,千万不要留着却不标框。漏标的目标在训练时会被网络当作背景,模型学到的反而成了“这个区域没有宠物”,这是最坑的错误信号。
类别平衡方面,我统计的是框数量而不是图片数量,因为一张图可能同时有好几只猫或狗。最终猫框约3400个,狗框约3300个,基本做到1:1。如果某一类框偏多,我会优先补充另一类的困难样本,而不是用损失函数的采样权重硬凑,因为前者能让模型真正见到更多样化的目标。
3. YOLO系列模型选型与训练环境搭建
3.1 为什么选YOLO而不是其他检测方案
刚入门的朋友常纠结YOLO和Faster R-CNN这类两阶段检测器怎么选。我的判断标准很直接:先看部署设备,再看精度要求。
两阶段检测器先挖候选区域再做分类和回归,在小目标检测、密集场景上确实有精度优势,但推理速度往往跟不上。假设你的设备是普通x86 CPU或者Jetson这类边缘硬件,Faster R-CNN做单帧推理很难达到理想帧率。YOLO把检测当成单次回归问题,网络一次性输出所有框、类别和置信度,虽然在极度复杂的场景下精度上限可能不如两阶段,但猫狗识别这种类别少、目标尺度偏向中等大小的任务,YOLO的精度已经完全够用。
还有生态因素。Ultralytics官方把训练、验证、导出、部署集成得很完善,一个命令就能从零开始训练,再一个命令导出ONNX或TensorRT。如果后续要做实例分割,YOLOv8-seg和更新版本的分割模型也支持直接训练,不需要另起炉灶换框架。对做实际产品的人来说,这套链路确实好上手。
3.2 选YOLOv8s还是YOLOv8n
我最终用的是YOLOv8s,没有选更小的n,也没有选更大的m,原因要结合数据集规模和部署目标来说。
- YOLOv8n:参数量约3.2M,适合手机端和极低功耗场景,但参数量小,如果训练数据质量或数量不够,精度上限会比较紧张。
- YOLOv8s:参数量约11.2M,精度比n高一截,在CPU上用ONNX推理640×640输入大约100~200ms/帧,在GPU或NPU上可以轻松实时。这是我在精度和速度之间找平衡选择。
- YOLOv8m:约26M参数,精度更高,但推理开销更大,适合算力充足的设备。
4300张的数据量对n来说不算特别富裕,用s更稳。如果将来要上电池供电的低功耗设备,我会先训练一个精度很好的s模型,再把它蒸馏到n上,而不是直接拿n裸训。机器学习的通用经验是,当模型容量不够时,先让大模型学到好特征,再蒸馏给小模型,效果往往好过小模型自己死磕。
3.3 环境安装与硬件选择
训练环境我用的版本组合是Python 3.10、PyTorch 2.1以上、CUDA 11.8或12.1,加上Ultralytics 8.x。安装就一条命令:
pip install ultralytics装好后先确认GPU能用:
import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))我手里是一张RTX 3060 12GB显卡。训练YOLOv8s、batch=16、imgsz=640完全没压力。如果你的显存是8GB,那就把batch降到8,同时要确保AMP自动混合精度开着。Ultralytics默认就开启AMP,能用半精度训练,显存占用减少很多,训练速度也会提升。手头没有NVIDIA GPU也没关系,小数据集用CPU跑也能出结果,只是训练时间会拉长到几小时甚至十几小时,适合只做实验验证。
4. 完整训练流程与核心参数调优
4.1 数据集划分与data.yaml配置
我按train:val:test=8:1:1来划分,也就是3440张训练、430张验证、430张测试。验证集用来做早停和调参,测试集只在最终评估时看一眼,避免模型“背答案”。
这里有一个关键细节:划分不能直接全量随机打乱,然后切一刀。因为我的数据里有大量来自视频抽帧的图片,同源连续帧之间高度相似,如果随机切分,训练集和验证集里会出现大量同源图,验证得分虚高。我的做法是把图片按来源分桶,再从每个桶里按比例抽,保证同一个视频序列的帧尽可能只落在一个集合里。
data.yaml的写法如下:
path: /data/pet_detection train: images/train val: images/val test: images/test names: 0: cat 1: dognames的顺序必须和标注txt里的class_id完全一致。我标注时约定了cat=0、dog=1,那训练和推理阶段也要保持这个顺序,否则部署时就是把“猫的框”贴上“狗的名字”。
4.2 训练启动与逐参数解读
实际训练命令:
yolo detect train \ data=data.yaml \ model=yolov8s.pt \ epochs=150 \ batch=16 \ imgsz=640 \ patience=30 \ device=0参数选择背后的逻辑:
- epochs=150:4300张图片不算多,配合数据增强,150轮足够收敛。如果数据量只有几百张,最好加载预训练权重并加强增强,盲目加大epoch只会更快过拟合。
- batch=16:在12GB显存下跑得很稳,比这更大能提升一点训练稳定性,但显存会接近瓶颈。
- imgsz=640:与官方预训练权重最匹配的输入尺寸。训练和部署都应保持一致,不要训练用1280、部署用640,那样会引入分辨率不一致的性能损失。
- patience=30:连续30轮验证集mAP不提升就早停。这个参数能节省大量无效等待时间。
训练启动后我会同时打开tensorboard看实时曲线,Ultralytics会在runs/detect目录下输出日志,也可以直接加--project和--name参数管理实验。训练结束后,weights文件夹里会出现best.pt和last.pt,best.pt是验证集上表现最好的权重,之后所有评估和部署都默认用它。
4.3 训练过程观察与损失曲线怎么看
训练日志里主要关注三类loss:box_loss定位损失、cls_loss分类损失、dfl_loss分布焦点损失。YOLOv8的回归分支用的是CIoU损失,DFL则对边界框四条边的分布做建模,让框更贴合目标边缘。
正常状态是三类loss都缓缓下降,大约第50到第80轮后进入平台期。如果cls_loss一直在降而验证集指标反而反弹,说明模型开始过拟合,这时候不要急着加数据增强,先检查训练集里是不是有大量重复帧在给模型“灌答案”。
我这套数据训到第95轮左右触发早停。best.pt在验证集上的指标大约是mAP@0.5在0.938上下,mAP@0.5:0.95在0.851附近。两个类别这种任务量能有这个数字已经不错。如果你的目标检测场景更复杂,比如大量小目标,mAP还会明显下降,这是正常现象,不是模型故障。
4.4 评估指标:不只盯着mAP
很多人只看mAP,但宠物识别这种场景,我更建议把precision和recall分开看。
- 如果是宠物门禁,漏报猫等于功能失效,recall优先级更高,宁可偶尔误报,也不能漏。
- 如果是宠物监控告警,用户对误报零容忍,频繁误触发会直接卸载功能,那precision优先级更高。
- 两者都要兼顾,就看F1分数。
YOLO训练完会在验证目录里生成混淆矩阵和PR曲线。生产环境里,我把置信度阈值conf_thres设成0.4,NMS的iou阈值设成0.45,这样一个配置能让precision和recall相对均衡。如果你要追高召回,把conf降到0.25;要追高精确,就把conf升到0.5以上。这个阈值调整不需要重新训练,改推理参数就行。
5. 高频问题排查与实战避坑
5.1 Loss不下降或震荡如何排查
我训练第一版数据时遇到过失控状况,loss到第60轮左右突然向上抬头,当时第一反应是学习率太大,结果检查半天才发现,是有几个标注文件的class_id写成了2,但names里根本没有这个类,模型等于在学一个不存在的标签。
我把排查顺序整理成了固定套路,碰到loss异常先按这个顺序走,效率高很多:
- 先看loss曲线从头到尾的趋势。如果从不下降或震荡剧烈,先怀疑标注文件,随机抽查image和txt的对应关系。
- 可视化训练集中的图片,把标注框画出来,确认框确实落在目标上,而不是飘在背景里。
- 检查数据集切分,确认没有同源图片跨集合。
- 以上都正常,才轮到学习率、数据增强、imgsz这些超参数。
做深度学习项目,数据问题永远排在超参数前面。数据不乱,模型通常不会疯。
5.2 漏检、误检集中的位置怎么处理
漏检最典型的场景是目标太小。宠物在画面远处只有几十个像素高,YOLOv8s容易直接把它当背景扫过去。处理思路是提高训练分辨率到768,补充这类小目标在数据集里的占比。如果部署密度允许,也可以在推理时用更大的imgsz,代价是延迟变高。
严重遮挡和极端姿态也是一个漏检重灾区。猫团成一团只露半张脸,或背对镜头只给你一个屁股,这些情况人眼都容易看走眼。这类样本没法靠hsv颜色抖动、平移旋转这些通用增强变出来,必须靠现实采集专门补充。
误检经常集中在固定区域。如果摄像头长期固定,画面里某个玩偶、盆栽长得像猫,就会频繁误报。最简单的方案是给固定机位拍一段空场景视频,做负样本微调,让模型学会“这个角落是背景不是目标”。做监控类产品时,这条经验几乎每次都能派上用场。
5.3 训练结果和部署结果不一致的原因
训练时一切正常,导出ONNX到目标设备后同一张图检测结果对不上,这个坑几乎每个人都会遇到。常见原因有以下四个。
第一是预处理通道顺序。训练时模型输入是RGB,但OpenCV读图默认是BGR,如果推理前不做cv2.cvtColor(img, cv2.COLOR_BGR2RGB),模型看到的颜色就全乱了。
第二是归一化方式不同。YOLO通常用像素值除以255归一化到0~1,有些导出模型已经在内部做了归一化,有些则要求输入原始0~255图像,必须看模型的实际输入约定。
第三是letterbox填充。训练时输入统一到640×640,但原始图片宽高比各不相同,需要做灰色填充的letterbox,保证目标比例不被拉伸。如果直接把原图resize到640×640,框的位置和大小都会偏。
第四是NMS参数不一致。训练时NMS的iou阈值如果和部署时不同,重复框的过滤结果也会不同。我遇到过一个案例,部署时把iou_thres从0.45改到了0.6,结果原本两个重叠框应该合并成一个的,模型却输出了两个框,视觉上就像一次误检。
这几个点列成一个小表就是下面这样的快速排查卡:
| 现象 | 优先怀疑项 | 检查方法 |
|---|---|---|
| 检测框整体偏移 | letterbox或resize逻辑不一致 | 打印预处理后的图像尺寸和坐标映射 |
| 检测框偏大偏小重叠多 | NMS阈值与训练时不一致 | 对比部署端iou_thres和训练配置 |
| 颜色异常、类别混乱 | RGB/BGR通道顺序搞反 | 转成RGB后肉眼对比图像颜色 |
| 置信度普遍偏低 | 归一化方式不一致 | 确认模型输入是否要求除以255 |
6. 部署落地与后续扩展经验
6.1 导出ONNX并在CPU上跑起来
YOLOv8导出ONNX只需要一条命令:
yolo export model=best.pt format=onnx opset=12 simplify=True导出后可以用onnxruntime进行推理,这样目标机器上就不需要再装PyTorch了。核心工作其实只有一个:把训练时的预处理链路完整搬到推理端。推荐写一个最小可用的脚本,先验明正确性再扩展到多线程场景。
import cv2 import numpy as np import onnxruntime as ort session = ort.InferenceSession("best.onnx", providers=["CPUExecutionProvider"]) image = cv2.imread("test.jpg") image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 这里继续完成letterbox调整尺寸,然后归一化到0~1 # 最后转成CHW格式,增加batch维度,送入session.run推理输出一般是(1, 84, 8400)这样的维度,8400对应不同尺度下的候选框数量,84里包含4个边界框坐标、1个目标置信度、2个类别得分,再加上其他辅助参数。如果不手写后处理,直接用Ultralytics的predict接口也能跑,但那还需要PyTorch环境,所以需要ONNX还是建议自己把后处理写一遍。
在普通CPU服务器上,YOLOv8s ONNX跑640×640推理,单帧延迟大概在120到180毫秒之间,做实时跟踪会吃力,但做摄像头抽帧检测或事件触发完全够用。如果要在GPU或NPU上做多路实时视频检测,配合TensorRT优化之后单帧可以降到几毫秒,就能撑起多路并行分析,不过那是另一个话题了。
6.2 实际项目中哪些坑是模型解决不了的
训练集再完善,也不代表上线后万事大吉。我在真实项目里遇到过几个数据集很难根治的情况。
隔着纱窗或玻璃时,猫在纱窗后面,摄像头捕捉到的是网格纹理和反光叠加在一起的杂乱图像,模型检测时会飘,这是成像问题,不是模型问题,只能靠硬件或图像预处理改善。
强逆光导致宠物只剩一个剪影,毛发的纹理信息几乎全丢了,模型只能勉强给个框,根本不稳。这种情况需要调整摄像头曝光策略,或者换一台宽动态范围更好的摄像头,纯靠喂数据效果有限。
还有一个经典陷阱:摄像头对着客厅,电视机正好在播放猫狗视频,模型就会误以为家里来宠物了。这个问题的合理解法是做运动检测,或者对固定机位设定活动区域,电视画面属于“不合理的活动区域”,直接屏蔽。做这类系统,数据和模型只占一半,另一半是设备侧的规则工程,越早想明白越省事。
6.3 如果继续做,我会在数据集上补什么
当前这4300张数据已经能处理通用的猫狗检测问题,但如果要把这套数据集升级成更完整的宠物识别方案,我会补三块内容。
第一是品种级细分类。现在只分猫狗两类,下一步可以加品种标签,做成树状结构,比如先判断是猫还是狗,再细分到常见品种。这样需要收集更多样本,不然布偶猫、橘猫、德牧、柯基这些常见品种的数据很难覆盖全。
第二是姿态与关键点。检测框只能表达宠物在哪个位置,表达不了它正在趴着、站着还是跑动。加上关键点检测或姿态估计之后,就能做行为分析,比如判断猫是否在磨爪子、狗是否在转圈叼东西。
第三是实例分割。把检测框升级成分割掩码,可以更好地区分宠物和背景混杂的边缘,遮挡情况下的计数也会更准。现在YOLO系列已经有分割模型可以直接训练,数据标注工具Mirror上是复用这套检测标注做二次精修,代价是标注成本会明显上升。
说实话,后面这几项我还在摸索推进中。也希望正在做宠物识别的朋友,能从这篇内容里少踩几个坑,把精力和时间花在真正值得打磨的需求上。