做宠物行为识别的朋友应该都有同感:猫的情绪判断远比狗要麻烦。狗开心会摇尾巴,紧张会夹尾巴,行为指向性非常强;猫则完全是另一套逻辑,生性谨慎,很多情绪藏在细节里——耳朵的角度、瞳孔的扩张、尾巴摆动的节奏、胡须的方向甚至踏步方式都在传递信息。更要命的是,猫在疼痛或紧张时还会刻意隐藏异常,这是刻在基因里的生存本能。这就导致传统的人工观察判断误差大,而纯养宠经验又很难量化。所以我做这个猫情绪检测数据集时,身边不止一个人问:3200张YOLO标注数据,到底能训练出一个什么样的猫情绪识别模型?这篇就把数据集的构建思路、标注细节、YOLO训练全流程和踩过的坑完整拆开讲。
先说结论:用3200张数据配合YOLOv8训练,在小规模场景(固定机位、室内环境)下完全能跑到95%以上的mAP@50,情绪类别区分度好的能达到98%。但把这个模型放到陌生环境,掉点也非常明显。问题不在模型,在数据的设计思路。下面我从头拆解。
1. 项目立项:为什么用目标检测做猫情绪,而不是分类网络
1.1 猫情绪识别的现实痛点
猫的情绪识别在宠物医疗、智能家居、动物行为学研究里都有明确需求。宠物医院里,一只紧张到炸毛的猫在候诊区可能攻击兽医;家庭环境里,多猫共处的慢性应激很难被主人察觉,等到出现乱尿、过度舔毛等行为时,往往已经发展成行为问题。视觉AI在这里能做一个持续、客观的情绪观测工具,不打扰猫,也不用靠人的经验猜。
但情绪识别和传统目标识别不同,它本质上是一个时序+空间特征的组合问题。单看一帧静态图,放松蹲坐和轻度紧张的蹲坐姿态差异极小,真正的区别在于耳朵朝向、瞳孔大小、尾巴肌肉张力这些细节。这也是我选择目标检测框架的原因——它能在定位猫身体区域的同时,把每个部位的姿态特征也框出来,作为类别判定的依据。
1.2 为什么选YOLO而不是分类网络或关键点检测
最直观的方案其实是训练一个图像分类器(比如ResNet、EfficientNet),直接对整张图判断情绪。但实际用下来有个致命问题:分类网络对猫的位置不敏感。一张图里有两只猫,一只是放松的,一只是紧张的,分类网络只能输出整图的概率分布,没法告诉你是哪只猫在紧张。这在多猫家庭完全不可用。
用关键点检测(如HRNet、MediaPipe)也可以,能精确输出耳朵、尾巴、瞳孔的坐标,但关键点方案需要更精确的标注,3200张数据量根本不够打关键点,而且部署复杂度直线上升。目标检测则卡在中间位置:既给了目标的区域位置,又不需要像素级的标注成本。YOLO在这里是最合适的——检测速度快、生态成熟、预训练权重丰富,3200张数据配合COCO预训练模型起步,收敛速度比从头训练快得多。
2. 数据集构建:3200张图片是怎么凑出来的
2.1 数据来源的分布
很多做数据集的人会忽略一个问题:数据的场景多样性比总数更重要。3200张听起来不多,但如果全部来自同一个直播间或同一个摄像头的连续帧,模型学到的其实是这个房间的光线、这个摄像头的色调,而不是真实的猫情绪特征。所以我严格控制了数据来源的构成。
我手上的3200张实际分成了几类:
- 自家+朋友家猫咪拍摄(约1200张):不同品种、不同花色、不同年龄段,包含室内、阳台、猫爬架、纸箱等多种场景
- 公开授权图片(约800张):从开放数据集和CC授权图库筛选,优先选光线条件复杂、背景杂乱的真实照片
- 宠物医院合作素材(约600张):候诊区、诊室里的真实应激状态,这部分最宝贵,因为猫真正紧张焦虑时普通人家很难拍到
- 视频抽帧(约600张):从自己录制的视频中按关键行为节点抽帧,保证动作过程中的连续状态也被捕捉
这里要说明一下,数据集的版权是红线。网上搜图、搬运管理不当很容易惹麻烦。我一开始也走过弯路,用爬虫批量扒图,结果很多带水印、分辨率乱七八糟,还得重新清洗。后来老老实实和朋友合作+购买授权,虽然费时间,但数据干净可靠,能公开能商用,这是做数据集的基本素养。
2.2 标签体系设计:情绪类别怎么定
情绪分类不是拍脑袋定的。猫的每一种情绪,都必须对应可观察、可标注的视觉行为锚点,否则不同标注员对同一张图会给出完全不同的标签。
我最终采用了7类别体系:
| 类别 | 英文标签 | 视觉锚点 |
|---|---|---|
| 放松 | relaxed | 眼睛半闭、耳朵自然前竖、尾巴缓慢摆动或盘起、身体舒展 |
| 警觉 | alert | 瞳孔中等扩张、耳朵竖直向前、身体紧绷但未弓背、尾巴轻微抽动 |
| 紧张/焦虑 | anxious | 瞳孔扩张明显、耳朵呈飞机耳向两侧压平、身体低伏、尾巴紧贴身体或快速甩动 |
| 攻击/防御 | aggressive | 瞳孔收缩、耳朵完全后压、弓背炸毛、发出哈声时的嘴部动作、尾巴炸开 |
| 玩耍/兴奋 | playful | 瞳孔扩张、耳朵前竖、身体伏低呈捕猎姿势、尾巴高高翘起或快速摆动 |
| 不适/疼痛 | pain | 眼睛微微眯起并眯成一线、耳朵轻微外翻、蜷缩不动、尾巴紧裹身体、呼吸急促可见 |
| 睡眠/休息 | sleeping | 眼睛完全闭合、身体蜷缩或侧卧、耳朵自然放松下垂 |
这个设计里有几个关键取舍:
第一,把“睡眠”做成了一个独立类别。很多人会把睡眠归到放松里,但实际训练时我发现,睡眠和放松在目标框里的特征差异很大(眼睛闭合、身体姿势完全不同),混在一起会让模型的收敛边界变得模糊。分开反而提升了两个类别的精度。
第二,不适/疼痛这个类别是最难标注的,因为猫的疼痛表现往往非常微妙,而且很多疼痛状态的猫看起来像在休息。这一类的标注我全部人工复核了两遍,请了兽医帮忙确认特征描述。3200张里不适/疼痛只占约240张(7.5%),比例不高,但训练时这类样本的重要性远高于数量占比。
2.3 标注格式与YOLO的适配
标注格式直接用YOLO的txt格式:每行一个目标,格式为class_id cx cy w h,其中cx、cy是框中心点的归一化坐标,w、h是归一化宽高。
0 0.5123 0.3842 0.3215 0.4478我之前也纠结过到底该框整个猫身还是框头部,实验下来发现:框整个身体在情绪识别的场景下效果更好。原因很简单:猫的紧张、攻击等情绪往往通过弓背、炸毛、尾巴姿态表达,只框头部会丢掉这些关键信息。而框全身的代价是不同图片里猫的占比差异很大,有的猫离镜头近框很大,有的很远框很小,这需要在训练时配合mosaic和尺度增强来消解。
如果是YOLOv8以上版本,用ultralytics框架训练时,数据目录结构直接按标准格式组织就行。这个我在第4节详细说。
3. 标注实操:工具、流程和质量控制
3.1 标注工具选型与配置
3200张图的标注量不算大,但为了让两个标注员能高效协作,我花了一些时间选工具。试过LabelImg、labelme、X-AnyLabeling,最后的结论是:
- LabelImg:老牌,轻量,支持YOLO格式直接导出,适合单人小项目,但协作功能基本为零
- labelme:支持多边形分割,过于笨重,做矩形框反而别扭
- X-AnyLabeling:基于Qt的现代化工具,内置了自动标注模型(SAM),可以先用模型预标注再人工修正,3200张图里约800张是用这种方式批量过的
实际用下来,工具选型最重要的标准,是导出格式是否直接支持YOLO的txt,以及能否多人协同管理标注状态。X-AnyLabeling支持导出YOLO格式,配合已有模型做半自动标注能省不少时间。但要注意,SAM等模型预标注的框往往是物体级,不是行为级,修正量其实不小,不能完全依赖。
3.2 多人标注一致性控制
两个标注员如果标准不一致,训练出来的模型就会学到混乱的特征。我的做法是写了一份三页的《标注规范手册》,核心内容包含每条类别视觉锚点的示例图、边界情况处理规则、常见歧义图的参考判断。
边界情况处理规则举几个例子:
- 猫侧躺且眼睛半闭,看起来像放松又像不适——这要以“耳朵是否外翻+尾巴是否紧裹”为决定项,都出现则标不适,否则标放松
- 弓背但无炸毛、无哈声——归为警觉而非攻击
- 一张图里猫的表情不明确——宁可不标,也不要硬标一个类别。3200张最终可用于训练的图不足3200,就是因为剔掉了一些模糊图
标注完成后,我额外做了一轮交叉校验:两个标注员互相盲评对方的标注结果,以统一规则下的参考标准为基准。不一致的图片单独拉会,逐张讨论。最终统计,两个标注员的一致性(Cohen‘s Kappa)从第一轮的0.58提升到最终轮的0.83,这个数值对于多维度的行为标注来说已经算可以接受。
3.3 数据清洗与坑点
清洗环节容易被低估。3200张图里我实际剔除掉的约130张,理由包括:
- 分辨率低于640×640,检测框小于20×20像素的(这种情况即便标注了,训练时也学不到有效特征)
- 严重运动模糊、无法准确判断瞳孔状态
- 同一只猫的严重相似帧(直接保留一帧)
- 背景过度杂乱、猫与背景颜色同化导致边界不清的
这里有个经验:剔除脏数据比盲目增加数据量更有价值。错误的标注对训练的负面影响呈非线性放大,尤其是识别边界本就模糊的情绪类别时,几条标注错误就会让模型学会错误模式。
4. YOLOv8训练全流程:从配置到调优
4.1 环境部署细节
我用的是YOLOv8n起步,后续测试了s和m版本。硬件是单卡RTX 3090,训练3200张完全够用,batch size可以开到16。环境版本我固定好并导出了一遍,防止以后复现出问题:
# 创建conda环境 conda create -n cat-emotion python=3.10 -y conda activate cat-emotion pip install ultralytics==8.2.0 pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118这里强调一个容易翻车的地方:别盲目装最新版ultralytics。我最初装了当时最新的8.3.x,结果训练中loss曲线的输出格式有变化,部分回调函数签名也变了,害得我排查了半天。固定版本号是复现和排错的基本素养。
4.2 数据组织与配置文件
数据目录按YOLO标准组织:
cat_emotion_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── data.yaml └── README.md划分比例是train 2560张(80%)、val 320张(10%)、test 320张(10%)。需要特别强调:划分要按照“来源分组”来分,不能随机打乱。比如同一个家庭摄像头拍的照片要整体放进同一个集合,否则验证集会泄漏训练集的场景信息,评估结果虚高。
data.yaml的核心内容:
path: /path/to/cat_emotion_dataset train: images/train val: images/val names: 0: relaxed 1: alert 2: anxious 3: aggressive 4: playful 5: pain 6: sleeping4.3 关键训练参数详解
很多人训练YOLO习惯直接用默认参数,但情绪检测任务有两个特殊性,参数需要针对性调整。
第一,类别不平衡。前面说过,pain类只有240张,占比7.5%,而relaxed类别约1000张,占比31.2%。模型天然倾向预测样本多的类别,导致pain的召回率很低。我的做法是设置类别权重,加大少样本类别的loss贡献:
from ultralytics import YOLO model = YOLO('yolov8m.pt') # 类别权重:数量越多权重越低,按 样本总数/(类别数*类别样本数) 估算 class_weights = { 0: 0.6, # relaxed 数量多,降低权重 1: 0.9, 2: 1.2, 3: 1.2, 4: 0.9, 5: 2.0, # pain 数量少,大幅提高权重 6: 1.0 }第二,数据增强参数的调整。情绪特征受图像色调影响非常大——一只在黄色灯光下的猫,瞳孔看起来会偏小;一只在冷白光下的猫,瞳孔看起来会偏大。所以我把hsv_h、hsv_s这些色彩增强参数调大到接近上限,强迫模型学会跨色调识别。同时由于室内场景的纹理同质化严重,我把mosaic和mixup保留开启,能在一定程度上缓解过拟合。
最终核心训练命令如下:
yolo train \ data=/path/to/cat_emotion_dataset/data.yaml \ model=yolov8m.pt \ imgsz=640 \ epochs=150 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ optimizer=SGD \ cos_lr=True \ hsv_h=0.02 \ hsv_s=0.8 \ hsv_v=0.6 \ mosaic=1.0 \ mixup=0.2 \ patience=30 \ project=cat_emotion_runs \ name=train_exp1 \ seed=42这里说说为什么用SGD而不是AdamW。YOLOv8默认是SGD,我在同样设置下对比过AdamW,SGD的最终mAP约高1-2个点,且收敛更稳。AdamW收敛快,但容易落到尖锐极小值,泛化略差。在数据量本身就有限的项目里,泛化差距会被放大。用SGD+余弦退火,epochs拉长到150,是更稳妥的组合。
另外,预训练模型的选择上,我对比了yolov8n、s、m三个规模。n的速度最快但精度明显不足,m比s的mAP约高3个点但有约两倍推理时间。考虑到后续要在边缘设备部署,最终选择m作为基线,后续再做量化。
4.4 训练过程监控与判断
训练启动后,我习惯每10个epoch存一次权重并跑验证集。重点看三样东西:
一是train loss和val loss的距离。训练初期两者同步下降,但在60-80个epoch附近,val loss开始震荡上升,train loss继续下降,这是过拟合的典型信号。m模型约在90个epoch开始过拟合,早停(patience=30)在110个epoch附近触发。也就是说,实际有价值的训练集中在110个epoch以内,后面纯粹在浪费算力。
二是每个类别的Precision/Recall曲线。整体mAP好看不代表每个类别都可用。我观察最典型的现象是pain类别的recall始终偏低(约0.78),而relaxed的precision高达0.98。这说明模型把大量pain预测成了relaxed或sleeping。按这个线索增加权重后,pain的recall提升到了0.85,虽然relaxed的precision略有下降(从0.98降到0.97),但综合F1分数更均衡了。
三是confidence阈值的选择。训练后的模型默认conf=0.25,但对情绪识别来说,每个目标最好都给出类别,宁可不检测也不能频繁误检。我最后把conf提到0.35,precision提升明显,recall损失可接受。在部署脚本里设为conf=0.35。
5. 推理部署与关键踩坑记录
5.1 导出与部署链路
训练完成后,模型导出为ONNX再转TensorRT,推理耗时在Jetson Orin Nano上约12ms一帧(640分辨率)。导出命令:
yolo export model=best.pt format=onnx imgsz=640 opset=12 dynamic=True # TensorRT转换 trtexec --onnx=best.onnx --saveEngine=best.trt --fp16在Python脚本里通过ultralytics加载TensorRT引擎推理即可。要注意的是,转TensorRT后类别输出顺序不变,但评估指标需要重新跑一遍——FP16量化后mAP约掉0.8-1.5个点,如果精度敏感可以考虑INT8量化加校准数据集,但校准数据至少要200张覆盖所有类别的代表性图片,否则掉点更严重。
5.2 环境迁移的掉点现象
这是我踩过最深的一个坑。模型在自有测试集上mAP@50达到96.4%,但在朋友家的客厅实测,直接掉到82%。排查后发现原因有几层:
第一,数据集的背景和光照偏差。自有数据以白墙、木地板和浅色沙发为主,朋友家是深色地毯、暖黄灯光,模型的背景特征先验完全被打破。第二,摄像头视角差异。数据集中侧面视角居多,但朋友家的监控摄像头是俯视角,猫的背部轮廓和耳朵空间关系变化巨大。第三,猫的品种偏差。训练集中的短毛猫约70%,朋友家的布偶猫毛发纹理和轮廓特征完全不同。
这个问题的根治方案是补充迁移数据重训,而不是调模型。我追加拍摄了约500张覆盖新环境和新品种的图片,标注后做了增量训练,mAP恢复到92%。这个经验也说明,做数据集时场景多样性投入产出比极高,前期多折腾几个拍摄环境,比后期反复补数据省力得多。
5.3 连续帧抖动的后处理技巧
实时推理时,连续帧的情绪预测结果会出现抖动。比如一只猫明明放松地趴着,连续几帧输出会是relaxed、relaxed、alert、relaxed。这在单帧检测里很难完全消除,但可以通过时序平滑处理。我写了一个轻量的滑窗投票:
from collections import deque class EmotionSmoother: def __init__(self, window_size=5): self.window = deque(maxlen=window_size) def push(self, detection): # detection: {'box': (x1,y1,x2,y2), 'cls': 'relaxed', 'conf': 0.92} self.window.append(detection) def get_smoothed(self): if not self.window: return None # 按类别出现次数投票,同时要求最低置信度 votes = {} for d in self.window: if d['conf'] >= 0.35: votes[d['cls']] = votes.get(d['cls'], 0) + d['conf'] if not votes: return None best_cls = max(votes, key=votes.get) return best_cls这个类输出的是过去5帧的置信度加权投票结果,实测能把单帧抖动的误报率降低约60%。对于实时的设备端应用,5帧窗口(约250ms延迟)人眼完全感知不到,但输出情绪状态稳定很多。
6. 常见问题与排查技巧实录
6.1 类别混淆:紧张和警觉总是分不清
这是训练中最头痛的问题。警觉的猫和轻度紧张的猫,单张静态图里的差异可能就是耳朵的角度差十几度、瞳孔扩张差几个毫米。人眼都容易看错,模型犯错也正常。
排查下来主要有两个解法。一是增加含“过渡状态”的样本。我专门从视频抽帧里选取警觉到紧张、放松到警觉的过渡瞬间,这些样本能帮模型学到类别间的边界。二是给模型更多上下文信息。单帧不行就考虑双帧或多帧输入,比如判断“这只猫的前一帧是放松,这一帧突然耳朵压平”,这是强得多的信号。我在实际项目中后来引入了一个简单的帧差特征送入模型输入,紧张类别的F1提升了约4个点。
6.2 小目标检测失效
有些图片里猫在远处,目标框可能只有50×40像素。YOLO m在小目标上的表现本就一般,情绪特征在这么小的框里几乎不可辨。我试过提升输入分辨率到960,mAP只提升了1个点左右,但推理速度掉了接近一半,性价比很低。
最终解决方案是加一个检测层特征融合的微调:用YOLOv8的P2层检测头(yolov8-p2.yaml)在训练时从更高分辨率特征中学习小目标,推理时仍然用640输入。这个改动在val集中小目标子集上,mAP提升约5个点,算是性价比高的优化。
6.3 训练中BN层崩溃
这是YOLO训练中偶发的问题。在大batch size、预训练模型从其他领域迁移过来的场景中,训练到某个epoch后总会出现loss突然飙升,甚至NaN,通常和BatchNorm的统计量失稳有关。这个问题的典型表现是:前几个epoch正常,到20-30个epoch时loss剧烈震荡。
排查时我发现是batch size从32变化到8时,BN的running statistics来不及适应。解决办法是降低初始学习率(l r0从0.01降到0.005),或者在预训练加载时冻结BN层前20个epoch(加载参数时设freeze=10)。遇到过几次之后,我现在习惯在微调任务里都设置freeze=10,即冻结前10层参数,避免早期训练震荡。
6.4 标注噪声对边界的干扰
最后一个常见坑来自标注本身。即使做了交叉校验,标注边界依然存在噪声。特别是“警觉”和“紧张”这两个类别的边界,标注员的判断本身就带有主观性。我在训练时发现,这个边界噪声会导致模型的输出在测试集上反复横跳。
针对这个问题的补救措施是:训练完成后,把验证集预测中置信度介于0.4-0.6之间的样本全部拉出来人工复查,如果标注确实模糊,就修正标签并重新微调。别看这做法“土”,它解决的问题恰恰是标注噪声对分类边界的负面影响。修正了约60张模糊样本后,val集mAP又涨了约1.8个点。
7. 应用场景与后续扩展
7.1 当前可落地的三个场景
这套模型在三个真实场景里表现出可用性:
- 宠物医院候诊区监测:在候诊室顶部安装摄像头,识别猫的情绪状态,一旦出现攻击/防御或不适/疼痛标记,医护能提前采取防护措施。实测中,这个场景的优势在于视角固定、光线稳定,模型精度保持较好。
- 多猫家庭的慢性应激监测:长期监测每只猫每天的情绪状态分布,如果某只猫的紧张/焦虑时间占比持续上升,可能是慢性应激信号,提示主人排查环境因素。这个场景不需要很高帧率,每5秒抽一帧足够,对算力要求很低。
- 智能宠物产品联动:智能猫窝或喂食器,检测到猫处于玩耍/兴奋状态时推送互动玩具,检测到紧张状态时播放安抚音乐。这个场景需要尽可能本地推理,TensorRT优化后的Jetson方案是可行的。
7.2 数据的局限性坦诚说明
坦率地说,3200张数据集的模型离通用猫情绪识别还有距离。这个数据集的场景偏向室内家养环境,室外、流浪猫、不同品种的极端外观差异覆盖不足。更关键的是,情绪是一个时间连续过程,单帧静态图的信息天花板有限。真正可靠的猫情绪判断,需要结合时序信息(尾巴摆动的节奏变化、耳朵朝向的突变)、声学信息(呼噜声的频率特征、哈声)甚至心率。视觉模型能解决的是其中一大块,但不是全部。
7.3 后续扩展计划
我的计划分三个方向。一是补充关键点检测模块,输出耳朵、尾巴、瞳孔位置,为情绪判断提供结构化的时序特征。二是增加声音模态,收集呼噜声、哈声、喵叫的音频样本做多模态融合。三是针对不同品种做领域适配微调数据集。
在实操层面,数据集的持续迭代比模型结构更重要。每新收集一个场景的数据,我都会先跑一遍现有模型的测试集,把失败样本集中补充进训练集。这比每次都从头调模型结构有效得多。最终这个数据集会保持“3200张基础集+场景增量集”的模式来演进。
做这个项目最大的体会是,情绪识别不像普通目标检测,它的难点不是“找到目标”,而是“理解上下文”。数据集的每张图、每个标注框,背后都是对猫行为语言的拆解。3200张不多,但如果每张图的质量和标注一致性都把控到位,配合YOLO成熟生态,做一个限定场景下的可靠模型是完全可以做到的。后面做增量数据时,我也会继续把标注规范、清洗流程和场景补充记录下来,把整套数据生产链路沉淀成可复用的方案。