☰
基于Python、CNN与Flask的宠物行为识别系统设计与实现
2026/10/20 2:50:11 网站建设 项目流程

选题一眼看过去是典型的计算机毕设方向:基于 Python 的 Web 网页版 + CNN 卷积神经网络 + 宠物行为识别。这类题目在深度学习入门项目里非常常见,但真正做下来你会发现,它比单纯跑一个 MNIST 分类器复杂得多——因为你需要把模型训练、图像预处理、Web 交互、前端展示全部串起来,形成一个闭环。这篇文章我会把一个可落地的完整方案拆开讲,从模型结构设计、数据集组织、训练调参,到 Flask 网页部署和常见坑,适合正在做类似毕设、或者想用宠物行为识别练手 CNN 项目的同学参考。


1. 项目整体设计与技术选型

1.1 这个毕设到底在做什么

先把这个题目翻译成人话:你想通过摄像头或者上传一张宠物照片,让网页自动判断这只宠物当前在做什么行为——比如狗是在“坐”“卧”“站立”“奔跑”,猫是在“舔毛”“进食”“睡觉”。核心流程是一条直线:图片输入 -> CNN 提取特征 -> 分类器输出行为标签 -> Web 前端展示结果。

这个题目本质是图像分类任务,只不过把通用的“猫狗分类”“车型识别”换成了宠物行为识别场景。它同时包含两个模块:一是深度学习模型,二是 Web 网页系统。很多毕设只做其中一块,但这道题要求你两者都做,所以工作量主要在“打通全链路”,而不只是写好一个 CNN。

适合什么人参考?如果你是第一次做深度学习项目,这个选题很合适——它不要求你发明新算法,只需要会用 CNN 做分类,并且能把模型部署到网页上展示。如果你是准备毕设答辩的同学,这套方案也能帮你把“模型训练”“系统实现”“效果展示”三个环节都讲得有据可查。

1.2 为什么选 Python + Flask + CNN 这套组合

选择技术栈时我直接把方案定为:Python 3.10 + PyTorch 1.13 + Flask 2.3 + Bootstrap 前端。理由是:

第一,Python 是深度学习生态最完善的语言,PyTorch 写网络结构比 TensorFlow 1.x 时代的静态图直观太多,调试时可以打印每一层的输出尺寸,特别适合新手。

第二,Flask 足够轻量。这个项目不需要数据库,不需要用户系统,核心接口就两个——GET 返回页面、POST 接收图片并返回预测结果。用 Flask 几十行代码就能搞定,不需要上 Django。

第三,网页端用原生 HTML + 少量 JavaScript 足够了。很多同学一上来就想着前后端分离、Vue 框架,其实完全没必要。这个题目的展示重点是“模型识别效果”,不是“页面交互复杂度”。

我这套组合在实验室机器和普通笔记本上都能跑。CPU 也能训练,只是慢一些;如果有 NVIDIA 显卡,训练速度会快 5-10 倍。

1.3 项目目录结构与模块划分

这是我实际采用的目录结构:

pet_behavior/ ├── app.py # Flask 主程序 ├── model.py # CNN 网络定义 ├── train.py # 训练脚本 ├── predict.py # 单张图片预测脚本(调试用) ├── requirements.txt # 依赖清单 ├── data/ │ ├── train/ │ │ ├── sit/ │ │ ├── stand/ │ │ ├── lie/ │ │ ├── run/ │ │ └── bark/ │ ├── val/ │ └── test/ ├── models/ │ └── best_model.pth # 训练好的权重 ├── static/ │ └── uploads/ # 用户上传图片临时存放 └── templates/ └── index.html # 前端页面

这个结构的好处是每块职责清晰:model.py只管网络结构,train.py只管训练,app.py只管 Web 服务。很多毕设代码把所有功能塞进一个文件,后期自己都改不动,答辩时被老师一追问就露馅。


2. CNN 模型架构与训练细节

