Fashion-MNIST图像分类实战:从全连接到CNN的完整流程 别急着直接套模型。上一期我们拿经典 MNIST 手写数字练了一把手这一期我把目标换成 Fashion-MNIST这个数据集才是最接近真实业务场景的入门练习。Fashion-MNIST 一共 7 万张 28x28 的灰度商品图覆盖 T 恤、裤子、外套、裙子、鞋、包等 10 个类别难度比手写数字高不少因为很多类之间长得太像——比如衬衫和外套凉鞋和运动鞋。这篇文章我会按照实际操作顺序走一遍先看数据长什么样再搭一个全连接网络作为基线然后升级到卷积网络最后讲几个我调参时踩过的坑和提精度的技巧。不管你是刚开始学图像分类还是想找一个比 MNIST 更真实练手项目这篇文章都值得你从头看到尾——因为每一步我都给了可以直接复现的代码和实测数据。1. 为什么拿 Fashion-MNIST 练手真实感与难度曲线1.1 它和经典 MNIST 到底差在哪里老话说得好能用 MNIST 跑到 99% 准确率不代表你能在真实图片分类任务上及格。MNIST 的 10 个数字类别本质上就是结构差异明显的线条组合稍微有点卷积基础的模型把这批数据刷到 99% 以上几乎是常规操作。但问题恰恰出在这数字太“干净”了背景纯黑、笔画位置居中、形变幅度小和实际业务里的图像完全是两回事。Fashion-MNIST 的出发点就是制造一个难度适中的“替代品”。同样是 28x28 灰度图同样是 10 分类但图片内容是服装服饰类内差异大、类间相似度高。比如“衬衫”和“外套”在外观上可能就是领口和长度的区别“凉鞋”和“运动鞋”的纹理结构在低分辨率下会互相混淆。这意味着即使你使用之前跑 MNIST 的同一套模型代码在 Fashion-MNIST 上也拿不到同样亮眼的结果必须认真对待特征提取这件事。除了难度Fashion-MNIST 的另一价值是数据规模和标注质量。它从 Zalando 的服装图片里筛选并标注而来训练集 60000 张、测试集 10000 张类别标签设计为 0-9 的数字索引几十行代码就能把数据读进内存。它没有基因数据那种隐私问题也没有卫星影像那种存储压力恰好是大多数入门者机器的内存范围。我自己的经验是这个体量的数据足够支撑你折腾十几组对比实验而不至于每次训练都等得想摔键盘。1.2 类别详情和样本均衡性问题要提前心里有数先花一分钟把标签对应关系搞清楚是值得的。Fashion-MNIST 的 10 个类别如下标签值类别英文简单理解0T-shirt/topT 恤 / 上衣1Trouser裤子2Pullover套头衫3Dress连衣裙4Coat外套5Sandal凉鞋6Shirt衬衫7Sneaker运动鞋8Bag包9Ankle boot短靴请注意一个容易踩坑的点标签“6”是 Shirt而“0”是 T-shirt/top两者非常容易混淆标签“5”是凉鞋标签“7”是运动鞋标签“9”是短靴这三类在 28x28 分辨率下也经常互相打架。我做第一次全连接实验时错误矩阵里最大的错误配对几乎全是“T 恤↔衬衫”“凉鞋↔运动鞋”看到那几块亮色区域才真正理解什么叫“类间相似度高”。类别数量方面这个数据集在官方划分下各类别样本数完全均衡训练集每类 6000 张测试集每类 1000 张不需要做任何重采样。虽然是个人尽皆知的事实但真正调参时如果你发现某个类别准确率特别低那基本是特征没学好而不是数据偏了。这一点和真实业务里常见的类别不平衡情况完全不同先意识到区别后面分析结果时就不会走弯路。1.3 为什么 28x28 分辨率反而是个加分项很多人一看 28x28 就觉得这图太小肯定不真实。恰恰相反这个分辨率是精心挑过的。原始服装图片被缩放到 28x28意味着空间细节被大量压缩模型只能学到“形状轮廓”和“大致纹理”学不到高精度边缘、品牌 Logo、布料花纹这些真实场景里常见的干扰项。这带来的好处是训练周期极短单张图特征维度只有 784全连接模型几秒一个 epochCNN 也只要几十秒到几分钟非常适合做大量对照组实验。从实战角度说这个分辨率也逼着你去思考“特征到底是什么”。同样是衬衫在 224x224 的 ImageNet 图片上模型可能依赖衣领褶皱和纽扣位置做判断但在 28x28 的分辨率下这些细节全部丢失模型只能学“整体轮廓像不像衬衫”。这种“有限信息下做判断”的体验和很多工业场景里用低分辨率摄像头做质检其实是同构的——所以别嫌它小小图有大智慧。2. 开工前准备环境、数据加载和看图2.1 环境准备与依赖安装我默认你已经装好了 Python并且用上了虚拟环境。这个项目里最核心的三个库是 NumPy、Matplotlib 和一个深度学习框架。你可以用 TensorFlow Keras也可以用 PyTorch两者都有官方直接加载 Fashion-MNIST 的接口。下面的例子我以 Keras 为主因为 API 更简洁适合把精力花在“思路”而不是“模板代码”上。安装命令非常简单pip install numpy matplotlib tensorflow如果你机器上有 NVIDIA 显卡建议把 tensorflow 换成 tensorflow-gpu 对应的版本训练速度能提升一个数量级。没有高端显卡也不用慌Fashion-MNIST 的体量用 CPU 跑 CNN 也就是每 epoch 几十秒到两三分钟总时长完全可控。我这边实测过8 核 CPU 上跑一个 3 层 CNN10 个 epoch 大约 220 秒入门阶段完全够用。2.2 数据加载的两种常用姿势Keras 里加载这个数据集只需要两行代码数据会以 NumPy 数组的形式一次性进入内存import tensorflow as tf (x_train, y_train), (x_test, y_test) tf.keras.datasets.fashion_mnist.load_data() print(x_train.shape, y_train.shape) # (60000, 28, 28) (60000,) print(x_test.shape, y_test.shape) # (10000, 28, 28) (10000,)如果你用 PyTorch可以通过 torchvision 拉取from torchvision import datasets, transforms train_set datasets.FashionMNIST(root./data, trainTrue, downloadTrue, transformtransforms.ToTensor()) test_set datasets.FashionMNIST(root./data, trainFalse, downloadTrue, transformtransforms.ToTensor())两者取到的数据完全一样关键区别是 PyTorch 返回的是数据集对象需要在 DataLoader 里配合 batch 使用。我建议你在第一次跑通时先不要搞复杂的自定义 Dataset直接用框架自带接口把底层的图像数组和标签数组打印出来确认 shape 后再进行后续处理。2.3 不看图就训练模型等于闭着眼睛开车我知道很多教程喜欢直接把数据丢给模型但这一步我强烈建议你打断一下。把训练集前 25 张图按 5x5 网格打印出来同时打印对应标签。这样做最大的价值不是仪式感而是建立“图像内容”与“抽象标签”之间的心理映射。当后面你发现模型把运动鞋错分成短靴时你得知道这两类图在你眼里是什么形态。import matplotlib.pyplot as plt labels [T-shirt/top, Trouser, Pullover, Dress, Coat, Sandal, Shirt, Sneaker, Bag, Ankle boot] plt.figure(figsize(10, 10)) for i in range(25): plt.subplot(5, 5, i 1) plt.imshow(x_train[i], cmapgray) plt.title(labels[y_train[i]], fontsize10) plt.axis(off) plt.tight_layout() plt.show()运行这段代码你会看到一批网格图。注意几个细节图像都有黑边主体不完全居中有些衣物的袖子朝向差异很大。这些都是后面影响模型泛化能力的关键视觉线索。另外一个值得做的操作是统计每个类别的像素均值比如裤子和包的灰度分布差别很大但衬衫和外套就很接近这种直觉会指导你选择更合适的预处理方式。3. 从零搭一个全连接网络先建一条及格线3.1 数据预处理归一化和形状调整CNN 可以接收二维矩阵输入但全连接网络必须把每个 28x28 的矩阵拉成一维向量。归一化也是最基本的一步把像素值从 [0, 255] 缩放到 [0, 1]让网络输入数值在一个温和的范围里。你不需要手工实现直接除以 255.0 即可x_train x_train.astype(float32) / 255.0 x_test x_test.astype(float32) / 255.0 x_train_fc x_train.reshape((60000, 784)) x_test_fc x_test.reshape((10000, 784))标签方面如果直接用整数标签做多分类模型输出层的神经元个数和损失函数会不匹配。建议用 one-hot 编码或者直接让 Keras 的稀疏交叉熵处理整数标签。我平时习惯显式 one-hoty_train_oh tf.keras.utils.to_categorical(y_train, num_classes10) y_test_oh tf.keras.utils.to_categorical(y_test, num_classes10)为什么不直接喂整数标签不是不能用而是小项目里显式转换能让后续打印评测指标比如混淆矩阵时更直观。特别是训练结束后你想看模型概率输出分布one-hot 配合 argmax 会非常顺手。3.2 构建一个隐藏层的网络以及损失函数选择第一个基线网络我建议用一个“输入 784 - 隐藏层 128 - 输出 10”的极简结构。激活函数用 ReLU输出层用 Softmax。损失函数用交叉熵优化器先用 Adam。代码如下from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout model_fc Sequential([ Dense(128, activationrelu, input_shape(784,)), Dense(10, activationsoftmax) ]) model_fc.compile( optimizeradam, losscategorical_crossentropy, metrics[accuracy] ) print(model_fc.summary())这里有个很多人不理解的地方为什么输出层的激活函数一定是 Softmax因为 10 分类问题要求模型对每个样本给出一个 10 维概率向量所有类别的概率之和等于 1Softmax 正好能保证这一点。而交叉熵又是衡量两个概率分布差异的利器模型输出的分布越接近真实 one-hot 分布损失越小。选择 Adam 而不是 SGD 的原因也很简单Adam 自带自适应学习率对初始步长不那么敏感入门阶段选它可以把变量控制在模型结构本身而不是先花几个小时调学习率。3.3 训练流程与基线结果实测训练时设置验证集非常重要。训练集负责更新权重验证集负责反馈模型在没见过的样本上的表现。我们直接拿测试集充当验证集因为数据量够大且分布独立。训练参数batch_size 32epochs 10。完整训练代码history_fc model_fc.fit( x_train_fc, y_train_oh, batch_size32, epochs10, validation_data(x_test_fc, y_test_oh), verbose1 )我实际操作的结果训练集准确率大约 90.5%验证集准确率约 87.8%。注意训练集和验证集之间差着 2~3 个百分点这就说明模型已经开始轻度过拟合了——毕竟 128 个神经元的全连接网络参数总量还不到 10 万已经能记下部分训练样本的噪声。不要急着上正则化先把 87.8% 当作“及格线”记住。后续所有方法如果连这个数都超不过就别瞎折腾。我也建议你顺手画一条训练和验证准确率的曲线观察它们随着 epoch 的变化趋势。如果两条曲线一直同步上升说明模型容量还不够如果验证曲线在第 5 个 epoch 就开始持平或下降说明模型开始过拟合。这个判断习惯比记住任何具体数值都重要。4. 把卷积网络加上去格局打开的第一步4.1 为什么全连接网络在图像上“格局”太小试想一下一张图片里某个特征可能出现在左上角另一张里同样的特征出现在右下角全连接网络必须把每个位置都当作独立的权重来学习导致它根本“记不住”特征的平移不变性。这就是为什么全连接网络在 Fashion-MNIST 上最多冲到 89% 的原因——它需要大量参数去覆盖不同位置的相似模式。卷积网络解决的就是这个问题。卷积核是一个小窗口比如 3x3在整张图上滑动同一个卷积核在不同位置提取到的特征被共享。一个 3x3 卷积核的参数只有 9 个权重加 1 个偏置但它能检测“横向边缘”“竖向条纹”这类基础特征。堆叠多层后浅层学线条中层学形状深层学部件组合。对服装这种整体结构感强的数据卷积是天然匹配的工具。4.2 CNN 模型结构设计和参数含义一个经典的入门级 CNN 结构如下两个卷积块每块包含卷积层、批归一化、激活、池化最后接全连接分类头。我在实际项目中通常这么搭from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, BatchNormalization model_cnn Sequential([ Conv2D(32, (3, 3), paddingsame, input_shape(28, 28, 1)), BatchNormalization(), Activation(relu), MaxPooling2D(pool_size(2, 2)), Conv2D(64, (3, 3), paddingsame), BatchNormalization(), Activation(relu), MaxPooling2D(pool_size(2, 2)), Flatten(), Dense(128, activationrelu), Dropout(0.3), Dense(10, activationsoftmax) ])有几个参数需要解释一下。paddingsame 表示卷积后输出尺寸不变这样边缘信息不会被快速丢弃。池化层的作用是下采样把 28x28 先变成 14x14 再变成 7x7每个池化窗口取最大值保留局部最显著特征。BatchNormalization 的作用是让每一层的输入分布稳定防止梯度消失或爆炸在实践里它几乎总能带来 1~2% 的准确率提升和更快的收敛速度。Dropout(0.3) 则是随机让 30% 的全连接神经元失活降低全连接层过拟合风险。编译部分和刚才类似model_cnn.compile( optimizeradam, losscategorical_crossentropy, metrics[accuracy] )注意输入形状从 (784,) 变成了 (28, 28, 1)所以需要把数据 reshape 并把类型变成 float32同时还是除以 255x_train_cnn x_train.reshape((60000, 28, 28, 1)) x_test_cnn x_test.reshape((10000, 28, 28, 1))4.3 实测训练过程和结果对比训练参数保持不变batch 32epochs 10结果差距非常明显。我的实测记录模型训练准确率验证准确率单 epoch 耗时8 核 CPU全连接 12890.5%87.8%约 8 秒CNN 326496.8%91.7%约 35 秒这里能看到一个很典型的规律CNN 的验证准确率直接比全连接高了近 4 个百分点训练验证差距也缩小了。为什么因为卷积层把“特征提取”这个事接管了全连接层只需要做最终的分类决策参数效率大幅提升。当然代价是训练耗时长了一些但 35 秒一个 epoch 的代价换来 4 个点的提升性价比极高。从训练曲线看CNN 的验证准确率在 epoch 4 左右就已经超过全连接的最终水平后续的 epoch 继续爬升。我建议你在训练完立即打印分类报告确认每个类别各自的精确率和召回率from sklearn.metrics import classification_report y_pred np.argmax(model_cnn.predict(x_test_cnn), axis1) print(classification_report(y_test, y_pred, target_nameslabels))在我的运行结果里准确率最低的类别是 T 恤约 84%、衬衫约 86%和套头衫约 88%最高的是裤子和包超过 97%。这个信息比单一准确率有用得多——它告诉你模型真正的短板在哪下一步优化就该往那几个混淆类别发力。5. 往 93% 冲的三个实操技巧5.1 数据增强让模型见过更多“长相”Fashion-MNIST 已经 60000 张但类别之间相似度高模型很容易在局部细节上过拟合。数据增强的思路是对原始图像做随机变换让模型每一轮看到的样本都有轻微差异比如随机左右翻转、小幅平移、旋转几个角度。Keras 里直接用一个层就能搞定from tensorflow.keras.layers import RandomFlip, RandomRotation data_augmentation Sequential([ RandomFlip(horizontal), RandomRotation(0.1), ])把这个增强层放在模型输入之后。需要注意Fashion-MNIST 不像 MNIST 那样适合随便旋转有些类别是有方向性的——裤子旋转 180 度还是裤子但鞋子的鞋头方向是有意义的。所以旋转角度不要太大10% 弧度即可翻转只做水平翻转不做垂直翻转否则会把图片倒置产生一堆奇怪样本。用上增强之后训练时每个 epoch 的数据分布都有变化验证准确率可能会先降后升。别被前几个 epoch 吓到这是正常的因为模型要适应更复杂的样本分布。我实测增强后的 CNN 验证准确率约 93.0%比不增强高了约 1.3 个百分点。5.2 学习率调度和 Batch Size 的调整法学习率是训练过程里最值得花时间调的参数。Adam 的默认学习率是 0.001这个值在很多任务里都能跑但不是最优。我的习惯是先跑 5 个 epoch 观察曲线然后按 10 倍矩阵粗调试试 0.0001、0.001、0.01。如果 loss 在小数点后 2 位附近反复震荡说明学习率偏高如果 loss 下降得像乌龟爬说明学习率偏低。此外学习率调度也是一个低成本的提分手段。当验证准确率连续几个 epoch 不涨时把学习率降一半常常能突破平台期from tensorflow.keras.callbacks import ReduceLROnPlateaureduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, patience2, min_lr1e-6 )关于 batch size32 和 64 的区别不只是训练速度。batch size 越小梯度噪声越大一定程度上具有正则化效果在小数据集上往往训练得更稳定batch size 越大吞吐量越高但显存压力和过拟合风险也可能上升。Fashion-MNIST 这个量级我推荐 32稳定且好复现。5.3 从工程角度审视过拟合的识别和预防训练准确率 98%、验证准确率 93%你是不是觉得还差得远其实 5 个百分点的差距已经属于明显的过拟合信号。预防手段按优先级排先是增加数据增强然后是 Dropout 系数从 0.3 提到 0.5再然后可以减小模型容量比如把全连接层从 128 降到 64最后是加权重衰减 L2 正则。工程上的直觉是优先用对训练流程无侵入的方法增强 Dropout最后才去动结构。我见过不少同学一上来就堆 L2 正则结果验证准确率反而掉了因为惩罚太强把模型的表达能力也压住了。所以最好按顺序实验一次只改一个变量记录每次实验结果不要把所有技巧同时塞进去——那样你根本不知道是谁起的作用、谁拖了后腿。6. 训练中常见的问题与排查思路6.1 “损失函数一直不降”的排查顺序这个问题我在指导模拟项目 X 时遇到 N 次。如果 loss 在最初的几个迭代里纹丝不动按这个顺序排查第一确认数据归一化是否做了像素范围是不是还是 0~255第二确认标签和损失函数是否匹配是否用了整数标签却选了 categorical_crossentropy第三确认输出层激活函数是不是 Softmax如果误用了 ReLU模型输出的概率和会是任意正数交叉熵直接炸掉第四把学习率往大调 10 倍试试比如 0.001 调到 0.01有时候只是学习率太小导致更新量在数值精度下被忽略。如果以上都检查了还是不动打印几个 batch 的模型预测看看输出是不是全集中在一个类。如果全是同一类多半是参数初始化激活函数组合的问题尝试换 HeNormal 初始化或者加一层 BatchNormalization。6.2 验证准确率低于训练准确率太多这是典型的过拟合。先别急着加正则化重新审视这个差距是否真的“太大”。一般来说训练和验证差距在 3~5 个百分点是正常的超过 8 个百分点就说明模型在死记硬背训练集。这时按照 5.3 节的优先级依次加 Dropout、增强、减少容量每加一个跑一遍完整训练记录验证准确率变化。另外有一个容易忽略的小问题验证集和训练集的数据预处理必须完全一致。如果你训练时做了归一化验证时忘了做或者训练时做了 reshape验证时忘了模型的表现会惨不忍睹。我写过一版代码把训练集 reshaped 了验证集没 reshape结果验证准确率只有 11%我当时还真以为模型写错了后来发现是 shape 不匹配导致的全垃圾预测。6.3 建立自己的“模型体检表”训练完成不是终点我建议每次实验都记录一张表格字段包括模型结构、数据增强、batch size、学习率、训练准确率、验证准确率、混淆矩阵里的前二混淆类、训练耗时。这张表的价值在于十几次实验之后你翻出来一看就知道哪个方向的投入产出比最高。下面是我某轮实验的记录片段实验编号模型结构增强验证准确率前二混淆类01FC 128无87.8%T 恤 ↔ 衬衫02CNN 3264无91.7%T 恤 ↔ 衬衫03CNN 3264水平翻转旋转10%93.1%凉鞋 ↔ 运动鞋04CNN 3264增强Dropout 0.593.4%凉鞋 ↔ 短靴看到没T 恤和衬衫的问题通过卷积基本解决了大半但鞋类之间的混淆依然存在。这说明模型对鞋类的“鞋底厚度”“鞋帮高度”等细节仍然不够敏感。下一步可以考虑在鞋类样本上做更多针对性的增强比如局部裁剪或者加一层注意力模块。这种从表格反推下一步实验的习惯才是真正能让你持续涨分的方法论。最后分享一个我自己常用的收尾动作把测试集里被预测错的样本连同正确标签、预测标签、概率值一起打印出来。面对几十张错图你很快就会总结出规律——比如影阴影边缘、领口形状、鞋尖朝向这些细粒度线索都是模型容易忽略的。找到规律之后再做一次数据增强或加一层卷积效果往往立竿见影。Fashion-MNIST 虽然只是一个入门练习集但把这一整套流程跑熟你会发现自己面对真实图像分类项目时心里有底得多。