news 2026/9/26 15:57:58

YOLO犬类情绪识别实战:从数据标注到实时部署全流程解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLO犬类情绪识别实战:从数据标注到实时部署全流程解析

简介:一套完整的基于YOLO的犬类情绪识别项目包,主要面向深度学习初学者和图像分类实战开发者,尤其适合作为毕业设计、课程设计或期末大作业的参考项目。项目覆盖从数据准备、模型训练到测试评估与结果可视化的全流程,能帮助使用者理解目标检测与动物情绪分类的落地方式,适用于教学演示、项目汇报和算法对比实验。压缩包共72个文件,约60.25MB,其中以jpg/jpeg/png图像样本和结果图为多数,配合py训练与测试脚本、md说明文档、依赖文件及gif演示结果;目录中另有模型目录、输出结果目录和评估图表,包含P曲线、R曲线、PR曲线、F1曲线、混淆矩阵等,便于直接查看模型效果。已有56人浏览学习。通过源码、样例图片、训练后的模型与说明文档,使用者可复现犬类情绪识别实验,也可替换数据集进行迁移训练,节省从零搭建代码与调参的时间,是兼具教学参考和实际应用价值的完整资料。

1. 犬类情绪识别:一个 YOLO 就能带走的课程设计项目

如果你正在为毕业设计或期末大作业发愁,想找一个“深度学习 + 图像识别”方向、能落地演示又能写进简历的项目,基于 YOLO 的犬类情绪识别是个很务实的选择。它能对狗的脸部表情和姿态做实时分类,区分开心、焦虑、愤怒、悲伤、平静这几类典型情绪状态,覆盖从数据标注、模型训练到摄像头推理的完整流程。

这套资源最大的价值不是“识别有多准”,而是把深度学习中目标检测落地的完整链路都串起来了:数据怎么清洗、VOC 标注怎么转 YOLO 格式、训练参数怎么调、摄像头实时推理怎么接、模型怎么导出部署。对新手来说,它是一份能跟到最后的完整代码包;对已经有基础的人来说,它也能当成一个快速出结果的基线工程,省掉从零搭环境的时间。

我拆这个项目时最直观的感受是:真正的功夫不在训练那一步,而在数据整理和参数调试上。下面按我自己的复现路径往下走,每个环节都给到可直接抄的代码和参数。

2. 数据先行:整理、标注转换与五分类标签的落地方法

2.1 数据从哪来:构建与清洗

犬类情绪识别本质上是一个目标检测任务,输入是包含犬只的图片,输出是检测框加上情绪类别。情绪识别比普通品种识别更难,因为同类情绪在不同犬种上的表现差异很大,哈士奇和柯基的“开心脸”完全是两个画风。因此数据集的构成直接影响模型能不能收敛。

资源里自带的原始图片多为网图、公开宠物数据集和少量自拍素材的混合体,第一件要做的事是清洗。常见做法是把这三类图片按“犬只朝向正面或侧面 45 度以内、脸部占比大于 1/4、单张图只有一只狗、无大面积遮挡”的标准筛一遍。我一般会用一个简单的脚本统计每张图的尺寸和通道,把损坏的、灰度异常的、分辨率低于 300px 的图直接剔除。

import os from PIL import Image src_dir = "raw_images" valid_dir = "cleaned_images" os.makedirs(valid_dir, exist_ok=True) min_side = 300 removed = [] for name in os.listdir(src_dir): path = os.path.join(src_dir, name) try: img = Image.open(path) img.verify() # 检查文件是否损坏 w, h = img.size if w < min_side or h < min_side: removed.append((name, "too small")) continue if img.mode == "L": removed.append((name, "grayscale")) continue img = Image.open(path).convert("RGB") img.save(os.path.join(valid_dir, name)) except Exception as e: removed.append((name, str(e))) print(f"removed {len(removed)} files: {removed[:10]}")

