Illustration
Blog

Un exemple détaillé montrant comment utiliser un générateur de données avec Keras

Fork   Star
python keras 2 fit_generator large volume de données multiprocessing

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.

La structure utilisée dans ce guide est celle donnée par le package Python Keras, qui peut être utilisé par dessus une installation GPU de TensorFlow ou de Theano.

Guide

Situation précédente

Avant de lire ce guide, votre script Keras ressemblait proablement à cela :

import numpy as np
from keras.models import Sequential

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

# Mise en place du modèle
model = Sequential()
[...] # Votre architecture
model.compile()

# Entrainement du modèle sur vos données
model.fit(x=X, y=y)

Ce guide se concentre sur le changement de la ligne de code qui chargeait d'une traite l'ensemble des données. Cette opération peut causer des problèmes de mémoire dans le cas où la RAM n'aurait pas assez de place pour y tenir toutes les données en même temps.

Pour ce faire, nous allons voir une recette utilisant un générateur de données 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 Keras 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_keras.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.

Générateur de données

Maintenant, rentrons dans les détails qui vont permettre d'écrire votre classe Python GenerateurDeDonnees, utilisée pour acheminer vos données vers votre modèle Keras en temps réel.

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

def __init__(self, liste_IDs, labels, taille_lot=32, dim=(32,32,32), n_canaux=1,
             n_classes=10, melange=True):
    'Initialisation'
    self.dim = dim
    self.taille_lot = taille_lot
    self.labels = labels
    self.liste_IDs = liste_IDs
    self.n_canaux = n_canaux
    self.n_classes = n_classes
    self.melange = melange
    self.on_epoch_end()

On y rentre des informations clés sur les données telles que leur dimension (par ex. un volume 3D de taille 32 sera associé au paramètre dim=(32,32,32)), nombre de canaux, nombre de classes, taille de mini-lot, ainsi que le choix de mélanger l'ordre des données vues à chaque nouveau parcours. On y garde aussi des informations telles que les labels ainsi que la liste des IDs identifiant les données que l'on souhaite générer.

Ici, la méthode on_epoch_end se déclenche une fois au tout debut de l'entrainement, ainsi qu'à la fin de chaque epoch. Autre point notable : dans le cas où le paramètre melange a la valeur True, un nouvel ordre d'exploration sera généré au début de chaque parcours.

def on_epoch_end(self):
  'Mise à jour de l'ordre des indices après chaque epoch'
  self.indices = np.arange(len(self.liste_IDs))
  if self.melange == True:
      np.random.shuffle(self.indices)

Mélanger 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.

Une autre méthode au coeur de la génération des données est celle effectuant la tâche la plus cruciale ici : produire des lots de données. La méthode privée en charge de cette tâche se nomme __generation_de_donnees et prend comme argument la liste des IDs identifiant les exemples du lot en question.

def __generation_de_donnees(self, liste_IDs_temp):
  'Génère des données avec `taille_lot` exemples' # X : (n_exemples, *dim, n_canaux)
  # Initialisation
  X = np.empty((self.taille_lot, *self.dim, self.n_canaux))
  y = np.empty((self.taille_lot), dtype=int)

  # Génération de données
  for i, ID in enumerate(liste_IDs_temp):
      # Stockage de l'exemple
      X[i,] = np.load('donnees/' + ID + '.npy')

      # Stockage de la classe
      y[i] = self.labels[ID]

  return X, keras.utils.to_categorical(y, num_classes=self.n_classes)

Au cours de la génération de données, ce code lit la matrice NumPy de chaque exemple depuis son fichier correspondant ID.npy. 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.

Nous avons par ailleurs fait usage de la fonction keras.utils.to_categorical de Keras pour convertir nos labels numériques y vers leur notation binaire associée (par ex. pour un problème à 6 classes, le troisième label correspond à [0 0 1 0 0 0]), qui est la notation adaptée au problème de classification.

Il est maintenant temps de rassembler ces pièces ensemble. Chaque appel demande un indice de lot compris entre 0 et le nombre total de lots, où ce dernier est indiqué dans la méthode __len__.

def __len__(self):
  'Désigne le nombre de lots par epoch'
  return int(np.floor(len(self.liste_IDs) / self.taille_lot))

