资讯中心

从Fashion-MNIST入门:PyTorch实战服装图像分类与CNN调优全流程

📅 2026/8/28 13:53:56
从Fashion-MNIST入门:PyTorch实战服装图像分类与CNN调优全流程
简介卷积神经网络CNN是计算机视觉领域的核心模型其通过卷积层、池化层等结构自动提取图像特征。在图像分类任务中CNN能有效学习图像的层次化表示从边缘、纹理到复杂图案。这一技术价值在于将传统手工特征工程转化为端到端的自动化学习极大提升了模型在复杂场景下的泛化能力。其应用场景广泛涵盖工业质检、医疗影像分析、自动驾驶感知等。本文以经典的Fashion-MNIST服装数据集为例结合PyTorch框架详细解析从数据预处理、模型构建、训练调优到评估部署的完整流程并深入探讨了学习率调度与Dropout等关键调优策略帮助读者掌握图像分类项目的工程实践方法。1. 项目缘起从课程设计到实战应用的跨越最近在整理硬盘时翻出了一个几年前做的课程设计项目——“基于深度学习框架的服装图像智能分类与识别系统”。当时为了完成这个作业从零开始啃论文、调代码、写报告折腾了小半个月。现在回头看这个项目虽然作为课程设计略显“标准”但它麻雀虽小五脏俱全完整地走通了从数据处理、模型构建、训练调优到评估部署的整个深度学习应用链路。对于刚入门深度学习和计算机视觉的朋友来说这绝对是一个绝佳的练手项目远比单纯跑通一个MNIST手写数字识别要来得有挑战性和成就感。这个项目的核心是使用Python和主流的深度学习框架如TensorFlow或PyTorch对著名的Fashion-MNIST数据集进行智能分类。Fashion-MNIST包含了10个类别的灰度服装图像如T恤、裤子、套头衫等每张图片都是28x28像素。你可能会问这不就是个换皮的MNIST吗确实从数据格式上看很像但服装图像的分类难度和实际意义远大于手写数字。数字的笔画结构相对固定而服装的款式、褶皱、拍摄角度千变万化这更能考验模型提取抽象特征和泛化的能力。通过这个项目你不仅能学会如何搭建一个卷积神经网络CNN更能深入理解图像分类任务中的关键环节数据预处理、模型结构设计、损失函数选择、优化器调参以及防止过拟合的策略。我打算在这篇分享里不仅带你复现这个项目更会结合我后来在工业界踩过的坑补充许多课程设计报告里不会写的“私货”。比如如何根据你的显卡显存合理设置批次大小Batch Size学习率Learning Rate下降策略到底该怎么选当准确率卡在某个瓶颈上不去时应该从哪几个维度进行“玄学调参”我们会用PyTorch框架来实现因为它动态图的设计对调试更加友好代码也更Pythonic。放心即使你之前只用过TensorFlow跟着步骤走也完全没问题。我们目标是让你拿到这份代码和报告后不仅能运行出结果更能真正理解每一行代码背后的意图并具备举一反三将其应用到其他图像分类任务比如你自己收集的猫狗图片、花卉图片的能力。2. 环境搭建与数据初探万事开头“细”在撸起袖子写代码之前把环境理顺、把数据摸透能避免后面一大堆莫名其妙的错误。很多人急着跑模型结果卡在环境依赖或数据读取上非常打击积极性。2.1 构建一个干净的Python深度学习环境我强烈建议使用conda或venv创建独立的虚拟环境。这能确保项目依赖不会污染你的系统环境也方便你管理不同项目可能需要的不同版本的库。# 使用conda创建环境假设你安装了Anaconda或Miniconda conda create -n fashion-mnist-classify python3.8 conda activate fashion-mnist-classify # 安装核心依赖 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 以CUDA 11.8为例请根据你的显卡驱动选择对应版本 pip install numpy pandas matplotlib seaborn jupyter notebook pip install scikit-learn # 用于评估指标和可能的数据处理注意PyTorch的安装命令需要去官网生成。上面只是一个示例。如果你的电脑没有NVIDIA显卡或者不想用GPU可以安装CPU版本的PyTorch命令会更简单。但有一点要明确哪怕只有一张消费级的GPU比如GTX 1060 6GB训练速度也会比CPU快上一个数量级。对于Fashion-MNIST这种小数据集CPU可能还能忍受但养成使用GPU的习惯对后续做更大的项目至关重要。验证安装是否成功import torch print(torch.__version__) print(torch.cuda.is_available()) # 输出True则表示GPU可用2.2 深入理解Fashion-MNIST数据集Fashion-MNIST数据集通常可以通过torchvision.datasets模块直接下载和加载。但在写代码前我们得先搞清楚我们在处理什么。from torchvision import datasets, transforms # 定义数据转换将图像数据转换为Tensor并做归一化加快收敛 transform transforms.Compose([ transforms.ToTensor(), # 将PIL Image或numpy.ndarray转换为Tensor并自动缩放到[0,1] transforms.Normalize((0.5,), (0.5,)) # 将[0,1]归一化到[-1,1]。对于单通道灰度图均值和标准差都是0.5 ]) # 下载并加载训练集和测试集 train_dataset datasets.FashionMNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.FashionMNIST(root./data, trainFalse, downloadTrue, transformtransform)加载后我习惯先对数据做一番“体检”print(f训练集样本数: {len(train_dataset)}) print(f测试集样本数: {len(test_dataset)}) print(f图像形状: {train_dataset[0][0].shape}) # 输出应为 torch.Size([1, 28, 28]) [通道高宽] print(f类别数: {len(train_dataset.classes)}) print(f类别标签: {train_dataset.classes})输出会告诉你我们有6万张训练图1万张测试图图像是单通道28x28共10个类别。类别标签是[T-shirt/top, Trouser, Pullover, Dress, Coat, Sandal, Shirt, Sneaker, Bag, Ankle boot]一个关键的实操心得一定要可视化看看用几行代码把训练集的前若干张图片和对应标签画出来。import matplotlib.pyplot as plt figure plt.figure(figsize(8, 8)) cols, rows 5, 5 for i in range(1, cols * rows 1): sample_idx torch.randint(len(train_dataset), size(1,)).item() img, label train_dataset[sample_idx] figure.add_subplot(rows, cols, i) plt.title(train_dataset.classes[label]) plt.axis(off) # 因为之前做了Normalize显示前需要反归一化 plt.imshow(img.squeeze(), cmapgray) # img.squeeze()去掉通道维度从[1,28,28]变成[28,28] plt.show()这个步骤至关重要。它能帮你确认数据加载是否正确更直观地感受分类任务的难度。比如你会发现“T-shirt/top”、“Pullover”、“Shirt”、“Coat”这几类有时候连人眼都容易混淆这就能提前让你对模型的性能有一个合理的预期——别指望能达到99.9%的准确率。同时检查图像是否清晰、有无损坏对于Fashion-MNIST一般没有但如果是你自己的数据集这一步就是必须的。2.3 构建数据加载器DataLoader数据集Dataset是数据的容器而数据加载器DataLoader负责在训练过程中按批次batch抽取数据并可以进行打乱shuffle、并行加载等操作。from torch.utils.data import DataLoader batch_size 64 # 一个关键超参数 train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) test_loader DataLoader(test_dataset, batch_sizebatch_size, shuffleFalse) # 测试集不需要打乱为什么batch_size重要它是一次迭代iteration输入模型的样本数量。设置太小如16梯度更新方向噪声大训练不稳定且慢设置太大如1024可能超出GPU显存且可能陷入尖锐的极小值点泛化能力变差。对于Fashion-MNIST和一般消费级GPU如8GB显存64或128是一个不错的起点。你可以通过nvidia-smi命令监控GPU显存使用情况来调整。3. 模型构建设计一个高效的卷积神经网络CNN现在进入核心环节——构建模型。对于图像分类任务卷积神经网络CNN是绝对的主流。我们不会直接用现成的ResNet、VGG而是从零搭建一个中等复杂度的CNN这有助于理解每一层的作用。3.1 CNN基础结构与我们的设计思路一个典型的CNN由卷积层Conv、激活函数如ReLU、池化层Pooling和全连接层Linear交替组成。卷积层负责提取局部特征如边缘、纹理池化层负责降低空间维度减少计算量并增加感受野全连接层负责综合所有特征做出分类决策。针对28x28的小图我们设计一个包含两个“卷积-池化”块后接全连接层的网络第一个卷积块用较小的卷积核3x3捕捉基础特征。输入通道1灰度输出通道32。经过ReLU激活后进行2x2最大池化将特征图尺寸从28x28降至14x14。第二个卷积块加深网络提取更复杂的特征。输入通道32输出通道64。同样经过ReLU和2x2最大池化特征图尺寸从14x14降至7x7。展平与全连接层将7x7x64的三维特征图展平成一维向量77643136个特征。然后经过一个或两个全连接层最终映射到10个类别的输出。3.2 使用PyTorch实现模型类在PyTorch中我们通过继承nn.Module类来定义自己的模型。import torch.nn as nn import torch.nn.functional as F class FashionMNISTCNN(nn.Module): def __init__(self): super(FashionMNISTCNN, self).__init__() # 第一个卷积块 self.conv1 nn.Conv2d(in_channels1, out_channels32, kernel_size3, padding1) # padding1保证输出尺寸不变 self.pool1 nn.MaxPool2d(kernel_size2, stride2) # 输出尺寸减半 # 第二个卷积块 self.conv2 nn.Conv2d(in_channels32, out_channels64, kernel_size3, padding1) self.pool2 nn.MaxPool2d(kernel_size2, stride2) # 全连接层 # 经过两次池化28x28 - 14x14 - 7x7 通道数为64 self.fc1 nn.Linear(in_features64 * 7 * 7, out_features128) self.fc2 nn.Linear(in_features128, out_features10) # 输出10个类别 # Dropout层用于防止过拟合 self.dropout nn.Dropout(p0.25) def forward(self, x): # x的形状: [batch_size, 1, 28, 28] x self.pool1(F.relu(self.conv1(x))) # - [batch_size, 32, 14, 14] x self.pool2(F.relu(self.conv2(x))) # - [batch_size, 64, 7, 7] # 展平 x x.view(-1, 64 * 7 * 7) # - [batch_size, 3136] # 全连接层 x F.relu(self.fc1(x)) x self.dropout(x) # 只在训练时起作用 x self.fc2(x) # - [batch_size, 10] # 注意这里没有用Softmax因为后面的损失函数CrossEntropyLoss自带Softmax return x # 实例化模型并移动到GPU如果可用 device torch.device(cuda if torch.cuda.is_available() else cpu) model FashionMNISTCNN().to(device) print(model)几个关键设计点的解释卷积核与Padding使用3x3小卷积核是VGG网络推广的最佳实践在减少参数量的同时能通过堆叠获得与大卷积核相同的感受野。padding1是为了让卷积操作不改变特征图的高和宽对于3x3核步长为1时padding1可保持尺寸。激活函数ReLURectified Linear Unit是目前最常用的激活函数它计算简单且能有效缓解梯度消失问题。Dropout在全连接层后加入Dropout是一种简单有效的正则化方法。它在训练时随机“丢弃”置零一部分神经元的输出强迫网络不过度依赖某些特定的神经元从而增强泛化能力。参数p0.25表示每个神经元有25%的概率被丢弃。注意在模型验证和测试时Dropout会自动关闭。输出层我们没有显式使用Softmax是因为PyTorch的nn.CrossEntropyLoss损失函数内部已经包含了Softmax操作。所以模型直接输出10个类别的“得分”logits即可。4. 训练流程的完整实现与核心超参数剖析模型定义好了接下来就是训练循环。这是深度学习的“发动机”里面有很多细节和技巧。4.1 定义损失函数与优化器import torch.optim as optim criterion nn.CrossEntropyLoss() # 交叉熵损失适用于多分类 optimizer optim.Adam(model.parameters(), lr0.001) # Adam优化器学习率设为0.001损失函数CriterionCrossEntropyLoss是分类任务的标准选择。它衡量模型预测的概率分布与真实标签的one-hot分布之间的差异。优化器Optimizer负责根据损失函数的梯度更新模型参数。Adam优化器结合了动量Momentum和自适应学习率的优点在大多数情况下表现稳定是默认的首选。SGD随机梯度下降配合动量momentum0.9和学习率衰减weight_decay有时能获得更好的最终精度但需要更精细的调参。对于初学者先用Adam。学习率Learning Rate, lr是训练中最重要的超参数之一。它决定了参数更新的步长。太大可能导致损失震荡甚至发散如下图左太小则收敛缓慢如下图右。0.001是Adam一个比较通用的初始值。我们后面会讨论如何动态调整它。4.2 编写训练与验证循环一个完整的训练周期Epoch包含在训练集上的参数更新和在验证集/测试集上的性能评估。def train(model, device, train_loader, optimizer, criterion, epoch): model.train() # 切换到训练模式启用Dropout等 train_loss 0 correct 0 total 0 for batch_idx, (data, target) in enumerate(train_loader): data, target data.to(device), target.to(device) optimizer.zero_grad() # 关键清空上一轮的梯度 output model(data) # 前向传播 loss criterion(output, target) # 计算损失 loss.backward() # 反向传播计算梯度 optimizer.step() # 优化器更新参数 train_loss loss.item() _, predicted output.max(1) # 获取预测的类别 total target.size(0) correct predicted.eq(target).sum().item() # 可选每处理一定批次打印一次进度 if batch_idx % 100 0: print(fTrain Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} f({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}) avg_loss train_loss / len(train_loader) accuracy 100. * correct / total print(f\n训练集平均损失: {avg_loss:.4f}, 准确率: {accuracy:.2f}%) return avg_loss, accuracy def test(model, device, test_loader, criterion): model.eval() # 切换到评估模式关闭Dropout等 test_loss 0 correct 0 total 0 with torch.no_grad(): # 关键不计算梯度节省内存和计算 for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) test_loss criterion(output, target).item() # 累加批次损失 _, predicted output.max(1) total target.size(0) correct predicted.eq(target).sum().item() avg_loss test_loss / len(test_loader) accuracy 100. * correct / total print(f测试集平均损失: {avg_loss:.4f}, 准确率: {accuracy:.2f}%\n) return avg_loss, accuracy代码中的关键点与避坑指南optimizer.zero_grad()这是新手最容易忘记的一步。PyTorch的梯度是累加的如果不每轮清空梯度会不断累积导致更新方向错误。务必在loss.backward()之前调用。model.train()和model.eval()这两个模式切换非常重要。train()会启用Dropout和BatchNorm的训练行为如用当前批次的统计量进行归一化。eval()会关闭Dropout并使用BatchNorm的运行均值/方差进行归一化保证评估结果的一致性。with torch.no_grad()在测试和验证时使用这个上下文管理器可以禁止自动求导大幅减少内存消耗并加速计算。损失和准确率的计算损失是每个批次计算并累加的最后除以批次数量得到平均损失。准确率是累计正确样本数除以总样本数。注意loss.item()是将单个值的张量转换为Python数字。4.3 执行训练并监控过程现在我们可以运行多个Epoch来训练模型了。num_epochs 10 train_losses, train_accs [], [] test_losses, test_accs [], [] for epoch in range(1, num_epochs 1): print(f\n--- Epoch {epoch} ---) train_loss, train_acc train(model, device, train_loader, optimizer, criterion, epoch) test_loss, test_acc test(model, device, test_loader, criterion) train_losses.append(train_loss) train_accs.append(train_acc) test_losses.append(test_loss) test_accs.append(test_acc)运行这段代码你会看到控制台打印出每个Epoch的训练和测试损失与准确率。通常训练损失会稳步下降训练准确率会上升。测试准确率也会随之上升但最终会趋于平稳甚至后期因过拟合而略有下降。5. 模型评估、调优与可视化分析训练完成后我们得到的准确率可能还不错比如90%以上但工作远未结束。我们需要深入分析模型的表现找出薄弱环节并尝试优化。5.1 绘制学习曲线诊断模型状态学习曲线训练/测试损失和准确率随Epoch的变化是诊断模型的最重要工具。import matplotlib.pyplot as plt epochs_range range(1, num_epochs 1) fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 4)) # 绘制损失曲线 ax1.plot(epochs_range, train_losses, labelTrain Loss) ax1.plot(epochs_range, test_losses, labelTest Loss) ax1.set_xlabel(Epochs) ax1.set_ylabel(Loss) ax1.set_title(Training and Test Loss) ax1.legend() ax1.grid(True) # 绘制准确率曲线 ax2.plot(epochs_range, train_accs, labelTrain Accuracy) ax2.plot(epochs_range, test_accs, labelTest Accuracy) ax2.set_xlabel(Epochs) ax2.set_ylabel(Accuracy (%)) ax2.set_title(Training and Test Accuracy) ax2.legend() ax2.grid(True) plt.tight_layout() plt.show()通过曲线我们可以判断欠拟合训练和测试的损失/准确率都很差且训练损失下降缓慢。解决方案增加模型复杂度、训练更长时间、使用更好的特征等。过拟合训练损失持续下降、准确率很高但测试损失在某个点后开始上升、准确率停滞甚至下降即两条曲线出现“剪刀差”。这正是我们最常遇到的问题。5.2 针对过拟合的调优策略如果发现明显的过拟合可以尝试以下方法按常用顺序增加数据对于Fashion-MNIST我们可以使用数据增强Data Augmentation。在训练时对图像进行随机变换如旋转、平移、翻转等可以极大地增加数据的多样性是缓解过拟合最有效的方法之一。修改transformtrain_transform transforms.Compose([ transforms.RandomHorizontalFlip(), # 随机水平翻转对服装有效 transforms.RandomRotation(10), # 随机旋转±10度 transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) # 测试集不要做数据增强 test_transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ])调整Dropout率尝试将全连接层前的Dropout(p0.25)提高到0.3或0.5。也可以在卷积层后加入Dropout2d。添加L2正则化权重衰减在优化器中设置weight_decay参数惩罚大的权重值。optimizer optim.Adam(model.parameters(), lr0.001, weight_decay1e-4) # 较小的权重衰减简化模型如果模型过于复杂例如卷积通道数过多、全连接层神经元过多可以适当减少参数。使用早停Early Stopping监控测试集损失当其在连续多个Epoch不再下降时提前终止训练避免在过拟合区域继续训练。5.3 分析混淆矩阵定位模型弱点准确率是一个整体指标但我们需要知道模型在哪些类别上容易混淆。混淆矩阵Confusion Matrix能清晰展示这一点。from sklearn.metrics import confusion_matrix import seaborn as sns import numpy as np model.eval() all_preds [] all_labels [] with torch.no_grad(): for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) _, preds output.max(1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(target.cpu().numpy()) cm confusion_matrix(all_labels, all_preds) plt.figure(figsize(10, 8)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelstrain_dataset.classes, yticklabelstrain_dataset.classes) plt.xlabel(Predicted Label) plt.ylabel(True Label) plt.title(Confusion Matrix) plt.show()从混淆矩阵中你可以清晰地看到模型预测错误的主要集中在哪些类别之间。例如很可能“Shirt”被误判为“T-shirt/top”、“Pullover”或“Coat”。这非常符合直觉因为这些类别本身视觉上就相似。这告诉我们如果想进一步提升性能可能需要更复杂的模型如ResNet来提取更细微的特征或者引入注意力机制。5.4 学习率调度与更高级的优化技巧固定学习率可能不是最优的。我们可以使用学习率调度器Scheduler在训练过程中动态降低学习率。# 使用StepLR每过一定epoch将学习率乘以一个因子gamma scheduler optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.1) # 在每一个epoch的训练循环结束后调用 # for epoch in range(...): # train(...) # test(...) # scheduler.step() # 更新学习率StepLR是一种简单的策略。还有ReduceLROnPlateau当指标停止改善时降低学习率、CosineAnnealingLR余弦退火等更复杂的调度器。对于这个项目StepLR通常就够用了。6. 模型保存、加载与推理演示训练出一个满意的模型后我们需要保存它以便后续使用或部署。6.1 保存与加载模型PyTorch推荐保存模型的state_dict状态字典即模型参数而不是整个模型对象这样更灵活且与代码结构解耦。# 保存模型 torch.save(model.state_dict(), fashion_mnist_cnn.pth) print(模型已保存为 fashion_mnist_cnn.pth) # 加载模型在新脚本中 # 首先需要重新实例化模型结构 loaded_model FashionMNISTCNN().to(device) # 然后加载参数 loaded_model.load_state_dict(torch.load(fashion_mnist_cnn.pth, map_locationdevice)) loaded_model.eval() # 别忘了切换到评估模式 print(模型加载成功)6.2 编写一个简单的推理函数我们可以写一个函数输入一张图片或一批图片输出预测的类别和置信度。def predict_image(image_tensor, model, device, class_names): 对单张图片进行预测。 image_tensor: 形状为 [1, 1, 28, 28] 的Tensor且已经过相同的transform处理。 model.eval() with torch.no_grad(): image_tensor image_tensor.to(device) output model(image_tensor) # 获取概率应用Softmax probabilities F.softmax(output, dim1) # 获取最高概率的类别和分数 conf, predicted_class torch.max(probabilities, 1) return class_names[predicted_class.item()], conf.item() # 从测试集中随机取一张图片测试 sample_img, sample_label test_dataset[0] sample_img sample_img.unsqueeze(0) # 增加批次维度变成[1,1,28,28] pred_class, confidence predict_image(sample_img, loaded_model, device, train_dataset.classes) true_class train_dataset.classes[sample_label] print(f真实类别: {true_class}) print(f预测类别: {pred_class}, 置信度: {confidence:.2%})6.3 可视化预测结果最后我们可以批量可视化一些测试图片并标注出模型的预测结果正确用绿色错误用红色这能给我们更直观的感受。def plot_predictions(model, device, test_dataset, num_images25): model.eval() indices torch.randint(len(test_dataset), size(num_images,)) fig, axes plt.subplots(5, 5, figsize(12, 12)) axes axes.flatten() for idx, ax in zip(indices, axes): img, label test_dataset[idx] img_tensor img.unsqueeze(0).to(device) with torch.no_grad(): output model(img_tensor) _, pred output.max(1) pred_label train_dataset.classes[pred.item()] true_label train_dataset.classes[label] ax.imshow(img.squeeze(), cmapgray) ax.axis(off) color green if pred.item() label else red ax.set_title(fTrue: {true_label}\nPred: {pred_label}, colorcolor, fontsize9) plt.tight_layout() plt.show() plot_predictions(loaded_model, device, test_dataset)通过这个可视化你能一目了然地看到模型在哪些图片上犯了错这些错误是否合理比如混淆的类别确实很像从而对模型的性能边界有更深刻的认识。这个项目虽然基于经典的Fashion-MNIST但它所涵盖的流程——数据准备、模型构建、训练、评估、调优、保存与推理——是任何一个深度学习图像分类项目的通用框架。当你掌握了这个框架后完全可以更换数据集比如猫狗分类、花卉分类调整模型结构去解决更多实际的问题。希望这份详细的拆解和补充的实操经验能帮你不仅完成一个课程设计更能真正踏入深度学习应用开发的大门。本文还有配套的精品资源点击获取