简介:本资源为基于AI的动物识别技术研究毕业设计完整源码包,面向计算机相关专业进行毕业设计或课程设计的学生与开发者。项目以卷积神经网络(CNN)为核心,搭建可快速识别动物种类的Web网站,可应用于动物保护、搜救及环境生态保护等场景。开发环境采用Python 3.6.8、MySQL 5.7数据库与Navicat11管理工具,开发软件为PyCharm,技术栈清晰、上手门槛适中。压缩包共436个文件,约22.09MB,涵盖53个py源码、70个js脚本、30个css样式、11个html页面及gif、jpg、png等图像素材,另含yaml配置、sql建库脚本、pt模型权重与部署说明文档,前后端与模型资源齐备。目前已有62人学习下载。读者可获得完整可运行的CNN动物识别项目源码、数据库脚本、模型文件与部署文档,便于快速理解图像分类流程、Web端调用模型逻辑及环境配置方法,适合作为毕业设计参考或二次开发基础。
1. 从一份动物识别源码说起:AI 落地到底卡在哪
很多同学拿到「基于 AI 的动物识别技术研究源代码」这类毕业设计包时,第一反应是解压、装依赖、跑main.py,然后发现要么报错,要么识别结果一塌糊涂。问题不在代码本身,而在于大多数人跳过了「数据长什么样、模型为什么这么选、推理链路怎么串」这三件事。动物识别本质是一个细粒度图像分类任务,猫和狗好分,但花豹和猎豹、阿拉斯加和哈士奇,才是真正拉开差距的地方。这篇笔记不讲空泛概念,而是顺着一个可复现的 Python 项目结构,把数据准备、模型选型、训练调参、推理部署和踩坑排查完整走一遍。适合正在做计算机毕业设计、想用 Python 快速搭出一个能演示、能答辩、还能继续迭代的动物识别系统的同学。读完你至少能判断:手上这份源码值不值得改,以及怎么改才能跑出像样的准确率。
2. 动物识别项目的技术选型与数据准备
2.1 为什么细粒度分类不能直接套 ImageNet 预训练就完事
动物识别听起来简单,但真正做起来会发现两个现实问题。第一,类别不均衡。网上公开的动物数据集里,猫狗图片动辄上万张,而某些濒危动物可能只有几百张,直接训练会让模型严重偏向多数类。第二,类间差异极小。以鸟类为例,不同品种之间的区别可能只是喙的形状或羽毛纹理,靠全局平均池化后的特征向量很难区分。常见做法是先用 ImageNet 预训练权重做迁移学习,再针对动物数据集做微调,同时引入数据增强和类别权重来缓解不均衡。如果源码里只是简单CrossEntropyLoss且没有weight参数,那基本可以判断这份代码在真实数据上会翻车。
我一般会先做一件事:统计每个类别的样本数,画出分布直方图。如果最大类和最小类差距超过 10 倍,就必须处理。处理方式有三种:对少数类做随机过采样、对多数类做欠采样、或者在损失函数里加类别权重。第三种最省事,也最不容易引入过拟合。
2.2 用 Python 脚本把原始图片整理成训练集
拿到源码包后,第一步不是跑训练,而是检查数据目录结构。常见的组织方式是dataset/类别名/图片文件,但很多毕业设计包里的数据是混在一起的,需要自己写脚本划分训练集、验证集和测试集。下面这段代码是我常用的划分脚本,按 7:2:1 比例分层抽样,保证每个类别在三个集合中的比例一致。
import os import random import shutil from pathlib import Path from collections import defaultdict # 原始数据目录,结构为 raw_data/类别名/图片 RAW_DIR = Path("raw_data") # 输出目录 OUT_DIR = Path("split_data") # 划分比例 RATIOS = {"train": 0.7, "val": 0.2, "test": 0.1} # 随机种子,保证可复现 SEED = 42 random.seed(SEED) def split_dataset(): for cls_dir in RAW_DIR.iterdir(): if not cls_dir.is_dir(): continue cls_name = cls_dir.name images = [f for f in cls_dir.iterdir() if f.suffix.lower() in (".jpg", ".jpeg", ".png")] random.shuffle(images) n = len(images) n_train = int(n * RATIOS["train"]) n_val = int(n * RATIOS["val"]) splits = { "train": images[:n_train], "val": images[n_train:n_train + n_val], "test": images[n_train + n_val:] } for split_name, files in splits.items(): target = OUT_DIR / split_name / cls_name target.mkdir(parents=True, exist_ok=True) for f in files: shutil.copy2(f, target / f.name) print(f"{cls_name}: total={n}, train={len(splits['train'])}, val={len(splits['val'])}, test={len(splits['test'])}") if __name__ == "__main__": split_dataset()这段脚本的关键点有三个。SEED固定后每次划分结果一致,方便复现实验;shutil.copy2保留原始文件元信息,避免某些 EXIF 信息丢失影响后续读取;按类别循环而不是全局打乱,保证分层抽样。参数方面,RATIOS可以根据数据量调整,如果某个类别样本少于 100 张,建议把验证集和测试集比例降到 0.1 和 0.1,把更多数据留给训练。跑完脚本后,检查split_data下每个类别的文件数是否和打印一致,不一致说明有文件被跳过,通常是后缀名大小写问题。
2.3 数据增强参数怎么设才不帮倒忙
数据增强是双刃剑。用得好能提升泛化,用过头会让模型学不到真实特征。动物识别场景下,我一般只用以下几种增强:随机水平翻转、随机旋转 ±15 度、颜色抖动(亮度、对比度、饱和度各 0.2)、随机裁剪并缩放到统一尺寸。注意不要用垂直翻转,因为动物通常不会倒着出现,垂直翻转会引入不自然的样本。也不要过度使用 CutMix 或 MixUp,这类增强在细粒度任务上容易让模型混淆局部特征。
如果用torchvision,可以这样组织:
from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])RandomResizedCrop的scale参数控制裁剪区域占原图的比例,设成 0.8 到 1.0 意味着最少保留 80% 的画面,避免把动物主体裁掉。Normalize的均值和标准差是 ImageNet 的统计值,如果用的是预训练模型就必须保持一致,否则输入分布对不上,微调效果会打折扣。验证集只用Resize加CenterCrop,不做随机增强,保证评估结果稳定。
3. 模型搭建、训练与评估的完整链路
3.1 用迁移学习搭一个能打的基线模型
动物识别不需要从零训练,常见做法是拿 ResNet50 或 EfficientNet-B0 做骨干,替换最后的全连接层。ResNet50 参数量约 2500 万,EfficientNet-B0 约 500 万,后者在数据量不大时更不容易过拟合。如果毕业设计对精度要求高且机器有 GPU,选 ResNet50;如果只有 CPU 或者数据量少于 5000 张,EfficientNet-B0 更稳。
import torch import torch.nn as nn from torchvision import models def build_model(num_classes, backbone="resnet50", pretrained=True): if backbone == "resnet50": model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT if pretrained else None) # 冻结前面的层,只训练最后两个 block 和分类头 for name, param in model.named_parameters(): if "layer4" not in name and "fc" not in name: param.requires_grad = False in_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) elif backbone == "efficientnet_b0": model = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.DEFAULT if pretrained else None) for name, param in model.named_parameters(): if "features.7" not in name and "features.8" not in name and "classifier" not in name: param.requires_grad = False in_features = model.classifier[1].in_features model.classifier = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return model冻结策略是迁移学习的关键。layer4是 ResNet 最后一个残差块,负责高层语义特征,必须微调;前面的层提取的是边缘、纹理等通用特征,冻结后能防止小数据集上过拟合。Dropout(0.3)加在分类头前面,进一步抑制过拟合。如果训练集准确率远高于验证集,可以把 Dropout 提高到 0.5;如果两者都低,说明欠拟合,需要解冻更多层或者增加训练轮数。
3.2 训练循环里必须监控的三个指标
训练脚本不能只看 loss,否则很容易被「loss 降了但准确率没涨」的玄学现象迷惑。我一般同时记录训练损失、验证损失和验证准确率,每轮打印一次。如果验证损失连续 5 轮不下降,就触发早停。下面是一个精简的训练循环:
import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR def train(model, train_loader, val_loader, device, epochs=30, lr=1e-3): criterion = nn.CrossEntropyLoss() # 只优化需要梯度的参数 optimizer = optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=lr, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=epochs) best_acc = 0.0 patience = 5 counter = 0 for epoch in range(epochs): model.train() train_loss = 0.0 for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() train_loss += loss.item() * imgs.size(0) train_loss /= len(train_loader.dataset) model.eval() val_loss = 0.0 correct = 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) loss = criterion(outputs, labels) val_loss += loss.item() * imgs.size(0) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() val_loss /= len(val_loader.dataset) val_acc = correct / len(val_loader.dataset) scheduler.step() print(f"Epoch {epoch+1}/{epochs} | train_loss={train_loss:.4f} | val_loss={val_loss:.4f} | val_acc={val_acc:.4f}") if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_model.pth") counter = 0 else: counter += 1 if counter >= patience: print(f"Early stopping at epoch {epoch+1}") break return best_accAdamW的weight_decay设成1e-4是常见起点,如果验证损失震荡厉害可以调到1e-3。CosineAnnealingLR让学习率按余弦曲线下降,比固定学习率更容易收敛到好的局部最优。早停的patience设 5 轮,意味着连续 5 轮验证准确率没提升就停,避免浪费时间。注意保存的是state_dict而不是整个模型,这样加载时更灵活。
3.3 评估不能只看准确率:混淆矩阵和 Top-3 准确率
动物识别里,准确率会被多数类拉高。比如 10 个类别中有 3 个类别占了 70% 的样本,模型只要把这 3 类分对就能拿到 70% 准确率,但剩下的类别全错。所以必须看混淆矩阵和每类召回率。另外,细粒度任务中 Top-3 准确率比 Top-1 更有参考价值,因为有些样本确实长得像,模型给出前三个合理候选也算可用。
from sklearn.metrics import confusion_matrix, classification_report import numpy as np def evaluate(model, test_loader, device, class_names): model.eval() all_preds = [] all_labels = [] top3_correct = 0 total = 0 with torch.no_grad(): for imgs, labels in test_loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) _, top3 = outputs.topk(3, dim=1) for i in range(labels.size(0)): if labels[i] in top3[i]: top3_correct += 1 total += labels.size(0) all_preds.extend(outputs.argmax(dim=1).cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(f"Top-1 Accuracy: {np.mean(np.array(all_preds) == np.array(all_labels)):.4f}") print(f"Top-3 Accuracy: {top3_correct / total:.4f}") print(classification_report(all_labels, all_preds, target_names=class_names)) print("Confusion Matrix:") print(confusion_matrix(all_labels, all_preds))classification_report会输出每个类别的精确率、召回率和 F1 分数,重点看召回率低的类别,那通常是模型混淆最严重的地方。混淆矩阵能直观看出哪些类别被互相误判,比如「狼」和「哈士奇」如果互相混淆严重,就需要针对性补充这两类的区分性样本,或者调整数据增强策略。
4. 推理部署与性能优化的实操细节
4.1 把训练好的模型导出成推理脚本
训练完拿到best_model.pth后,需要写一个独立的推理脚本,支持单张图片预测和批量预测。推理脚本要处理几个细节:图片预处理必须和验证集一致、模型要切换到eval模式、输出要带置信度和 Top-3 结果。
import torch from PIL import Image from torchvision import transforms def load_model(ckpt_path, num_classes, backbone="resnet50", device="cpu"): model = build_model(num_classes, backbone, pretrained=False) model.load_state_dict(torch.load(ckpt_path, map_location=device)) model.to(device) model.eval() return model def predict(image_path, model, class_names, device="cpu"): transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) img = Image.open(image_path).convert("RGB") tensor = transform(img).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(tensor) probs = torch.softmax(outputs, dim=1)[0] top3_prob, top3_idx = probs.topk(3) results = [(class_names[idx], float(prob)) for prob, idx in zip(top3_prob, top3_idx)] return resultsImage.open后必须convert("RGB"),因为有些图片是 RGBA 或灰度图,直接转 tensor 会报通道数不匹配。unsqueeze(0)增加 batch 维度,因为模型期望输入是[N, C, H, W]。torch.softmax把 logits 转成概率,方便展示置信度。如果置信度普遍偏低,说明模型没训练充分或者测试图片和训练分布差异太大。
4.2 推理速度优化:从 200ms 降到 50ms 的几种手段
如果部署环境是 CPU,推理速度往往是瓶颈。我实测过几种优化手段,效果从高到低排列:第一,用torch.jit.trace把模型转成 TorchScript,能减少 Python 解释器开销,速度提升约 30%;第二,把输入尺寸从 224 降到 192,速度提升约 40%,但准确率可能掉 1 到 2 个百分点;第三,用 ONNX Runtime 推理,在 CPU 上通常比原生 PyTorch 快 1.5 到 2 倍。
# TorchScript 导出 model.eval() example_input = torch.randn(1, 3, 224, 224) traced_model = torch.jit.trace(model, example_input) traced_model.save("model_traced.pt") # ONNX 导出 torch.onnx.export( model, example_input, "model.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, opset_version=11 )TorchScript 导出时注意模型里不能有动态控制流,否则trace会失败,这种情况改用torch.jit.script。ONNX 的dynamic_axes让 batch 维度可变,方便批量推理。opset_version选 11 是因为兼容性最好,太新的版本某些推理引擎不支持。
4.3 用 Gradio 快速搭一个演示界面
毕业设计答辩通常需要一个可视化界面,Gradio 是最省事的选择,十几行代码就能跑起来。它支持上传图片、实时显示预测结果,还能同时展示 Top-3 置信度。
import gradio as gr def gradio_predict(image): img = Image.fromarray(image).convert("RGB") img.save("temp.jpg") results = predict("temp.jpg", model, class_names, device) return {name: prob for name, prob in results} iface = gr.Interface( fn=gradio_predict, inputs=gr.Image(), outputs=gr.Label(num_top_classes=3), title="动物识别演示" ) iface.launch(server_name="0.0.0.0", server_port=7860)gr.Image()默认返回 numpy 数组,需要转成 PIL Image 再走预处理。gr.Label的输出格式是字典,key 是类别名,value 是置信度,正好匹配predict函数的返回结构。server_name设成0.0.0.0可以让局域网内其他设备访问,方便答辩时用手机或平板演示。
5. 动物识别项目避坑与常见问题排查
5.1 训练 loss 不下降,准确率卡在随机水平
现象:训练几轮后 loss 一直在 2.3 左右(10 类任务的随机水平),准确率约 10%。原因通常是学习率太大导致梯度爆炸,或者数据标签和类别名没对上。解决:先把学习率降到1e-4试一轮,如果 loss 开始下降说明是学习率问题;如果仍然不动,检查class_names的顺序是否和训练时一致,很多源码包里类别映射是写死的,换了数据集后顺序错乱会导致模型永远学不对。
5.2 验证集准确率远低于训练集
现象:训练集准确率 95%,验证集只有 60%。原因一般是过拟合,或者训练集和验证集的数据分布不一致。解决:先检查划分脚本是否按类别分层,如果验证集里某些类别样本极少,评估结果波动会很大;然后增加数据增强强度,把Dropout从 0.3 提到 0.5,同时加weight_decay到1e-3。如果还不行,说明模型容量太大,换 EfficientNet-B0 试试。
5.3 推理时图片预处理和训练不一致
现象:训练时验证准确率 85%,但用推理脚本预测同一张图结果完全不对。原因多半是推理脚本里的Resize和Normalize参数和训练时不一样。解决:把训练时的val_transform直接复制到推理脚本里,不要凭记忆重写。特别注意Resize的目标尺寸和CenterCrop的尺寸,这两个参数写错一个就会导致输入分布偏移。
5.4 类别不均衡导致少数类召回率极低
现象:混淆矩阵显示多数类召回率 90% 以上,少数类只有 30%。原因就是样本不均衡。解决:在CrossEntropyLoss里加weight参数,权重按类别样本数的倒数计算,归一化后传入。如果少数类样本实在太少(少于 50 张),建议先用过采样把数量补到 200 张以上,再过采样容易导致过拟合,可以配合更强的数据增强一起用。
5.5 GPU 显存不足导致训练中断
现象:训练到一半报CUDA out of memory。原因可能是 batch size 太大,或者模型参数量太大。解决:先把 batch size 减半,如果还不行就换更小的骨干网络。另外检查是否有残留的 tensor 没释放,比如在验证循环里忘了torch.no_grad(),会导致计算图一直累积。加上torch.no_grad()后显存占用通常能降 30% 以上。
6. 把动物识别模型推到更高准确率的两个技巧
第一个技巧是测试时增强。训练时做数据增强,推理时也可以做。具体做法是对同一张测试图片做多次不同的增强(比如水平翻转、不同裁剪区域),分别推理后把 softmax 概率平均,取平均概率最大的类别作为最终结果。这个方法几乎不增加训练成本,但通常能提升 1 到 3 个百分点的准确率。实现上就是在predict函数里循环几次增强,把结果累加后除以次数。
def predict_with_tta(image_path, model, class_names, device="cpu", n_aug=5): img = Image.open(image_path).convert("RGB") base_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) aug_transforms = [ transforms.Compose([transforms.Resize((256, 256)), transforms.CenterCrop(224)]), transforms.Compose([transforms.Resize((256, 256)), transforms.RandomCrop(224)]), transforms.Compose([transforms.Resize((256, 256)), transforms.RandomHorizontalFlip(p=1.0), transforms.CenterCrop(224)]), ] probs_sum = None for i in range(n_aug): t = aug_transforms[i % len(aug_transforms)] tensor = base_transform(img) if i == 0 else transforms.Compose([t, transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])(img) tensor = tensor.unsqueeze(0).to(device) with torch.no_grad(): outputs = model(tensor) probs = torch.softmax(outputs, dim=1) probs_sum = probs if probs_sum is None else probs_sum + probs avg_probs = probs_sum / n_aug top3_prob, top3_idx = avg_probs[0].topk(3) return [(class_names[idx], float(prob)) for prob, idx in zip(top3_prob, top3_idx)]第二个技巧是用混淆矩阵指导补充数据。跑完评估后,找出混淆最严重的类别对,比如「狐狸」和「狼」互相误判率超过 20%,那就专门去收集这两类中容易混淆的样本,或者对这两类做更有针对性的增强(比如调整亮度、对比度模拟不同光照条件)。这比盲目增加所有类别的数据更有效。我自己的习惯是每训练完一版模型,先看混淆矩阵,再决定下一轮补什么数据、调什么参数,而不是反复调学习率这种玄学操作。希望这些经验能帮你在动物识别这个方向上少走弯路,把毕业设计做出真正能打的东西。
本文还有配套的精品资源,点击获取