img.verify()只做完整性校验,不开图,速度很快;img.mode == "L"是灰度图过滤,因为训练时颜色信息对情绪识别有贡献,灰度图会引入分布偏移。这里把筛掉的图片名打印出来,便于回头补数据时排查。

清洗后大约能留下 70%~80% 的图片。这一步不能省,脏数据进训练集,后面所有指标都会变得奇怪。

2.2 标注格式转换:VOC 转 YOLO

清洗完成后要处理的是标注格式。资源包里原始标注是 XML(VOC 格式),而 YOLO 训练需要的是每个图片对应一个 txt,每行写“类别_id x_center y_center width height”,坐标全部归一化到 0 到 1 之间。

# convert_voc_to_yolo.py import xml.etree.ElementTree as ET import os # 类别顺序必须与 data.yaml 中完全一致 CLASSES = ["angry", "anxious", "calm", "happy", "sad"] def voc_to_yolo(xml_path, out_txt_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) lines = [] for obj in root.iter("object"): cls_name = obj.find("name").text.strip().lower() if cls_name not in CLASSES: continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h box_w = (xmax - xmin) / w box_h = (ymax - ymin) / h cls_id = CLASSES.index(cls_name) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") with open(out_txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) xml_dir = "annotations/voc" txt_dir = "annotations/yolo" os.makedirs(txt_dir, exist_ok=True) for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue stem = os.path.splitext(xml_name)[0] voc_to_yolo(os.path.join(xml_dir, xml_name), os.path.join(txt_dir, stem + ".txt"))

宽度和高度必须从<size>标签里取,不能自己用 PIL 再算一次。有些手机照片带 EXIF 旋转信息,直接读图得到的宽高会被方向信息干扰,标注会整体偏移。归一化坐标让不同分辨率的图片在训练时能统一到同一个尺度,这也是 YOLO 系列一直用这种方式的原因。

转换完成后抽查几份 txt,如果出现 x_center 大于 1 或小于 0 的情况,说明标注框越界,需要回到原 XML 修正。

2.3 数据划分与类别均衡

分类项目的数据划分不能只用随机切分,尤其是情绪识别这类天然不均衡的数据集。狗的表情里“开心”和“平静”的样本远多于“焦虑”和“愤怒”,如果按原始比例切分,验证集里愤怒样本可能只有个位数,模型全猜平静也能拿到高准确率,这会让指标失真。

import os import random from collections import Counter from shutil import copy2 image_dir = "cleaned_images" label_dir = "annotations/yolo" train_dir = "dataset/images/train" val_dir = "dataset/images/val" train_label_dir = "dataset/labels/train" val_label_dir = "dataset/labels/val" for d in [train_dir, val_dir, train_label_dir, val_label_dir]: os.makedirs(d, exist_ok=True) label_files = [f for f in os.listdir(label_dir) if f.endswith(".txt")] random.seed(42) # 统计每个类别的样本框数量,按框数而不是图片数分配 cls_counter = Counter() for lf in label_files: with open(os.path.join(label_dir, lf), "r", encoding="utf-8") as f: for line in f: cls_id = int(line.split()[0]) cls_counter[cls_id] += 1 print("class distribution:", dict(cls_counter)) train_ratio = 0.85 train_files = [] val_files = [] # 分层采样:保证每个类别在验证集中都有足够样本 for cls_id in range(len(CLASSES)): cls_files = [ lf for lf in label_files if any(int(line.split()[0]) == cls_id for line in open(os.path.join(label_dir, lf), "r", encoding="utf-8")) ] random.shuffle(cls_files) split_idx = int(len(cls_files) * train_ratio) train_files.extend(cls_files[:split_idx]) val_files.extend(cls_files[split_idx:]) for f in train_files: copy2(os.path.join(image_dir, f.replace(".txt", ".jpg")), train_dir) copy2(os.path.join(label_dir, f), train_label_dir) for f in val_files: copy2(os.path.join(image_dir, f.replace(".txt", ".jpg")), val_dir) copy2(os.path.join(label_dir, f), val_label_dir)

