Construire un réseau antagoniste génératif (GAN) simple en utilisant TensorFlow
Introduction
Les réseaux antagonistes génératifs ou GANs sont l'un des domaines les plus actifs de la recherche et du développement en apprentissage profond en raison de leur incroyable capacité à générer des résultats synthétiques. Dans ce blog, nous allons développer l'intuition de base des GANs à travers un exemple concret. Ce post est structuré de la manière suivante :
- Idée de base et intuition derrière le fonctionnement des réseaux adversariaux génératifs
- Mise en œuvre d'un modèle basé sur un GAN qui génère des données à partir d'une distribution simple
- Visualiser et analyser différents aspects du GAN pour mieux comprendre ce qui se passe en coulisses.
Le code de ce blog peut être trouvé ici.
Réseaux antagonistes génératifs
L'idée de base derrière les GANs est en réalité très simple. Au cœur d'un GAN, il y a deux agents avec des objectifs concurrents qui travaillent à travers des buts opposés. Cette configuration relativement simple entraîne les deux agents à inventer des méthodes de plus en plus complexes pour se tromper mutuellement. Ce type de situation peut être modélisé en théorie des jeux comme un jeu minimax.
Prenons un exemple théorique du processus de contrefaçon de monnaie. Dans ce processus, nous pouvons imaginer deux types d'agents : un criminel et un policier. Voyons leurs objectifs concurrents :
- Objectif du criminel : L'objectif principal du criminel est de trouver des moyens complexes de contrefaire de l'argent de telle sorte que le Policier ne puisse pas distinguer l'argent contrefait de l'argent réel.
- Objectif du policier : L'objectif principal du policier est de trouver des moyens complexes afin de distinguer la fausse monnaie de la vraie monnaie.
Au fur et à mesure que ce processus progresse, le policier développe une technologie de plus en plus sophistiquée pour détecter la contrefaçon de monnaie et le criminel développe une technologie de plus en plus sophistiquée pour contrefaire de l'argent. C'est la base de ce que l'on appelle un Processus Adversarial.
Les réseaux antagonistes génératifs tirent parti des processus adverses pour entraîner deux réseaux neuronaux qui se concurrencent jusqu'à atteindre un équilibre souhaitable. Dans ce cas, nous avons un réseau générateur G(Z) qui prend en entrée du bruit aléatoire et tente de générer des données très proches du jeu de données que nous possédons. L'autre réseau s'appelle le réseau discriminateur D(X) qui prend en entrée des données générées et tente de distinguer les données générées des données réelles. Ce réseau, dans son essence, met en œuvre une classification binaire et fournit la probabilité que les données en entrée proviennent réellement du jeu de données réel (par opposition aux données synthétiques ou fausses).
Au sens formel, la fonction objective de tout ce processus peut être écrite comme :
Le point d'équilibre souhaitable habituel pour les GANs définis ci-dessus est que le Générateur doit modéliser les données réelles et que le Discriminateur doit donner une probabilité de 0,5 puisque les données générées sont les mêmes que les données réelles - c'est-à-dire qu'il n'est pas sûr si les nouvelles données provenant du générateur sont réelles ou fausses avec une probabilité égale.
Vous pourriez vous demander pourquoi un processus d'apprentissage aussi complexe est même nécessaire ? Quels sont les avantages d'apprendre un tel modèle ? Eh bien, l'intuition derrière cela et toutes les approches génératives suivent une célèbre citation de Richard Feynman :
Ce que je ne peux pas créer, je ne comprends pas.
Ceci est pertinent car si nous sommes capables de générer une distribution de données réelles à partir d'un modèle, cela signifie que nous savons tout ce qu'il y a à savoir sur ce modèle. Souvent, ces distributions réelles incluent des millions d'images et nous pouvons les générer en utilisant un modèle qui a des milliers de paramètres, alors ces paramètres capturent l'essence des images données.
Les GAN ont également de nombreuses autres applications pratiques à court terme, que nous discuterons dans une section ultérieure.
Implémentation des GAN
Dans cette section, nous allons générer une distribution de données très simple et essayer d'apprendre une fonction Générateur qui génère des données à partir de cette distribution en utilisant le modèle GAN décrit ci-dessus. Cette section est globalement divisée en 3 parties. Tout d'abord, nous écrirons une fonction de base pour générer une distribution quadratique (la véritable distribution de données). Ensuite, nous écrivons le code pour les réseaux Générateur et Discriminateur. Puis, nous utiliserons les données et les réseaux pour écrire le code permettant d'entraîner ces deux réseaux de manière adversariale.
L'objectif de cette mise en œuvre est d'apprendre une nouvelle fonction capable de générer des données à partir de la même distribution que les données d'entraînement. L'attente de l'entraînement est que notre réseau Générateur commence à produire des données qui suivent la distribution quadratique. Cela est expliqué et démontré plus en détail dans la section suivante. Bien que nous commencions avec une distribution de données très simple, cette approche peut être facilement étendue pour générer des données à partir d'un ensemble de données beaucoup plus complexe. Quelques exemples de GAN ont réussi à générer des images de chiffres manuscrits, de visages de célébrités, d'animaux, etc.
Génération de données d'entraînement
Nous mettons en œuvre notre véritable ensemble de données en générant des échantillons aléatoires à l'aide de la bibliothèque numpy puis en générant la seconde coordonnée à l'aide d'une sorte de fonction. Pour les besoins de cette démonstration, nous avons conservé la fonction sous forme quadratique pour plus de simplicité. Vous pouvez jouer avec ce code pour générer un ensemble de données avec plus de dimensions et/ou une relation plus complexe entre ses caractéristiques, telle qu'un polynôme de degré supérieur, sinus, cosinus, etc.
import numpy as np
def get_y(x):
return 10 + x*x
def sample_data(n=10000, scale=100):
data = []
x = scale*(np.random.random_sample((n,))-0.5)
for i in range(n):
yi = get_y(x[i])
data.append([x[i], yi])
return np.array(data)Les données générées sont très simples et peuvent être tracées comme vu ici :
Implémentation des réseaux générateur et discriminateur
Nous allons maintenant implémenter les réseaux Générateur et Discriminateur en utilisant les couches de tensorflow. Nous implémentons le réseau Générateur en utilisant la fonction suivante :
def generator(Z,hsize=[16, 16],reuse=False):
with tf.variable_scope("GAN/Generator",reuse=reuse):
h1 = tf.layers.dense(Z,hsize[0],activation=tf.nn.leaky_relu)
h2 = tf.layers.dense(h1,hsize[1],activation=tf.nn.leaky_relu)
out = tf.layers.dense(h2,2)
return outCette fonction prend en entrée le placeholder pour les échantillons aléatoires (Z), un tableau hsize pour le nombre d'unités dans les 2 couches cachées et une variable reuse qui est utilisée pour réutiliser les mêmes couches. En utilisant ces entrées, elle crée un réseau de neurones entièrement connecté de 2 couches cachées avec le nombre de nœuds donné. La sortie de cette fonction est un vecteur bidimensionnel correspondant aux dimensions du jeu de données réel que nous essayons d'apprendre. La fonction ci-dessus peut être facilement modifiée pour inclure plus de couches cachées, différents types de couches, différentes activations et différentes correspondances de sortie.
Nous implémentons le réseau Discriminateur en utilisant la fonction suivante :
def discriminator(X,hsize=[16, 16],reuse=False):
with tf.variable_scope("GAN/Discriminator",reuse=reuse):
h1 = tf.layers.dense(X,hsize[0],activation=tf.nn.leaky_relu)
h2 = tf.layers.dense(h1,hsize[1],activation=tf.nn.leaky_relu)
h3 = tf.layers.dense(h2,2)
out = tf.layers.dense(h3,1)
return out, h3Cette fonction prend en entrée placeholder pour les échantillons de l'espace vectoriel du jeu de données réel. Les échantillons peuvent être à la fois des échantillons réels et des échantillons générés par le réseau Generator. De manière similaire au réseau Generator ci-dessus, elle prend également en entrée hsize et reuse. Nous utilisons 3 couches cachées pour le Discriminateur dont nous prenons la taille des deux premières couches en entrée. Nous fixons la taille de la troisième couche cachée à 2 afin de pouvoir visualiser l’espace de caractéristiques transformé dans un plan 2D comme expliqué dans la section suivante. La sortie de cette fonction est une prédiction logit pour le X donné et la sortie de la dernière couche qui est la transformation de caractéristiques apprise par le Discriminateur pour X. La fonction logit est l’inverse de la fonction sigmoïde et est utilisée pour représenter le logarithme des cotes (le rapport entre la probabilité que la variable soit 1 et celle qu’elle soit 0).
Entraînement adversarial
Pour le but de l'entraînement, nous définissons les espaces réservés suivants X et Z pour les échantillons réels et les échantillons de bruit aléatoire respectivement :
X = tf.placeholder(tf.float32,[None,2])
Z = tf.placeholder(tf.float32,[None,2])Nous devons également créer le graphe pour générer des échantillons à partir du réseau Générateur et alimenter les échantillons réels et générés au réseau Discriminateur. Cela se fait en utilisant les fonctions et les espaces réservés définis ci-dessus :
G_sample = generator(Z)
r_logits, r_rep = discriminator(X)
f_logits, g_rep = discriminator(G_sample,reuse=True)
En utilisant les logits pour les données générées et les données réelles, nous définissons les fonctions de perte pour les réseaux Générateur et Discriminateur comme suit :
disc_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=r_logits,labels=tf.ones_like(r_logits)) + tf.nn.sigmoid_cross_entropy_with_logits(logits=f_logits,labels=tf.zeros_like(f_logits)))
gen_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=f_logits,labels=tf.ones_like(f_logits)))Ces pertes sont des pertes basées sur la sigmoid cross entropy en utilisant les équations que nous avons définies ci-dessus. Il s'agit d'une fonction de perte couramment utilisée pour ce que l'on appelle la classification discrète. Elle prend en entrée le logit (qui est donné par notre réseau discriminator) et les étiquettes réelles pour chaque échantillon. Elle calcule ensuite l'erreur pour chaque échantillon. Nous utilisons la version optimisée de celle-ci telle qu'implémentée par TensorFlow, qui est plus stable que le calcul direct de l'entropie croisée. Pour plus de détails, vous pouvez consulter l'API TensorFlow pertinente ici.
Ensuite, nous définissons les optimiseurs pour les deux réseaux en utilisant les fonctions de perte définies ci-dessus et le périmètre des couches définies dans les fonctions generator et discriminator. Nous utilisons l'optimiseur RMSProp pour les deux réseaux avec un taux d'apprentissage de 0.001. En utilisant le périmètre, nous récupérons les poids/variables uniquement pour le réseau donné.
gen_vars = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES,scope="GAN/Generator")
disc_vars = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES,scope="GAN/Discriminator")
gen_step = tf.train.RMSPropOptimizer(learning_rate=0.001).minimize(gen_loss,var_list = gen_vars) # G Train step
disc_step = tf.train.RMSPropOptimizer(learning_rate=0.001).minimize(disc_loss,var_list = disc_vars) # D Train stepNous entraînons ensuite les deux réseaux de manière alternée pour le nombre de pas requis :
for i in range(100001):
X_batch = sample_data(n=batch_size)
Z_batch = sample_Z(batch_size, 2)
_, dloss = sess.run([disc_step, disc_loss], feed_dict={X: X_batch, Z: Z_batch})
_, gloss = sess.run([gen_step, gen_loss], feed_dict={Z: Z_batch})
print "Iterations: %d\t Discriminator loss: %.4f\t Generator loss: %.4f"%(i,dloss,gloss)Le code ci-dessus peut être modifié pour inclure des procédures d'entraînement plus complexes telles que l'exécution de plusieurs étapes de mise à jour du discriminateur et/ou du générateur, la récupération des caractéristiques des échantillons réels et générés et le tracé des échantillons générés. Veuillez vous référer au dépôt de code pour de telles modifications.
Analyse des GAN
Visualiser les pertes d'entraînement
Pour mieux comprendre ce qui se passe dans ce processus, nous pouvons tracer les pertes d'entraînement après chaque 10 itérations. À partir du graphique ci-dessous, nous pouvons voir comment les variations de la perte diminuent progressivement et que la perte devient presque constante vers la fin de l'entraînement. Ce changement négligeable de la perte à la fois du Discriminateur
et du Générateur indique un équilibre.
Visualisation des échantillons pendant l'entraînement
Nous pouvons également tracer les échantillons réels et générés après chaque 1000 itérations d'entraînement. Ces graphiques visualisent comment le réseau Générateur commence avec un mappage initial aléatoire entre l'entrée et l'espace vectoriel du jeu de données, puis progresse progressivement pour ressembler aux échantillons réels du jeu de données. Comme vous pouvez le voir, l'échantillon « faux » commence à ressembler de plus en plus à la distribution de données « réelle ».
Visualisation de la mise à jour du générateur
Dans cette section, nous visualisons l'effet de la mise à jour des poids du réseau Générateur dans le cadre du processus d'entraînement adversarial. Nous faisons cela en traçant les activations de la dernière couche cachée du réseau Discriminateur. Nous avons choisi que la dernière couche cachée ait une taille de 2 afin qu'il soit facile de la représenter graphiquement sans nécessiter de réduction de dimensionnalité (c'est-à-dire la transformation de l'échantillon d'entrée vers un espace vectoriel différent). Nous nous intéressons à la visualisation de la fonction de transformation des caractéristiques apprise par le réseau Discriminateur. Cette fonction est ce que notre réseau apprend afin que les données réelles et générées puissent être séparées.
Nous traçons la transformation des caractéristiques des échantillons réels et générés telle qu'apprise par la dernière couche du réseau Discriminator avant et après la mise à jour des poids du réseau Generator. Nous traçons également les centroïdes des points que nous obtenons après la transformation des caractéristiques des échantillons d'entrée. Enfin, nous calculons les centroïdes des points séparément pour les données réelles et artificielles, avant et après la mise à jour du générateur. À partir des graphiques, nous pouvons déduire les éléments suivants :
- Comme prévu, il n'y a aucun changement dans les caractéristiques transformées des échantillons de données réelles. D'après les graphiques, nous pouvons voir qu'elles coïncident totalement.
- À partir des centroïdes, nous pouvons voir que le centroïde des caractéristiques des échantillons de données générées se déplace presque toujours vers le centroïde des caractéristiques des échantillons de données réelles.
- Nous pouvons également voir qu'à mesure que le nombre d'itérations augmente, les caractéristiques transformées des échantillons réels se mélangent de plus en plus avec les caractéristiques transformées des échantillons générés. Cela est également attendu, car à la fin de l'entraînement, le réseau Discriminateur ne devrait pas être capable de distinguer les échantillons réels des échantillons générés. Par conséquent, à la fin de l'entraînement, les caractéristiques transformées des deux types d'échantillons devraient coïncider.
Conclusion et travaux futurs
Nous avons mis en œuvre un modèle GAN de preuve de concept pour générer des données à partir d'une distribution de données très simple. Comme exercice pour le lecteur curieux, nous recommandons de modifier le code ci-dessus pour faire ce qui suit :
- Visualisez l'avant et l'après de la mise à jour du Discriminateur.
- Changez les fonctions d'activation des couches et observez la différence lors de l'entraînement et dans les échantillons générés.
- Ajoutez plus de couches et différents types de couches et observez l'effet sur le temps d'entraînement et la stabilité de l'entraînement.
- Modifiez le code de génération de données pour inclure des données provenant de 2 courbes différentes
- Modifiez le code ci-dessus pour qu'il fonctionne avec des données plus complexes telles que MNIST, CIFAR-10, etc.
Dans les travaux futurs, nous discuterons des limites des GANs et des modifications nécessaires pour les résoudre.






