使用 Ultralytics 进行 K 折交叉验证

简介

本综合指南展示了如何在 Ultralytics 生态系统中为 目标检测 数据集实现 K 折交叉验证。我们将利用 YOLO 检测格式以及诸如 sklearn、pandas 和 PyYAML 等关键 Python 库,引导你完成必要的设置、生成特征向量的过程以及执行 K 折数据集拆分。

K-fold cross validation data splitting

无论你的项目涉及水果检测(Fruit Detection)数据集还是自定义数据源,本教程旨在帮助你理解并应用 K 折交叉验证,以提高你的 机器学习 模型的可靠性和稳健性。虽然我们在本教程中使用了 k=5 折,但请记住,最佳折数取决于你的数据集和项目的具体情况。

让我们开始吧。

设置

  • 你的标注应采用 YOLO 检测格式

  • 本指南假定标注文件可在本地访问。

  • 在我们的演示中,使用了 水果检测 数据集。

    • 该数据集总共包含 8479 张图像。
    • 它包含 6 个类别标签,每个标签的实例总数如下所示。
类别标签实例计数
Apple7049
Grapes7202
Pineapple1613
Orange15549
Banana3536
Watermelon1976
  • 必要的 Python 软件包包括:

    • ultralytics
    • sklearn
    • pandas
    • pyyaml
  • 本教程以 k=5 折进行操作。不过,你应该根据你的具体数据集确定最佳的折数。

  1. 为你的项目启动一个新的 Python 虚拟环境 (venv) 并激活它。使用 pip(或你首选的软件包管理器)安装:

    • Ultralytics 库:pip install -U ultralytics。或者,你也可以克隆官方 仓库
    • Scikit-learn、pandas 和 PyYAML:pip install -U scikit-learn pandas pyyaml
  2. 验证你的标注是否为 YOLO 检测格式

    • 在本教程中,所有标注文件均位于 Fruit-Detection/labels 目录中。

为目标检测数据集生成特征向量

  1. 首先创建一个新的 example.py Python 文件以执行以下步骤。

  2. 接着获取数据集的所有标签文件。

    from pathlib import Path
    
    dataset_path = Path("./Fruit-detection")  # replace with 'path/to/dataset' for your custom data
    labels = sorted(dataset_path.rglob("*labels/*.txt"))  # all data in 'labels'
  3. 现在,读取数据集 YAML 文件的内容并提取类别标签的索引。

    import yaml
    
    yaml_file = "path/to/data.yaml"  # your data YAML with data directories and names dictionary
    with open(yaml_file, encoding="utf8") as y:
        classes = yaml.safe_load(y)["names"]
    cls_idx = sorted(classes.keys())
  4. 初始化一个空的 pandas DataFrame。

    import pandas as pd
    
    index = [label.stem for label in labels]  # uses base filename as ID (no extension)
    labels_df = pd.DataFrame([], columns=cls_idx, index=index)
  5. 统计标注文件中存在的每个类别标签的实例数。

    from collections import Counter
    
    for label in labels:
        lbl_counter = Counter()
    
        with open(label) as lf:
            lines = lf.readlines()
    
        for line in lines:
            # classes for YOLO label uses integer at first position of each line
            lbl_counter[int(line.split(" ", 1)[0])] += 1
    
        labels_df.loc[label.stem] = lbl_counter
    
    labels_df = labels_df.fillna(0.0)  # replace `nan` values with `0.0`
  6. 以下是填充后的 DataFrame 的示例视图:

                                                           0    1    2    3    4    5
    '0000a16e4b057580_jpg.rf.00ab48988370f64f5ca8ea4...'  0.0  0.0  0.0  0.0  0.0  7.0
    '0000a16e4b057580_jpg.rf.7e6dce029fb67f01eb19aa7...'  0.0  0.0  0.0  0.0  0.0  7.0
    '0000a16e4b057580_jpg.rf.bc4d31cdcbe229dd022957a...'  0.0  0.0  0.0  0.0  0.0  7.0
    '00020ebf74c4881c_jpg.rf.508192a0a97aa6c4a3b6882...'  0.0  0.0  0.0  1.0  0.0  0.0
    '00020ebf74c4881c_jpg.rf.5af192a2254c8ecc4188a25...'  0.0  0.0  0.0  1.0  0.0  0.0
     ...                                                  ...  ...  ...  ...  ...  ...
    'ff4cd45896de38be_jpg.rf.c4b5e967ca10c7ced3b9e97...'  0.0  0.0  0.0  0.0  0.0  2.0
    'ff4cd45896de38be_jpg.rf.ea4c1d37d2884b3e3cbce08...'  0.0  0.0  0.0  0.0  0.0  2.0
    'ff5fd9c3c624b7dc_jpg.rf.bb519feaa36fc4bf630a033...'  1.0  0.0  0.0  0.0  0.0  0.0
    'ff5fd9c3c624b7dc_jpg.rf.f0751c9c3aa4519ea3c9d6a...'  1.0  0.0  0.0  0.0  0.0  0.0
    'fffe28b31f2a70d4_jpg.rf.7ea16bd637ba0711c53b540...'  0.0  6.0  0.0  0.0  0.0  0.0

