news 2026/10/1 12:09:02

自定义image captioning数据集格式整理与清洗实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自定义image captioning数据集格式整理与清洗实战指南

简介:这份资源面向从事图像描述(image captioning)研究与开发的算法工程师、研究生及高年级本科生,聚焦自定义数据集从零构建到可直接训练的全流程格式整理。内容围绕数据集结构设计、图像与caption的JSON组织方式,以及训练集、验证集、测试集划分等核心环节展开,并覆盖BUTD特征构建、AOA注意力机制适配、DLCT网格特征规整、224×224图像统一缩放等模型输入预处理要点,帮助读者打通数据准备到模型训练的衔接链路。资源包共10个文件,全部为Python脚本,压缩后约11KB,按编号顺序对应合并原始数据、划分数据集、生成annotations、提取特征、规整训练文件等步骤,结构清晰、便于按需调用。目前已有214人学习下载,适合希望系统掌握captioning数据工程细节、减少重复造轮子的读者参考借鉴。

1. 自定义 image captioning 数据集到底要整理成什么样

你手里可能有一批图,每张图配了一句或几句中文描述,想拿来训一个 image captioning 模型。真正动手时第一个卡住的地方往往不是模型结构,而是数据格式:图片放哪、描述写哪、编码用什么、一条样本长什么样。我见过太多人卡在这一步,把时间耗在写胶水脚本上,而不是调模型。

这篇讲的就是自定义 image captioning 数据集的格式整理。核心目标只有一个:把散落在文件夹、Excel、txt 里的图文对,整理成训练框架能直接吃的标准结构,并且保证后续换框架、加字段、做清洗时不用推倒重来。适合两类人:一是第一次自己攒图文数据、准备微调 caption 模型的新手;二是已经有数据但格式混乱、想统一成可维护结构的熟手。下面从格式设计讲到落地脚本,再到踩过的坑,尽量让你照着就能跑通。

2. image captioning 数据集的三种主流格式与选型

2.1 为什么格式选错会让后面每一步都难受

image captioning 的数据本质是「一张图对应一到多条文本描述」。听起来简单,但不同框架对这份对应关系的组织方式差别很大。常见的有三类:一是每张图一个同名 txt,图片和文本靠文件名绑定;二是集中式 JSON,所有图文对写在一个文件里;三是类 COCO 的 annotations 结构,图片、描述、划分各自成表。

选型不是看哪个高级,而是看你的数据规模和后续动作。数据量在几千张以内、描述只有一句,同名 txt 最省事,肉眼可查,出问题直接打开文件看。数据上万、每图多句描述、还要做训练/验证/测试划分,集中式 JSON 更好维护,改一个字段不用动几千个文件。如果你打算复用现成的 COCO 评测脚本或直接对接某些训练框架的默认 loader,那 COCO 风格的结构兼容性最好。

我一般的判断顺序是:先确认训练框架默认吃什么,再确认描述条数,最后确认要不要频繁做划分和清洗。三个问题答完,格式基本就定了。下面这张表是我常用的对照,参数都是实操里反复验证过的。

格式类型适用规模每图描述数划分支持主要风险
同名 txt千张以内1 句靠文件夹文件数爆炸,易漏配
集中式 JSON万张级1~5 句字段控制单文件大,需流式读
COCO 风格万张以上多句独立划分文件结构层级多,易写错 id

2.2 集中式 JSON 的字段设计:别只存 image 和 caption

很多人第一版 JSON 就两个字段:image和caption。跑通没问题,但一旦要做数据清洗、去重、多描述、来源追溯,就得回头改结构,所有下游脚本跟着改。我的习惯是一开始就把字段留够,哪怕暂时用不上。

一个可维护的样本结构大致是这样:image存相对路径而不是绝对路径,方便迁移;caption存文本列表而不是单字符串,天然支持多描述;split存 train/val/test;source记录数据来源,方便排查脏数据;id用稳定唯一值,别用行号,因为排序一变行号就废了。下面是我常用的字段定义。

