简介:ILSVRC2012_devkit_t12.tar.gz 面向计算机视觉研究者、深度学习参赛者以及需要复现 ImageNet 2012 图像分类实验的开发者,提供挑战赛配套开发工具与评估基础设施。压缩包共 15 个文件,以 MATLAB 脚本和文本说明为主,另含类别元数据与许可文件,整体约 2.45MB;其中脚本覆盖评估入口、哈希校验、XML 解析、预测示例与结果核对等环节,文本则用于说明数据格式和评测流程,重点在元数据、评估程序与提交规范。已有 3054 人学习或下载,适合作为入门 ILSVRC、理解官方评测口径的轻量参考。使用者可借此梳理训练集、验证集与测试集的划分方式,掌握 Top-1/Top-5 错误率的计算逻辑,并借助随包代码完成预测结果格式检查、ground truth 对照和本地评估,为复现 AlexNet 等经典模型、排查提交错误与搭建规范化实验流程提供直接帮助。
1. 一个 3MB 的包决定验证集能不能用:ILSVRC2012 devkit 的定位
图像数据早就下完了,train 的 1000 个 tar 解完、val 的 5 万张 JPEG 也放好了,模型一跑,验证集准确率不是 0.1% 就是 99.9%。反复查了数据加载器和归一化参数,都没问题,最后发现原因在于编译链之外:ImageNet2012 devkit 这个只有几 MB 的 tar.gz 一直被扔在下载目录里没有解压。它体积不大,却装着 synset 词表、验证集 50000 条 ground truth 和一套官方评估脚本;没有它,那 5 万张图就只是没有标签的 JPEG,任何按文件名猜类别的做法都只是看似对上了,实际上全错。这篇按安装流程记录一遍:版本怎么选、包解到哪、meta.mat 怎么读、标签错位怎么救,适合刚接手 ILSVRC2012、或者要把旧环境搬到新机器上的人。
2. 运行时选型与依赖安装:Python 3.10 + NumPy 1.26 这套组合怎么定
2.1 先决定用哪条链路读 devkit
打开 devkit 的目录你会发现它内部结构很固定:data/放两个纯数据文件(一个.mat、一个.txt),evaluation/放一套 MATLAB 脚本,根目录一个 README。这个结构决定了你有两条读取链路可选,而且两条链路的依赖完全不一样。
Python 主链路:SciPy 的scipy.io.loadmat直接读meta.mat,内置open()读那个 txt,NumPy 负责数组对齐。训练框架只要求「图片路径 + 一个 0 到 999 的整数标签」,这条链路足够,装三个包就能跑通。我 90% 的场景都走这条。
MATLAB 旁路:只在两种情况下需要。一是要复现官方评估脚本的打分口径,二是手上有一段老代码依赖 devkit 里那个 VOC 编号转 ILSVRC 编号的转换函数。旁路不必和主链路装在同一台机器上,也不必让主链路的 Python 环境去迁就 MATLAB 的版本。
先把这两条链路分开的意义在哪?在于版本冲突。MATLAB 那边对 Python 一无所知,Python 那边也不需要 MATLAB Runtime。我见过有人为了「一次装全」,把 MATLAB 的 Python 引擎装进训练环境的 venv,结果numpy被引擎依赖强行升到 2.x,一堆老脚本直接崩。分开装,一次都不用修。
提示:如果你的目标只是把 ILSVRC2012 跑起来训练,不要装 MATLAB。
evaluation/目录留着不碍事,它不参与数据读取。
2.2 依赖安装与版本锁定
Python 侧我一般钉三个版本:numpy、scipy、Pillow。选 Python 3.10 不是因为它新,是因为它同时满足「老库还发 wheel」和「新库还支持」这两个条件——3.8 已经有一批包不再更新,3.12 又有一些编译型依赖需要现场编。3.10 或 3.11 都行,我更常用 3.10。
# 建独立环境,别把 devkit 的依赖塞进系统 Python python3.10 -m venv /opt/envs/ilsvrc2012 source /opt/envs/ilsvrc2012/bin/activate # pip 先升一版,老 pip 解析 wheel 标签会挑错包 python -m pip install --upgrade pip # 三个包全部钉死版本,CI 和本地保持一致 python -m pip install \ "numpy==1.26.4" \ "scipy==1.11.4" \ "Pillow==10.2.0"numpy钉在 1.26 而不是 2.x,原因是血泪经验:不少读meta.mat的老脚本里还写着np.object、np.int、np.float这类别名,NumPy 2.0 把这些别名彻底移除,一 import 就直接AttributeError。scipy.io.loadmat本身在 1.11 和 1.13 上行为一致,差别主要在返回的结构体对象上,钉 1.11 是为了跟 1.26 的 NumPy 搭配经过验证。Pillow不是读 devkit 必需的,但后面验证环节要抽检图片能不能打开、后缀是不是.JPEG,装上省事。
| 依赖 | 钉住版本 | 作用 | 换成别的会怎样 |
|---|---|---|---|
| Python | 3.10 | 运行时 | 3.12 部分编译包要现场编,3.8 部分 wheel 停更 |
| NumPy | 1.26.4 | 数组与结构化字段 | 2.x 移除np.object等别名,老脚本报错 |
| SciPy | 1.11.4 | 读 v5 格式.mat | 版本差异主要在结构体返回形态 |
| Pillow | 10.2.0 | 抽检 JPEG 完整性 | 不装也能跑,只是少一道校验 |
如果你要用 MATLAB 旁路,版本上我一般要求 R2018a 及以上。devkit 的meta.mat是老的 v5 格式,MATLAB 从很早的版本就能load,但evaluation/里那套脚本用了一些较新的字符串和表格处理函数,太老的版本会在函数名上报 undefined。Octave 可以跑通一部分,但字符串处理和containers.Map的行为跟 MATLAB 有出入,我不建议把 Octave 当作评估脚本的主力环境,最多用来快速看一眼数据。
2.3 装完先跑三行自检
环境装完不要急着解压数据,先花十秒确认这三个包能互相认出来:
python - <<'PY' import numpy, scipy, PIL from scipy.io import loadmat # 打印版本,确认没有被其他环境串味 print("numpy :", numpy.__version__) print("scipy :", scipy.__version__) print("pillow :", PIL.__version__) # 确认 loadmat 是可调用的,而不是被同名文件遮蔽 print("loadmat:", callable(loadmat)) PY这段脚本有两个作用。第一行确认版本号和你要钉的一致,尤其在多环境机器上,which python和python -c "import sys; print(sys.executable)"经常指向不同的东西。第二行确认loadmat是真函数——有些项目根目录里放了一个叫scipy.py的调试文件,import 的时候会把它当模块,报出来的错会非常离奇。
自检通过之后再看一眼解释器路径,把它记到部署脚本的注释里,下次环境对不上时能直接比对。
3. 解压与目录布局:devkit 和 train/val 该摆成什么样
3.1 解压前先列表,别直接 -xzf
拿到ILSVRC2012_devkit_t12.tar.gz之后,第一件事不是解压,是看它里面到底有没有顶层目录:
# -t 只列目录不解压,确认顶层是不是 ILSVRC2012_devkit_t12/ tar -tzf ILSVRC2012_devkit_t12.tar.gz | head -20 # 顺手看一眼文件数量,正常是十几个文件,不是几千个 tar -tzf ILSVRC2012_devkit_t12.tar.gz | wc -l为什么要先列?因为有的整理包会把ILSVRC2012_devkit_t12/这一层剥掉,直接以data/、evaluation/开头。如果你不检查就tar -xzf ... -C /data/imagenet/ILSVRC2012/,解出来的data/会和别的东西混在同一层,后面所有相对路径全要重写。先-t看一眼,成本两秒,省掉半小时。
确认顶层目录存在之后,再决定解到哪。我习惯让 devkit 和图像目录平级:
# 建根目录,后面的 train/val 都放这一层 mkdir -p /data/imagenet/ILSVRC2012 # -C 指定目标目录,-z 解 gzip,-x 解包,-f 指定文件,顺序不能乱 tar -xzf ILSVRC2012_devkit_t12.tar.gz -C /data/imagenet/ILSVRC2012/ # 确认解出来的层级对不对 ls -l /data/imagenet/ILSVRC2012/ILSVRC2012_devkit_t12/-C这个参数是后面所有路径问题的源头。解到/data/imagenet/ILSVRC2012/之后,devkit 的完整路径是/data/imagenet/ILSVRC2012/ILSVRC2012_devkit_t12/,数据文件就是它下面的data/meta.mat。如果你把 devkit 解到/opt或者用户家目录,而图像放在/data,那代码里就得写两套绝对路径,迁移机器时必炸。
注意:解压完成后先
du -sh看一下占了多少空间。devkit 本身很小,如果你看到解出来几个 G,说明拿错包了,那多半是图像压缩包。
3.2 目录结构对照表
解完之后,整个 ILSVRC2012 的目录我一般摆成下面这样。这张表建议直接对着核一遍,缺一项后面就会在某个环节报错:
| 路径 | 内容 | 必需性 |
|---|---|---|
ILSVRC2012/ILSVRC2012_devkit_t12/data/meta.mat | 1000 类 synset 元信息 | 必需 |
ILSVRC2012/ILSVRC2012_devkit_t12/data/ILSVRC2012_validation_ground_truth.txt | 50000 条验证集标签 | 必需 |
ILSVRC2012/ILSVRC2012_devkit_t12/evaluation/ | MATLAB 评估脚本 | 选装 |
ILSVRC2012/ILSVRC2012_devkit_t12/README | 官方说明 | 参考 |
ILSVRC2012/ILSVRC2012_img_train/ | 解压后的 1000 个类别子目录 | 训练必需 |
ILSVRC2012/ILSVRC2012_img_val/ | 50000 张验证集 JPEG | 验证必需 |
有个容易误解的点:devkit 里没有验证集图像,也没有训练集图像。它只提供「答案」和「答案的编号规则」,图像要另外下载。所以如果你手上只有这个 tar.gz,能做的只是把词表和标签准备好,跑不起来任何训练。这也是为什么很多人第一次接触时以为包下错了。
3.3 图像目录的两种摆法,选一种就别改
训练集的摆法有两种,取决于你用不用框架自带的ImageFolder之类按目录读类的加载器。
第一种是保留原始 tar 解压出来的 1000 个子目录,每个子目录名就是 WNID(形如n01440764),里面是一堆n01440764_xxxxx.JPEG。这种摆法对ImageFolder友好,但目录数多,ls会刷屏。
第二种是全部摊平到一个目录,靠文件名前缀区分类别。这种摆法对自定义 Dataset 友好,但 128 万张图放一个目录,ls和某些文件系统都会很难受。
# 方案一:保留 WNID 子目录(推荐,别改) # 假设 train 的 1000 个 tar 已经逐个解到 ILSVRC2012_img_train/ 下 ls /data/imagenet/ILSVRC2012/ILSVRC2012_img_train/ | head -3 # 期望输出类似 n01440764 n01443537 n01484850 # 方案二:摊平(只在自定义 Dataset 明确要求时用) # find 会把所有 JPEG 拍到一个层级,注意这一步不可逆,先备份目录结构 find /data/imagenet/ILSVRC2012/ILSVRC2012_img_train \ -name "*.JPEG" -exec mv -t /data/imagenet/ILSVRC2012/flat_train/ {} +方案一里的 WNID 是 WordNet 的同步集编号,和meta.mat里的WNID字段一一对应。方案二摊平之后,你就只能靠文件名的前缀字符串去反查类别,多了一层字符串解析,出错概率更高。我建议除非框架强制,否则保留子目录。
验证集目录没有子目录,5 万张图平铺,文件名从ILSVRC2012_val_00000001.JPEG到ILSVRC2012_val_00050000.JPEG。这个命名规律是整个验证集标签对齐的基础,第 4 章会专门处理它。
权限上,如果训练进程不是当前用户跑的,记得给目录加读权限,别等到 DataLoader 报PermissionError才回头改:
# 给所有用户读权限,训练容器挂载时不会因为 uid 不同读不到 chmod -R a+rX /data/imagenet/ILSVRC2012 # 确认关键文件可读 test -r /data/imagenet/ILSVRC2012/ILSVRC2012_devkit_t12/data/meta.mat && echo "meta.mat 可读"4. 读 meta.mat 与验证集标签:三条命令把 1000 类词表和 50000 条答案拉出来
4.1 meta.mat 的结构:synsets 数组到底长什么样
meta.mat是 MATLAB 的 v5 格式,用loadmat读进来是个 dict。里面真正有用的只有一个 key:synsets。它是一个长度为 1000 的结构体数组,每个元素带这些字段:
| 字段 | 含义 | 用法 |
|---|---|---|
ILSVRC2012_ID | 1 到 1000 的连续编号 | 验证集 txt 里的数字就是这个 |
WNID | WordNet 同步集编号,形如n01440764 | 对应训练集子目录名 |
words | 类别英文描述 | 人工核对时看 |
num_children | 子节点数 | 一般不用 |
children | 子节点列表 | 一般不用 |
wordnet_height | 在 WordNet 中的层级 | 一般不用 |
num_train_images | 该类训练图数量 | 做采样权重时有用 |
直接loadmat不加参数,返回的每个字段都是嵌套的(1,1)数组,取值要写synsets[0][0]['ILSVRC2012_ID'][0][0]这种套娃形式。参数一加就干净了:
from scipy.io import loadmat # squeeze_me 把 (1,1) 的嵌套数组压成标量 # struct_as_record=False 让结构体变成可以用点号访问的对象 meta = loadmat( "/data/imagenet/ILSVRC2012/ILSVRC2012_devkit_t12/data/meta.mat", squeeze_me=True, struct_as_record=False, ) synsets = meta["synsets"] # 形状 (1000,) 的对象数组 first = synsets[0] print(len(synsets)) # 1000 print(int(first.ILSVRC2012_ID)) # 1 print(str(first.WNID)) # n01440764 print(str(first.words)) # tench, Tinca tinca这两个参数是整套读取逻辑的关键。squeeze_me=True解决的是 MATLAB 里所有标量都存成 1×1 矩阵的问题,不加就会到处[0][0]。struct_as_record=False解决的是结构体访问方式,加了之后synsets[0]是一个对象,字段直接点出来;不加的话synsets[0]是numpy.void,只能像字典一样按键取。
把 ID 到 (WNID, words) 的映射建好,后面反复用:
# 一次构建,全局复用;key 是 1..1000 的整数 id2meta = {} for s in synsets: idx = int(s.ILSVRC2012_ID) id2meta[idx] = (str(s.WNID), str(s.words)) # 抽查首尾两个,确认编号连续、没有空洞 assert sorted(id2meta.keys()) == list(range(1, 1001)), "ID 不连续" print(id2meta[1]) # ('n01440764', 'tench, Tinca tinca') print(id2meta[1000]) # 最后一条,类别名以实际文件为准那个assert很重要。正常情况ILSVRC2012_ID就是从 1 到 1000 连续编号,但如果你拿到的是二次整理的包,字段可能被改过。先断言一次,比后面训练的时候标签整体偏一位再去查要省事得多。
4.2 验证集标签与文件名的对齐规则
ILSVRC2012_validation_ground_truth.txt是纯文本,一行一个整数,共 50000 行,第 N 行对应ILSVRC2012_val_%08d.JPEG里的第 N 张。这个「第 N 行对第 N 张」是整篇最容易出错的地方,因为它没有任何冗余校验——错一位,整个验证集准确率就是一个随机数。
GT = "/data/imagenet/ILSVRC2012/ILSVRC2012_devkit_t12/data/ILSVRC2012_validation_ground_truth.txt" with open(GT, "r") as f: val_labels = [int(line.strip()) for line in f if line.strip()] # 三条硬校验,任何一条不过都别往下走 assert len(val_labels) == 50000, f"行数不对:{len(val_labels)}" assert min(val_labels) == 1, "最小标签不是 1,可能被改成了 0 起始" assert max(val_labels) == 1000, "最大标签不是 1000,可能混进了别的编码" # 文件名按 1 起始补零到 8 位 val_names = [f"ILSVRC2012_val_{i:08d}.JPEG" for i in range(1, 50001)]min(val_labels) == 1这条断言抓住过不止一次问题。有些来源的包把标签改成了 0 起始,最小是 0、最大是 999,行数依然是 50000,肉眼完全看不出异常,但映射到id2meta的时候 0 找不到键,或者整体错一位。加上这条断言,启动阶段就报错,不用等训练跑完一轮。
val_names里range(1, 50001)的起始值也是同一个道理。第一张图是00000001,不是00000000。
把三者接起来,就得到一条可以直接喂给 Dataset 的记录:
# records[i] = (图片绝对路径, 0..999 的类别索引, WNID, 类别描述) VAL_DIR = "/data/imagenet/ILSVRC2012/ILSVRC2012_img_val" records = [] for name, label in zip(val_names, val_labels): wnid, words = id2meta[label] # label 是 1..1000 records.append((f"{VAL_DIR}/{name}", label - 1, wnid, words)) print(records[0]) # ('/data/imagenet/ILSVRC2012/ILSVRC2012_img_val/ILSVRC2012_val_00000001.JPEG', # 某整数, 'n0xxxxxxx', '某类别名')注意label - 1这一步。训练框架的交叉熵通常要求标签从 0 开始,而 devkit 的编号从 1 开始,减一这一步必须在这里做完,不能留到后面。留到后面就会出现「训练时减了、验证时忘了减」这种经典错位,症状是训练 loss 正常下降,验证准确率永远卡在 0.1% 附近。
4.3 训练集一侧怎么和词表对上
训练集不需要 devkit 给标签,因为类别信息就在目录名和文件名里。每个 WNID 子目录名就是一个类别,目录里的每张图都属于这个类。要把它映射到 1 到 1000 的ILSVRC2012_ID,靠的是meta.mat里的WNID字段:
# 反查表:WNID 字符串 -> ILSVRC2012_ID wnid2id = {wnid: idx for idx, (wnid, _) in id2meta.items()} import os TRAIN_DIR = "/data/imagenet/ILSVRC2012/ILSVRC2012_img_train" subdirs = sorted(os.listdir(TRAIN_DIR)) # 训练集应该有 1000 个 WNID 子目录,且每个都能在词表里查到 assert len(subdirs) == 1000, f"子目录数不对:{len(subdirs)}" missing = [d for d in subdirs if d not in wnid2id] assert not missing, f"这些目录在 meta.mat 里查不到:{missing[:5]}" print("训练集目录与词表完全对齐")missing那个断言拦住的是另一类问题:有人只解压了部分 tar,目录数不到 1000,训练照样能跑,但模型只见过一部分类,评估时准确率怎么都上不去。提前断言,比训练两天后才发现要省事得多。
MATLAB 旁路那边,读取方式简单得多,load之后变量名直接就是synsets:
% 加载后工作区会出现 synsets 变量,长度 1000 load('/data/imagenet/ILSVRC2012/ILSVRC2012_devkit_t12/data/meta.mat'); % 取第一条,字段直接点出来 synsets(1).ILSVRC2012_ID synsets(1).WNID synsets(1).words % 验证集标签读成列向量 gt = dlmread('/data/imagenet/ILSVRC2012/ILSVRC2012_devkit_t12/data/ILSVRC2012_validation_ground_truth.txt'); numel(gt) % 应该是 50000MATLAB 里dlmread读单列纯数字文本很稳,比readtable少一层类型推断,不会把整数读成double之后又在比较时出问题。evaluation/目录下那套脚本的入口是一个 demo 脚本,跑之前先把里面的路径常量改成你机器上的实际路径,别指望它自己找。
提示:把
id2meta落成一个 CSV 存下来,路径放在数据根目录。每次启动都读一遍meta.mat要几百毫秒,训练脚本反复重启的时候这个开销很烦。
5. 避坑与排查:五个让验证集准确率变成 0.1% 的坑
5.1 现象:验证集准确率稳定在 0.1% 附近,训练 loss 正常
原因几乎总是标签整体错位一位。ILSVRC2012_validation_ground_truth.txt第 1 行对应00000001.JPEG,但代码里用range(50000)生成文件名,第一张变成了00000000.JPEG,后面全部往前挪一格。50000 张图里只有极少数类别碰巧对上,准确率就落在随机猜测的 0.1% 附近。
解决是把文件名生成那行改成range(1, 50001),同时在读标签之后立刻加一条断言,比较第一张图的标签和id2meta里第一条记录是否合理。更彻底的做法是在 Dataset 初始化时校验len(records) == 50000并且文件名集合和标签列表长度一致,让错位在启动阶段就报出来,而不是等训练跑完一轮。
5.2 现象:loadmat报KeyError: 'synsets',或者synsets长度是 1
原因有两个方向。一是读的文件根本不是meta.mat——有人把验证集标签的 txt 改名成.mat放进去,loadmat会把它当二进制解析然后返回一个奇怪的 dict。二是loadmat加了squeeze_me=True之后,synsets被压成了单个对象而不是数组,len()自然报错。
解决办法是先确认文件来源,用file命令看一眼类型:
file /data/imagenet/ILSVRC2012/ILSVRC2012_devkit_t12/data/meta.mat # 期望输出里带 MatLab v5 mat-file确认类型没问题之后,把squeeze_me去掉单独读一次,看meta.keys()里到底有什么,再决定加不加参数。squeeze_me和struct_as_record这两个参数不是越多越好,要看具体的.mat生成方式。
5.3 现象:AttributeError: module 'numpy' has no attribute 'object'
原因是 NumPy 2.x 移除了np.object、np.int、np.float这些历史别名,而很多读 devkit 的老脚本里还在用它们做类型判断。这类报错通常不在你的代码里,而在某个第三方库或者从别处拷来的工具函数里。
解决有两条路。最省事的是把 NumPy 降回 1.26,这也是第 2 章钉版本的原因。如果必须留在 2.x,就在入口处加一层兼容垫片,把别名补回去:
import numpy as np # 兼容垫片:只在 NumPy 2.x 下生效,避免老脚本 import 就崩 if not hasattr(np, "object"): np.object = object np.int = int np.float = float np.bool = bool # 垫片放在所有业务 import 之前,顺序不能反垫片要放在所有业务模块 import 之前,因为报错发生在模块导入期,晚一步就来不及。这条经验在别的老数据集工具上也通用。
5.4 现象:FileNotFoundError,路径里多了一层或少了一层目录
原因是tar -xzf解压时带不带-C、-C指到哪一层,直接决定了 devkit 的相对位置。带了-C /data/imagenet/ILSVRC2012/,路径是/data/imagenet/ILSVRC2012/ILSVRC2012_devkit_t12/data/meta.mat;如果-C指到了ILSVRC2012_devkit_t12里面,路径就会少一层。
解决是用find反查一次,不要靠猜:
# 从数据根目录往下找 meta.mat,输出就是真实路径 find /data/imagenet -name "meta.mat" -type f # 如果找到多个,说明解压了不止一次,把旧的删掉再统一路径找到之后把代码里的路径常量改成绝对路径,不要用../这种相对路径。相对路径在本地 notebook 里能跑,一进训练容器就全错。
5.5 现象:训练集子目录数不是 1000,模型只学会了一部分类
原因是 train 的 1000 个 tar 只解压了一部分,或者解压过程中某个 tar 损坏被跳过。这个问题最隐蔽的地方在于训练不会报错,只是类别覆盖不全,最终准确率上不去,而你会一直怀疑是模型结构或者学习率的问题。
解决办法是在 Dataset 构建阶段加一条断言,把子目录数和词表长度对齐,同时在解压脚本里对每个 tar 单独解压并记录失败列表,不要用一个for循环加&&串起来——串起来的话中途失败会静默跳过。
# 逐个解压并记录失败的 tar,失败的不中断整体流程 cd /data/imagenet/ILSVRC2012/ILSVRC2012_img_train failed="" for f in *.tar; do tar -xf "$f" -C . || failed="$failed $f" done [ -n "$failed" ] && echo "以下 tar 解压失败:$failed" || echo "全部解压完成"||捕获失败但不退出,循环结束后统一汇报。这个模式在批量解压场景里比set -e好用,因为你要的是「知道哪些坏了」,不是「一坏就停」。
6. 装完怎么验证跑通:抽 10 张图走完文件名到类别名的闭环
装完不验证,等于没装。我验证 devkit 的方式不是「代码能 import」,而是抽 10 张验证集图片,把「文件名 → 标签行号 → ILSVRC2012_ID → WNID → 类别描述 → 文件存在且能打开」这条链路完整走一遍,任何一环断了都立刻能看出来。
import os, random from PIL import Image from scipy.io import loadmat ROOT = "/data/imagenet/ILSVRC2012" DEVKIT = f"{ROOT}/ILSVRC2012_devkit_t12" VAL_DIR = f"{ROOT}/ILSVRC2012_img_val" # 1. 重建词表映射 meta = loadmat(f"{DEVKIT}/data/meta.mat", squeeze_me=True, struct_as_record=False) id2meta = {int(s.ILSVRC2012_ID): (str(s.WNID), str(s.words)) for s in meta["synsets"]} # 2. 重建标签列表 with open(f"{DEVKIT}/data/ILSVRC2012_validation_ground_truth.txt") as f: labels = [int(x) for x in f if x.strip()] # 3. 固定随机种子抽 10 张,便于复现 random.seed(20240501) picks = random.sample(range(1, 50001), 10) for n in sorted(picks): fn = f"ILSVRC2012_val_{n:08d}.JPEG" path = os.path.join(VAL_DIR, fn) lid = labels[n - 1] # 第 n 行对应第 n 张 wnid, words = id2meta[lid] exists = os.path.exists(path) # 只 open 不 decode,确认文件头没坏,速度快 openable = False if exists: with Image.open(path) as im: openable = im.size[0] > 0 and im.size[1] > 0 print(f"{fn} -> id={lid:<4} wnid={wnid} file={exists} open={openable} | {words[:40]}")这段脚本的关键是labels[n - 1]这个下标。文件名里的n从 1 开始,列表下标从 0 开始,减一是必须的,也是第 5 章那个错位坑的根源。如果你把这一行写成labels[n],循环跑到最后一张会直接越界报错,反而容易发现;真正难查的是文件名也用range(50000)生成的那种,越界不报,只是全错。
跑完之后看输出,检查三件事:
| 检查项 | 期望结果 | 不对说明什么 |
|---|---|---|
file=列 | 全是 True | 图像目录路径配错了,或者该图缺失 |
open=列 | 全是 True | 文件存在但已损坏,需要重新下载对应部分 |
wnid与words | 语义上对得上 | 标签行与文件名的对齐关系错了 |
第三项需要一点人工判断。words是类别英文描述,wnid是 WordNet 编号,两者在meta.mat里本来就是一对,只要你读的是同一份文件就不会错——真正会错的是lid拿到的是不是正确的那一行。所以更好的做法是再抽一类做覆盖检查:
from collections import Counter # 50000 条标签里 1000 个类应该各出现 50 次 cnt = Counter(labels) assert len(cnt) == 1000, f"只覆盖了 {len(cnt)} 个类" assert set(cnt.values()) == {50}, f"每类样本数不均匀:{set(cnt.values())}" print("验证集类别分布检查通过:1000 类 × 50 张")这个断言比逐张核对可靠得多。验证集是均衡的,每类正好 50 张,如果Counter出来不是这个结果,要么标签文件被改过,要么读取的时候漏行或多行。加上这条,devkit 的数据完整性基本就有保障了。
最后一个小习惯:把id2meta导出成 CSV,和 devkit 放在一起,下次接手的人不用再读一遍.mat。
import csv with open(f"{DEVKIT}/data/id2meta.csv", "w", newline="") as f: w = csv.writer(f) w.writerow(["ilsvrc2012_id", "wnid", "words"]) for i in range(1, 1001): wnid, words = id2meta[i] w.writerow([i, wnid, words]) print("已导出 1000 行词表")从那以后,我每次在新机器上装完 devkit,都会强制走一遍这 10 张图的抽查和那两条断言,通过之后才动训练脚本。环境这东西,装的时候多花十分钟,比训练跑两天发现标签错位再回头查要划算太多。希望帮到你。
本文还有配套的精品资源,点击获取