这里用了分层采样,先把包含某个类别的图片单独拎出来,再按比例抽取,确保“愤怒”这类长尾类别不会在验证集里消失。训练集比例取 0.85,如果数据总量少于 1000 张,可以提高到 0.9。这里有一个常见的翻车点:按图片数切分看似没问题,但一张图里可能同时有两只狗、两个情绪框,按图片数切分会让验证集和训练集出现内容重叠,所以我按标签文件数来切,并且只关心标签里出现过的类别。

划分完成后,数据集目录结构就是标准的 YOLO 布局:

dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/

最后写一个data.yaml,这是训练时唯一要指定的数据集配置文件,类别列表顺序必须与上一步的 CLASSES 完全一致。

path: dataset train: images/train val: images/val names: 0: angry 1: anxious 2: calm 3: happy 4: sad

3. 训练自己的识别模型:环境、参数配置与损失函数解读

3.1 环境搭建与预训练权重

训练环境是第一个拦路虎,网上搜“YOLO 环境配置”能看到的坑基本都集中在 CUDA 和 torch 版本不匹配上。资源包里的代码基于 YOLOv8(ultralytics 框架),Python 版本 3.8 到 3.10 都能跑,我建议用 3.10。

# 创建虚拟环境,避免污染系统 Python conda create -n yolo python=3.10 -y conda activate yolo # 安装 PyTorch,先确认本机 CUDA 版本再选择对应命令 pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 # 安装 ultralytics pip install ultralytics==8.0.100

这里不要直接pip install ultralytics了事,锁版本很有必要。YOLOv8 的 API 在 8.0.x 之后有过一些调整,资源代码是在这个版本下调试过的,升到 8.2 以上有些接口签名会变,新手照抄代码可能跑不通。安装完成后用yolo predict source="https://ultralytics.com/images/bus.jpg"验证环境,能出结果说明环境正常。

预训练权重建议用yolov8s.pt而不是yolov8n.pt。虽然 nano 模型更小、训练更快,但情绪特征(嘴角弧度、耳朵位置)非常细微,nano 的浅层特征表达力不够,最终 mAP 会比 s 低 5 到 8 个点。如果显卡显存低于 6G,再退回到yolov8n.pt。

3.2 训练命令与超参数设置

训练命令看起来简单,真正决定结果的是参数。下面是我在这套资源上调通的一组基线参数。

yolo train \ model=yolov8s.pt \ data=dog_emotion.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ patience=30 \ optimizer=AdamW \ lr0=0.001 \ lrf=0.01 \ mosaic=1.0 \ fliplr=0.5 \ scale=0.3 \ project=runs/detect \ name=dog_emotion_train

逐个说参数含义。epochs=100对于这个规模的数据集够用,太多会过拟合,太少不收敛;imgsz=640是训练输入分辨率,如果显卡允许,改成 768 对情绪识别有明显帮助,因为脸部关键特征在低分辨率下容易糊掉;batch=16由显存决定,显存 8G 用 16,12G 以上可以到 32;patience=30表示连续 30 个 epoch 验证集指标没有提升就提前停止,这是防止时间浪费的保险丝;optimizer=AdamW比默认的 SGD 收敛更快,尤其适合迁移学习场景。

mosaic=1.0是马赛克增强,把四张图拼成一张训练,对小目标检测非常有效,但如果训练集里单张图只有一只狗,mosaic 过强会在早期造成“半个头”的幻觉,训练初期 loss 可能不降反升。真遇到这种情况,把mosaic=0.5跑几个 epoch,后期再调到 1.0 也行。

训练过程中重点看两行输出:box_loss和cls_loss。box_loss 是检测框回归误差,cls_loss 是分类误差。情绪识别的难点在 cls_loss 上,如果训练了十几轮 cls_loss 还在 1.5 以上,先想到的是数据问题而不是模型问题。