{ "id": "sample_000001", "image": "images/000001.jpg", "captions": [ "一只橘猫趴在窗台上晒太阳", "窗边的橘色猫咪正在休息" ], "split": "train", "source": "self_collected_2024", "width": 640, "height": 480 }

逻辑说明:id用固定前缀加序号,保证跨次生成稳定;image用相对路径,整个数据集目录搬到别的机器不用改;captions是列表,训练时随机采一条或全部用都行;split让划分信息内聚在样本里,不用额外维护三个文件;width/height可选,但做分辨率过滤和分桶训练时很有用。

参数说明:id建议零填充到固定位数,排序时不会出现 1、10、2 这种乱序;image路径分隔符统一用正斜杠,Windows 和 Linux 都能读;captions里每条描述建议去掉首尾空白,空字符串直接丢弃,别留占位。

2.3 从零散文件到标准 JSON 的整理流程

假设你现在的原始数据是:一个raw_images文件夹放图,一个captions.txt每行是「文件名\t描述」,或者一个 Excel 两列。整理流程分四步:扫描图片、读取描述、配对校验、写出 JSON。

第一步扫描图片时,别只认 jpg,png、jpeg、webp 都要覆盖,同时记录文件大小,后面过滤损坏图用得上。第二步读描述,注意编码,中文 txt 常见 utf-8 和 gbk 两种,读错就是乱码。第三步配对,用文件名做键,图片有描述没配上的、描述有图片找不到的,都要单独记下来,这是最常见的脏数据来源。第四步写出,建议同时输出一份统计信息,比如总样本数、无描述图片数、无图片描述数。

import os import json import hashlib IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"} def build_id(path): # 用路径哈希生成稳定 id,避免行号漂移 return "sample_" + hashlib.md5(path.encode("utf-8")).hexdigest()[:12] def load_captions(txt_path): pairs = {} with open(txt_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue # 约定用制表符分隔,避免描述里含空格被切错 parts = line.split("\t") if len(parts) < 2: continue fname, cap = parts[0].strip(), parts[1].strip() if cap: pairs.setdefault(fname, []).append(cap) return pairs def collect_images(img_dir): result = {} for root, _, files in os.walk(img_dir): for name in files: ext = os.path.splitext(name)[1].lower() if ext in IMG_EXTS: full = os.path.join(root, name) rel = os.path.relpath(full, img_dir) result[name] = rel.replace(os.sep, "/") return result def build_dataset(img_dir, cap_txt, out_json): images = collect_images(img_dir) caps = load_captions(cap_txt) samples, missing_cap, missing_img = [], [], [] for fname, rel in images.items(): if fname in caps: samples.append({ "id": build_id(rel), "image": rel, "captions": caps[fname], "split": "train", "source": "raw" }) else: missing_cap.append(fname) for fname in caps: if fname not in images: missing_img.append(fname) with open(out_json, "w", encoding="utf-8") as f: json.dump(samples, f, ensure_ascii=False, indent=2) print(f"有效样本: {len(samples)}") print(f"有图无描述: {len(missing_cap)}") print(f"有描述无图: {len(missing_img)}") return samples if __name__ == "__main__": build_dataset("raw_images", "captions.txt", "dataset.json")

逻辑说明:build_id用路径哈希而不是递增序号,保证同一张图多次生成 id 一致,做增量更新时不会错位。load_captions用制表符分隔,因为中文描述里常带空格,用空格切会切错。collect_images递归扫描并统一路径分隔符,跨平台不会出问题。最后把「有图无描述」和「有描述无图」分别统计,这两类就是后续要人工处理的脏数据。

参数说明:IMG_EXTS按你实际数据增删,加了新格式记得同步;split这里先全给 train,划分放到下一步单独做;ensure_ascii=False必须加,否则中文会变成转义序列,肉眼没法看;indent=2方便人工检查,正式训练前可以压成一行减小体积。

3. 划分、清洗与多描述处理:让数据集真正能训

