拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于YOLO的猫情绪检测实战:从3200张数据集到边缘部署

基于YOLO的猫情绪检测实战:从3200张数据集到边缘部署

猫这种动物,情绪表达极其微妙。养过猫的人都懂,它开心的时候尾巴竖得像根天线,不爽的时候耳朵往后一压、瞳孔一缩,整套信号在零点几秒内就完成了。问题是,人眼能捕捉到这些细节,但要让机器去识别,难度就完全不一样了。我最近拿到一份3200张规模的猫情绪检测数据集,标注格式是YOLO,打算用它来训练一个能实时判断猫咪情绪状态的目标检测模型。这篇文章就把我从数据检查、类别设计、训练调参到部署验证的完整过程拆开来讲,适合做目标检测落地、宠物行为分析、边缘端视觉项目的朋友参考。

1. 拿到数据集先别急着训练:3200张猫脸背后的标注质量审查

很多人拿到数据集的第一反应是直接丢进YOLO开跑,跑完看mAP还行就收工。但我在实际项目里踩过太多次坑——标注框偏移、类别混淆、重复图片、极端长尾分布,这些问题不提前处理,训练出来的模型在真实场景里就是个摆设。3200张听起来不算少,但分摊到多个情绪类别之后,每类可能只有几百张,容错空间很小。

1.1 猫情绪类别的定义方式决定了模型上限

猫的情绪不像人脸有公认的七类标准(高兴、悲伤、愤怒等),它更多是行为学层面的状态划分。常见的做法是映射成几个可视觉区分的类别,比如:

  • 放松/愉悦:耳朵朝前、瞳孔正常、尾巴自然下垂或轻微摆动、身体舒展
  • 警觉/好奇:耳朵竖立前倾、瞳孔适度放大、身体前倾、尾巴可能快速摆动
  • 恐惧/紧张:耳朵后压或扁平、瞳孔放大、身体蜷缩、尾巴夹紧
  • 攻击/愤怒:耳朵完全后压、瞳孔收缩、背部拱起、毛发竖立、露出牙齿
  • 疼痛/不适:面部肌肉紧绷、眼睛半闭或眯起、耳朵侧向、胡须僵硬

这五类是我在多个宠物行为项目里总结出来的、视觉特征相对可区分的划分方式。如果你的数据集标注类别跟这个不一致,先别改标注,而是先统计每类的样本量。我见过一个数据集把"警觉"和"恐惧"混在一起标,结果模型在这两类上完全分不开,mAP掉到0.3以下。

提示:猫的情绪是连续光谱,不是离散标签。标注时如果遇到模棱两可的样本(比如一只猫既警觉又有点紧张),宁可丢弃也不要强行归类,否则会污染整个类别的特征分布。

1.2 用脚本快速体检:重复图、坏图、标注越界一次查清

3200张图靠肉眼一张张看是不现实的。我一般写一个Python脚本做批量体检,核心检查项包括:

import os import hashlib from PIL import Image from collections import Counter def check_dataset(img_dir, label_dir): # 1. 重复图片检测(基于MD5) hashes = {} duplicates = [] for fname in os.listdir(img_dir): if not fname.lower().endswith(('.jpg', '.png', '.jpeg')): continue path = os.path.join(img_dir, fname) with open(path, 'rb') as f: md5 = hashlib.md5(f.read()).hexdigest() if md5 in hashes: duplicates.append((fname, hashes[md5])) else: hashes[md5] = fname # 2. 坏图检测 broken = [] for fname in os.listdir(img_dir): try: img = Image.open(os.path.join(img_dir, fname)) img.verify() except Exception: broken.append(fname) # 3. 标注越界与类别统计 class_counter = Counter() out_of_bound = [] for fname in os.listdir(label_dir): if not fname.endswith('.txt'): continue with open(os.path.join(label_dir, fname)) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls, x, y, w, h = int(parts[0]), *map(float, parts[1:]) class_counter[cls] += 1 if not (0 <= x <= 1 and 0 <= y <= 1 and 0 < w <= 1 and 0 < h <= 1): out_of_bound.append((fname, line.strip())) print("重复图片:", duplicates) print("坏图:", broken) print("类别分布:", dict(class_counter)) print("越界标注:", out_of_bound[:10])

这个脚本跑一遍,基本能筛掉80%的脏数据问题。重复图片尤其要重视——有些数据集是从视频抽帧来的,相邻帧几乎一样,如果不剔除,训练集和验证集之间会产生数据泄漏,验证指标虚高,实际部署就露馅。

1.3 长尾分布的处理策略:过采样还是重标注