2.1 为什么 CNN 适合宠物行为识别

宠物行为识别本质上是在识别图像里的“空间特征组合”。比如判断一只狗是在“坐”还是“站”,关键特征是后腿是否弯曲、前腿是否竖直、身体轮廓的宽高比。这些特征与图片里的像素位置直接相关,而 CNN 的卷积操作正好是在局部区域上做特征提取。

卷积神经网络有两个核心特性:局部感受野和权值共享。局部感受野就好比人观察图片时先看眼睛、鼻子、嘴巴这些局部区域,而不是一下子扫描整张图;权值共享则是说同一个卷积核会在整张图上滑动,用同一套“模板”去匹配不同位置是否有相似特征。这两点让 CNN 比全连接网络参数少得多,也更适合图像数据。

对宠物行为识别来说,行为类别之间的差异主要体现在姿态和轮廓,而不是颜色或纹理。所以我会在数据预处理阶段把图片统一缩放到 64x64 或 128x128,保留姿态轮廓信息,同时尽量减小模型计算量。

2.2 网络结构:从输入层到全连接层

我用的网络是一个三层卷积的 CNN,结构如下:

Input: (3, 64, 64) # RGB 图片,缩放到 64x64 Conv1: 3 -> 16, kernel=3, padding=1 + ReLU + MaxPool(2) Conv2: 16 -> 32, kernel=3, padding=1 + ReLU + MaxPool(2) Conv3: 32 -> 64, kernel=3, padding=1 + ReLU + MaxPool(2) Flatten: 64 * 8 * 8 = 4096 FC1: 4096 -> 128 + ReLU + Dropout(0.5) FC2: 128 -> num_classes

为什么输入用 64x64 而不是 224x224?因为宠物行为分类不需要太精细的纹理特征,64x64 已经能看清姿态轮廓。而且输入尺寸直接影响全连接层的参数量:如果输入是 224x224,经过三层池化后特征图是 28x28x64,展平后就有 50176 个神经元,全连接层参数量会爆炸,训练容易过拟合。

特征图尺寸的变化有固定计算公式。以 64x64 输入为例,每经过一个 kernel=3 且 padding=1 的卷积层,尺寸不变;每经过一次 MaxPool(2),尺寸减半。所以三组卷积池化后就是 64 -> 32 -> 16 -> 8,最后一层特征图是 8x8x64,展平后 4096。这个计算过程建议你自己推一遍,因为答辩时老师十有八九会问“你每一层输出尺寸怎么算的”。

PyTorch 代码实现如下:

import torch.nn as nn class BehaviorCNN(nn.Module): def __init__(self, num_classes=5): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 16, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 8 * 8, 128), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))

2.3 数据集准备与标注规范

数据集是这类项目最大的坑。宠物行为识别没有像 ImageNet 那样现成的公共数据集可以直接下载,所以大部分同学需要自己采集或通过网络图片整理。我的做法是:

  • 每个行为类别收集 400-600 张图片,一共 5 类,总计约 2500 张。
  • 训练集和验证集按 8:2 划分,测试集另外留 200 张,保证测试集不参与训练。
  • 图片内容尽量贴近真实场景:有室内,有户外,有不同光照条件。
  • 图片里宠物最好居中、占画面比例大,减少背景干扰。

数据目录结构就是 1.3 节里的那样,用文件夹名作为标签。使用 PyTorch 的ImageFolder可以自动读取目录并生成标签,非常方便。

标注时有个细节容易忽略:行为类别之间要有区分度,且不能重叠。比如“坐”和“蹲”如果都算一类,模型会混淆;但如果你定义的类别是“坐”“卧”“站”“跑”“叫”,这五类姿态差异明显,分类难度就低很多。如果题目要求识别的行为类别本身很相近,比如区分“摇尾巴”和“翘尾巴”,那建议增加数据量或者换用迁移学习。

2.4 训练参数与优化策略

训练超参数我直接给一套能用的配置:

