☰
深度学习图像分类实战:基于PyTorch的鞋类分类毕设全流程解析
2026/10/21 12:17:16 网站建设 项目流程

1. 这个毕设题目选得聪明,但坑也不少

每年毕业季我都会被问到同一个问题:“老师,深度学习方向的毕设,到底选什么题目能顺利做完又不至于太水?”后来某高校一个朋友让我帮他把关他的毕设开题,题目就是“基于Python深度学习的鞋类分类”,我当时第一反应是:这题目很适合作为进入视觉分类领域的第一块跳板,它的技术栈完整覆盖了数据准备、迁移学习、模型训练到评估展示,难度曲线又足够平缓,不至于让人卡在某个数学推导上直接放弃。

鞋类分类本质上是一个细粒度图像分类问题。它和猫狗分类、花草识别这些通用分类不太一样,鞋子品类之间的差异往往集中在鞋头形状、鞋跟高度、鞋帮轮廓、材质反光这些局部细节上。比如运动鞋和帆布鞋,整体轮廓都是圆头低帮,但鞋底厚度、鞋面纹理、系带方式完全不同;高跟凉鞋和无跟凉鞋最大的区别可能只在鞋跟那几厘米的侧面剪影。这种“大框架很像、局部细节决定身份”的特性,正好能逼着你把数据增强、模型微调、混淆矩阵分析这些环节真正做深,而不是简单套一套代码就完事。很多学生做到后面会发现,真正的难点不是让代码跑起来,而是让模型学会区分那些长得像的类别。

这篇文章我就用自己实际辅导过的一段完整流程作为主线,从题目拆解、数据集整理、模型选型、训练调参到最后的答辩素材准备,把每一步怎么判断、怎么做、为什么这样做讲清楚。适合正在准备相关方向毕设、或者想系统走一遍深度学习图像分类全流程的朋友参考,有基础的可以直接跳到你关心的章节看,纯新手的建议从头往后读。

2. 把“鞋类分类”拆成三个问题:数据、算力、算法边界

2.1 题目拆解:约等于一次“带约束的行业落地”

拿到“基于Python深度学习的鞋类分类”这个题目,你先别急着打开代码编辑器。我习惯让学生做的第一件事,是把题目翻译成三句大白话:我们需要拿到一批包含多种鞋类图片的数据;我们要训练一个模型,让它对没见过的新图片输出正确的鞋类标签;我们要用足够有说服力的方式,证明这个模型确实可靠。这三句话对应到实际操作里,就是数据采集清洗、模型训练调优、指标体系构建三个模块,任何一个模块做不好,整篇论文都会出现短板。

环境依赖方面,主流做法就是Python配合深度学习框架,我这次用的是PyTorch,选它的原因后面会单独说。算法层面通常跑不出一条从零训练的卷积神经网络路线,绝大多数人会选择基于ImageNet预训练权重做迁移学习,这也是目前图像分类任务里性价比最高的一条路——你不需要几十万的标注数据和几周的GPU时间,几千张样本图加上一张消费级显卡就能做出不错的效果。

这个题目的“边界”也很重要。鞋类分类和通用物体分类不同,它要求的不是一个粗粒度的“这是不是鞋”的判断,而是“这是哪一类鞋”的细分判断,这就决定了模型的最后分类层数量、数据集的类别结构都必须仔细设计。很多学生在这里翻车,比如拿到的数据集类别数量不一致、同一种鞋在不同来源的命名规则不同,直接导致训练脚本报错。

2.2 一个容易被低估的约束条件:算力和时间

算力是很多人写开题报告时不重视、实际做起来才痛苦的变量。如果你在本地用CPU训练一个ResNet50,一个epoch跑完可能就需要十几分钟,而一个像样的迁移学习流程至少要跑20到30个epoch,再算上反复调参的时间,CPU方案的体验会很差。我平时给的建议是:优先用可用的GPU环境,没有的话就尽量选轻量级的骨干网络,比如ResNet18或MobileNetV3,不要一上来就堆ResNet152,很多人的显存和耐心都撑不住。

