Illustration
Blog

Un exemple détaillé montrant comment générer vos données en parallèle avec PyTorch

Fork   Star
pytorch data loader large volume de données parallèle

Par Afshine Amidi et Shervine Amidi

Motivation

Avez-vous déjà eu à charger en mémoire une quantité de données tellement élevée que vous auriez aimé une alternative à cette manière de procéder peu optimale ? De nos jours, la quantité de données à laquelle nous avons affaire tend à être de plus en plus élevée et va de pair avec l'évolution d'algorithmes qui deviennent toujours plus exigeants en ressources.

Nous devons garder en tête que dans certains cas, même les meilleures configurations PC risquent de ne pas avoir assez de mémoire RAM pour garder des données qui avaient l'habitude de ne pas poser de problème auparavant. C'est pour cette raison que nous avons besoin d'autres moyens pour effectuer cette même tâche, mais de manière plus efficace. Dans cet article de blog, nous allons voir comment générer vos données sur plusieurs coeurs en temps réel et les envoyer directement vers votre modèle d'apprentissage profond.

Ce guide a pour but de vous montrer comment le faire avec le package PyTorch qui, étant adapté à l'utilisation GPU, a besoin d'une structure de génération de données efficace pour mettre à profit le potentiel de votre GPU pendant la phase d'entraînement de votre modèle.

Guide

Situation précédente

Avant de lire ce guide, votre script PyTorch ressemblait proablement à ceci :

# Chargement en mémoire de l'ensemble des données
X, y = torch.load('ensemble_d_entrainement_avec_labels.pt')

# Entrainement du modèle
for epoch in range(max_epochs):
    for i in range(n_lots):
        # Lots locaux et leurs labels associés
        X_local, y_local = X[i*n_lots:(i+1)*n_lots,], y[i*n_lots:(i+1)*n_lots,]

        # Votre modèle
        [...]

ou même à cela :

# Générateur peu optimisé
generateur_entrainement = UnGenerateurMonocoeur('ensemble_d_entrainement_avec_labels.pt')

# Entrainement du modèle
for epoch in range(max_epochs):
    for X_local, y_local in generateur_entrainement:
        # Votre modèle
        [...]

Ce guide se concentre sur l'optimisation du chargement des données de sorte à ce qu'il ne devienne pas un goulot d'étranglement du processus d'entraînement.

Pour ce faire, nous allons utiliser une recette construisant un générateur de données parallèle adapté à ce genre de situation. Le code qui va suivre donne une structure qui peut coller à n'importe quel projet ; copiez/collez les parties de code suivantes et adaptez les valeurs à votre situation.

Notations

Avant de se lancer, parlons d'abord de quelques astuces qui permettent de bien s'organiser lorsque l'on a affaire à des données volumineuses.

Notons ID la chaîne de caractères Python qui identifie un exemple donné de notre ensemble. Un moyen efficace pour traquer les exemples ainsi que leurs labels est d'adopter la convention suivante :

  1. Avoir un dictionnaire partition contenant :

    • dans partition['entrainement'] une liste d'identifiants correspondants aux exemples de votre ensemble d'entraînement
    • dans partition['validation'] une liste d'identifiants correspondants aux exemples de votre ensemble de validation
  2. Avoir un dictionnaire labels où pour chaque identifiant ID de votre ensemble de données, le label associé est donné par labels[ID]

Disons par exemple que notre ensemble d'entraînement contient id-1, id-2 et id-3 de labels respectifs 0, 1 et 2. Supposons aussi que l'ensemble de validation contient l'exemple id-4 ayant pour label 1. Dans ce cas, les variables Python partition et labels seraient de la forme

>>> partition
{'entrainement': ['id-1', 'id-2', 'id-3'], 'validation': ['id-4']}

et

>>> labels
{'id-1': 0, 'id-2': 1, 'id-3': 2, 'id-4': 1}

Aussi, par souci de modularité, nous allons écrire le code PyTorch et les classes que nous allons personnaliser dans des fichiers différents. Votre dossier aura donc le format qui suit :

folder/
├── mes_classes.py
├── script_pytorch.py
└── donnees/

donnees/ est le dossier où se trouvent vos données.

Il est par ailleurs bon de noter que le code donné par ce guide a pour but d'être général et minimal, de manière à ce que vous puissiez l'adapter facilement à votre propre cas.

Données

Maintenant, rentrons dans les détails qui vont permettre d'écrire votre classe Python Donnees, qui a pour but de recenser les caractéristiques principales des données que vous souhaitez générer.

Tout d'abord, écrivons la fonction qui initialise cette classe. Celle-ci hérite des propriétés de torch.utils.data.Dataset de sorte à pouvoir profiter de fonctionnalités commodes telles que le multiprocessing.

