Skip to content

Validation croisée K-Fold avec Ultralytics

Introduction

Ce guide complet illustre la mise en œuvre de la validation croisée K-Fold pour les ensembles de données de détection d'objets dans l'écosystème Ultralytics . Nous nous appuierons sur le format de détection YOLO et les principales bibliothèques Python telles que sklearn, pandas et PyYaml pour te guider dans la configuration nécessaire, le processus de génération de vecteurs de caractéristiques et l'exécution d'un fractionnement de l'ensemble de données K-Fold.

Aperçu de la validation croisée K-Fold

Que ton projet implique l'ensemble de données Fruit Detection ou une source de données personnalisée, ce tutoriel a pour but de t'aider à comprendre et à appliquer la validation croisée K-Fold pour renforcer la fiabilité et la robustesse de tes modèles d'apprentissage automatique. Alors que nous appliquons k=5 plis pour ce tutoriel, garde à l'esprit que le nombre optimal de plis peut varier en fonction de ton ensemble de données et des spécificités de ton projet.

Sans plus attendre, plongeons dans le vif du sujet !

Mise en place

  • Tes annotations doivent ĂŞtre au format de dĂ©tectionYOLO .

  • Ce guide suppose que les fichiers d'annotation sont disponibles localement.

  • Pour notre dĂ©monstration, nous utilisons l'ensemble de donnĂ©es de dĂ©tection de fruits.

    • Cet ensemble de donnĂ©es contient un total de 8479 images.
    • Il comprend 6 Ă©tiquettes de classe, chacune avec son nombre total d'instances indiquĂ© ci-dessous.
Étiquette de classe Nombre d'instances
Pomme 7049
Raisins 7202
Ananas 1613
Orange 15549
Banane 3536
Melon d'eau 1976
  • Les paquets nĂ©cessaires sur Python comprennent

    • ultralytics
    • sklearn
    • pandas
    • pyyaml
  • Ce tutoriel fonctionne avec k=5 plis. Cependant, tu dois dĂ©terminer le meilleur nombre de plis pour ton ensemble de donnĂ©es spĂ©cifique.

  • Lance un nouvel environnement virtuel Python (venv) pour ton projet et l'activer. Utilise pip (ou ton gestionnaire de paquets prĂ©fĂ©rĂ©) pour l'installer :

    • La bibliothèque Ultralytics : pip install -U ultralytics. Tu peux aussi cloner le fichier officiel repo.
    • Scikit-learn, pandas et PyYAML : pip install -U scikit-learn pandas pyyaml.
  • VĂ©rifie que tes annotations sont au format de dĂ©tectionYOLO .

    • Pour ce tutoriel, tous les fichiers d'annotation se trouvent dans le dossier Fruit-Detection/labels rĂ©pertoire.

Générer des vecteurs de caractéristiques pour l'ensemble de données de détection d'objets

  1. Commence par créer un nouveau fichier Python et importe les bibliothèques nécessaires.

    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. Procède à la récupération de tous les fichiers d'étiquettes de ton jeu de données.

    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. Maintenant, lis le contenu du fichier YAML du jeu de données et extrais les indices des étiquettes de classe.

    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. Initialise un fichier vide 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. Compte les instances de chaque classe-étiquette présente dans les fichiers d'annotation.

    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. Voici un exemple de vue du DataFrame peuplé :

                                                           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
    

Les lignes indexent les fichiers d'étiquettes, chacun correspondant à une image de ton jeu de données, et les colonnes correspondent à tes indices d'étiquettes de classe. Chaque ligne représente un pseudo-vecteur de caractéristiques, avec le nombre de chaque étiquette de classe présente dans ton ensemble de données. Cette structure de données permet d'appliquer la validation croisée K-Fold à un ensemble de données de détection d'objets.

Fractionnement de l'ensemble de données K-Fold

  1. Nous allons maintenant utiliser le KFold de la classe sklearn.model_selection pour générer k les divisions de l'ensemble de données.

    • Important :
      • RĂ©glage shuffle=True assure une rĂ©partition alĂ©atoire des classes dans tes fractionnements.
      • En rĂ©glant random_state=M oĂą M est un nombre entier choisi, tu peux obtenir des rĂ©sultats reproductibles.
    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. L'ensemble des données a maintenant été divisé en k plis, chacun ayant une liste de train et val indices. Nous allons construire un DataFrame pour afficher ces résultats plus clairement.

    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. Nous allons maintenant calculer la distribution des étiquettes de classe pour chaque pli sous la forme d'un ratio des classes présentes dans val aux personnes présentes dans 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
    

Le scénario idéal est que tous les ratios de classe soient raisonnablement similaires pour chaque fractionnement et pour toutes les classes. Cela dépend toutefois des spécificités de ton jeu de données.

  1. Ensuite, nous créons les répertoires et les fichiers YAML des jeux de données pour chaque fractionnement.

    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. Enfin, copie les images et les étiquettes dans le répertoire respectif ('train' ou 'val') pour chaque fractionnement.

    • NOTE : Le temps nĂ©cessaire pour cette partie du code variera en fonction de la taille de ton ensemble de donnĂ©es et du matĂ©riel de ton système.
    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)
    

Sauvegarder les enregistrements (optionnel)

En option, tu peux enregistrer les enregistrements des DataFrames de fractionnement K-Fold et de distribution d'étiquettes sous forme de fichiers CSV pour référence ultérieure.

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

Entraîne YOLO à l'aide de K-Fold Data Splits

  1. Charge d'abord le modèle YOLO .

    weights_path = 'path/to/weights.pt'
    model = YOLO(weights_path, task='detect')
    
  2. Ensuite, itère sur les fichiers YAML de l'ensemble de données pour exécuter la formation. Les résultats seront enregistrés dans un répertoire spécifié par l'option project et name arguments. Par défaut, ce répertoire est 'exp/runs#' où # est un indice entier.

    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
    

Conclusion

Dans ce guide, nous avons exploré le processus d'utilisation de la validation croisée K-Fold pour l'entraînement du modèle de détection d'objets YOLO . Nous avons appris à diviser notre ensemble de données en K partitions, en veillant à ce que la répartition des classes soit équilibrée entre les différents plis.

Nous avons également exploré la procédure de création de DataFrames de rapport pour visualiser les divisions de données et les distributions d'étiquettes sur ces divisions, ce qui nous a permis d'avoir un aperçu clair de la structure de nos ensembles de formation et de validation.

En option, nous avons sauvegardé nos enregistrements pour les consulter ultérieurement, ce qui pourrait être particulièrement utile dans les projets à grande échelle ou lors du dépannage des performances du modèle.

Enfin, nous avons mis en œuvre la formation réelle du modèle en utilisant chaque fractionnement dans une boucle, en sauvegardant nos résultats de formation pour une analyse et une comparaison plus approfondies.

Cette technique de validation croisée K-Fold est un moyen robuste de tirer le meilleur parti de tes données disponibles, et elle permet de s'assurer que les performances de ton modèle sont fiables et cohérentes dans les différents sous-ensembles de données. Il en résulte un modèle plus généralisable et plus fiable, moins susceptible de s'adapter de façon excessive à des modèles de données spécifiques.

N'oublie pas que même si nous avons utilisé YOLO dans ce guide, ces étapes sont en grande partie transférables à d'autres modèles d'apprentissage automatique. Comprendre ces étapes te permet d'appliquer efficacement la validation croisée dans tes propres projets d'apprentissage automatique. Bon codage !



Créé le 2023-11-12, Mis à jour le 2023-12-03
Auteurs : glenn-jocher (5), Burhan-Q (1)

Commentaires