3.1 训练验证测试划分的三个硬约束

划分看着简单,随机切一刀就行,但实际有三个约束容易忽略。第一,同一张图的多条描述必须落在同一个 split,否则验证集里出现训练时见过的图,指标虚高。第二,划分比例要按图片数算,不是按描述条数算,否则多描述的图会稀释比例。第三,划分要可复现,用固定随机种子,别每次跑结果都不一样。

我一般按 8:1:1 切,数据量小于一千时改成 7:1.5:1.5,保证验证集有足够样本。划分时先按图片聚合,再对图片列表打乱,最后按比例切分,把 split 写回每条样本。

import random from collections import defaultdict def split_dataset(samples, ratios=(0.8, 0.1, 0.1), seed=42): random.seed(seed) # 按图片聚合,保证同图多描述不跨 split by_image = defaultdict(list) for s in samples: by_image[s["image"]].append(s) images = list(by_image.keys()) random.shuffle(images) n = len(images) n_train = int(n * ratios[0]) n_val = int(n * ratios[1]) train_imgs = set(images[:n_train]) val_imgs = set(images[n_train:n_train + n_val]) for img, group in by_image.items(): if img in train_imgs: sp = "train" elif img in val_imgs: sp = "val" else: sp = "test" for s in group: s["split"] = sp return samples

逻辑说明:先按image聚合成组,再对图片维度打乱和切分,这样同一张图的所有描述自然进同一个 split。seed固定保证可复现。切分用图片数算比例,不受多描述影响。

参数说明:ratios三个数之和必须为 1,否则切分数量对不上;seed建议写进配置,换数据集时改一下;如果某类图片特别少,可以考虑分层抽样,但大多数场景随机切够用。

3.2 描述文本清洗:中文场景下必须处理的几类脏数据

中文 caption 的脏数据比英文更隐蔽。常见的有:全角半角混用、多余空格、结尾标点不统一、繁体简体混杂、以及从网页复制带进来的不可见字符。这些不处理,训练时 tokenizer 会多出一堆奇怪 token,模型学得慢还容易输出乱码。

清洗顺序建议是:先去不可见字符,再统一标点,再处理空格,最后做长度过滤。长度过滤别一刀切,中文描述一般 5 到 50 字比较合理,太短的可能是「图」「照片」这种无效描述,太长的可能是误粘的段落。

import re import unicodedata def clean_caption(text): if not text: return "" # 去掉零宽字符和不可见控制符 text = re.sub(r"[\u200b-\u200f\u2028-\u202f\ufeff]", "", text) # 全角转半角(保留中文标点) text = unicodedata.normalize("NFKC", text) # 合并多余空白 text = re.sub(r"\s+", " ", text).strip() # 统一结尾标点,中文描述结尾不加句号更常见 text = text.rstrip("。.!!??") return text def filter_captions(samples, min_len=5, max_len=50): kept = [] for s in samples: caps = [clean_caption(c) for c in s["captions"]] caps = [c for c in caps if min_len <= len(c) <= max_len] if caps: s["captions"] = caps kept.append(s) return kept

逻辑说明:NFKC归一化会把全角字母数字转半角,同时保留中文标点,是中文清洗里比较稳的一步。零宽字符用正则显式去掉,这类字符肉眼看不见但会进 tokenizer。结尾标点统一去掉,是因为中文 caption 数据集里带句号和不带句号混在一起很常见,统一后模型输出更干净。

参数说明:min_len和max_len按你的数据分布调,可以先统计一下长度直方图再定;filter_captions里如果一张图所有描述都被过滤掉,整条样本丢弃,避免出现空 captions 的样本。

3.3 多描述样本的两种用法与取舍

一张图多条描述,训练时有两种用法。一是随机采一条,每个 epoch 采到的可能不同,相当于数据增强,实现简单,大多数框架默认这么干。二是全部拼接成一条长描述,信息全但长度翻倍,容易超长截断,而且拼接后语义不自然。