Une valeur couramment utilisée est $$\biggl\lfloor\frac{\#\textrm{ exemples}}{\textrm{taille d'un lot}}\biggr\rfloor$$ de manière à ce que le modèle puisse voir les exemples d'entraînement (au plus) une fois par epoch.

Maintenant, à chaque requête d'un indice de lot 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 lot de données'
  # Génération des indices correspondant au lot
  indices = self.indices[indice*self.taille_lot:(indice+1)*self.taille_lot]

  # Établir la liste des IDs
  liste_IDs_temp = [self.liste_IDs[k] for k in indices]

  # Génération des données correspondantes
  X, y = self.__generation_de_donnees(liste_IDs_temp)

  return X, y

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

import numpy as np
import keras

class GenerateurDeDonnees(keras.utils.Sequence):
    'Génère des données pour Keras'
    def __init__(self, liste_IDs, labels, taille_lot=32, dim=(32,32,32), n_canaux=1,
                 n_classes=10, melange=True):
        'Initialisation'
        self.dim = dim
        self.taille_lot = taille_lot
        self.labels = labels
        self.liste_IDs = liste_IDs
        self.n_canaux = n_canaux
        self.n_classes = n_classes
        self.melange = melange
        self.on_epoch_end()

    def __len__(self):
        'Désigne le nombre de lots par epoch'
        return int(np.floor(len(self.liste_IDs) / self.taille_lot))

    def __getitem__(self, indice):
        'Génère un lot de données'
        # Génération des indices correspondant au lot
        indices = self.indices[indice*self.taille_lot:(indice+1)*self.taille_lot]

        # Établir la liste des IDs
        liste_IDs_temp = [self.liste_IDs[k] for k in indices]

        # Génération des données correspondantes
        X, y = self.__generation_de_donnees(liste_IDs_temp)

        return X, y

    def on_epoch_end(self):
        'Mise à jour de l'ordre des indices après chaque epoch'
        self.indices = np.arange(len(self.liste_IDs))
        if self.melange == True:
            np.random.shuffle(self.indices)

    def __generation_de_donnees(self, liste_IDs_temp):
        'Génére des données avec `taille_lot` exemples' # X : (n_exemples, *dim, n_canaux)
        # Initialisation
        X = np.empty((self.taille_lot, *self.dim, self.n_canaux))
        y = np.empty((self.taille_lot), dtype=int)

        # Génération de données
        for i, ID in enumerate(liste_IDs_temp):
            # Stockage de l'exemple
            X[i,] = np.load('donnees/' + ID + '.npy')

            # Stockage de la classe
            y[i] = self.labels[ID]

        return X, keras.utils.to_categorical(y, num_classes=self.n_classes)

Script Keras

À présent, nous devons modifier le script Keras de manière adéquate pour prendre en compte le générateur de données que nous venons de créer.

import numpy as np

from keras.models import Sequential
from my_classes import GenerateurDeDonnees

# Paramètres
params = {'dim': (32,32,32),
          'taille_lot': 64,
          'n_classes': 6,
          'n_canaux': 1,
          'melange': True}

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

# Générateurs
generateur_entrainement = GenerateurDeDonnees(partition['entrainement'], labels, **params)
generateur_validation = GenerateurDeDonnees(partition['validation'], labels, **params)

# Mise en place du modèle
model = Sequential()
[...] # Architecture
model.compile()

# Entrainement du modèle sur vos données
model.fit_generator(generator=generateur_entrainement,
                    validation_data=generateur_validation,
                    use_multiprocessing=True,
                    workers=6)

Comme vous pouvez le voir, nous appelons depuis l'objet model la méthode fit_generator (au lieu de fit), où l'on a juste eu à spécifier les générateurs de données correspondants dans la liste des arguments. Keras s'occupe du reste !

Notons que cette implémentation nous permet aussi d'utiliser l'argument multiprocessing de fit_generator, où le nombre de threads spécifié dans workers correspond à ceux qui génèrent les lots 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).

Conclusion

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

python3 script_keras.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.

Vous pouvez trouver un exemple complet de cette stratégie sur un problème donné sur GitHub où les codes de génération de données ainsi que du script Keras peuvent être consultés.

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