ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

自定义Image Captioning数据集格式整理:10个Python脚本从原始文件到可训练数据

自定义Image Captioning数据集格式整理:10个Python脚本从原始文件到可训练数据 简介这份资源面向从事图像描述image captioning研究与开发的算法工程师、研究生及高年级本科生聚焦自定义数据集构建这一关键环节帮助解决原始图像与文本描述格式不统一、难以直接喂入模型的问题。压缩包共10个文件全部为Python脚本整体约11KB涵盖数据合并、训练/验证/测试集划分、标注文件生成、BUTD特征构建、AOA与DLCT网格特征规整、224×224图像统一缩放等完整流程脚本按编号顺序组织便于按需调用与二次修改。已有214人学习下载说明该整理方案在同类任务中具有一定参考价值。读者可据此快速搭建从原始素材到可直接训练的数据管线理解多来源caption数据的合并逻辑、划分比例控制与特征文件生成方式减少在格式对齐与预处理上的重复试错成本适合需要落地image captioning项目或复现相关模型的中高级开发者。1. 从一堆散图到能训的 caption 数据这套脚本到底省掉了哪几步手里有一批图每张图配了几句人写的描述想拿去做 image captioning 训练结果卡在第一步——数据根本喂不进去。图像尺寸五花八门caption 散在 txt、csv、json 里训练集验证集测试集没分特征文件格式对不上模型要求。这不是模型的问题是数据格式整理没做。这份「自定义 image captioning 数据集的格式整理完整版」就是冲着这个环节来的里面是 10 个 Python 脚本从原始文件合并一路做到直接可训练的文件规整覆盖了 UIC 数据格式、BUTD 特征、AOA 模型输入、DLCT 网格特征、224×224 图像缩放这几条常见技术路线。适合正在自己搭 caption 数据集、被格式问题反复卡住的人也适合想看清楚一套完整预处理流水线长什么样的从业者。下面按脚本编号顺序拆重点讲每个脚本干什么、参数怎么改、哪里容易翻车。2. 数据合流与划分1-1 到 1-3 三个脚本把原始文件变成可切分的数据集2.1 1-1 合并原始文件到 uic_new_split先把散落的数据收进一个 JSON原始数据往往不是一个文件可能是多个来源的 caption 文件、不同批次的图像列表甚至手工整理的 Excel。1-1 这个脚本干的事就是把这些来源统一读进来输出一个结构一致的 JSON通常命名为uic_new_split.json或类似名字。核心逻辑是遍历输入文件、按图像 ID 聚合 caption、去重、补全缺失字段。import json import os from collections import defaultdict # 输入多个来源的 caption 文件每行格式假设为 image_id\tcaption source_files [ raw_data/captions_batch1.txt, raw_data/captions_batch2.txt, raw_data/manual_annotations.csv ] # 用 defaultdict 按 image_id 聚合 caption image_captions defaultdict(list) for filepath in source_files: with open(filepath, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue # 按制表符或逗号切分具体分隔符看原始文件 parts line.split(\t) if \t in line else line.split(,) if len(parts) 2: continue image_id parts[0].strip() caption parts[1].strip() # 去重同一张图的相同 caption 不重复加入 if caption not in image_captions[image_id]: image_captions[image_id].append(caption) # 输出统一 JSON output [] for image_id, captions in image_captions.items(): output.append({ image_id: image_id, captions: captions, split: unsplit # 占位后续脚本会改 }) with open(uic_new_split.json, w, encodingutf-8) as f: json.dump(output, f, ensure_asciiFalse, indent2) print(f合并完成共 {len(output)} 张图像)这段代码的关键参数是source_files列表和分隔符判断。常见做法是把所有原始文件路径写死在列表里但如果文件多可以改成glob.glob(raw_data/*.txt)自动收集。分隔符那里用了一个简单判断实际数据里可能是逗号、制表符、竖线需要根据真实文件调整。去重逻辑用的是列表成员判断数据量大时建议换成 set 加速。输出 JSON 里每张图保留image_id、captions列表和一个split占位字段方便后续脚本直接改这个字段来划分数据集。注意如果原始 caption 里有中文ensure_asciiFalse必须加否则 JSON 里全是转义字符后面读的时候容易出乱码。2.2 1-2 划分训练集、验证集与测试集比例和随机种子都要固定合并完的 JSON 里所有图像都是unsplit状态1-2 脚本负责按比例切分。captioning 任务里常见的比例是 8:1:1 或 7:1:2具体看数据量。数据量小于 1 万张时建议 7:1:2验证集和测试集各留够至少 500 张否则评估指标波动会很大。import json import random # 固定随机种子保证每次运行划分结果一致 random.seed(42) with open(uic_new_split.json, r, encodingutf-8) as f: data json.load(f) # 打乱顺序 random.shuffle(data) total len(data) train_ratio, val_ratio 0.8, 0.1 train_end int(total * train_ratio) val_end train_end int(total * val_ratio) for i, item in enumerate(data): if i train_end: item[split] train elif i val_end: item[split] val else: item[split] test with open(uic_new_split.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) # 打印各集合数量方便核对 from collections import Counter counts Counter(item[split] for item in data) print(ftrain: {counts[train]}, val: {counts[val]}, test: {counts[test]})random.seed(42)这行是血泪经验——不固定种子每次跑出来的划分不一样模型评估结果没法复现调参时根本分不清是模型变了还是数据变了。比例参数train_ratio和val_ratio按需改剩下的自动归 test。打乱之后再切分避免原始数据按类别或来源有序排列导致某个集合偏斜。打印计数是必须的曾经有一次 val 只有 3 张图训练时 loss 曲线看着正常一评估就发现指标完全不可信。2.3 1-3 更改 uic.json 的训练、验证与测试归类字段名和格式要对齐下游1-2 改的是uic_new_split.json但有些模型代码读的是uic.json字段名可能不一样比如用phase而不是split或者要求每个 split 单独一个文件。1-3 脚本就是做这个适配的。常见做法是读入 1-2 的输出按 split 分组分别写出train.json、val.json、test.json或者统一写一个文件但把字段名改成下游期望的。import json with open(uic_new_split.json, r, encodingutf-8) as f: data json.load(f) # 按 split 分组 splits {train: [], val: [], test: []} for item in data: split item.get(split, train) # 字段名适配下游可能用 phase 而不是 split record { image_id: item[image_id], caption: item[captions], # 注意这里 captions 变成 caption phase: split } splits[split].append(record) # 分别写出 for split_name, records in splits.items(): with open(fuic_{split_name}.json, w, encodingutf-8) as f: json.dump(records, f, ensure_asciiFalse, indent2) print(f{split_name}: {len(records)} 条)这里有个容易翻车的点字段名从captions变成caption值从列表变成列表本身下游代码如果按单条 caption 读可能期望的是字符串而不是列表。我一般会保留列表结构但在文档里写清楚。另一个坑是phase的取值有的代码要求train/val/test有的要求training/validation/testing改之前先看一眼模型的数据加载代码。3. 标注文件与特征构建1-4 到 1-6 把图像和文本转成模型能吃的格式3.1 1-4 生成 annotations 系列文件词汇表和图像 ID 映射是重点captioning 模型训练时需要把 caption 里的词转成索引这就需要一个词汇表文件通常叫vocab.json或word2idx.json。同时还需要图像 ID 到文件路径的映射以及每个 split 的标注文件。1-4 脚本一般做三件事统计所有 caption 里的词频、按阈值过滤低频词、生成词汇表和对应的标注文件。import json from collections import Counter with open(uic_new_split.json, r, encodingutf-8) as f: data json.load(f) # 统计词频 word_counter Counter() for item in data: for caption in item[captions]: # 简单按空格切分中文需要另外的分词逻辑 words caption.lower().split() word_counter.update(words) # 过滤低频词阈值一般设 3 到 5 min_freq 3 vocab {pad: 0, start: 1, end: 2, unk: 3} for word, freq in word_counter.most_common(): if freq min_freq: vocab[word] len(vocab) with open(vocab.json, w, encodingutf-8) as f: json.dump(vocab, f, ensure_asciiFalse, indent2) # 生成图像 ID 到路径的映射 id2path {} for item in data: image_id item[image_id] # 假设图像统一放在 images/ 目录下扩展名按实际情况改 id2path[image_id] fimages/{image_id}.jpg with open(id2path.json, w, encodingutf-8) as f: json.dump(id2path, f, ensure_asciiFalse, indent2) print(f词汇表大小: {len(vocab)})min_freq这个参数直接影响词汇表大小和unk的比例。设太高很多词变成 unknown模型学不到细节设太低词汇表膨胀嵌入层参数变多小数据集上容易过拟合。我一般先在 3 到 5 之间试然后看unk在验证集上的占比超过 5% 就调低阈值。中文 caption 不能直接用split()需要先分词常见做法是用 jieba 或者按字符切分具体看模型预期。3.2 1-5 BUTD 特征构建Faster R-CNN 的输出怎么存BUTDBottom-Up and Top-Down特征在 captioning 里很常用核心是用 Faster R-CNN 在图像上检测物体区域每个区域提取一个特征向量再配上区域的空间位置。1-5 脚本一般调用预训练检测模型对每张图输出固定数量的区域特征存成.npy或.h5文件。import numpy as np import json import os # 假设已经用检测模型提取好了特征存在 features/ 目录下 # 每个文件是 (num_regions, feature_dim) 的 numpy 数组 feature_dir features feature_dim 2048 max_regions 50 with open(id2path.json, r, encodingutf-8) as f: id2path json.load(f) # 构建特征索引文件 feature_index {} for image_id in id2path: feat_path os.path.join(feature_dir, f{image_id}.npy) if not os.path.exists(feat_path): print(f缺失特征: {image_id}) continue feat np.load(feat_path) # 统一区域数量不足补零超出截断 if feat.shape[0] max_regions: pad np.zeros((max_regions - feat.shape[0], feat.shape[1])) feat np.vstack([feat, pad]) else: feat feat[:max_regions] np.save(os.path.join(feature_dir, f{image_id}_fixed.npy), feat) feature_index[image_id] f{image_id}_fixed.npy with open(feature_index.json, w, encodingutf-8) as f: json.dump(feature_index, f, ensure_asciiFalse, indent2) print(f特征处理完成共 {len(feature_index)} 张)max_regions设 50 是常见做法但具体看检测模型输出和显存。区域数不统一会导致 batch 拼接失败所以必须补齐或截断。补齐用零向量截断按置信度排序取 top-N。特征维度feature_dim要和模型输入对齐Faster R-CNN 的 fc7 层通常是 2048 维但有些实现用 1024 或 4096改之前确认一下。3.3 1-6 归整 AOA 所需文件注意力模型的输入格式适配AOAAttention on Attention模型对输入格式有特定要求通常需要图像特征、区域位置、caption 索引三部分对齐。1-6 脚本把前面生成的特征文件、词汇表、标注文件整合成 AOA 能直接读的格式常见的是.h5或.pth文件里面按 split 存好每个样本的特征和 caption 索引。import json import numpy as np import h5py with open(uic_new_split.json, r, encodingutf-8) as f: data json.load(f) with open(vocab.json, r, encodingutf-8) as f: vocab json.load(f) with open(feature_index.json, r, encodingutf-8) as f: feature_index json.load(f) # 按 split 写入 h5 for split in [train, val, test]: split_data [item for item in data if item[split] split] with h5py.File(faoa_{split}.h5, w) as h5f: for i, item in enumerate(split_data): image_id item[image_id] if image_id not in feature_index: continue feat np.load(ffeatures/{feature_index[image_id]}) h5f.create_dataset(ffeatures/{i}, datafeat) # caption 转索引 caption item[captions][0] # 取第一条多 caption 需要另外处理 tokens caption.lower().split() indices [vocab.get(start, 1)] indices [vocab.get(w, vocab[unk]) for w in tokens] indices.append(vocab.get(end, 2)) h5f.create_dataset(fcaptions/{i}, datanp.array(indices)) print(f{split} 写入完成)这里取captions[0]是简化处理实际训练时可能每轮随机选一条 caption 做数据增强。AOA 模型通常要求 caption 长度固定需要在写入前做 padding 或截断代码里没展开但实际用的时候要加。h5 文件的 key 命名要和模型的数据加载代码对齐改之前先看模型源码里怎么读的。4. 网格特征与直接训练文件1-7 到 1-9 补齐另外几条技术路线4.1 1-7 规整网格特征文件DLCT网格划分和特征维度要对齐DLCTDeep Grid Context Transformer用的是网格特征不是区域特征。做法是把图像均匀划分成 N×N 个网格每个网格提取一个特征向量再按空间顺序排列。1-7 脚本负责把图像转成这种网格特征格式常见的是 7×7 或 14×14 网格特征维度通常是 512 或 2048。import numpy as np import os import json # 假设用 CNN 提取网格特征输出 (H, W, C) 的特征图 # 这里模拟一个 7x7x2048 的特征图 grid_size 7 feature_dim 2048 with open(id2path.json, r, encodingutf-8) as f: id2path json.load(f) grid_feature_dir grid_features os.makedirs(grid_feature_dir, exist_okTrue) for image_id in id2path: # 实际使用时这里应该是 CNN 前向传播的输出 # 这里用随机数模拟 feat_map np.random.randn(grid_size, grid_size, feature_dim).astype(np.float32) # 展平成 (grid_size*grid_size, feature_dim) feat_flat feat_map.reshape(-1, feature_dim) np.save(os.path.join(grid_feature_dir, f{image_id}.npy), feat_flat) print(f网格特征生成完成共 {len(id2path)} 张)grid_size和feature_dim必须和模型定义一致改一个另一个也要跟着改。展平后的顺序是按行优先模型里如果按列优先读特征就错位了。常见做法是在文件里存一个 meta 信息记录 grid_size 和排列顺序但很多开源代码直接写死改的时候要两边对齐。4.2 1-8 规整直接训练所需文件把所有依赖收进一个目录到这一步图像、特征、词汇表、标注文件都齐了但散在不同目录。1-8 脚本做最后的归整把所有训练需要的文件复制或软链到一个统一目录下生成一个dataset_config.json记录各文件路径和参数。这样训练脚本只需要读一个配置不用到处找文件。import json import os import shutil # 配置输出目录 output_dir caption_dataset os.makedirs(output_dir, exist_okTrue) # 复制关键文件 files_to_copy { vocab.json: vocab.json, id2path.json: id2path.json, uic_train.json: annotations/train.json, uic_val.json: annotations/val.json, uic_test.json: annotations/test.json, } for src, dst in files_to_copy.items(): dst_path os.path.join(output_dir, dst) os.makedirs(os.path.dirname(dst_path), exist_okTrue) if os.path.exists(src): shutil.copy(src, dst_path) print(f复制 {src} - {dst_path}) else: print(f缺失: {src}) # 生成配置文件 config { vocab_file: vocab.json, id2path_file: id2path.json, train_annotation: annotations/train.json, val_annotation: annotations/val.json, test_annotation: annotations/test.json, feature_dir: features, image_dir: images, max_caption_length: 20, vocab_size: 10000 } with open(os.path.join(output_dir, dataset_config.json), w, encodingutf-8) as f: json.dump(config, f, ensure_asciiFalse, indent2) print(数据集归整完成)max_caption_length和vocab_size这两个参数要和训练脚本里的模型定义一致不一致的话要么报错要么静默出错。我一般会在训练脚本开头加一个校验读 config 之后检查 vocab_size 和实际词汇表大小是否匹配不匹配直接抛异常比训练到一半才发现强。4.3 1-9 规整 224×224 图片数据集缩放、裁剪和格式统一VGG、ResNet 这类骨干网络通常要求输入 224×224。1-9 脚本把原始图像统一缩放到这个尺寸同时处理格式转换比如 PNG 转 JPG、通道统一灰度转 RGB、文件名规范化。常见做法是用 PIL 或 OpenCV 做 resize但要注意保持宽高比还是直接拉伸。from PIL import Image import os import json target_size (224, 224) with open(id2path.json, r, encodingutf-8) as f: id2path json.load(f) output_dir images_224 os.makedirs(output_dir, exist_okTrue) for image_id, src_path in id2path.items(): if not os.path.exists(src_path): print(f缺失: {src_path}) continue img Image.open(src_path).convert(RGB) # 直接 resize 到 224x224不保持宽高比 img_resized img.resize(target_size, Image.BILINEAR) dst_path os.path.join(output_dir, f{image_id}.jpg) img_resized.save(dst_path, JPEG, quality95) print(f图像缩放完成共 {len(id2path)} 张)Image.BILINEAR是双线性插值速度和质量折中。如果对质量要求高可以用Image.LANCZOS但慢一些。直接拉伸会改变宽高比如果原始图像不是正方形物体可能变形。常见做法是先按短边缩放再中心裁剪但那样会丢失边缘信息。captioning 任务里两种做法都有看模型训练时的数据增强策略。保存质量设 95 是避免 JPEG 压缩伪影影响特征提取但文件会大一些。5. 避坑与排查这 10 个脚本跑下来最容易翻车的几个地方5.1 图像 ID 对不上导致特征和 caption 错位现象训练时 loss 正常下降但生成的 caption 和图像内容完全无关。原因特征文件按文件名索引caption 按 JSON 里的 image_id 索引两边 ID 格式不一致比如一个带扩展名一个不带或者大小写不同。解决在 1-1 合并阶段就统一 ID 格式去掉扩展名、统一小写、去掉前后空格。跑完 1-4 之后加一步校验遍历所有 image_id检查 id2path、feature_index、标注文件里是否都能找到缺一个就打印出来。5.2 词汇表里特殊 token 的索引被覆盖现象模型训练时start和end的预测概率始终很低生成的 caption 没有正常结尾。原因1-4 生成词汇表时特殊 token 先占了 0 到 3但后面统计词频时如果某个词恰好也叫pad或start会把索引覆盖掉。解决统计词频前先把特殊 token 从词表里排除或者用不可能出现在真实 caption 里的字符串作为 key比如__pad__、__start__。5.3 特征文件补齐用零向量导致注意力机制学到无效区域现象BUTD 或 AOA 模型训练时注意力权重集中在图像边缘或空白区域。原因1-5 补齐区域特征时用零向量填充模型把零向量也当成有效区域参与注意力计算。解决补齐的同时生成一个 mask 数组标记哪些区域是真实的、哪些是填充的模型里用 mask 把填充区域的注意力分数置为负无穷。如果模型不支持 mask就把补齐方式改成复制最后一个有效区域的特征而不是补零。5.4 训练集和验证集的图像特征文件混在一起现象验证集指标异常高但测试集指标很低。原因1-2 划分数据集时只改了 JSON 里的 split 字段但特征文件目录没有按 split 分开训练时数据加载器可能读到了验证集的特征。解决在 1-8 归整阶段把特征文件按 split 复制到不同子目录或者生成三个独立的特征索引文件训练脚本按 split 读对应的索引。5.5 224×224 缩放后图像文件覆盖原始文件现象跑完 1-9 之后发现原始图像被覆盖了想重新处理只能重新下载。原因输出目录设成了和原始图像相同的目录或者文件名规则导致同名覆盖。解决输出目录永远用一个新名字比如images_224并且在脚本开头检查输出目录是否已存在存在就报错退出避免误覆盖。我一般会在脚本里加一行assert not os.path.exists(output_dir), 输出目录已存在请先删除或改名。6. 从能跑到能复现固定随机种子、校验文件完整性和一条命令重跑这套脚本跑通一次不难难的是换一批数据还能跑通或者过两周自己还能复现。我后来养成的习惯是每个脚本开头固定random.seed(42)和np.random.seed(42)所有涉及随机的地方都走同一个种子每个脚本输出文件时附带一个.done标记文件记录输入文件列表和输出文件列表的 MD5下次跑之前先校验输入没变就跳过省时间也避免重复处理引入的不一致。import hashlib import json import os def file_md5(path): h hashlib.md5() with open(path, rb) as f: for chunk in iter(lambda: f.read(8192), b): h.update(chunk) return h.hexdigest() def check_and_mark(inputs, outputs, mark_file): if os.path.exists(mark_file): with open(mark_file, r) as f: record json.load(f) current {p: file_md5(p) for p in inputs if os.path.exists(p)} if record.get(inputs) current: print(输入未变跳过) return False # 执行处理逻辑这里省略 record { inputs: {p: file_md5(p) for p in inputs if os.path.exists(p)}, outputs: {p: file_md5(p) for p in outputs if os.path.exists(p)} } with open(mark_file, w) as f: json.dump(record, f, indent2) return True这个校验逻辑不复杂但能省掉大量重复跑的时间。尤其是 1-5 和 1-7 这种特征提取脚本跑一次几十分钟输入没变就没必要重跑。另一个习惯是把所有脚本串成一个run_all.sh按编号顺序执行中间任何一步失败就停避免用半成品数据继续往下跑。#!/bin/bash set -e # 任何一步失败就退出 python 1-1_merge_raw_files.py python 1-2_split_dataset.py python 1-3_convert_to_uic_format.py python 1-4_generate_annotations.py python 1-5_build_butd_features.py python 1-6_prepare_aoa_files.py python 1-7_build_grid_features.py python 1-8_organize_training_files.py python 1-9_resize_images_224.py echo 全部完成set -e这行是关键不加的话中间某步报错但脚本继续跑后面全用错数据排查起来很痛苦。从那以后我每次跑这种多步骤流水线都强制走一遍set -e加 MD5 校验宁可多花几分钟确认也不想在训练到一半时发现数据是错的。希望帮到你。本文还有配套的精品资源点击获取
返回列表