
简介基于AlexNet的FashionMNIST图像分类项目面向深度学习入门的开发者和学生使用PyTorch框架实现解决服装图像识别任务中从数据准备到网络训练的完整流程问题。资源共26个文件涵盖FashionMNIST数据集、Python训练与测试脚本、最佳模型权重、项目配置文件和说明文档其中数据文件以idx格式存储模型权重为pth格式压缩包约260.82MB。已有102人学习适合在图像分类场景中快速复现AlexNet。项目的代码按数据预处理、模型构建、训练与测试进行组织明确拆分了训练集/验证集和测试集处理流程AlexNet网络包含特征提取层、自适应平均池化层与分类器层并使用Adam优化器和交叉熵损失函数读者还可结合训练好的权重直接评估模型效果。整体目录清晰从数据集到模型权重一一对应可帮助理解经典卷积网络在FashionMNIST上的落地实践。1. FashionMNIST只有28x28AlexNet原版结构直接抄会出问题FashionMNIST的样本是28×28单通道灰度图AlexNet原论文却把输入设计成227×227的RGB三通道。直接拿官方模型结构套到这个数据集上第一层卷积11×11、stride4就会把特征图压到几乎没有空间信息后续全连接层只能拿到高度聚合的残渣训练精度会长期停留在0.1附近相当于每次都在随机猜类别。这个项目表面上是「图像分类」入门真正的门槛在于拆掉AlexNet对输入尺寸和通道数的假设再在没有预训练权重的前提下让它稳定收敛。下面按数据加载、模型改造、训练参数、收敛检查四条线展开全程用PyTorch实现CPU也能完整跑通只是每轮要慢一些。适合能看懂卷积层、池化层但第一次把经典模型迁移到其他数据集上的人。2. FashionMNIST数据集加载与28x28像素结构从torchvision到DataLoader2.1 用FashionMNIST接口一次加载训练集和测试集PyTorch生态里加载FashionMNIST最直接的方式是走torchvision.datasets它内部把下载、解压、标签映射都处理好了代码不需要手动读二进制文件。import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.2860,), (0.3530,)) ]) train_dataset datasets.FashionMNIST( root./data, trainTrue, downloadTrue, transformtransform ) test_dataset datasets.FashionMNIST( root./data, trainFalse, downloadTrue, transformtransform )root./data表示数据落在当前目录的data子目录下训练集和测试集会分别存好二次运行不再重复下载。trainTrue/False用来区分60,000张训练图与10,000张测试图。FashionMNIST的官方切分是固定的不需要自己按比例拆。downloadTrue表示缺失时自动下载。网络慢时可以把压缩包手动放到data/FashionMNIST/raw目录下再执行一次加载代码它就能跳过下载。transform接收PIL图像并转成张量后面每个batch实际拿到的形状是[B, 1, 28, 28]最后一维是通道数1代表灰度图。这套写法在换CIFAR、SVHN这类torchvision内置数据集时同样成立区别只在datasets.xxx名称和归一化参数上。2.2 归一化均值0.2860、标准差0.3530从哪来为什么不能用ImageNet那组很多人在迁移ImageNet模型时会顺手把Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))抄过来这在单通道FashionMNIST上会出问题通道数对不上而且均值标准差完全不是这个分布算出来的。FashionMNIST官方向导里推荐的预计算统计值就是均值0.2860、标准差0.3530。这两个数是把训练集全部像素除以255后在全体像素上统计得到的全局均值和全局标准差。注意PyTorch的Normalize期望每个通道一个均值一个标准差所以写法是(0.2860,)和(0.3530,)保留长度为1的元组。若忘记做归一化模型仍能训练但收敛明显迟钝。AlexNet没有BatchNorm层准确说是原始结构没有任何归一化层数据分布方差大时靠近输入层的梯度很容易抖动学习率稍大就直接发散。一个自查办法是归一化后打印一个batch的分布均值应接近0标准差接近1。如果看到像素值还在0到1之间说明ToTensor之后没有执行Normalize是管道顺序写反了Compose里必须把Normalize放在ToTensor之后。2.3 28x28输入不缩放模型侧适配比数据侧放大更划算FashionMNIST每张图主体只占画面中央一小块强行插值到227×227约等于凭空捏造像素。放大后边缘区域引入了大量无效插值信息11×11的卷积核在原始28×28上还能捕捉一些局部纹理放大后反而被稀释。更实际的问题是计算量一幅图从28×28变成224×224单通道数据量膨胀64倍CPU训练成本直接翻好几番。常见做法是保持输入28×28不变把尺寸适配放到模型内部。AlexNet只有一个输入分支改造点集中在第一层卷积的stride和最后的池化策略这比在DataLoader里改图像尺寸更干净也更容易复现。后面第3章会专门讲这个适配层怎么加。2.4 DataLoader参数batch、shuffle、num_workers三组配置train_loader DataLoader( train_dataset, batch_size128, shuffleTrue, num_workers2 ) test_loader DataLoader( test_dataset, batch_size256, shuffleFalse, num_workers2 )batch_size训练集用128测试集用256。测试阶段不需要反向传播可以开大一点减少IO次数。shuffle训练集必须打乱否则每个epoch内模型看到的类别顺序固定梯度更新会带有周期性偏置测试集不需要shuffle保持原始顺序即可不影响指标统计。num_workersLinux下用2左右比较稳妥Windows下多进程启动偶发卡死推荐先设成0排查问题确认代码跑通再调大。显存和内存紧张时优先把num_workers降到0而不是降batch size。标签是整数索引0到9不是字符串。想看具体类别名访问train_dataset.classes就能拿到按索引排好的列表。这个映射要在训练开始前确认一遍后面分类头输出维度、混淆矩阵图例、分类报告都依赖它。3. 用PyTorch复刻AlexNet自适应池化解决尺寸冲突分类头改为10类3.1 原版227x227的假设在哪一层断掉AlexNet原文针对ImageNet设计输入被固定为227×227整个网络的计算图都围绕这个尺寸展开。把28×28直接喂进去第一层卷积的尺寸变化是H_out floor((28 2*2 - 11) / 4) 1 6结构里第一层卷积后紧跟核大小为3、步长为2的MaxPool6×6变成2×2。第二层卷积带padding2、核5×52×2输入算完还是2×2再池化一次变成1×1。到这一步第三个卷积块还没有执行特征图已经完全失去空间分辨率。问题在于原版分类器期望的输入是6×6×256的展平向量共9216维而28×28输入走完卷积堆叠只剩1×1×256。强行跑会直接报维度不匹配。工程上不会去改全连接层的维度因为4096→4096→1000这套结构是AlexNet容量和Dropout设计的一部分改小反而丢失原本的表达能力。常用解法是在卷积输出和分类器之间插入AdaptiveAvgPool2d((6, 6))把特征图统一池化到6×6再展平进入分类器。3.2 复刻模型代码保留卷积块接上自适应池化import torch import torch.nn as nn class AlexNetFMNIST(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 64, kernel_size11, stride4, padding2), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), nn.Conv2d(64, 192, kernel_size5, padding2), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), nn.Conv2d(192, 384, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(384, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), ) self.avgpool nn.AdaptiveAvgPool2d((6, 6)) self.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(inplaceTrue), nn.Linear(4096, num_classes), ) def forward(self, x): x self.features(x) x self.avgpool(x) x torch.flatten(x, 1) return self.classifier(x)这里每个参数都有讲究Conv2d(1, 64, kernel_size11, stride4, padding2)的in_channels1是因为FashionMNIST是灰度图不是RGB三通道stride4是AlexNet快速下采样的核心设计虽然对28×28有点激进但配合自适应池化仍然可用。inplaceTrue复用输入张量内存省一点显存在CPU训练时也能降低峰值占用。AdaptiveAvgPool2d((6, 6))的作用不是传统意义上的下采样而是把任意尺寸的输入映射到指定的输出尺寸。即使前面的卷积输出只有1×1它也能通过平均池化得到6×6这比手动计算池化核大小和步长要省事得多。torch.flatten(x, 1)从通道维开始展平第0维batch保留输出形状是[B, 256*6*6]。3.3 各模块输出尺寸核对表模块输出形状batch1输入[1, 1, 28, 28]Conv1 ReLU[1, 64, 6, 6]Pool1[1, 64, 2, 2]Conv2 ReLU[1, 192, 2, 2]Pool2[1, 192, 1, 1]Conv3到Conv5 ReLU[1, 256, 1, 1]MaxPool AdaptiveAvgPool[1, 256, 6, 6]展平[1, 9216]全连接分类器[1, 10]num_classes传入10对应FashionMNIST的10个服饰类别。最后一个全连接层的输出不再接任何激活函数直接作为logits返回。很多初学者在这里被误导多加一层Softmax会在下一章损失函数部分解释原因。3.4 Dropout在训练与评估下的行为差异模型里两处Dropout(0.5)只在训练时随机置零推理时必须关闭。PyTorch是通过model.train()和model.eval()两个模式切换来控制这个行为的不是靠重新实例化模型。用验证集或测试集计算准确率时漏写model.eval()会让Dropout继续生效同一批数据每次预测结果都不同验证集指标会像噪声一样跳来跳去且系统性偏低。提示每个epoch开头先确认模式。训练循环之前写model.train()验证循环之前写model.eval()这是图像分类任务里最隐蔽、但也最容易修的一个性能杀手。4. 训练参数实操优化器、学习率与损失函数在FashionMNIST上的配置4.1 用Adam替换SGDmomentum收敛更省心AlexNet论文里用的是SGD初始学习率0.01、动量0.9、权重衰减5e-4。那个配置是针对ImageNet这种百万级数据的FashionMNIST只有6万张图模型又相对深直接用SGD调起来比较费劲。常见做法是改用Adam默认学习率0.001对学习率的敏感度低很多。优化器改动不影响模型结构只影响权重更新规则。Adam内部维护一阶动量和二阶动量学习率自适应当前参数的梯度尺度。FashionMNIST这个数据集规模下Adam能在30个epoch内达到一个合理精度而SGD需要额外调学习率退火策略对新手不友好。如果你后续要刷更高精度再换回SGD配合CosineAnnealing不迟。4.2 训练循环主体30个epoch每轮同时输出训练和验证指标device torch.device(cuda if torch.cuda.is_available() else cpu) model AlexNetFMNIST(num_classes10).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) def run_epoch(loader, model, criterion, optimizerNone, devicecpu): is_train optimizer is not None model.train() if is_train else model.eval() total_loss 0.0 total_correct 0 total 0 for images, labels in loader: images, labels images.to(device), labels.to(device) logits model(images) loss criterion(logits, labels) if is_train: optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * images.size(0) total_correct (logits.argmax(dim1) labels).sum().item() total images.size(0) return total_loss / total, total_correct / total for epoch in range(1, 31): train_loss, train_acc run_epoch( train_loader, model, criterion, optimizer, device ) val_loss, val_acc run_epoch( test_loader, model, criterion, None, device ) print(fepoch {epoch:02d} ftrain_loss {train_loss:.4f} train_acc {train_acc:.3f} fval_loss {val_loss:.4f} val_acc {val_acc:.3f}) if epoch % 10 0: torch.save(model.state_dict(), falexnet_fmnist_{epoch}.pth)optimizer is not None用来区分训练和验证两种模式验证时只前向传播不更新权重。logits.argmax(dim1)取每行最大logits对应的索引作为预测类别再与标签做逐元素比较。loss.item()取标量否则会带着梯度图叠加内存会越积越多。每10个epoch存一次模型只保存state_dict()不保存整个对象加载时再实例化模型结构即可。环境上有GPU就用GPU没有就CPU硬跑。CPU单epoch大约两分钟左右30个epoch约一小时可接受。如果连得慢优先把num_workers调大再看batch size是否能增到256。4.3 核心超参数速查表参数建议值调参方向优化器Adam收敛慢换SGDmomentum0.9学习率0.001训练震荡降到0.0005batch_size128爆显存时降到64epochs30验证集连续5轮不涨就早停weight_decay1e-4过拟合时增大到5e-4Dropout0.5深度过拟合时增大到0.7weight_decay在PyTorch的Adam里对应L2正则化能抑制全连接层4096维参数过拟合。FashionMNIST空间分辨率低抗过拟合的关键其实是Dropout和全连接层的容量控制数据增强反而没那么重要。验证集准确率在25个epoch后不再上升可以直接用最后保存的模型不必等到30轮。4.4 CrossEntropyLoss直接吃logits最后一层别加SoftmaxPyTorch的nn.CrossEntropyLoss内部已经组合了LogSoftmax和负对数似然损失。如果模型最后一层输出原始logits直接传给它即可如果自己在全连接层后手动加nn.Softmax(dim1)会计算两次softmax数值上破坏梯度信息训练初期loss下降缓慢后期精度也会受影响。只在推理时如果需要概率输出可以单独调用torch.softmax(logits, dim1)训练过程中完全不涉及。类别数10与num_classes一致标签是0到9的整数索引CrossEntropyLoss内部会自动做one-hot形式的计算不需要手动转独热编码。5. 收敛检查先在1个batch上通链路再按三个故障位排错5.1 用单个batch验证模型输出形状模型写完先别急着跑30个epoch取一个batch做链路测试确认输入输出维度、设备、损失函数三件事全部正确。这一步能节省大量排查时间。model.eval() sample_loader DataLoader(test_dataset, batch_size4, shuffleTrue) images, labels next(iter(sample_loader)) print(images:, images.shape, labels:, labels.shape) with torch.no_grad(): logits model(images.to(device)) print(logits:, logits.shape) print(pred:, logits.argmax(dim1))正常输出是images: [4, 1, 28, 28]、labels: [4]、logits: [4, 10]。next(iter(sample_loader))只取第一个batch不遍历整个数据集。模型在验证模式下跑避免Dropout带来随机性。如果logits最后一维不是10去检查分类器的num_classes如果第2维不是1去检查Conv2d的in_channels。设备不一致会在images.to(device)之后报错。CPU和GPU混跑时模型和数据必须都在同一个device上否则RuntimeError提示信息里会包含cpu和cuda:0字样一眼能认出来。5.2 三个高发故障位与排查脚本第一个故障位是loss不降。训练5个epoch后train_loss仍停留在0.5以上先打印梯度范数确认学习率是否过大def grad_norm(model): total 0.0 for p in model.parameters(): if p.grad is not None: total p.grad.norm().item() ** 2 return total ** 0.5 # 在 loss.backward() 之后 optimizer.step() 之前调用 # print(grad_norm(model))梯度范数超过100说明梯度爆炸把学习率降到0.0005或0.0001。如果范数一直小于0.01说明梯度消失或学习率过小这时候需要检查是不是在训练循环里误用了model.eval()导致Dropout关闭但BatchNorm也不更新整体前向计算被固定在初始状态。第二个故障位是训练准确率高、验证准确率只有0.1左右。这种症状基本是标签错位。打印labels的形状和取值范围确认它是0到9的整数。如果标签是从1开始编号的10类分类器输出10维损失计算不会报错但所有的预测都会偏向某几类准确率上不去。第三个故障位是训练极其缓慢。FashionMNIST的图很小瓶颈不在计算而在数据IO和线程调度。先确认num_workers设置合理再到任务管理器看CPU占用率。CPU占用率长期低于30%说明数据加载在等待可以把num_workers调大如果内存不足导致交换则要减小num_workers。GPU环境下给DataLoader加pin_memoryTrue可以进一步缩短数据搬运时间CPU环境下这个参数无意义保持默认即可。最后一个验证技巧是把run_epoch训练循环里的日志打印频率调成每个batch一次观察前几个batch的loss是否在下降。前10个batchloss从2.3降到2.0左右说明链路和优化器都是正常的剩下的就是等它慢慢收敛。把这条链路检查脚本固定下来换数据集、换网络、换设备时先跑一遍能省掉至少半个晚上的盲调时间。本文还有配套的精品资源点击获取