batch_size = 32 learning_rate = 0.001 epochs = 60 optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) criterion = nn.CrossEntropyLoss() scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5)

学习率设置 0.001 是 Adam 优化器的默认推荐值,实测下来在这个任务上比较稳定。如果 loss 震荡不下降,就降到 0.0001;如果下降太慢,可以先用 0.003 跑几个 epoch 再降下来。

数据增强我用的是 PyTorch 的transforms组合:

train_transforms = transforms.Compose([ transforms.Resize((64, 64)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ])

训练过程中我记录每一轮的train_loss、val_loss和val_accuracy,并在val_accuracy创新高时保存模型权重。只保存最后一轮的权重有个问题:最后几轮可能已经过拟合,验证集准确率反而下降了。所以我按验证集表现来选最优模型,这个在下文 4.1 还会细讲。


3. Web 端交互与部署实现

3.1 Flask 快速搭建后端服务

Flask 部分的核心逻辑非常简单:启动时加载模型,收到图片后做预处理,丢给模型推理,返回预测结果。我把它拆成两个接口:

  • GET /:返回网页首页。
  • POST /predict:接收上传图片,返回 JSON。

代码主体如下:

import os import torch from flask import Flask, request, jsonify, render_template from PIL import Image import torchvision.transforms as transforms from model import BehaviorCNN app = Flask(__name__) model = BehaviorCNN(num_classes=5) model.load_state_dict(torch.load('models/best_model.pth', map_location='cpu')) model.eval() class_names = ['sit', 'stand', 'lie', 'run', 'bark'] class_names_zh = ['坐', '站立', '卧', '奔跑', '吠叫'] transform = transforms.Compose([ transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ]) @app.route('/') def index(): return render_template('index.html') @app.route('/predict', methods=['POST']) def predict(): file = request.files['image'] img = Image.open(file.stream).convert('RGB') img_tensor = transform(img).unsqueeze(0) with torch.no_grad(): outputs = model(img_tensor) probs = torch.softmax(outputs, dim=1) confidence, idx = torch.max(probs, dim=1) return jsonify({ 'class_name': class_names_zh[idx.item()], 'class_label': class_names[idx.item()], 'confidence': round(confidence.item(), 4) }) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)

注意五个细节:

  • map_location='cpu'是必须的,因为训练可能在 GPU 上,部署到没有 GPU 的机器上时没有这个参数会报错。
  • model.eval()一定不能漏。漏掉的话 BatchNorm 和 Dropout 在推理时的行为会不一致,预测结果会不稳定。
  • PIL Image.open()读取后要.convert('RGB'),否则遇到带透明通道的 PNG 图时会出错。
  • 前端上传的图片不能直接交给模型,必须做和训练时一样的预处理。这里最容易踩坑的就是忘记 Resize 或 Normalize。
  • torch.no_grad()在推理时加了能减少显存占用、提升速度,行列虽小,但一个负责任的实现都会加上。

3.2 前端页面的核心设计

前端页面我用的是单页面方案:一个图片上传区域、一个识别按钮、一个结果展示区。用户选择图片后可以预览,点击“开始识别”后通过fetch发请求到后端。

核心 HTML 片段:

<div class="container mt-5"> <h2>宠物行为识别系统</h2> <div class="mb-3"> <input type="file" id="imageInput" accept="image/*"> <img id="preview" class="img-fluid mt-3" style="max-width: 320px;"> </div> <button id="predictBtn" class="btn btn-primary">开始识别</button> <div id="result" class="mt-4"></div> </div>

对应的 JavaScript:

document.getElementById('predictBtn').addEventListener('click', function() { const fileInput = document.getElementById('imageInput'); if (!fileInput.files.length) { alert('请先选择图片'); return; } const formData = new FormData(); formData.append('image', fileInput.files[0]); fetch('/predict', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { document.getElementById('result').innerHTML = '识别结果:' + data.class_name + '(' + data.class_label + '),置信度:' + data.confidence; }) .catch(err => { console.error(err); alert('识别失败,请检查图片格式'); }); });

