养过猫的人都知道,猫的情绪不像狗那么直白——狗开心会摇尾巴,紧张会夹尾巴,这些特征相对固定;猫呢?耳朵动一下、瞳孔缩一下、尾巴尖抖两下,每一种都是信号,但组合起来千变万化。想用AI做宠物行为分析,第一步不是选模型,而是先搞定一套能反映真实场景的标注数据。这篇就说说我做的这份3200张YOLO宠物行为数据集,从采集、清洗、标注到训练YOLOv8的完整过程,以及中间踩过的那些坑。
1. 猫情绪检测这件事,为什么卡在数据集上
1.1 行为识别和情绪识别是两码事
很多入门项目会把“行为识别”和“情绪识别”混为一谈。行为识别解决的是“猫在干什么”,比如跑、跳、抓挠、舔毛;但情绪识别要回答的是“猫现在处于什么心理状态”,比如放松、警觉、紧张、愤怒。行为是外显的、可观测的,情绪则要靠行为姿态去推断。
我自己一开始也走了弯路,直接去搜“cats emotion dataset”,结果发现公开数据集非常少,而且大部分是学术场景下的实验室录影,猫被固定在一个小房间内,光线单一,镜头角度固定。这种数据训练出来的模型,放到家里客厅用,基本没法看——猫的角度、遮挡、光照全变了,模型立刻退化。
另一个常见误区是拿人脸的FER(Facial Expression Recognition)逻辑硬套到猫身上。人的情绪表情有相对稳定的面部肌肉模式,但猫的面部肌肉结构完全不同,表达情绪更多依靠耳朵角度、胡须位置、瞳孔宽度和整体身体姿态。单看一张脸,很多时候判断不了,必须把头部和身体一起放进检测框里。
所以我最终确定的思路是:用YOLO目标检测框架,把整只猫作为检测目标,类别直接定义为情绪状态。一张图里可能有三只猫,就标三个框,框里分别写猫的类别标签。这不是分类任务,是检测任务——模型不仅要判断情绪,还要输出猫在画面中的位置。
1.2 通用宠物数据集到底缺在哪
市面上能用的宠物数据集,比较有名的是Oxford-IIIT Pet Dataset,里面有37类猫狗品种,但标签是品种和物种,和情绪没有半点关系。COCO数据集里有猫这个类别,但只有“cat”一个标签,模型能识别出猫,却分辨不了它处于什么状态。
还有一些做“猫行为识别”的论文数据集,比如用传感器记录猫的日常活动,配合加速度计和摄像头。这类数据集有一个致命问题——传感器数据很难和视频帧精确对齐,而且传感器的佩戴会影响猫的自然表现。猫这种生物,身上挂个东西之后,行为会明显改变,采集到的所谓“自然行为”其实已经变形了。
情绪数据的采集困难还在于状态的不均匀分布。猫一天中大多数时间是放松或睡觉,紧张、愤怒、恐惧这类负向情绪状态占比很低。如果按自然比例采样,3200张图里可能有一半以上都是“放松”,模型训练出来对负向情绪的召回率会非常难看。
这种情况下,自制数据集几乎是唯一出路。3200张图不算多,但针对单一物种、单一目标类别、六类情绪状态,做精做细,落地到家庭环境中是完全够用的。
2. 3200张图的采集与清洗:宁可少,不能脏
2.1 采集渠道和场景分布
我这批数据主要来自三个渠道,每个渠道的比例刻意做了平衡,避免模型对某一类场景过拟合。
第一类是家里安装固定摄像头录制的视频。这是主要来源——固定机位的好处是视角稳定,白天和夜晚的光线变化都在同一个位置,猫经过时产生的姿态变化非常自然。我从大约40小时监控录像中,按照关键帧抽取,抽帧策略不是均匀抽帧,而是先过一遍预检测,帧里有猫才抽出来送人工筛选。这一步省了大量时间,否则40小时视频按1秒1帧来抽,要抽14万帧,人根本看不过来。
第二类是手机手持拍摄的日常片段。这部分补充了固定机位缺失的角度,比如俯拍、猫躺在人腿上的仰拍、贴近地板的平拍。手持拍摄的抖动问题比较严重,清洗时我简单粗暴地用了一个标准:目标区域的模糊程度超过肉眼可辨识的限度就删。
第三类是网络公开图库中明确标注可商用的素材。使用前要确认授权范围,挨个检查来源页面上的license声明。图库素材能补充一些家里拍不到的品种和场景,比如布偶猫、缅因猫这类长毛品种,我家没有。品种多样性对情绪检测是有意义的,因为长毛猫的耳朵形态和短毛猫有差异,模型如果只在短毛猫上训练,遇到长毛猫的“飞机耳”就容易误判。
最后留下来的分布大概是:监控视频抽帧约1400张、手机拍摄约1000张、图库素材约800张,合计3200张。
2.2 去重、模糊帧和负样本处理
清洗阶段我会按优先级筛掉四类素材:
一是相似度极高的连续帧。视频抽帧很容易产生几乎相同的画面——猫在同一个位置睡了十几秒,抽出来的每一帧姿态都差不多。这种帧如果在数据集中大量存在,会让训练集和验证集之间产生信息泄漏,模型会“记住”而不是“学会”。我用感知哈希算法算图像之间的相似度,阈值设置得比较保守,相似度超过0.92的帧只保留一张。
二是运动模糊。猫的动作速度比人快,尤其是扑咬、跳跃的瞬间,快门跟不上就大面积糊掉。人眼能识别,但YOLO的标注框在这种图上很难框准边界。YOLO训练时对边界框的精准度要求其实很高,框差5个像素,损失函数里就会体现出来,模糊图上标的框位置是随机的,等于给模型喂了噪声。
三是目标占比过小的图。猫在画面边缘、只占整张图的不到5%,检测框太小,模型基本学不到有效特征。这种图保留下来会拉低学习效率。我定了条线:猫的边界框短边小于32像素的直接删,这是YOLO普遍采用的锚框尺寸下限,小于这个值,模型很难提取到稳定纹理特征。
四是真正无法判断情绪的图。比如猫完全背对镜头、身体被毯子整个盖住、只能看到一团轮廓。这种图硬标的话,标注员只能靠猜,标签可靠性存疑,训练出来也是带噪声的映射。
清洗之后的数据量会有明显缩水——我从抽出来的6000多张里筛到4200张左右,标注过程中又因为边界框质量问题淘汰了一批,最后稳定在3200张。这个过程很肉痛,但是值得。数据质量不过关,后面所有调参工作都是白费。
3. 情绪类别体系设计:先定义清楚再看YOLO
3.1 六类情绪状态的判定标准
很多项目上来就定十几个类别,什么“好奇”“无聊”“期待”“不满”,标注员和模型全都一头雾水。情绪类别不是越多越好,类别之间要有可观察的形态差异,模型才学得动。
我最终用了六个类别,每个类别都有明确的视觉标志:
- 放松(relaxed):身体舒展,往往侧躺或俯卧,眼睛半闭,耳朵自然竖立或微微后转,尾巴缓慢摇摆或静止。
- 警觉(alert):耳朵直立向前,瞳孔放大,身体紧绷但不收缩,视线集中在某一点,尾巴大幅摆动。
- 愉快(happy):尾巴高高竖起且尖端微颤,身体蹭人,前爪交替踩踏,面部肌肉松弛,有时伴随眼睛眯成缝。
- 紧张(anxious):身体蜷缩压低,耳朵向侧面下压,瞳孔扩大,尾巴紧贴身体或夹在两腿间,频繁舔嘴唇。
- 愤怒(aggressive):炸毛,弓背,耳朵完全压成飞机耳,瞳孔缩小,张嘴露牙或发出哈气,尾巴快速拍打地面。
- 恐惧(fearful):身体试图躲藏,头部压低贴近地面,耳朵完全向后贴平,瞳孔异常放大,身体整体收缩成球状。
这六个类别的区分度并不是均匀的。放松和愉快之间经常混淆——猫在踩踏时身体姿态是放松的,但行为上已经表现出愉悦。我的处理原则是,只要一组典型行为占据主导,就标成对应类别;如果两种状态的标志行为同时出现且强度相当,比如放松侧躺但尾巴尖在颤动,就取那个更具体的行为状态。这里我建议宁可少标,也不要标一半一半的“混合态”——模型没有能力学出模糊边界,只会学会输出错误的置信度。
3.2 同一画面中的多猫与多状态处理
多猫家庭的场景,同一个画面里会出现几只猫,状态可能不一致:一只醒着、一只睡着,一只放松、一只紧张。YOLO允许一张图有多个边界框,每个框可以有独立的类别,所以不存在结构上的障碍。
实际操作中比较棘手的是两只猫挤在一起的情况——身体有重叠,边界框交叉,猫A的身体遮住了猫B的头。我的标注规则是:边界框只覆盖可见部分,被遮挡超过50%的目标不标。这样做会让边界框的类别特征弱一些,但总比标注错误的边界框好得多。
另一个容易被忽视的问题出在边界框的标注范围上。YOLO训练时,框内的特征会被池化提取,如果框得过大,把背景算进太多,模型学到的特征里会混入环境噪声;如果框得过小,只框住头部,那愤怒类别的炸毛特征就丢失了。我的经验是框住全身,但需要容纳全身粗细变化最大的区域。比如一只炸毛的猫,宽度比平时大三分之一,标注时一定要把炸开的毛圈进去,否则模型会误认为这是“胖”,而不是“愤怒”。
更细化的一个问题:同类别不同个体的体态差异极大。小猫的耳朵比例比成年猫大,长毛猫和短毛猫的轮廓线条也不同。我在标注规范里加了备注:以行为特征为主要判据,不要被品种外观带偏。一只耳朵贴平的长毛猫,和一只耳朵贴平的短毛猫,都要标成同一种情绪状态。
数据集中六个类别的分布如下表:
| 类别 | 图像数量 | 边界框数量 |
|---|---|---|
| 放松 | 980 | 1210 |
| 警觉 | 620 | 690 |
| 愉快 | 480 | 505 |
| 紧张 | 430 | 470 |
| 愤怒 | 370 | 385 |
| 恐惧 | 320 | 330 |
可以看到“放松”类别占了将近三分之一,其他类别相对平衡。这种分布不是完全均衡的,但我训练时没有直接做数据均衡,而是用了损失函数加权的方式。原因后面会提到。
4. YOLO标注实操:工具选择与质检流程
4.1 目录结构和坐标归一化
数据集的标注格式严格按照YOLO检测的标准:
datasets/cat_emotion/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── classes.txt每个图像文件对应一个同名的纯文本标注文件,放在labels目录下对应的子目录里。比如images/train/000123.jpg,对应labels/train/000123.txt。如果某张图没有任何标签,也就是没有含猫目标,对应的txt文件为空,但文件本身要保留,否则数据加载器会报错。
txt文件的每行格式为五个字段:
class_id x_center y_center width height- class_id:整数,从0开始,对应classes.txt中的行号
- x_center:边界框中心点的x坐标除以图像宽度,取0到1之间的浮点数
- y_center:同x_center,根据高度归一化
- width:边界框的宽度除以图像宽度
- height:边界框的高度除以图像高度
举个例子,一张1280像素宽、720像素高的图上,猫的边界框左上角在(300, 200),右下角在(800, 600),对应的归一化计算如下:
x_center = (300 + 800) / 2 / 1280 = 0.4297 y_center = (200 + 600) / 2 / 720 = 0.5556 width = (800 - 300) / 1280 = 0.3906 height = (600 - 200) / 720 = 0.5556这行就是0 0.4297 0.5556 0.3906 0.5556,类别0是放松。这个坐标系的坑在于:如果标注时不小心把x_center和y_center写反,或者忘了归一化,模型训练时的损失值会爆炸式增长,且很难排查。我推荐标注工具直接导出YOLO格式,不要手工写坐标。
我用的标注工具是LabelImg和CVAT组合。LabelImg适合单人小批量标注,界面简单,直接支持YOLO格式导出,快捷键D切换下一张图,W创建框,效率还算可以。但批量处理几百张图以上,LabelImg没有云端协作和冲突检测,团队协作时容易出乱子。后来转到CVAT,它支持多人同时标注、任务分配、标注结果审查,而且有自动标注辅助功能,可以先拿一个初版模型对未标注的图做预标注,人工只需要调整框的位置,效率至少提升一倍。
4.2 双人复核与标注纠错
标注环节单独一个人做,越到后面越手滑。我两个批次共整理了三个月的数据,中间出现过的错误大概有三类:
第一类是类别误判。最典型的是紧张和愤怒的混淆:一只猫耳朵后压、身体压低但没有炸毛,标注员标成紧张,实际上是已经发出哈声的愤怒前兆。这类错误不是纯粹手滑,而是类别边界本身的模糊性带来的。解决办法不是重新培训标注员,而是在标注规范里加了一条:存在哈气、露牙、炸毛这三种行为中的任意一种,一律标愤怒。
第二类是边界框偏移。猫在快速转头时,标注框往往会框住头部加一部分空气,漏掉脖子以下的身体。这会导致训练时“愤怒炸毛”特征被截断,模型学到一半特征。CVAT的“按目标追踪”功能可以帮一点忙,但最终质检还是要人眼一格一格看。
第三类是文件格式错误。我们有一批图是从视频抽帧工具导出的,图像尺寸是1920x1080,但标注工具读取时被压缩成了960x540。在压缩图上标注的框坐标归一化后没问题——因为归一化是相对于图像尺寸的——但导出后如果图像路径对不上,训练脚本按原图尺寸加载,框的位置不会错,但比例信息是准的。这个坑发生在标注工具缓存了旧尺寸的情况下。质检脚本要多加一道校验,读取每张图像的实际宽高,重新核算标注框是否在合法范围内。
质检流程我按三步走:
- 脚本先生成一个叠加边界框的预览图,每张生成后随机抽样10%人工检查,重点关注框是否贴合目标、是否漏标、有没有类别错标。
- 跑一遍yolo格式的合法性校验,检查每个txt文件的行数、字段数量是否都为5、坐标是否在[0,1]区间内。
- 用初版权重对全量数据做一次推理,把模型输出和人工标注不一致的样本拉出来给人看——不一定是标注错,可能是模型错,但差异大的样本一定是需要关注的。
这个流程走完,3200张图大概又修正了150多个标注框,最终才进入正式训练。标注这步偷的懒,后面训练时全都会加倍还回来。
5. 用这份数据集训练YOLOv8的实战记录
5.1 训练环境与参数设定
硬件上我用的是一张RTX 3090,显存24GB,这个规模跑YOLOv8s完全没有压力。CPU是AMD Ryzen 9,内存32GB,数据集整体加载进内存绰绰有余。如果用V100或者更小的卡,把batch size调小也可以跑,只是慢一些。
训练脚本基于ultralytics的YOLOv8s模型。选择s版本而不是m或l,是因为情绪检测的应用场景基本是实时摄像头,FPS要稳定在30帧以上,m和l在CPU或低端GPU上很难做到。精度上s版本比m低一些,但在单目标检测任务上差距没有想象中那么大,毕竟我们只是检测“猫”,不是检测80个类别。
以下是训练配置的关键参数:
最大输入尺寸640。虽然YOLOv8支持多尺度训练,数据集里的原始图尺寸基本都大于640,比如1080p或720p,训练时会被缩放。640是精炼速度和质量后的常用折中。如果你的显卡足够强,可以试试640以上的输入,比如832,边界框会稍微更精细,但显存占用会上升不少。 batch size设为16。3090跑640分辨率,16这个值不会爆显存,而且BN层的统计量足够稳。 epoch数设为80,配合早停。我用patience=20——如果连续20个epoch验证集mAP没有上涨,就提前结束。实际训练到第61个epoch时早停触发了,验证集mAP50稳定在0.78左右。 数据划分按6:2:2的比例,也就是train 1920张、val 640张、test 640张。
提示:验证集和测试集必须从不同来源的视频序列中抽取,不能简单随机乱分。如果测试集里混入了和训练集同一时间段拍摄的监控帧,模型“背下来”的痕迹会被当成有效学习成果,验证结果会虚高10个百分点以上。我做划分时以“拍摄片段”为最小单位,同一个片段里的帧全部放进同一份数据集,避免帧级数据泄漏。
5.2 训练结果与逐类指标分析
训练完成后,最关心的自然是每类情绪能不能正确识别。我在测试集上的指标如下(数据集随机划分,不同种子略有波动,但方向一致):
| 类别 | 精确率Precision | 召回率Recall | AP50 | AP50-95 |
|---|---|---|---|---|
| 放松 | 0.87 | 0.91 | 0.91 | 0.62 |
| 警觉 | 0.74 | 0.80 | 0.78 | 0.49 |
| 愉快 | 0.68 | 0.71 | 0.72 | 0.42 |
| 紧张 | 0.70 | 0.66 | 0.69 | 0.43 |
| 愤怒 | 0.82 | 0.77 | 0.82 | 0.55 |
| 恐惧 | 0.75 | 0.69 | 0.73 | 0.46 |
整体mAP50约0.78,mAP50-95约0.51。这个结果作为基准线是能用的,但细看逐类指标,问题集中在愉快、紧张、恐惧三类的召回率上。一个共性原因是:这三类状态的典型特征持续时间短,猫可能在一两秒内从紧张转成防御甚至攻击,标注时如果抽帧时机没有捕捉到峰值状态,标签就会偏弱。
比较典型的一个失败案例:画面的猫正对着镜头啃猫抓板,尾巴轻轻摆动,我标成了“愉快”。模型实际预测为“放松”,置信度0.85。这其实是合理的——啃咬的动作模式在YOLO看来更接近静态行为,除非把“尾巴竖直+踩踏”这种动态特征清晰拍进画面,否则模型学不出反差。
6. 训练中的典型翻车现场与调优经验
6.1 类别不平衡的损失函数加权
看到前面那张数据分布表,可能有读者会说:放松类占比接近三分之一,要不要随机下采样,让六个类别的框数量接近?我试过,效果不好。原因在于放松状态本身的“形态特征”波动巨大——有侧躺、俯卧、蜷缩、舒展,身体姿态差异不亚于跨类别差异。如果把放松类的样本数量硬压下来,模型对放松类各种姿态的判别边界就会退化,实际应用中出现误报的频率反而增加。
我最终在损失函数里给“紧张”“恐惧”“愤怒”三个类别加了系数,做法是在ultralytics的配置里传入每个类别的权重向量,让低样本量的类别梯度贡献更大。具体数值:“放松”1.0,“警觉”1.0,“愉快”1.1,“紧张”1.3,“愤怒”1.2,“恐惧”1.4。这是相对黑盒的调优方式,效果上恐惧类AP50提升约6个百分点,但并没有解决根本问题——最终还是要补数据的。如果你的项目预算允许,这个方向值得投入更多。
6.2 相似行为类别的人工规则兜底
YOLO的可解释性差,它内部不会告诉你“这只猫为什么被判定为紧张”——它只会输出一个置信度。如果置信度比较低,比如0.5以下,人根本不知道模型的依据是什么。我在部署阶段加了一个简单的后处理规则:检测结果在紧张、恐惧、愤怒三类置信度都不超过0.4时,默认归入“警觉”类别。理由很简单:非常刻板的“警觉”特征——耳朵竖起、瞳孔较大、身体紧绷——是这三种负向情绪的共同前兆。模型对负向情绪的区分不完备,但至少对“非放松”的识别还是稳定的。用保守策略宁可错报“紧张”,也不要漏报真正的攻击前兆。
6.3 从单帧检测到时序预测的延伸
上面所有结论都基于单帧静态推理。实际部署时,我发现单帧预测的稳定性不够——同一只猫在同一段监控里连续出现10秒,每隔0.5秒抽一帧,帧与帧之间的预测类别会在“放松”和“警觉”之间跳来跳去。这不能完全怪模型,猫本身的情绪状态就是实时波动的,加上抽帧间隔期间姿态连续变化,单帧的瞬时状态只能表示那一刻的行为,不能代表一段时间的整体情绪。
解决的思路有两种。第一种是滑动窗口多数投票:取最近30帧的检测结果,统计每一类的出现频次,取频次最高且置信度超过阈值的类别作为当前情绪状态。这种做法实现简单,10行代码就够,但会引入几个帧的延迟,对实时交互场景有影响。第二种是引入视频行为识别模型,比如用SlowFast或TimeSformer,对连续帧序列建模,利用时序特征判别情绪走向。这种方法精度上限更高,但需要额外的GPU资源,而且标注成本直接翻倍——时序模型训练需要的是标注到帧的连续事件时间段,我的数据集目前还不足以支撑这个方向。
如果后面继续扩展,我更倾向于先给现有数据集补充音频特征。猫的哈气声、呼噜声、嚎叫声都是非常强烈的情绪信号,和视觉特征互补性极强。到时候数据集就需要做多模态对齐了,不只标注框,还要标注声音事件的时间戳。这是一个完整的系统工程,但和纯视觉方案相比,它能解决“画面静止但情绪波动”的核心盲区。
我个人的感受是,这份3200张的数据集在单帧静态检测上已经具备实用价值,但真正的落地场景几乎没有纯静态的。摄像头方案加一个时序投票模块,准确率立刻能起来一两个点。如果你也在做类似的项目,我建议从一开始就把时序信息考虑进标注规范里,不然等模型库训练完后想回补,成本是现在的三倍还不止。