3.3 损失函数与训练曲线怎么看

YOLOv8 的损失由三部分构成:分类损失(BCE 变体)、框回归损失(CIoU)、分布式焦点损失(DFL)。DFL 是 YOLOv8 相对 v5 的新增项,它把边界框的四个边当成分布来回归,而不是直接预测数值,这提升了小目标的定位精度,代价是训练时对学习率更敏感。

我习惯每 10 个 epoch 停下来看一下results.png里的曲线。有几个异常信号需要记住:

  • train/box_loss快速下降但val/box_loss震荡上升,这是过拟合的早期信号,立即加大weight_decay或降低epochs;
  • cls_loss在训练初期就在 0.3 以下,说明类别分布严重不均衡,模型在走捷径,优先学容易的类别;
  • 训练开始就出现nanloss,几乎可以肯定是学习率过大或某张图的标注框坐标异常(比如宽高为 0),去检查数据集比调参更有用。

训练结束后在runs/detect/dog_emotion_train/目录下会生成best.pt和last.pt,后续推理用best.pt。这个目录还包含confusion_matrix.png,它比 loss 曲线更能说明问题——如果对角线很亮但某个类别的列全黑,那就是典型的长尾漏检。

4. 推理与部署:从单张图片到实时摄像头识别

4.1 单图推理脚本

训练完模型后的第一件事是用它跑几张训练集之外的图片,验证基本效果。下面这段脚本可以直接复制使用。

# inference_image.py from ultralytics import YOLO model = YOLO("runs/detect/dog_emotion_train/weights/best.pt") results = model.predict( source="test_images", conf=0.35, iou=0.45, imgsz=640, save=True, save_txt=True, device=0 ) for r in results: boxes = r.boxes if boxes is not None: for box in boxes: cls_id = int(box.cls[0].item()) conf = float(box.conf[0].item()) print(f"class={model.names[cls_id]} confidence={conf:.3f}")

conf=0.35是置信度阈值,低于这个值的检测框会被丢弃。情绪识别的置信度普遍比普通目标检测低,因为狗脸角度变化大,设 0.35 到 0.4 是一个平衡点。iou=0.45是 NMS 交并比阈值,值越小抑制越强,同一只狗被框两次的场景可以调到 0.5。打印类别名和置信度有助于快速定位哪些图片误检严重。

4.2 摄像头实时检测

课程设计答辩现场用摄像头实时演示,效果远比静态图片震撼。这里给一个接 OpenCV 的实时推理版本。

# inference_webcam.py import cv2 from ultralytics import YOLO model = YOLO("runs/detect/dog_emotion_train/weights/best.pt") cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) while True: ret, frame = cap.read() if not ret: break results = model.predict(frame, imgsz=640, conf=0.4, device=0, verbose=False) annotated_frame = results[0].plot() fps = cap.get(cv2.CAP_PROP_FPS) cv2.putText(annotated_frame, f"FPS: {fps:.0f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow("Dog Emotion Recognition", annotated_frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

实时推理时我把imgsz从 640 降到 416,这不是为了精度,而是为了帧率。摄像头场景下运动模糊已经让细小情绪特征不可靠,640 的输入会拖慢推理速度,反而导致画面卡顿。这里有个隐藏问题:results[0].plot()会在原图上绘制检测框和标签,但如果检测到多个框且类别置信度不高,画面上的字会非常多,答辩时反而显得 demo 杂乱,可以只在画面上画置信度高于 0.5 的框。

4.3 模型导出与边缘部署

训练好的 PyTorch 权重不能直接跑在手机上或者 Jetson 这类边缘设备上,需要导出成 ONNX 或 TensorRT 格式。