时间因素同样关键。毕设的实际周期往往比想象中短,前期要写开题报告、中期要出阶段性成果、后期还要预留答辩材料制作时间,真正留给调参的时间窗口普遍只有一到两周。所以整个技术路线要偏保守:用成熟稳定的预训练模型,用公开可复现的数据集,把精力放在特征分析和结果解释上,而不是折腾花哨的新网络结构。学校里做研究,也要追求“结果可控”,这是选择题目后第一件要想明白的事。

3. 数据准备:这个环节决定了你后面50%的成败

3.1 数据集结构设计与类别梳理

鞋类分类现在网上能找到一个公开的鞋类图片数据集,里面按照品类划分成了13个大类,包括运动鞋、皮鞋、帆布鞋、凉鞋、靴子、高跟鞋、拖鞋这些常见类型。我让学生先别急着跑模型,而是先把数据集目录结构整理清楚。PyTorch的ImageFolder机制要求每个类别一个文件夹,文件夹名字就是类别标签,目录结构和预期不一致会直接影响加载。

举个例子,这份数据集里同一个大类下可能存在风格或视角上的差异,比如“靴子”里既有短靴也有过膝长靴、既有侧面视角也有俯拍视角,这些差异如果没有在数据划分阶段做好均匀分配,训练集和验证集的效果会出现明显波动。我的做法是先统计每个类别的样本数,然后按类别分层划分训练集、验证集和测试集,比例大约在7:2:1左右。注意这里一定是“分层划分”,如果用随机划分,某些样本少的类别很可能在训练集或测试集里出现数量异常,最终得出的准确率也会有偏差。

3.2 图像清洗与基础预处理

原始数据不能直接喂给模型。鞋子图片通常尺寸不一致,有的商家图分辨率很高,有的用户拍摄图非常模糊,甚至还有少量图片带着水印文字、尺码标注、背景抠图残留的痕迹。我在预处理阶段做的最核心一件事是统一输入尺寸,常用的是224x224,这是ResNet系列默认接收的大小。原始图片在resize过程中如果直接拉伸,鞋子的长宽比会变形,影响模型学习形状特征,所以建议先做等比缩放再居中裁剪,这样能保留更多有效信息。

另外,为了保证模型在不同光照和背景下都能稳定工作,我还加了一个简化的背景干扰排除步骤:优先保留主体占比大的图片,丢弃明显的纯背景图或信息残缺图。这一步不是必须的,但做了之后测试集上的准确率确实更稳定。全部整理完后,单类别样本数尽量控制在几百张以上,类别太少时要考虑是否需要借增强来补充,这方面后面会说。

3.3 数据增强:少样本和过拟合的救星

鞋类数据集整体规模不大,单类别样本数可能只有几百张到一千张,直接训练很容易出现过拟合。数据增强在这里的作用不只是“把数据变多变花”,更重要的是模拟出实际场景里的各种变化,让模型学会“不管鞋子在图片里是正着放、斜着放、背景换掉,都能认出来”。

我实际用到的增强策略:随机水平翻转、随机旋转(正负10度以内)、随机仿射变换、颜色抖动(亮度、对比度、饱和度微调)、随机裁剪后缩放回原尺寸。水平和垂直翻转要注意一点,鞋类图片左右对称,水平翻转安全;垂直翻转一般不用,因为会改变鞋子的“摆放姿态”,在部分类别上反而会引入不合理样本。另外,如果训练集中某些样例带有较大透明度或反光效果,可以适当加入少量随机擦除(Random Erasing),模拟鞋面被部分遮挡的情况,增强模型的鲁棒性。

增强幅度要适度。过强反而会让模型学不到有效特征,甚至导致训练集损失迟迟降不下去。我自己踩过的判断标准是:增强后的图片人眼仍然能轻松认出是什么鞋,如果连人都认不出来了,这个增强强度就该回调。

4. 模型选型和训练策略:为什么预训练迁移学习是首选

4.1 从零训练卷积网络为什么不划算

很多新手拿到图像分类项目,第一反应是自己搭一个卷积神经网络,比如几层卷积加池化再加全连接。这个思路在玩具数据集上没问题,但放到13类鞋分类上效果会迅速触顶。原因不复杂:卷积网络从零训练需要大量标注数据来学习底层纹理、边缘、颜色分布这些基础特征,而鞋类数据集规模远达不到这个量级。模型很容易陷入“在训练集上记得住、在测试集上认不出”的状态。