3200张图分到5个类别,如果分布是[1200, 900, 600, 350, 150]这种长尾形态,直接训练会导致模型严重偏向头部类别。我的处理顺序是:

  1. 先确认尾部类别是否值得保留:如果"疼痛/不适"只有150张且标注质量差,考虑合并到"恐惧/紧张"或直接删除
  2. 对保留的尾部类别做过采样:复制样本时配合数据增强(随机翻转、色彩抖动、Mosaic),避免简单复制导致过拟合
  3. 调整损失函数权重:YOLOv8支持通过cls参数调整分类损失权重,尾部类别可以适当加权

我实测下来,过采样+增强的组合比单纯调损失权重效果更稳,因为前者直接增加了尾部类别的特征多样性。

2. 为什么选YOLO而不是分类网络:猫情绪检测的任务本质

有人会问,情绪识别不是分类任务吗,为什么用目标检测?这个问题我在项目初期也纠结过,后来想明白了:猫的情绪表达是局部特征驱动的,耳朵、瞳孔、尾巴、身体姿态各自携带独立信号,用整图分类会把这些信号平均掉。

2.1 目标检测 vs 图像分类:猫情绪场景下的取舍

维度图像分类目标检测(YOLO)
输入假设整图只有一只猫,且占满画面图中可能有多只猫,或猫只占局部
特征粒度全局平均池化,局部信号被稀释每个检测框独立提取特征
多猫场景无法处理天然支持
标注成本只需类别标签需要边界框+类别
部署复杂度低中等

实际场景里,一张照片可能有多只猫,或者猫只占画面一角(比如监控视角)。分类网络在这种场景下直接失效,而YOLO可以逐只猫输出情绪标签。另外,检测框本身提供了空间约束,模型会聚焦在猫的身体区域,不会被背景干扰。

2.2 YOLO版本选型:v5、v8还是v11

数据集是YOLO格式,理论上v5、v8、v11都能直接用。我的选型逻辑是:

  • YOLOv5:生态最成熟,文档和社区资源最多,适合快速验证。但架构相对老旧,同等精度下参数量偏大
  • YOLOv8:Ultralytics维护,API统一,训练/验证/导出一条龙,支持分类、检测、分割多任务。我目前的主力选择
  • YOLOv11:2024年发布,在v8基础上优化了骨干网络和检测头,小目标检测有提升,但社区资源还不如v8丰富

对于3200张这种中小规模数据集,我建议从YOLOv8n或YOLOv8s起步。nano版本参数量只有3.2M,在边缘设备上跑实时推理毫无压力;如果精度不够再升级到s或m。别一上来就用x版本,3200张图喂不饱大模型,过拟合风险极高。

注意:YOLOv8的预训练权重默认在COCO上训练,包含"cat"类别。你可以直接用这个权重做迁移学习,冻结骨干网络前几层,只训练检测头,收敛速度会快很多。

2.3 数据格式转换:从标注文件到YOLO训练目录

YOLO格式的标注是每张图对应一个txt文件,每行格式为class_id x_center y_center width height,坐标都是归一化到0-1的。训练目录结构需要组织成:

dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml

data.yaml的内容:

path: ./dataset train: images/train val: images/val test: images/test nc: 5 names: ['relaxed', 'alert', 'fearful', 'aggressive', 'pain']

划分比例我一般用7:2:1,但如果某类样本特别少,验证集里可能一张都没有。这时候要用分层抽样,确保每个类别在train/val/test里都有代表。sklearn的train_test_split配合stratify参数可以做到,但需要先把每张图的类别标签提取出来。

3. 训练调参实录:从mAP 0.42到0.78的迭代过程

这部分是我踩坑最多的地方。第一次训练直接用默认参数跑YOLOv8n,100个epoch下来mAP@0.5只有0.42,验证集损失震荡严重。下面是我逐步调整的过程和背后的逻辑。

3.1 第一轮失败复盘:学习率与batch size的匹配问题

默认配置下,YOLOv8的初始学习率是0.01,batch size是16。我的显卡是RTX 3060 12G,跑16 batch没问题,但训练曲线显示前20个epoch损失几乎不降。原因是我用了预训练权重,但没冻结骨干网络,大学习率直接把预训练特征打乱了。

调整方案:

  • 初始学习率降到0.001,配合余弦退火调度
  • 前5个epoch用warmup,学习率从0.0001线性升到0.001
  • batch size保持16,如果显存够可以上32,但学习率要同步放大
from ultralytics import YOLO model = YOLO('yolov8n.pt') model.train( data='data.yaml', epochs=150, imgsz=640, batch=16, lr0=0.001, lrf=0.01, warmup_epochs=5, cos_lr=True, patience=30, device=0 )

这一轮跑完,mAP@0.5升到0.58,但"fearful"和"aggressive"两类仍然混淆严重。