yolo export model=runs/detect/dog_emotion_train/weights/best.pt format=onnx imgsz=640 opset=12 yolo export model=runs/detect/dog_emotion_train/weights/best.pt format=engine device=0 half=True

ONNX 导出的关键是opset版本,设成 12 兼容性最好,新版 PyTorch 默认的 opset 17 在部分老设备推理引擎上会报不支持的算子。TensorRT 导出前先确认本机 CUDA 版本和 TensorRT 版本匹配,否则引擎构建到一半会报显存问题。half=True开启 FP16 精度,推理速度提升明显但精度有轻微损失,情绪识别这个任务影响不大。

如果在树莓派或者 RK3588 这类设备上部署,ONNX 格式是更稳妥的选择,配合 RKNN-Toolkit 转换工具可以做硬件加速。这方面的行话叫“边缘部署监控误检率高”,核心原因是边缘设备上为了性能往往会降低输入分辨率,而情绪特征恰恰依赖细节,所以部署时的输入分辨率不要小于 512。

5. 常见问题与避坑:训练不收敛、BN 崩溃与误检排查

5.1 训练阶段:BN 崩溃、过拟合与损失异常

现象一:训练到第 20 个 epoch 左右,loss 突然变成nan,控制台开始刷红色警告,模型权重无法保存。

这大概率是训练中 BN 崩溃(BatchNorm blowup)。产生原因通常是学习率设置过高,加上早期批次数据里某些标注框异常,导致某一层的统计量爆炸。

解决方法是先把lr0降到 0.0005,同时把batch设成 8 或 16 的倍数,确保每个批次里的样本数足够让 BN 统计量稳定。另一个常见做法是在yolo train命令里加cache=True,把数据缓存到内存,减少读取抖动带来的输入异常。

现象二:训练集精度升到 95% 以上,验证集 mAP 却只有 50%,而且还在往下掉。

这是典型的过拟合,在小数据集上非常普遍,尤其是清洗后图片数少于 800 张时。

对策不止一个,我建议按顺序操作:先加weight_decay=0.0005,再把mosaic关掉或降到 0.3,然后尝试用yolov8s-cls.pt的分类预训练权重做迁移初始化。注意,这不是玄学,是超参数和数据规模不匹配的必然结果。

现象三:val/cls_loss曲线一路平坦,怎么调参都下不去。

先检查你的data.yaml里类别顺序是否和训练标签的 class_id 对应。我见过不止一次因为 CLASSES 列表顺序变了,导致模型把“开心”学成了“焦虑”的惨案。模型没毛病,是标签语义对不上。

5.2 推理阶段:误检、漏检与混淆矩阵异常

现象四:视频里对着墙壁或地板也会框出一个小方框,置信度还不低,打上了某个情绪类别。

原因是背景过拟合。如果训练集里大量图片的背景都是室内地板或狗窝,模型会把纹理特征当成判断依据。

解决方式是做数据增强:加hsv_h=0.5 hsv_s=0.5 hsv_v=0.5随机改变色调饱和度,降低颜色纹理对分类的依赖;再用translate=0.1让目标在画面里移动,破坏固定的位置关联。推理时把conf调高到 0.5 也能过滤一部分背景误检。

现象五:confusion_matrix.png里所有类别的横排总和明显大于纵排总和,甚至出现奇怪的对角线。

搜过“YOLO 混淆矩阵总合不唯一”的应该都知道,目标检测的混淆矩阵计算逻辑与图像分类不同,它按预测框是否与真实框匹配来计数,一个真实框可能被多个预测框命中,一个预测框也可能对应多个类别得分。

看混淆矩阵时不要追求行和列相等,重点看每个类别是否在对角线上有足够高的值。某一行除对角格外还有高响应,说明模型在类别间混淆,优先补区分这两个类别的数据,比调参更有效。

6. 进阶:注意力机制改进与 TensorBoard 验证习惯

6.1 给 YOLOv8 加一个轻量注意力模块