ImageNet预训练权重相当于替你把“看世界的基础能力”练好了,它已经知道什么是边缘、什么是纹理、什么是物体轮廓,我们要做的只是让它把这些能力迁移到“区分不同鞋子”这个具体任务上。这在工业界也几乎是最常见的选择——用预训练模型做迁移,再用相对较小的数据量微调,成本低、效果好、可解释性强。

4.2 骨干网络对比:ResNet18、ResNet50还是EfficientNet

在动手之前,我让学生先做了个小表格对比,这步花不了多少时间,但能让选型理由充足地写进开题报告:

骨干网络参数量推理速度效果水平(迁移学习)显存占用适用情况
ResNet18较小快良好低CPU可接受,入门推荐
ResNet50中等中等优秀中有入门级GPU推荐
EfficientNet-B0较小快良好偏上低追求性价比
EfficientNet-B4较大慢优秀较高算力充足可选
MobileNetV3最小极快中等偏上极低强调部署场景

我最终选择的是ResNet50作为主干,理由是它在参数量和效果之间平衡得最好,ResNet的残差结构在训练稳定性上也比一些深层VGG类网络强很多,整个训练过程不容易跑飞。如果你只有CPU环境,建议用ResNet18,效果差距在鞋类这种难度不算极端的数据集上不会特别大,但训练时间会显著缩短。

在PyTorch里加载预训练模型只需要一行代码,把分类头替换成自己任务对应的类别数就可以了:

import torchvision.models as models import torch.nn as nn model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) # 替换最后一层全连接,13个类别 in_features = model.fc.in_features model.fc = nn.Linear(in_features, 13)

4.3 冻结与微调:先让特征提取层稳住,再动分类头

迁移学习里最关键的策略问题是:哪些层要冻结、哪些层要训练。常见的做法分两种,一种是把整个骨干网络参数全部冻结,只训练最后新加的分类层;另一种是骨干网络全部参与训练,从头到尾微调一遍。前者速度快、不容易过拟合,但效果上限有限;后者拟合能力强,但训练时间更长,对数据量和正则化要求更高。

我这次采用了一个中间策略:第一轮训练时冻结骨干网络的所有层,只训练新替换的分类头,用较大的学习率快速训练几个epoch,让新分类头先学会利用预训练特征;然后解冻骨干网络的最后几个残差块,用较小的学习率继续训练。这样可以兼顾效率和精度,也避免了一开始就全量微调导致特征被破坏的问题。

具体实现时,可以设置参数的requires_grad属性来控制哪些层参与训练:

# 先冻结全部参数 for param in model.parameters(): param.requires_grad = False # 解冻最后一层分类头 for param in model.fc.parameters(): param.requires_grad = True # 第一轮只优化分类头 optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001) # 第二轮解冻部分骨干层 for param in model.layer4.parameters(): param.requires_grad = True optimizer = torch.optim.Adam([ {"params": model.layer4.parameters(), "lr": 0.0001}, {"params": model.fc.parameters(), "lr": 0.0001} ], weight_decay=1e-4)

这样的两阶段做法在调整参数时也更直观:第一轮如果分类头已经能稳到一定的准确率,说明特征提取没问题;第二轮微调后准确率还能继续上升,说明任务确实需要一些更加个性化的特征。

5. 训练参数、损失函数与完整代码框架

5.1 训练脚本的工程结构

如果把整个项目比作一份外卖订单,训练脚本就是厨房里负责炒菜的灶台,前面所有数据准备都是为了给灶台备好菜。我习惯把工程拆成几个文件,每个文件只负责一件事,这样后期调试和写论文时都方便对照:数据加载和增强逻辑放一个文件,模型定义放一个文件,训练主循环放一个文件,评估代码单独再放一个文件。不要想着把所有代码写到一个脚本里跑完就算,否则后面定位问题、补充实验对比时你会非常痛苦。

5.2 核心训练代码:稳定可跑的最小实现

