
搞跨模态检索和图文匹配的人应该都绕不开NUS-WIDE这个数据集。它是新加坡国立大学做的多模态基准数据里面有将近27万张网络图片每张图带81类语义标签还附了文本标签和一组官方特征。我去年接了个快速迭代实验需要一份规模适中、标签干净的子集来跑模型网上能找到的NUS-WIDE-10K基本都是别人处理的旧版本有的特征还是几百维的传统特征有的标签对齐有问题最后我干脆从原始数据开始自己制作。这篇文章会把我从下载原始图片、解析标签、筛选样本、提取特征、到打包输出h5文件的完整流程全部写出来附带可复现的代码和踩坑记录适合正好想做多模态数据集或者想复用NUS-WIDE做实验的同学。首先得说清楚制作NUS-WIDE-10K不是一个“随机抽一万张图”的简单活儿。数据集的标签是-1、0、1三值标注文本标签和图片路径一一对应特征提取要选择适合任务的模型划分训练测试还得保证类别分布合理。任何一个环节处理不好后面训练模型的时候都会出各种难以定位的问题。这篇文章就是把我实际做过的工序完整过一遍尽量把每步的为什么也讲明白。1. 为什么还要自己制作NUS-WIDE-10K1.1 先搞清楚NUS-WIDE原始数据长什么样NUS-WIDE是新加坡国立大学在2009年左右发布的多模态数据集主要来自网络图床的图片和用户标注。完整数据集图片数量约269648张每张图都对应多个标签这些标签经过清洗和映射后被统一成81个语义类别比如动物、汽车、天空、海滩、建筑这些常见概念。需要特别注意标签不是简单的0和1而是三种取值1表示图片明确属于这个类别-1表示明确不属于0表示缺乏标注信息。这种三值设计在后来处理时会直接影响样本筛选如果简单把0当成负样本等于把所有未标注信息都判断成“不属于”会引入很大的噪声。除了81维标签向量官方还给每张图片提供了文本标签通常放在类似于AllTags1.txt的文件里每一行是这张图全部标签文本多个标签之间会用分隔符隔开。有的版本里还细分了AllTags81.txt和AllTags1.txt两种文件前者是81维的数值向量后者是一段文本。文本标签是NUS-WIDE备受跨模态检索方向关注的原因因为同时具备图片像素、语义类别、文本标签三种模态的数据集并不多尤其是这种规模能超过二十万张的更少见。官方还额外发布了六种手工特征包括颜色直方图CH、颜色矩CM、颜色相关图CORR、边缘方向直方图EDH、小波纹理WT、以及SIFT词袋特征维度分别是64、225、144、73、128、500。早年做跨模态哈希、图片检索的论文基本都在这些特征上跑结果。特征文件通常是.mat或者.txt格式解析难度不高但如果你要做比较新的深度学习实验这些传统特征的表达力会明显不够用。1.2 原始数据直接用的痛点与10K子集的价值直接用完整NUS-WIDE做实验第一个问题是体量。图片压缩包下载下来后解压整个图片目录就会有几十GB光是加载一张原图做预处理在高清分辨率下就要花不少时间。如果你只是调一个loss函数、验证一个想法跑一遍完整数据集可能几分钟才能见到一次回调调试效率很低。第二个问题是老特征。官方自带的六种手工特征虽然方便但和现在主流模型提取出的深层特征相比信息密度和判别力都不在一个量级很多新的跨模态方法在这些老特征上根本发挥不出来。第三个问题出现在所谓“现成10K子集”上。我在网上能找到的NUS-WIDE-10K多数是几年前有人处理过的有的只保留了部分类别有的特征格式是.mat而不是通用npy有的图片内容和标签根本对不上。想直接拿来做实验你得先花时间去怀疑它是否正确这种不确定性比自己做一份还麻烦。10K子集的价值就在于规模小到一张普通显卡甚至纯CPU都能快速跑通pipeline数据形态又尽可能保留了完整数据集的分布特征。制作时最好让81个类别都有基本覆盖文本标签完整保留图像特征用较新的ResNet系列提取。这样无论是做图文检索、跨模态哈希、多标签图像分类还是教学演示都能拿它当快速试验场论文里需要更大规模再换上全量数据。2. 动手前的准备找到原始数据并梳理目录2.1 原始文件下载与完整性校验制作的第一步是去NUS-WIDE官网下载原始数据。官网一般会提供图片压缩包、标签文件、图片列表和官方特征文件。图片压缩包体积不小下载时经常因为网络波动中断我的建议很简单用支持断点续传的工具下载尽量别用脆弱的浏览器下载方式。下载完成后算一下MD5或者SHA256和官方给的校验值比对。如果是一两个文件损坏重下对应部分就行不带校验直接往下走后面的标签对齐工作全白做。解压图片目录时记得预留充足的磁盘空间我一般留出压缩包两倍以上的空间防止解压中途磁盘写满。解压完以后看一眼图片文件数量和官方说明的数量做对比。如果数量差距过大第一步就回头检查是不是下载包不完整。图片文件名通常是编号加时间戳的格式比如0001_12345678901.jpg前面的编号只是顺序号不一定连续后面的时间戳是图床上的ID这个规律在后面做文件名解析和去重时会用到。2.2 工程目录与命名规范做这种多模态数据集最怕目录乱。图片、标签、特征、划分文件全部混在一起过两周自己都找不到哪份文件对应哪次处理。我用的目录结构是这样的nuswide-10k/ ├── raw/ │ ├── images/ # 原始图片只读按官方文件名保存 │ ├── labels/ # 标签文件AllTags81、AllTags1等 │ └── meta/ # 官方特征、图片列表、划分文件 ├── processed/ │ ├── images_filtered/ # 筛选后的图片 │ ├── features/ # 提取好的特征npy │ └── labels_clean/ # 清洗后的标签 ├── splits/ │ ├── train.txt │ └── test.txt └── output/ ├── nuswide10k.h5 └── meta_info.jsonraw目录里的东西永远保持只读不修改原始文件processed和output都是脚本生成的可丢弃产物。这样即使处理脚本调整了多次也不会污染原始数据。命名上我有几个固定习惯图片保留官方原始文件名不重命名特征文件名带数据集名、模型名、特征维度比如resnet50_feat_2048.npy划分文件简简单单用train.txt和test.txt。这些看着是小事实验周期一旦拉长省下的时间非常可观。3. 标签解析与样本筛选10K子集的“选人”逻辑3.1 解析81维标签和文本标签文件标签解析是整个流程里最需要细心的一步。NUS-WIDE的81维标签文件每行对应一张图片一行81个取值取值为-1、0、1列之间用空格或者制表符分隔。解析时用Python的open按行读就可以但要注意编码。官方文件有的版本不是标准UTF-8直接用utf-8解析会抛UnicodeDecodeError我通常先用utf-8尝试失败后用latin-1兜底或者直接open时传errorsreplace基本都能读下来。解析成NumPy数组后它的行顺序必须和图片文件列表严格一致。这个要求看起来简单但实际项目里翻车最多的就是这里。我建议把文件名列表、81维标签数组、文本标签列表放在同一个数据容器中比如一个字典或者一个NamedTuple别把它们拆成多个独立文件维护。顺序一旦错位后面的特征对齐会变成一场灾难。文本标签的解析也不难。AllTags1这类文件里面一行就是一张图的全部标签文本多个标签之间的分隔符不同版本略有不同常见的是#。处理时把一行按#拆分每个标签去掉首尾空格和空串保存成一个list。有的论文只保留能映射到81类上的那部分文本有的则直接用全部原始标签这取决于你的任务因此这一步我会把两种版本都存下来一份清洗后的完整文本一份过滤后仅保留高频概念的文本后续按实验需要取用。3.2 筛选策略与类别分布设计10K子集筛选有两种思路。第一种是纯随机抽1万张做起来简单但后果很现实多标签数据的长尾类别可能只覆盖很少样本有些类别甚至一张都没有。第二种是按类别覆盖来抽先保证81个类别每个至少有一定量的正样本再按原始分布加权采样补足剩余数量。由于是多标签每张图可以同时属于多个类别所以不能像单标签数据那样直接分层采样。更稳妥的做法是先统计每个类别的正样本池从每类池子里抽取目标数量然后把所有抽出来的样本合并去重如果去重后还不到1万张再从没被选中的样本里按原始类别频率补足。我实际用的配置是每个类别至少保留10张正样本剩下的约9000张按原始类别频率的平方根加权采样。这样小类别不会被丢掉同时整体仍保留长尾结构不会变成一个人为强行平衡的假数据集。补充说明一点计算采样权重时我使用的是该类别的正样本数也就是标签为1的数量-1和0都不会进入正样本池。脚本跑完以后我会打印一张筛选前后各类别样本数的对照表人工扫一眼看看有没有类别数量异常少的情况。这一步千万别省很多分布问题不落到统计数字上看你根本注意不到。4. 特征提取与模态对齐4.1 从官方手工特征到公开预训练模型特征官方那几组手工特征可以直接用但效果有限为了让NUS-WIDE-10K能适配较新的模型我更推荐自己提取深度特征。我的标准做法是用ImageNet预训练的ResNet-50图片先等比缩放让短边变成256再中心裁剪成224x224按ImageNet的均值和方差做归一化输入模型后取全局平均池化层的输出得到一个2048维向量。这个流程是图像检索实验里最常见的配置改动小、对标论文容易。下面是特征提取的核心片段import torch from torchvision import transforms, models model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) model.fc torch.nn.Identity() # 去掉分类头只保留特征 model.eval() preprocess transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) def extract_feature(img_path): img Image.open(img_path).convert(RGB) x preprocess(img).unsqueeze(0) with torch.no_grad(): feat model(x) return feat.squeeze(0).numpy() # 2048维如果你是纯CPU环境跑1万张图可能要比较久。我实测过单卡普通显卡用ResNet-50提取1万张224x224图片大概十几分钟纯CPU的话可能会根据机器配置变成一两个小时。如果只是为了快速验证可以换成MobileNetV3输出维度改成1280速度能提升不少精度在检索任务里也足够用。提特征时最好分批处理每批64张或者128张用torch.no_grad()包裹最后把所有特征拼成一个(10000, 2048)的NumPy数组。需要注意的是图片读取的顺序必须和标签数组顺序一致。我这里的做法是先用一个list存储所有选中图片的路径标签数组也基于同一份list的索引生成然后提特征时严格按这个list顺序遍历后面就不会出现特征和标签对不齐的问题。4.2 文本标签向量化与模态对齐存储文本标签向量化要看任务需要。做图文检索实验最简单的做法是保留清洗后的原始文本标签训练时直接用分词结果做embedding也可以离线用TF-IDF或者Word2Vec把标签转成向量减少在线计算量。我这里先把文本标签清洗去掉URL、特殊符号统一小写然后按图片文件名做key保存成一个字典key是图片文件名value是清洗后的标签list。这样图像特征、81维标签、文本标签都通过文件名关联起来后续无论怎么打乱划分都能靠这个映射重新对齐。这种“以文件名为唯一主键”的对齐思路是整个数据集制作的核心。特征数组可以用索引定位但文本标签用数组存没法高效做embedding所以用字典更合适而81维标签由于是整齐的矩阵多数算法直接吃NumPy矩阵。把它们统一放进一个结构里训练代码只需要一个load函数就能拿到全部模态的数据。我自己是写了一个load_nuswide10k()函数封装这类读取逻辑所有下游实验都调这个函数上游需要改格式时只改这一处。5. 数据集划分与文件组织5.1 训练集/测试集划分方案划分策略取决于任务类型。官方完整数据是有TrainTestLabels的但10K子集通常要自己重新分。我一般按80比20随机划分8000张训练、2000张测试。如果做的是多标签分类光随机还不够我会在划分后检查测试集里每个类别是否至少有一张正样本如果有类别缺失就重新随机划分一次最多重试几次。为了保证结果可复现划分脚本里固定随机种子生成的train.txt和test.txt直接落盘。这样的静态划分文件有几个好处第一实验之间不会因为数据划分不同产生额外方差第二后续训练脚本只读文件不负责划分减少出bug的可能第三想换划分比例或者新增验证集只需要重新生成一次txt不需要重新提特征。我早期直接在训练脚本里设置随机种子来做划分每次跑实验都重新切数据结果一旦想对比两个方法它们的数据顺序都不一样问题定位特别费劲。后来改成静态划分文件整个对比实验顺畅了很多。5.2 几种好用的存储格式数据集的组织格式会让下游开发效率差异很大。我常用的有三种格式优点缺点适用场景HDF5单文件多数据集读取快适合高频IO查看内部结构需要工具训练脚本直接加载npy json纯NumPy生态加载直接文本和路径管理麻烦快速原型验证TFRecordTensorFlow生态读取吞吐稳定组织稍微复杂TF训练管线我推荐用HDF5为主格式原因是可以同时存图像特征、81维标签、文本标签列表、图片路径和meta信息。写入的时候用h5py创建几个dataset和一个group保存文本字典训练时一次性load返回一个字典结构。import h5py with h5py.File(output/nuswide10k.h5, w) as f: f.create_dataset(features, datafeatures, compressiongzip) f.create_dataset(labels, datalabels, compressiongzip) f.create_dataset(filenames, datafilenames) f.create_dataset(category_names, datacategory_names)HDF5的好处是单个文件就包含了所有模态的信息不用处理“特征在一个文件、标签在另一个文件”的对账问题。同时我会在output里额外保存一组npy和json方便纯NumPy环境直接使用。meta_info.json里写清楚样本量、类别数、特征来源模型、图像预处理方式、划分比例、创建时间相当于给数据文件做了个说明书如果以后要把数据传给同事或者写进论文里这份meta几乎是必需的。6. 制作过程中的常见问题与排查技巧6.1 图片下载失败与断点续传制作流程里最耗时间的往往不是特征提取而是图片下载。一组公开数据集的图片来自网络图床有些图片链接年久失效下着下着就断。我的做法是写下载脚本时自带断点续传机制先维护一个已完成文件名的集合启动时遍历目标图片列表已经在本地且文件大小非零的跳过剩下的才去下载。下载用requests配合流式写入每张图设置超时最多重试三次每100张打印一次进度。这样哪怕中途断网重新跑一遍脚本就能接着下不用从头再来。6.2 标签文件解析的常见问题标签解析这块的坑我总结成一个表问题现象解决办法文件编码不对UnicodeDecodeError用latin-1或errorsreplace读取行数与图片数不一致解析出的标签长度不等于图片长度立即抛出异常核对官方文件版本标签列带空字符取值读成字符串无法计算强制split后astype(int)标签顺序和图片列表不一致特征与标签错位用文件名为唯一主键解析完做断言检查我在解析脚本里会加上类似断言标签数组的行数必须等于图片列表长度不等就抛异常绝不带病继续。因为这类问题一旦进入特征提取阶段错位会以指数级别放大排查起来非常痛苦。6.3 类别不平衡与样本质量检查多标签数据常见类别不平衡筛选完以后必须统计每个类别的正样本数把少于10次的类别放大打印。如果有的类别一张正样本都没有说明筛选策略没覆盖到要回退调整采样参数。图片质量也要检查下载回来的文件可能被替换成404页面或者损坏的占位文件最简单的校验是用PIL的Image.open配合verify()跑一遍发现坏文件直接标记出来从候选池里补一张。这个检查必须在特征提取之前做否则特征矩阵里会混进异常向量后面模型训练时收敛异常定位起来非常头疼。我做完这版NUS-WIDE-10K之后最大的体会是数据集制作看着是体力活真正决定后续实验效率的其实是工程细节。标签解析时多写一个断言、文件名始终作为主键、静态保存划分文件、特征带meta说明这些事情在当时都只多花了十几分钟但后续实验过程中节省的时间是几十倍。如果你也在做数据集相关的工作不妨从这些地方入手把数据流程当成一个小型工程来维护。这套流程不光适用于NUS-WIDE-10K其他多模态数据集像MIRFlickr、Pascal VOC组合子集甚至是自己爬来的图片语料也都可以照着这套思路走。最后再分享一个小技巧把制作脚本的参数全部提取成配置项样本量、筛选阈值、特征模型、输出路径都通过配置文件传入下次想生成一个5K或者20K版本时改一行配置就能跑比复制粘贴脚本好用太多。