FashionMNIST数据集加载、缓存与CNN基线实战
1. 先把 FashionMNIST 这个数据集摸清楚
1.1 从手写数字到日常服饰的一次刻意升级
FashionMNIST 这个机器学习数据集,本质上是一份 28×28 像素的灰度服饰图片集合,一共 70000 张,分 10 个类别,训练集 60000 张、测试集 10000 张。它最早被设计出来的动机非常直接:原来那个手写数字数据集太容易了,随便一个两层全连接网络都能刷到 97% 以上,模型之间的差距拉不开,也测不出正则化、批归一化、数据增强这些手段到底有没有用。于是有人用同样的格式、同样的尺寸、同样的划分比例,换掉了内容,把"认数字"换成了"认衣服"。
这个替换动作看着简单,实际影响很大。数字只有 10 个形状高度规整的符号,笔画结构差异明显,而服饰类别之间存在大量视觉重叠——T 恤、衬衫、套头衫、外套这四类,在 28×28 的灰度图里往往只差一个领口轮廓或者袖子长度。这就逼着模型必须学到更有判别力的特征,而不是靠几个像素的统计量蒙混过关。
我把这个数据集推荐给两类人:一类是刚学完反向传播、想找个不占资源又能跑出有意义结果的任务练手的初学者;另一类是手上有新想法(比如某个注意力模块、某种损失函数、某套增强策略),需要一个干净的、结论可复现的小规模基准来快速验证的中级开发者。它最大的优势就是"小"——整份数据压缩后二十多兆,一张普通显卡甚至纯 CPU 都能在几分钟内跑完一轮实验,试错成本极低。
1.2 十个类别与数据的组织形式
类别标签是按固定顺序编码的 0 到 9,这个映射关系必须记牢,后面画图、看混淆矩阵、分析误判全靠它:
| 标签 | 英文类别名 | 中文含义 | 训练样本数 | 测试样本数 |
|---|---|---|---|---|
| 0 | T-shirt/top | T 恤/上衣 | 6000 | 1000 |
| 1 | Trouser | 长裤 | 6000 | 1000 |
| 2 | Pullover | 套头衫 | 6000 | 1000 |
| 3 | Dress | 连衣裙 | 6000 | 1000 |
| 4 | Coat | 外套 | 6000 | 1000 |
| 5 | Sandal | 凉鞋 | 6000 | 1000 |
| 6 | Shirt | 衬衫 | 6000 | 1000 |
| 7 | Sneaker | 运动鞋 | 6000 | 1000 |
| 8 | Bag | 包 | 6000 | 1000 |
| 9 | Ankle boot | 短靴 | 6000 | 1000 |
完全均衡,每类正好 6000 张训练图、1000 张测试图。这一点很关键:意味着你不需要为类别不平衡做任何加权采样或者重采样设计,评估时直接用准确率(accuracy)就够了,不必纠结 F1 还是 AUC。做基线对比实验时,这种"无干扰"的数据分布能省掉大量解释成本。
数据本身是单通道灰度图,像素值 0 到 255 的无符号整型。原始文件采用 idx 格式存储(这是当年为了读取方便设计的一种简单二进制格式),头部带魔数和维度信息,用标准库读取二十行代码就能搞定,不过实际工作中我们基本都用框架封装好的接口,没必要自己去解析字节流。
1.3 它到现在还能当基准用的原因
有人会问,现在都什么年代了,还用这种小图做实验?我的看法是:正因为小,它才没有被淘汰。一份数据集能不能长期充当基准,看的不是它有多难,而是它的结论是否稳定、是否容易复现、是否能快速给出反馈。FashionMNIST 在这三点上几乎是满分——数据是固定的,不存在版本漂移;划分是固定的,不存在训练测试泄漏;规模是固定的,一次实验几分钟出结果,你今天下午想到一个点子,晚上就能知道它靠不靠谱。
当然也要认清它的局限。28×28 的分辨率损失了大量细节,很多服饰的纹理信息完全丢失,导致这个数据集上的"天花板"大概在 94% 到 95% 左右,再往上基本都是过拟合测试集或者用了外部数据。所以拿它做论文主实验是不够的,但拿它做消融实验、做 sanity check、做教学演示,依然是性价比最高的选择之一。我自己有个习惯:任何新模块写完之后,先在 FashionMNIST 上跑一遍,如果连 90% 都上不去,那八成是实现写错了,不用浪费大卡时间。
2. 下载路径的三选一与选型逻辑
2.1 框架自动下载:绝大多数情况下的最优解
所谓"自动下载",指的是你在构造数据集对象的时候传一个download=True(PyTorch 风格)或者直接调load_data()(Keras 风格),框架发现本地目录里没有数据,就自己去网上拉取、解压、校验、整理成可直接迭代的格式。整个过程你不需要关心文件叫什么、放在哪、怎么解压。
我为什么说这是最优解?因为自动下载背后做了一件很多人忽略的事——完整性校验。框架在下载前会比对目标文件的校验值,如果发现文件存在但长度不对或者哈希不匹配,会自动重新拉一遍。这个机制能挡住"下载到一半网络抖动,得到一个残缺压缩包,然后训练时莫名其妙报错"这类最难排查的问题。
不过要提醒一句,自动下载依赖的是公共网络地址,在国内网络环境下偶尔会慢或者超时。所以实际工程里我一般会做两手准备:先用自动下载跑通流程,跑通之后立刻把raw目录整个备份到本地,后面所有实验都从这份备份恢复。这样既享受了自动化的便利,又不会在关键时候被网络卡住。
2.2 手动下载与目录摆放的规则
手动下载适用于两种情况:一是目标机器完全没有外网,二是你需要在多台机器之间复制数据。核心要点是目录结构必须完全符合框架的预期,否则框架会认为数据不存在,继续尝试下载。
以 PyTorch 系为例,构造对象时传入的root参数是一个父目录,框架会在它下面自动创建一个名为FashionMNIST的子目录,再在下面创建raw子目录。四个原始压缩包必须放在raw里,文件名必须严格匹配:训练图像、训练标签、测试图像、测试标签,分别对应train-images-idx3-ubyte.gz、train-labels-idx1-ubyte.gz、t10k-images-idx3-ubyte.gz、t10k-labels-idx1-ubyte.gz。少一个字母、多一个后缀、大小写不对,框架都会重新下载。
提示:手动放好文件后,把
download参数设为False再跑一次,如果能够正常读到 60000 条训练样本,说明目录结构正确;如果报"数据集未找到",那就是路径或者文件名有问题,别急着怀疑数据本身。
2.3 离线环境的搬运方案
内网训练环境是很多团队的真实场景。我的做法是分三步走:先在能上网的机器上用自动下载跑一遍,得到完整的FashionMNIST目录;然后把整个目录打包(压缩后二十多兆,邮件附件都能发),拷进内网;最后在内网机器上解压到指定路径,download设为False直接读取。
这里有个细节值得说。新版框架在首次加载时会在raw目录里额外生成一份解析后的缓存文件,把 idx 二进制转成更高效的序列化格式,后续加载直接走缓存,速度明显更快。这份缓存是跟框架版本和 Python 版本绑定的,跨大版本复制有时候会报警告。我的处理方式是:搬运时只保留四个原始压缩包,让目标机器自己生成缓存。多花十几秒,但省掉了一堆版本兼容的麻烦。
如果想进一步缩减体积,还有个更省事的办法——用 Keras 接口加载一次,它会自动把数据打包成一份 npz 缓存在用户主目录下,这份 npz 跨平台通用性更好,拷贝到别的机器上直接放到对应位置就能用,比搬 idx 文件省心。
3. 自动下载的完整实操,参数一个个拆
3.1 PyTorch 版本的逐行拆解
先看最常用的写法,代码不长,但每一行都有讲究:
import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 1) 定义预处理管线 transform = transforms.Compose([ transforms.ToTensor(), # 转张量并把 0-255 映射到 0-1 transforms.Normalize((0.2860,), (0.3530,)) # 单通道均值方差归一化 ]) # 2) 训练集:download=True 触发自动下载 train_set = datasets.FashionMNIST( root="./data", train=True, transform=transform, download=True ) # 3) 测试集:第一次会自动下载,之后命中缓存 test_set = datasets.FashionMNIST( root="./data", train=False, transform=transform, download=True ) # 4) 迭代器 train_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=2) test_loader = DataLoader(test_set, batch_size=256, shuffle=False, num_workers=2)root用相对路径在本地实验里方便,但在服务器上我建议一律写绝对路径。原因很实在:训练脚本经常被放在不同目录下用nohup或者任务调度器启动,工作目录一变,相对路径就指向别处,框架发现找不到数据就重新下载一遍,白等几分钟还占磁盘。
ToTensor()这一步做两件事,把 PIL 图像转成float32张量,同时把像素从 0-255 线性缩放到 0-1。很多人只记得它"转张量",忽略了它自带缩放,结果后面又手动除了一次 255,数据全变成 0 到 0.004 的小数,训练直接崩掉。
Normalize的两个参数是单通道的均值和标准差,常用值是 0.2860 和 0.3530,这是在这个数据集上统计出来的经验值。这里必须注意:因为数据是单通道,参数要写成只含一个元素的元组(0.2860,),漏掉那个逗号就变成了普通括号包一个浮点数,会直接报类型错误。这个坑我见过太多次了,尤其是从三通道的彩色数据任务切过来的时候。
train=True/False控制读训练集还是测试集,新版接口也支持split="train"这种字符串写法,两者等价。download=True只需要在第一次设成 True,后续设 False 也行,设 True 也不会重复下载——框架发现文件齐全且校验通过就跳过了。
3.2 Keras 版本的等价写法
TensorFlow/Keras 那边更直接,一行就拿到四个数组:
import tensorflow as tf import numpy as np (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(y_train.shape, y_train.dtype) # (60000,) int64 # 手动补上通道维并归一化 x_train = (x_train.astype("float32") / 255.0)[..., np.newaxis] x_test = (x_test.astype("float32") / 255.0)[..., np.newaxis]load_data()在首次调用时会自动下载并把四个 idx 文件打包成一份 npz 缓存在用户主目录下的.keras/datasets里,之后每次调用直接读缓存,快到几乎无感。要改缓存位置,设置环境变量KERAS_HOME就行。
注意 Keras 版本默认不做通道维扩展,x_train的形状是(N, 28, 28),直接喂给卷积层会因为缺少通道维报错。加上[..., np.newaxis]变成(N, 28, 28, 1)才能用。这个细节在教程里经常被一笔带过,新手照着抄往往卡在这一步。
3.3 目录结构与缓存复用的实测记录
跑完上面任意一段代码,./data目录下会长成这样:
data/ └── FashionMNIST/ └── raw/ ├── train-images-idx3-ubyte.gz ├── train-labels-idx1-ubyte.gz ├── t10k-images-idx3-ubyte.gz ├── t10k-labels-idx1-ubyte.gz └── (框架生成的解析缓存)我实测过几个数据点,供参考:首次自动下载在普通家用宽带下耗时大约 10 到 40 秒,取决于链路质量;四个压缩包合计约 30MB 左右,解压后约 55MB;从缓存加载训练集(60000 条)耗时约 0.3 到 0.8 秒,明显快于首次解析。
复用的原则很简单:只要root指向的目录里那份数据还在,download设什么都不影响。所以多实验共享数据的标准做法是搞一个全局数据目录,比如统一放在~/datasets下面,所有项目的root都指向它。这样一个 GB 的磁盘就用不着为二十个项目各存一份,也不存在"这个项目的数据是旧版本、那个项目是新版本"的混乱。
注意:如果你的机器上有多个 Python 环境,缓存文件可能不共享。换环境后第一次加载变慢是正常的,别以为数据丢了。
3.4 预处理与 DataLoader 的配置细节
数据下载只是第一步,真正决定实验能不能跑起来的是加载器配置。几个参数值得单独说:
batch_size在 32 到 128 之间选,这个数据集样本少,64 是我最常用的默认值,显存占用小、梯度噪声适中。如果做对比实验要求严格复现,建议固定成 64 并且记录随机种子。
shuffle=True只对训练集开,测试集必须关掉,否则你后面按索引对标签、画混淆矩阵的时候会错位,而且这种错位很隐蔽,损失值看起来正常,就是准确率偏低两个点,能查一下午。
num_workers控制子进程数量。在 Linux 上设 2 到 4 通常能加速,但在 Windows 上必须把主程序放进if __name__ == "__main__":保护块里,否则子进程会重新导入主模块,直接递归爆栈。这个坑是 Windows 用户的必修课。
pin_memory=True配合 GPU 训练能减少主机到设备的拷贝开销,数据量小的时候提升不明显,但没什么副作用,我一般顺手就开了。drop_last在训练集上可以开,避免最后一个不满批次的批归一化统计量抖动,不过 60000 能被 64 整除,这里其实不影响。
4. 数据到手后的一次基线验证
4.1 先看数据:抽样与像素统计
拿到数据之后别急着上模型,先花两分钟做个体检。把第一批 16 张图拼成网格看一眼,确认图像方向和标签对得上;再算一下全集像素的均值和标准差,验证和预设的归一化参数是否吻合。
import torch # 直接读原始数据,不做 Normalize raw = datasets.FashionMNIST(root="./data", train=True, download=False, transform=transforms.ToTensor()) loader = DataLoader(raw, batch_size=10000, shuffle=False) imgs, labels = next(iter(loader)) print("mean:", imgs.mean().item()) # 约 0.286 print("std :", imgs.std().item()) # 约 0.353如果算出来的均值明显偏离 0.286(比如变成 0.13 或者 0.5),那多半是预处理管线写错了,比如漏了缩放或者重复缩放。这个检查只要十秒钟,能挡住后面一小时的无效训练。
还可以顺手统计一下各类别的像素均值。T 恤和衬衫这类浅色衣物整体偏亮,短靴和包这类深色物品整体偏暗,如果你的模型总是把浅色类判成某一类,那可能不是模型的问题,而是它学到了"亮度"这个捷径特征。FashionMNIST 里这种捷径特征确实存在,做严谨分析的时候值得警惕。
4.2 一个够用的小 CNN 与参数量估算
基线模型不需要复杂,两个卷积块加两层全连接就够把准确率推到 91% 上下:
import torch.nn as nn class SmallCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.Conv2d(32, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 28 -> 14 nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 14 -> 7 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 7 * 7, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x))把参数量算一遍,心里有数才能判断模型是不是过大了。第一层卷积3×3×1×32加偏置 32,共 320;第二层3×3×32×32加偏置 32,共 9248;第三层3×3×32×64加偏置 64,共 18496;第四层3×3×64×64加偏置 64,共 36928。特征提取部分合计约 65k。全连接第一层输入是64×7×7 = 3136,输出 128,参数 3136×128+128 = 401536;输出层 128×10+10 = 1290。总计约 47 万参数,其中八成以上集中在那个全连接层上。
这个数字说明一件事:在这个任务上,"参数量大头在全连接层"是常态。如果你发现模型准确率上不去,先别急着加深卷积,往往是全连接之前的特征图分辨率被压得太狠了。7×7 的空间尺寸意味着每个位置要承载相当多的语义信息,对 28×28 的输入来说已经比较紧张。
4.3 训练循环与结果解读
超参就用最常见的配置:Adam 优化器、学习率 1e-3、交叉熵损失、10 个 epoch。这个组合不是最优,但胜在稳定,几乎不需要调就能出结果。
import torch.optim as optim device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = SmallCNN().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) for epoch in range(10): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() # 每轮评估一次 model.eval() correct = total = 0 with torch.no_grad(): for imgs, labels in test_loader: imgs, labels = imgs.to(device), labels.to(device) pred = model(imgs).argmax(dim=1) correct += (pred == labels).sum().item() total += labels.size(0) print(f"epoch {epoch+1:02d} test_acc={correct/total:.4f}")我用这套配置在 CPU 上跑过多次,单个 epoch 大约 20 到 30 秒,10 轮下来测试集准确率稳定落在 91% 到 92.5% 之间,波动来源主要是随机初始化。这个区间可以作为你的"健康线":跑出来低于 90%,说明代码里有 bug;跑出来高于 93%,先检查测试集是不是混进了训练流程。
这里必须强调一个最常见也最致命的错误:把测试集当验证集用。上面代码里我在每轮结束后都评估测试集,严格来说这已经是"看着测试集调模型"了。做正式对比实验时,正确做法是从 60000 张训练集里切出 5000 到 10000 张作为验证集,用它做模型选择,测试集只在最后评估一次。这个纪律在小数据集上尤其重要,因为 10000 张测试集本身就有约 0.3% 的统计波动,反复在它上面挑超参,很容易挑出"运气好"的那一个。
5. 踩坑实录:下载与加载环节的高频故障
5.1 下载中断与校验失败的处理顺序
自动下载最常见的故障是网络中途断开,留下一个不完整的压缩包。框架的校验机制通常能发现这个问题并重试,但如果重试也失败,就会抛出一句很含糊的错误,说你"数据集未找到"或者"文件损坏"。这时候的排查顺序应该是这样:
| 现象 | 最可能原因 | 处理动作 |
|---|---|---|
| 报数据集未找到,但 raw 目录里有文件 | 文件不完整(大小不对) | 删除整个 raw 目录重新下载 |
| 卡在下载阶段没有任何输出 | 链路慢,或目标地址不可达 | 换成手动下载,放到 raw 目录 |
| 报校验值不匹配 | 下载内容被代理页面替换 | 清空 raw,手动下载后再跑 |
| 下载成功但加载报维度错误 | 缓存文件来自不同版本 | 删掉raw下的缓存文件重新生成 |
| 反复下载同一份数据 | root路径每次都变 | 改用绝对路径,固定数据目录 |
我的经验是:遇到下载问题,第一步永远是删除raw目录重来。因为这个目录里可能残留半截文件、错误的缓存、旧版本的副本,你根本不知道哪个是罪魁祸首,清理干净的成本远低于逐个排查。整份数据才三十兆,重下比查错快得多。
还有一种隐蔽情况:某些企业内网会拦截下载请求并返回一个漂亮的 HTML 错误页,状态码还是 200。框架拿到这个"文件",校验必然失败,但错误信息里往往只字不提"你被拦了"。如果你在办公网环境下反复下载失败,先找个普通网页确认网络出口正常,再考虑是不是被策略拦了。
5.2 多进程、路径与跨平台的那些坑
num_workers > 0在 Linux 上一般很稳,但有几个组合要小心。一是 Windows 上不加主模块保护,子进程重新导入主脚本,会看到"RuntimeError: An attempt has been made to start a new process before..."这类提示,解决方案就是把所有执行代码包进if __name__ == "__main__":。二是 Jupyter 环境里开多进程有时候会卡死,交互式调试阶段建议先设成 0,脚本跑通之后再往上加。
路径问题同样常见。反斜杠在字符串里是转义符,"C:\data"里的\d会被解释成别的字符,程序找不到目录。用原始字符串r"C:\data"或者正斜杠"C:/data"都能避免。更稳妥的做法是统一用pathlib.Path拼接路径,跨平台从来不翻车。
磁盘空间也值得看一眼。看着数据才三十兆,但你要知道 PyTorch 生成缓存后目录会膨胀到七八十兆,如果同时保留多份副本、多个版本,几个 G 很快就没了。训练脚本跑飞的另一个常见原因就是磁盘写满,报错信息却指向别处。
5.3 缓存被污染后的识别与重建
有一次我遇到一个很怪的现象:同一个模型、同一份代码,在两台机器上跑出来的准确率差了将近三个点。查了很久最后发现,其中一台机器的测试集缓存文件被之前某次实验覆盖过——那次我为了做数据增强实验,把测试集的变换也改了,虽然只是存了索引没真的改数据,但缓存里的一些元信息对不上,导致标签映射悄悄错位。
这类问题的识别特征是:损失下降曲线看起来很健康,但准确率始终卡在一个明显偏低的水平,而且混淆矩阵里会出现"整列偏移"的规律性错误。一旦看到这种规律性,立刻怀疑标签。快速验证的办法是打印前十条样本的标签,人工对照图像确认。
重建的做法就是删掉raw下的缓存文件(保留四个原始压缩包),重新构造数据集对象让框架重新生成。养成一个习惯:任何对预处理的修改,都同步清理缓存。宁可多等三十秒,也别拿一份来路不明的缓存跑实验。
6. 把数据用透:增强组合与误判分析
6.1 适合灰度服饰图的增强组合
在 FashionMNIST 上做数据增强,思路和自然图像不太一样。彩色图常用的色彩抖动、色相偏移在这里基本没意义,因为只有单通道;而一些看似合理的几何变换,需要判断它是否会破坏类别语义。
我实际测下来比较有效的一组是这样:
train_transform = transforms.Compose([ transforms.RandomCrop(28, padding=4), # 先补边再随机裁,模拟轻微平移 transforms.RandomRotation(10), # 小角度旋转,模拟拍摄角度 transforms.RandomHorizontalFlip(p=0.5), # 左右翻转 transforms.ToTensor(), transforms.Normalize((0.2860,), (0.3530,)) ])RandomCrop(28, padding=4)会先把图补到 36×36,再随机裁回 28×28,等价于带填充的随机平移。这个增强在服饰任务上收益稳定,因为它模拟的就是"物体在画面里位置略有偏移"这一真实变化,而且不会改变类别。旋转角度控制在 10 度以内,再大就会出现鞋子朝上、衣服倒挂这种语义崩坏。
左右翻转需要斟酌。对于上衣、裤子、鞋、包这些类别,翻转后依然是同一类,安全;但对某些带有明显方向性的类别(比如连衣裙的某些款式),翻转可能造成分布偏移。整体来说在这个数据集上翻转是正收益,我自己做过对比,开启后测试准确率提升约 0.8 到 1.2 个点。
有一个增强我要特别劝阻:不要用随机擦除或者大比例裁剪。28×28 的信息密度本来就低,擦掉一块可能直接把区分 T 恤和衬衫的那点领口细节抹掉,模型只能瞎猜。这类强增强在大分辨率数据集上有效,在这里往往是负收益。
6.2 混淆矩阵与易混类别分析
训练完一定要画一次混淆矩阵,这是理解这个数据集最快的方式。根据我多次实验的结果,误判高度集中在以下几个组合上:
| 易混组合 | 典型误判方向 | 原因分析 |
|---|---|---|
| T 恤 / 衬衫 | 互相误判 | 都是上装,轮廓接近,灰度图丢失颜色与纹理 |
| 套头衫 / 外套 | 套头衫判成外套居多 | 都有袖子,区别在开襟,低分辨率下开襟线条模糊 |
| 运动鞋 / 短靴 | 互相误判 | 都是脚部鞋类,鞋帮高度差异在小图上只差几个像素 |
| 凉鞋 / 运动鞋 | 凉鞋判成运动鞋 | 鞋带与绑带结构在灰度下都是深色条纹 |
反过来,长裤、包、连衣裙这三类的准确率通常能到 97% 以上,因为它们的整体轮廓太独特了。这个分布告诉你两件事:一是模型的主体是能学到东西的,二是瓶颈集中在少数几个语义相近的类别对上。
针对这种情况,我试过两个比较有效的改进方向。一个是针对性增强,对易混类别对施加更强的平移和旋转扰动,迫使模型关注更细的局部特征。另一个是标签平滑,把目标从 one-hot 换成 0.1 的软标签,能缓解模型在易混样本上的过度自信,实测准确率有小幅提升,同时让预测置信度更有参考价值。
如果还想再往上推,可以引入类别权重或者难例挖掘,但在这份数据上收益递减很快。说实话,91% 到 93% 这个区间已经把这个数据集的绝大部分可学信息吃掉了,再花时间抠那一两个点,不如换个更大的数据集验证你的想法是否具有普适性。
还有个小技巧值得分享。分析误判样本的时候,别只看数量,把误判的图按像素均值排序看一遍。我的经验是:误判最多的样本往往集中在"极端亮度"两端——特别暗的(几乎全黑,只剩轮廓)和特别亮的(细节被冲掉)。这提示你可以在预处理阶段做一次对比度拉伸,或者用直方图均衡化,成本很低,有时候能白捡零点几个点。
我自己这几年在这个数据集上折腾下来,最大的体会是:它的价值不在于把准确率刷多高,而在于给你一个低成本的反馈回路。数据固定、结论稳定、几分钟一轮,这三点加起来意味着你可以在一晚上试完十种想法。而真正决定你项目成败的那些判断力——怎么设计对照实验、怎么识别数据泄漏、怎么从误判里读出模型的偏好——全都是在这种快速迭代里练出来的。至于下载这一步,跑通一次、备份一份、路径固定下来,后面就再也不用管它了。