Un exemple détaillé montrant comment générer vos données en parallèle avec PyTorch
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 :
-
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
- dans
-
Avoir un dictionnaire
labels
où pour chaque identifiantID
de votre ensemble de données, le label associé est donné parlabels[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/
où 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 :
batch_size
, qui dénote le nombre d'exemples contenu dans chaque lot généré.shuffle
. Quand ce paramètre a la valeurTrue
, un nouvel ordre d'exploration sera généré à chaque nouveau parcours du jeu de données (l'ordre restera inchangé dans le cas contraire). Modifier l'ordre de parcours des exemples au cours des epochs est utile pour donner l'impression au modèle que chaque parcours est différent des autres. En pratique, cela aide le modèle à être plus robuste.num_workers
, qui représente le nombre de threads générant des lots de données en parallèle. Un nombre de workers assez élevé assure le fait que les calculs effectués sur le CPU sont gérés de manière efficace, ou en d'autres termes que le goulot d'étranglement de l'ensemble du processus d'entraînement sera bien dû aux opérations de propagation du réseau de neurones sur le GPU (et ne sera pas dû à la génération de données).
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.