最近不少同学在问软件学院的项目实训该选什么方向,我刚好在带某高校软件学院的2026年度实训课题,其中有一个题目是“宠物情绪识别”。这个题目听起来有点萌,但做起来其实是一个非常典型的计算机视觉落地项目:数据采集、标注、模型训练、服务封装、前后端联调,一个完整闭环全都有。这篇文章先记录第一部分内容——从题目拆解到方案选型,再到数据准备和开发环境搭建,把前期的关键步骤和踩过的坑一并写出来。
如果你也在纠结实训选型,或者想做一个“能跑起来、能演示、能答辩”的项目,宠物情绪识别是个不错的选择。它不算太复杂,但足够承载一个团队的工作量:有人做模型、有人做后端、有人做前端,还可以往嵌入式或者移动端方向扩展。这篇文章会尽量写得细一点,适合刚接触深度学习、又想完整走一遍项目流程的同学。
1. 项目选题与需求拆解:这个题目到底在做什么
1.1 为什么选宠物情绪识别
宠物情绪识别的出发点很直接:越来越多的家庭把猫狗当成员养,但宠物不会说话,情绪状态只能靠主人猜。很多宠物用品厂商、智能喂食器、自动猫砂盆甚至宠物医疗设备,都在考虑加入“读懂情绪”的能力。从技术角度看,它属于图像分类问题的变体,但和普通物体分类不同的是,情绪是抽象标签,标注时有很强的主观性,这反而让项目更有真实感。
作为软件学院的项目实训,选这个题目还有一个原因——它足够深,也足够广。深,是指可以走到深度学习模型微调、Attention可视化、多模态融合;广,是可以拆成数据工程、模型训练、前后端开发、部署演示多个方向。一个小组三四个人,刚好一人扛一块,不会没事干,也不会干不了。
1.2 目标场景与功能边界
我们在定义需求时,没有一上来就谈算法,而是先把使用场景画清楚。假设用户是一个宠物主人,手上有一只猫或狗,想知道此刻宠物是开心、放松还是焦虑。他拿出手机拍一张照片,系统返回情绪标签和置信度,最好再给一段简单的行为建议。这就是第一个版本要交付的最小可用产品。
功能边界分成三层:
- 基础功能:上传宠物照片,识别情绪,返回结果。
- 扩展功能:历史识别记录、多宠物体征档案、情绪趋势统计。
- 非功能要求:单次识别延迟控制在2秒以内,能在普通笔记本CPU上跑训练,模型文件大小控制在200MB以内。
这些约束不是拍脑袋定的,而是考虑到实训周期大概12到16周,团队平均每天能投入4到6小时。如果一开始就追求多模态、实时视频流,大概率做不完。把边界收窄,才能把一个闭环做透。
1.3 用户画像与场景假设
最后再想清楚给谁用。我们定的核心用户是宠物主人,辅以宠物店、宠物医院的工作人员。这两种用户的理解能力差别很大:主人不会关心你用的哪款网络结构,只想知道“我家狗现在是不是害怕”;店员更在意操作是否快捷。因此前端交互要足够简单,识别结果不能用干巴巴的标签,得配一句人话,比如“看起来有点紧张,建议先带它离开嘈杂环境”。
这个定位也直接影响了结果展示设计,从模型输出的数字到用户能读懂的反馈,中间需要一层规则转换。这也是项目实训中容易忽略的一环——大家普遍盯着准确率数字,却忘了最终用户要的是可行动的提示。
2. 技术选型与整体方案:不追新,只求稳
2.1 模型方案的选择逻辑
宠物情绪识别本质上是个图像分类任务。第一反应是上CNN,但直接从头训练一个CNN在几千张图片上很难收敛。实训项目要的是在有限时间内达到可演示效果,所以重点考虑两种思路:微调预训练模型,或者用现成的分类网络做特征提取。
最终我们选了微调方案,即加载一个在大规模图像数据集上预训练好的模型,把最后一层分类头替换成自己的情绪分类器,只训练后面几层。选它的原因很简单:收敛快、精度可控、抗过拟合能力强。对比测试下来,哪怕只用大约3000张训练图,也能在验证集上拿到接近80%的准确率,这个效果足以支撑演示和答辩。
具体框架选PyTorch,原因是生态成熟、社区教程多、调试相对直观。TensorFlow也不是不行,但考虑到学生对调试工具链的熟悉程度,PyTorch的上手门槛略低。
2.2 前后端架构设计
后端选择FastAPI。因为项目规模不大,Django和Flask都可以,但FastAPI有一个优势,它对异步处理支持好,模型推理这种IO密集任务不需要额外引入太多框架就能调用。前端我们做了一个极简的Web页面,用HTML加一点点原生JavaScript,同时预留了小程序端的接口。之所以不做成小程序,是因为实训期间申请小程序账号、审核体验比较折腾,Web页面演示更方便。
整体数据流是这样:用户通过前端页面上传图片,后端接口接收文件后调用模型进行推理,模型输出一个情绪标签和一组概率值,后端再把标签映射为面向用户的中文提示和文案,最后返回给前端渲染。整个过程用语言描述是这样的逻辑顺序,不需要复杂的架构图,但代码里要严格按照这个方向解耦。
2.3 为什么把部署也纳入实训范围
很多项目实训到最后就停在了“模型在notebook里准确率不错”这个阶段,这其实是半成品。为了让项目真正可验收,我们把部署纳入必修环节:即使只跑在本机,也要提供一致的调用接口,让前端可以随时调起模型。这份约束迫使团队必须处理模型输入输出的规范化、内存释放、并发安全这些实战问题。虽然前期会多花一点时间,但后期联调会非常顺利。
3. 情绪类别定义与数据准备:不重视这步,模型直接白练
3.1 情绪类别怎么定最合理
“宠物情绪”是一个很宽泛的概念。查阅动物行为学资料再结合实际可行性,最常见的猫情绪有放松、紧张、兴奋、攻击、不安、快乐;狗同理。但一开始如果我们设定太多类别,比如八类十类,数据量根本撑不住,类间混淆也会很严重。
我们最终定了6个类别:开心、放松、紧张、害怕、生气、难过,外加一个“不确定”作为兜底,实际训练时先把“不确定”排除。这样每个类别大概收集500张以上图片,总数据量在3000张出头,训练起来压力不大。
还有一点很关键:情绪标签不是互斥的,现实中一只狗可能既紧张又兴奋,但我们做分类任务时强制要求单选。所以在标注时要定一个“主情绪”规则,告诉标注人员:如果出现复合情绪,只选表现最明显的那一个。这个规则如果不提前定,后面数据一致性会很差。
3.2 数据来源与清理
数据来源主要有两个:一是开源的猫狗情绪数据集,二是自己拍摄加网络爬取。开源数据集的好处是节省时间,但坏处也很明显——标注质量参差不齐,有些图片里宠物独占画面很小,背景信息太多。所以数据清理绝对不能跳过。
清理过程我们干了这么几件事:
- 删除模糊、过暗、过曝的图片,保留清晰且主体在画面中央的。
- 统一尺寸到224×224,使用双线性插值缩放,避免直接拉伸变形。
- 排除含有明显水印、多只宠物同时出现、人类手指遮挡画面的图片。
- 对标签做二次审核,同一张图在两个标注人员意见不一致时,交给第三人裁决。
这个过程听着琐碎,但特别影响最终效果。有一次我们偷懒,没删掉一批背景全是沙发的图,结果模型把“沙发纹理”当成了特征,验证集上准确率虚高,换到真实场景立刻暴跌。
3.3 数据增强策略与实现
数据增强是必须做的。我们用的增强方法包括:水平翻转、随机旋转(±15度)、亮度饱和度调整、随机裁剪缩放、加一点高斯噪声。OpenCV配合PyTorch的transforms可以很方便地实现。
有一点要特别提醒:旋转角度不要太大,宠物是既定朝向的生物,翻转90度或者180度的数据没有真实性,反而会干扰模型学到的姿态特征。亮度扰动幅度也要控制,太夸张会让模型过度鲁棒,反而损失对真实色彩的敏感度。
4. 开发环境搭建与项目初始化:先把路铺平再开车
4.1 环境版本与安装步骤
开发环境我用的是Python 3.10,配合CUDA 11.8版本的PyTorch。如果机器没有NVIDIA显卡,也不用慌,这个规模的数据量用CPU跑也能出结果,只是训练时间会拉长到原来的三四倍。下面这组命令是在Ubuntu环境下的安装过程:
python3 -m venv pet-env source pet-env/bin/activate pip install --upgrade pip pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python pillow numpy fastapi uvicorn python-multipart scikit-learn matplotlibpython-multipart必须装,不然FastAPI接收文件上传请求时会报错,这是很容易踩的坑。
安装完成后,先跑一段简单的GPU检查代码,确认环境没问题再继续。
import torch print(torch.__version__) print(torch.cuda.is_available())这一步值得单独写出来,因为实训过程中有两次都是环境没装好,同学们硬着头皮往下写代码,最后报错根本分不清是环境问题还是代码问题。
4.2 项目目录结构设计
我们按工程化思路组织了目录,结构如下:
pet-emotion/ ├── data/ │ ├── raw/ 原始图片 │ ├── processed/ 清洗后的图片 │ ├── labels.csv 标签文件 ├── models/ 模型保存位置 ├── utils/ 数据处理、可视化等脚本 ├── app/ │ ├── main.py FastAPI入口 │ ├── predict.py 推理封装 ├── notebooks/ 探索性分析笔记本 ├── requirements.txt 依赖锁定 └── README.md目录结构的好处是责任清晰:负责数据的人只动data和utils,负责模型的人只动models和训练脚本,前端联调的人看app。不要让所有人把代码堆在同一个notebook里,否则等到集成时会非常痛苦。
4.3 依赖锁定与版本管理
requirements.txt不要手写,而是用pip freeze生成,这样能避免版本不一致导致的crash:
pip freeze > requirements.txt同时建议整个项目初始化一个Git仓库,每次改动提交一次,哪怕是一个人做也要养成这个习惯。实训过程中模型实验会有很多次,如果没有版本管理,想回退到某个效果好的模型权重会非常麻烦。
5. 核心模块实现:从数据加载到模型训练,逐步打通
5.1 自定义数据集的加载要点
不要直接拿文件夹分类当标签,我们使用一个CSV文件管理所有图片路径和标签,这样后面做数据划分、清洗记录、错误样本回溯都更方便。自定义Dataset的核心代码大致是这样:
import pandas as pd import torch from torch.utils.data import Dataset from PIL import Image from torchvision import transforms class PetEmotionDataset(Dataset): def __init__(self, csv_file, img_dir, transform=None): self.df = pd.read_csv(csv_file) self.img_dir = img_dir self.transform = transform self.label_map = {label: idx for idx, label in enumerate(sorted(self.df['label'].unique()))} def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.iloc[idx] img_path = f"{self.img_dir}/{row['filename']}" image = Image.open(img_path).convert('RGB') label = self.label_map[row['label']] if self.transform: image = self.transform(image) return image, label数据划分很有讲究。我们按类别分层抽样的方式,把数据分成训练集、验证集、测试集,比例是7:2:1。分层抽样很重要,不然可能某个类别全部跑到了训练集,验证集上该类别一张测试图都没有。
5.2 模型微调的关键参数选择
预训练模型我们使用了ResNet50,理由很简单:边缘设备也跑得动,特征提取能力足够,社区资料多。微调时,把最后一层全连接层从默认的1000类改成6类,并且只训练最后一层和最后一个残差块,前面的层全部冻结。这样做能极大减少计算量,同时保留预训练模型学习到的通用特征。
import torchvision.models as models import torch.nn as nn model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) for param in model.parameters(): param.requires_grad = False num_features = model.fc.in_features model.fc = nn.Linear(num_features, 6) for param in model.layer4.parameters(): param.requires_grad = True优化器用Adam,学习率初始值建议设在1e-4左右,比常见的1e-3要低一些,因为微调阶段L2正则与学习率配合不当很容易把预训练权重冲坏。损失函数用CrossEntropyLoss,同时设置label smoothing为0.1,这个细节能让类别间的关系不那么绝对,对宠物情绪这种本身带有模糊性的问题很有帮助。
5.3 训练过程与收敛监控
训练时记录每个epoch的训练损失、验证损失和准确率。除了看准确率,更要关注验证损失:准确率上升但验证损失开始反弹,是过拟合的信号。我们加了Early Stopping,连续5个epoch验证损失不下降就停止,保存验证损失最小的模型权重。
best_loss = float('inf') patience = 0 for epoch in range(epochs): train_loss = train_one_epoch(model, train_loader, optimizer, criterion) val_loss, val_acc = evaluate(model, val_loader, criterion) if val_loss < best_loss: best_loss = val_loss torch.save(model.state_dict(), 'models/best_model.pth') patience = 0 else: patience += 1 if patience >= 5: print("Early stopping triggered") break这个训练流程跑完大概需要几十分钟,取决于显卡。建议把训练日志存下来,后面写报告或者答辩时可以直接引用曲线图。
5.4 封装成API服务
训练完成后,最容易被忽略的是模型部署封装。我们单独写了一个推理模块,加载模型权重并做预处理和后处理:
# app/predict.py import torch from PIL import Image from torchvision import transforms class EmotionPredictor: def __init__(self, model_path, device="cpu"): self.device = device self.model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) num_features = self.model.fc.in_features self.model.fc = nn.Linear(num_features, 6) self.model.load_state_dict(torch.load(model_path, map_location=device)) self.model.eval() self.transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) def predict(self, image: Image.Image): img = self.transform(image).unsqueeze(0).to(self.device) with torch.no_grad(): outputs = self.model(img) probs = torch.softmax(outputs, dim=1) return probsFastAPI接口负责接收上传文件并调用这个类,再把结果映射成用户可读的文案。这里有一个很容易忽略的细节:预测时一定要调用model.eval(),否则BatchNorm和Dropout的行为会不一致,导致线上和训练时的推理结果漂移。
6. 实训过程中常见问题与排查实录
6.1 图片上传后接口报错:Unsupported Media Type
这是FastAPI新手最容易遇到的问题。上传文件需要接收UploadFile,并且路由要声明File(...)参数,同时不要忘了安装python-multipart。如果已经安装还报错,多半是路由定义里没有声明@app.post("/predict")的content-type,前端用FormData上传即可。另外建议先写一个最简单的echo接口验证上传链路,再接入模型推理,方便定位问题出在哪一层。
6.2 训练正常但验证集准确率忽高忽低
一个典型现象是训练loss在降,验证集准确率却上蹿下跳。先不要急着调模型,优先检查数据划分是否存在类别分布不均衡。我们用sklearn.model_selection.train_test_split的stratify参数按标签分层划分,问题就解决了。如果确认划分没问题,再看验证集图片是否存在清洗不干净的低质量图像。
6.3 GPU显存不足:CUDA Out Of Memory
实训机房的显卡显存一般不大,ResNet50跑批大小为32时可能会爆显存。不是所有时候都需要换显卡,可以先降低batch size到16或者8,同时开启pin_memory=True、num_workers=2加速数据读入。如果还是不够,把输入图片size从224降到192,效果差别很小但显存占用明显下降。
6.4 CPU推理太慢,前端体验卡顿
演示阶段如果用CPU跑ResNet50,单张推理可能需要2到3秒,体验很差。我们做了两个优化:一是把模型torchscript化,去掉动态图的额外开销;二是限制同时推理的请求数,用简单的队列串行化请求,避免多个请求同时加载模型导致更慢。实训阶段最推荐的做法是直接不加载到GPU,纯CPU推理加队列,虽然单次慢了但整体稳定。
7. 项目后续迭代方向与个人体会
7.1 模型的扩展空间
第一版只用了静态图片,后续完全可以加入宠物音频、肢体动作等模态信号。比如狗的吠叫频率、尾巴摆动速度,这些在情绪判断中有很大信息量。多模态融合后模型会更接近真实的情绪识别场景,但这会让实训工作量成倍增长,建议在有基础版本能演示之后再做扩展。
另外可以把模型换成轻量化网络,比如MobileNetV3或者EfficientNet-Lite,压缩后部署到树莓派或者手机端,做一个纯离线的情绪识别装置。这个方向虽然对工程能力要求高,但展示效果非常抢眼,特别适合答辩加分。
7.2 我在整个实训过程中的几点感受
第一,项目实训最怕的不是技术难,而是团队每个人各做一块最后拼不起来。所以结构设计上从第一天就必须一致,数据格式、接口约定、模型输入输出都要提前对齐。第二,宠物情绪识别是个看着有趣做起来磨人的题目,光数据清洗就花掉了整个项目大概两周时间,但这段时间对最终模型效果贡献最大,值得。
我个人的建议是:如果让你重新选一次实训题目,依然可以选这个,但一定把重心放在“数据质量”和“工程闭环”上,而不是纠结哪款预训练模型更强。做得完整、做得下沉,比做得高深但跑不通有价值得多。后面我会接着写模型训练结果分析以及前端联调的细节,到时候再和大家分享。