Overslaan naar inhoud

K-voudige kruisvalidatie met Ultralytics

Inleiding

Deze uitgebreide handleiding illustreert de implementatie van K-Fold Cross Validation voor objectdetectie datasets binnen het Ultralytics ecosysteem. We maken gebruik van het YOLO detectieformaat en de belangrijkste Python bibliotheken zoals sklearn, pandas en PyYaml om je door de noodzakelijke instellingen, het proces van het genereren van kenmerkvectoren en de uitvoering van een K-Fold dataset splitsing te leiden.

Overzicht K-voudige kruisvalidatie

Of je project nu de Fruit Detection dataset of een aangepaste databron bevat, deze tutorial helpt je bij het begrijpen en toepassen van K-Fold Cross Validation om de betrouwbaarheid en robuustheid van je machine learning modellen te versterken. Terwijl we k=5 Houd er rekening mee dat het optimale aantal vouwen voor deze tutorial kan variëren, afhankelijk van je dataset en de specifieke kenmerken van je project.

Zonder verder oponthoud, laten we erin duiken!

Setup

  • Je annotaties moeten het formaatYOLO detectie hebben.

  • Deze handleiding gaat ervan uit dat annotatiebestanden lokaal beschikbaar zijn.

  • Voor onze demonstratie gebruiken we de Fruit Detection dataset.

    • Deze dataset bevat in totaal 8479 afbeeldingen.
    • Het bevat 6 klassenlabels, elk met zijn totale aantal instanties hieronder vermeld.
Klasse-etiket Aantal instanties
Appel 7049
Druiven 7202
Ananas 1613
Oranje 15549
Banaan 3536
Watermeloen 1976
  • Noodzakelijke Python pakketten zijn onder andere:

    • ultralytics
    • sklearn
    • pandas
    • pyyaml
  • Deze tutorial werkt met k=5 vouwen. Je moet echter het beste aantal vouwen bepalen voor je specifieke dataset.

  • Start een nieuwe Python virtuele omgeving (venv) voor je project en activeer het. Gebruik pip (of de pakketbeheerder van je voorkeur) om te installeren:

    • De Ultralytics bibliotheek: pip install -U ultralytics. Je kunt ook de officiële repo.
    • Scikit-learn, pandas en PyYAML: pip install -U scikit-learn pandas pyyaml.
  • Controleer of je annotaties het formaatYOLO detectie hebben.

    • Voor deze tutorial zijn alle annotatiebestanden te vinden in de Fruit-Detection/labels map.

Genereren van objectvectoren voor objectdetectiedataset

  1. Begin met het maken van een nieuw Python bestand en importeer de benodigde bibliotheken.

    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. Ga verder met het ophalen van alle labelbestanden voor je dataset.

    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. Lees nu de inhoud van het YAML-bestand van de dataset en extraheer de indices van de klassenlabels.

    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. Een lege 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. Tel de instanties van elke klasse-label in de annotatiebestanden.

    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. Het volgende is een voorbeeldweergave van het bevolkte 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
    

De rijen indexeren de labelbestanden, die elk corresponderen met een afbeelding in je dataset, en de kolommen corresponderen met je klasse-label indices. Elke rij vertegenwoordigt een pseudo-kenmerkvector, met de telling van elk klasse-label aanwezig in je dataset. Deze gegevensstructuur maakt het mogelijk om K-Fold kruisvalidatie toe te passen op een dataset voor objectdetectie.

K-voudige dataset opsplitsing

  1. Nu zullen we de KFold klasse van sklearn.model_selection om k splitsingen van de dataset.

    • Belangrijk:
      • Instellen shuffle=True zorgt voor een willekeurige verdeling van klassen in je splitsingen.
      • Door random_state=M waarbij M een gekozen geheel getal is, kun je herhaalbare resultaten krijgen.
    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. De dataset is nu opgesplitst in k vouwen, elk met een lijst van train en val indexen. We zullen een DataFrame maken om deze resultaten duidelijker weer te geven.

    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. Nu berekenen we de verdeling van klassenlabels voor elke vouw als een verhouding van de klassen die aanwezig zijn in val aan de aanwezigen in 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
    

Het ideale scenario is dat alle klassenratio's redelijk gelijk zijn voor elke splitsing en tussen klassen. Dit is echter afhankelijk van de specifieke kenmerken van je dataset.

  1. Vervolgens maken we de mappen en dataset YAML-bestanden aan voor elke splitsing.

    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. Kopieer tenslotte de afbeeldingen en labels naar de respectievelijke map ('train' of 'val') voor elke splitsing.

    • OPMERKING: De tijd die nodig is voor dit deel van de code hangt af van de grootte van je dataset en je systeemhardware.
    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)
    

Records opslaan (optioneel)

Optioneel kun je de records van de K-Fold split en label distributie DataFrames opslaan als CSV-bestanden voor toekomstig gebruik.

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

Train YOLO met behulp van K-voudige gegevenssplitsingen

  1. Laad eerst het model YOLO .

    weights_path = 'path/to/weights.pt'
    model = YOLO(weights_path, task='detect')
    
  2. Doorloop vervolgens de YAML-bestanden van de dataset om de training uit te voeren. De resultaten worden opgeslagen in een map gespecificeerd door de project en name argumenten. Standaard is deze map 'exp/runs#' waarbij # een gehele index is.

    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
    

Conclusie

In deze handleiding hebben we het proces onderzocht van het gebruik van K-voudige kruisvalidatie voor het trainen van het YOLO objectdetectiemodel. We hebben geleerd hoe we onze dataset in K partities kunnen verdelen, waarbij we zorgen voor een evenwichtige klassenverdeling over de verschillende vouwen.

We onderzochten ook de procedure voor het maken van rapport DataFrames om de datasplitsingen en labelverdelingen over deze splitsingen te visualiseren, waardoor we een duidelijk inzicht kregen in de structuur van onze trainings- en validatiesets.

Optioneel sloegen we onze records op voor toekomstig gebruik, wat vooral handig kan zijn bij grootschalige projecten of bij het oplossen van problemen met modelprestaties.

Tot slot implementeerden we de eigenlijke modeltraining met elke splitsing in een lus, waarbij we onze trainingsresultaten opsloegen voor verdere analyse en vergelijking.

Deze techniek van K-voudige kruisvalidatie is een robuuste manier om het beste uit je beschikbare gegevens te halen en het helpt om ervoor te zorgen dat de prestaties van je model betrouwbaar en consistent zijn over verschillende subsets van gegevens. Dit resulteert in een meer generaliseerbaar en betrouwbaar model dat minder snel te veel past bij specifieke gegevenspatronen.

Onthoud dat we in deze gids weliswaar YOLO hebben gebruikt, maar dat deze stappen meestal ook toepasbaar zijn op andere modellen voor machinaal leren. Als je deze stappen begrijpt, kun je cross-validation effectief toepassen in je eigen machine learning projecten. Veel plezier met coderen!



Gemaakt op 2023-11-12, Bijgewerkt op 2023-12-03
Auteurs: glenn-jocher (5), Burhan-Q (1)

Reacties