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

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 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-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, Обновлено 2023-12-03
Авторы: glenn-jocher (5), Burhan-Q (1)

Комментарии