简介:这是一套完整的基于Python卷积神经网络CNN的图像分类系统毕业设计资料,面向计算机相关专业学生,适用于毕业设计、课程设计、作业或初期项目演示,也适合零基础及初中级学习者进阶参考。项目覆盖LeNet-5、AlexNet、GoogLeNet、ResNet等经典网络结构,并分别提供TensorFlow和PyTorch两种框架实现,包含数据预处理、模型训练、网络结构对比、模型预测与前端展示等环节,能够帮助读者梳理图像分类项目的完整开发链路。压缩包共22个文件,主要类型包括Python源码、数据集与训练好的模型、说明文档及配置文件,资源包大小约62KB,目录结构按功能拆分,便于查阅和二次开发。目前已有130人学习下载,代码经测试运行成功,模型和数据可直接用于复现实验,也可以在此基础上增加类别、调整网络结构或接入其他应用,是一份实践性很强的参考资料。
1. 拿到一份 CNN 图像分类毕设资源:先别急着跑模型
做毕业设计最怕的不是没思路,而是拿到一份看着挺全的源码包,解压之后不知道先看哪个文件。这份基于 Python 卷积神经网络(CNN)的图像分类系统资源,压缩包里有训练好的模型、四个经典网络结构的源码、说明文档和整套数据资料,对于计算机相关专业拿来交毕设或课设的人来说,结构上属于“开箱即用”的类型。但我建议你先别双击 main.py,先把目录结构摸清楚,再决定从哪条路径入手。因为这类资源核心价值不在代码量,而在于它把从数据、训练到部署预测的完整闭环都给你搭好了。你需要在上面做的是理解、验证,然后再替换成自己的任务。
2. 把源码跑起来:环境、目录结构与一次完整预测
2.1 目录里都有什么:从根目录到 APP 逐层拆
拿到压缩包解压后,建议先用tree命令或编辑器自带的文件树看一遍结构,不要急着点开某个 .py 文件。通常这类毕设资源的目录会分几块:根目录放文档和说明,核心代码在某个子目录里,模型权重、数据资料单独放。这份资源里能看到几个关键入口:README.md是写给人看的说明文档,main.py是应用启动入口,model.py是网络结构定义,Matrix.py是混淆矩阵评估脚本,class_indices.json记录类别索引映射。APP 目录下还有templates和static,说明它把 TensorFlow、PyTorch 后端分别与四个经典网络结构(LeNet-5、AlexNet、GoogLeNet、ResNet)组合了起来,两个深度学习框架各有一份完整实现。这样设计的好处是:TensorFlow 系和 PyTorch 系的用户都不用改环境,直接挑自己熟悉的那套跑。我在本地用的是 Python 3.8 + PyTorch 1.13 + CUDA 11.7,TensorFlow 那套则用 CPU 就能完成推理。
# 建议先按这份清单核对解压后的文件 . ├── README.md # 项目说明,先读这个 ├── main.py # Flask 入口,Web 界面启动脚本 ├── model.py # CNN 网络结构定义 ├── Matrix.py # 混淆矩阵与分类指标计算 ├── class_indices.json # 类别名到索引的映射文件 ├── APP/ │ ├── templates/ # 前端 HTML 页面 │ ├── static/ # 静态资源、上传图片目录 │ └── ... ├── 数据集与训练好的模型/ # 训练权重与数据资料 └── ...(PyTorch / TensorFlow 双版本目录)解压后第一步先确认模型权重是否完整,比如文件大小不为 0、能够被torch.load或tf.keras.models.load_model加载。很多同学复现失败,第一原因就是权重文件在传输中损坏或者版本间序列化格式不兼容。检查完文件完整性再装依赖,能省掉后面一大半的排查时间。
2.2 最小运行路径:安装依赖、启动界面、上传一张图
最稳妥的起步方式是走 Web 界面这条路径,因为不用碰训练逻辑,直接验证模型有没有通。先把依赖装齐,然后启动main.py,浏览器打开本地端口。这个过程中如果你用的是 PyTorch 版本,注意torchvision的版本要和torch对应上,否则加载预训练权重时可能出现算子不匹配的报错。
# 创建虚拟环境,避免污染系统 Python python -m venv cnn_env source cnn_env/bin/activate # Windows 下用 cnn_env\Scripts\activate # 安装核心依赖 pip install torch torchvision flask pillow numpy # 如果跑 TensorFlow 版本,替换为: # pip install tensorflow flask pillow numpy # 启动 Web 应用,默认端口一般是 5000 python main.py启动后浏览器访问http://127.0.0.1:5000,界面会有一个上传框。找一张测试集中的图片传上去,模型会返回预测类别和置信度。这一步如果顺利跑通,说明预训练权重、网络结构定义和前端交互三者是匹配的,整个项目骨架没问题。我习惯在上传前先用 Python 脚本直接对单张图片做预测,因为这样能看到更多调试信息,Web 界面往往把中间过程都屏蔽了。
# quick_predict.py:加载模型并预测单张图片,用于验证权重文件可用性 import json import torch from PIL import Image from torchvision import transforms # 假设 model.py 里定义了 CNN 模型类,import 进来 from model import CNN # 加载类别映射文件,注意编码指定 UTF-8,Windows 下容易踩坑 with open('class_indices.json', 'r', encoding='utf-8') as f: class_indices = json.load(f) # JSON 里的键是字符串,转成 int 方便索引 idx_to_class = {int(v): k for k, v in class_indices.items()} # 初始化模型并加载训练好的权重,strict=False 防止键名不匹配时报错 model = CNN() model.load_state_dict(torch.load('模型权重路径.pth', map_location='cpu'), strict=False) model.eval() # 标准预处理:缩放、转张量、归一化,参数必须与训练时一致 transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) img = Image.open('test.jpg').convert('RGB') img = transform(img).unsqueeze(0) # 增加 batch 维度 with torch.no_grad(): outputs = model(img) _, predicted = torch.max(outputs, 1) print('预测类别:', idx_to_class[predicted.item()])这个脚本有几个关键点。map_location='cpu'是防止你本地没有 GPU 时加载报错;strict=False允许权重字典里有多余或缺失的键,虽然不建议长期这么干,但排查问题时能让你更快区分是结构不匹配还是权重损坏。预处理里的Resize尺寸必须和训练时一致,这份资源里大多数模型用的是 224×224,如果你自己换数据集时发现预测准确率异常低,优先检查这一段。
2.3 模型加载与预测的代码解剖
从model.py里能看到,这份源码并不是只有一个网络结构,而是把 LeNet-5、AlexNet、GoogLeNet、ResNet 都写进去了,通过参数切换。这种设计很适合毕设答辩,因为你可以对比不同结构的精度差异,还能讲清楚为什么 ResNet 比 LeNet-5 在复杂数据集上表现更好。核心的加载逻辑一般是:先根据参数实例化对应模型,然后加载对应权重,最后封装一个predict方法给 Flask 调用。
# model.py 中典型的网络选择与加载逻辑(PyTorch 版简化示例) import torch.nn as nn def get_model(model_name='resnet', num_classes=10): if model_name == 'lenet5': model = LeNet5(num_classes) # 自定义 LeNet-5 实现 elif model_name == 'alexnet': model = AlexNet(num_classes) # 自定义 AlexNet 实现 elif model_name == 'googlenet': model = GoogLeNet(num_classes) # 含 Inception 模块 elif model_name == 'resnet': model = ResNet(num_classes) # 含残差连接 return model选择用哪个模型,取决于你的数据集规模和硬件条件。如果数据集只有几千张图且类别数少,LeNet-5 就够用,训练速度快、不易过拟合;如果是从零训练 ImageNet 级别的大规模数据,ResNet 是更稳妥的起点。这种多模型并存的结构,让这份资源在毕设答辩时可以展示横向对比实验,这是一个很加分的点。
3. 四代模型同台:LeNet-5 到 ResNet 的代码差异与选型依据
3.1 四个经典结构在图片分类里分别扮演什么角色
压缩包里同时出现 LeNet-5、AlexNet、GoogLeNet、ResNet,不是随便堆在一起的。它们恰好代表了 CNN 发展的四个阶段:LeNet-5 是 1998 年的开山之作,验证了卷积加池化加全连接这个组合能做手写数字识别;AlexNet 在 2012 年通过 ReLU、Dropout、GPU 并行让深度学习在 ImageNet 上大幅刷新纪录;GoogLeNet 引入了 Inception 模块,用不同尺寸的卷积核并行提取特征;ResNet 用残差连接解决了深层网络退化问题,把网络深度从十几层直接推到上百层。在你的毕设论文里,这四个模型的演进本身就是一段很好的背景介绍,答辩老师一般都会对“为什么选 ResNet 作为最终方案”感兴趣,你的回答要落到具体数据上,而不是说“因为它效果最好”。
3.2 从 LeNet-5 到 AlexNet:卷积核与 Dropout 的关键作用
LeNet-5 的核心代码很短,两个卷积层、两个池化层、三个全连接层。它用的是 5×5 卷积核和 2×2 平均池化,输入是 32×32 灰度图。在代码里体现为:
class LeNet5(nn.Module): def __init__(self, num_classes=10): super().__init__() self.conv1 = nn.Conv2d(1, 6, kernel_size=5, padding=2) # 输入单通道,6 个 5×5 卷积核 self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2) # 平均池化,尺寸减半 self.conv2 = nn.Conv2d(6, 16, kernel_size=5) # 16 个卷积核,无 padding self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2) self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, num_classes) def forward(self, x): x = self.pool1(torch.relu(self.conv1(x))) x = self.pool2(torch.relu(self.conv2(x))) x = x.view(x.size(0), -1) # 展平成一维,16*5*5=400 x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) return self.fc3(x)这里有个细节:LeNet-5 默认输入是灰度图,如果你直接拿三通道 RGB 图片去跑,第一个卷积层会因为in_channels=1报维度错误。所以如果你要复用到自己的彩色数据集,需要把nn.Conv2d(1, 6, ...)改成nn.Conv2d(3, 6, ...),或者在前处理阶段用img.convert('L')转灰度。AlexNet 在代码上的变化就很明显了:5×5 和 3×3 卷积核并用,ReLU 替代 tanh,中间插入 Dropout 层防止过拟合,而且因为当时 GPU 显存有限,把网络分成两支并行计算。现代实现里不需要再去模拟双 GPU 结构,直接顺序堆叠即可,但 Dropout 层的保留非常值得注意——在你自己微调的时候,训练集不大时 Dropout 能明显抑制过拟合。
3.3 GoogLeNet 与 ResNet:Inception 和残差连接在代码里长什么样
GoogLeNet 最核心的代码片段是 Inception 模块。它把 1×1、3×3、5×5 三个尺寸的卷积和 3×3 最大池化并行堆叠,在通道维度上拼接输出。1×1 卷积在这里的作用是降维,把上一层的通道数先压缩再送进大卷积核,大幅减少计算量。这个思路非常经典,后来很多轻量化网络都借鉴了它。代码里通常实现为一个自定义模块,大概长这样:
class Inception(nn.Module): def __init__(self, in_channels, ch1x1, ch3x3_reduce, ch3x3, ch5x5_reduce, ch5x5, pool_proj): super().__init__() # 分支1:1x1 卷积 self.b1 = nn.Conv2d(in_channels, ch1x1, kernel_size=1) # 分支2:先 1x1 降维,再 3x3 卷积 self.b2 = nn.Sequential( nn.Conv2d(in_channels, ch3x3_reduce, kernel_size=1), nn.Conv2d(ch3x3_reduce, ch3x3, kernel_size=3, padding=1) ) # 分支3:先 1x1 降维,再 5x5 卷积 self.b3 = nn.Sequential( nn.Conv2d(in_channels, ch5x5_reduce, kernel_size=1), nn.Conv2d(ch5x5_reduce, ch5x5, kernel_size=5, padding=2) ) # 分支4:3x3 最大池化后接 1x1 卷积 self.b4 = nn.Sequential( nn.MaxPool2d(kernel_size=3, stride=1, padding=1), nn.Conv2d(in_channels, pool_proj, kernel_size=1) ) def forward(self, x): return torch.cat([self.b1(x), self.b2(x), self.b3(x), self.b4(x)], dim=1)ResNet 的残差连接在代码里更直接,一个if判断动态决定是否需要对输入做下采样,然后输出x = out + identity。这个加法操作让梯度可以跨层直接回传,这就是为什么 ResNet 能训练上百层还不退化。你在复现时注意stride参数,在 ResNet 的下采样阶段通常设置为 2 来缩小特征图尺寸,如果设置错了,残差连接时形状不匹配,程序会直接报错。
class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride=1): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) # 输入输出通道不一致时,用 1x1 卷积对齐维度 self.shortcut = nn.Sequential() if stride != 1 or in_channels != out_channels: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(out_channels) ) def forward(self, x): identity = self.shortcut(x) out = torch.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out += identity # 残差相加 return torch.relu(out)3.4 四模型对比与选型建议
| 模型 | 提出年份 | 关键技巧 | 适合场景 | 训练难度 |
|---|---|---|---|---|
| LeNet-5 | 1998 | 卷积+池化+全连接 | 灰度小图、手写字符 | 低 |
| AlexNet | 2012 | ReLU、Dropout、双 GPU | 中等规模彩色图像 | 中 |
| GoogLeNet | 2014 | Inception 多尺度卷积 | 数据量较大,追求效率 | 中高 |
| ResNet | 2015 | 残差连接、批归一化 | 大规模数据集、迁移学习 | 中高 |
如果你只是为了交毕设,时间又紧,我建议直接用 ResNet 做主力模型,因为它收敛稳定、迁移学习友好,答辩时还能讲清楚“为什么深层网络没有退化”这个理论点。GoogLeNet 的 Inception 不适合在答辩中深入展开,因为细节很多,容易被追问卡住。LeNet-5 适合做一个精度对比的 baseline,体现你的实验严谨性。
4. 训练与评估:从 class_indices.json 到混淆矩阵的完整闭环
4.1 数据目录约定与类别索引的生成
这份资源里已经帮你训练好模型了,但毕设答辩经常会问“如果换成你的数据集,怎么训练”,所以训练流程还是要过一遍。第一步是数据组织。PyTorch 的ImageFolder规定,数据目录下每个子文件夹名字就是类别名,子文件夹里面放该类的图片。这份资源的class_indices.json就是这个流程的产物,形如{"cat": 0, "dog": 1},把文件夹名映射到整数索引。当你新增类别或调整数据集时,需要重新生成这个文件,否则加载权重后索引错位,预测结果会全部对不上。
# 生成 class_indices.json 的常见做法 import json import os data_root = 'dataset/train' classes = sorted(os.listdir(data_root)) # 按文件夹名字母顺序排序 class_indices = {cls: idx for idx, cls in enumerate(classes)} with open('class_indices.json', 'w', encoding='utf-8') as f: json.dump(class_indices, f, indent=2, ensure_ascii=False) print(class_indices)注意这里用了sorted,因为ImageFolder也是按名称排序分配索引的,两边保持一致才不会乱。很多同学在换自己数据集时翻车,就是因为手动改了类别顺序,但 JSON 没更新,导致模型输出索引 0 对应的实际是另一个类别。数据划分上,常见做法是按照 8:1:1 或 7:2:1 切训练集、验证集、测试集。如果原数据集没有划分好,可以用torch.utils.data.random_split或直接写一段脚本按比例复制文件到不同目录。
4.2 训练超参数:batch size、学习率、epochs 怎么定
训练脚本里最影响结果的是学习率和 batch size 的组合。这份资源的默认配置,我没记错的话是 batch size 32、学习率 0.001、动量 0.9、权重衰减 1e-4,这是 PyTorch 官方迁移学习教程里的经典配置,足够稳定。但要注意,学习率 0.001 是针对 ImageNet 预训练权重微调的场景;如果你的数据集和预训练数据分布差异大,或者你是从零训练,这个学习率可能偏大或偏小,要观察 loss 曲线调整。
# 训练核心逻辑(PyTorch 版简化),重点是参数配置 import torch.optim as optim from torch.optim.lr_scheduler import StepLR device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = get_model('resnet', num_classes=len(class_indices)).to(device) criterion = nn.CrossEntropyLoss() # 多分类标配损失函数 optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=1e-4) scheduler = StepLR(optimizer, step_size=10, gamma=0.1) # 每 10 个 epoch 学习率降为 1/10 for epoch in range(30): model.train() running_loss = 0.0 for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) scheduler.step() # 每个 epoch 结束后在验证集上算一次准确率,记录日志 print(f'Epoch {epoch+1}, Loss: {running_loss/len(train_dataset):.4f}')这里几个参数的用意值得说明。weight_decay=1e-4是 L2 正则化,能抑制大权重,对防止过拟合有实际帮助;StepLR的学习率衰减策略,让模型在训练后期用小学习率精细调整权重;CrossEntropyLoss在 PyTorch 里已经内置了 softmax,所以网络最后一层不需要手动加 softmax。如果你在测试时想输出概率值,再手动nn.Softmax(dim=1)。epoch 设多少没有定论,我一般先看验证集准确率的变化曲线,连续 5 个 epoch 不涨就早停,避免浪费时间。
4.3 Matrix.py 混淆矩阵:评估指标不是只看准确率
Matrix.py这个文件是容易被忽视的,但它其实是毕设加分项。准确率只能告诉你模型整体表现如何,而混淆矩阵能告诉你哪些类别之间互相混淆。比如车辆分类模型,准确率 95%,但你可能不知道它经常把“卡车”识别成“公交车”,这两类的区分度差在哪里,看混淆矩阵一目了然。这个文件通常用sklearn.metrics.confusion_matrix生成矩阵,用matplotlib和seaborn画热力图,最后输出每个类别的精确率、召回率、F1-score。
# Matrix.py 的核心逻辑:评估脚本 from sklearn.metrics import confusion_matrix, classification_report import matplotlib.pyplot as plt import seaborn as sns import numpy as np # 假设 y_true 是真实标签列表,y_pred 是模型预测标签列表 cm = confusion_matrix(y_true, y_pred) report = classification_report(y_true, y_pred, target_names=list(class_indices.keys())) plt.figure(figsize=(10, 8)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=class_indices.keys(), yticklabels=class_indices.keys()) plt.xlabel('Predicted Label') plt.ylabel('True Label') plt.tight_layout() plt.savefig('confusion_matrix.png', dpi=200)画混淆矩阵的图在论文里是实打实的实验结果,答辩老师通常会盯着这张图问问题,比如“为什么这两个类别容易混淆,你觉得数据层面有什么原因”。你要能回答上来,比如光照影响、背景相似、样本数量不均衡等,这比你在论文里堆十个模型结构图更有说服力。评估脚本建议在测试集上跑,不要在训练集上跑,否则指标虚高,答辩现场演示时露馅就尴尬了。
5. 避坑排查:复现 CNN 图像分类项目时常见的五个翻车点
5.1 加载权重时报错:键名不匹配或者参数尺寸对不上
现象:torch.load之后调用load_state_dict抛异常,提示size mismatch或Missing key(s)。原因:权重文件是用不同网络结构训练的,或者num_classes不一致。比如原项目是 10 类,你想改成 5 类,最后一层全连接的权重维度肯定对不上。解决:加载时先看报错信息里提示的是哪一层,如果是最后一层,直接把strict=False加上,然后在训练脚本里只微调最后几层。更规范的做法是在加载之前把新模型最后一层替换成匹配的尺寸:
# 处理类别数不同的情况 model = get_model('resnet', num_classes=10) # 原模型 10 类 state_dict = torch.load('weight.pth', map_location='cpu') # 去掉最后一层权重,避免尺寸冲突 state_dict.pop('fc.weight', None) state_dict.pop('fc.bias', None) model.load_state_dict(state_dict, strict=False) # 替换最后一层为新类别数 model.fc = nn.Linear(model.fc.in_features, 5)5.2 上传图片后预测结果离谱但程序不报错
现象:Web 界面上传一张猫的图片,返回“dog”且置信度 0.99。原因:预处理的归一化参数和训练时不一致,或者 Resize 尺寸不对,或图片是 BGR 顺序。RGB 与 BGR 通道顺序错位是 OpenCV 读取图片的经典坑,cv2.imread读出来是 BGR,用 PIL 打开是 RGB,直接混用会导致颜色特征完全错乱。解决:统一用 PIL 做预处理链路,或者在脚本里cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。再检查Normalize的均值和标准差是否与训练时一致,这份资源的预训练模型通常用的 ImageNet 统计值,如果你的数据分布差异大,考虑自己算一遍。
5.3 Windows 下中文路径与编码问题
现象:数据集目录包含中文,程序报FileNotFoundError或读取到的路径乱码。原因:Python 在 Windows 下的默认编码是 UTF-8 的兼容层,但某些依赖库用系统默认编码(GBK)读取文件。解决:所有代码开头加上# -*- coding: utf-8 -*-,读取 JSON 时显式指定encoding='utf-8',处理文件路径时用pathlib.Path替代字符串拼接:
from pathlib import Path data_dir = Path('数据集与训练好的模型') # 中文路径用 Path 对象处理 for img_path in data_dir.glob('*.jpg'): # 避免路径字符串拼接导致的编码问题 img = Image.open(str(img_path))5.4 训练时 loss 不下降或者直接变成 NaN
现象:loss 一直在 2.3 左右徘徊(多分类随机猜测值),或者直接打印nan。原因:学习率过大导致梯度爆炸,或者数据归一化没做,输入像素值范围在 0-255 而不是 0-1,或者标签不是从 0 开始的连续整数。解决:先确认数据预处理里有没有ToTensor(),它会把像素值自动缩放到 0-1;再把学习率降到 0.0001 试试;最后检查CrossEntropyLoss的输入数据是否有 NaN 值,有的话在DataLoader里加drop_last=True或者过滤掉损坏图片。排查这类问题最有效率的方式是记录前几个 batch 的梯度范数,如果梯度范数突然变大,基本就是学习率的问题。
5.5 Flask 上传接口报 400 或 413 错误
现象:点击上传按钮后返回 400 Bad Request,或者大图片上传失败。原因:Flask 默认对上传文件大小没有硬限制,但前端表单的enctype如果没设置成multipart/form-data,文件就传不上去;413 则是服务器限制主体大小。解决:检查 HTML 表单标签是否写了enctype="multipart/form-data";在 Flask 配置里显式设置上传大小上限:
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 限制 16MB # 处理上传请求时,先判断是否有文件 if 'file' not in request.files: return jsonify({'error': '未接收到文件'}), 400这段配置写在 Flask 初始化之后、路由定义之前。遇到上传失败时,先在浏览器开发者工具里看网络请求的响应体,Flask 的报错信息一般会直接告诉你原因,不要只看状态码就去改代码。
6. 把 CNN 模型交付成可演示的 Web 应用:Flask 封装与迁移学习
6.1 用 Flask 把预测逻辑包成接口
实现 Web 界面调用模型,核心逻辑就是把main.py拆成三块:模型初始化(只做一次)、图片预处理、预测与返回。关键点是模型不能每次请求都重新加载,要在模块加载时初始化成全局变量,否则并发请求时内存直接爆炸。
# main.py 核心结构 from flask import Flask, request, jsonify, render_template import torch from PIL import Image from torchvision import transforms # 模型初始化,整个过程只执行一次 model = get_model('resnet', num_classes=10) model.load_state_dict(torch.load('best_model.pth', map_location='cpu'), strict=False) model.eval() app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 @app.route('/', methods=['GET']) def index(): # 渲染上传页面 return render_template('index.html') @app.route('/predict', methods=['POST']) def predict(): if 'file' not in request.files: return jsonify({'error': '缺少上传文件'}), 400 file = request.files['file'] # 这里要直接读内存,不要先保存再临时读取 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) conf, idx = torch.max(probs, 1) return jsonify({ 'class': idx_to_class[idx.item()], 'confidence': round(conf.item(), 4) }) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)这里有几个值得注意的细节:debug=False一定要设置,因为调试模式下 Flask 会在异常时抛出交互式控制台,在公网环境这是安全漏洞;file.stream直接传给Image.open可以避免把临时文件写到磁盘;softmax转成概率的目的,是让前端不仅能显示预测类别,还能显示置信度,答辩演示时更有说服力。前端模板传参数的时候,注意用render_template把类别列表也传过去,方便在页面上渲染图例。
6.2 换数据集的最佳实践:迁移学习而不是从零训练
毕设如果只是把别人的成果原样跑通,分数不会高。聪明做法是保留预训练权重,替换最后一层,在你的自定义数据集上做迁移学习。原因是 CNN 的前几层学习的是通用的边缘、纹理、颜色特征,这些特征在不同数据集之间是可复用的,只有最后几层才是针对特定任务的高层语义。具体操作:把model.fc替换成新的全连接层,冻结前面所有层的参数,只训练最后一层,等收敛后再解冻全部层用小学习率微调。我一般先冻结训练 10 个 epoch,再解冻训练 20 个 epoch,这种两阶段策略比直接全量微调更稳。
# 迁移学习:冻结特征提取层,只训练分类头 for name, param in model.named_parameters(): if 'fc' not in name: # 最后一层通常是 fc,保持可训练 param.requires_grad = False # 只把 fc 层的参数传给优化器 optimizer = optim.SGD(filter(lambda p: p.requires_grad, model.parameters()), lr=0.001, momentum=0.9)你从网上搜“最新的图像分类模型”时,会发现 Transformer 架构的 ViT、Swin Transformer 现在很火,但这份资源没有涉及它们。毕设里你可以先用 ResNet 把流程跑通,如果你对 ViT 感兴趣,可以直接把主干网络替换成timm库里的vit_base_patch16_224,训练流程完全不用改,只要保证预处理尺寸一致。展示出你愿意尝试新模型的主动性,比守着四个旧模型更能打动答辩老师。
6.3 现场演示前的验证习惯
我每次在答辩或提交课设前,都会强制走一遍完整的验收流程:先在测试集上跑一次官方评估脚本,用混淆矩阵确认每个类别的准确率都过了预期线;再准备三五张训练集之外的真实图片,逐个上传到 Web 界面看预测结果,避免现场翻车。其中一张特意选模型容易混淆的“边缘案例”,比如背景复杂的、光线暗的、有遮挡的图片,结果如果虽然错了但在合理范围内(比如置信度偏低、两个类别概率接近),反而能引出你的分析,让答辩变成一场对任务难度的讨论。从那以后我每次拿到新的模型资源,都会在解压后 15 分钟内完成“读文档、跑推理、看混淆矩阵”这三步,确认项目健康再加入自己的改动。希望这个习惯能帮到你。
本文还有配套的精品资源,点击获取