资讯动态

按需定制ImageNet数据集:采样、并发下载与断点续传指南

发布时间:2026/10/13 16:01:03 来源:尧图企业网站定制
简介这是一套基于Python 3编写的ImageNet数据集下载工具面向需要快速构建自定义图像子集的深度学习开发者和研究人员。通过命令行参数指定所需类别数以及每类应下载的图片数量程序会自动从ImageNet官方API获取图像URL并进行批量下载有效替代手工爬取样本的繁琐操作。压缩包仅7个文件体积1.55MB包含两个Python脚本下载主程序与数据统计脚本、Markdown说明文档、类别清单CSV、类别信息JSON以及运行依赖文本结构清晰便于直接使用。目前已有1002人学习下载。借助其中已经整理的类别清单与URL可用性分析使用者可以快速筛选满足样本量要求的类别按需生成包含指定图片数量的数据集同时可参考文档中的实现思路对类别筛选、下载逻辑进行二次定制适合作为图像分类、模型微调等场景的数据准备工具。1. 为什么下载 ImageNet 不直接用官方压缩包而是按类按数量去采样很多人第一次接触 ImageNet 下载第一反应是找官方整包下载地址。但真实场景里整包训练集不仅体积庞大而且结构是全部类目、全部图片一股脑铺开。你做迁移学习、做小样本验证、做某个特定类别集合的微调时根本用不上那上千个类也不想把几百 GB 落进磁盘。这个 ImageNet-Datasets-Downloader 的思路正好相反先拉取完整的类目标识映射再通过指定类数量、每类图片数量切片出一个干净的子集给你。也就是说它解决的不是「怎么把 ImageNet 下载下来」而是「怎么把 ImageNet 变成你真正需要的那个最小可用数据集」。对于做分类任务、做模型压缩、做少样本学习的开发者来说这个按需切片的思路能省掉大量等待时间和存储成本。新手不用面对整包数据的解压和整理熟手也能把采样逻辑直接固定成可复用的脚本不用反复手工挑目录。2. 认识 ImageNet 的目录结构与类目标识自定义数据集的前提2.1 为什么 ImageNet 不能像 MNIST 那样一条命令拉完MNIST、CIFAR 这类数据集官方会打包成单个文件下载后一行代码解出 tensor。ImageNet 不一样它天生是「一个类目一个目录」的组织方式每个类目下是成千上万张图片文件。即便有现成库能直接加载完整版 ImageNet也要你先自己把整包数据准备好库本身不负责替你下载。这时候就出现了一个现实问题ImageNet 的类别编号不是 0、1、2 这种连续数字而是用 WNIDWordNet ID来表示类似 n01440764、n02085620 这样的字符串。每个 WNID 对应一个 synset即一个语义类别集合。自定义数据集的第一步不是写下载循环而是搞懂怎么把 WNID 映射到你认识的类名上再从映射表里筛出你想下载的那几个类。常见做法是先从 ImageNet 官方的标签映射文件出发解析得到 WNID 到人类可读类名的字典。然后根据你传入的类别关键词或类别数量确定要下载哪几个 WNID再针对这些 WNID 去样本清单里取每类的图片地址。整个过程可以抽象成三步拿映射表、筛类别、按类别抓取样本列表。2.2 类目标识与数据清单的对应关系ImageNet 训练集的样本清单公开形式是一个大文本文件每一行是一条样本记录包含该样本所属的 WNID 和图片的网络地址。你要做的是把这个大清单拆成「某 WNID → 该类的所有图片地址」的倒排索引之后按类别采样就简单了。数据对象形式用途标签映射文件文本文件按行列出 WNID 和可读类名把 n01440764 转成 tench确定用户要哪几个语义类别样本清单文本文件每行一条样本记录提供每个 WNID 下所有图片的网络地址池下载脚本Python 脚本读取以上两个文件按类采样并执行下载我一般会先把标签映射和样本清单都解析成内存里的字典和列表解析代码控制在几十行内不做复杂依赖。解析时有几个点容易踩一是标签文件的行尾可能带\r或空格最好统一 strip二是清单文件很大逐行readlines()反而比for line in file更省内存三是某些类名里带有逗号和括号直接用逗号切分可能出错需要用制表符或者按固定字段位置解析。2.3 按类数量和图像数量生成采样计划拿到倒排索引以后核心逻辑就是生成采样计划。假设你指定num_classes10、images_per_class50脚本会先从类名映射表里取出前 10 个类或匹配你关键词的类然后对每个类从图片地址池里随机抽 50 条最后把这 500 条下载任务展开成一个平铺任务列表。采样计划这一步建议单独输出成一个 JSON 文件记录每个类的 WNID、类名、目标图片数和图片地址列表。好处是下载过程可以被反复重跑只要计划文件还在断点续传就知道自己还剩哪些图片没完成。后面并发下载和失败重试也都是围绕这个计划文件做状态记录。import json import random def build_plan(label_file, sample_file, num_classes, images_per_class, output_plan): # 解析 WNID - 可读类名 wnid_to_name {} with open(label_file, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue parts line.split(\t) # 优先按制表符切 if len(parts) 2: wnid, name parts[0], parts[1] wnid_to_name[wnid] name # 解析样本清单建立 WNID - 图片地址列表 wnid_to_urls {} with open(sample_file, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue fields line.split() # 一般字段间用空格分隔 if len(fields) 2: continue wnid fields[0] url fields[-1] # 地址一般在行尾 wnid_to_urls.setdefault(wnid, []).append(url) # 按需选取前 num_classes 个类 selected_wnids list(wnid_to_name.keys())[:num_classes] plan {} for wnid in selected_wnids: urls wnid_to_urls.get(wnid, []) # 如果该类图片数不够尽量全取 sampled random.sample(urls, min(images_per_class, len(urls))) plan[wnid] { class_name: wnid_to_name[wnid], target_count: len(sampled), urls: sampled } print(fclass {wnid}: {wnid_to_name[wnid]}, sampled {len(sampled)}) with open(output_plan, w, encodingutf-8) as f: json.dump(plan, f, ensure_asciiFalse, indent2)这里有个细节值得说明fields line.split()默认按任意空白字符切分能处理行内有多个空格的情况。如果样本清单的 URL 和 WNID 之间有别的信息字段取最后一项作为 URL 是最稳妥的。random.sample可以保证同一类内部每张图被抽到的概率均匀不需要自己去维护随机种子但如果你希望每次生成完全一致的采样计划建议在脚本开头调用random.seed(42)让整个计划可复现这对后续实验对比很重要。num_classes参数决定类的数量images_per_class决定每个类下载多少张图。两个参数共同决定了最终数据集规模总量约等于二者乘积。实际使用时要注意有的类在样本清单里的图片总数可能不足你指定的数所以代码里用min(images_per_class, len(urls))做了兜底否则抽样会直接抛异常。3. 用指定类数和图像数创建自定义数据集下载脚本与参数说明3.1 单线程基础版下载器先跑通再谈速度在引入并发之前先把一条最简单的下载链路跑通。这个版本做的事情很直接读上一个章节生成的计划 JSON遍历每个类的 URL 列表逐个发起网络请求把图片内容写到本地目录。目录结构按train/{wnid}/{序号}.jpg组织方便后续直接用各类深度学习框架的数据加载器读取。import json import os import requests def download_image(url, save_path): headers {User-Agent: Mozilla/5.0 (compatible; ImageNet-Downloader/1.0)} r requests.get(url, headersheaders, timeout(5, 30)) r.raise_for_status() # 非 2xx 状态码直接抛异常 with open(save_path, wb) as f: f.write(r.content) def run(plan_file, output_dir): with open(plan_file, r, encodingutf-8) as f: plan json.load(f) for wnid, info in plan.items(): class_dir os.path.join(output_dir, wnid) os.makedirs(class_dir, exist_okTrue) for idx, url in enumerate(info[urls]): ext .jpg save_path os.path.join(class_dir, f{idx:05d}{ext}) try: download_image(url, save_path) except Exception as e: print(ffailed: {url} - {e})timeout(5, 30)两个值分别表示连接超时和读取超时。图片服务器有时候响应慢连接超时设置太短会大量误报设置太长则会让失败任务卡住整个队列。我这里连接给 5 秒、读取给 30 秒属于比较保守的值生产环境可以按自己的网络状况调整。raise_for_status()会在 HTTP 状态码不是 200 时直接抛异常避免把错误页面当成图片保存下来。这个单线程版本的意义在于验证计划文件是否正确、网络能否连通、目录结构是否符合预期。我第一次跑的时候直接用并发版本结果大量请求因为同一个问题接连失败日志刷屏却看不出根因。先用单线程跑通 10 张图确认链路没问题再上并发这是最省时间的做法。3.2 多线程并发下载让任务列表真正跑起来单线程下载的瓶颈在网络往返延迟而不是带宽。比如一张图从发起请求到接收完成要 1 秒其中真正传输可能只需要 0.1 秒剩下的都是等待时间。用线程池把等待时间重叠起来整体吞吐可以提升数倍。常见的做法是用concurrent.futures.ThreadPoolExecutor它比手动管理线程简单得多也方便统一收集异常。import json import os import requests from concurrent.futures import ThreadPoolExecutor, as_completed def download_one(args): url, save_path args headers {User-Agent: Mozilla/5.0 (compatible; ImageNet-Downloader/1.0)} for attempt in range(3): # 最多重试 3 次 try: r requests.get(url, headersheaders, timeout(5, 30)) r.raise_for_status() with open(save_path, wb) as f: f.write(r.content) return True except Exception as e: if attempt 2: return False return False def run_concurrent(plan_file, output_dir, workers8): with open(plan_file, r, encodingutf-8) as f: plan json.load(f) tasks [] for wnid, info in plan.items(): class_dir os.path.join(output_dir, wnid) os.makedirs(class_dir, exist_okTrue) for idx, url in enumerate(info[urls]): save_path os.path.join(class_dir, f{idx:05d}.jpg) tasks.append((url, save_path)) ok 0 with ThreadPoolExecutor(max_workersworkers) as pool: futures [pool.submit(download_one, t) for t in tasks] for future in as_completed(futures): if future.result(): ok 1 print(fdone: {ok}/{len(tasks)})max_workers8是一个起步值。对于家庭宽带和小型服务器8 到 16 个线程足够跑满带宽盲目调到 50 以上容易触发图片服务器的限流策略反而大量返回 403。线程数不是越大越好关键是观察失败率和吞吐量之间的关系。as_completed会在每个任务完成时返回这样主线程可以实时统计成功数量。每个任务内做了 3 次重试但这只是最基础的重试策略。真实场景里一次失败的请求在 1 秒后重试可能还是失败问题往往出在服务器端对某个 IP 的临时限流。更稳妥的做法是失败后等一段随机时间再试比如time.sleep(random.uniform(1, 3))避免所有线程在同一时刻对同一台服务器发起重试风暴。3.3 参数怎么定线程数、超时、重试次数参数推荐值调整依据线程数8-16网络带宽充足可适当增加发现大量 403 就减少连接超时5 秒网络差可放宽到 10 秒但不宜更大读取超时30 秒图片体积大、带宽低时放宽到 60 秒重试次数3 次网络抖动频繁可增加到 5 次重试间隔1-3 秒随机固定间隔会让重试请求在时间上对齐加重服务器压力这里要给一个容易翻车的提醒不要把超时时间当成「越大越好」。连接超时设成 30 秒表面上减少了误报实际上当一个域名解析失败或端口不通时每个线程都要白白等 30 秒8 个线程就是 8 个 30 秒的堆积。遇到那种整体不可用的时间段整个下载队列会被拖得非常慢。合理的做法是让连接超时短一些快速失败快速重试。重试次数也不是越多越好。超过 5 次以后大部分失败已经是永久性失败图片源地址失效或服务器拒绝继续重试只是在浪费时间。我一般在脚本里把重试次数放到配置项里而不是硬编码出现大面积失败时改配置重跑即可不用改代码。4. 断点续传与下载校验大任务不重跑的落地做法4.1 基于已完成文件列表的断点续传机制下载任务一旦跑起来你就没法保证中途不被打断。笔记本合盖、服务器重启、网络断开任何一个意外都会让几百上千张图片的下载半途而废。最直接的后悔药是断点续传每次下载完成后把一个标记写入状态文件下次启动时先读状态文件跳过已经完成的部分。常见做法不是逐条查数据库而是维护一个「已完成 URL 集合」下载前判断当前 URL 是否在这个集合里。集合可以用一个纯文本文件保存一行一个 URL既方便人眼查看也方便脚本加载。import os class DownloadState: def __init__(self, state_file): self.state_file state_file self.done_urls set() self._load() def _load(self): if os.path.exists(self.state_file): with open(self.state_file, r, encodingutf-8) as f: self.done_urls {line.strip() for line in f if line.strip()} def mark_done(self, url): self.done_urls.add(url) with open(self.state_file, a, encodingutf-8) as f: f.write(url \n) def is_done(self, url): return url in self.done_urlsmark_done用的是追加写模式而不是每次重写整个文件。这样即使中途崩溃最多丢失最后几条未刷盘的记录不会损坏整个状态文件。加载时用 set 而不是 list是因为判断url in done_urls时 set 的复杂度是O(1)而 list 是O(n)。当状态文件里有几十万条记录时这个差异能省下大量时间。实际下载时主循环里第一件事就是判断state.is_done(url)为真直接跳过。需要注意状态文件里记录的是 URL 而不是本地文件名因为 URL 是任务的唯一标识。同一个 URL 如果因为目录结构调整换了保存路径状态文件依然有效。4.2 图片有效性校验防止把错误响应存成本地文件断点续传解决的是「下载中断」但还有一个更隐蔽的问题任务显示成功、文件也写入了但文件根本不是图片。典型的场景是图片服务器返回 404 页面但 HTTP 状态码是 200或者反爬机制返回一段 JSON 错误信息。raise_for_status()只检查状态码无法识别这种内容类型的错误。所以在保存文件之后必须对文件内容做二次校验。最可靠的方式是检查文件头部的二进制魔数magic numberJPEG 文件以\xff\xd8\xff开头PNG 文件以\x89PNG开头。如果文件头部对不上直接判定为无效文件并删除。def is_valid_image(file_path): try: with open(file_path, rb) as f: header f.read(8) if header[:3] b\xff\xd8\xff: return True if header[:4] b\x89PNG: return True return False except Exception: return False def download_with_check(url, save_path): # 下载逻辑同前省略 ... if not is_valid_image(save_path): os.remove(save_path) raise RuntimeError(downloaded file is not a valid image)前 3 个字节对于 JPEG 来说几乎足以确定身份但严格的 JPEG 校验可以进一步检查第 4 个字节是否为\xe0、\xe1等有效标记。实际使用中\xff\xd8\xff三字节判断已经过滤掉绝大部分错误响应。PNG 的头 4 字节是固定签名基本不会误判。把这个校验放在下载函数内部、写文件之后有一个额外好处断点续传的状态标记只在校验通过后写入。如果校验失败任务抛异常状态文件不会把这条 URL 标记为完成下次重跑时会重新下载。这样才能保证状态文件里记录的每一条都是「真正可用的图片」。4.3 失败任务的独立记录与重跑策略即使做了重试和校验一个几千张图片的下载任务总会有个别图片彻底下不下来。这些失败任务不应该混在日常日志里应该单独收集到一个失败清单文件里。下载主流程跑完后再针对失败清单做二次重跑或者人工查看原因。实现方式是在主流程里维护一个failed_urls列表每当任务最终失败就把(url, save_path, error_message)追加进去。全部跑完后统一写出到一个 CSV 文件。第二次重跑时可以只针对这个失败清单生成新的任务列表而不是重新扫描所有 URL。import csv def save_failed_tasks(failed_tasks, failed_file): with open(failed_file, w, encodingutf-8, newline) as f: writer csv.writer(f) writer.writerow([url, save_path, error]) writer.writerows(failed_tasks)CSV 的好处是可以用表格软件直接打开按错误类型排序快速判断失败原因集中在哪一类。如果几百条失败记录的 error 全是403那基本是请求频率被限制改善策略是降低线程数或增加重试间隔如果全是404则是图片源地址失效只能换数据源或跳过这些样本。这里有个关键点需要明白失败清单不是一个「一次性」的东西。你可以对同一份失败清单反复重跑每跑一次能恢复的临时性失败就被消化掉一批剩下的才是真正不可用的数据。我在实际使用中见过一个 5 万张图片的任务第一次跑失败 800 张重跑一次恢复 600 张剩下的 200 张里有一半是重复图片、一半是源站永久删除的孤儿链接。这个过程不需要改任何代码只是反复执行同一个重跑命令。5. ImageNet 下载避坑排查现象、原因与解决方案5.1 下载正常但打不开图片图片文件其实是错误页现象所有任务都显示下载成功文件大小也正常但用图像库加载时大量报错打开文件看到的是一段 HTML 文本。原因图片服务器对非法请求或失效链接返回了 404 页面但响应头里的状态码依然是 200。raise_for_status()无法察觉这种错误文件被原样写入磁盘。这类情况在 ImageNet 旧同步集里特别常见很多图片原始地址已经失效服务器用软 404 的方式应答。解决不要信任状态码在保存后立即检查文件头部魔数。JPEG 检查\xff\xd8\xffPNG 检查\x89PNG不符合就删除文件并计入失败清单。这一步不能省否则后续加载数据集时你会被一堆坏文件反复干扰。5.2 大量 403 错误请求频率太高触发了限流现象刚开始下载一切正常跑到几百张图之后突然连续出现 403 Forbidden并且后续几乎每张图都失败。原因短时间内的密集请求被图片服务器识别为爬虫行为对当前 IP 实施临时限流。线程数设置越大触发限流的时间点就越早。有些源站还会对无 UA 或 UA 雷同的请求做额外限制。解决首先确认请求头里带上了合理且唯一的User-Agent然后把线程数降到 4-8 个最后在重试逻辑里加入随机退避time.sleep(random.uniform(2, 5))。如果依然 403就需要暂停一段时间比如 30 分钟再继续或者更换网络出口。5.3 同一张图片在多个类里重复下载数据冗余与带宽浪费现象最终数据集的整体文件数大于预期而且用去重工具检查时发现不少重复图片但它们的保存路径属于不同类目录。原因ImageNet 本身是允许多标签的一张图片可能同时属于多个 synset。当你指定下载多个类时这张图片会出现在不同类的 URL 列表里于是被重复下载多次。这在做分类任务时会直接造成训练集与验证集之间的数据泄漏。解决在采样计划生成时维护一个全局 URL 集合。每处理一个类先检查当前 URL 是否已经在集合里如果已存在则跳过。这样能保证一张图片在整个数据集中只有一个副本从根源上避免数据泄漏问题。5.4 断点续传状态下文件数对不上状态文件记录的是 URL不是路径现象重新运行下载脚本后状态文件显示很多任务已完成但本地目录里找不到对应文件或者文件数为 0。原因状态文件记录的是 URL 字符串如果上一次运行时保存路径规则和本次不同比如目录名从 wnid 换成了可读类名状态判断会认为该 URL 已完成而跳过下载但实际上新路径下并没有文件。解决状态记录和保存路径必须绑定同一套规则。如果改了目录结构旧状态文件就失效了需要清空后重新下载。更稳妥的设计是在状态文件里同时记录 URL 和保存路径判断时两者匹配才算完成。5.5 图片源地址失效导致固定比例下载失败尽早接受数据损耗现象无论怎么重试总有 1%-3% 的图片下载失败错误信息要么是超时、要么是 404反复重试后依然失败。原因ImageNet 的图片来自互联网上不同的图库站点经过多年时间部分图片已经被源站删除或迁移。这部分数据损耗是客观存在的不是脚本 bug。解决不要在无效图片上无限重试。给自己设定一个可接受的失败率阈值比如 5%。如果失败率低于阈值直接接受这部分损耗在训练代码里做好样本数统计即可。如果失败率过高优先检查网络环境和请求频率而不是去逐条排查失效链接。6. 把自定义子集喂给训练框架目录规整与数据校验下载完成后最后一步是把散落的文件整理成主流深度学习框架能直接读取的结构。PyTorch 的torchvision.datasets.ImageFolder期望的目录结构是root/class_name/image.jpg其中 class_name 是可读类名而不是 WNID。所以一般做法是把下载好的wnid目录批量重命名为对应的可读类名再做一次全量校验确认每个目录下的图片数量符合预期。import os import json def rename_to_classnames(root_dir, plan_file): with open(plan_file, r, encodingutf-8) as f: plan json.load(f) for wnid, info in plan.items(): old_path os.path.join(root_dir, wnid) new_path os.path.join(root_dir, info[class_name]) if os.path.exists(old_path): os.rename(old_path, new_path)重命名之前先看一下class_name是否包含/或其他非法字符某些可读类名里带斜杠会破坏目录层级。碰到这种情况可以先把类名里的非法字符替换成下划线保证目录结构合法。重命名之后再用一段校验脚本扫描每个目录统计实际有效图片数量比计划文件里的目标数量少了多少做到心里有数。我自己习惯在最后一步把训练和验证做一次随机划分按比例切成train/和val/两个大目录避免后续每次实验都重新切分。这个动作只需要几十行代码但能让整个自定义数据集变成一个标准可复用的资源后续不管换什么框架都能直接读取。说到底这个下载器真正省事的地方不是「下载」本身而是把 ImageNet 从「一个需要全量拥抱的庞然大物」变成了「一个可以按需裁剪的工具箱」。先跑最小规模把链路验证通再逐步扩大类数和每类图片数是我每次做类似任务都要坚持的习惯。项目跑到一半发现参数不合理宁可停下来重新生成计划也不要在一个错误的数据集上继续往下走。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