Перейти к содержимому

K-Fold Cross Validation с Ultralytics

Введение

Это исчерпывающее руководство иллюстрирует реализацию K-Fold Cross Validation для наборов данных обнаружения объектов в экосистеме Ultralytics. Мы используем формат обнаружения YOLO и ключевые библиотеки Python, такие как sklearn, pandas и PyYaml, чтобы провести тебя через необходимую настройку, процесс генерации векторов признаков и выполнение K-Fold разбиения набора данных.

Обзор перекрестной валидации K-Fold

Независимо от того, задействован ли в твоем проекте набор данных Fruit Detection или пользовательский источник данных, это руководство призвано помочь тебе понять и применить K-Fold Cross Validation для повышения надежности и прочности твоих моделей машинного обучения. Пока мы будем применять k=5 складки для этого руководства, имей в виду, что оптимальное количество складок может меняться в зависимости от набора данных и специфики твоего проекта.

Без лишних слов, давай погрузимся в игру!

Настройка

  • Твои аннотации должны быть в форматеYOLO detection.

  • В этом руководстве предполагается, что файлы аннотаций доступны локально.

  • Для демонстрации мы используем набор данных Fruit Detection.

    • Этот набор данных содержит в общей сложности 8479 изображений.
    • Он включает в себя 6 меток классов, для каждого из которых общее количество экземпляров указано ниже.
Ярлык класса Количество экземпляров
Apple 7049
Виноград 7202
Ананас 1613
Оранжевый 15549
Банан 3536
Арбуз 1976
  • Необходимые пакеты Python включают в себя:

    • ultralytics
    • sklearn
    • pandas
    • pyyaml
  • Этот учебник работает с k=5 сгибов. Однако ты должен определить оптимальное количество складок для своего конкретного набора данных.

  • Запустите новую виртуальную среду Python (venv) для своего проекта и активируй его. Используй pip (или предпочитаемый тобой менеджер пакетов) для установки:

    • Библиотека Ultralytics: pip install -U ultralytics. Как вариант, ты можешь клонировать официальный Репо.
    • Scikit-learn, pandas и PyYAML: pip install -U scikit-learn pandas pyyaml.
  • Убедись, что твои аннотации имеют формат обнаруженияYOLO .

    • В этом учебнике все файлы аннотаций находятся в папке Fruit-Detection/labels каталог.

Генерирование векторов признаков для набора данных обнаружения объектов

  1. Начни с создания нового файла Python и импортируй необходимые библиотеки.

    import datetime
    import shutil
    from collections import Counter
    from pathlib import Path
    
    import numpy as np
    import pandas as pd
    import yaml
    from sklearn.model_selection import KFold
    from ultralytics import YOLO
    
  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-Fold Cross Validation к набору данных для обнаружения объектов.

Разделение набора данных K-Fold

  1. Теперь мы будем использовать KFold класс из sklearn.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 складки, каждая из которых имеет список train и val Индексы. Для более наглядного отображения этих результатов мы построим 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-Fold split и распределения меток DataFrames в виде CSV-файлов для дальнейшего использования.

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

Обучи YOLO с помощью K-Fold разбиения данных

  1. Для начала загрузи модель YOLO.

    weights_path = "path/to/weights.pt"
    model = YOLO(weights_path, task="detect")
    
  2. Далее пройдись итерацией по YAML-файлам набора данных, чтобы запустить обучение. Результаты будут сохранены в директорию, указанную параметром project и name аргументы. По умолчанию эта директория - '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-Fold для обучения модели обнаружения объектов YOLO. Мы узнали, как разбить наш набор данных на K разделов, обеспечив сбалансированное распределение классов по разным складкам.

Мы также изучили процедуру создания отчета DataFrames для визуализации разбиения данных и распределений меток по этим разбиениям, что дало нам четкое представление о структуре наших обучающих и проверочных наборов.

По желанию мы сохраняли свои записи для дальнейшего использования, что может быть особенно полезно в масштабных проектах или при устранении неполадок в работе модели.

Наконец, мы реализовали фактическое обучение модели, используя каждый сплит в цикле, сохраняя результаты обучения для дальнейшего анализа и сравнения.

Эта техника перекрестной валидации K-Fold - надежный способ извлечь максимум пользы из имеющихся у тебя данных, и она помогает обеспечить надежность и согласованность работы твоей модели в разных подмножествах данных. В результате получается более обобщенная и надежная модель, которая с меньшей вероятностью будет подстраиваться под конкретные шаблоны данных.

Помни, что хотя в этом руководстве мы использовали YOLO, эти шаги в основном можно перенести и на другие модели машинного обучения. Понимание этих шагов позволит тебе эффективно применять кросс-валидацию в своих собственных проектах по машинному обучению. Счастливого кодинга!



Создано 2023-11-12, Обновлено 2024-05-18
Авторы: glenn-jocher (6), Burhan-Q (1)

Комментарии