如果完成基础版本之后还有余力,给模型加一个 SE(Squeeze-and-Excitation)注意力模块是性价比很高的改进方向。犬类情绪识别依赖的耳朵位置、嘴角弧度这些细小特征,注意力机制能让特征提取网络更关注这些区域,同时计算开销非常小。

# common.py 中添加的 SE 模块 import torch import torch.nn as nn class SEBlock(nn.Module): def __init__(self, channels, reduction=16): super(SEBlock, self).__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Sequential( nn.Linear(channels, channels // reduction, bias=False), nn.ReLU(inplace=True), nn.Linear(channels // reduction, channels, bias=False), nn.Sigmoid() ) def forward(self, x): b, c, _, _ = x.size() y = self.avg_pool(x).view(b, c) y = self.fc(y).view(b, c, 1, 1) return x * y.expand_as(x)

这个模块通过全局平均池化把每个通道压缩成一个值,再用两层全连接学习通道间的关系,最后用 Sigmoid 产生 0 到 1 的通道权重乘回原特征图。reduction=16控制中间层压缩比,值越大参数越少,但信息损失也越多。修改完模型结构后,训练命令不变,只是在yaml配置里替换对应层的模块名。

6.2 用 TensorBoard 对比改进前后的训练曲线

改进是否有效,不能靠感觉。ultralytics 框架自带 TensorBoard 支持,但需要先安装依赖再启用。

pip install tensorboard tensorboard --logdir runs/detect

打开http://localhost:6006就能同时看到基线模型和注意力改进模型的 loss 曲线。我通常会对比两组指标:val/cls_loss和mAP@0.5:0.95。如果加了注意力模块后cls_loss更低但box_loss略高,这是正常的,注意力机制本质上是牺牲一部分定位精度换取分类精度,在情绪识别这个任务上是划算的。

对比训练时还有个习惯值得养成:用固定的random_seed=42让两次训练的数据增强顺序一致。如果不固定种子,两次训练的提升可能是随机性造成的,那就失去了对比的意义。

这个资源我前后拆了三遍,第一遍踩在数据清洗上,第二遍死在 BN 崩溃,第三遍才把训练跑顺。从那以后我每次做目标检测项目,都会强制自己先写一个数据分布统计脚本,再进训练环节。模型可以迭代,数据问题不能靠模型硬扛。完整的标签转换脚本、分层采样代码和训练配置都在资源包里,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 15:56:58

Java小白生存地图:免费资源网站避坑指南与上岸路径

1. 这不是一份“网站清单”&#xff0c;而是一张Java小白的生存地图你搜“Java免费资源网站”&#xff0c;页面刷出来几百个链接&#xff0c;点开三个&#xff1a;一个首页写着“2024最新”&#xff0c;点进去发现最后更新是2021年&#xff1b;一个标榜“全网最全”&#xff0c…

作者头像 李华
网站建设 2026/9/26 15:56:19

Ajenti 插件开发指南:通过 HttpPlugin 与 @endpoint 构建 HTTP 处理接口

后端运维 【免费下载链接】ajenti Ajenti Core and stock plugins 项目地址&#xff1a; https://gitcode.com/gh_mirrors/aj/ajenti 点击查看 免费下载 导读 本文围绕 docs/source/dev/http.rst 的开发者文档展开&#xff0c;系统讲解 Ajenti 插件如何注册并处理 HTTP 请求&…

作者头像 李华
网站建设 2026/9/26 15:54:40

快乐8数据预测工具:历史开奖统计与走势分析实战

简介&#xff1a;这是一套面向快乐8&#xff08;KL8&#xff09;彩票走势分析的个人学习型工具&#xff0c;采用Python工程化结构&#xff0c;可直接本地部署、开箱即用&#xff0c;适合具备一定Python基础、希望用数据化方式复盘历史开奖的爱好者。资源包共75个文件&#xff0…

作者头像 李华