3.2 数据增强的取舍:Mosaic不是万能的

YOLO默认开启Mosaic增强,把4张图拼成1张。这个策略在通用目标检测上很有效,但在猫情绪检测上反而有害——Mosaic会把不同情绪的猫拼在一起,模型学到的上下文被打乱了。我做了对比实验:

增强策略mAP@0.5备注
默认(Mosaic+HSV+翻转)0.58情绪混淆严重
关闭Mosaic,保留HSV+翻转0.64明显改善
关闭Mosaic,加入随机裁剪0.67进一步提升
关闭Mosaic,加入Cutout0.66略低于裁剪

最终我选择关闭Mosaic,保留HSV色彩抖动、随机水平翻转、随机裁剪。色彩抖动对猫情绪识别有帮助,因为不同光照下猫的毛色和瞳孔表现差异很大,模型需要对这些变化鲁棒。

model.train( data='data.yaml', epochs=150, imgsz=640, batch=16, lr0=0.001, mosaic=0.0, # 关闭Mosaic hsv_h=0.015, # 色调抖动 hsv_s=0.7, # 饱和度抖动 hsv_v=0.4, # 亮度抖动 fliplr=0.5, # 水平翻转概率 scale=0.5, # 随机缩放 translate=0.1, # 随机平移 device=0 )

3.3 类别混淆的针对性处理:难例挖掘与损失加权

"fearful"和"aggressive"混淆的根因是:两者都有耳朵后压的特征,区别在于瞳孔和身体姿态。但很多标注图里猫的脸部不够清晰,模型只能靠耳朵位置判断,自然分不开。

我的处理方式:

  1. 难例挖掘:训练完后用验证集跑推理,把置信度低或分类错误的样本挑出来,人工复核标注。我发现有大约80张图的"fearful"和"aggressive"标反了,修正后重新训练
  2. 损失加权:在YOLOv8的损失函数里,分类损失用BCEWithLogitsLoss,可以通过cls参数放大分类损失的权重,让模型更关注分类精度
  3. 增加输入分辨率:从640提升到768,猫脸部的细节更清晰,瞳孔特征更容易被捕捉

这三步做完,mAP@0.5冲到0.78,"fearful"和"aggressive"的混淆率从35%降到12%。

3.4 训练过程中的BN崩溃与梯度爆炸排查

训练到第80个epoch左右,我遇到过一次损失突然变成NaN的情况。排查下来是BatchNorm层在某个batch的方差接近零,导致除零。这种情况在小batch或数据分布极端时容易出现。

解决方案:

  • 把batch size从16降到8,同时把学习率减半
  • 在data.yaml里开启rect=True,让同一batch内的图片尺寸一致,减少padding带来的分布偏移
  • 加入梯度裁剪,torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10.0)

如果你也遇到BN崩溃,先别急着换模型,检查一下数据里有没有全黑或全白的异常图,这种图会让BN统计量失真。

4. 模型部署前的最后一公里:从PyTorch到TensorRT的加速验证

训练出mAP 0.78的模型只是第一步,真正落地要考虑推理速度。我的目标是在边缘设备上跑实时检测,至少25FPS。PyTorch原生推理在RTX 3060上跑YOLOv8n 640分辨率大约80FPS,但换成Jetson或更低功耗的设备就不够了。

4.1 导出ONNX与TensorRT引擎的完整流程

YOLOv8支持一键导出多种格式:

from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') # 导出ONNX model.export(format='onnx', imgsz=640, simplify=True, opset=12) # 导出TensorRT(需要CUDA环境) model.export(format='engine', imgsz=640, half=True, device=0)

导出TensorRT时注意几点:

  • half=True开启FP16精度,速度提升约1.5倍,精度损失通常在1%以内
  • imgsz必须和训练时一致,否则检测框坐标会错位
  • 首次导出会花几分钟做引擎优化,之后加载就很快了

4.2 推理速度实测:不同硬件平台的对比

我在几个平台上做了推理速度对比,输入分辨率统一640,FP16精度:

硬件平台推理框架单帧耗时FPS
RTX 3060 12GPyTorch12ms83
RTX 3060 12GTensorRT FP165ms200
Jetson Orin NanoTensorRT FP1618ms55
Jetson Xavier NXTensorRT FP1635ms28
Intel NUC(核显)OpenVINO45ms22

如果你要在Jetson上部署,TensorRT是必选项,PyTorch原生推理在Jetson上只有10FPS左右,完全达不到实时要求。

4.3 后处理中的坑:NMS阈值与置信度阈值的联动

YOLO的输出需要经过非极大值抑制(NMS)才能得到最终检测框。默认的conf_thres=0.25和iou_thres=0.45在猫情绪检测上不一定最优。