我的经验是:描述之间差异大(比如一条讲主体、一条讲场景)时用随机采样;描述之间高度相似(只是换词)时,可以只保留质量最高的一条,减少冗余。判断标准很简单,把同一张图的两条描述放一起读,如果读起来像在说两件不同的事,就保留多条;如果只是同义改写,留一条就够。

import random def sample_caption(sample, mode="random"): caps = sample["captions"] if not caps: return "" if mode == "random": return random.choice(caps) elif mode == "first": return caps[0] elif mode == "concat": # 拼接时用分隔符,方便后续切分 return ";".join(caps) return caps[0]

逻辑说明:random模式配合固定 seed 可复现;first模式适合描述已按质量排序的情况;concat模式要配合长度检查,超长直接截断或退回first。

参数说明:mode建议写进训练配置,方便对比实验;拼接分隔符用中文分号,比逗号更不容易和描述内部标点混淆。

4. 避坑与排查:整理 image captioning 数据集时最容易翻车的五件事

4.1 图片能打开但训练报错,多半是通道或位深问题

现象:用 PIL 打开图片正常,训练时却报 shape 或 channel 错误。原因:部分 png 带 alpha 通道是四通道,部分灰度图是单通道,模型默认吃三通道 RGB。解决:整理阶段统一转 RGB,遇到四通道丢弃 alpha,遇到单通道复制成三通道。这一步放在预处理脚本里,别等到训练时才发现。

4.2 中文描述乱码,编码判断不能靠猜

现象:读出来的描述是「涓枃」这类乱码。原因:文件实际是 gbk 或 gb18030,却用 utf-8 读。解决:先尝试 utf-8,失败再试 gb18030,还失败就报错让人工确认,别静默跳过。更稳的做法是整理阶段统一转成 utf-8 存储,后续只认一种编码。

4.3 路径用绝对路径,换台机器全废

现象:本地跑通,换机器或换目录后所有图片找不到。原因:JSON 里存了绝对路径。解决:一律存相对路径,读取时用数据集根目录拼接。迁移时整个目录搬走即可,不用改任何配置。

4.4 划分后验证集指标异常高,检查是否同图跨 split

现象:验证 loss 低得离谱,生成结果和训练集高度相似。原因:同一张图的多条描述被分到不同 split,等于验证集泄了训练集。解决:按图片聚合后再划分,划分完做一次校验,确认没有图片同时出现在两个 split。

4.5 描述里混入文件名或编号,模型学会输出无意义串

现象:模型生成结果里出现「IMG_20240101」这类内容。原因:原始描述文件里有些行把文件名当描述,或者编号列没去掉。解决:清洗阶段加规则过滤,纯数字、纯文件名模式、长度过短的描述直接丢弃,并统计丢弃数量,数量异常大时回头查原始数据。

5. 用校验脚本给数据集上保险,以及一个我常留的后悔药

整理完不是写完 JSON 就结束,我习惯加一个校验脚本,每次数据更新后跑一遍。校验项包括:每条样本图片文件存在、captions 非空、split 取值合法、id 全局唯一、图片能被正常解码。这几项能挡住九成以上的低级错误,比训练时报错再回头查省事得多。

import json import os from PIL import Image def validate(json_path, root_dir): with open(json_path, "r", encoding="utf-8") as f: samples = json.load(f) ids = set() errors = [] for s in samples: if s["id"] in ids: errors.append(f"重复 id: {s['id']}") ids.add(s["id"]) img_path = os.path.join(root_dir, s["image"]) if not os.path.exists(img_path): errors.append(f"图片缺失: {s['image']}") continue try: with Image.open(img_path) as im: im.verify() except Exception as e: errors.append(f"图片损坏: {s['image']} -> {e}") if not s.get("captions"): errors.append(f"空描述: {s['id']}") if s.get("split") not in {"train", "val", "test"}: errors.append(f"非法 split: {s['id']}") print(f"样本总数: {len(samples)}") print(f"错误数: {len(errors)}") for e in errors[:20]: print(" -", e) return errors if __name__ == "__main__": validate("dataset.json", ".")