这些行对应标签文件,每一个都对应数据集中的一张图像,列则对应你的类别标签索引。每一行都代表一个伪特征向量,包含数据集中存在的每个类别标签的计数。这种数据结构使得将 K 折交叉验证 应用于目标检测数据集成为可能。

K 折数据集拆分

  1. Now we will use the KFold class from sklearn.model_selection to generate k splits of the dataset.

    • 重要提示:
      • 设置 shuffle=True 可确保拆分中类别的随机分布。
      • 通过设置 random_state=M(其中 M 为你选择的整数),你可以获得可重复的结果。
    import random
    
    from sklearn.model_selection import KFold
    
    random.seed(0)  # for reproducibility
    ksplit = 5
    kf = KFold(n_splits=ksplit, shuffle=True, random_state=20)  # setting random_state for repeatable results
    
    kfolds = list(kf.split(labels_df))
  2. 数据集现已拆分为 k 折,每一折都有一个 trainval 索引列表。我们将构建一个 DataFrame 来更清晰地显示这些结果。

    folds = [f"split_{n}" for n in range(1, ksplit + 1)]
    folds_df = pd.DataFrame(index=index, columns=folds)
    
    for i, (train, val) in enumerate(kfolds, start=1):
        folds_df[f"split_{i}"].loc[labels_df.iloc[train].index] = "train"
        folds_df[f"split_{i}"].loc[labels_df.iloc[val].index] = "val"
  3. 现在,我们将计算每一折的类别标签分布,即 val 中存在的类别与 train 中存在的类别之比。

    fold_lbl_distrb = pd.DataFrame(index=folds, columns=cls_idx)
    
    for n, (train_indices, val_indices) in enumerate(kfolds, start=1):
        train_totals = labels_df.iloc[train_indices].sum()
        val_totals = labels_df.iloc[val_indices].sum()
    
        # To avoid division by zero, we add a small value (1E-7) to the denominator
        ratio = val_totals / (train_totals + 1e-7)
        fold_lbl_distrb.loc[f"split_{n}"] = ratio

    理想情况下,每个拆分中所有类别的比例应相当接近,且跨类别也应保持一致。然而,这取决于你的数据集的具体情况。

  4. 接下来,我们为每个拆分创建目录和数据集 YAML 文件。

    import datetime
    
    supported_extensions = [".jpg", ".jpeg", ".png"]
    
    # Initialize an empty list to store image file paths
    images = []
    
    # Loop through supported extensions and gather image files
    for ext in supported_extensions:
        images.extend(sorted((dataset_path / "images").rglob(f"*{ext}")))
    
    # Create the necessary directories and dataset YAML files
    save_path = Path(dataset_path / f"{datetime.date.today().isoformat()}_{ksplit}-Fold_Cross-val")
    save_path.mkdir(parents=True, exist_ok=True)
    ds_yamls = []
    
    for split in folds_df.columns:
        # Create directories
        split_dir = save_path / split
        split_dir.mkdir(parents=True, exist_ok=True)
        (split_dir / "train" / "images").mkdir(parents=True, exist_ok=True)
        (split_dir / "train" / "labels").mkdir(parents=True, exist_ok=True)
        (split_dir / "val" / "images").mkdir(parents=True, exist_ok=True)
        (split_dir / "val" / "labels").mkdir(parents=True, exist_ok=True)
    
        # Create dataset YAML files
        dataset_yaml = split_dir / f"{split}_dataset.yaml"
        ds_yamls.append(dataset_yaml)
    
        with open(dataset_yaml, "w") as ds_y:
            yaml.safe_dump(
                {
                    "path": split_dir.as_posix(),
                    "train": "train",
                    "val": "val",
                    "names": classes,
                },
                ds_y,
            )
  5. 最后,将图像和标签复制到每个拆分的相应目录('train' 或 'val')中。

    • 注意: 这部分代码所需的时间取决于你的数据集大小和系统硬件。
    import shutil
    
    from tqdm import tqdm
    
    for image, label in tqdm(zip(images, labels), total=len(images), desc="Copying files"):
        for split, k_split in folds_df.loc[image.stem].items():
            # Destination directory
            img_to_path = save_path / split / k_split / "images"
            lbl_to_path = save_path / split / k_split / "labels"
    
            # Copy image and label files to new directory (SamefileError if file already exists)
            shutil.copy(image, img_to_path / image.name)
            shutil.copy(label, lbl_to_path / label.name)

保存记录(可选)

