dataset loader custom keras tensorflow batch Cours pratiques - Deep learning

Réaliser son propre générateur de données

Pourquoi faire son custom loader de données ?

Nous avons vu dans l’article Charger & entrainer le réseau sur des images : fit() vs fit_generator(), qu’il est préférable d’utiliser un générateur pour charger l’ensemble de ses données à son réseau de neurones via des mini-lots de données, par rapport à envoyer le dataset dans sa globalité via un fichier (numpy par exemple).

Vous pouvez gagner du temps en utilisant les générateurs intégré à Keras :

  • flow : charge des données via une variable
  • flow_from_dataframe : charge des données via un dataframe Pandas
  • flow_from_directory : charge des données via un dossier spécifique sur l’ordinateur

 

Vous répondre à une problématique simple, ils feront largement l’affaire. Mais ils vous imposent certaines choses :

  • Train/Validation dataset : ça peut devenir un peu tricky pour définir ces deux jeux de données via Keras. Par exemple construire ces deux jeux avec des données dans un dossier unique. Vous pouvez à la rigueur vous en sortir comme cela :
  • Réaliser des opérations spécifiques, comme de la data augmentation à la volée, avant qu’elles ne soient envoyé au réseau. Keras en propose quelque une via sa classe ImageDataGenerator, mais reste limiter.

 

Cette solution nous permet donc de nous adapter face à de large dataset plus facilement, et évite tout saturation de RAM ou celle du GPU. On profitera en plus du calcul parallèles via les threads de votre CPU.

Etat actuel des choses

Vous devriez avoir pour le moment quelques choses comme ceci comme cheminement pour charger vos données. Je minimalise l’exemple pour la compréhension :

  1. On charge le dataset dans sa globalité via Numpy
  2. On créer son modèle et le compile selon son optimiser et diverses métriques souhaités
  3. On entraine notre modèle sur nos données précédemment chargé

 

Réalisation du générateur

Squelette de base

Voici le squelette de base que je préconise. Votre classe doit hériter de la classe Keras.utils.Sequence :

Cette classe parente vous assure dans le cas ou vous souhaitez utiliser du calcul parallèle avec vos threads, de garantir de parcourir une seule et unique fois vos données au cours d’une époch, contrairement à pures custom générateurs maison que j’ai déjà vu sur des forums qui ne se synchronisaient pas entre eux. On verra plus tard dans le tutoriel comment utiliser ce multiprocess.

 

Implémentation du constructeur

Obligatoire à écrire, et défini dans l’objet de base de Keras.utils.Sequence. Permet d’instancier un nouvel objet.

Vous aurez besoin au minima besoin de vos données X et Y. Je vous montre ici un exemple :

  • data : nos données dans un dataframe Pandas
  • xLabel : nom de colonne du df contenant nos données X
  • yLabel : nom de colonne du df contenant nos données Y
  • batchSize : taille d’un mini lot de données
  • shuffle : booléen si on souhaite envoyer des données de façon aléatoire, ou dans l’ordre de l’index du dataframe
  • targetSize : afin de resize nos images

 

Implémentation __len__

Obligatoire à écrire, et défini dans l’objet de base de Keras.utils.Sequence. Définie le nombre de batch durant une époch.

 

Implémentation on_epoch_end

Défini dans l’objet de base de Keras.utils.Sequence. Appelé à chaque fin d’epoch. On va s’en servir ici afin de rendre aléatoire l’ordre de la liste des ID des items à envoyer pour constituer le batch de données courant.

 

Implémentation __getitem__

Obligatoire à écrire, et défini dans l’objet de base de Keras.utils.Sequence. Génère un batch de données.

Concrètement je récupère une liste d’ID qui correspond à des items spécifiques (X et Y) contenu dans le dataframe de données, de taille batchSize.  Ici pour l’exemple je ne fais que charger des images dans un tableau Numpy

A vous d’effectuer vos transformation souhaités sur vos données, selon votre problématique. A savoir normaliser les données (diviser par 255 les valeurs des images par exemple pour passer de 0<x<255 à 0<x<1), réaliser de la data augmentation, etc.

 

Appel à notre générateur

On a plus qu’a créer un générateur pour le jeu d’entrainement, et un second pour le jeu de validation à partir de notre classe custom. On lui fourni un dataframe avec un nombre d’élément spécifique à chaque type de jeu de données spécifié avec un ratio de 0,8/0,2.

On fourni ainsi nos deux générateurs au modèle via la méthode fit_generator(). On peut spécifier si on souhaite utiliser nos threads afin de paralléliser le chargement des données du disque dur vers le GPU.

 

Data augmentation à la volée

Vous pouvez très bien réaliser une data augmentation sur votre fichier de données en même temps que vos masques, en local, au préalable. L’utilisation du générateur prendra en compte l’ensemble des fichiers de vos deux dossiers.

