卷积神经网络(CNN)在图像分类里早就不是什么新鲜事,但每次到了课程设计或者毕业设计选题目的时候,总有一批同学盯着"基于深度学习的鞋类分类"这类题目发懵。鞋类分类听起来简单,实际做起来要踩的坑比想象中多:数据集怎么选、标签怎么定、模型用现成的还是自己搭、训练的时候显存不够怎么办、最后成绩怎么评估——每个环节都能卡住人。前阵子我帮A同学把这个项目从头到尾捋了一遍,从环境搭建到最终提交报告,整个过程积累了不少经验。这篇文章就把完整过程整理出来,给准备做同类图像分类项目的朋友做个参考。
题目里的"卷神经网络"其实是个常见笔误,指的正是卷积神经网络(CNN)。如果你也拿到了类似表述的题目,不用怀疑,主题就是图像分类任务。下面直接进入正题。
1. 项目需求与方案设计
1.1 课程设计到底在考察什么
课程设计或毕业设计的核心考察点,并不是把准确率刷到99%,而是你有没有完整走通"数据—模型—训练—评估"这条主线。鞋类分类是一个典型的细粒度图像分类任务,鞋子的类别(运动鞋、皮鞋、凉鞋、拖鞋、靴子、帆布鞋等)之间存在较明显的形态差异,但又不至于像猫和狗那样差别巨大。这种任务非常适合用来考察学生对特征提取、数据增强、训练调参、结果分析的综合理解。
动手之前,先想清楚一个关键问题:最终交付物是什么?是代码能跑、有训练日志和评估曲线就行,还是需要做一个带Web界面的实时识别演示?这两种目标的方案选型完全不一样。如果只是课程报告,本地脚本加可视化曲线足够;如果要求演示系统,还要额外考虑模型导出、推理接口和简单的前端页面。A同学的需求是后者,所以我在训练完成后额外加了一个基于浏览器上传图片的识别页面,这部分工作其实不大,但对最终分数很有帮助。
另一个容易被忽略的点是题目的验收标准。有的老师看重创新点,有的看重工程完整性。如果题目明确写了"基于深度学习",那核心工作量要放在网络结构或训练策略上,而不是把精力全花在写一个花哨的GUI上。建议动手前先找往届报告或老师沟通一下,确认评分维度,再分配时间。
1.2 方案选型:为什么用卷积神经网络而不是传统方法
传统方法做鞋类分类,常见套路是提取HOG、颜色直方图或局部二值模式特征,再喂给支持向量机或随机森林。这种方案在类别少、背景干净的场景下确实能跑,但一旦遇到复杂背景、光照变化、拍摄角度多样的情况,准确率就会迅速下降。深度学习方案,尤其是卷积神经网络,能够自动从像素中学习层次化的特征表达,从边缘、纹理到语义部件,端到端地完成分类,省去了大量手工特征工程。
那为什么不直接上Vision Transformer(ViT)?ViT在大型数据集上表现很好,但课程设计的数据规模通常在几千到一两万张图片。ViT通常需要大规模预训练或者更多数据才能收敛,否则容易欠拟合。反观CNN,归纳偏置更强,在小数据集上培训效率高、显存占用低、超参数更鲁棒。对于鞋类分类这种任务,CNN是性价比最优的选择。如果你在报告中能把这个对比写清楚,老师会觉得你有全局视野,而不是只会跟风。
1.3 技术路线总览
整个项目的技术路线可以拆成四个环节:数据准备、模型构建、训练调优、评估部署。数据环节包括数据集获取、清洗标注、划分训练/验证/测试集、预处理和数据增强;模型环节包括选择基础网络结构、决定是否使用预训练权重;训练环节包括设置优化器、学习率、批次大小、正则化策略;评估部署环节包括计算指标、绘制混淆矩阵、导出模型并做推理演示。
这套流程看起来常规,但每一步都有值得展开的细节。比如数据集怎么划分才能避免同类样本全部挤在训练集里?迁移学习时哪些层应该冻结?学习率什么时候该降?验证集上发生过拟合时先动数据还是先动模型?这些问题如果没提前想明白,后面跑起来会非常被动。下面按实际操作顺序详细拆解。
2. 数据准备与预处理
2.1 数据集选择与标注问题
鞋类分类项目最先卡人的就是数据。公开数据集方面,可以选某开源数据平台上的鞋类子集,或者从电商平台商品图数据集中筛出鞋子相关类别。如果找不到现成的,也可以用爬虫自己爬,但爬虫方案有版权风险,而且图片质量参差不齐,人工清洗的工作量非常大。对于课程设计周期,我的建议是优先用公开数据集,把省下来的时间花在模型调参上。
我帮A同学选定了一个约一万五千张图片的鞋类子集,共十类:运动鞋、篮球鞋、皮鞋、凉鞋、拖鞋、靴子、帆布鞋、高跟鞋、板鞋、雨靴。图片来源比较复杂,存在类别边界模糊的问题。比如"运动鞋"和"篮球鞋"有大量形态重合,如果标注的人凭感觉分类,模型就会学到错误边界。建议提前约定好标签规则,比如按鞋帮高度、鞋底厚度、系带方式等客观标准区分,减少主观判断。
拿到数据后一定要做清洗。常见问题包括:图片文件损坏、尺寸小于32x32、图片内容是鞋盒而非鞋子、同一张图片出现在多个类别文件夹里。清洗方式可以写脚本遍历检查图片能否正常打开和尺寸是否满足要求,也可以人工抽样浏览快速过一遍。这一步很枯燥,但没有它,后面模型精度会崩得很离谱。A同学清洗掉了大约三百多张问题图片,最终训练效果明显提升。
2.2 图像预处理细节
预处理的核心是把原始图片统一成模型能吃进去的格式。卷积神经网络通常要求固定尺寸输入,常见的有224x224、227x227、299x299。ResNet、VGG系列用224x224,Inception系列常用299x299。尺寸越大,信息保留越多,但显存和计算量也随之增加。课程设计推荐224x224,这个尺寸在精度和资源消耗之间最平衡。
除了尺寸调整,还需要做归一化。PyTorch中常用这样一组均值方差:mean为[0.485, 0.456, 0.406],std为[0.229, 0.224, 0.225],这组参数来自ImageNet数据集。如果你的模型使用了ImageNet预训练权重,就必须用这套归一化参数,否则输入数据分布和预训练时的分布不一致,迁移效果会打折。如果是从零训练,也可以用数据集的全局均值方差,但实际影响不大。
一个很容易踩的坑是:训练集和验证集的预处理策略必须不同。训练集需要随机增强(随机裁剪、翻转等),验证集应该只用确定性变换(中心裁剪、缩放)。A同学一开始把验证集也加了随机增强,导致每个epoch验证准确率忽高忽低,搞了半天才发现是验证集引入了随机性。这个细节在写报告时也要特别说明,能体现你对数据管线的理解。
2.3 数据增强策略
数据增强是深度学习分类任务中对抗过拟合最有效的手段之一。鞋类这个场景很适合做随机旋转和水平翻转,因为鞋子本身没有严格的朝向属性。我设计了一套增强管线:RandomResizedCrop到224x224、概率0.5水平翻转、随机旋转±15度、颜色抖动(亮度、对比度、饱和度各0.2)。这套配置在我的实验中大约能把验证准确率提升3到5个百分点。
但增强不是越狠越好。旋转角度过大(比如±45度)会产生很多畸变的鞋子,反而让模型学坏。A同学把旋转调大后,准确率不升反降。对于细粒度分类,颜色抖动也要克制,因为颜色本身是重要判别特征,比如红底鞋、小白鞋,大幅度改变色相会破坏特征一致性。比较稳妥的做法是先用较小幅度的增强,看验证集反馈再逐步加大,别一上来就堆满所有增强。
3. 模型构建与训练参数调优
3.1 从零训练还是迁移学习
鞋类分类项目最常被问的问题:模型是应该自己写一个还是用别人的?如果手里只有几千到两万张图片,我的建议很明确:直接迁移学习。加载ImageNet上预训练好的ResNet18或者ResNet34,把最后全连接层替换成本地类别数量,然后微调。这样能大幅缩短训练时间、提高准确率,代码量也小,非常适合课程设计周期。
有人担心"迁移学习是不是太取巧了?"实际上,迁移学习是深度学习标准的工程实践。只要你在报告里写清楚为什么迁移、哪些层冻结、哪些层微调、迁移带来的收益是什么,老师不会觉得这是抄近路。真正容易翻车的情况是:自己搭了个两层卷积网络,跑了四十个epoch准确率只有60%,还讲不清楚网络结构设计的依据。
如果执意从零训练,也不是不行,但建议至少使用残差结构的网络,并加较强的正则化。从零训练的好处是你可以自定义网络形态,更自由地展示设计思考,但代价是训练时间长、超参数敏感、结果方差大。以我的经验,课程设计时间有限,迁移学习是更稳妥的路线。
3.2 网络结构设计(含常用结构对比)
课程设计层面,常用结构基本是VGG、ResNet、MobileNet、EfficientNet。为了直观对比,我整理了一个表:
| 网络 | 参数量 | 单张推理耗时(约) | 适合场景 |
|---|---|---|---|
| VGG16 | 1.38亿 | 高 | 基础教学,感受过拟合 |
| ResNet34 | 2170万 | 中 | 综合性价比高 |
| MobileNetV2 | 350万 | 低 | 资源受限,部署演示 |
| EfficientNetB0 | 530万 | 低 | 精度和速度均衡 |
对于一个课程设计,我建议先用ResNet34打底。它的残差结构能有效缓解梯度消失,代码实现简单,预训练权重丰富。如果后面要做网页演示,可以再换成MobileNetV2,模型体积小很多,CPU推理速度更快。
这里有个重要观念:网络越深不一定越好。ResNet50比ResNet34深,但在万级数据量上,两者差距极小,而ResNet50训练更慢、显存需求更高。A同学最初直接选了ResNet50,连续两次显存溢出,换回ResNet34后反而更快更稳。模型选择必须匹配数据规模,这是工程经验和堆模型粉丝之间的本质区别。
3.3 训练参数配置与调参心得
训练参数的常见配置:优化器用SGD,学习率0.001,动量0.9,权重衰减5e-4,批次大小32,训练轮数30到50。也可以使用AdamW,学习率降到0.0001。两种我都试过,SGD配合学习率调整策略时在迁移学习场景下效果更稳,AdamW收敛快但最终精度略低。如果追求稳定,选SGD;如果时间紧,AdamW也可以。
学习率是最敏感的超参数。推荐使用余弦退火(CosineAnnealingLR)或者ReduceLROnPlateau。前者在训练后期平滑降学习率,效果稳定;后者根据验证集指标自动衰减,实操更省心。我在本项目中使用ReduceLROnPlateau,模式设为min,监控验证集损失,patience设为5,factor设为0.1。意思是验证损失连续5个epoch不下降时,学习率乘0.1。后期训练曲线很平滑,省了我不少盯着看的精力。
批次大小的选择受显存限制。如果显存不够,可以降到16甚至8。但注意批次越小,梯度噪声越大,训练稳定性和最终精度都可能受影响。A同学在8G显存的笔记本上跑ResNet34和224x224输入,批次32正好,再大就报OOM。如果显吃紧,优先减批次,也别轻易缩输入尺寸,精度损失会更明显。
4. 实验评估与结果分析
4.1 评估指标怎么看
分类任务最直接的指标是准确率,但光看准确率不够。鞋类十分类任务里,如果某一类样本特别多,模型把所有样本都预测成那一类,准确率也可能有三四十,但你根本不知道每个类别的真实情况。除了总体准确率,还需要看每个类别的精度(Precision)、召回率(Recall)和F1值。这些指标能告诉你每一类的表现如何,哪些类容易被混淆。
举个例子,凉鞋和拖鞋容易混淆。模型很可能把拖鞋预测成凉鞋,导致凉鞋的召回率偏低,同时拖鞋的精度偏低。如果只看总体准确率,这种局部问题会被掩盖。所以在实验报告里,一定要画混淆矩阵,从中找出错得最多的两个类别,再分析成因。这个分析过程是课程设计报告的加分项,比单纯写"准确率93%"有说服力得多。
4.2 混淆矩阵与错误分析
A同学训练后的模型总体准确率约93%。单看这个数字还行,但混淆矩阵暴露了不少细节:高跟鞋被误判为靴子的比例偏高,原因是部分靴子图片拍摄角度低,鞋帮显得很高,和高跟鞋侧影相似;雨靴和帆布鞋也有混淆,因为数据集里很多雨靴颜色鲜艳,轮廓和帆布鞋接近。这些错误模式如果不看混淆矩阵,永远发现不了。
针对错误模式,可以做的改进有三个方向。第一,增加易混类别图片的采样权重,让模型在训练时多看这些样本。第二,增强中增加更多随机裁剪,迫使模型关注鞋帮高度、鞋底厚度等局部细节,而不是整体轮廓。第三,如果形态特征难以区分,可以考虑引入颜色纹理辅助特征,但这就超出课程设计范围了。总体来说,分析错误比盲目调参更有价值,也是真正体现工程能力的地方。
4.3 模型对比实验
为了说明方案选择的合理性,我建议做一组简单对比实验:从零训练的小网络、迁移学习的ResNet34、迁移学习的MobileNetV2。A同学跑了三个模型,结果如下:
| 模型 | 训练轮数 | 验证准确率 | 推理速度(CPU) |
|---|---|---|---|
| 2层卷积小网络(从零) | 40 | 76% | 快 |
| ResNet34(迁移) | 30 | 93% | 中 |
| MobileNetV2(迁移) | 30 | 91% | 快 |
这个表格放在报告里很有说服力。它至少说明两点:一是迁移学习在中小规模数据集上优势巨大;二是轻量模型在精度损失很小的情况下可以获得明显的速度收益。课程设计报告中建议包含类似的实验对比,这比大段文字描述更能体现工作量和技术判断力。
5. 实操中的坑与解决实录
5.1 环境配置踩坑
PyTorch环境搭建是很多同学的第一道坎。建议直接用Anaconda创建虚拟环境,Python版本3.9或3.10,然后安装对应平台的PyTorch稳定版。如果电脑有NVIDIA显卡,要先安装对应的CUDA驱动和配套依赖,再用带cu118或cu121后缀的安装命令。没有显卡或者显存不够,就装CPU版,虽然训练慢,但跑通流程是够用的。
A同学的电脑是Windows系统,按官方命令装完PyTorch后,跑第一个epoch时弹出"xx.dll not found"。排查后发现少了Microsoft Visual C++ Redistributable。这个问题很常见,搜索结果第一条就能解决。另外建议不要直接用conda默认源下载PyTorch,容易下载中断或版本不对,换成国内镜像源会快很多。
5.2 显存不足怎么办
显存不足(OOM)是训练中最常见的报错。遇到OOM时,第一反应是减小批次大小,其次是把输入尺寸从224降到160,但后者会损失精度。还有一种替代方案是使用梯度累积:把一个大批次拆成几个小批次分别前向计算,积累多个梯度后再做一次参数更新。这样可以在不大幅增加显存的前提下模拟大批次效果。
A同学遇到的OOM还有一层隐藏原因:验证阶段忘记关闭梯度计算。代码里model.eval()只切换层状态,并不会关闭梯度追踪。如果验证循环里没有使用with torch.no_grad(),GPU会额外保存中间变量,显存占用几乎翻倍。加一行包裹推理过程,问题就解决了,还顺带提升了验证速度。
5.3 过拟合与类别不均衡
课程设计的数据规模下,过拟合几乎必然出现。典型表现是训练集准确率接近100%,验证集准确率长时间不涨。解决过拟合的优先级顺序是:先增强数据,再加正则化,最后降低模型复杂度。对于迁移学习,过拟合通常不严重,因为预训练特征足够泛化。A同学从零训练小网络时,第三个epoch训练集准确率就冲到99%,验证集只有70%,典型的过拟合。换成迁移学习并加强增强后,验证集才稳步上升。
类别不均衡是另一个问题。数据结构中可能皮鞋和拖鞋样本数量是运动鞋的两倍,模型会倾向预测样本多的类。解决办法有三种:第一,不平衡采样,对少样本类重复采样或对多样本类降采样;第二,在损失函数中设置类别权重,PyTorch的CrossEntropyLoss可以直接传weight参数;第三,使用Focal Loss,实现稍复杂,课程设计不推荐。我采用的方法是给CrossEntropyLoss传权重,权重与样本数的倒数成正比,操作简单有效。
5.4 模型保存与推理
训练完后保存模型也有讲究。PyTorch可以保存整个模型,也可以只保存state_dict。我建议保存state_dict,因为只含参数,跨环境加载更稳定。保存格式用.pt或.pth都行。加载模型时先构建相同结构,再调用load_state_dict。注意如果训练时用了DataParallel,state_dict里参数名会带"module."前缀,加载时要处理一下。
如果要做演示系统,还得把模型放到CPU上,并保证输入的图片和训练时走完全相同的预处理流程,包括解码、缩放、中心裁剪、归一化。A同学用Flask写了个简单的网页服务:上传图片,后端加载模型,输出类别和置信度。这个演示环节虽然简单,但答辩时非常加分,能直观展示"深度学习模型做了什么事"。
6. 关键代码实现(可直接抄作业)
6.1 数据加载与增强
数据加载建议用PyTorch的Dataset和DataLoader。如果图片按类别放在不同文件夹里,torchvision.datasets.ImageFolder是最直接的方案。配合transforms可以在线实现数据增强。
from torchvision import transforms, datasets from torch.utils.data import DataLoader train_transforms = transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), 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_transforms = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_dataset = datasets.ImageFolder(root='data/train', transform=train_transforms) val_dataset = datasets.ImageFolder(root='data/val', transform=val_transforms) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4)如果你的数据集不是按类别分文件夹的,需要先写一个整理脚本把图片移动到对应子目录。ImageFolder会自动根据子文件夹名生成类别索引,并且按字母顺序排序。测试时要注意类别顺序,不要搞混。
6.2 模型定义
加载预训练ResNet34并替换全连接层:
import torch.nn as nn from torchvision import models def get_model(num_classes=10): model = models.resnet34(weights=models.ResNet34_Weights.IMAGENET1K_V1) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) return model如果PyTorch版本较旧,没有weights参数,可以用pretrained=True,新版会提示改用weights方式,但依然能跑。如果想只微调最后几层,可以把前面所有参数的requires_grad设为False。但在我的实验里,全量微调在万级数据上效果更好,区别是训练时间稍长。全量微调时需要把学习率调小(比如0.001),避免预训练权重被大幅破坏。
6.3 训练与评估脚本
下面是一个简化但完整的训练循环,包括验证、学习率调整和最佳模型保存。这段代码可以直接复用,但要根据自己的数据路径和显存情况调整。
import torch import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import ReduceLROnPlateau model = get_model(num_classes=10) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=5e-4) scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=5) num_epochs = 30 best_acc = 0.0 for epoch in range(num_epochs): model.train() running_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) model.eval() val_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) val_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() val_loss /= len(val_dataset) val_acc = correct / total scheduler.step(val_loss) print(f'Epoch {epoch+1}/{num_epochs}, Loss: {running_loss/len(train_dataset):.4f}, Val Loss: {val_loss:.4f}, Acc: {val_acc:.4f}') if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pth')代码里有一个需要特别提醒的地方:ReduceLROnPlateau的scheduler.step()要在每个epoch计算完验证损失后调用,而且传入的是验证损失,不是训练损失。很多人在这里写错,导致学习率不下降。另外num_workers在Windows下有时会报错,建议设为0。训练结束时best_model.pth就是验证集上表现最好的模型权重,后续直接加载推理即可。
最后说一点个人体会:做这类课程设计项目,最稳的路径就是把数据和评估做扎实。A同学从拿到题目到完成演示,前后两周,每天两三个小时,大部分时间花在清洗数据和跑对比实验上,真正改代码的时间反而不多。如果你也在做类似题目,建议先花一天把数据集整理干净,再跑一个小规模基线实验,然后再逐步换模型调参。把这一步走稳了,后面的一切都会顺很多。