作为选项,你可以将 K 折拆分和标签分布 DataFrame 的记录保存为 CSV 文件,以供将来参考。

folds_df.to_csv(save_path / "kfold_datasplit.csv")
fold_lbl_distrb.to_csv(save_path / "kfold_label_distribution.csv")

使用 K 折数据集拆分训练 YOLO

  1. 首先,加载 YOLO 模型。

    from ultralytics import YOLO
    
    weights_path = "path/to/weights.pt"  # use yolo26n.pt for a small model
    model = YOLO(weights_path, task="detect")
  2. 接下来,迭代数据集 YAML 文件以运行训练。结果将保存到由 projectname 参数指定的目录中。默认情况下,此目录为 'runs/detect/train#',其中 # 为整数索引。

    results = {}
    
    # Define your additional arguments here
    batch = 16
    project = "kfold_demo"
    epochs = 100
    
    for k, dataset_yaml in enumerate(ds_yamls):
        model = YOLO(weights_path, task="detect")
        results[k] = model.train(
            data=dataset_yaml, epochs=epochs, batch=batch, project=project, name=f"fold_{k + 1}"
        )  # include any additional train arguments
  3. 你也可以使用 Ultralytics data.utils.autosplit 函数进行自动数据集拆分:

    from ultralytics.data.split import autosplit
    
    # Automatically split dataset into train/val/test
    autosplit(path="path/to/images", weights=(0.8, 0.2, 0.0), annotated_only=True)

结论

在本指南中,我们探讨了使用 K 折交叉验证来训练 YOLO 目标检测模型的过程。我们学习了如何将数据集拆分为 K 个分区,从而确保不同折之间的类别分布平衡。

我们还探讨了创建报告 DataFrame 以可视化数据拆分和跨拆分的标签分布的过程,这让我们能清晰地洞察训练集和验证集的结构。

作为选项,我们保存了记录以便日后参考,这在大规模项目中或排查模型性能问题时特别有用。

最后,我们通过循环使用每个拆分实现了实际的模型训练,并保存了训练结果以供进一步分析和比较。

这种 K 折交叉验证技术是充分利用现有数据的有效方法,有助于确保模型性能在不同数据子集上保持可靠和一致。这会生成一个更具通用性且更可靠的模型,降低模型 过拟合 特定数据模式的可能性。

请记住,虽然我们在本指南中使用了 YOLO,但这些步骤大多适用于其他机器学习模型。理解这些步骤能让你在自己的机器学习项目中有效地应用交叉验证。

常见问题 (FAQ)

什么是 K 折交叉验证,为什么它在目标检测中很有用?

K 折交叉验证是一种将数据集划分为 'k' 个子集(折)以更可靠地评估模型性能的技术。每一折既充当训练数据,也充当 验证数据。在目标检测中,使用 K 折交叉验证有助于确保你的 Ultralytics YOLO 模型在不同数据拆分上的性能稳健且具有通用性,从而增强其可靠性。有关使用 Ultralytics YOLO 设置 K 折交叉验证的详细说明,请参考 使用 Ultralytics 进行 K 折交叉验证

如何使用 Ultralytics YOLO 实现 K 折交叉验证?

要使用 Ultralytics YOLO 实现 K 折交叉验证,你需要遵循以下步骤:

  1. 验证标注是否采用 YOLO 检测格式
  2. 使用像 sklearnpandaspyyaml 这样的 Python 库。
  3. 从你的数据集创建特征向量。
  4. Split your dataset using KFold from sklearn.model_selection.
  5. 在每个拆分上训练 YOLO 模型。

有关综合指南,请参阅我们文档中的 K 折数据集拆分 部分。

为什么我应该使用 Ultralytics YOLO 进行目标检测?

Ultralytics YOLO 提供具有高 准确率 和效率的尖端实时目标检测。它功能多样,支持多种 计算机视觉 任务,例如 检测实例分割语义分割分类。此外,它还能与诸如 Ultralytics Platform 等工具无缝集成,进行无代码模型训练和部署。欲了解更多详情,请探索我们 Ultralytics YOLO 页面 上的优势和功能。

如何确保我的标注符合 Ultralytics YOLO 的正确格式?

你的标注应遵循 YOLO 检测格式。每个标注文件必须列出对象类别及其在图像中的 边界框 坐标。YOLO 格式可确保为训练目标检测模型提供简化且标准化的数据处理。有关正确标注格式的更多信息,请访问 YOLO 检测格式指南

我可以使用 K 折交叉验证处理水果检测以外的自定义数据集吗?

可以,只要标注采用 YOLO 检测格式,你就可以将 K 折交叉验证用于任何自定义数据集。将数据集路径和类别标签替换为你自定义数据集特定的内容。这种灵活性确保任何目标检测项目都能从使用 K 折交叉验证进行的稳健模型评估中受益。有关实际示例,请查阅我们的 生成特征向量 部分。

评论