数据集的 train/val/test 划分:解读 Ultralytics data/split.py 数据集切分工具
数据准备是训练一切 YOLO 模型的第一步,而把一份原始图片合理划分成 train / val / test 三个子集则是数据准备中最常被低估的环节。本文以 Ultralytics 仓库中的 数据切分模块 为对象,深入讲解它提供的两大数据集切分工具——面向图像分类任务的 split_classify_dataset 与面向检测 / 分割 / 姿态等任务、按路径清单切分的 autosplit,覆盖其设计动机、目录结构约定、函数参数、底层实现与真实数据集(如 xView)中的落地用法。读完本文,你将掌握 Ultralytics 官方数据切分的全部机制,并能结合自己的数据规模正确选择切分方案。
模块全景:一个文件解决两类切分需求
ultralytics/data/split.py 是 Ultralytics 工具链中专门负责"把原始数据划分成训练可用子集"的模块,其公开 API 由两个函数组成(对应 API 参考文档 中自动生成的 ultralytics.data.split.split_classify_dataset 与 ultralytics.data.split.autosplit 两节):
| 函数 | 适用任务 | 切分产物 | 是否复制文件 |
|---|---|---|---|
split_classify_dataset(source_dir, train_ratio=0.8) | 图像分类 | 新建 {source}_split/train、{source}_split/val 目录并复制图片 | 是(shutil.copy2) |
autosplit(path, weights=(0.9, 0.1, 0.0), annotated_only=False) | 检测 / 实例分割 / 姿态等(YOLO 标签体系) | 在 path.parent 生成 autosplit_train.txt / autosplit_val.txt / autosplit_test.txt 路径清单 | 否(仅写文本清单) |
之所以需要两套方案,是因为两类任务的数据组织方式不同:图像分类数据集按 class_dir/图片 的目录结构组织,类别由目录名隐含,必须物理拆分目录才能得到独立子集;而 YOLO 检测类数据集通过一个数据集 YAML 指向 train / val 图片路径(可为目录或清单文件),标签则通过"把 images 换成 labels"的路径约定与图片一一对应,因此切分只要**生成图片路径清单(.txt)**即可,无需复制任何文件。
分类数据集切分:split_classify_dataset
split_classify_dataset 专门服务于以"子目录 = 类别"组织的图像分类数据集(如 Caltech101、ImageNet 的简化目录结构),它会在原目录旁新建一个 {source_dir}_split 目录,在其中按比例重建 train / val 两级子目录,并将图片按类别复制进去。
函数签名与参数
从 split.py 源码 可确认完整签名:
def split_classify_dataset(source_dir: str | Path, train_ratio: float = 0.8) -> Path:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
source_dir | str \| Path | 必填 | 分类数据集根目录路径,其下每个子目录代表一个类别,子目录内为图片文件 |
train_ratio | float | 0.8 | 分配给 train 集的比例,取值区间 (0, 1);val 集比例为 1 - train_ratio |
| 返回值 | Path | — | 新建的切分目录 {source_dir}_split 的路径 |
目录结构约定
源码 docstring 中给出了切分前后的标准结构对照(split.py):
切分前: 切分后:
caltech/ caltech_split/
├── class1/ ├── train/
│ ├── img1.jpg │ ├── class1/
│ ├── img2.jpg │ │ ├── img1.jpg
│ └── ... │ │ └── ...
├── class2/ │ ├── class2/
│ ├── img1.jpg │ │ └── ...
│ └── ... └── val/
└── ... ├── class1/
│ ├── img2.jpg
│ └── ...
├── class2/
└── ...
底层行为与实现细节
对照源码可以梳理出该函数的完整执行流程(split.py):
- 目录创建:以
Path(f"{source_path}_split")构造输出根目录,并依次创建train、val子目录(exist_ok=True保证可重复执行不会报错)。 - 统计与日志:枚举
source_dir下所有子目录作为类别目录,统计类别数与图片总数,并通过LOGGER.info输出形如Splitting caltech (2 classes, 100 images) into 80% train, 20% val...的进度信息。 - 逐类别切分:对每个类别目录,仅收集后缀匹配图片格式白名单的文件(见下文),调用
random.shuffle打乱后,按int(len(image_files) * train_ratio)计算切分点,前段复制到 train、后段复制到 val。 - 复制而非移动:使用
shutil.copy2,会保留文件的修改时间等元数据,且原始数据不受任何影响,可放心重复实验。 - 返回结果:日志输出
Split complete in {split_path} ✅并返回切分目录路径。
需要注意的一个实现细节:这里使用了 Python 全局 random 模块且没有固定随机种子,因此每次调用划分结果是随机的;若需要可复现的划分,可在调用前自行 random.seed(...)。
只挑选合法图片:IMG_FORMATS 白名单
函数只处理扩展名(小写后)位于 IMG_FORMATS 集合内的文件。该集合定义于 data/utils.py,共 13 种常见图像格式:
avif, bmp, dng, heic, heif, jp2, jpeg, jpg, mpo, png, tif, tiff, webp
因此即使类别目录中混有 README.txt、缩略图缓存等无关文件,也不会被复制进 train / val 子集,天然实现了数据清洗。
实际调用示例
from ultralytics.data.split import split_classify_dataset
# 默认 80/20 划分,返回 Path("path/to/caltech_split")
split_path = split_classify_dataset("path/to/caltech")
# 自定义 75/25 划分
split_classify_dataset("path/to/caltech", 0.75)
一个自动触发场景:缺失 train 目录时的隐式切分
split_classify_dataset 并非只能手动调用——它在 data/utils.py 的 check_cls_dataset 中被自动触发,构成一个很实用的兜底逻辑:
- 当校验分类数据集时发现
data_dir / "train"目录不存在; - 若递归扫描
data_dir仍能找到合法的图片文件(image_files := [f for f in data_dir.rglob("*.*") if ...]),则日志输出Dataset 'split=train' not found...Attempting to split...; - 自动以
train_ratio=0.8调用split_classify_dataset,并重新指向新生成的train子目录。
这意味着:当你手头只有一个"全部类别平铺在一起"的原始分类图片目录、直接把它作为 data 传给分类训练入口时,Ultralytics 会主动帮你完成 80/20 的 train/val 划分,无需手工准备目录。这一自动行为也从侧面印证了上述切分函数在设计上"对原始目录零破坏、只新增 _split 目录"的保守策略是刻意为之的。
检测类数据集的自动切分:autosplit
autosplit 面向 YOLO 家族最常见的标签体系(images/ 与 labels/ 同级、同名 txt 标签),核心思路是:不复制图片,而是在图片目录的父目录下生成 3 个持久化的路径清单文件 autosplit_train.txt / autosplit_val.txt / autosplit_test.txt,之后在数据集 YAML 中用这些清单作为 train / val 的来源。
函数签名与参数
从 split.py 源码 可确认完整签名:
def autosplit(
path: Path = DATASETS_DIR / "coco8/images",
weights: tuple[float, float, float] = (0.9, 0.1, 0.0),
annotated_only: bool = False,
) -> None:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
path | Path | DATASETS_DIR / "coco8/images" | 待切分图片所在目录,递归收集其下所有合法格式图片 |
weights | tuple[float, float, float] | (0.9, 0.1, 0.0) | 依次对应 train / val / test 三个子集被抽中的权重,即 (训练, 验证, 测试) 的期望比例 |
annotated_only | bool | False | 为 True 时仅收录存在对应 txt 标签文件的图片 |
| 返回值 | None | — | 结果以 3 个 autosplit_*.txt 文件落盘 |
底层实现要点
对照源码可以梳理其执行逻辑(split.py):
- 递归收集图片:
files = sorted(x for x in path.rglob("*.*") if x.suffix[1:].lower() in IMG_FORMATS),即对path做递归展开并同样以IMG_FORMATS白名单过滤,保证子目录嵌套也能被覆盖。 - 固定随机种子:
random.seed(0)被显式调用,意味着多次运行同一数据集会得到完全一致的划分结果,这对可复现实验至关重要。 - 按权重抽样分桶:
indices = random.choices([0, 1, 2], weights=weights, k=n),对每一张图片独立地按权重从 0(train)/1(val)/2(test) 中抽取一个归属,即每个子集的图片数只是期望比例而非精确比例。 - 清理旧产物:若
path.parent下已存在同名清单文件,先unlink删除再追加写入,避免重复运行产生脏数据。 - 逐图片落盘:遍历(带 TQDM 进度条)时,若
annotated_only=True则通过img2label_paths检查标签是否存在,只有通过检查的图片才会被追加写入对应的txt[i]。 - 写入相对路径:清单内每行是相对
path.parent的 POSIX 风格相对路径并带./前缀,例如./images/img1.jpg(split.py)。写./前缀是因为 data/base.py 在解析数据集 YAML 的 train/val 条目时,会把./开头的相对路径再拼接到数据集根目录上,形成"本地 → 全局"的路径转换。
标签路径换算:img2label_paths
autosplit 的 annotated_only 过滤依赖 data/utils.py 的 img2label_paths,它实现 YOLO 标准的路径映射规则:把路径中的 {sep}images{sep} 段替换为 {sep}labels{sep},同时把扩展名替换为 .txt。例如:
- 输入
.../coco8/images/train/0001.jpg - 输出
.../coco8/labels/train/0001.txt
因此在使用 autosplit(..., annotated_only=True) 前,请务必保证标签目录采用 images ↔ labels 同级替换的默认布局;若自定义了标签目录名,可通过 img2label_paths(..., label_dir=...) 换算确认后再决定是否启用该开关。
典型用法示例
from ultralytics.data.split import autosplit
# 默认 90/10/0 划分(无测试集),作用于默认 coco8 图片目录
autosplit()
# 常用:80/15/5,且仅收录带标签的图片
autosplit(path="path/to/images", weights=(0.8, 0.15, 0.05), annotated_only=True)
运行完成后,会在 path 的父目录(例如 datasets/coco8/)中生成三个清单文件,可直接供数据集 YAML 引用:
# coco8 风格的 autosplit 引用写法
path: ../datasets/coco8
train: images/autosplit_train.txt
val: images/autosplit_val.txt
真实落地:xView 数据集的官方下载流程即用 autosplit
autosplit 不是孤立的工具函数——Ultralytics 官方维护的 xView 遥感检测数据集配置 就把它内嵌进 download: 脚本作为数据准备流水线的一环(xView.yaml):
from ultralytics.data.split import autosplit
...
convert_labels(dir / "xView_train.geojson") # 1. GeoJSON → YOLO 标签
Path(dir / "train_images").rename(dir / "images" / "train") # 2. 整理图片目录
Path(dir / "val_images").rename(dir / "images" / "val")
autosplit(dir / "images" / "train") # 3. 对训练图片做 90/10 自动切分
对应地,该数据集的 YAML 中 train / val 直接指向生成的清单(xView.yaml):
train: images/autosplit_train.txt # 约 90% 的 847 张标注图片
val: images/autosplit_val.txt # 约 10% 的 847 张标注图片
从该数据集的官方文档说明可以看出这套流程的最终效果:autosplit_train.txt 列出约 90% 的带标注训练图,autosplit_val.txt 列出约 10% 用于验证。值得注意的是 xView 的 847 张训练图片本身就有官方标注,而官方单独的 val 图片无标签,所以这里切分的是"标注过的训练图"这一子集——这正是 autosplit 在真实数据集流水线中的典型定位。
与 fraction 训练参数的区别
很多开发者会把 autosplit 与训练时的 fraction 参数混为一谈,Ultralytics 官方在 simple-utilities 文档 中明确区分了两者:
autosplit(本模块):一次性生成持久化的autosplit_*.txt清单文件,划分结果固化在磁盘上,多次训练跨进程共享同一划分;fraction训练参数:在单次训练启动时按比例随机抽取子集用于本次 run,不落盘、每次运行可变化,适合快速试跑与消融实验。
两者可以组合使用:先用 autosplit 得到稳定的 train/val 清单,再配合 fraction 对训练子集做进一步的临时采样。此外,split_classify_dataset 是"物理复制文件"型划分,而 autosplit 是"生成路径清单"型划分,前者适用于目录即类别的分类任务,后者适用于 images/labels 布局的检测类任务——请根据任务类型选择,不要混用。
进阶阅读与延伸
- 若想进行更严谨的、按类别均衡的 K 折交叉验证而非简单随机切分,仓库提供了完整指南 kfold-cross-validation.md,其中也明确推荐了
autosplit作为自动切分手段,并与基于YOLODataset的kfold_split方案做了对比。 - 更多实用的数据集与标签工具(如
check_cls_dataset的自动兜底切分、图片压缩、路径换算)集中在 simple-utilities.md。 - 两个函数的完整 API 文档见 split.md 参考页。
小结
Ultralytics 通过 data/split.py 一个模块优雅地覆盖了两种主流数据组织方式下的切分需求:split_classify_dataset 负责"按类别目录物理拆分、复制出 train/val 两套目录",并在分类数据缺失 train 目录时被 check_cls_dataset 自动触发;autosplit 负责"在 images/labels 布局下按权重随机生成持久化的路径清单",以固定种子保证可复现,其可靠性经过了 xView 等官方数据集下载流程的验证。理解这两个函数的分工与内部约定,是把自己的原始数据快速、正确地送入 Ultralytics 训练管线的前提。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