我的调参经验:

  • 置信度阈值:猫情绪检测的误检代价较高(把放松误判为攻击会导致不必要的干预),所以我把conf_thres提到0.4,宁可漏检不可误检
  • NMS IoU阈值:多只猫靠得很近时,IoU阈值太低会把不同猫的框合并。我调到0.5,配合agnostic_nms=False(按类别独立做NMS)
  • 最大检测数:默认300,对于猫情绪场景,一张图最多也就几只猫,设成10就够了,能减少后处理耗时
results = model.predict( source='test_image.jpg', conf=0.4, iou=0.5, max_det=10, agnostic_nms=False, half=True )

4.4 实际场景验证:室内监控与手机拍摄的差异

训练数据主要来自室内近距离拍摄,但实际部署场景可能是监控视角(俯视、远距离)或手机随手拍(角度随意)。我拿模型在这两种场景下做了测试:

  • 室内监控视角:猫只占画面1/10左右,检测框偏小,mAP掉到0.55。解决方案是用imgsz=960推理,小目标召回率明显提升,但速度降到原来的60%
  • 手机拍摄:角度多变,但猫通常占画面比例大,mAP保持在0.72左右。主要问题是逆光场景下瞳孔特征丢失,导致"fearful"误判为"relaxed"

针对逆光问题,我在预处理里加了一步自适应直方图均衡化(CLAHE),对亮度通道做局部增强,瞳孔区域的对比度提升后,误判率下降了约8个百分点。

5. 数据集扩展与模型迭代的长期思路

3200张图训练出来的模型,在受控环境下表现不错,但真实世界的猫情绪表达远比数据集丰富。如果你打算把这个项目长期做下去,下面几个方向值得投入。

5.1 主动学习:让模型自己挑出值得标注的样本

与其盲目标注新数据,不如让当前模型对未标注图片做推理,挑出置信度低或类别分布异常的样本,优先标注这些"难例"。我通常按以下优先级筛选:

  1. 置信度在0.3-0.6之间的检测框(模型犹豫的样本)
  2. 同一张图里出现多个不同情绪类别的样本(模型可能混淆)
  3. 与训练集特征分布差异大的样本(比如不同品种、不同光照)

这套流程跑下来,每标注100张新图带来的mAP提升,比随机标注300张还多。

5.2 多模态融合:加入音频与姿态信息

猫的情绪不仅体现在面部,还体现在叫声和身体姿态上。如果条件允许,可以:

  • 用麦克风采集猫的叫声,做音频分类(呼噜声、嘶嘶声、喵呜声对应不同情绪)
  • 用姿态估计网络提取猫的骨骼关键点,判断身体紧张程度
  • 把视觉、音频、姿态三个模态的特征做融合,输出最终情绪标签

多模态融合的复杂度高,但鲁棒性远好于单视觉模态。我做过一个简单实验,视觉+音频的融合模型在"疼痛/不适"类别上的召回率比纯视觉模型高15%。

5.3 边缘端持续学习:让模型适应你家的猫

每只猫的情绪表达都有个体差异。我家的橘猫"放松"时耳朵是朝前的,但朋友家的布偶猫"放松"时耳朵略微侧向。通用模型在这种个体差异上会犯错。

一个可行的方案是在边缘设备上做轻量级微调:把用户反馈的错误检测结果存下来,定期用这些小样本对模型的最后一层做微调。YOLOv8支持冻结骨干网络只训练检测头,在Jetson上跑几十张图的微调只需要几分钟。

提示:边缘端微调要注意灾难性遗忘问题。建议保留一部分原始训练数据,和新样本混合训练,避免模型只适应新场景而丢失通用能力。

5.4 标注规范的持续迭代

数据集不是一次性的,标注规范需要随着模型迭代不断细化。比如最初我把"耳朵后压"统一标为"fearful",后来发现"aggressive"也有这个特征,于是补充了"瞳孔收缩+身体前倾"作为"aggressive"的必要条件。这种规范的迭代需要标注团队和算法团队紧密配合,每次模型暴露出的系统性问题,都要反哺到标注规范里。

我在实际项目里维护了一份"标注争议案例库",把每次遇到模棱两可的样本和最终决策记录下来。新标注员入职时先看这个库,标注一致性会高很多。这个习惯看起来麻烦,但长期来看能省下大量返工时间。

最后分享一个我在部署时的小技巧:YOLO模型导出TensorRT后,第一次推理会有一个明显的延迟(引擎初始化),如果你做的是实时监控,建议在服务启动时先跑一张空白图做warmup,把初始化开销提前消化掉。这个细节在文档里不会写,但实际部署时能避免第一帧卡顿导致的误判。

返回列表