Vous pouvez néanmoins réaliser de la data augmentation au sein même du générateur, à la volée. Cela permet de stocker votre dataset de base, sans avoir des dizaines et dizaines de giga-octets supplémentaire et ainsi d’économiser du stockage sur votre disque dur. Cependant vous aurez alors un points négatif dans cette affaire. Vous perdrez légèrement en temps de chargement des données vers le GPU. En effet, en plus de lire et charger les images en local, on ajoute une petit étape ici, de créer de nouveaux échantillons à la volée avant de tout envoyer au GPU. A vous de voir quel moyen au final est le plus adapté selon votre type de dataset, votre hardware et votre approche pour répondre à votre problématique.

Ici je vous montre les quelques changements à réaliser pour adapter notre précédent générateur, afin d’y ajouter de la data augmentation à la volée.

 

Modification du constructeur

On va initialiser ici un nouvel attribut, batchSizeAugmented :

  • batchSizeAugmented : nombre d’image total du batch
  • batchSize : nombre d’image lu en local de notre dataset d’origine

Ici comme exemple, je prends batchSize =2 et batchSizeAugmented =32. Dans mon raisonnement, je souhaite que pour une image lu en local, je crée 15 nouvelles images augmentées avec divers effets. Donc si je lis 2 images, j’aurais 30 images augmentés. Ce qui fait 2 + 30 = 32 images au total.

 

Modification de __len__

Rappelez vous cette méthode défini le nombre de batch par époch. On remplace alors batchSize par batchSizeAugmented :

 

Modification de __getitem__

On souhaite générer des images augmentés (30) à partir d’image local (2), aucun changement donc pour l’objet currentBatchIdsRow. 

On change cependant la première dimension de xTrain et yTrain de batchSize à batchSizeAugmented, qui correspond au nombre total d’image par batch ( donc les images locales en plus des augmentés).

On ajoute une nouvelle boucle dans la première , et c’est ici que vous allez opérez vos transformations pour générer de nouvelles images.

fit vs fit_generator keras tensorflow numpy generator Cours pratiques - Deep learning

Charger & entrainer le réseau sur des images :…

Petit comparatif sur deux méthodes permettant de chargeur un dataset pour entrainer un réseau de neurones via Tensorflow et Keras.

 

Fit()

Cela permet d’envoyer en un seul coup l’ensemble du dataset au réseau de neurones. En conséquence, on doit être sur que le dataset puisse rentrer en RAM.

C’est donc plutôt adapté pour les petits dataset.

 

Générer les fichiers numpy en local

On commence par définir nos tableau qui vont contenir nos images. Pour cela je vous donne deux méthodes différentes :

 

Maintenant on va parcourir un dossier supposé contenant nos images que l’on souhaite ajouter à nos deux précédents tableau. Je vous montre selon les deux types d’initialisation faîte précédemment. Pour chaque image que l’on aura, on va devoir les ouvrir et les transformer en tenseur de taille 3 (largeur x hauteur x canal, ou canal=3 si RGB ou canal=1 si Noir/Blanc). Une couleur peut avoir un gradient allant de 0 à 255, selon son intensité. Pour chaque pixel, on aura alors des triplets (0<Intensité rouge<255, 0<intensité vert<255, 0<intensité bleu<255). Pour des raisons d’optimisation, les réseaux apprennent plus facilement sur des valeurs normalisés. Pour cela, je vais divisé par 255 les valeurs, pour avoir des valeurs comprises entre 0 et 1.

 

Maintenant on les enregistre en local :

 

Charger les fichiers numpy

On a plus qu’a charger les fichiers précédemment sauvegardé en local, et de les fournir directement au réseau via la méthode fit():

 

Fit_Generator()

On se sert de générateurs afin d’envoyer des mini-lots (batch) de notre dataset au réseau.

C’est donc plutôt adapté pour les grands dataset et convient donc mieux aux problématiques rencontré dans la vie réel. Cela permet en plus d’ajouter de la data augmentation à la volée, donc pratique !

 

Besoin de data augmentation ?

C’est ici que l’on défini notre data augmentation. Vous pouvez laisser vide si vous n’en souhaitez pas. On laisse juste la normalisation des données, comme vu précédemment :

 

Chargement des données

On peut charger les données directement via un dossier spécifique ou via un dataframe de pandas. Gardez un seed identique permettant de synchro les deux générateurs sur les mêmes images en entrée entre sa donnée et son label.

 

Via un dossier spécifique

 

Via un datafame pandas

 

Train & Validation set depuis un même dossier commun

Simple exemple pour de la segmentation d’image. Mais selon votre problématique vous devrez changez dans la méthode flow_from_directory/dataframe le class_mode (type de vos données en Y, catégorie, binaire, etc.) et le classes (liste de vos classes [chiens, chats, etc.])