Salta para o conteúdo

K-Fold Cross Validation com Ultralytics

Introdução

Este guia abrangente ilustra a implementação do K-Fold Cross Validation para conjuntos de dados de deteção de objectos no ecossistema Ultralytics . Aproveitaremos o formato de deteção YOLO e as principais bibliotecas Python , como sklearn, pandas e PyYaml, para te guiar através da configuração necessária, do processo de geração de vectores de características e da execução de uma divisão de conjunto de dados K-Fold.

Visão geral da validação cruzada K-Fold

Quer o teu projeto envolva o conjunto de dados Fruit Detection ou uma fonte de dados personalizada, este tutorial tem como objetivo ajudar-te a compreender e aplicar o K-Fold Cross Validation para reforçar a fiabilidade e a robustez dos teus modelos de aprendizagem automática. Enquanto estiveres a aplicar k=5 para este tutorial, tem em mente que o número ideal de dobras pode variar dependendo do teu conjunto de dados e das especificidades do teu projeto.

Sem mais demoras, vamos lá!

Instalação

  • As tuas anotações devem estar no formato de deteçãoYOLO .

  • Este guia assume que os ficheiros de anotações estão disponíveis localmente.

  • Para a nossa demonstração, utilizamos o conjunto de dados Fruit Detection.

    • Este conjunto de dados contém um total de 8479 imagens.
    • Inclui 6 etiquetas de classe, cada uma com a contagem total de instâncias listada abaixo.
Etiqueta de classe Contagem de instâncias
Apple 7049
Uvas 7202
Ananás 1613
Laranja 15549
Banana 3536
Melancia 1976
  • Necessário Python pacotes incluem:

    • ultralytics
    • sklearn
    • pandas
    • pyyaml
  • Este tutorial funciona com k=5 dobras. No entanto, deves determinar o melhor número de dobras para o teu conjunto de dados específico.

  • Inicia um novo ambiente virtual Python (venv) para o teu projeto e ativa-o. Utiliza pip (ou o teu gestor de pacotes preferido) para instalar:

    • A biblioteca Ultralytics : pip install -U ultralytics. Em alternativa, podes clonar o ficheiro oficial repo.
    • Scikit-learn, pandas e PyYAML: pip install -U scikit-learn pandas pyyaml.
  • Verifica se as tuas anotações estão no formato de deteçãoYOLO .

    • Para este tutorial, todos os ficheiros de anotação encontram-se na pasta Fruit-Detection/labels diretório.

Geração de vectores de características para o conjunto de dados de deteção de objectos

  1. Começa por criar um novo ficheiro Python e importa as bibliotecas necessárias.

    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. Prossegue para recuperar todos os ficheiros de etiquetas do teu conjunto de dados.

    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. Agora, lê o conteúdo do ficheiro YAML do conjunto de dados e extrai os índices das etiquetas das classes.

    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. Inicializa um ficheiro vazio 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. Conta as instâncias de cada etiqueta de classe presente nos ficheiros de anotação.

    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. Segue-se uma vista de amostra do DataFrame preenchido:

                                                           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
    

As linhas indexam os ficheiros de etiquetas, cada um correspondendo a uma imagem no teu conjunto de dados, e as colunas correspondem aos índices das etiquetas de classe. Cada linha representa um pseudo-vetor de características, com a contagem de cada etiqueta de classe presente no teu conjunto de dados. Esta estrutura de dados permite a aplicação de K-Fold Cross Validation a um conjunto de dados de deteção de objectos.

Divisão do conjunto de dados K-Fold

  1. Agora vamos utilizar o KFold classe de sklearn.model_selection para gerar k divide o conjunto de dados.

    • Importante:
      • Definição shuffle=True garante uma distribuição aleatória de classes nas tuas divisões.
      • Por definição random_state=M onde M é um número inteiro escolhido, podes obter resultados repetíveis.
    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. O conjunto de dados foi agora dividido em k dobras, cada uma com uma lista de train e val índices. Constrói um DataFrame para apresentar estes resultados de forma mais clara.

    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. Agora vamos calcular a distribuição das etiquetas das classes para cada dobra como uma proporção das classes presentes em val para os presentes no 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
    

O cenário ideal é que todos os rácios de classe sejam razoavelmente semelhantes para cada divisão e entre classes. No entanto, isto estará sujeito às especificidades do teu conjunto de dados.

  1. Em seguida, criamos os directórios e os ficheiros YAML do conjunto de dados para cada divisão.

    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. Por fim, copia as imagens e as etiquetas para o respetivo diretório ('train' ou 'val') para cada divisão.

    • NOTA: O tempo necessário para esta parte do código varia de acordo com o tamanho do conjunto de dados e o hardware do sistema.
    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)
    

Guardar registos (opcional)

Opcionalmente, podes guardar os registos dos DataFrames de divisão K-Fold e de distribuição de etiquetas como ficheiros CSV para referência futura.

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

Treina YOLO utilizando K-Fold Data Splits

  1. Primeiro, carrega o modelo YOLO .

    weights_path = 'path/to/weights.pt'
    model = YOLO(weights_path, task='detect')
    
  2. Em seguida, itera sobre os ficheiros YAML do conjunto de dados para executar o treino. Os resultados serão salvos em um diretório especificado pelo parâmetro project e name argumentos. Por defeito, este diretório é 'exp/runs#' onde # é um índice inteiro.

    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
    

Conclusão

Neste guia, explorámos o processo de utilização da validação cruzada K-Fold para treinar o modelo de deteção de objectos YOLO . Aprendemos a dividir o nosso conjunto de dados em K partições, garantindo uma distribuição de classes equilibrada entre as diferentes dobras.

Também explorámos o procedimento de criação de DataFrames de relatório para visualizar as divisões de dados e as distribuições de etiquetas nestas divisões, fornecendo-nos uma visão clara da estrutura dos nossos conjuntos de treino e validação.

Opcionalmente, guardámos os nossos registos para referência futura, o que pode ser particularmente útil em projectos de grande escala ou na resolução de problemas de desempenho do modelo.

Por fim, implementámos o treino do modelo real utilizando cada divisão num ciclo, guardando os resultados do treino para análise e comparação posteriores.

Esta técnica de validação cruzada K-Fold é uma forma robusta de tirar o máximo partido dos dados disponíveis e ajuda a garantir que o desempenho do modelo é fiável e consistente em diferentes subconjuntos de dados. Isto resulta num modelo mais generalizável e fiável que tem menos probabilidades de se ajustar excessivamente a padrões de dados específicos.

Lembra-te de que, embora tenhamos utilizado YOLO neste guia, estes passos são, na sua maioria, transferíveis para outros modelos de aprendizagem automática. A compreensão destes passos permite-te aplicar a validação cruzada de forma eficaz nos teus próprios projectos de aprendizagem automática. Boa programação!



Criado em 2023-11-12, Atualizado em 2023-12-03
Autores: glenn-jocher (5), Burhan-Q (1)

Comentários