训练主循环其实没有太多神秘的地方,PyTorch生态已经很成熟,主要是注意几个容易出错的小点:模型和数据的device一致、每个epoch结束要记得验证、保存模型参数而不是保存整个模型对象。下面是一个整理过的最小训练框架:

import torch import copy from torch.utils.data import DataLoader from tqdm import tqdm def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss = 0 correct = 0 total = 0 for images, labels in tqdm(loader, desc="Training"): images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() return total_loss / len(loader), 100.0 * correct / total def evaluate(model, loader, criterion, device): model.eval() total_loss = 0 correct = 0 total = 0 with torch.no_grad(): for images, labels in loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) total_loss += loss.item() _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() return total_loss / len(loader), 100.0 * correct / total

有了这个基础框架之后,换数据集、换模型、改超参数都很方便,这也是我推荐同学们把训练代码模块化的重要原因,后续要加学习率调度、早停机制都只需要在对应位置插入几行代码。

5.3 损失函数的选择:为什么交叉熵就够了

鞋类分类是标准的单标签多分类问题,一张图对应一个类别,交叉熵损失是最自然的选择,多数情况下没有理由换成其他损失。PyTorch里nn.CrossEntropyLoss()自带Softmax计算,你只需要把模型的原始输出丢给它,不需要额外对输出做Softmax。

不过有两个细节值得注意。第一个是类别不均衡问题,当出现某个类别样本数远多于另一个类别时,模型会倾向于把所有图片都预测成大类。调整策略可以是按类别频率设置权重,把损失里小类的loss权重调高:

from sklearn.utils.class_weight import compute_class_weight import numpy as np classes = np.array(list(train_dataset.class_to_idx.values())) sample_labels = train_dataset.targets class_weight = compute_class_weight(class_weight="balanced", classes=classes, y=sample_labels) class_weight = torch.tensor(class_weight, dtype=torch.float32).to(device) criterion = torch.nn.CrossEntropyLoss(weight=class_weight)

第二个细节是标签平滑(Label Smoothing)。如果数据集中存在少量误标注的图片,硬标签会在训练中放大大类的置信度,而标签平滑相当于给标签留了一点容错空间,通常能带来1%到3%的准确率提升。PyTorch里在构造交叉熵损失时传一个label_smoothing=0.1参数就能开启,多数项目都适用。

5.4 优化器、学习率与训练轮数:几组可以直接参考的参数

优化器我尝试过SGD和Adam。SGD配合momentum在微调阶段表现稳健,Adam在前期收敛速度快。考虑到毕设的实验周期不会太长,我选择的主优化器是AdamW,它的收敛稳定,配合weight decay实现起来更顺手。学习率调度我用了余弦退火,它能在训练后期逐步缩小步长,让模型在最优解附近精细调整,比学习率固定不变或少次step decay更平滑。

如果你希望有个公式化的起点,可以直接参考这套参数:批量大小32、初始学习率0.0001(微调阶段)、weight_decay 1e-4、训练总epoch为25到30轮。这组参数在ResNet50加13类鞋分类的配置下表现正常,如果你的显卡很好,可以增大批量到64,配合同步增大一点学习率;如果训练不稳定,就把学习率再调低一半。不要迷信某个具体数值,观察损失曲线及时调整才是关键。

6 训练过程中的真实坑点:从90%验证集跌到78%测试集说起

6.1 一个经典的翻车现场:验证集很高,测试集很低

训练进行到一半时,我让学生跑了一下测试集,发现准确率只有78%左右,而验证集上已经快到90%了。这个数字很能迷惑新手,因为从验证集上看训练一切正常,至少说明模型没有“没学会”。但验证集和测试集的准确率大幅差值,直接指向了一点:训练过程和验证过程存在某种程度的信息泄漏,或者模型依赖了不该依赖的背景信息。

