基于ResNet的动物图像分类系统:PyTorch与Flask三端联调实战

基于ResNet的动物图像分类系统:PyTorch与Flask三端联调实战 简介本资源是一个完整的基于ResNet的动物图像分类系统实现面向Python与深度学习初学者及高校课程设计学生解决图像识别类期末大作业开发需求。系统融合PyQt桌面端界面、Flask轻量Web服务、HTML5前端展示与PyTorch模型训练四大技术栈支持本地图片上传→ResNet18推理→实时分类结果可视化全流程。压缩包共27个文件含8个核心Python脚本如train.py、predict.py、myflask.py、11张UI与效果截图、1个训练好的resnet18_e_best.pth模型权重、2个pyc编译文件、1个README.md说明文档及HTML模板等总大小41.75MB结构清晰模块分离明确含utils工具、assets资源、templates模板、logs日志等目录。已有49人学习下载提供从数据预处理、模型微调、GUI交互到Web部署的一站式参考方案特别适合理解多框架协同开发模式与ResNet在实际项目中的工程化落地。1. 作业需求拆解一个分类系统为什么需要三套交互层先说下这份大作业的定位。标题里写的很清楚这是一份基于ResNet的动物图像分类系统技术栈是PyQt加Flask加HTML5加PyTorch最终交付物是一个zip压缩包。很多同学看到这个组合第一反应是“这么多东西怎么串起来”其实它本质上是同一套推理服务套了三种不同的交互外壳。期末大作业的评审逻辑通常是这样的一是看模型本身的准确率和训练流程是否规范二是看系统是否具备完整的交互体验三是看工作量是否足够支撑答辩。单独用Jupyter Notebook跑一个ResNet训练脚本只能覆盖第一点但如果你把训练好的模型封装成Web接口再做桌面端和浏览器端两个客户端工作量就能铺满整个评分表。PyQt负责桌面交互Flask负责提供一个统一的HTTP推理服务HTML5页面负责浏览器端展示三者在后端逻辑上是同一个模型只是请求方式不同。所以这篇博文我会按照一个完整的课程设计路线来拆解从数据准备、模型训练、服务封装、界面开发到最后的打包发布每一步都给出我实际踩过坑之后的方案而不是教科书式的步骤堆叠。先给你一张整体架构图方便后面理解各模块的职责边界浏览器HTML5页面 | POST /predict multipart/form-data上传图片 v Flask应用app.py加载resnet18模型返回JSON ^ | POST /predict multipart/form-data上传图片 PyQt桌面客户端MainWindow选择图片后发送请求模型训练完成后导出权重文件Flask在启动时加载一次三个端共用这一个权重不存在三份模型重复加载的问题。这一点在做项目演示时很重要因为答辩现场你不可能让评审老师等模型加载三回。2. ResNet模型选型与图像分类的核心原理2.1 为什么是ResNet而不是VGG或者GoogLeNet图像分类的经典模型不少VGG16、GoogLeNet、ResNet系列在torchvision里都有现成实现。对于期末大作业我强烈建议直接用ResNet18理由很简单VGG16在ImageNet上的表现虽然也不错但参数量有1.38亿推理一张图在CPU上要等好几秒而ResNet18参数量只有约1170万在CPU上推理一张224x224的图通常在几百毫秒级别这直接影响演示的流畅度。GoogLeNetInception v3性能介于两者之间但结构理解成本更高答辩时老师如果深入问Inception模块的设计动机你解释起来比解释ResNet的残差连接要复杂得多。ResNet18在torchvision中可以直接加载ImageNet预训练权重迁移学习时只需要改最后一层全连接输出维度。这里有个小技巧直接用torchvision.models.resnet18(weightsResNet18_Weights.DEFAULT)而不是旧版的pretrainedTrue。新版API把权重参数独立出来了pretrained在PyTorch 2.0之后已经标记为弃用写作业时用新版写法能少一个警告信息也显得你对框架版本演进有了解。2.2 残差结构到底解决了什么问题我在这里必须多说一句因为很多同学答辩时把残差结构背得滚瓜烂熟但一被问到“为什么加深网络会导致准确率下降”就卡壳。ResNet的核心创新不是“网络更深”而是通过残差学习让深层网络不至于退化。传统卷积网络每一层都在学习输入到输出的完整映射即H(x) F(x)。网络加深后梯度在反向传播过程中连续相乘容易出现梯度消失或梯度爆炸导致深层网络训练不动测试准确率反而不如浅层网络。ResNet把学习目标从一个完整映射改成残差映射让某一层网络去学习F(x) H(x) - x而最终的输出是F(x) x。这个x就是输入本身通过一个恒等捷径identity shortcut直接加到输出上。理解这个设计的关键在于反向传播。当损失对输出求梯度时捷径连接的存在让梯度既能经过权重层传播也能直接流回上一层。即使中间层的权重梯度极小那一份恒等映射带来的梯度依然能让前面的层继续学习。用生活类比来说如果传统网络是一个必须逐级传递消息的传话游戏深度一长消息就失真ResNet相当于给每一级加了一条直线电话线消息失真了也能通过直连线路找回原话所以网络再深学习效率也不会断崖式下降。2.3 预训练模型和从零训练的选择逻辑动物图像分类属于典型的中小规模图像分类任务从零训练ResNet18并不是不行但你需要足够大的数据集和足够多的训练轮次否则很容易陷入过拟合。我建议直接用ImageNet预训练权重做迁移学习冻结前几层参数只微调最后几层然后在自己的动物数据集上重新训练。这样做有三点好处训练时间大幅缩短CPU上也能在十几分钟内完成微调预训练模型已经具备通用的边缘、纹理、形状特征提取能力小样本也能收敛答辩时解释“迁移学习”概念是一个天然的加分点当然如果老师要求“模型必须从零训练”那你就要多准备数据增强手段保证训练集充足。后面会讲数据准备部分。3. PyTorch训练流程与模型管理3.1 环境搭建与关键版本匹配我用的是Python 3.9 PyTorch 2.1.2 torchvision 0.16.2。为什么强调版本匹配因为torchvision和PyTorch的版本必须一一对应比如torchvision 0.16.2对应PyTorch 2.1.2搞错了装完import就会报错。安装命令建议用pip且指定版本pip install torch2.1.2 torchvision0.16.2 --index-url https://download.pytorch.org/whl/cpu这里有个容易被忽略的坑如果机器没有独立显卡不要装默认的CUDA版本直接装CPU版。CPU版安装体积小运行也不会有问题。如果你的电脑有NVIDIA显卡想用GPU加速训练那就先运行nvidia-smi查看CUDA版本再装对应CUDA支持的PyTorch版本。我在项目里训练用的就是CPU版ResNet18微调5轮大概耗时20多分钟完全能接受。3.2 数据集准备下载、划分与预处理动物分类要准备多少类、多少张图取决于你的任务范围。我使用的是一个包含十类动物的公开数据集每类约500张图片包含猫、狗、大象、马、蝴蝶、鸡、牛、羊、蜘蛛、松鼠这十类。你也可以直接使用牛津IIIT宠物数据集Oxford-IIIT Pet或斯坦福狗数据集这些数据集在学术圈用得比较多老师一听就知道你用了规范的数据源。拿到数据后第一步是整理目录结构。PyTorch的torchvision.datasets.ImageFolder要求数据按类别放在子目录中dataset/ train/ cat/ 0001.jpg ... dog/ 0001.jpg ... ... val/ cat/ ... dog/ ...torch.utils.data.DataLoader在读取时会自动把子目录名映射为类别标签这样就省去了手动管理标签的麻烦。目录结构整理好后用下面的代码划分训练集和验证集。我的划分比例是8:2其中8成训练2成验证。import torch import torchvision from torchvision import transforms from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder # 数据增强 train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_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]) ]) train_data ImageFolder(dataset/train, transformtrain_transform) val_data ImageFolder(dataset/val, transformval_transform) train_loader DataLoader(train_data, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_data, batch_size32, shuffleFalse, num_workers2)为什么Normalize的mean和std用ImageNet的标准值因为预训练模型是在ImageNet上训练的输入分布必须和预训练时的分布一致否则会严重影响迁移效果。很多人在这里踩坑觉得自己正常化也行但白化的分布差异会直接体现在准确率上。使用ImageNet的标准化参数是最稳妥的做法。3.3 微调训练脚本的核心参数加载预训练模型并替换分类头的代码很简练。这里我用resnet18把最后一层全连接从1000类改成10类from torchvision import models model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) num_ftrs model.fc.in_features model.fc torch.nn.Linear(num_ftrs, 10)训练阶段的优化器和损失函数选择我建议用交叉熵损失加Adam优化器。交叉熵是多分类任务的标准损失函数它对输出概率分布和真实标签之间的差异进行度量Adam优化器则是自适应学习率方法的代表相比于SGD它对学习率不那么敏感适合课程设计这种没有大把时间调参的场景。基础学习率设为1e-4这是迁移学习惯用的低学习率区间。因为预训练模型的特征层已经学得比较好了学习率太高会导致模型在微调时大幅震荡反而破坏已经学到的好特征。batch_size用32在训练数据量不大时完全够用。训练时我做了这样一个判断前几层卷积提取的是通用特征在微调时不参与参数更新所以冻结了model.conv1和model.bn1其余层不冻结。你也可以选择更极端的做法只训练最后全连接层但那样的话准确率通常不够理想。下面的脚本包含完整训练循环import time device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) # 冻结conv1和bn1 for name, param in model.named_parameters(): if name.startswith(conv1) or name.startswith(bn1): param.requires_grad False criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-4) def train_one_epoch(model, loader, criterion, optimizer, device): model.train() running_loss, correct, total 0.0, 0, 0 for inputs, labels in 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) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) return running_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() running_loss, correct, total 0.0, 0, 0 with torch.no_grad(): for inputs, labels in loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) running_loss loss.item() * inputs.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) return running_loss / total, correct / total epochs 10 for epoch in range(epochs): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) print(fEpoch {epoch1}/{epochs} | fTrain Loss: {train_loss:.4f} Acc: {train_acc:.4f} | fVal Loss: {val_loss:.4f} Acc: {val_acc:.4f})我训练了10轮最终在验证集上准确率约为93.2%。如果只是训练5轮准确率大约在88%左右当然这个数字和数据集规模直接相关十类每类两百多张图的规模下10轮已经足够收敛。你如果自己准备的数据集更大适当增加训练轮数即可。3.4 模型保存与加载一个版本升级引发的血泪坑训练结束后保存模型我推荐只保存权重参数而非整个模型对象。torch.save(model.state_dict(), animal_resnet18.pth)这个文件就是整个系统的核心产物后续Flask会加载它。这里我要特别提醒一个坑这个坑在PyTorch 2.6版本之后变得极其常见新版PyTorch把torch.load的weights_only参数默认值改成了True这就导致如果用旧版本保存的模型、新版本加载时会报“Weights only load failed”之类的错误。在校生经常在课程群里互相传代码和权重文件别人的环境是2.4你的环境是2.6加载别人的模型就可能报错。解决办法有两种一是加载时显式指定weights_onlyFalse二是用兼容性更好的格式。我推荐用方案一model.load_state_dict(torch.load(animal_resnet18.pth, map_locationcpu, weights_onlyFalse))map_locationcpu是为了应对GPU训练的权重在CPU机器上加载的情况建议这个参数也写上因为答辩现场的机器配置不可控。4. 三端联调PyQt客户端、Flask后端与HTML5前端的协同实现4.1 Flask后端模型推理接口设计模型训练完以后我们把它封装成HTTP服务。Flask在这个项目里的角色是后端推理服务它接收图像上传请求把图像送入ResNet18模型进行推理然后返回识别结果和置信度。选择Flask而不是FastAPI首要原因是期末大作业要求里出现了Flask其次Flask足够简单一个文件就能搞定全部逻辑。完整接口代码块如下import io import torch import torch.nn.functional as F from PIL import Image from flask import Flask, request, jsonify from torchvision import transforms, models app Flask(__name__) # 类别名称按目录顺序填写 CLASS_NAMES [butterfly, cat, chicken, cow, dog, elephant, horse, sheep, spider, squirrel] device torch.device(cuda if torch.cuda.is_available() else cpu) # 模型加载 model models.resnet18(weightsNone) num_ftrs model.fc.in_features model.fc torch.nn.Linear(num_ftrs, len(CLASS_NAMES)) model.load_state_dict( torch.load(animal_resnet18.pth, map_locationcpu, weights_onlyFalse) ) model.to(device) model.eval() # 预处理 tfms transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) app.route(/predict, methods[POST]) def predict(): if file not in request.files: return jsonify({error: missing file}), 400 file request.files[file] img Image.open(file.stream).convert(RGB) img_t tfms(img).unsqueeze(0).to(device) with torch.no_grad(): outputs model(img_t) probs F.softmax(outputs, dim1) conf, idx torch.max(probs, 1) return jsonify({ class: CLASS_NAMES[idx.item()], confidence: round(conf.item(), 4) }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)有四个细节值得注意。第一加载模型时你必须先重新构建一遍模型结构再加载state_dict否则会报尺寸不匹配错误第二请求参数名必须用file前后端传参名要统一这里PyQt端和HTML5端上传的字段名都必须是file第三图像打开后强制转成RGB避免有些图片是RGBA四通道导致预处理维度报错第四app.run里开了host0.0.0.0这意味着同一局域网内其他设备也能访问答辩时方便老师用手机浏览器连上来测。4.2 PyQt客户端界面布局与推理调用PyQt在这个项目里承担的是桌面客户端角色。你可以理解成一个“包装过的浏览器”用户不需要打开浏览器直接双击桌面程序就能上传图片并看到识别结果。我用的是PyQt5界面组件核心就那么几个QPushButton选择图片和开始识别两个按钮QLabel显示选中的图片和识别结果文本QFileDialog弹出文件选择对话框获取图片路径界面上采用上下结构布局上方显示图片下方放“选择图片”按钮和“识别”按钮侧边一个标签显示结果。下面是一段核心逻辑import sys import requests from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget, QFileDialog, QMessageBox) from PyQt5.QtGui import QPixmap from PyQt5.QtCore import Qt class AnimalClassifierWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(动物图像分类系统) self.setFixedSize(520, 620) self.image_path None self.image_label QLabel(请选择图片) self.image_label.setFixedSize(480, 400) self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet(border: 1px solid #ccc; background: #f8f8f8;) self.result_label QLabel(识别结果无) self.result_label.setAlignment(Qt.AlignCenter) self.btn_select QPushButton(选择图片) self.btn_predict QPushButton(开始识别) self.btn_select.clicked.connect(self.select_image) self.btn_predict.clicked.connect(self.predict_image) layout QVBoxLayout() layout.addWidget(self.image_label) layout.addWidget(self.result_label) layout.addWidget(self.btn_select) layout.addWidget(self.btn_predict) container QWidget() container.setLayout(layout) self.setCentralWidget(container) def select_image(self): path, _ QFileDialog.getOpenFileName( self, 选择图片, , 图片文件 (*.jpg *.jpeg *.png *.bmp)) if path: self.image_path path pixmap QPixmap(path).scaled( self.image_label.width(), self.image_label.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.image_label.setPixmap(pixmap) def predict_image(self): if not self.image_path: QMessageBox.warning(self, 警告, 请先选择图片) return try: with open(self.image_path, rb) as f: resp requests.post( http://127.0.0.1:5000/predict, files{file: f}, timeout10 ) if resp.status_code 200: data resp.json() self.result_label.setText( f识别结果{data[class]} | 置信度{data[confidence]}) else: self.result_label.setText(识别失败请检查后端服务) except Exception as e: QMessageBox.critical(self, 连接错误, f无法连接后端{str(e)})为什么用requests而不是PyQt自带的网络模块从课程设计角度requests足够简单也不需要额外事件循环两行代码就能搞定HTTP请求QNetworkAccessManager虽然更原生但需要结合信号槽机制代码复杂度提升不少对期末场景没必要。这里要特别注意PyQt程序只是发送HTTP请求真正的模型推理发生在Flask服务端。一旦你忘了先启动FlaskPyQt这边会直接报连接错误。答辩顺序务必是先启动Flask进程再打开客户端程序。我在代码里对连接失败做了异常捕获提示也写清楚了。4.3 HTML5前端浏览器端的上传与展示HTML5前端本质上也是一个Flask应用的前端页面用于在浏览器中完成推理。它更像一个可视化展示页方便答辩时切换演示方式。如果把PyQt端比作“桌面版”HTML5端就是“网页版”。我构建了一个简单的单页应用包含一个图片上传控件、一个预览区域和一个结果展示区域。前端用原生JavaScript的fetch接口发送POST请求不需要任何前端框架也不用引入jQuery直接把文件对象封装成FormData发出去就行!DOCTYPE html html langzh-CN head meta charsetUTF-8 title动物图像分类系统/title style body { font-family: Microsoft YaHei, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; background: #f5f5f5; } .container { background: #fff; border-radius: 12px; padding: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } h1 { text-align: center; } .upload-area { border: 2px dashed #aaa; border-radius: 8px; padding: 30px; text-align: center; cursor: pointer; margin: 20px 0; } .preview { text-align: center; margin: 20px 0; } .preview img { max-width: 360px; border-radius: 8px; border: 1px solid #ddd; } .result { text-align: center; font-size: 20px; font-weight: bold; color: #333; margin-top: 16px; } .btn { display: inline-block; padding: 10px 24px; background: #4a90d9; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; } /style /head body div classcontainer h1动物图像分类系统/h1 div classupload-area iduploadArea p点击选择图片/p input typefile idfileInput acceptimage/* hidden /div div classpreview idpreview styledisplay:none; img idpreviewImg alt预览 /div div styletext-align:center; margin:16px 0; button classbtn idpredictBtn开始识别/button /div div classresult idresult等待识别.../div /div script const uploadArea document.getElementById(uploadArea); const fileInput document.getElementById(fileInput); const preview document.getElementById(preview); const previewImg document.getElementById(previewImg); const predictBtn document.getElementById(predictBtn); const result document.getElementById(result); uploadArea.onclick () fileInput.click(); fileInput.onchange (e) { const file e.target.files[0]; if (!file) return; previewImg.src URL.createObjectURL(file); preview.style.display block; result.textContent 等待识别...; }; predictBtn.onclick async () { const file fileInput.files[0]; if (!file) { alert(请先选择图片); return; } const formData new FormData(); formData.append(file, file); result.textContent 识别中...; try { const resp await fetch(/predict, { method: POST, body: formData }); const data await resp.json(); result.textContent 识别结果${data.class} | 置信度${data.confidence}; } catch (err) { result.textContent 识别失败 err.message; } }; /script /body /html这里有个容易出错的位置如果你用flask.render_template返回这个HTML页面那么页面里fetch(/predict)的请求地址就是相对路径会被浏览器自动拼成http://服务器IP:5000/predict无需硬编码IP。但如果你直接双击打开HTML文件fetch(/predict)就会请求到本地文件的错误地址。所以我建议把HTML文件放到Flask的templates目录下并在Flask里增加一个渲染首页的路由from flask import render_template app.route(/, methods[GET]) def index(): return render_template(index.html)这样整个项目只需要启动一个Flask进程同时服务于HTML5页面和API接口PyQt端访问的也是同一个接口三个端用一套服务逻辑清晰也省去跨域处理的麻烦。5. 端到端测试与打包发布5.1 本地联调流程整个项目开发完成后我做了完整的端到端测试确保三个端都能正常调用模型接口。为了保证测试的可复现性我准备了几张每个类别有代表性的测试图片分别来自训练集外部和网络下载验证模型泛化能力。测试流程如下启动Flask服务终端出现Running on http://0.0.0.0:5000说明服务已就绪浏览器访问http://127.0.0.1:5000/打开HTML5页面点击上传按钮选择一张测试图点击“开始识别”确认返回结果启动PyQt客户端点击“选择图片”打开同一张测试图点击“开始识别”确认结果一致用curl命令直接向接口发送POST请求验证API返回结果curl测试命令curl -X POST -F filedog_test.jpg http://127.0.0.1:5000/predict返回结果示例{class: dog, confidence: 0.9721}这套流程基本覆盖了全部核心功能。我特别验证了类别结果在三个端一致。如果某个端结果不同多半是预处理参数不一致比如HTML5端图片做了缩放而PyQt端没做需要排查各端预处理逻辑。5.2 PyQt客户端打包成exePyQt程序的打包是很多同学卡壳的地方。我用的是PyInstaller打包命令如下pip install pyinstaller pyinstaller -F -w -i ./icon.ico client.py参数说明-F将程序打包成单独一个exe文件方便拷贝和演示-w指定窗口程序不输出控制台调试信息打包出来的程序不会闪黑框-i指定exe的图标答辩时加分用的换成自己的图标就行打包完成后exe文件位于dist/client.exe大小一般在50MB左右因为PyQt5本体就比较大。首次双击启动会比较慢因为要解压临时文件属于正常现象。运行exe时如果不提示无法找到requests模块那就在打包前额外指定隐藏导入pyinstaller -F -w --hidden-importrequests client.py这个问题是因为PyInstaller静态分析时可能漏掉一些动态导入的模块。我实际打包过程中第一次没加--hidden-import就出现了“No module named requests”的报错。5.3 答辩演示时的三个注意事项演示环节最容易翻车的点我总结为三件事第一模型文件路径问题。Flask代码里加载模型用的是相对路径animal_resnet18.pth这个路径是相对于当前运行目录的。答辩前务必把你的权重文件和app.py放在同一个目录下或者干脆在代码里用绝对路径用os.path.join(os.path.dirname(__file__), animal_resnet18.pth)来定位这样不管从哪个目录启动都不会出错。第二端口占用。如果答辩前调试时Flask出现端口被占用直接换一个端口比如5001同时更新PyQt和HTML5里的请求地址。代码中尽量把后端地址统一放在一个配置变量里别在多个文件里硬编码改来改去。我写代码时在client.py里定义了API_URL http://127.0.0.1:5000/predict改起来只动一行。第三启动顺序。必须先启动Flask服务端再打开PyQt客户端。答辩前把启动流程在终端里提前跑一遍确保权重能加载、端口能监听。如果现场用教室电脑建议提前把所有依赖环境打包成requirements.txt答辩前一天在目标机器上装好环境别在答辩现场装依赖。6. 进阶优化方向与我的踩坑总结6.1 从ResNet18升级到ResNet50如果你的老师对准确率有更高要求或者你希望挑战更有分量的大作业可以考虑把ResNet18换成ResNet50。ResNet50引入了Bottleneck结构先用1x1卷积降维再用3x3卷积提取特征最后用1x1卷积升维整体参数量约2350万比ResNet18大了一倍左右但也完全在训练资源可控范围内。我在项目中期做过一次对比实验同一份数据增强配置下ResNet50的验证准确率比ResNet18高出约1.5到2个百分点但训练时间长了大约一倍CPU环境下10轮训练大概要40多分钟。如果训练资源受限建议还是ResNet18为主把准确率瓶颈放在数据增强和数据质量上比如增加更多随机裁剪、色彩抖动等变换操作。6.2 几个印象深刻的坑关于PyTorch 2.6的weights_only变更我在前面讲过了我再强调一遍这个坑非常隐蔽因为代码没问题、报错信息却不直观。新版默认只允许加载张量、字典等基础类型旧版保存的完整模型对象会直接拒绝加载所以加载模型时统一加weights_onlyFalse是最省心的做法。另一个坑是PIL图像旋转。现代手机拍摄的照片自带EXIF方向信息但PIL的Image.open默认不会自动应用方向修正这会导致明明是一张“卧倒”的猫照片模型输入时却是横着的识别结果自然不对。解决办法是在预处理前加一行from PIL import ImageOps img ImageOps.exif_transpose(img)这段代码会自动根据EXIF信息旋转图像确保输入方向正确。做前端页面时这个问题尤其明显因为用户上传的图片大部分来自手机拍摄。最后一个坑是多进程加载数据导致的死锁。Windows环境下DataLoader里的num_workers如果大于0模型训练脚本直接双击运行可能会卡死。解决方法是把训练代码放到if __name__ __main__:代码块里或者在DataLoader里设置num_workers0。我第一次写训练脚本时没注意双击运行直接卡在第一个epoch改成终端里python train.py运行才正常。我在实际操练这个项目时最深刻的感受是期末大作业的核心不在于用了多前沿的模型而在于整个系统能不能跑通、能不能讲清楚、能不能现场演示。你把ResNet的原理讲明白了把迁移学习的细节说清楚了再把Flask、PyQt、HTML5这条数据链路理顺了这次课程设计的训练目标其实就已经达成了。最后再分享一个小技巧把所有代码文件放进项目根目录附上一份README写清楚运行步骤、环境版本、各模块功能和演示截图打包成zip交上去这个分数通常不会低。本文还有配套的精品资源点击获取