ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

猫情绪检测实战:YOLO行为标注与数据集制作全流程

猫情绪检测实战:YOLO行为标注与数据集制作全流程 1. 项目定位为什么是“行为检测 情绪推断”而不是直接标情绪1.1 核心需求解析这个项目的标题是“猫情绪检测数据集”3200张YOLO宠物行为数据集。我第一次拿到这个需求时第一反应不是“找猫图、画框、训练”而是先问一个问题情绪到底能不能作为目标检测的标签直接标注答案是不能。情绪是内在状态没有物理边界你没法画一个框说“这块区域是开心”。但行为信号是可以标注的——耳朵是前倾还是后压、眼睛是圆睁还是眯起、尾巴是竖直还是夹紧、嘴是闭合还是龇牙。这些是视觉上客观存在、可框选、可分类的。所以真正合理的做法是目标检测只负责输出“猫在什么部位、表现出什么行为”情绪状态由行为组合在上层规则里推断出来。整套系统分两层第一层是YOLO检测器识别耳朵、眼睛、尾巴、嘴等局部行为标签第二层是规则或轻量分类器根据同一条猫在某一帧里同时出现的行为信号映射到猫咪当前的应激状态——放松、警惕、紧张、防御等。数据集是为第一层服务的但它决定了第二层的上限。这类项目的应用场景很广宠物摄像头在主人离家时监测猫咪状态、智能猫窝根据应激程度调整环境、猫咖或宠物店的实时行为告警、甚至兽医对慢性疼痛伴随行为变化的辅助观察。核心价值不在“识别情绪”这个口号而在“把可观测的行为信号变成结构化数据”。1.2 从“情绪”到“可标注行为”的转换实操中最容易踩的坑就是标注人员直接按主观情绪打标签。情绪标签的主观性太强不同标注员对“开心”的判断差异巨大模型学到的是标注员的个人偏好不是猫的真实状态。我做标签体系的时候把情绪拆成了12个行为类别全部是可观察、可定义、边界清晰的中性行为耳部状态ear_forward耳朵前倾、ear_flat耳朵平贴、ear_backed耳朵后压眼部状态eye_open正常睁眼、eye_squinted眯眼嘴部状态mouth_open张嘴可能包含喘息或发声、teeth_bared龇牙暴露牙齿尾部动作tail_up尾巴竖直通常伴随友好或警觉、tail_tucked夹尾通常伴随恐惧、tail_lashing尾巴快速甩动通常伴随烦躁身体姿态body_crouch身体蜷缩压低、body_relaxed身体舒展放松这里有一个关键原则只标注行为不标注情绪。比如“夹尾”不等于“害怕”它只是恐惧的强信号之一但尾巴夹紧也可能出现在疼痛或极端寒冷场景下。模型的职责是把行为检测准推情绪是上层规则的事。如果强行在数据集里标“害怕”遇到一只打着石膏夹着尾巴的猫你的训练样本就会打架。1.3 为什么选YOLO架构与3200张的规模设定YOLO是这场景下最合适的目标检测架构没有之一。宠物行为检测属于典型的中小型目标检测任务目标类别不多12类但要求推理速度快、部署门槛低。Transformer类检测器比如DETR精度上限高但训练要吃更多数据推理延迟也高放到家庭摄像头或嵌入式设备上不现实。我之前试过用改进的Transformer头和Mamba结构做实验精度有小幅提升但换来的是部署复杂度和显存压力的明显上升对宠物监测这种成本敏感的产品形态来说不划算。YOLOv8和YOLO11是这个阶段最稳的选择。它们的Head里用了DFLDistribution Focal Loss和CIoU损失对小目标、模糊边缘的框回归更友好训练稳定性和收敛速度都优于v5系列。这类行为检测里耳朵和眼睛经常只有十几到几十像素大小框回归不稳会直接造成行为误判所以损失函数的设计比模型本身的深度更关键。3200张的规模是怎么定的我当时的估算逻辑是12个类别每类至少需要150到300个有效实例才能让分类头学到稳定的特征总共约2400到3600个实例按平均每张图4到5个实例折算对应约600到900张完整标注图但如果只做600到900张任何标注噪声都会被成倍放大没有冗余余量考虑到验证集和测试集要完全独立、不能和训练集同源我把总量定到了3200张按约8:1:1划分训练集约2560张验证集和测试集各约320张最终3200张图里标注实例数大约在13500个左右平均每张4.2个框。对于目标检测数据集来说这个规模不算大但足够稳定训练出一个可用模型前提是标注质量必须可控。2. 数据采集与标注全流程实操2.1 拍摄环境与素材收集策略数据采集的策略决定了模型的上限很多人忽略这一点。我见过太多项目买了现成数据集测试mAP很高一上真实场景mAP掉一半——根因就是训练数据的分布和部署场景差异太大。这个数据集的素材采集分了三条线第一条线是室内家庭环境。我用一台微单相机和三台手机在不同家庭录制猫咪日常包含客厅、卧室、厨房、阳台等不同场景。录制时使用连拍和视频抽帧结合视频抽帧能保证同一姿态的连续性连拍能捕捉瞬时行为。第二条线是猫咖和宠物店。这类场景的猫经常处于陌生环境和多猫共存状态能采集到更多紧张、防御、夹尾、龇牙的样本这些是家庭环境里很难碰到的稀缺行为。第三条线是公开数据库和网络素材补充主要补齐极端姿态和罕见角度。采集时必须覆盖的是光线、机位、距离的多维度变化。我试过只用顺光照片训练结果一到夜间摄像头和逆光场景就全线崩溃。现在我会专门保留20%的夜间红外、15%的强背光、10%的运动模糊样本计入训练集之后再缺什么补什么。一个非常重要的经验同一只猫的同一段视频抽出来的帧不能同时出现在训练集和验证集里。猫咪个体的毛色、花纹、习惯姿势具有很强的辨识特征模型如果记住了这只猫就会在验证集上得到一个虚高的mAP换一只猫就原形毕露。所以我在划分数据时先按“采集会话”分组再以组为单位分割而不是简单按文件随机切。2.2 标签体系与标注规则设计前面提到的12类标签是对的但只列类别是不够的必须配合一套标注规则书否则多人协作时一定会标准漂移。我这套规则的核心是“目标单位”的划分。猫行为检测不建议整只猫打一个框因为一只猫同时会有多个行为信号耳朵前倾、眼睛睁大、尾巴竖直可能同时存在。如果整猫一个框类别就只能取一个信息大量丢失。所以我把检测目标拆成局部区域头部框负责耳部状态和眼部状态两个维度的标签写入。头部框可以画大一些包含整个头颈因为耳朵位置和眼睛位置都在头部框的语义范围内躯干框负责身体姿态维度从肩到髋部的区域尾部框负责尾部动作维度尾巴包含尾根到尾尖嘴部框单独框选口鼻区域只用于mouth_open和teeth_bared两个标签标注规则里还有一些硬性约定被遮挡超过50%的局部区域不标宁可缺标也不标半框模糊到看不清耳廓或瞳孔轮廓的图直接丢进废弃区不强行标注同一局部区域在同一帧里只标一个行为状态以最明显的状态为准运动模糊导致的框偏移手工修正到视觉中心位置这里有个细节耳朵和眼睛的状态经常会在不同部位出现矛盾比如耳朵前倾但眼睛眯起。这种矛盾不是错误恰恰是猫复合情绪的正常表现标注时必须如实记录后期情绪推断层会处理这种组合。2.3 标注工具与质控流程工具选型上我最终用的是X-AnyLabeling而不是传统LabelImg。原因很直接X-AnyLabeling支持调用现成的检测或分割模型做预标注能直接输出YOLO格式。我先用已经训练好的猫检测模型把头部、躯干、尾巴、嘴部框自动标好人工只需要修正框边界、填行为类别效率大概能提升60%。如果是小团队纯手动起步LabelImg也是可以的但建议所有标注文件统一用YOLO txt格式保存不要中间转一次COCO再转回来格式转换是标注错位的高发区。质控这块我坚持的是“三层校验”第一层是标注员自检每标完50张用可视化脚本随机抽5张叠加标签画框检查。第二层是交叉校验两个标注员互换检查不一致的地方标记出来讨论。第三层是模型反哺校验用已训练模型对训练集做预测把预测结果和人工标注差异超过阈值IoU低于0.5或类别不一致的样本全部抽出来人工复核。三层跑完大概会修正掉8%到12%的标签错误。这些错误如果不处理会让mAP掉3到5个点在行为检测这种对细粒度敏感的课题上影响更大。2.4 数据集组织与YOLO格式转换训练时的目录结构我强烈建议固定一套标准避免后面换人接手或换机器时踩路径的坑。我用的结构是cat_emotion_dataset/ ├── images/ │ ├── train/ # 2560张 │ ├── val/ # 320张 │ └── test/ # 320张 ├── labels/ │ ├── train/ # 与images同名txt │ ├── val/ │ └── test/ └── cat_emotion.yamlYOLO格式的txt每行含义是class_id center_x center_y width height其中坐标全部是归一化到0到1的小数。转换时最容易出错的地方有两个一是用绝对值坐标忘了归一化二是归一化时除以了图片宽高的错误顺序。我写过一个Python脚本做批量校验检查坐标是否越界、框宽高是否为负数、以及图片与标签文件名是否一一对应。import os from PIL import Image def validate_labels(img_dir, lbl_dir): issues [] for img_name in os.listdir(img_dir): if not img_name.lower().endswith((.jpg, .jpeg, .png)): continue stem os.path.splitext(img_name)[0] img_path os.path.join(img_dir, img_name) lbl_path os.path.join(lbl_dir, stem .txt) if not os.path.exists(lbl_path): issues.append(fmissing label: {lbl_path}) continue w, h Image.open(img_path).size with open(lbl_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: issues.append(fbad line in {lbl_path}: {line}) continue cid, cx, cy, bw, bh map(float, parts) if cid 0 or cx 0 or cy 0 or bw 0 or bh 0: issues.append(finvalid value in {lbl_path}: {line}) if cx bw / 2 1.0001 or cx - bw / 2 -0.0001: issues.append(fx out of range in {lbl_path}: {line}) return issues这一步看起来不起眼但75%的YOLO训练报错“no labels found”或者“image not found”根因都是文件结构或格式问题。数据校验脚本跑一次只需要十几秒能省掉后面数小时的无用调试。3. 模型训练配置、超参与损失曲线解读3.1 模型选型与预训练权重模型规模的起点我建议YOLOv8s不要一上来就用nano。nano推理最快但在耳朵平贴、眯眼这种细粒度行为上特征表达能力不足AP会明显偏低。我实测过同一份数据集nano和s的mAP50差距约3到4个点s和m的差距约1到2个点。m的性价比就开始下滑了训练时间和显存翻倍精度收益有限。所以折中方案是YOLOv8s作为主力部署时再根据实际算力剪枝或蒸馏到nano。这也是行业里普遍的做法——训练用大一点的模型部署时再压缩而不是一上来就用小模型省事。预训练权重强烈建议用不要从零训练。YOLO在COCO上学到的通用视觉特征比如边缘、纹理、局部形状对猫的不同身体部位是直接可迁移的。从零训练3200张图收敛慢且容易过拟合。我用的命令是自动下载官方的yolov8s.pt如果网络环境受限就手动下载放当前目录。3.2 训练配置与命令全解数据集配置文件cat_emotion.yaml的内容如下train: cat_emotion_dataset/images/train val: cat_emotion_dataset/images/val test: cat_emotion_dataset/images/test nc: 12 names: 0: ear_forward 1: ear_flat 2: ear_backed 3: eye_open 4: eye_squinted 5: mouth_open 6: teeth_bared 7: tail_up 8: tail_tucked 9: tail_lashing 10: body_crouch 11: body_relaxed训练命令我用了ultralytics框架完整参数如下yolo train datacat_emotion.yaml modelyolov8s.pt \ imgsz640 batch16 epochs200 device0 \ lr00.02 lrf0.01 warmup_epochs3 \ optimizerAdamW weight_decay0.0005 \ mosaic1.0 fliplr0.5 hsv_h0.015 hsv_s0.7 hsv_v0.4 \ patience50 workers8 projectruns/cat_emotion nameexp1几个关键参数的解释imgsz设为640。猫身体部位在画面里通常不是小目标640是兼顾检测精度和训练速度的甜点位。如果部署时摄像头分辨率不足可以用512重训但不要直接拿640模型去跑480输入退化很严重batch设16是基于单张24G显存的经验值。如果是12G的卡建议batch降到8同时按比例把lr0降到0.008。很多人训练崩了不是因为模型问题而是batch变了没同步调学习率optimizer用AdamW。SGD在数据量小、标签噪声偏大的时候收敛不稳定AdamW对这类细粒度任务更友好尤其是注意力机制引入的冗余参数mosaic增强保留1.0。Mosaic把四张图拼在一起能强制模型学习被截断的目标这对于局部区域检测非常有帮助。但有一个坑到了训练最后20个epoch我会关掉mosaic因为过度拼接会让模型对真实完整目标的边界框回归产生偏移3.3 损失函数与训练过程监控YOLOv8的损失由三部分组成box_loss用CIoUcls_loss用BCE还加了一个DFL分支。DFL的核心思想是让框回归不再直接输出一个确定值而是输出一个分布然后从分布中提取期望值作为最终坐标。这个设计的实际收益是边框定位更稳尤其是耳朵、尾巴这类边缘不清晰的目标。训练时我主要看三张曲线图。第一是box_loss它应该平稳下降如果出现明显的反弹震荡说明学习率偏大或者数据里有标签错位。第二是cls_loss它下降的速度决定了类别收敛效率。第三是mAP50-95曲线mAP50达到90以上不一定能落地因为真实摄像头场景不会像测试集那样友善而mAP50-95对框轻微偏移和尺度变化更敏感更重要。还有一个容易忽略的监控点训练结束贴标签的置信度分布。如果大量预测框置信度集中在0.3到0.5而训练集中人工标注的置信度都在0.9以上说明模型对这类样本的信心不足应该检查标注框质量或者对应类别的样本量。3.4 评估指标与典型结果解读这个数据集在YOLOv8s上的典型结果我整理成了表格类别样本数PrecisionRecallmAP50mAP50-95ear_forward182091.288.592.171.3ear_flat115089.485.989.667.8ear_backed98690.887.391.069.2eye_open215095.192.894.574.1eye_squinted77984.681.286.362.4mouth_open68387.984.588.265.7teeth_bared41282.378.183.960.5tail_up135090.486.789.268.0tail_tucked40281.576.882.458.9tail_lashing35880.774.579.655.3body_crouch112089.886.288.766.9body_relaxed175092.389.691.870.5从结果能看到一个规律识别率最低的类别几乎都是稀缺行为比如tail_lashing只有358个样本、teeth_bared 412个样本。这说明模型的主要瓶颈不在架构而在长尾数据。你如果只想快速提升mAP与其花时间调参不如花精力去补充稀缺类别的样本。4. 常见问题与排坑实录4.1 标签不一致与边界样本处理行为检测最容易出现的问题就是同一行为在不同猫、不同角度、不同距离下长得完全不像。耳朵前倾在正脸视角看是两耳外张在侧脸视角看是耳尖朝前这两类样本如果被标进同一个类别而模型又无法同时学到两种形态的特征就会在其中一个视角上持续误判。我的处理办法是在标注时细分视角虽然类别名不区分视角但规则书会记录每张图的视角标签。训练时通过分层采样保证每个类别里正脸和侧脸比例相对均衡。眯眼和正常睁眼的边界同样棘手。我给标注员的判断标准是瞳孔的可见面积。瞳孔缩小到正常状态下的一半以下并且眼睑明显下垂才允许标eye_squinted。这个标准虽然粗暴但胜在唯一让不同标注员之间的一致性大幅提升。模型训练出来也在实测中表现最稳定。4.2 类别失衡与稀缺样本的补救稀缺类别的样本不是简单复制粘贴就能解决的。复制粘贴相同的框只会让模型记住特定姿态而不会泛化。我的补救顺序是优先补充真实样本。去猫咖蹲守、去宠物救助站点拍应激状态虽然耗时但这是唯一能保证真实性的路径使用半自动增强。对稀缺类别的样本做左右翻转、轻微亮度扰动、随机裁剪拼贴注意最大旋转角度不要超过15度猫的姿态语义在旋转角度过大时会改变用在线增强补偿。在训练时对稀缺类别所在的样本提高Mosaic的概率相当于每轮都在生成新的合成样本经验数据是稀缺类别从358个提升到800个mAP50大概能提升5到7个点。而单纯把训练epoch从100涨到200同类别只提升1到2个点。数据的边际收益远大于算力的边际收益。4.3 遮挡、模糊与个体差异家庭场景里有大量沙发缝、猫窝里的遮挡镜头。规则是“遮挡超过50%不标”但实际操作中我发现猫从猫窝里探头出来的场景头部是完整的尾巴被挡住这时只需要标头部框不要为了凑数硬标半截尾巴。还有一类更隐蔽的问题是个体差异。无毛猫和长毛猫的耳朵视觉差异极大缅因猫和暹罗猫的尾巴粗细悬殊。如果你只是简单随机划分数据集模型可能对特定品种产生偏好。我在采集时记录了每只猫的品种和个体ID划分数据时确保每个品种在所有子集中都有分布。如果你的数据里品种混杂严重建议至少保证训练集和测试集的品种分布大致一致否则评估结果会失真。4.4 硬件、环境与复现类问题的快速排查训练过程中最常见的一个报错是“CUDA out of memory”。这时候优先做三件事把batch减小到原来的一半、把workers降到4、关闭Mosaic。Mosaic本身会额外占用显存所以训练后期关闭它不仅能提升稳定性还能临时解决一部分显存压力。还有一个非常容易被忽略的问题多卡训练和单卡训练的BN差异。YOLO在DataLoader里默认开了shuffle和persistent_workers一旦在Windows下用多线程读取数据容易出现随机的“Broken pipe”中断。我直接统一在Linux环境下训练Windows只用来做数据预处理和可视化。倒不是Windows跑不了而是这类中断在长训练里会反复打断节奏排查成本太高。复现性方面ultralytics框架默认结果会有随机性因为PyTorch、CUDA、cuDNN的核函数选择不完全确定。如果你需要严格的复现在train脚本开头手动设置import random import torch import numpy as np random.seed(42) np.random.seed(42) torch.manual_seed(42) torch.cuda.manual_seed_all(42) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False注意开启deterministic后训练速度会下降一些但换来的是同配置下跑两次结果基本一致。这个在调试阶段非常有用能帮你判断mAP浮动到底是数据问题还是环境噪声。5. 部署落地与扩展方向5.1 边缘设备迁移与推理加速模型训练好之后直接跑pt文件做推理是演示阶段的做法真实项目里第一步就是导出成ONNX再转TensorRT。yolo export modelbest.pt formatonnx imgsz640 opset12 simplifyTrue trtexec --onnxbest.onnx --saveEnginebest.trt --fp16在Jetson Orin Nano上YOLOv8s的FP16推理延迟大约在12到18毫秒加上NMS后整体在18到25毫秒完全可以承担实时摄像头分析。如果部署平台性能更弱比如树莓派4B我建议把模型压缩到nano并用INT8量化单帧推理大约能压到40到60毫秒勉强可用但不适合高帧率场景。还有一个容易忽略的细节导出ONNX时imgsz必须与训练一致否则模型会无声地退化。我见过有人训练时用640导出时代码里误写成了416mAP直接掉了近20个点而且这类bug不会报错特别坑。5.2 从单帧检测到连续行为分析单帧检测只是一个片段猫的行为判断需要时间上下文。一只猫耳朵后压1秒钟可能只是伸懒腰的伴生动作但如果连续5秒耳朵持续后压、身体压低、瞳孔部分可见那大概率是恐惧或防御状态。我的做法是引入ByteTrack做了跟踪把同一只猫的多帧检测结果按ID聚合然后维护一个时间窗为3秒的行为事件队列。队列里对每类行为做加权统计再通过一套可配置的规则表映射为情绪状态估计。这套方案比直接用视频分类网络有效得多因为视频分类需要大量时序样本3200张静态图撑不起来。而检测加跟踪的方式可以把静态数据集的成果直接复用到视频推理上属于更稳妥的技术路线。5.3 多模态融合与产品化思路摄像头只能看到视觉维度猫的情绪还有很多体现在声音上哈气声、呼噜声、短促的喵叫。视觉和声音的结合能明显提升情绪判断的准确率特别是那些尾巴和耳朵被遮挡、视觉信号缺失的场景。我给这个小项目留了一个扩展接口视觉侧YOLO继续输出行为事件音频侧用简单的关键声音频谱分类器输出声学事件两者进了同一个行为队列做融合判断。音频端的模型不需要很复杂先用短时傅里叶变换提取特征再用一个轻量分类器足以区分哈气声、呼噜声和正常叫声。产品化时还有几个实际问题需要提前想清楚本地化推理几乎是必须的猫毛和家庭隐私问题导致云推理接受度极低所以模型压缩和端侧部署是绕不开的投入。数据闭环同样重要真实用户环境里的失败案例需要定期回流并增量训练。如果你的目标是一个可售卖的家庭宠物监测产品我建议从第一天就设计好“模型预测结果回传-人工抽样评估-增量数据集-定时重训”这条闭环链路而不是定期手动导数据、手动训练、手动发布。这套数据集和方法论不只是给“猫情绪检测”项目用的。你在数据标注、模型训练、部署落地这几个环节踩过的坑在任何一个小型目标检测项目里都会重现。数据和场景虽然变了但方法论是通用的。
返回列表