跳至内容

K-Fold 交叉验证Ultralytics

导言

本综合指南说明了如何在Ultralytics 生态系统中对对象检测数据集实施 K 折交叉验证。我们将利用YOLO 检测格式和关键的Python 库(如 sklearn、pandas 和 PyYaml),指导您完成必要的设置、生成特征向量的过程以及 K-Fold 数据集拆分的执行。

K 折交叉验证概述

无论您的项目涉及水果检测数据集还是自定义数据源,本教程都将帮助您理解和应用 K 折交叉验证,以增强机器学习模型的可靠性和稳健性。当我们应用 k=5 请记住,最佳的折叠次数可能因数据集和项目的具体情况而异。

废话不多说,让我们开始吧!

设置

  • 您的注释应采用YOLO 检测格式

  • 本指南假设注释文件可从本地获取。

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

    • 该数据集共包含 8479 幅图像。
    • 它包括 6 个类别标签,每个标签的实例总数如下。
类别标签 实例计数
苹果 7049
葡萄 7202
菠萝 1613
橙色 15549
香蕉 3536
西瓜 1976
  • 必要的Python 软件包包括

    • ultralytics
    • sklearn
    • pandas
    • pyyaml
  • 本教程使用 k=5 折叠次数。不过,您应该根据具体数据集确定最佳折叠次数。

  • 启动新的Python 虚拟环境 (venv)并激活它。使用 pip (软件包管理器)进行安装:

    • Ultralytics 图书馆: pip install -U ultralytics.或者,您也可以克隆官方的 repo.
    • Scikit-learn、pandas 和 PyYAML: pip install -U scikit-learn pandas pyyaml.
  • 验证您的注释是否采用YOLO 检测格式

    • 在本教程中,所有注释文件都在 Fruit-Detection/labels 目录。

生成物体检测数据集的特征向量

  1. 首先创建一个新的Python 文件,然后导入所需的库。

    import datetime
    import shutil
    from pathlib import Path
    from collections import Counter
    
    import yaml
    import numpy as np
    import pandas as pd
    from ultralytics import YOLO
    from sklearn.model_selection import KFold
    
  2. 继续检索数据集的所有标签文件。

    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 文件的内容并提取类标签的索引。

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

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

    for label in labels:
        lbl_counter = Counter()
    
        with open(label,'r') as lf:
            lines = lf.readlines()
    
        for l in lines:
            # classes for YOLO label uses integer at first position of each line
            lbl_counter[int(l.split(' ')[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. 现在,我们将使用 KFoldsklearn.model_selection 以产生 k 对数据集进行分割。

    • 重要:
      • 设置 shuffle=True 确保在分班时,班级的分布是随机的。
      • 通过设置 random_state=M 其中 M 是一个选定的整数,这样就可以得到可重复的结果。
    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=indx, columns=folds)
    
    for idx, (train, val) in enumerate(kfolds, start=1):
        folds_df[f'split_{idx}'].loc[labels_df.iloc[train].index] = 'train'
        folds_df[f'split_{idx}'].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
    

最理想的情况是,每次分割和不同类别的所有类别比率都相当相似。不过,这取决于数据集的具体情况。

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

    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 (unchanged)
    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)
    
  2. 最后,将图像和标签复制到每个分割的相应目录("train "或 "val")中。

    • 注意:这部分代码所需的时间将根据数据集的大小和系统硬件而有所不同。
    for image, label in zip(images, labels):
        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 折分割和标签分布数据框的记录保存为 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 模型。

    weights_path = 'path/to/weights.pt'
    model = YOLO(weights_path, task='detect')
    
  2. 接下来,遍历数据集 YAML 文件以运行训练。结果将保存到由 projectname 参数。默认情况下,该目录为 "exp/runs#",其中 # 为整数索引。

    results = {}
    
    # Define your additional arguments here
    batch = 16
    project = 'kfold_demo'
    epochs = 100
    
    for k in range(ksplit):
        dataset_yaml = ds_yamls[k]
        model.train(data=dataset_yaml,epochs=epochs, batch=batch, project=project)  # include any train arguments
        results[k] = model.metrics  # save output metrics for further analysis
    

结论

在本指南中,我们探索了使用 K 折交叉验证来训练YOLO 物体检测模型的过程。我们学习了如何将数据集分成 K 个分区,确保不同折叠中的类分布均衡。

我们还探索了创建报告 DataFrames 的程序,以可视化数据拆分和标签在这些拆分中的分布,让我们清楚地了解训练集和验证集的结构。

此外,我们还保存了我们的记录,以备将来参考,这在大型项目或排除模型性能故障时尤为有用。

最后,我们在一个循环中使用每个拆分来执行实际的模型训练,保存训练结果,以便进一步分析和比较。

这种 K 折交叉验证技术是充分利用现有数据的一种稳健方法,有助于确保模型在不同数据子集中的性能是可靠和一致的。这将产生一个更具通用性和可靠性的模型,从而减少对特定数据模式的过度拟合。

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



创建于 2023-11-12,更新于 2023-12-03
作者:glenn-jocher(5)、Burhan-Q(1)

评论