排查思路是这样的:第一步检查数据划分。如果验证集和训练集来自同一个目录、只是随机抽样拆分,那么验证集的分布和训练集高度重合,模型在验证集上表现好并不代表泛化能力好。第二步检查数据集内部结构。鞋类数据集中的商详图经常会带纯色背景,而部分用户上传的照片场景多样,如果某一类在训练集中很多图片都是白底商详图,测试集却主要是真实场景照片,模型就会学到“白色背景=运动鞋”这种错误规则。第三步才是最让人头疼的,需要实际去看看那些预测错误的样本,确认模型到底在做规则记忆还是规则理解。

6.2 过拟合证据:从损失曲线到预测行为

在B同学(我带的另一个学生)的项目里,他当时通过绘制训练损失和验证损失的曲线,很快就看出问题:训练损失一路下降到0.2以下,验证损失却在epoch 8附近开始反弹。这就是教科书级的过拟合曲线,但在鞋类数据集里,这种反弹有时候没那么明显,因为验证集整体准确率还在涨,只有细分到某些类别时才能看到端倪。

我还推荐过一个更直接的判断方法:把训练集输入模型,看它的预测置信度。如果模型对训练样本的置信度普遍超过0.99,而对测试样本的置信度明显下降很多,说明模型更多是在“背答案”。一个健康的模型应该能在不熟悉的图片上保持信心,同时又不至于对错误分类给出极高的错误置信度。这种置信度分析可以直接做成论文里的可视化和分析图。

6.3 针对过拟合的四个修复动作

发现问题之后,我建议的顺序是先调整数据,再调整模型约束,最后才动结构。在鞋类这个项目里,有几个具体动作很有效:第一是重新做严格分层划分,保证训练、验证、测试三个集合中各类别比例一致,如果必要,直接把验证集和测试集从不同来源的图片中抽出来。第二是增强强度上调10%到20%,尤其是随机裁剪和颜色抖动,让模型不再依赖固定背景或固定构图。第三是开启随机擦除,目的前面说过,是为了让模型学会通过局部特征判断类别。第四是加大weight_decay,从较小的值逐步升到1e-3,当损失产生明显波动时,说明正则化已经过强,需要回调。

还有一个很容易被忽视的点:增强策略不能用于验证集和测试集,只能用于训练集。有人图省事,把所有的图片在进入模型前都统一做随机翻转,这等于让验证集的每一轮评估都在“变题目”,最后得到的结果没有可比性。我在代码里用transforms区分了训练集和验证集的数据处理流程,验证集只做resize和normalize,不做任何随机操作。

6.4 调整后的效果:从78%到91%

经过上述几步修改,测试集准确率从78%稳定升到91%左右,这个过程大概用了三轮实验。第一轮只修数据划分方案,准确率升到84%;第二轮增强强度微调加梯度裁剪,升到88%;第三轮把冻结策略换成“先冻结再解冻后几层”,最后到了91%。这说明该项目的性能瓶颈并不在于模型结构不够复杂,而在于数据带来的虚假规律干扰。对于绝大多数类似的分类项目,这个结论都适用:先查数据,再调模型,顺序反了你可能调一星期都找不到方向。

7. 评估可视化与论文素材:分数之外,答辩真正看的“证据”

7.1 混淆矩阵:看每一类到底“像谁”

准确率只是一个数字,放在毕设答辩里说服力不足。混淆矩阵才是真正能展示“模型哪里分得清、哪里容易混淆”的硬材料。鞋类分类结果里,运动鞋和帆布鞋混在一起、高跟鞋和凉鞋偶尔分不清,这类混淆信息用矩阵一眼就能看到。

我习惯把混淆矩阵绘制成带颜色深度的热力图,纵轴是真类别,横轴是预测类别。答辩时你指着矩阵里偏离对角线的亮点说明“模型主要混淆了哪两类,原因可能是它们的轮廓特征接近,后续可以怎么做”,这种逻辑线比单纯报一个准确率高很多。计算混淆矩阵可以用sklearn.metrics.confusion_matrix,数据准备好之后画图很顺手。

除了混淆矩阵,类别级别的精确率、召回率和F1分数也要单独整理成表。13个类别的F1如果大都在85%以上,只有一两个类别偏低,那这个项目的评分天然具备“偏科分析”的素材。如果所有类别F1都很平均,说明模型在各类别上表现稳定,也是加分项。

7.2 模型可视化:注意力热力图和错误样本分析

