简介:这份资源是基于PyTorch与Flask构建的宠物图像识别完整项目包,面向具备一定深度学习基础、希望打通从模型训练到Web服务部署全流程的开发者与学习者。包内共2000个文件,以1993张jpg宠物图片作为训练与测试样本,辅以4个Python脚本、2个JSON配置文件和1份Markdown说明文档,压缩包约34.73MB。其中classify.py负责图像分类,predict.py支持单张或批量预测,api.py基于Flask对外提供识别接口,crawling.py承担数据爬取,train_loss_accuracy.json与classes.json分别记录训练指标和类别定义,readme.md则给出部署指引。图片覆盖猫、犬、爬行动物与两栖动物等多类宠物,目录结构清晰,便于按模块检索。目前已有41人学习下载,适合作为课程设计、毕业项目或图像识别入门实战的参考方案,帮助读者理解数据采集、模型训练与服务封装之间的衔接思路。
1. 从一份宠物图像识别包说起:PyTorch 训练加 Flask 上线的完整闭环
很多人做图像识别,卡的不是模型结构,而是「训练完的权重怎么变成一个别人能点开就用的网页」。这份基于 PyTorch 和 Flask 的宠物图像识别资源包,解决的正是这个断层:它把深度学习图像识别的训练侧和 Flask 部署侧串成了一条线,让你拿到的不只是一段推理脚本,而是一个能本地跑起来、能上传图片、能返回分类结果的轻量级 Web 应用。适合谁?刚学完 PyTorch 基础、想找一个端到端小项目练手的人;也适合做毕设或课程设计、需要「模型 + 网页」完整交付物的同学。它不追求 SOTA 精度,追求的是流程完整、依赖清晰、改起来不迷路。下面我按「环境怎么搭、数据怎么喂、模型怎么训、Flask 怎么接、坑在哪」的顺序拆一遍,你照着走能复现,熟手也能看到参数边界。
2. 环境搭建与依赖锁定:PyTorch 装 CPU 还是 GPU 版,先想清楚
2.1 为什么这个项目对 PyTorch 版本敏感
宠物图像识别本质是迁移学习或小型 CNN 分类,训练侧依赖 PyTorch 的torchvision做图像增强和预训练权重加载。PyTorch 的版本差异会直接影响三件事:torchvision.transforms的 API 是否兼容、预训练模型下载地址是否可用、以及 CUDA 版本与显卡驱动是否匹配。常见做法是锁定一个稳定组合,比如 PyTorch 2.x 配对应 torchvision,而不是无脑装最新。资源包里如果带了requirements.txt,优先按它来;没带的话,我一般会手动固定主版本,避免pip install torch拉到与代码不兼容的版本。
另一个容易被忽略的点是:Flask 侧只做推理,不需要 GPU。也就是说,训练可以在有显卡的机器上做,部署可以扔到普通 CPU 服务器。把训练环境和部署环境分开,是这个项目最省心的用法。
2.2 用 conda 还是 venv,以及 GPU 版的安装路径
新手最容易翻车的地方是「装完 torch 发现torch.cuda.is_available()返回 False」。原因通常不是显卡不行,而是装成了 CPU 版。判断方法很简单:装之前先确认显卡驱动和 CUDA 版本,再按官方给的命令装对应 CUDA 的 wheel。下面是我常用的 conda 环境创建和验证流程:
# 创建独立环境,Python 版本建议 3.9~3.11,太新可能没有对应 wheel conda create -n pet_cls python=3.10 -y conda activate pet_cls # 安装 PyTorch,这里以 CUDA 11.8 为例,具体命令按你的驱动版本调整 # 如果只用 CPU,把 --index-url 换成 cpu 源即可 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 验证 GPU 是否可用 python -c "import torch; print(torch.__version__, torch.cuda.is_available())"逻辑说明:先隔离环境,避免和系统里其他项目的 torch 打架;--index-url指定官方 wheel 源,比默认源更稳;最后一行是「后悔药」,装完立刻验证,别等训练跑起来才发现用的是 CPU。参数上,python=3.10是兼容性较好的折中,cu118要换成你驱动支持的版本,不确定就先用 CPU 版跑通流程。
2.3 Flask 与推理依赖的安装
Flask 本身很轻,真正要留意的是推理时图像处理的依赖,比如Pillow、numpy。这些在装 torchvision 时通常已经带上,但版本可能冲突。我一般单独再确认一遍:
pip install flask pillow numpy pip install -r requirements.txt # 如果资源包提供了,优先执行这一条逻辑说明:Flask 负责 HTTP 层,Pillow 负责把上传的图片解码成模型能吃的张量,numpy 负责数组转换。参数上没什么可调的,关键是版本别和 torchvision 自带的冲突。如果pip install -r requirements.txt报依赖冲突,先看是哪两个包抢同一个依赖,再决定降级谁,不要直接--force-reinstall一把梭。
提示:环境搭好后,先跑一遍资源包里的推理脚本(如果有),确认模型能加载、能出结果,再去碰 Flask。训练和部署分开验证,出问题好定位。
3. 数据组织与模型训练:把宠物图片喂进 PyTorch 的正确姿势
3.1 数据集目录结构与类别划分
图像识别项目里,数据组织方式直接决定你能不能少写代码。PyTorch 的ImageFolder要求按类别分文件夹,这是最常见也最省事的做法。假设你要识别猫、狗、兔三类,目录应该长这样:
dataset/ ├── train/ │ ├── cat/ │ ├── dog/ │ └── rabbit/ └── val/ ├── cat/ ├── dog/ └── rabbit/逻辑说明:ImageFolder会自动把子文件夹名当作类别标签,按字母序生成class_to_idx。这个映射关系在部署时必须和训练时一致,否则预测结果会张冠李戴。参数上,训练集和验证集的比例常见是 8:2 或 7:3,类别要均衡,某一类特别少会导致模型偏向多数类。如果资源包自带数据集,先数一遍每类图片数量,心里有数再开训。
3.2 数据增强与 DataLoader 参数
宠物图片的拍摄角度、光照、背景差异大,不做增强很容易过拟合。常见做法是训练侧用随机裁剪、翻转、颜色抖动,验证侧只做 resize 和归一化。下面是一段可直接抄的 transforms 和 DataLoader 配置:
import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练侧增强:随机性越强,泛化通常越好,但别过度 train_tf = transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸,和预训练模型输入对齐 transforms.RandomHorizontalFlip(p=0.5), # 水平翻转,宠物左右对称场景安全 transforms.RandomRotation(15), # 小角度旋转,模拟拍摄倾斜 transforms.ColorJitter(0.2, 0.2, 0.2), # 亮度/对比度/饱和度扰动 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # ImageNet 均值方差 ]) # 验证侧只做确定性处理,保证评估可复现 val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds = datasets.ImageFolder('dataset/train', transform=train_tf) val_ds = datasets.ImageFolder('dataset/val', transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4)逻辑说明:Resize((224,224))是为了匹配 ResNet 等预训练模型的输入;Normalize用的那组均值方差是 ImageNet 统计值,用预训练权重时保持一致效果最好。参数上,batch_size=32是显存和速度的折中,显存小就降到 16 或 8;num_workers=4在 Windows 上有时会出问题,报错就改成 0;shuffle=True只给训练集,验证集必须关掉,否则评估结果没有可比性。
3.3 迁移学习训练循环与关键参数
从零训一个小 CNN 也能跑,但宠物识别这种场景,迁移学习收敛快、精度高,是更实际的选择。核心思路是加载预训练模型,替换最后一层全连接,只训分类头或整体微调。下面是一个最小可用的训练循环:
import torch.nn as nn import torch.optim as optim from torchvision import models device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 加载预训练 ResNet18,替换最后的全连接层 model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) num_classes = len(train_ds.classes) model.fc = nn.Linear(model.fc.in_features, num_classes) model = model.to(device) criterion = nn.CrossEntropyLoss() # 只优化分类头时学习率可以大一点;整体微调时建议调小 optimizer = optim.Adam(model.parameters(), lr=1e-3) for epoch in range(10): model.train() 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() # 每个 epoch 后在验证集上评估 model.eval() correct = total = 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) preds = model(imgs).argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) print(f'epoch {epoch+1}, val_acc={correct/total:.4f}') # 保存权重和类别映射,部署时要用 torch.save({'state_dict': model.state_dict(), 'classes': train_ds.classes}, 'pet_model.pth')逻辑说明:ResNet18_Weights.DEFAULT会自动下载预训练权重,第一次跑需要联网;model.fc换成你的类别数,这是迁移学习的关键一步。参数上,lr=1e-3适合只训分类头,如果你解冻了前面的层做整体微调,学习率要降到1e-4量级,否则容易把预训练学到的特征冲垮。epoch=10是起步值,看验证集准确率不再涨就可以停。保存时把classes一起存进去,这一步很多人漏掉,部署时类别顺序对不上,预测全乱。
注意:训练完先别急着上 Flask,用几张验证集外的图片手动跑一遍推理,确认模型输出合理。训练准确率高但实际预测离谱,通常是归一化没对齐或类别映射错了。
4. Flask 接口设计:把模型推理包成一个能上传图片的网页
4.1 最小 Flask 应用结构与路由规划
Flask 部署图像识别的核心就两件事:一个页面让用户传图,一个接口接收图片返回结果。目录结构我一般这样放:
app/ ├── app.py # Flask 主程序 ├── pet_model.pth # 训练好的权重 ├── templates/ │ └── index.html # 上传页面 └── static/ └── uploads/ # 临时存放上传图片逻辑说明:templates放 HTML,static放静态资源和上传文件,这是 Flask 的默认约定,不按这个放就得手动配路径。参数上,上传目录要确保有写权限,Linux 服务器上经常因为权限问题导致上传失败。路由规划上,GET /返回页面,POST /predict处理图片,职责分开,方便后面加接口。
4.2 图片上传与预处理对齐训练侧
部署阶段最容易翻车的地方,是推理时的预处理和训练时不一致。训练用了Resize(224)加 ImageNet 归一化,推理也必须一模一样,否则精度断崖式下跌。下面是一段完整的 Flask 推理代码:
import io import torch from flask import Flask, request, jsonify, render_template from PIL import Image from torchvision import transforms, models import torch.nn as nn app = Flask(__name__) # 加载模型,结构和训练时保持一致 device = torch.device('cpu') # 部署侧用 CPU 即可 checkpoint = torch.load('pet_model.pth', map_location=device) classes = checkpoint['classes'] model = models.resnet18(weights=None) model.fc = nn.Linear(model.fc.in_features, len(classes)) model.load_state_dict(checkpoint['state_dict']) model.eval().to(device) # 预处理必须和验证侧完全一致 preprocess = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) @app.route('/') def index(): return render_template('index.html') @app.route('/predict', methods=['POST']) def predict(): file = request.files.get('image') if not file: return jsonify({'error': 'no image'}), 400 try: img = Image.open(io.BytesIO(file.read())).convert('RGB') except Exception: return jsonify({'error': 'invalid image'}), 400 tensor = preprocess(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): logits = model(tensor) probs = torch.softmax(logits, dim=1)[0] idx = int(probs.argmax()) return jsonify({'class': classes[idx], 'score': round(float(probs[idx]), 4)}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)逻辑说明:map_location='cpu'保证在没有 GPU 的机器上也能加载;weights=None是因为我们要加载自己的权重,不需要再下预训练;unsqueeze(0)是给单张图补上 batch 维度,模型只认四维张量。参数上,host='0.0.0.0'让局域网内其他设备能访问,只本机用可以改回127.0.0.1;port=5000是 Flask 默认端口,被占用就换。返回里带上score,前端可以显示置信度,用户心里有底。
4.3 前端上传页面与结果展示
前端不需要多复杂,一个表单加一段 JS 就够了。关键是enctype="multipart/form-data",漏了这个后端收不到文件。
<!DOCTYPE html> <html> <head><meta charset="utf-8"><title>宠物识别</title></head> <body> <h2>上传一张宠物图片</h2> <input type="file" id="file" accept="image/*"> <button onclick="upload()">识别</button> <p id="result"></p> <script> async function upload() { const f = document.getElementById('file').files[0]; if (!f) return; const fd = new FormData(); fd.append('image', f); const res = await fetch('/predict', { method: 'POST', body: fd }); const data = await res.json(); document.getElementById('result').textContent = data.error ? ('出错:' + data.error) : ('结果:' + data.class + ',置信度 ' + data.score); } </script> </body> </html>逻辑说明:FormData负责把文件打包成 multipart 请求,fetch发到/predict,拿到 JSON 后更新页面。参数上没什么可调的,注意accept="image/*"只是前端过滤,后端仍要做格式校验,不能只靠前端。这套前后端分离的写法,比在 Flask 里直接拼 HTML 更清晰,也方便你后面换成别的框架。
提示:本地跑通后,如果要把 Flask 部署到服务器,别用
app.run()直接对外,生产环境常见做法是用 gunicorn 或 uwsgi 加 nginx。这一步资源包不一定带,但迟早会遇到。
5. 避坑与排查:图像识别加 Flask 部署最常见的五个翻车点
5.1 预测结果永远是同一个类别
现象:不管传什么图,返回的类别都一样,置信度还很高。原因通常是推理预处理和训练不一致,比如训练用了归一化、推理忘了,或者Resize尺寸对不上,导致输入分布完全变了。解决:把推理侧的 transforms 逐行和训练验证侧对比,确保Resize、ToTensor、Normalize三步完全一致,一个参数都不能差。
5.2 类别映射错乱,猫被识别成狗
现象:模型能出结果,但类别名对不上,明明传的是猫,返回 rabbit。原因是训练时ImageFolder按文件夹字母序生成class_to_idx,部署时如果自己手写了一个类别列表,顺序不一致就全错。解决:训练保存权重时把classes一起存进去,部署时直接读,不要手动维护类别列表。这是血泪经验,手动维护迟早出错。
5.3 上传大图导致内存暴涨或超时
现象:传手机拍的原图(几 MB 甚至十几 MB),服务卡住或直接 500。原因是Image.open会把整张图解码进内存,大图加上模型推理,内存吃紧。解决:在预处理前先限制图片尺寸,比如img.thumbnail((1024, 1024)),或者在前端做压缩。参数上,限制长边 1024 对识别精度几乎没影响,但内存占用降很多。
5.4 Windows 上 DataLoader 报多进程错误
现象:训练时num_workers大于 0 就报错,提示无法 pickle 或进程启动失败。原因是 Windows 的进程启动方式和 Linux 不同,num_workers在 Windows 上容易出玄学问题。解决:把num_workers改成 0,训练慢一点但稳定;或者把训练代码放进if __name__ == '__main__':保护块里。这是平台差异,不是代码写错了。
5.5 模型文件加载报 missing keys 或 unexpected keys
现象:load_state_dict报键不匹配,模型加载失败。原因是保存和加载时的模型结构不一致,比如保存时改了fc层,加载时忘了改。解决:加载前先把模型结构搭成和训练时完全一样,再load_state_dict。如果只是分类头不同,可以用strict=False跳过,但要确认跳过的确实是你想跳的层,别把关键层也跳了。
6. 进阶技巧:把推理速度压下来,以及一个验证模型是否真的学会了的习惯
跑通之后,很多人会想「能不能再快一点」。Flask 侧最直接的优化是模型转 ONNX 或用torch.jit做推理加速,但在这之前,有个更划算的动作:把模型设成eval()并全程torch.no_grad()。这两步不做,推理会白白多算很多。下面是一个带计时和批量推理的进阶写法:
import time import torch def batch_predict(model, images, preprocess, classes, device='cpu'): """images: PIL Image 列表,返回类别和耗时""" model.eval() tensors = torch.stack([preprocess(img) for img in images]).to(device) start = time.time() with torch.no_grad(): probs = torch.softmax(model(tensors), dim=1) cost = time.time() - start idxs = probs.argmax(dim=1).tolist() return [(classes[i], round(float(probs[j][i]), 4)) for j, i in enumerate(idxs)], cost逻辑说明:torch.stack把多张图拼成一个 batch,一次前向比循环单张快很多;time.time()包住推理段,方便你对比优化前后的真实差距。参数上,batch 大小受内存限制,CPU 部署一般 8 到 16 张一批比较稳。这个函数可以直接替换掉 Flask 里的单张推理,接口层不用大改。
另一个我强烈建议养成的习惯:训练完别只看验证集准确率,手动挑几张「训练集里没有、但和训练集同分布」的图跑一遍,再挑几张「明显不同分布」的图(比如卡通宠物、模糊照片)看看模型什么反应。前者验证它学会了,后者验证它的边界在哪。我见过太多验证集 95%、实际用起来一塌糊涂的模型,问题就出在验证集和真实场景分布不一致。从那以后我每次训完分类模型,都强制走一遍「同分布 + 异分布」两组手测,再决定要不要上线。希望帮到你。
本文还有配套的精品资源,点击获取