用txt文件干净切分深度学习数据集:训练/验证/测试划分实战

发布时间:2026/10/21 23:04:25
用txt文件干净切分深度学习数据集:训练/验证/测试划分实战 最近帮一个做图像分类的朋友调训练流程卡在数据准备阶段整整两天。数据明明都放在目录里PyTorch的ImageFolder一读就能开训但训练集、验证集、测试集一划分就出现重叠验证集指标虚高测试集结果根本没法信。后来我让他把所有样本路径写进三个txt文件用一个脚本划分十分钟解决问题指标也恢复正常了。这篇文章就把用txt文件做训练集、测试集、验证集划分的完整思路和实操过程记录下来包括我踩过的坑、踩完之后总结的注意事项以及配套的读取代码。文章适合正在准备深度学习数据集的人看尤其是刚接触目标检测、图像分类、文本分类这类任务不想用复杂工具、只想把数据集干净利落切分好的朋友。1. 为什么非要用txt来管理数据集划分1.1 直接按目录划分的痛点很多框架和现成工具默认的数据组织方式是“一个类别一个文件夹”训练时按目录读取。比如train/dog/、train/cat/val/dog/、val/cat/。这种结构对单标签分类任务非常友好只需要把图片复制到对应目录就行了。但实际项目里会遇到几个问题第一不是所有数据都长这样。目标检测的数据集往往是一张图片对应一个标注文件标注文件可能是xml、json或者txt图片和标注是分离的。这时候按目录划分就不太靠谱因为一张图片的数据不止是图片本身还得带上它的标注文件。第二复制文件会产生大量冗余。一张训练图片如果同时进了训练集目录和验证集目录的候选区磁盘空间直接翻倍而且很容易搞混哪个才是真正被使用的版本。第三目录划分不好做精细控制。比如我希望测试集尽量覆盖所有类别或者希望某些特殊样本永远只出现在测试集中不去污染训练过程按目录手动挑选就很痛苦。txt文件划分的核心思路就是把哪些样本进哪个集合这个决策从文件系统层面抽离出来变成一份可读、可改、可追溯的清单。目录只是数据的仓库txt才是数据的使用方案。1.2 txt清单带来的三个直接好处使用txt作为划分载体我总结下来有三个优点是纯目录结构不具备的首先是可追溯性。训练结束之后想复现结果只写命令行参数还不够数据集划分方式也得能还原。txt文件放进项目仓库里git diff一看就知道这次训练比上次训练多用了哪些样本、去掉了哪些样本。这个是目录结构很难做到的因为你不会把整个数据集目录提交到git。其次是灵活性。想调整验证集比例不需要移动任何文件只需要重新执行划分脚本。想排除某些异常样本从txt里删掉一行就行。想观察增加某个数据子集后模型效果的变化直接编辑txt重新训练。整个流程操作的是文本效率非常高。第三是跨平台、跨框架通用。txt这种纯文本格式无论PyTorch、TensorFlow还是PaddlePaddle无论Windows还是Linux都能轻松解析。数据集的清单可以随代码一起分发别人拿到你的项目后不用猜你的数据集长什么样。我这段时间接触过的图像分割项目用的也是txt划分方式而且txt里存的不是图片路径而是mask路径和原图路径——两个txt一一对应读起来非常规整。只要设计好了清单格式txt方案能适配很多复杂数据组织。2. 准备工作数据目录与清单格式约定2.1 数据目录怎么组织最省事虽然说了不依赖目录来划分集合但数据本身的物理存放还是要有章法。我一般推荐的目录结构是data/ raw/ images/ 000001.jpg 000002.jpg labels/ 000001.xml 000002.xml splits/ train.txt val.txt test.txtraw目录存放原始数据图片和标注分开互不干扰。splits目录专门存放划分好的txt清单。这样做的理由是原始数据永远只保留一份不会因为划分而复制所有划分结果集中放在一个目录训练脚本、数据检查脚本只需要知道splits目录的路径就能拿到全部数据分配信息。如果数据集来自公开数据集比如经典的图像分类数据集原始目录里已经天然按类别分好了文件夹那也不需要重新复制一份。txt清单里直接写相对路径让数据加载代码去原目录找文件即可。2.2 txt清单的格式怎么定txt格式有讲究但不需要设计得太复杂。最常见的两种第一种每行一个样本。对分类任务就是图片路径加类别标签用空格或逗号分隔images/000001.jpg 0 images/000002.jpg 1 images/000003.jpg 0第二种每行两个路径。适合图像分割、目标检测这类样本和标注分离的任务images/000001.jpg labels/000001.xml images/000002.jpg labels/000002.xml路径的写法我强烈建议用相对路径不要用绝对路径。比如images/000001.jpg而不是E:/project/data/raw/images/000001.jpg。绝对路径一旦换了机器或者移动了项目目录txt就失效了相对路径配合项目的根目录定位拿到代码的人只要把数据集放在约定位置就能跑通。写路径时统一用斜杠/不要用Windows的反斜杠\。Python的os.path.join在Windows上会生成反斜杠路径写进txt之后再跨平台读经常出问题。手动处理时全用/就对了。关于编码统一保存成UTF-8不要用带BOM的UTF-8。BOM是藏在文件开头的几个字节很多脚本读取时不会自动跳过结果第一个样本的路劲前多了一个不可见字符查半天查不出来。我建议写txt和读txt的动作都在代码里显式指定encodingutf-8。3. 核心脚本从零实现一个数据集划分工具3.1 最简单的随机按比例划分先给一个基础版本逻辑非常简单import os import random def split_dataset(all_files, train_ratio0.8, val_ratio0.1): # 固定随机种子保证每次划分结果可复现 random.seed(42) # 随机打乱 random.shuffle(all_files) total len(all_files) train_end int(total * train_ratio) val_end int(total * (train_ratio val_ratio)) train_files all_files[:train_end] val_files all_files[train_end:val_end] test_files all_files[val_end:] return train_files, val_files, test_files核心思路是先收集所有样本打乱再切片完成切分。random.seed(42)这一行必须写否则每次运行划分结果都不一样训练结果没法复现。train_ratio和val_ratio两个参数控制切分比例剩余部分自动变成测试集。完整读取和写文件名清单的版本import os import random def read_all_image_paths(data_dir): # 遍历data_dir下所有jpg文件 all_paths [] for root, _, files in os.walk(data_dir): for f in files: if f.lower().endswith((.jpg, .jpeg, .png)): # 生成相对路径统一使用/分隔 full_path os.path.join(root, f) rel_path os.path.relpath(full_path, startos.path.dirname(data_dir)) # 统一替换反斜杠为斜杠 rel_path rel_path.replace(\\, /) all_paths.append(rel_path) return all_paths def write_split(paths, out_file): # 每个路径一行写入txt with open(out_file, w, encodingutf-8) as f: for p in paths: f.write(p \n) if __name__ __main__: data_root data/raw/images output_dir data/splits all_images read_all_image_paths(data_root) train, val, test split_dataset(all_images, 0.8, 0.1) os.makedirs(output_dir, exist_okTrue) write_split(train, os.path.join(output_dir, train.txt)) write_split(val, os.path.join(output_dir, val.txt)) write_split(test, os.path.join(output_dir, test.txt)) print(ftotal: {len(all_images)}, train: {len(train)}, val: {len(val)}, test: {len(test)})这里有一个小设计值得注意我把read_all_image_paths单独分成一个函数。这样后续如果要加标签信息只需要在这个函数里同时返回路径和标签即可不用改动划分逻辑。write_split也独立出来方便自定义格式比如写入两列数据。运行这个脚本后就会在data/splits目录下生成三个txt文件每个文件是图片路径列表。后续训练代码只需要按行读取对应txt即可。3.2 带标签的按类别均衡划分随机划分有一个问题如果某个类别的样本数量本来就少随机打乱后可能全部掉进测试集训练集里完全看不到这个类别。这时候就需要按类别分层采样。from collections import defaultdict import random def stratified_split(samples, train_ratio0.8, val_ratio0.1): # samples: list of (path, label) # 按类别分组 class_to_samples defaultdict(list) for path, label in samples: class_to_samples[label].append((path, label)) train, val, test [], [], [] for label, items in class_to_samples.items(): random.shuffle(items) total len(items) train_end int(total * train_ratio) val_end int(total * (train_ratio val_ratio)) train.extend(items[:train_end]) val.extend(items[train_end:val_end]) test.extend(items[val_end:]) # 最后再整体打乱一次避免训练时同类别样本连续出现 random.shuffle(train) random.shuffle(val) random.shuffle(test) return train, val, test这个脚本对每一个类别单独划分再合并。训练时同类别样本连续出现的概率降低了模型能更快收敛。对类别不平衡很严重的数据集这是非常重要的一步千万别省略。如果数据集自带标注文件读取时把标签也读出来def read_samples_with_labels(image_dir, label_dir): samples [] label_names sorted(os.listdir(label_dir)) label_to_id {name: i for i, name in enumerate(label_names)} for root, _, files in os.walk(image_dir): for f in files: if f.lower().endswith((.jpg, .jpeg, .png)): # 假设标注文件名与图片名一致扩展名不同 base_name os.path.splitext(f)[0] label_file os.path.join(label_dir, base_name .xml) if os.path.exists(label_file): full_path os.path.join(root, f) rel_path os.path.relpath(full_path, startdata) rel_path rel_path.replace(\\, /) # 这里根据实际情况提取标签 label extract_label_from_xml(label_file) samples.append((rel_path, label)) return samples, label_namesextract_label_from_xml这个函数逻辑因标注格式而异关键是返回一个整数类别ID。整体思路是一样的读数据时把标签带上划分时按类分层。3.3 从生成结果的校验脚本划分完不是看一眼输出就完事我强烈建议写一个小校验脚本。数据集划分这块最常见的问题是训练集和验证集重叠或者某些文件根本不存在。def verify_split(train_file, val_file, test_file): def load_ids(txt_file): ids set() with open(txt_file, r, encodingutf-8) as f: for line in f: line line.strip() if line: ids.add(line) return ids train load_ids(train_file) val load_ids(val_file) test load_ids(test_file) assert not (train val), 训练集和验证集有交集 assert not (train test), 训练集和测试集有交集 assert not (val test), 验证集和测试集有交集 # 检查文件是否真的存在 all_ids train | val | test for img_id in all_ids: if not os.path.exists(img_id): print(f文件不存在: {img_id}) print(f校验通过总样本数: {len(all_ids)})这段代码用集合求交集的方式检查三个集合是否重叠。逻辑直观、运行快。如果发现交集说明划分脚本或者读取逻辑有bug得回去查。文件存在性检查也很关键尤其是路径写错、大小写不匹配这类低级但在所难免的错误。4. 实际操作中踩过的坑4.1 换行符与编码问题在Windows上写txt默认换行符是\r\nLinux上读取时strip()能处理掉但有些代码不会strip直接拿路径去打开文件路径末尾多个\r字符报文件不存在。这个坑很隐蔽报错信息里路径看起来完全正常但就是打不开。解决办法是写文件时用newline\n或者读取时统一strip()加容错处理。如果txt文件用Excel打开过并重新保存编码可能变成带BOM的UTF-8。第一次读到的路径开头会有\ufeff字符。我习惯在读取时判断一下def load_lines(file_path): with open(file_path, r, encodingutf-8-sig) as f: return [line.strip() for line in f if line.strip()]utf-8-sig这个编码在Python里会自动去掉BOM读取带BOM的txt也不会出错是一个非常省心的选择。4.2 Windows路径分隔符问题Windows下os.walk返回的路径默认用反斜杠分隔。如果直接写入txt到了Linux服务器上读取路径全部失效。我的做法是写入txt之前统一替换rel_path rel_path.replace(\\, /)这个操作在所有平台上都要做因为不能假设开发环境和训练环境是同一个系统。在Linux上用os.path.relpath生成的就是斜杠路径替换一下也无所谓反而保证了路径格式的一致性。4.3 类别分布不均衡随机划分后训练集和测试集的类别分布可能不一样如果原始数据中类别样本数量差异很大差异会更严重。比如一个二分类数据集A类有9000张B类有1000张。随机划分0.8/0.1/0.1后很可能测试集的B类样本只有几十张模型在B类上的性能评估波动很大完全不可信。处理思路有两个方向其一用前面提到的分层抽样保证每个类别的比例在各集合中一致其二对于极其不均衡的情况考虑在训练时做类别重采样但这就不是划分阶段要解决的问题了。分层划分是最省事、最直接的改进。4.4 训练集和验证集的样本重叠重叠问题最容易出现在手动调整阶段。比如训练时发现某些难样本效果差想把这些样本从训练集挪进验证集看看指标变化直接手动编辑了两个txt但漏改了另一个txt导致同一个样本同时出现在训练集和验证集。这种情况下验证集指标基本失去意义。所以无论改动多小改完之后务必跑一遍校验脚本。我在实际项目中就把这三条交集断言封装成独立脚本每当数据划分有任何改动都先跑一次确认通过再开始训练。4.5 新增数据后的递增划分项目进行到一半经常会有新的数据进来。这时候最怕的是重新划分所有数据因为之前划分的结果已经被模型训练、报告引用全部推翻会浪费大量工作。我推荐的做法是新增数据时把新数据写入一个单独的原始文件列表然后在旧的划分结果上增量追加。具体而言用train.txt中已有路径的集合作为参照把新样本按同样比例随机分配进三个txt并保证新加入的样本不会出现在多个txt里。def add_new_samples(new_sample_list, old_train_file, old_val_file, old_test_file, ratios(0.8, 0.1, 0.1)): loaded_train load_lines(old_train_file) loaded_val load_lines(old_val_file) loaded_test load_lines(old_test_file) existing_all set(loaded_train) | set(loaded_val) | set(loaded_test) candidates [s for s in new_sample_list if s not in existing_all] random.shuffle(candidates) total len(candidates) train_end int(total * ratios[0]) val_end int(total * (ratios[0] ratios[1])) new_train candidates[:train_end] new_val candidates[train_end:val_end] new_test candidates[val_end:] # 追加写回 loaded_train.extend(new_train) loaded_val.extend(new_val) loaded_test.extend(new_test) write_file(loaded_train, old_train_file) write_file(loaded_val, old_val_file) write_file(loaded_test, old_test_file)这个流程保证原有划分结果稳定只处理新增部分。配合校验脚本能随时保证三个集合互不重叠。5. 训练框架如何读取txt清单5.1 PyTorch自定义Dataset划分好了txt下一步就是让训练代码读起来方便。PyTorch里通常自定义一个Dataset类import torch from torch.utils.data import Dataset from PIL import Image class ImageListDataset(Dataset): def __init__(self, txt_path, transformNone): self.samples [] self.labels [] with open(txt_path, r, encodingutf-8-sig) as f: for line in f: line line.strip() if not line: continue # 支持 path label 和 path 两种情况 parts line.split() img_path parts[0] if len(parts) 1: label int(parts[1]) else: label 0 self.samples.append((img_path, label)) self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] image Image.open(img_path).convert(RGB) if self.transform: image self.transform(image) return image, label注意这里用utf-8-sig读取能自动跳过BOM。split()对空格和多个空格都有效比手动处理逗号分隔更省事。如果格式是逗号分隔把split()改成split(,)即可。训练、验证、测试分别加载不同txttrain_dataset ImageListDataset(data/splits/train.txt, transformtrain_transform) val_dataset ImageListDataset(data/splits/val.txt, transformval_transform) test_dataset ImageListDataset(data/splits/test.txt, transformtest_transform)5.2 数据增强的边界在哪数据增强会改变图片内容但很多人会把增强不分青红皂白地应用到所有集合上。正确做法是训练集用较重的数据增强比如随机裁剪、随机翻转、颜色抖动验证集和测试集只用标准化和确定性变换比如Resize再加Normalize。因为验证集和测试集的作用是评估模型的真实泛化能力如果也做随机增强每次评估的输入不一样结果不再可复现。这一条看似和txt划分无关但如果txt划分出来的测试集样本在评估时做了随机增强造成性能波动你首先会怀疑划分是否合理然后浪费大量时间查数据重叠问题。提前在设计阶段避开这个坑能让后续排查轻松很多。5.3 多数据集的扩展思路单个train.txt只是起点。某些场景下需要把多个来源的数据集混合训练。这时候可以准备多个txt让训练代码同时读取class ConcatListDataset(Dataset): def __init__(self, txt_paths): self.samples [] for txt_path in txt_paths: with open(txt_path, r, encodingutf-8-sig) as f: for line in f: line line.strip() if line: self.samples.append(line) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path self.samples[idx] image Image.open(img_path).convert(RGB) return image又比如做K折交叉验证txt划分就变成了生成多个互不重叠的划分文件def generate_kfold_splits(all_samples, k5, output_dirdata/folds): import random random.seed(42) random.shuffle(all_samples) fold_size len(all_samples) // k folds [] for i in range(k): folds.append(all_samples[i * fold_size:(i 1) * fold_size]) for i in range(k): val_fold folds[i] train_fold [s for j, fold in enumerate(folds) if j ! i for s in fold] with open(f{output_dir}/train_{i}.txt, w, encodingutf-8) as f: for s in train_fold: f.write(s \n) with open(f{output_dir}/val_{i}.txt, w, encodingutf-8) as f: for s in val_fold: f.write(s \n)从这里可以看出txt天然适合做程序化生成。每次交叉验证的划分都是一个独立文件想追溯哪次实验用了哪个划分看文件名就行。6. 一套顺手的辅助检查技巧6.1 快速统计各个文件的样本数比起用Excel打开txt数行数我习惯用一行命令wc -l data/splits/*.txt这个命令能一次性输出所有txt文件的行数一眼看出比例是否符合预期。如果txt里可能有空行wc -l统计的是换行符数量而不是有效行数建议在脚本里再做一次精确校验。非严格场景下命令行的grep -c 也行grep -c data/splits/train.txt6.2 可视化类别分布热力图对分类任务我偶尔会做一个简单的类别分布统计。做法是把每个txt读入后按照类别ID计数然后打印表格。不需要画图纯文本输出足够看出问题from collections import Counter def report_class_distribution(txt_path, label_to_nameNone): counter Counter() with open(txt_path, r, encodingutf-8-sig) as f: for line in f: parts line.strip().split() if len(parts) 1: counter[int(parts[1])] 1 for label_id in sorted(counter.keys()): name label_to_name.get(label_id, label_id) if label_to_name else label_id print(f{name}: {counter[label_id]})如果三个txt都跑一遍这个函数就能直观看到每个集合里类别分布是否均衡。发现某个类别在训练集样本极少、测试集又很多这种划分基本不能用要回到分层划分逻辑去修。6.3 检查txt路径是否对应真实标注文件目标检测、分割这类任务txt里不仅有图像路径还有标注路径。校验的时候要成对检查def verify_image_label_pairs(txt_path): missing [] with open(txt_path, r, encodingutf-8-sig) as f: for line in f: parts line.strip().split() if len(parts) ! 2: missing.append((line.strip(), 格式错误)) continue img_path, label_path parts if not os.path.exists(img_path): missing.append((img_path, 图片不存在)) if not os.path.exists(label_path): missing.append((label_path, 标注不存在)) if missing: for item, reason in missing[:10]: print(f{item}: {reason}) else: print(全部图片-标注对均存在且格式正确)这个脚本我每次做数据集准备都会跑一次。很多标注文件缺失问题是累积出来的早期不检查训练到一半才发现损失异常回头查数据往往非常痛苦。7. 关于txt划分的几条忠告用txt文件管理数据集划分看起来是最土的方案但它稳定、透明、可控。我在实际项目中见过不少团队为了省事直接用框架内置的数据集划分工具结果自定义数据格式不兼容反而浪费更多时间。txt方案最大的价值在于划分过程可以完全被记录、被复现并且不依赖任何特定框架。如果你正在处理的数据集还比较小几百张或者几千张用txt顺手一划可能感觉不到明显优势。但当数据量到了几十万张类别上百个标注格式还不统一的时候txt方案的简单可靠就成了最大的竞争力。我常用的一个工作流是新增数据后先把文件名追加到候选清单然后跑划分脚本再跑校验脚本最后看一眼类别分布报告整个流程不超过五分钟。最后再分享一个小技巧txt划分文件名里建议加上样本总量信息。比如train_12000.txt这样在多个版本的数据集之间做比较时单看文件名就知道规模差异不用每次都打开文件数行。这个方法治不了什么大问题但能省不少翻阅历史文件的精力。我经过好几次这个train.txt到底是哪个版本的迷糊之后养成了这个习惯现在推荐给所有搞数据准备的朋友。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询