图片预览可以加一个监听事件change,用FileReader把本地图片显示到<img>标签里,这样用户能确认自己要识别的是哪张图。这个小功能在答辩演示时特别加分。

3.3 模型加载与预测接口封装

有一个问题需要注意:每次请求都重新加载模型是灾难性的。模型权重文件几十 MB,磁盘 IO 加上反序列化要好几秒,如果用户连续识别多次,页面会卡死。正确做法是在 Flask 应用启动时只加载一次,之后所有请求共享同一个模型实例。

对于模型文件,建议统一命名规则,比如models/best_model.pth。每次训练完把最优模型复制到这个固定位置,这样 Web 端代码不需要频繁改路径。

推理时如果想要更快的响应速度,可以考虑把图片先压缩再上传。比如前端在canvas里把图片缩到 256px 以内再发送,既能减少网络传输时间,也不会影响识别效果,因为模型输入本身只有 64x64。

3.4 本地部署与模型文件管理

本地部署的流程很简单:

  1. 安装依赖:pip install flask torch torchvision pillow
  2. 启动服务:python app.py
  3. 浏览器访问http://127.0.0.1:5000

如果需要让同一局域网的手机或另一台电脑访问,就用host='0.0.0.0',然后访问电脑的局域网 IP,端口 5000。这个演示方式在答辩现场很实用,老师可以在自己电脑上打开页面测试你的系统。

模型文件管理这块我吃过亏。最开始我把每个 epoch 的权重都保存在同一个文件里,结果训练到后期发现最优模型被覆盖了。后来改成按 epoch 命名 + 单独存一个 best 副本:

torch.save(model.state_dict(), f'models/epoch_{epoch}.pth') torch.save(model.state_dict(), 'models/best_model.pth')

同时定期清理无用的中间权重,避免磁盘空间被占满。一个 5 万参数的 CNN 权重文件不到 5MB,但如果是 ResNet 这种大模型,每份权重可能 40MB+,30 个 epoch 就超过 1GB,不清理很快撑爆磁盘。


4. 常见问题与排查技巧实录

4.1 训练阶段的高频问题

loss 不下降。优先检查学习率是否为 0.001;其次检查输入图片的归一化方式与模型是否匹配。很多人训练时用了Normalize([0.5],[0.5]),推理时忘了加,导致输入分布完全不同。如果都正常,看数据标签是否有错乱——比如训练集目录里放了错误类别的图片。

验证集准确率远低于训练集。这是过拟合信号。解决办法按优先级排序:增加数据量、增强数据增强、增大 Dropout、减小模型容量。我的经验是第三层卷积输出通道从 64 降到 32 往往就能缓解过拟合,因为宠物行为识别的特征复杂度没有想象中那么高。

数据类别不均衡。比如“跑”的图片少,“坐”的图片多。轻则模型把所有图片都预测成“坐”,重则训练不收敛。对策有两个:一是在DataLoader里设置WeightedRandomSampler,让每个类别被采样的概率相同;二是在损失函数里传入class_weight。我更推荐前一种,因为它是数据层面的处理,不影响模型结构,答辩时也好解释。

图像尺寸对全连接层的影响。如果你把训练输入尺寸从 64 改成 128,模型代码里nn.Linear(64*8*8, 128)就必须改成64*16*16=16384,否则会有维度不匹配报错。很多同学写死第一层全连接的维度,换输入尺寸就崩,建议在forward里动态获取展平后维度,或者像我一样固定输入尺寸,避免这类问题。

随机性影响结果。CNN 训练有随机性,同样的数据和代码,每次结果会有几个百分点的波动。建议在训练脚本开头设置随机种子:

torch.manual_seed(42)

这个细节在答辩时如果被问到“结果是否可复现”,你可以直接展示。

4.2 预测效果与预期不符怎么排查