现在做视觉类毕设,不做可解释性分析总觉得少了点什么。ResNet这类模型不好直接可视化卷积核,但可以用Grad-CAM生成注意力热力图,把模型分类时重点关注的区域叠加到原图上。具体效果就是:模型判断一双运动鞋时,热力图的高亮区域集中在鞋底和鞋面装饰线附近,而不是背景或者标签水印上,这样的图放进答辩PPT会非常加分,它证明模型从语义上理解鞋子结构。

Grad-CAM的实现网上有很多现成封装,要注意的是选择目标层,一般取最后一个残差块的输出,太浅的层可视化结果比较碎,太深的层则分辨率低。错误样本分析也是一样,挑选三到五张预测错误的测试图片,每张图配上“真实类别、预测类别、置信度、人工分析可能原因”四行信息,这类素材在答辩时常常能提前堵住评委“这个模型为什么错了”的追问。

7.3 把实验记录整理成论文素材

毕设论文不是把代码注释拿来抄一遍。根据实际的实验流程,我当时帮学生整理了这样几条主线:迁移学习与随机初始化的对比实验,记录两者在训练损失、验证准确率上的差异,这可以直接支撑“为什么要用预训练模型”的背景;不同骨干网络(ResNet50和EfficientNet-B0)的效果对比表,包含准确率、F1、推理时间和占用显存,用来支撑选型理由;数据增强前后测试集准确率变化,这组数据能直接证明增强的有效性。

需要注意的是,实验记录最好从一开始就写下来。很多同学把训练脚本跑完就结束了,觉得结果已经有了,等写论文时才发现当时改了哪些参数完全没记住。我建议在项目目录下建一个“实验日志”的文档,每次训练前写下参数设置和预期,训练后记录实际效果和发现的问题,写论文时你只需要把这些内容串起来,远比事后回忆靠谱。

7.4 模型导出:顺便加一个简单Web演示页

如果时间充裕,我强烈建议把训练好的模型导出,做一个极简的Web演示页面。PyTorch模型可以用torch.onnx.export导出成ONNX格式,再借助任意前端框架(比如Flask或Streamlit)包一层接口,页面放一个上传按钮,用户传一张鞋子的照片,页面返回分类结果和各类别置信度。演示时真实上传一张测试集之外的图片,模型输出运动鞋89%置信度,评委的观感会比看一页实验结果好很多。而且这个工作本身也能写进论文的“系统设计与实现”章节,让毕设的完整性再上一个台阶。

模型的导出代码不复杂:

model.eval() dummy_input = torch.randn(1, 3, 224, 224, device=device) torch.onnx.export( model, dummy_input, "shoe_classifier.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, opset_version=11 )

这样一个20余行代码的模块,换来的是“具备可用性”的评价,既有前瞻性又不用过多精力。

8 一点个人经验:毕设重点不是刷分,而是讲好一条完整的验证链

看着代码和文档从零搭起来,到最后测试集准确率91%、混淆矩阵和热力图素材齐备,我心里很清楚这个项目真正值钱的不是那91%,而是过程中反复打磨出的那一整条验证链:从数据是怎么来的、如何清洗划分,到为什么选这个模型、训练参数怎么调的,再到最终评估指标怎么计算、误差如何分析,每一步都有据可依,每一步都能回答“为什么这样做”。

如果接下来你也准备做类似方向,我最后想分享几个具体的小招式:不要等到全部训练结束了才开始写文档,训练间隙顺手记录过程和实验结果会让你后期轻松很多;不要把所有样本一次性打个数据集包,分层划分好训练、验证、测试后再打包,后续所有实验都用同一份划分,防止同一个数据在不同轮次之间反复横跳;不要以为跑通了模型就万事大吉,花两三个小时做一个简单的Web演示页,能给答辩带来非常高的印象分。

我自己总结的一个核心建议是:毕设不是前沿论文,不用追求刷分冲到99%,评委想看的是你能不能在有限资源里做出一个有人类意义、有逻辑、有展示度的完整项目。把这条验证链讲清楚,就算准确率只有85%,也是一份让人信服的毕设作品。

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

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

立即咨询