1. 项目缘起与整体设计思路
做图像分类练手,很多人第一个真正跑通的项目就是 Fashion-MNIST。它不像手写数字那样“干净得不像话”,10 个类别全是衣服鞋包,类间差异小、类内差异大,一件毛衣和一件衬衫在 28×28 的灰度图里经常糊成一团。我当初选它做自用项目,目标很明确:不追求刷榜,而是搭一条从数据加载、模型训练、准确率评估到图片展示的完整闭环,而且准确率要能稳稳站在 90% 以上,同时训练时间控制在普通笔记本能接受的范围。这个标题里的三个要素——识别、图片展示、准确率 90%——其实对应了三个真实需求:模型能分类、结果能看见、效果能验证。
先说清楚这个项目适合谁。如果你是刚学完卷积神经网络、想找个不那么玩具的数据集练手的人,它很合适;如果你是想给家里小孩做个“拍照识衣服”的小工具原型,它也能跑;如果你只是想要一份把训练、评估、可视化串起来的可复现代码骨架,那更合适。不适合谁呢?想直接拿去做工业级服饰识别的人不适合,28×28 灰度图的信息量摆在那里,它只能证明你的流程通顺、模型结构合理,换成真实商品图还得重新做数据。
设计思路上我做了几个取舍,后面会逐个展开。第一,模型不用太大:两层卷积加池化的结构就足够把准确率推过 90%,再往上堆层数收益递减,反而容易在小数据集上过拟合。第二,必须留出独立的验证集和测试集:很多人把测试集反复用来调参,最后报的 90% 是“看过答案的 90%”,没有意义。第三,图片展示这一环不能省:训练日志里的数字是抽象的,把预测结果和原图并排画出来,你才能直观感受到模型到底在哪些类别上犯糊涂。
还有一个容易被忽略的点:数据预处理的一致性。训练时做了什么归一化,推理时就必须做完全一样的处理,否则同一个模型在展示环节会表现得很差,你会误以为是模型不行,其实是流程有 bug。我在这个项目里把预处理封装成一个函数,训练、评估、单图推理三处共用,从根上避免这类问题。
2. 数据集拆解与预处理的关键细节
2.1 Fashion-MNIST 到底长什么样
这个数据集包含 70000 张 28×28 的灰度图片,其中 60000 张是训练集、10000 张是测试集,共 10 个类别:T 恤/上衣、裤子、套头衫、连衣裙、外套、凉鞋、衬衫、运动鞋、包、短靴。类别标签是 0 到 9 的整数,这个顺序一定要记牢,展示图片时要用它把数字翻译成中文名称,否则你看到一堆“类别 6”根本不知道是衬衫还是包。
它最大的特点是“规整”:每张图都是居中裁剪过的,人物、背景、光照这些干扰因素基本被去掉了。好处是入门友好,坏处是模型学到的特征偏“剪影”,换到真实场景会不灵。我个人的看法是:把它当流程验证集,别当能力上限。你在这个数据集上跑到 92%、93%,说明你的训练流程是对的;但别拿这个数字去对外宣称模型有多强。
还有一个实际使用中的坑:类别不均衡的问题虽然不严重,但衬衫(shirt)这一类特别难分,它和 T 恤、套头衫、外套在轮廓上高度重叠。我做混淆矩阵的时候发现,模型错得最多的就是衬衫被认成 T 恤、套头衫被认成外套。这不是你的代码有问题,是数据本身的相似度决定的,所以后面我在展示环节特意把错分样本单独画出来看。
2.2 归一化参数的来历与实操注意事项
常见的做法是除以 255 把像素压到 0 到 1 之间,或者用全数据集算出的均值 0.2860、标准差 0.3530 做标准化。这两种都行,我实测下来标准化收敛稍快一点,但对最终准确率影响不到半个百分点。真正会影响结果的是训练集和测试集必须用同一套参数。有些教程图省事,直接在测试集上重新算均值和方差,这是典型的数据泄漏,报出来的准确率虚高,一旦换成真实新图就原形毕露。
我给自己的规则是:统计量只在训练集上算一次,写成常量,训练、评估、推理全部引用它。代码里就这么几行,但能省掉后面大量的困惑。
import numpy as np MEAN = 0.2860 STD = 0.3530 def preprocess(images): x = images.astype(np.float32) / 255.0 x = (x - MEAN) / STD return x[:, :, :, np.newaxis] # 补上通道维注意:如果你用的是现成的数据加载库,它可能默认已经做过某种变换,别叠加两次。动手前先打印一张图的最大值、最小值和均值,确认数据处在什么范围,这一步花 10 秒,能避免后面半小时的排查。
2.3 数据增强:小数据集上的双刃剑
Fashion-MNIST 的图都是居中且朝向固定的,直接做水平翻转其实有点争议,因为左右翻了以后,凉鞋、短靴的形态会变得不太自然,但实测影响很小,我保留了这个操作。旋转和缩放我控制得很保守,旋转角度不超过 10 度,缩放比例在 0.9 到 1.1 之间,目的是让模型对轻微的位移鲁棒一点,而不是把图变成另一件衣服。
这里有个经验:增强强度不是越大越好。我早期试过 20 度旋转加 0.8 到 1.2 的缩放,训练集准确率掉得厉害,验证集却没涨,说明增强把有效信息破坏掉了。判断增强是否合适的标准很简单——你自己看图还能认出这是裤子吗?认不出就把参数调回来。
3. 模型结构与训练配置的落地实现
3.1 为什么选这个结构
我用的是一个很朴素的卷积网络,结构如下表。这个结构在 CPU 上训练十几轮就能过 90%,GPU 上更快,适合自用和反复调试。
| 层类型 | 参数配置 | 输出尺寸 | 说明 |
|---|---|---|---|
| 卷积层 1 | 32 个 3×3 卷积核,padding=1 | 28×28×32 | 提取边缘、纹理 |
| 池化层 1 | 2×2 最大池化 | 14×14×32 | 降采样 |
| 卷积层 2 | 64 个 3×3 卷积核,padding=1 | 14×14×64 | 组合低级特征 |
| 池化层 2 | 2×2 最大池化 | 7×7×64 | 降采样 |
| 展平 | - | 3136 | 转成一维 |
| 全连接层 1 | 128 个神经元,ReLU | 128 | 特征整合 |
| Dropout | 丢弃率 0.3 | 128 | 抑制过拟合 |
| 输出层 | 10 个神经元,Softmax | 10 | 输出类别概率 |
这套结构的逻辑是:两次池化把 28×28 压到 7×7,感受野足够覆盖整件衣服的轮廓;通道数从 32 翻到 64,让模型有空间去表达更复杂的纹理组合。全连接层只留一层 128,是为了防止参数过多。Dropout 放在全连接层前面而不是卷积层里,因为卷积层的参数共享本身就有一定正则效果,在卷积后加 Dropout 收益不大,还会拖慢收敛。
参数量的粗略计算也值得说一句,方便你判断显存和训练时间。第一层卷积约 32×3×3×1 加偏置,不到 320 个参数;第二层约 64×3×3×32,接近 1.8 万个;大头在全连接层,3136×128 约 40 万个。总参数量在 45 万左右,非常轻量,一张入门级显卡甚至纯 CPU 都能跑。这个规模的模型有个好处:你可以在一小时内做完几十次实验,快速感受学习率、批大小、增强强度对结果的影响,这比盯着一个大模型跑一整天更有学习价值。
3.2 训练配置与参数选择过程
优化器用 Adam,学习率初值 1e-3,批大小 64,训练轮数 20 轮。这几个数字不是我随口定的,说一下我当时的试错过程。学习率我试过 1e-2,前几轮损失下降很快,但到第 5 轮左右开始震荡,验证准确率卡在 87% 上不去;换成 1e-3 后曲线明显平滑,第 12 轮左右越过了 90%;1e-4 太慢,20 轮还没收敛。批大小方面,64 是比较稳的折中,128 训练更快但梯度估计噪声小、泛化略差,32 太慢。这些都是经验值,但方向是对的:学习率负责“走得对不对”,批大小负责“走得稳不稳”。
学习率调度我加了一个简单的衰减策略:验证集准确率连续 3 轮不提升,学习率乘以 0.5。这个策略解决了我遇到的一个具体问题——模型在第 10 轮前后准确率停滞,加了衰减后通常还能再挤出 1 到 2 个百分点。除了调度,我还用了早停,耐心值设为 6 轮,防止过拟合白跑。
model.compile( optimizer=keras.optimizers.Adam(learning_rate=1e-3), loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) callbacks = [ keras.callbacks.ReduceLROnPlateau( monitor='val_accuracy', factor=0.5, patience=3, min_lr=1e-5), keras.callbacks.EarlyStopping( monitor='val_accuracy', patience=6, restore_best_weights=True) ]注意:
sparse_categorical_crossentropy对应标签是整数(0 到 9),如果你的标签做成了独热编码,就要换成categorical_crossentropy,两者用错不会报错,但损失会算错,准确率永远上不去,这是很隐蔽的坑。
3.3 训练结果与准确率构成分析
最终我在测试集上拿到 91.6% 左右,验证集曲线和测试集基本贴合,没有出现验证集远高于测试集的情况,说明划分合理、没有明显过拟合。为了确认这个数字可信,我做了三件事:一是固定随机种子重跑三次,结果浮动在 0.4 个百分点以内;二是看混淆矩阵,确认错误的分布符合直觉(衬衫、套头衫、外套三者互相错分占了大头);三是把错分样本的图片调出来肉眼检查,发现有几张确实模糊到人眼也难判断。
这里特别提醒一句:达到 90% 不难,难的是知道这 90% 是怎么来的。如果你的训练准确率 98%、测试准确率 88%,说明过拟合,该加正则或减容量;如果两个都只有 85%,说明欠拟合,该加轮数或加容量;如果测试集和验证集差距大,先怀疑数据划分和预处理不一致。这几个判断路径比盲目调参有用得多。
4. 图片展示环节的实现与调优
4.1 展示什么比怎么展示更重要
很多人理解的“图片展示”就是把预测结果画出来,但真正有用的是分层展示。我把它分成三个视图:第一个是随机抽样展示,看图、真实标签、预测标签、置信度四元组;第二个是错分样本专页,只展示预测错误的图,按置信度从高到低排列,这些“错得很自信”的样本最值得研究;第三个是混淆矩阵热力图,看清哪两类纠缠最严重。
随机抽样展示的实现不难,用 matplotlib 画子图,把图片反归一化后显示,标题写“真:衬衫 / 预:T 恤 0.72”。这里有个细节:反归一化要用同一套均值和标准差,先乘标准差再加均值,最后裁到 0 到 1 区间,否则图会偏暗或偏亮,看起来像数据坏了。
错分样本视图是我认为最有价值的一环。我印象很深的是一张被认成 T 恤的衬衫,领口纹理在 28×28 下几乎消失,只剩一个宽肩轮廓,人眼乍看也会犹豫。这种样本提醒你:模型的错误不全是 bug,有一部分是分辨率决定的天花板。这也解释了为什么准确率在 91% 到 93% 之间就开始变得难挤——剩下的错误很多是数据本身的信息不足。
4.2 置信度阈值与人工复核的配合
展示层如果只显示“预测对了”就没意思了,我加了一个置信度阈值过滤:置信度低于 0.6 的预测标记为“待复核”,用不同颜色标注。实际用下来,这个阈值附近集中了大部分模棱两可的样本,人工扫一眼就能判断模型是错还是对,比通篇浏览高效得多。这其实是一个很朴素的“人机协同”思路:模型给概率,人看不确定的部分。
这里有个实操建议:如果要做成可视化界面给非技术同事看,别把 10 个类别的概率全堆上去,只显示前三名加概率条就够了。信息过载反而让人抓不住重点。我自己用的时候就把输出打印成“真实:外套 | 前 3 预测:外套 0.81、套头衫 0.12、衬衫 0.04”的格式,一眼就能看出模型是笃定还是犹豫。
4.3 展示环节的常见坑
第一个坑是颜色映射选错。灰度图用默认的彩色映射会画出诡异的荧光色,用cmap='gray'才是对的,还要记得vmin=0, vmax=1,不然对比度会被自动拉伸,看起来糊。
第二个坑是子图布局挤在一起。批量展示 25 张图时,标题文字很容易重叠,解决办法是设置合适的figsize,比如 12×12,再用tight_layout()自动调整间距。
第三个坑是推理时忘了补 batch 维度。单张图的形状是 28×28×1,模型要的是 1×28×28×1,直接喂进去会报形状不匹配。我在代码里固定写成img[np.newaxis, ...],避免每次都要想一遍。
5. 常见问题排查与实战避坑清单
5.1 准确率上不去,先按这个顺序查
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 训练准确率也低 | 欠拟合 | 加轮数、加通道数、调大学习率 |
| 训练高、验证低 | 过拟合 | 加 Dropout、加增强、减容量 |
| 两者都高但测试低 | 数据泄漏 | 检查是否用测试集调过参 |
| 损失变成 NaN | 学习率过大或输入异常 | 降到 1e-4,打印输入范围 |
| 准确率卡在 10% | 标签与损失函数不匹配 | 核对整数标签与损失类型 |
| 推理结果全是一类 | 归一化不一致或维度错 | 单张图跑一遍,逐层看输出形状 |
这个表我贴在项目目录的说明文件里,遇到问题先照着过一遍,能解决八成情况。最典型的一次是我把标签和损失函数配错了,训练损失一直在 2.3 附近晃,准确率稳在 10%,查了半小时代码才发现问题,回头看表格第一分钟就能定位。
5.2 复现性与可维护性的实操心得
做自用项目最容易犯的毛病是“跑通了就完事”,过两周想改点东西,发现自己都不知道当初改了什么。我的做法是:把配置全部抽到一个字典里,包括学习率、批大小、轮数、增强参数、随机种子,训练时打印一份配置快照,结果文件按时间戳命名。这个习惯看起来啰嗦,但你只要经历过一次“莫名其妙的 90% 和同样代码跑出来的 88%”,就会明白固定随机种子和记录配置有多重要。
还有个小技巧:训练完把模型权重、混淆矩阵图、错分样本清单一起存进一个以时间命名的文件夹,下次想对比实验直接翻文件夹,不用重跑。我靠这个办法在一个周末里对比了七组配置,最终确定的那套参数就是从这些记录里挑出来的。
5.3 达到 90% 之后还能做什么
准确率过了 90%,这个项目其实还有延展空间。我给自己的下一步是:把模型导出成轻量格式,做一个本地小工具,给定一张图片路径就输出类别和置信度;再往深一点,可以试试把灰度图换成彩色数据集,或者加上类激活图,看看模型到底盯着衣服的哪个部位做判断——这个分析对理解卷积网络特别有帮助,很多时候你会发现模型关注的位置和你以为的不一样。
不过话说回来,这个项目最大的价值不在于那 91% 的数字,而在于它逼着我把数据清洗、归一化一致性、验证集划分、可视化排查这几件事从头走了一遍,每一环都踩过坑、补过漏。后面我换任何数据集做分类,用的都是同一套流程骨架,只是换掉数据加载和类别名称。这套骨架跑顺了,下一个项目开工时你会发现,真正耗时间的从来不是写模型那二十行代码,而是那些看似琐碎、却决定结果可信度的准备工作。