def __init__(self, liste_IDs, labels):
    'Initialisation'
    self.labels = labels
    self.liste_IDs = liste_IDs

On y stocke des informations importantes telles que les labels ou la liste des IDs que l'on souhaite générer à chaque parcours.

Chaque requête vise un exemple dont l'indice maximum est spécifié dans la méthode __len__.

def __len__(self):
    'Représente le nombre total d'exemples du jeu de données'
    return len(self.liste_IDs)

Maintenant, à chaque requête d'un indice d'un exemple donné, le générateur exécute la méthode __getitem__ pour générer les données qui lui sont associées.

def __getitem__(self, indice):
    'Génère un exemple à partir du jeu de données'
    # Sélection de l'exemple
    ID = self.liste_IDs[indice]

    # Chargement des données et obtention du label
    X = torch.load('donnees/' + ID + '.pt')
    y = self.labels[ID]

    return X, y

Au cours de la génération des données, cette méthode lit le tenseur Torch d'un exemple donné à partir du fichier correspondant ID.pt. Notre code étant adapté à l'utilisation multi-coeur du processeur, vous pourrez noter le fait qu'il est possible d'effectuer des opérations plus compliquées (par ex. directement opérer sur des fichiers sources quitte à effectuer des calculs supplémentaires) sans se soucier du fait que la génération de données ne devienne un goulot d'étranglement du processus d'entraînement.

Le code complet correspondant aux étapes que nous venons de décrire est donné ci-dessous.

import torch

class Donnees(torch.utils.data.Dataset):
  'Caractérise un jeu de données pour PyTorch'
  def __init__(self, liste_IDs, labels):
        'Initialisation'
        self.labels = labels
        self.liste_IDs = liste_IDs

  def __len__(self):
        'Représente le nombre total d'exemples du jeu de données'
        return len(self.liste_IDs)

  def __getitem__(self, indice):
        'Génère un exemple à partir du jeu de données'
        # Sélection de l'exemple
        ID = self.liste_IDs[indice]

        # Chargement des données et obtention du label
        X = torch.load('donnees/' + ID + '.pt')
        y = self.labels[ID]

        return X, y

Script PyTorch

À présent, nous devons modifier le script PyTorch de manière adéquate pour prendre en compte le générateur de données que nous venons de créer. Pour ce faire, nous faisons usage de la classe PyTorch DataLoader qui, en plus de notre classe Donnees, prend aussi les arguments importants suivants :

La structure du code que vous pouvez écrire dans votre script est présenté ci-dessous.

import torch
from my_classes import Donnees


# CUDA pour PyTorch
use_cuda = torch.cuda.is_available()
device = torch.device("cuda:0" if use_cuda else "cpu")
torch.backends.cudnn.benchmark = True

# Paramètres
params = {'batch_size': 64,
          'shuffle': True,
          'num_workers': 6}
max_epochs = 100

# Ensemble de données
partition = # IDs
labels = # Labels

# Générateurs
jeu_entrainement = Donnees(partition['train'], labels)
generateur_entrainement = torch.utils.data.DataLoader(jeu_entrainement, **params)

jeu_validation = Donnees(partition['validation'], labels)
generateur_validation = torch.utils.data.DataLoader(jeu_validation, **params)

# Itération sur les epochs
for epoch in range(max_epochs):
    # Entrainement
    for lot_local, labels_local in generateur_entrainement:
        # Transfert vers le GPU
        lot_local, labels_local = lot_local.to(device), labels_local.to(device)

        # Calculs effectués par le modèle
        [...]

    # Validation
    with torch.set_grad_enabled(False):
        for lot_local, labels_local in generateur_validation:
            # Transfert vers le GPU
            lot_local, labels_local = lot_local.to(device), labels_local.to(device)

            # Calculs effectués par le modèle
            [...]

Conclusion

C'est bon ! Vous pouvez maintenant lancer votre script PyTorch avec la commande

python3 script_pytorch.py

et vous verrez lors de la phase d'entrainement que les données sont générées en parallèle par le CPU pour aller alimenter le GPU en temps réel.

Quelques liens pouvant aussi vous intéresser...

Pense-bêtes d'intelligence artificielle

  • • Modèles basés sur le réflex
  • • Modèles basés sur les états
  • • Modèles basés sur les variables
  • • Modèles basés sur la logique

Pense-bêtes d'apprentissage automatique

  • • Apprentissage supervisé
  • • Apprentissage non supervisé
  • • Apprentissage profond
  • • Petites astuces d'apprentissage automatique

Pense-bêtes d'apprentissage profond

  • • Réseaux de neurones convolutionnels
  • • Réseaux de neurones récurrents
  • • Petites astuces d'apprentissage profond