资讯动态

FashionMNIST图像分类入门:数据加载、归一化与CNN基线实战

发布时间:2026/10/27 6:38:28 来源:尧图企业网站定制
FashionMNIST 这个数据集在圈子里基本属于“人手一份”的入门标配。它的定位很明确10 个类别的服饰灰度图60000 张训练 10000 张测试每张 28×28格式与手写数字数据集完全一致。它能干的事情是让你在几分钟内跑通一条完整的图像分类流水线——下载、加载、归一化、训练、评估、看混淆矩阵全程不需要 GPU也不需要标注团队。它解决的问题也很具体手写数字那套数据太“干净”了一个两层卷积就能刷到 99% 以上你根本看不出自己改的网络结构、损失函数、优化器到底有没有用而 FashionMNIST 的难度刚好卡在一个能拉开差距的区间改一个 trick 通常能看到 0.5 到 1 个点的变化反馈足够灵敏。适合谁来参考刚学卷积网络想找个能跑通的练手项目的、要快速验证某个结构改动是否值得深挖的、给学生或新人做教学演示的、以及需要一套稳定 baseline 来做对比实验的。如果你只是想找一份“下载 加载 跑通”的可抄作业下面这些内容基本能覆盖你 90% 的疑问。1. 为什么我还在用这套数据做基准定位、能力边界与选型逻辑1.1 换掉的不只是图片是模型的区分度很多人第一次接触它是因为“手写数字刷腻了”。这个直觉是对的但原因值得说清楚。手写数字的类内差异极小——同一个人写的 3 和另一个人写的 3笔画结构高度相似类间差异极大——0 和 1 几乎不会有歧义。结果就是任务的决策边界很“平”一个线性分类器加一点手工特征就能到 92%两层卷积直接 99% 往上。当所有模型都在 99% 附近挤成一团时你根本没有信号判断自己的改动是好是坏实验就变成了自欺欺人。FashionMNIST 的设计者做的事情其实很克制保持图像尺寸、样本数量、标签格式、甚至文件二进制结构完全不变只把内容从数字换成服饰。这个“只换内容不换接口”的选择非常关键——你原来处理手写数字的代码改一个类名就能直接跑。但它把难度抬到了一个新层次类内差异被显著放大一件 T 恤可以是修身款、oversize、带印花、带拉链类间差异被压缩T 恤、套头衫、衬衫、外套这四类在 28×28 的灰度图上经常只剩一个模糊的轮廓加几条褶皱。这就逼着模型去学真正的形状特征而不是记住几个固定模式。我实测下来的经验区间是这样的逻辑回归这类线性模型大概 84%–86%一个不加任何技巧的三层卷积网络 91%–92%加上批归一化和合理的数据增强能到 93%–94%再往上每 0.5 个点都要付出明显代价。这个梯度分布非常“健康”意味着你的每一次改动都有可观测的反馈。1.2 它的能力边界最好在动手之前就认清用错数据集比不用更浪费时间。这套数据的局限我总结成四条都是踩过之后才认的。第一是分辨率。28×28 意味着总像素才 784 个一件衣服的袖口、领型、纽扣这些真正有判别力的细节基本都被下采样抹平了。你想做细粒度分类——比如区分不同款式的运动鞋——在这个分辨率下是没有出路的硬做只会得到一堆噪声。第二是灰度。颜色信息被完全丢弃一条红裙子和一条黑裙子在模型眼里一模一样。如果你的任务里颜色是强特征比如区分交通标志、区分水果成熟度这套数据给不了你任何参考。第三是构图。所有样本都是单物体居中、纯黑背景、无遮挡、无旋转、无透视变化。真实场景里的图像不是这样的背景杂物、光照、拍摄角度都会带来额外挑战。第四是类别分布。10 个类别严格均衡每类 6000 张训练样本。而真实业务里的类别分布往往严重长尾头部类占九成、尾部类只有几十张。在这套数据上训练的模型换到长尾场景会立刻露馅。所以我的定位判断很明确它是验证流程和结构的工具不是证明落地能力的工具。你要跑通训练管线、要对比两个网络结构、要教别人什么是过拟合用它你要证明模型能解决实际业务问题别用它。1.3 一次下载到底拿到了什么文件清单与体积核对自动下载这件事很多人是“点一下就不管了”出了问题完全不知道从哪查。我建议你至少清楚一次完整下载会落地哪些文件。文件名内容解压后大小字节说明train-images-idx3-ubyte训练图像47,040,01660000×784 16 字节头train-labels-idx1-ubyte训练标签60,00860000 8 字节头t10k-images-idx3-ubyte测试图像7,840,01610000×784 16 字节头t10k-labels-idx1-ubyte测试标签10,00810000 8 字节头那几个“16”“8”不是随便加的是 idx 格式的头部开销。图像文件的头 16 字节结构是4 字节魔数图像固定为 0x00000803、4 字节维度数值为 3、4 字节行数、4 字节列数之后才是连续的像素数据。标签文件的头 8 字节是4 字节魔数标签固定为 0x00000801、4 字节样本数。知道这个结构之后你就能自己写解析器也能一眼看出文件是不是被截断了——用文件大小核对完整性比看下载进度条靠谱得多。注意压缩包的大小和上面解压后的大小是两回事压缩包会小很多图像压缩包大概 26MB 出头。核对完整性时一定看清你手上的是压缩包还是解压后的文件用错基准去比对只会把自己绕进去。2. 把数据摸透类别、编码与那些反直觉的细节2.1 十个类别与标签的顺序别自己重排标签就是 0 到 9 的整数顺序是固定的模型的输出向量第几位就对应第几类。这个顺序一旦你自己重排过后面所有可视化、混淆矩阵、误差分析全部错位而且很难发现。标签英文名中文含义标签英文名中文含义0T-shirt/topT 恤/上衣5Sandal凉鞋1Trouser裤子6Shirt衬衫2Pullover套头衫7Sneaker运动鞋3Dress连衣裙8Bag包4Coat外套9Ankle boot短靴我习惯在代码里把这张表写成一个常量列表任何需要显示标签的地方都从它取绝不手写字符串。这个习惯救过我很多次——尤其是在做批量可视化和生成报告的时候。2.2 最容易混淆的几组和它们背后的现实原因如果你只看准确率会错过最重要的信息。真正该看的是混淆矩阵而这里面有规律可循。第一组是上衣四兄弟T 恤(0)、套头衫(2)、外套(4)、衬衫(6)。这四类占了全部错误的一大半。原因很朴素在 28×28 的灰度图上一件修身衬衫和一件薄外套的区别可能就体现在领口那一两个像素的走向上。模型看到的是“一个 torso 形状 两条袖子”它没有足够的信息去判断这块布是棉的还是羊毛的、有没有开襟。第二组是鞋类三兄弟凉鞋(5)、运动鞋(7)、短靴(9)。这三类的混淆主要发生在轮廓上——从正面或侧面看一双低帮运动鞋和一双短靴的剪影非常接近。有意思的是凉鞋很少和短靴混因为凉鞋的镂空结构在灰度图上会形成明显的暗区反而成了辨识特征。第三组是裤子(1)和连衣裙(3)。这两类在全身长度上接近主要差别在腰部是否有分割。图像太小时这个分割经常消失于是裤子被误判成连衣裙。这个规律对调参有直接指导意义。如果你发现模型整体 92%但混淆矩阵显示上衣四类之间互相错得一塌糊涂那你去加正则、调学习率意义都不大应该考虑的是如何在有限分辨率下强化局部结构特征——比如加深网络感受野、引入注意力、或者在数据增强上做文章。反过来如果错误均匀散布在所有类别上那更可能是欠拟合或者训练不充分。2.3 归一化参数千万别直接抄手写数字那一套这是新手最常犯、也最隐蔽的错误。网上大量教程里写的Normalize((0.1307,), (0.3081,))是手写数字数据集的统计值直接套到服饰数据上是不对的。两个数据集的内容分布差别很大手写数字的笔画细、留白多整体偏暗服饰图像的主体占据了画面的大部分面积整体明显偏亮。社区里广泛引用的服饰数据统计值是均值约 0.2860、标准差约 0.3530单通道。用错的后果不是报错而是准确率莫名其妙低一两个点、loss 曲线抖得厉害你会在网络结构上瞎找原因最后发现是归一化背锅。我的做法是自己算一遍再写死这样心里有底也顺便完成了一次数据体检import torch from torchvision import datasets, transforms # 第一步只做 ToTensor把 0-255 的 uint8 变成 0-1 的 float raw datasets.FashionMNIST( root./data, trainTrue, downloadTrue, transformtransforms.ToTensor() ) loader torch.utils.data.DataLoader(raw, batch_size1000, shuffleFalse, num_workers0) total, total_sq, count 0.0, 0.0, 0 for imgs, _ in loader: total imgs.sum().item() total_sq (imgs ** 2).sum().item() count imgs.numel() mean total / count std (total_sq / count - mean ** 2) ** 0.5 print(fmean{mean:.4f}, std{std:.4f}) # 大致落在 0.2860 / 0.3530 附近这段代码跑一遍大概十几秒跑完你就有了自己数据上的真实统计值。为什么要做归一化说白了就是让输入分布接近零均值、单位方差这样各层的梯度尺度更一致学习率更好调收敛也更稳。不做归一化也能跑但你会被迫用很小的学习率训练轮数蹭蹭往上涨。2.4 数据本身的瑕疵别死磕那几个点上不去有个事实得提前接受这套数据里存在标注本身就有歧义的样本。有些图你放大看半天也说不清是衬衫还是外套人和人之间都会有分歧。测试集里同样有这类噪声样本。这意味着模型的上限是被数据本身限制住的公开的高分结果大多在 94%–96% 这个区间不加外部数据、不做集成。我见过有人为了从 94.2% 刷到 94.8% 连续调了两周参最后发现是把测试集当验证集反复调模型已经悄悄过拟合测试集了。这个坑后面第 5 节会细说。另外还有少量样本几乎是纯黑或者只剩一个模糊轮廓这类图在训练时会贡献很大的梯度噪声如果你的 loss 曲线时不时出现尖峰很可能就是它们造成的。不用特意去清洗量太小影响有限但知道有这回事能让你少怀疑自己。3. 自动下载的几条路径与它们的取舍3.1 torchvision 路线一行 downloadTrue 背后发生了什么这是最常用的方式也是我推荐给新人的默认选择。核心代码就一行但它的内部逻辑值得搞清楚否则出了问题你完全无从下手。from torchvision import datasets, transforms train_set datasets.FashionMNIST( root./data, # 数据落地目录建议用绝对路径或固定相对路径 trainTrue, # True 取 60000 张训练集False 取 10000 张测试集 downloadTrue, # 文件不存在时才下载 transformtransforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.2860,), (0.3530,)), ]), ) test_set datasets.FashionMNIST( root./data, trainFalse, downloadTrue, transformtransforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.2860,), (0.3530,)), ]), )它的执行顺序大致是先检查root/FashionMNIST/raw/下有没有那四个解压后的文件如果没有就去下载对应的压缩包下载完成后解压、校验最后在raw目录下生成解压文件并写入一份处理完成的标记。这里有三个必须知道的行为特征。第一downloadTrue只在文件缺失时才真正发起下载已经存在的文件不会重复下载所以你不用担心每次运行都重复拉一遍。第二它不校验已有文件的完整性——如果你的文件之前下了一半被中断下次运行它会认为“文件在”然后在校验阶段直接报错而不是自动重新下载。这就是为什么第 5 节里“文件损坏”是最常见的问题。第三root参数决定了整个目录结构我强烈建议一开始就用一个固定的、明确的路径不要用默认的相对路径./data否则你在不同工作目录下跑脚本会得到好几份重复的数据白占几百兆空间。3.2 Keras 路线更省事但少了点控制力如果你用的是 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, x_train.dtype) # (60000, 28, 28) uint8 print(x_train.min(), x_train.max()) # 0 255注意几个差异点返回的是uint8 的 0–255 整数没有归一化形状是(N, 28, 28)没有通道维用卷积层之前要自己扩一维缓存位置默认在用户主目录下的隐藏目录里不在你当前项目目录换机器或者清缓存时要记得这一点。两条路线的差异我整理成一张表省得你来回翻文档对比项torchvision 路线Keras 路线返回类型Dataset 对象可迭代四个 numpy 数组数值范围ToTensor 后 0–1 浮点原始 0–255 uint8形状(N, 1, 28, 28)(N, 28, 28)归一化transform 里显式指定需手动除以 255缓存位置由 root 决定可控默认在用户目录离线兜底可手动放入 raw 目录需按其约定放置我的建议是如果你后续要做数据增强、要自定义采样策略、要接 DataLoader 的多进程预取走 torchvision如果你只是想快速验证一个想法、画几张图Keras 更省事。两者之间没有优劣只有场景匹配。3.3 手动下载与离线目录组织自动下载失败时的兜底方案有些环境访问不到下载源或者下载速度慢到不可用。这时候最稳的做法是手动把文件放到位然后关掉自动下载。关键中的关键目录结构和文件名必须一丝不差。正确的结构是这样的./data/ └── FashionMNIST/ └── raw/ ├── train-images-idx3-ubyte ├── train-labels-idx1-ubyte ├── t10k-images-idx3-ubyte └── t10k-labels-idx1-ubyte放好之后把download参数改成False代码其余部分不变。这里有两个高频错误我必须强调。错误一放的是压缩包不是解压后的文件。不同版本的库对.gz压缩包的处理行为不一致有的版本能自动解压有的版本要求raw目录里直接是解压后的文件。为了跨版本稳定统一放解压后的文件别偷懒。错误二多套了一层目录。从压缩包里解压出来很容易变成./data/FashionMNIST/raw/FashionMNIST/raw/...这种嵌套结构。库找不到文件就会重新尝试下载然后失败然后你再手动放形成死循环。放完之后用ls -RWindows 用dir /s看一眼真实结构三十秒的事。3.4 自己写一个 idx 解析器不依赖框架也能读我一直建议手写一遍解析器原因有两个一是你对数据格式的理解会立刻上一个台阶二是遇到“框架报错但不知道数据哪里有问题”的时候裸读一遍是最快的排查手段。import gzip import numpy as np def read_idx(path, is_imageTrue): 读取 idx 格式文件返回 numpy 数组 opener gzip.open if path.endswith(.gz) else open with opener(path, rb) as f: magic int.from_bytes(f.read(4), big) ndim int.from_bytes(f.read(4), big) dims [int.from_bytes(f.read(4), big) for _ in range(ndim)] data np.frombuffer(f.read(), dtypenp.uint8) expected_magic 0x00000803 if is_image else 0x00000801 assert magic expected_magic, f魔数不匹配: {hex(magic)} if is_image: return data.reshape(dims) # (N, 28, 28) return data # (N,) x read_idx(./data/FashionMNIST/raw/train-images-idx3-ubyte) y read_idx(./data/FashionMNIST/raw/train-labels-idx1-ubyte, is_imageFalse) print(x.shape, y.shape, x.dtype) # (60000, 28, 28) (60000,) uint8这段代码的好处是它同时做了校验。魔数不对说明文件根本不是这个数据frombuffer读出来的元素个数和形状对不上说明文件被截断了。这两条断言能帮你在一分钟内定位“到底是数据坏了还是代码写错了”。3.5 一次下载多项目复用同一台机器上跑多个实验没必要每个项目都存一份数据。我的做法是建一个公共数据目录比如~/datasets/FashionMNIST所有项目通过配置指向它。这样能省下几百兆空间更重要的是能保证不同实验用的是完全相同的一份数据——这对复现实验结果是必需的否则你很难解释两次结果差异是来自模型改动还是数据版本。这里有个需要注意的点某些版本在第一次运行时需要在root下写入处理完成的标记文件。如果你把公共目录设成只读第一次运行就会失败。解决办法是先用一次可写权限把数据完整跑通生成好全部产物再改成只读。之后再怎么跑都不会有问题。4. 从下载到跑通一份可以直接抄的完整流程4.1 环境与版本先把依赖对齐这套数据的来源很基础但框架本身的版本匹配是另一个常见陷阱。尤其是 torch 和 torchvision两者版本强绑定装了不匹配的组合导入时就会报undefined symbol之类的错误而且报错信息完全指不到问题根因。我的建议是直接按照对应版本的官方推荐组合一起装不要单独升级其中某一个pip install torch torchvision --index-url 与你环境匹配的官方源 pip install numpy matplotlib python -c import torch, torchvision; print(torch.__version__, torchvision.__version__)最后那行打印一定要跑确认两个版本都正常导入。很多人跳过了这一步然后在训练脚本跑了两分钟后突然崩掉白等。4.2 加载与归一化把参数算出结果再固定完整流程分两步走第一步用不带归一化的 transform 读原始数据第二步换成带归一化的 transform。这样你既能拿到统计值又能验证归一化后的数值分布是否符合预期。from torchvision import datasets, transforms IMG_SIZE 28 MEAN, STD 0.2860, 0.3530 train_tf transforms.Compose([ transforms.ToTensor(), # HWC uint8[0,255] - CHW float[0,1] transforms.Normalize((MEAN,), (STD,)), # 逐通道标准化 ]) test_tf transforms.Compose([ transforms.ToTensor(), transforms.Normalize((MEAN,), (STD,)), ]) train_set datasets.FashionMNIST(./data, trainTrue, downloadTrue, transformtrain_tf) test_set datasets.FashionMNIST(./data, trainFalse, downloadTrue, transformtest_tf)ToTensor()这一步做了两件容易被忽略的事把 HWC 布局转成 CHW这是卷积层的输入约定以及把 0–255 的整数缩放到 0–1 的浮点避免大数值把梯度炸掉。很多“训练不收敛”的问题追到根上就是漏了这一步。跑完之后强烈建议加两行检查imgs, labels next(iter(torch.utils.data.DataLoader(train_set, batch_size8))) print(imgs.shape, imgs.dtype, imgs.min().item(), imgs.max().item()) print(labels)期望看到形如torch.Size([8, 1, 28, 28])、float32数值范围大概在-0.81到2.02之间因为做了标准化超出 0–1 是正常的。如果 min 是 0、max 是 255说明 ToTensor 没生效如果形状是[8, 28, 28]说明通道维丢了。4.3 样本抽查与类别均值图三分钟确认数据没坏数据加载完先看一眼再训练这个习惯能省掉大量无用功。import matplotlib.pyplot as plt CLASSES [T-shirt, Trouser, Pullover, Dress, Coat, Sandal, Shirt, Sneaker, Bag, Ankle boot] fig, axes plt.subplots(4, 4, figsize(8, 8)) for ax, (img, label) in zip(axes.ravel(), zip(imgs, labels)): ax.imshow(img.squeeze(), cmapgray) ax.set_title(CLASSES[label], fontsize9) ax.axis(off) plt.tight_layout() plt.show()看什么三件事图是不是正常能看出衣服轮廓全黑说明解码错了、标题的类别和图片内容是否对得上对不上说明图像和标签配错了、灰度是不是有层次纯黑白二值说明被错误地做了阈值化。再进一步把每个类别的所有训练样本按像素求平均画出 10 张“类别平均图”。这个操作很朴素但信息量极大你能一眼看出哪些类别之间的平均图长得很像——那些就是模型会混的地方和第 2 节里的规律能对上。我在做任何新的图像分类任务时都会先画这一张图它对判断任务难度极其有效。4.4 DataLoader 参数几个容易踩坑的开关from torch.utils.data import DataLoader train_loader DataLoader( train_set, batch_size128, shuffleTrue, # 训练集必须打乱 num_workers0, # 先用 0 保证能跑通 pin_memoryFalse, # 有 GPU 且显存充裕时可开 drop_lastFalse, ) test_loader DataLoader(test_set, batch_size256, shuffleFalse, num_workers0)参数怎么选我给一张表参数建议值理由batch_size64 / 128 / 256太小梯度噪声大太大泛化变差128 是最稳的起点shuffle训练 True测试 False测试集打乱只会让结果无法复现num_workers先 0稳定后试 2–4数据本身小多进程收益有限反而容易在 Windows 上崩drop_last训练可 True避免最后一个不完整批次影响批归一化统计pin_memory用 GPU 时可开加速主机到显存的数据搬运注意在 Windows 上把num_workers设成大于 0多进程启动方式会变成 spawn如果你的训练代码没有放在if __name__ __main__:保护块里会直接无限递归创建进程然后崩掉。这个报错信息非常吓人但原因就是这个。4.5 一个最小可跑的基线网络基线的作用是给你一个参照系。后面任何改动都跟这个数比。我这套结构很朴素没什么花哨的东西import torch.nn as nn class SmallCNN(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), # 28 - 14 nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), # 14 - 7 nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), ) self.head nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.3), nn.Linear(128, num_classes), ) def forward(self, x): return self.head(self.features(x))为什么这么设计三层卷积的感受野从 3 逐步扩到 7 以上刚好覆盖一件衣服的主要结构每次卷积后接批归一化是为了让各层输入分布稳定这在只有 60000 张样本的情况下能显著加快收敛用自适应平均池化代替全连接参数从几十万降到几乎没有过拟合风险大幅下降。优化器用 Adam学习率 1e-3交叉熵损失训练 5 个 epoch。我实测的预期结果是第 1 个 epoch 结束测试准确率约 89%第 3 个约 91.5%第 5 个在 92% 上下。如果你的曲线明显偏离这个走势先别改模型回去检查数据和归一化。5. 踩坑记录与排查手册5.1 下载卡住、超时、断在半路现象通常是终端停在下载提示上不动或者报连接超时、读取超时。排查顺序我建议这样走。先确认是不是目录权限问题。有些环境下对目标目录没有写权限库尝试创建目录时静默失败然后一直重试看起来就像卡在下载。手动在目标路径下touch一个文件试试能不能创建成功。再确认已有文件是否完整。很多时候你以为在下载实际上raw目录里已经有一份半成品库在解压阶段卡住了。先看看raw里有什么、每个文件多大。最后才是走离线兜底手动把四个文件放到位downloadFalse。这条路是一劳永逸的我建议所有需要反复跑实验的人都提前准备好一份离线文件放在公共目录里。5.2 校验失败提示文件与预期不符这个报错信息一般会带上具体路径。逐条对照检查文件大小是否和解压后的理论值一致参考 1.3 节的表格文件是不是被截断了大小明显小于理论值.gz文件是不是真的 gzip 格式——有些下载工具在失败时会把一个错误页面保存成目标文件名文件后缀对内容是 HTML文件头和魔数对不对用 3.4 节的解析器一跑就知道提示遇到校验失败最省时间的做法是把raw目录下那一个报错的文件删掉只删这一个然后重跑。不要一上来就删整个数据目录重下那样既慢又容易把好的文件也弄没。5.3 手动放了文件它还是重新下载这是最高频的问题几乎全是目录层级或文件命名的问题。三个检查点第一层级对不对。正确是root/FashionMNIST/raw/四个文件不是root/raw/也不是root/FashionMNIST/raw/FashionMNIST/raw/。多一层少一层都会导致找不到。第二文件名对不对。四个文件名必须逐字符一致包括连字符的位置。常见的错法是train_image_idx3_ubyte下划线代替连字符、train-images-idx3-ubyte.gz多了后缀。第三放的是不是压缩包。参照 3.3 节统一放解压后的。5.4 归一化写错导致的“能跑但效果差”这类问题最折磨人因为程序不报错你只会觉得“这个模型怎么这么差”。写法现象判断方法套用手写数字的 0.1307/0.3081准确率低 1–2 个点loss 抖打印输入的最小值最大值看是否偏离零均值只做 ToTensor 不做 Normalize能收敛但需要更小学习率输入范围是 0–1均值约 0.29用了 0.5/0.5通常没事收敛略慢输入范围变成 -1 到 1训练用了一套参数测试用了另一套测试结果异常差检查两个 transform 是否一致第四条我单独拎出来说训练集和测试集的 transform 必须保持一致除了数据增强部分。我见过有人训练时加了 Normalize测试时忘了加测试准确率直接从 92% 掉到 30%然后怀疑是模型崩了。5.5 准确率卡在 91% 上不去先别急着改网络。按这个顺序排查打印混淆矩阵看错误集中在哪。如果集中在上衣四类之间那是任务本身的难点需要从特征表达上下功夫如果均匀分布多半是欠拟合——检查学习率是不是太小、epoch 是不是不够、批归一化是不是被误关了。检查训练集和验证集的准确率差距。训练 99% 测试 91%是过拟合加 dropout、加数据增强、减参数量训练 91% 测试 91%是欠拟合反过来做。检查标签和数据是否对齐。把数据和标签一起可视化几批确认没有错位。这个错误发生概率不高但一旦发生表现就是“怎么调都上不去”。5.6 几个反直觉但有价值的经验测试准确率比训练准确率高是正常的。训练时开着 dropout、批归一化用的是当前批的统计量模型处于“被削弱”状态测试时这些层切到推理模式反而发挥更完整。看到训练 90%、测试 91% 不要慌不是代码写错了。别把测试集当验证集用。这套数据只给了训练集和测试集没有独立验证集。很多人直接拿测试集反复调参调了二十次挑最好的一次这个数字已经没有参考意义了。正确做法是从 60000 张训练集里切出 5000 张做验证测试集只在最后评估一次。数据增强在这套数据上收益有限而且容易做错。灰度图做颜色抖动毫无意义随机裁剪要小心28×28 本来就没多少边距裁狠了会把袖子裁掉水平翻转对鞋子、包、裤子是合理的对某些有强方向性的款式可能引入噪声。我实测下来翻转加小幅度旋转±10 度是比较稳的组合能带来 0.5 个点左右的提升再多就容易伤到准确率。batch_size 对结果的影响比你想的大。同一套代码batch 从 128 换成 32最终准确率可能差 1 个点因为批归一化的统计稳定性变了。做对比实验时除了你要验证的那一个变量其余全部固定。5.7 常见问题速查表现象最可能的原因处理方式卡在下载不动目录无写权限 / 网络不可达检查权限改用手动放置报文件校验不符文件被截断 / 存成了错误页面删掉该文件重下或用解析器验证魔数手动放了还重新下载目录层级多套一层 / 文件名不符逐字符核对路径和文件名输入形状是 (N,28,28)缺通道维加unsqueeze(1)或检查 ToTensor训练 loss 不降忘了归一化 / 学习率太小 / 标签错位依次排查这三项测试准确率异常低训练和测试 transform 不一致统一两边的归一化参数Windows 上多进程崩溃缺__main__保护把入口代码包进保护块准确率 91% 上不去欠拟合 / 混淆集中在上衣类看混淆矩阵针对性调整6. 把它当跳板三个具体可执行的下一步跑到 92% 之后很多人会陷入迷茫——往上刷收益递减往下又觉得没意思。我给三个我自己走过的方向都是能在一两天内看到结果的。第一个方向拿同一套代码换数据。你现在这套加载、归一化、训练、评估的流程是通用的换成一个彩色数据集比如同样是 10 类但带颜色的服饰数据或者更高分辨率的数据你会发现代码几乎不用改只有Normalize的参数和输入通道数要动。这个迁移过程本身就是最好的练习——你会真正理解哪些部分是数据相关的、哪些部分是模型相关的。我建议至少做一次这个迁移做完你对整条流水线的掌控感会完全不一样。第二个方向从零训练切到迁移学习。找一个在更大图像数据上预训练的骨干网络把第一层改成单通道输入或者把灰度图复制三份变三通道冻结前面几层只训最后的分类头。你会发现即使只有 60000 张 28×28 的小图迁移学习也能把准确率推到 94% 以上而且收敛速度快得多。这里有个细节要注意预训练模型的输入归一化参数和这套数据不一样得单独处理否则效果会打折扣。第三个方向做一个能看的交互 demo。训练完的模型如果只停留在一个准确率数字上价值是有限的。把它导出成一个可以加载的文件配一个画板让人手写或者上传一张服饰图实时输出十个类别的概率条。这个过程会让你遇到一堆训练时根本不会想的问题推理时要关掉梯度、要切 eval 模式、输入要做和训练时完全一致的预处理、输出要过 softmax 才能当概率看。这些问题在真实项目里天天出现提前踩一遍比什么都值。我个人的习惯是每学一个新数据集最后一定落到一个能点开看的小东西上。纯粹盯着终端里的 loss 数字很难建立起对数据的直觉而当你亲手画的东西被模型判成“外套”的时候很多之前模糊的概念会一下子清晰起来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