逻辑说明:im.verify()只检查文件头,速度快,适合大批量扫描;重复 id 用集合查,O(1);错误只打印前 20 条,避免刷屏,完整列表可以写文件。这个脚本我一般挂在数据更新流程末尾,跑通才允许进训练。

参数说明:root_dir是数据集根目录,和 JSON 里相对路径拼接;如果图片量特别大,verify可以改成只检查文件存在,跳过解码,速度更快但漏检损坏图。

最后留一个我自己的习惯,也算后悔药:每次生成dataset.json时,同时存一份带时间戳的副本,比如dataset_20240101.json。数据清洗和划分都是不可逆操作,改错了想回退,有历史版本能省几小时。这个习惯帮我救过两次场,一次是划分脚本写错把验证集切没了,一次是清洗规则太激进删掉了一半样本。数据集整理这件事,稳比快重要,多留一份副本不亏。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 12:09:01

基于958张虎数据集VOC与YOLO双格式的YOLOv8自定义训练全流程

简介&#xff1a;这份虎目标检测数据集面向计算机视觉初学者与需要快速验证检测模型的研究者&#xff0c;解决虎类目标样本获取与标注成本高的问题。数据以VOC与YOLO双格式提供&#xff0c;jpg图片与对应的xml、txt标注文件一一对应&#xff0c;可直接接入主流检测框架训练与评…

作者头像 李华
网站建设 2026/10/1 12:08:49

vCenter日志满导致VAMI 5480打不开的应急清理与恢复

凌晨两点被监控电话叫醒&#xff0c;说 vCenter 管理界面打不开&#xff0c;5480 端口也不响应。爬起来 SSH 上去敲了一条df -h&#xff0c;/storage/log 那一行红得刺眼——100%&#xff0c;一个字节都不剩。这是我最近一次处理 vCenter 日志满问题的现场&#xff0c;也是很多…

作者头像 李华
网站建设 2026/10/1 12:08:07

Fedora部署搜狗拼音输入法的兼容性挑战与替代方案

1. 项目概述&#xff1a;Fedora 上部署搜狗拼音输入法的现实路径与本质矛盾“Fedora 搜狗拼音输入法 rpm 包”——这十个字背后&#xff0c;不是一条简单的下载安装流程&#xff0c;而是一场持续十年以上的生态博弈。我从 Fedora 14 时代开始在笔记本上装搜狗输入法&#xff0c…

作者头像 李华
网站建设 2026/10/1 12:08:07

C++ deque完全指南:底层原理、性能对比与实战避坑

C的STL容器家族里&#xff0c;deque&#xff08;双端队列&#xff09;一直是个“存在感不强但相当能打”的角色。学完vector和list之后&#xff0c;很多人会下意识跳过它&#xff0c;觉得不过是个“两头都能插的 vector”&#xff0c;真到用的时候又想不起来。但只要你写过滑动…

作者头像 李华
网站建设 2026/10/1 12:06:26

扩展卡尔曼滤波EKF实现锂离子电池SOC估计:模型、原理与Matlab代码

在电池管理系统&#xff08;BMS&#xff09;的日常开发里&#xff0c;SOC&#xff08;State of Charge&#xff0c;电荷状态&#xff09;估计一直是个既基础又让人头疼的问题。它不像测电压电流那样直接读个传感器就行&#xff0c;而是一个典型的“隐状态”问题——你永远没法拿…

作者头像 李华
网站建设 2026/10/1 12:06:01

Vue3+Vite项目图标方案全解析:从选型到性能优化与避坑指南

做Vue3项目&#xff0c;图标这个东西看着不起眼&#xff0c;真到用起来才发觉水很深。尤其是后台管理系统、商城这类页面多、功能杂的管理端&#xff0c;菜单、按钮、状态提示、空状态哪里都要图标&#xff0c;一个项目少说几十个图标&#xff0c;多了一两百个也不奇怪。我最早…

作者头像 李华