模型在训练集上准确率很高,但网页上识别效果很差——这是最多人遇到的情况。

首先确认预处理链路一致。训练时transforms.Resize((64, 64)),推理时你也要Resize((64, 64)),不能一个用 64 一个用 128。同理还有Normalize的参数。这是最容易排查的一步,却也是最容易出错的一步。

其次是图片背景复杂。训练集里如果大多是干净背景,而用户上传的是复杂场景照片,模型很容易被背景干扰。解决思路是在训练集里增加复杂背景样本,或者对输入图片做简单的前景裁剪。不过毕设阶段建议量力而行,把训练和测试数据的采集环境尽量保持一致是最稳的方案。

然后看置信度分布。如果模型对每张图都给出 90% 以上的置信度,但结果经常是错的,说明模型过拟合,而不是“判断犹豫”。如果置信度普遍偏低,比如 40%-60%,说明模型没有学到足够强的特征,可能是训练不充分或特征相似。

我写了一个简单的predict.py脚本,每次在网页调试前先用命令行跑一张图看结果。这样可以先确认模型本身没问题,再去找网页代码的问题。排查思路是从数据流的下游往上游走,先测模型,再测预处理,最后测 Web 接口。

4.3 多类别行为识别的数据增强策略

数据增强要把握一个度:不能为了增强而改变行为本身的可识别特征。比如把图片旋转 90 度,可能导致“坐”看起来像“卧”;把亮度调得过高,可能导致宠物轮廓看不清。我当时用的增强策略是:

  • 水平翻转:适合左右对称的宠物姿态,但要注意如果宠物有明显的左右朝向动作,翻转后会形成新样本,这其实是好事。
  • 小角度旋转:限制在 ±15 度以内,避免姿态变形过度。
  • 颜色扰动:亮度、对比度、饱和度小幅调整,模拟不同光照条件。
  • 随机裁剪:这是最有效也最容易引入噪声的增强。我当时裁剪范围是原图的 80%-100%,然后缩放回 64x64,不会让宠物主体被裁掉。

增强的目的是让模型见过更多“合理的变异”,而不是制造“不合理的数据”。一个简单的验证方法是把增强后的图片抽出来看一眼,如果人眼都无法判断行为,那模型大概率也学不会。

4.4 毕设答辩中容易被追问的点

做这类项目,常见被老师追问的问题有:

“为什么用三个卷积层,不用 ResNet?”回答思路:三个卷积层参数量小、训练快,在这个规模的数据集上不会过拟合;如果数据量很大确实可以考虑迁移学习,但在宠物行为识别场景下,自定义 CNN 的精度已经够用。这个回答既解释了你的选择,又展示了你对模型容量和数据规模匹配关系的理解。

“你的准确率是多少?有没有对比实验?”建议在文档里放一张表,对比有数据增强 vs 无数据增强、dropout vs 无 dropout 的验证集准确率。哪怕只有这四组对比,也比单跑一个模型有说服力得多。

“你如何证明模型学到的不是背景特征?”这个问题比较刁钻但很常见。如果你训练集和测试集背景差异很大,模型可能在“认背景”而不是“认宠物”。我当时用了一个小技巧:保留宠物区域,把背景替换成纯色,再用同类测试图测试,发现准确率没有明显下降,说明模型主要关注宠物本身。


最后分享一个我在实际调试过程中觉得最有用的经验:训练时每 5 个 epoch 保存一次权重,并且记录对应的训练配置和数据增强方式。这个习惯起初只是为了防止训练中断丢失进度,后来发现模型效果不满意时,你可以快速回到某一次权重,检查是“数据变了”还是“超参数变了”导致的差异。在网页联调阶段,先在本地跑通单张图片识别,再接入 Flask,会省掉大量“前后端说不清谁有问题”的排查时间。做毕设最重要的不是模型有多高级,而是整个链路每一环你都能讲清楚、改得动、调得好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询