ImageNet2012 devkit评测口径与索引对齐指南

发布时间:2026/10/26 4:32:59
ImageNet2012 devkit评测口径与索引对齐指南 简介ILSVRC2012_devkit_t12.tar.gz 是面向计算机视觉研究者、图像分类参赛者与深度学习实践者的开发工具包帮助读者解决类别映射、验证集标注读取、模型评估和结果提交等基础问题。压缩包共15个文件以MATLAB脚本、文本说明和mat元数据为主另有许可文件整体约2.45MB体积轻量却覆盖了挑战赛配套的核心接口。包内提供验证集标注、类别与节点映射信息以及Top-1/Top-5错误率计算、定位评估相关脚本并附带提交示例、数据格式说明、挑战规则与使用文档便于核对训练集、验证集和测试集划分复现官方评价流程减少格式与评测误差搭建可复用的训练与验证基线。已有3054人学习适合准备图像分类实验、复核模型指标或研究早期深度学习评测规范的中高级视觉学习者参考。1. ImageNet2012 devkit 拆包几 MB 的压缩包决定你的 top-1 算不算数很多人第一次跑完分类模型拿到一个 0.71 的 top-1就默认复现成功了。真到要跟论文里的数字对齐、或者把结果交上去的时候才发现差的不是模型是评测口径。ILSVRC2012_devkit_t12.tar.gz 这个几 MB 的包就是 ImageNet2012 官方发的那把尺子——它不参与训练只负责定义 1000 类的顺序、50000 张验证图的标签、以及每类到底有多少张验证图。做 ImageNet2012 devkit 相关的复现绕不开它。适合三类人要交付课程设计或竞赛结果的、要跟别人的数字对得上的、以及自己写评测脚本总觉得哪里不对的。下面按解压读盘、自建评测、训练侧对齐、避坑、进阶的顺序拆。2. 解压后的目录结构meta.mat、验证集标签与评测脚本各管什么2.1 四个组成部分的分工与数据流向解压出来是一个ILSVRC2012_devkit_t12/目录进去之后结构很朴素data/和evaluation/两块外加一个 README 和一个版权说明文件。我第一次拆的时候有点失望——就这么点东西但恰恰是这点东西决定了后面所有数字的可信度。把每个文件的作用列清楚比急着跑脚本有用得多路径格式作用常见翻车点data/ILSVRC2012_meta.matMATLAB v5 结构体1000 类的官方顺序WNID、每类验证样本数、WordNet 层级信息scipy 读出来是多层嵌套直接下标会取错字段data/ILSVRC2012_validation_ground_truth.txt纯文本50000 行验证集标签取值 1..1000是 1-based不是 0-baseddata/下的同目录辅助文件文本文件名与标签的对齐关系有人用os.listdir的顺序去对直接整体错位evaluation/main.py、evaluation/ILSVRC2012_eval.mPython / MATLAB官方 top-1 / top-5 评测年代久远参数名和依赖要自己确认数据流向其实是一条直线图片文件名里的编号 → ground truth 文件的行号 → 一个 1..1000 的标签 → 通过 meta.mat 里 synsets 的顺序映射成 WNID。任何一环错位最后的数字都会莫名其妙地低几个点而且低得很均匀看起来特别像模型欠拟合特别有迷惑性。我一般会先把这四个东西当成四条独立的证据链来验证meta 里的类数是不是 1000、ground truth 是不是正好 50000 行、标签最大值是不是 1000、最小值是不是 1。四个都对了再往下写评测代码。这个习惯帮我省过至少两次通宵。2.2 用 scipy 把 ILSVRC2012_meta.mat 读成能用的 synset 列表.mat是 MATLAB 的格式Python 侧用scipy.io.loadmat读。坑在于它默认保留 MATLAB 的维度习惯读出来是个(1, 1000)的对象数组每个元素又是一个 struct 数组你得一层层往下剥才能拿到 WNID。与其记住剥几层不如改两个参数让 scipy 直接把 struct 转成可点号访问的对象import scipy.io as sio import numpy as np META ILSVRC2012_devkit_t12/data/ILSVRC2012_meta.mat # squeeze_meTrue: 去掉 MATLAB 留下的多余维度 (1,1000) - (1000,) # struct_as_recordFalse: struct 变成对象可以用 s.WNID 访问而不是 s[0][1][0] meta sio.loadmat(META, squeeze_meTrue, struct_as_recordFalse) synsets meta[synsets] # (1000,) 的 struct 对象数组 wnids [str(s.WNID) for s in synsets] # 官方类顺序 words [str(s.words).strip() for s in synsets] # 人可读类名调试时有用 num_val np.asarray(meta[num_validation_images_per_class]).astype(int) # 四条自检类数、唯一性、首类、验证集总数 assert len(wnids) 1000 assert len(set(wnids)) 1000 assert wnids[0] n01440764 # 官方第一类十个鱼种之一 print(num_val sum , num_val.sum()) # 应该能对上 50000参数说明squeeze_meTrue负责把那些(1, N)的退化维度压掉struct_as_recordFalse负责把 MATLAB 的 struct 转成属性可访问的对象。这两个一起开synsets就变成一个长度 1000 的一维数组s.WNID、s.ILSVRC2012_ID、s.words都能直接点出来。只开其中一个你会得到形如synsets[0][i][0][1][0]的下标地狱写一次忘一次。这里wnids的顺序极其重要——它就是官方的类别索引顺序第 0 位对应标签 1第 999 位对应标签 1000。你后面所有的 softmax 输出、所有的混淆矩阵、所有的 per-class accuracy都必须按这个顺序解释。千万不要用sorted(os.listdir(train))去重建这个顺序两回事。2.3 验证集标签与文件名编号的对应关系ILSVRC2012_validation_ground_truth.txt是纯文本一行一个整数第 N 行对应ILSVRC2012_val_{N:08d}.JPEG。这个对应关系是隐式的、靠行号维持的文件里没有文件名列所以一旦你打乱了读入顺序标签就全乱了。GT ILSVRC2012_devkit_t12/data/ILSVRC2012_validation_ground_truth.txt with open(GT) as f: gt np.array([int(line) for line in f if line.strip()], dtypenp.int64) assert gt.shape (50000,) assert gt.min() 1 and gt.max() 1000 # 1-based这是关键 # 取第 4371 张验证图的标签 idx 4370 # 0-based 下标 fname fILSVRC2012_val_{idx 1:08d}.JPEG # 拼回 1-based 编号 label gt[idx] print(fname, label)这里的1和-1看着啰嗦但它们是整套流程里最容易出错的地方。我的做法是代码里统一用 0-based 下标做数组运算只在两个边界上做转换——拼文件名时idx 1比对标签时pred_idx 1。这两个转换点写死了后面就不会到处飘。另外提醒一句验证集图片是扁平的 50000 个 JPEG 文件堆在一个目录里没有子目录。这意味着你os.listdir拿到的顺序是文件系统给的不是编号顺序。虽然大多数文件系统在文件数量不多时会返回接近创建顺序的结果但这件事没有任何保证。正确姿势是用编号现拼文件名或者读完之后按文件名里的数字排序别赌。3. 自建评测流水线从 50000 张 JPEG 到可信的 top-1 / top-53.1 把模型输出对齐到官方 1000 类索引模型输出的第 i 维到底对应官方第几类这个问题在你自己从头训的时候是自洽的在加载别人预训练权重的时候就未必了。常见的索引来源有三种按 WNID 字母序排的、按官方 synsets 顺序排的、按某个框架内置的CLASSES列表排的。前两种在 ImageNet 上刚好不一致这就是精度凭空掉几个点的经典原因。对齐的办法是把你的模型第 i 维是什么 WNID显式写出来然后跟官方顺序做一次置换# official_wnids: meta.mat 里的顺序长度 1000 official_wnids wnids pos {w: i for i, w in enumerate(official_wnids)} # model_wnids: 你的权重训练时的类顺序从权重包里读出来 # 这里以按字母序为例实际以你那份权重的文档为准 model_wnids sorted(official_wnids) # perm[i] 官方第 i 类在你的模型输出里是第几维 perm np.array([model_wnids.index(w) for w in official_wnids], dtypenp.int64) def to_official_order(logits): 把模型输出重排成官方 synset 顺序形状 (N, 1000) return logits[:, perm]perm这个数组是整套流程的核心产物我一般会把它np.save落盘跟评测脚本放一起版本对不上就重新生成。逻辑说明perm[i]回答的是官方第 i 类在模型输出里的位置所以用logits[:, perm]取出来之后新数组的第 i 列就是官方第 i 类。方向搞反的话你会得到一个形状对、数值荒诞的结果通常表现为 top-1 在 0.1% 上下。如果你加载的是 ImageNet 预训练权重先别急着算精度先做一次肉眼对齐挑 8 到 10 张你认识的图比如一张金毛、一张斑马、一张咖啡杯跑一遍模型把 top-1 的索引转成words里的类名打印出来。名字对得上映射就是对的。这步花五分钟能省掉后面几小时的自我怀疑。3.2 一个不依赖官方脚本的评测器官方evaluation/main.py是很多年前写的基于旧版 argparse 和旧版 numpy不同环境下跑起来参数名、依赖都可能需要自己确认用python main.py -h看一眼最稳。我现在的做法是官方脚本用来做交叉验证日常实验全部走自己这三十行可控、可改、不依赖环境。import numpy as np def evaluate(scores, gt, topk(1, 5)): scores: (50000, 1000) 已按官方 synset 顺序排好的分数或 logits gt: (50000,) 的 1-based 标签 assert scores.shape (50000, 1000), scores.shape assert gt.shape (50000,) # argsort 降序取前 k 个位置再 1 转回 1-based 与 gt 同尺度 order np.argsort(-scores, axis1) result {} for k in topk: topk_idx order[:, :k] 1 hit (topk_idx gt[:, None]).any(axis1) result[ftop{k}] hit.mean() return result res evaluate(scores, gt) for k, v in res.items(): print(f{k}: {v * 100:.2f}%)逻辑说明np.argsort(-scores)拿到降序排列的下标第 0 列就是模型认为最可能的类。 1是把 0-based 下标抬到 1-based 标签空间这一步漏掉的话全表标签会整体偏移一位精度会掉到一个看起来像模型没训好的水平——通常是 top-1 掉 1 到 2 个点top-5 掉得更多特别像过拟合不足非常能骗人。参数说明topk里放你要的几个 k官方看的是 1 和 5。scores用 logits 还是 softmax 后的概率都行argsort对单调变换不敏感。内存上argsort对 50000×1000 的 float32 会临时开出约 200MB 的 int64机器紧张就分块算或者改用np.argpartition(order-scores, kth5)先切出前 5 再排序能省一半内存。如果你想直接吃官方的预测文件格式每行文件名 WNID补一个转换器就够了def load_official_pred(path, wnid_to_pos, num50000): 把 ILSVRC2012_val_00000001.JPEG n01440764 这种行转成 (50000, 1000) 的 one-hot scores np.zeros((num, 1000), dtypenp.float32) with open(path) as f: for line in f: parts line.split() if len(parts) ! 2: continue fname, wnid parts idx int(fname.split(_)[-1].split(.)[0]) - 1 # 转 0-based scores[idx, wnid_to_pos[wnid]] 1.0 return scores这个函数的两个参数要留意wnid_to_pos必须是官方 synset 顺序建出来的映射表不是字母序num默认 50000如果你的预测文件不是全量比如只测了子集要相应调整否则会静默地按全量算分母精度会被稀释得很难看。3.3 批量推理时的内存与 I/O 控制50000 张 JPEG 不算多但踩过的坑不少。最典型的是把所有图一次性读进内存再stack224×224×3 的 uint8 一张 150KB50000 张就是 7.5GB加上 float32 转换直接翻四倍16GB 的机器当场就卡死了。正确做法是流式读、按批转、只保留 logits。import cv2 import numpy as np BATCH 64 MEAN np.array([0.485, 0.456, 0.406], dtypenp.float32) STD np.array([0.229, 0.224, 0.225], dtypenp.float32) def load_batch(img_dir, start, end, size224): 读 [start, end) 这一段验证图返回 (N, 3, size, size) 的 float32 batch [] for i in range(start, end): path f{img_dir}/ILSVRC2012_val_{i 1:08d}.JPEG img cv2.imread(path, cv2.IMREAD_COLOR) if img is None: raise FileNotFoundError(path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 短边缩到 256 再中心裁 224这是 ImageNet 评测的通行做法 h, w img.shape[:2] scale 256 / min(h, w) img cv2.resize(img, (int(round(w * scale)), int(round(h * scale)))) h, w img.shape[:2] top, left (h - size) // 2, (w - size) // 2 img img[top:top size, left:left size] img (img.astype(np.float32) / 255.0 - MEAN) / STD batch.append(img.transpose(2, 0, 1)) return np.stack(batch)参数说明cv2.IMREAD_COLOR强制三通道能挡掉一部分灰度图导致transpose维度对不上的问题cvtColor是必须的OpenCV 读进来是 BGR不转的话精度会掉一点点而且掉得不明显属于典型的玄学问题256 → 224的中心裁剪口径要和训练时保持一致训练用 random crop 也许无所谓评测必须用确定性的中心裁剪。还有一点记得cv2.setNumThreads(4)之类的限制一下OpenCV 默认会开满所有核跟 DataLoader 的 worker 抢 CPU实际吞吐反而下降。我一开始不信测了才发现单进程读图比八进程还快因为八个进程各自开满线程互相打架。4. 训练集侧的对齐wnid 目录、映射表与一次性的防错设计4.1 train 目录名到类索引的两条路径训练集那边是另一套结构ILSVRC2012_img_train/下面 1000 个以 WNID 命名的子目录每个子目录里是该类的 JPEG。很多人用torchvision.datasets.ImageFolder直接加载它按目录名的字母序生成classes列表索引自然就是字母序的。这时候如果评测按官方 synset 顺序解释输出两套索引不匹配精度会掉到几乎随机——而且是看起来模型完全没学会的那种崩法很容易被误判成训练崩了。import os TRAIN ILSVRC2012_img_train train_wnids sorted(os.listdir(TRAIN)) # ImageFolder 的字母序 # 检查一下两套顺序到底差多少 diff sum(1 for a, b in zip(train_wnids, wnids) if a ! b) print(f1000 类里有 {diff} 类的字母序位置与官方序不同)跑一下这个diff你会发现数字大得惊人——绝大多数类的字母序位置都和官方序对不上。所以这件事不是可能有影响是一定有影响。解决路径有两条。第一条是把训练时的classes直接改成官方顺序加载器按映射重排第二条是训练照旧用字母序仅在评测入口做一次置换。我更倾向第二条因为它把训练和评测解耦了权重文件不用跟着评测口径变。代价是你必须始终带着perm这个数组走。4.2 落一份能到处复用的 label 映射文件映射这种东西写在某个脚本里迟早会丢最稳的是生成一次、落盘一次所有脚本都读同一份。我用一个.npz装三样东西官方 WNID 顺序、类名、每类验证样本数再加一个perm.npy装字母序到官方序的置换。import json import numpy as np # 1) 官方口径 np.savez(imagenet2012_labels.npz, wnidsnp.array(wnids), # 官方顺序 wordsnp.array(words), num_valnum_val) # 2) 训练侧字母序 - 官方序 的置换ImageFolder 场景 perm np.array([train_wnids.index(w) for w in wnids], dtypenp.int64) np.save(imagenet2012_perm_alpha2official.npy, perm) # 3) 顺手留一份人可读的 JSON方便肉眼查 with open(imagenet2012_labels.json, w, encodingutf-8) as f: json.dump({wnids: wnids, words: words}, f, ensure_asciiFalse, indent2)参数说明perm[i]的含义是官方第 i 类在字母序里排第几位所以把模型输出重排到官方序用的是logits[:, perm]。反过来如果你要的是字母序第 i 类在官方序里的位置那就是np.argsort(perm)。这两个别弄反弄反了精度看着正常但就是不如别人。落盘之后所有下游脚本里只写一行perm np.load(imagenet2012_perm_alpha2official.npy)谁也别再自己去sorted(os.listdir(...))。这条规矩执行下来我这边跨项目复现时的对齐问题基本绝迹了。4.3 验证映射正确性的笨办法映射写完必须验而且要用不依赖映射本身的方式验。我的办法是从验证集里随便挑 20 张图把预测的 top-1 类名、真实标签对应的类名打印出来人眼看。对得上就继续对不上就查perm的方向。def sanity_check(scores, gt, wnids, words, n20): 抽样打印人眼核对映射是否正确 wrong 0 for i in range(0, 50000, 50000 // n): pred int(np.argmax(scores[i])) # 官方序下标 true int(gt[i]) - 1 # 1-based - 0-based ok OK if pred true else BAD if pred ! true: wrong 1 print(f{ok} val_{i 1:08d} 真:{words[true][:40]:42s} 测:{words[pred][:40]}) print(f抽样 {n} 张错 {wrong} 张)一个已经训好的 ResNet-50 在验证集上的 top-1 大概在 76% 左右抽 20 张理论上应该错 4 到 5 张。如果你抽 20 张错了 18 张那不是模型的问题是映射的问题别往下调参了。这个笨办法我用了很多次每次都能在几分钟内定位到是索引错乱还是权重加载失败。5. 避坑与排查五个让精度悄悄变低的隐蔽问题5.1 现象top-1 稳定在 0.1% 左右loss 却收敛得很好原因1-based 与 0-based 混用。模型输出是 0..999ground truth 是 1..1000直接argmax gt永远不会相等但只差一位训练侧完全看不出来。解决所有比较统一抬到 1-basedpred 1 gt或者统一压到 0-basedgt - 1 pred。二选一写在一个地方别让两种口径在代码里共存。我现在的惯例是入口处压成 0-based出口处抬回 1-based就两个转换点。5.2 现象cv2.imread返回None或者返回的数组是两维的原因验证集里存在少量灰度图、CMYK 图、以及个别损坏的 JPEG。imread对损坏文件返回None对灰度图返回(H, W)后面transpose(2,0,1)直接崩。解决读的时候显式cv2.IMREAD_COLOR读完判空并且判ndim 3不合格的图用一个均值图占位并记录下来。50000 张里通常只有个位数有问题占位对整体精度的影响在 1e-4 量级可以接受但一定要打日志别静默吞掉。5.3 现象跑官方main.py报AttributeError: module numpy has no attribute float原因官方脚本是旧版 numpy 时代写的用了np.float、np.int这类已被移除的别名。解决要么装一个旧版 numpy 的虚拟环境跑官方脚本要么在脚本开头加两行兼容垫片np.float float; np.int int。我倾向后者快而且不影响主环境就是每次看到那两行都知道这是在给老代码擦屁股。5.4 现象换了台机器同一份权重精度掉了 3 个点原因图片读取顺序变了。有人用os.listdir或glob拿到文件列表就直接喂模型文件系统返回的顺序在不同机器、不同挂载方式下并不一致而标签是按行号固定的。解决永远用编号现拼文件名fILSVRC2012_val_{i 1:08d}.JPEG不要遍历目录。如果必须遍历遍历完按文件名里解析出的数字排序不要按字符串排——虽然补零到 8 位后字典序和数字序一致但依赖这个巧合本身就是隐患。5.5 现象验证集上算出的 per-class accuracy 有若干类明显偏低原因分母用错了。有人默认每类 50 张硬编码50做分母或者用len(gt[gt c])算完发现某些类数量不是 50 就慌了。解决分母永远从gt现算np.bincount(gt, minlength1001)[1:]一行搞定。meta.mat 里的num_validation_images_per_class是拿来做交叉校验的两边对不上说明你的标签文件被动过那就要先查数据来源而不是继续调模型。6. 进阶把 devkit 当校验位用以及一份评测快照的习惯走到这一步评测脚本能跑、精度看起来也合理。但真正让我少走了很多弯路的是一个额外动作把 devkit 当成数据完整性的校验位而不是只当标签来源。具体做法是写一个自检函数每次开始新实验前跑一遍十几秒的事def audit_devkit(meta_path, gt_path, img_dir): 开跑之前先审一遍数据任何一条不过就别往下走 meta sio.loadmat(meta_path, squeeze_meTrue, struct_as_recordFalse) wnids [str(s.WNID) for s in meta[synsets]] num_val np.asarray(meta[num_validation_images_per_class]).astype(int) with open(gt_path) as f: gt np.array([int(x) for x in f if x.strip()], dtypenp.int64) checks { 类数1000: len(wnids) 1000, 标签数50000: gt.shape[0] 50000, 标签范围 1..1000: gt.min() 1 and gt.max() 1000, 每类数量与 meta 一致: np.array_equal(np.bincount(gt, minlength1001)[1:], num_val), 首末图存在: os.path.exists(f{img_dir}/ILSVRC2012_val_00000001.JPEG) and os.path.exists(f{img_dir}/ILSVRC2012_val_00050000.JPEG), } for name, ok in checks.items(): print((PASS if ok else FAIL), name) return all(checks.values())这个函数最有价值的一条是每类数量与 meta 一致。它把 meta.mat 里的num_validation_images_per_class和实际标签分布做了对撞任何一个类的数量对不上就直接 FAIL。这意味着如果你手里的裸图包来自二次分发、有人删过坏图、或者标签文件被截断过这一步就能当场拦下来而不是等你训完三天发现精度怎么都上不去。另一件事是评测快照。每次算完精度我会把这次的配置落一个小 JSON用了哪份权重、perm的哈希、图片尺寸、裁剪方式、归一化参数、top-1 和 top-5。看起来啰嗦但这东西是后悔药——两个月后你看到一个 0.761 的数字想不起当时是 224 中心裁剪还是 256 全图缩放翻一眼快照就知道了。我吃过这个亏两组实验差 0.8 个点查了两天才发现是归一化均值写成了 BGR 的顺序权重和脚本都没问题就是那三个数字的位置错了。从那以后我每次开新实验audit_devkit和配置快照都强制走一遍不管多急。听上去是仪式感实际上是把自己从精度不对就先怀疑模型的思维定式里拽出来——大部分时候问题在数据口径不在网络结构。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询