Comment développer un réseau contradictoire génératif 1D à partir de zéro dans Keras
Les réseaux adverses génératifs, ou GAN en abrégé, sont une architecture d'apprentissage en profondeur pour former de puissants modèles de générateurs.
Un modèle générateur est capable de générer de nouveaux échantillons artificiels qui pourraient vraisemblablement provenir d’une distribution d’échantillons existante.
Les GAN sont composés à la fois de modèles générateurs et discriminateurs. Le générateur est chargé de générer de nouveaux échantillons à partir du domaine, et le discriminateur est chargé de classer si les échantillons sont réels ou faux (générés). Il est important de noter que les performances du modèle discriminateur sont utilisées pour mettre à jour à la fois les poids du discriminateur lui-même et le modèle générateur. Cela signifie que le générateur ne voit jamais réellement d'exemples du domaine et est adapté en fonction des performances du discriminateur.
Il s’agit d’un type de modèle complexe à comprendre et à entraîner.
Une approche pour mieux comprendre la nature des modèles GAN et comment les former consiste à développer un modèle à partir de zéro pour une tâche très simple.
Une tâche simple qui fournit un bon contexte pour développer un GAN simple à partir de zéro est une fonction unidimensionnelle. En effet, les échantillons réels et générés peuvent être tracés et inspectés visuellement pour avoir une idée de ce qui a été appris. Une fonction simple ne nécessite pas non plus de modèles de réseaux neuronaux sophistiqués, ce qui signifie que les modèles spécifiques de générateur et de discriminateur utilisés sur l'architecture peuvent être facilement compris.
Dans ce didacticiel, nous sélectionnerons une fonction unidimensionnelle simple et l'utiliserons comme base pour développer et évaluer un réseau adverse génératif à partir de zéro à l'aide de la bibliothèque d'apprentissage en profondeur Keras.
Après avoir terminé ce tutoriel, vous saurez :
- L’avantage de développer un réseau contradictoire génératif à partir de zéro pour une fonction simple unidimensionnelle.
- Comment développer des modèles de discriminateur et de générateur distincts, ainsi qu'un modèle composite pour entraîner le générateur via le comportement prédictif du discriminateur.
- Comment évaluer subjectivement les échantillons générés dans le contexte d'exemples réels du domaine du problème.
Démarrez votre projet avec mon nouveau livre Generative Adversarial Networks with Python, comprenant des tutoriels étape par étape et les fichiers code source Python pour tous les exemples.
Commençons.
Présentation du didacticiel
Ce didacticiel est divisé en six parties ; ils sont:
- Sélectionnez une fonction unidimensionnelle
- Définir un modèle discriminateur
- Définir un modèle de générateur
- Formation du modèle de générateur
- Évaluation des performances du GAN
- Exemple complet de formation du GAN
Sélectionnez une fonction unidimensionnelle
La première étape consiste à sélectionner une fonction unidimensionnelle à modéliser.
Quelque chose de la forme :
y = f(x)
Où x sont les valeurs d'entrée et y sont les valeurs de sortie de la fonction.
Plus précisément, nous voulons une fonction que nous pouvons facilement comprendre et tracer. Cela aidera à la fois à définir une attente quant à ce que le modèle devrait générer et à utiliser une inspection visuelle des exemples générés pour avoir une idée de leur qualité.
Nous utiliserons une fonction simple dex^2 ; c'est-à-dire que la fonction renverra le carré de l'entrée. Vous vous souvenez peut-être de cette fonction d'algèbre du lycée sous le nom de fonction en forme de u.
On peut définir la fonction en Python comme suit :
# simple function
def calculate(x):
return x * x
Nous pouvons définir le domaine d'entrée comme des valeurs réelles comprises entre -0,5 et 0,5 et calculer la valeur de sortie pour chaque valeur d'entrée dans cette plage linéaire, puis tracer les résultats pour avoir une idée de la façon dont les entrées sont liées aux sorties.
L’exemple complet est répertorié ci-dessous.
# demonstrate simple x^2 function
from matplotlib import pyplot
# simple function
def calculate(x):
return x * x
# define inputs
inputs = [-0.5, -0.4, -0.3, -0.2, -0.1, 0, 0.1, 0.2, 0.3, 0.4, 0.5]
# calculate outputs
outputs = [calculate(x) for x in inputs]
# plot the result
pyplot.plot(inputs, outputs)
pyplot.show()
L’exécution de l’exemple calcule la valeur de sortie pour chaque valeur d’entrée et crée un tracé des valeurs d’entrée et de sortie.
Nous pouvons voir que les valeurs éloignées de 0,0 entraînent des valeurs de sortie plus grandes, tandis que les valeurs proches de zéro entraînent des valeurs de sortie plus petites, et que ce comportement est symétrique autour de zéro.
Il s'agit du tracé en forme de U bien connu de la fonction unidimensionnelle X^2.
Nous pouvons générer des échantillons ou des points aléatoires à partir de la fonction.
Ceci peut être réalisé en générant des valeurs aléatoires comprises entre -0,5 et 0,5 et en calculant la valeur de sortie associée. Répéter cette opération plusieurs fois donnera un échantillon de points de la fonction, par ex. "de vrais échantillons."
Le tracé de ces échantillons à l'aide d'un nuage de points affichera le même tracé en forme de U, bien que composé d'échantillons aléatoires individuels.
L’exemple complet est répertorié ci-dessous.
Tout d’abord, nous générons des valeurs uniformément aléatoires entre 0 et 1, puis nous les déplaçons entre -0,5 et 0,5. Nous calculons ensuite la valeur de sortie pour chaque valeur d'entrée générée aléatoirement et combinons les tableaux en un seul tableau NumPy avec n lignes (100) et deux colonnes.
# example of generating random samples from X^2
from numpy.random import rand
from numpy import hstack
from matplotlib import pyplot
# generate randoms sample from x^2
def generate_samples(n=100):
# generate random inputs in [-0.5, 0.5]
X1 = rand(n) - 0.5
# generate outputs X^2 (quadratic)
X2 = X1 * X1
# stack arrays
X1 = X1.reshape(n, 1)
X2 = X2.reshape(n, 1)
return hstack((X1, X2))
# generate samples
data = generate_samples()
# plot samples
pyplot.scatter(data[:, 0], data[:, 1])
pyplot.show()
L'exécution de l'exemple génère 100 entrées aléatoires et leur sortie calculée et trace l'échantillon sous forme de nuage de points, montrant la forme en U familière.
Nous pouvons utiliser cette fonction comme point de départ pour générer des échantillons réels pour notre fonction discriminatrice. Plus précisément, un échantillon est composé d'un vecteur avec deux éléments, un pour l'entrée et un pour la sortie de notre fonction unidimensionnelle.
Nous pouvons également imaginer comment un modèle générateur pourrait générer de nouveaux échantillons que nous pouvons tracer et comparer à la forme en U attendue de la fonction X^2. Plus précisément, un générateur produirait un vecteur avec deux éléments : un pour l’entrée et un pour la sortie de notre fonction unidimensionnelle.
Définir un modèle discriminateur
L'étape suivante consiste à définir le modèle discriminateur.
Le modèle doit prendre un échantillon de notre problème, tel qu'un vecteur à deux éléments, et générer une prédiction de classification indiquant si l'échantillon est réel ou faux.
Il s'agit d'un problème de classification binaire.
- Entrées : échantillon avec deux valeurs réelles.
- Résultats : classification binaire, probabilité que l'échantillon soit réel (ou faux).
Le problème est très simple, ce qui signifie que nous n’avons pas besoin d’un réseau neuronal complexe pour le modéliser.
Le modèle discriminateur aura une couche cachée avec 25 nœuds et nous utiliserons la fonction d'activation ReLU et une méthode d'initialisation de poids appropriée appelée initialisation du poids He.
La couche de sortie aura un nœud pour la classification binaire utilisant la fonction d'activation sigmoïde.
Le modèle minimisera la fonction de perte d'entropie croisée binaire et la version Adam de la descente de gradient stochastique sera utilisée car elle est très efficace.
La fonction define_discriminator() ci-dessous définit et renvoie le modèle discriminateur. La fonction paramétre le nombre d'entrées à attendre, qui est par défaut deux.
# define the standalone discriminator model
def define_discriminator(n_inputs=2):
model = Sequential()
model.add(Dense(25, activation='relu', kernel_initializer='he_uniform', input_dim=n_inputs))
model.add(Dense(1, activation='sigmoid'))
# compile model
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
Nous pouvons utiliser cette fonction pour définir le modèle discriminateur et le résumer. L’exemple complet est répertorié ci-dessous.
# define the discriminator model
from keras.models import Sequential
from keras.layers import Dense
from keras.utils.vis_utils import plot_model
# define the standalone discriminator model
def define_discriminator(n_inputs=2):
model = Sequential()
model.add(Dense(25, activation='relu', kernel_initializer='he_uniform', input_dim=n_inputs))
model.add(Dense(1, activation='sigmoid'))
# compile model
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
# define the discriminator model
model = define_discriminator()
# summarize the model
model.summary()
# plot the model
plot_model(model, to_file='discriminator_plot.png', show_shapes=True, show_layer_names=True)
L'exécution de l'exemple définit le modèle discriminateur et le résume.
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 25) 75
_________________________________________________________________
dense_2 (Dense) (None, 1) 26
=================================================================
Total params: 101
Trainable params: 101
Non-trainable params: 0
_________________________________________________________________
Un tracé du modèle est également créé et nous pouvons voir que le modèle attend deux entrées et prédira une seule sortie.
Remarque : la création de ce tracé suppose que les bibliothèques pydot et graphviz soient installées. Si cela pose un problème, vous pouvez commenter l'instruction d'importation pour la fonction plot_model et l'appel à la fonction plot_model().
Nous pourrions commencer à entraîner ce modèle dès maintenant avec des exemples réels avec une étiquette de classe de un et des échantillons générés aléatoirement avec une étiquette de classe de zéro.
Cela n’est pas nécessaire, mais les éléments que nous développerons seront utiles plus tard, et il est utile de voir que le discriminateur n’est qu’un modèle normal de réseau neuronal.
Tout d'abord, nous pouvons mettre à jour notre fonction generate_samples() à partir de la section de prédiction et l'appeler generate_real_samples() et lui faire également renvoyer les étiquettes de classe de sortie pour les échantillons réels, en particulier : un tableau de 1 valeurs, où class=1 signifie réel.
# generate n real samples with class labels
def generate_real_samples(n):
# generate inputs in [-0.5, 0.5]
X1 = rand(n) - 0.5
# generate outputs X^2
X2 = X1 * X1
# stack arrays
X1 = X1.reshape(n, 1)
X2 = X2.reshape(n, 1)
X = hstack((X1, X2))
# generate class labels
y = ones((n, 1))
return X, y
Ensuite, nous pouvons créer une copie de cette fonction pour créer de faux exemples.
Dans ce cas, nous générerons des valeurs aléatoires comprises entre -1 et 1 pour les deux éléments d'un échantillon. L'étiquette de classe de sortie pour tous ces exemples est 0.
Cette fonction servira de notre faux modèle de générateur.
# generate n fake samples with class labels
def generate_fake_samples(n):
# generate inputs in [-1, 1]
X1 = -1 + rand(n) * 2
# generate outputs in [-1, 1]
X2 = -1 + rand(n) * 2
# stack arrays
X1 = X1.reshape(n, 1)
X2 = X2.reshape(n, 1)
X = hstack((X1, X2))
# generate class labels
y = zeros((n, 1))
return X, y
Ensuite, nous avons besoin d'une fonction pour entraîner et évaluer le modèle discriminateur.
Ceci peut être réalisé en énumérant manuellement les époques de formation et, pour chaque époque, en générant un demi-lot d'exemples réels et un demi-lot de faux exemples, et en mettant à jour le modèle pour chacun, par ex. tout un lot d’exemples. La fonction train() pourrait être utilisée, mais dans ce cas, nous utiliserons directement la fonction train_on_batch().
Le modèle peut ensuite être évalué sur les exemples générés et nous pouvons signaler la précision de la classification sur les échantillons réels et faux.
La fonction train_discriminator() ci-dessous implémente cela, en entraînant le modèle pour 1 000 lots et en utilisant 128 échantillons par lot (64 faux et 64 réels).
# train the discriminator model
def train_discriminator(model, n_epochs=1000, n_batch=128):
half_batch = int(n_batch / 2)
# run epochs manually
for i in range(n_epochs):
# generate real examples
X_real, y_real = generate_real_samples(half_batch)
# update model
model.train_on_batch(X_real, y_real)
# generate fake examples
X_fake, y_fake = generate_fake_samples(half_batch)
# update model
model.train_on_batch(X_fake, y_fake)
# evaluate the model
_, acc_real = model.evaluate(X_real, y_real, verbose=0)
_, acc_fake = model.evaluate(X_fake, y_fake, verbose=0)
print(i, acc_real, acc_fake)
Nous pouvons relier tout cela ensemble et entraîner le modèle discriminateur sur des exemples réels et faux.
L’exemple complet est répertorié ci-dessous.
# define and fit a discriminator model
from numpy import zeros
from numpy import ones
from numpy import hstack
from numpy.random import rand
from numpy.random import randn
from keras.models import Sequential
from keras.layers import Dense
# define the standalone discriminator model
def define_discriminator(n_inputs=2):
model = Sequential()
model.add(Dense(25, activation='relu', kernel_initializer='he_uniform', input_dim=n_inputs))
model.add(Dense(1, activation='sigmoid'))
# compile model
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
# generate n real samples with class labels
def generate_real_samples(n):
# generate inputs in [-0.5, 0.5]
X1 = rand(n) - 0.5
# generate outputs X^2
X2 = X1 * X1
# stack arrays
X1 = X1.reshape(n, 1)
X2 = X2.reshape(n, 1)
X = hstack((X1, X2))
# generate class labels
y = ones((n, 1))
return X, y
# generate n fake samples with class labels
def generate_fake_samples(n):
# generate inputs in [-1, 1]
X1 = -1 + rand(n) * 2
# generate outputs in [-1, 1]
X2 = -1 + rand(n) * 2
# stack arrays
X1 = X1.reshape(n, 1)
X2 = X2.reshape(n, 1)
X = hstack((X1, X2))
# generate class labels
y = zeros((n, 1))
return X, y
# train the discriminator model
def train_discriminator(model, n_epochs=1000, n_batch=128):
half_batch = int(n_batch / 2)
# run epochs manually
for i in range(n_epochs):
# generate real examples
X_real, y_real = generate_real_samples(half_batch)
# update model
model.train_on_batch(X_real, y_real)
# generate fake examples
X_fake, y_fake = generate_fake_samples(half_batch)
# update model
model.train_on_batch(X_fake, y_fake)
# evaluate the model
_, acc_real = model.evaluate(X_real, y_real, verbose=0)
_, acc_fake = model.evaluate(X_fake, y_fake, verbose=0)
print(i, acc_real, acc_fake)
# define the discriminator model
model = define_discriminator()
# fit the model
train_discriminator(model)
L'exécution de l'exemple génère des exemples réels et faux et met à jour le modèle, puis évalue le modèle sur les mêmes exemples et imprime la précision de la classification.
Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.
Dans ce cas, le modèle apprend rapidement à identifier correctement les exemples réels avec une précision parfaite et est très efficace pour identifier les faux exemples avec une précision de 80 à 90 %.
...
995 1.0 0.875
996 1.0 0.921875
997 1.0 0.859375
998 1.0 0.9375
999 1.0 0.8125
La formation du modèle discriminateur est simple. L’objectif est de former un modèle générateur, pas un modèle discriminateur, et c’est là que réside véritablement la complexité des GAN.
Définir un modèle de générateur
L'étape suivante consiste à définir le modèle du générateur.
Le modèle générateur prend en entrée un point de l'espace latent et génère un nouvel échantillon, par ex. un vecteur avec à la fois les éléments d'entrée et de sortie de notre fonction, par ex. x et x^2.
Une variable latente est une variable cachée ou non observée, et un espace latent est un espace vectoriel multidimensionnel de ces variables. Nous pouvons définir la taille de l'espace latent pour notre problème et la forme ou la distribution des variables dans l'espace latent.
En effet, l'espace latent n'a aucune signification jusqu'à ce que le modèle générateur commence à attribuer une signification aux points de l'espace au fur et à mesure de son apprentissage. Après l'entraînement, les points dans l'espace latent correspondront aux points dans l'espace de sortie, par ex. dans l’espace des échantillons générés.
Nous définirons un petit espace latent de cinq dimensions et utiliserons l'approche standard de la littérature GAN consistant à utiliser une distribution gaussienne pour chaque variable de l'espace latent. Nous générerons de nouvelles entrées en tirant des nombres aléatoires à partir d'une distribution gaussienne standard, c'est-à-dire une moyenne de zéro et un écart type de un.
- Entrées : point dans l'espace latent, par ex. un vecteur à cinq éléments de nombres aléatoires gaussiens.
- Sorties : vecteur à deux éléments représentant un échantillon généré pour notre fonction (x et x^2).
Le modèle générateur sera petit comme le modèle discriminateur.
Il aura une seule couche cachée avec cinq nœuds et utilisera la fonction d'activation ReLU et l'initialisation du poids He. La couche de sortie aura deux nœuds pour les deux éléments dans un vecteur généré et utilisera une fonction d'activation linéaire.
Une fonction d'activation linéaire est utilisée car nous savons que nous voulons que le générateur génère un vecteur de valeurs réelles et l'échelle sera de [-0,5, 0,5] pour le premier élément et d'environ [0,0, 0,25] pour le deuxième élément.
Le modèle n'est pas compilé. La raison en est que le modèle du générateur n’est pas directement adapté.
La fonction define_generator() ci-dessous définit et renvoie le modèle générateur.
La taille de la dimension latente est paramétrée au cas où nous voudrions jouer avec elle plus tard, et la forme de sortie du modèle est également paramétrée, correspondant à la fonction de définition du modèle discriminateur.
# define the standalone generator model
def define_generator(latent_dim, n_outputs=2):
model = Sequential()
model.add(Dense(15, activation='relu', kernel_initializer='he_uniform', input_dim=latent_dim))
model.add(Dense(n_outputs, activation='linear'))
return model
Nous pouvons résumer le modèle pour aider à mieux comprendre les formes d’entrée et de sortie.
L’exemple complet est répertorié ci-dessous.
# define the generator model
from keras.models import Sequential
from keras.layers import Dense
from keras.utils.vis_utils import plot_model
# define the standalone generator model
def define_generator(latent_dim, n_outputs=2):
model = Sequential()
model.add(Dense(15, activation='relu', kernel_initializer='he_uniform', input_dim=latent_dim))
model.add(Dense(n_outputs, activation='linear'))
return model
# define the discriminator model
model = define_generator(5)
# summarize the model
model.summary()
# plot the model
plot_model(model, to_file='generator_plot.png', show_shapes=True, show_layer_names=True)
L'exécution de l'exemple définit le modèle de générateur et le résume.
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 15) 90
_________________________________________________________________
dense_2 (Dense) (None, 2) 32
=================================================================
Total params: 122
Trainable params: 122
Non-trainable params: 0
_________________________________________________________________
Un tracé du modèle est également créé et nous pouvons voir que le modèle attend un point à cinq éléments de l'espace latent en entrée et prédira un vecteur à deux éléments en sortie.
Remarque : la création de ce tracé suppose que les bibliothèques pydot et graphviz soient installées. Si cela pose un problème, vous pouvez commenter l'instruction d'importation pour la fonction plot_model et l'appel à la fonction plot_model().
Nous pouvons voir que le modèle prend en entrée un vecteur aléatoire à cinq éléments de l'espace latent et génère un vecteur à deux éléments pour notre fonction unidimensionnelle.
Ce modèle ne peut pas faire grand-chose pour le moment. Néanmoins, nous pouvons démontrer comment l’utiliser pour générer des échantillons. Ce n’est pas nécessaire, mais encore une fois, certains de ces éléments pourront s’avérer utiles plus tard.
La première étape consiste à générer de nouveaux points dans l’espace latent. Nous pouvons y parvenir en appelant la fonction randn() NumPy pour générer des tableaux de nombres aléatoires tirés d'une gaussienne standard.
Le tableau de nombres aléatoires peut ensuite être remodelé en échantillons : c'est-à-dire n lignes avec cinq éléments par ligne. La fonction generate_latent_points() ci-dessous implémente cela et génère le nombre souhaité de points dans l'espace latent qui peuvent être utilisés comme entrée dans le modèle générateur.
# generate points in latent space as input for the generator
def generate_latent_points(latent_dim, n):
# generate points in the latent space
x_input = randn(latent_dim * n)
# reshape into a batch of inputs for the network
x_input = x_input.reshape(n, latent_dim)
return x_input
Ensuite, nous pouvons utiliser les points générés comme entrée du modèle générateur pour générer de nouveaux échantillons, puis tracer les échantillons.
La fonction generate_fake_samples() ci-dessous implémente cela, où le générateur défini et la taille de l'espace latent sont transmis comme arguments, ainsi que le nombre de points à générer par le modèle.
# use the generator to generate n fake examples and plot the results
def generate_fake_samples(generator, latent_dim, n):
# generate points in latent space
x_input = generate_latent_points(latent_dim, n)
# predict outputs
X = generator.predict(x_input)
# plot the results
pyplot.scatter(X[:, 0], X[:, 1])
pyplot.show()
En reliant cela ensemble, l’exemple complet est répertorié ci-dessous.
# define and use the generator model
from numpy.random import randn
from keras.models import Sequential
from keras.layers import Dense
from matplotlib import pyplot
# define the standalone generator model
def define_generator(latent_dim, n_outputs=2):
model = Sequential()
model.add(Dense(15, activation='relu', kernel_initializer='he_uniform', input_dim=latent_dim))
model.add(Dense(n_outputs, activation='linear'))
return model
# generate points in latent space as input for the generator
def generate_latent_points(latent_dim, n):
# generate points in the latent space
x_input = randn(latent_dim * n)
# reshape into a batch of inputs for the network
x_input = x_input.reshape(n, latent_dim)
return x_input
# use the generator to generate n fake examples and plot the results
def generate_fake_samples(generator, latent_dim, n):
# generate points in latent space
x_input = generate_latent_points(latent_dim, n)
# predict outputs
X = generator.predict(x_input)
# plot the results
pyplot.scatter(X[:, 0], X[:, 1])
pyplot.show()
# size of the latent space
latent_dim = 5
# define the discriminator model
model = define_generator(latent_dim)
# generate and plot generated samples
generate_fake_samples(model, latent_dim, 100)
L'exécution de l'exemple génère 100 points aléatoires à partir de l'espace latent, les utilise comme entrée dans le générateur et génère 100 faux échantillons à partir de notre domaine de fonctions unidimensionnel.
Comme le générateur n'a pas été entraîné, les points générés sont complètement nuls, comme nous nous y attendons, mais nous pouvons imaginer qu'au fur et à mesure que le modèle est entraîné, ces points commenceront lentement à ressembler à la fonction cible et à sa forme en U.
Nous avons maintenant vu comment définir et utiliser le modèle générateur. Nous devrons utiliser le modèle générateur de cette manière pour créer des échantillons que le discriminateur pourra classer.
Nous n'avons pas vu comment le modèle générateur est formé ; c'est la prochaine.
Formation du modèle de générateur
Les poids du modèle générateur sont mis à jour en fonction des performances du modèle discriminateur.
Lorsque le discriminateur détecte efficacement les faux échantillons, le générateur est davantage mis à jour, et lorsque le modèle du discriminateur est relativement médiocre ou confus lors de la détection de faux échantillons, le modèle du générateur est moins mis à jour.
Cela définit la relation à somme nulle ou contradictoire entre ces deux modèles.
Il peut exister de nombreuses façons d'implémenter cela à l'aide de l'API Keras, mais l'approche la plus simple consiste peut-être à créer un nouveau modèle qui englobe ou encapsule les modèles générateur et discriminateur.
Plus précisément, un nouveau modèle GAN peut être défini qui empile le générateur et le discriminateur de telle sorte que le générateur reçoive en entrée des points aléatoires dans l'espace latent, génère des échantillons qui sont directement introduits dans le modèle discriminateur, classés, et la sortie de ce modèle plus grand peut être définie. être utilisé pour mettre à jour les poids du modèle du générateur.
Pour être clair, nous ne parlons pas d'un nouveau troisième modèle, juste d'un troisième modèle logique qui utilise les couches et les pondérations déjà définies des modèles générateurs et discriminateurs autonomes.
Seul le discriminateur se préoccupe de distinguer les exemples réels des faux ; par conséquent, le modèle discriminateur peut être formé de manière autonome sur des exemples de chacun.
Le modèle générateur ne concerne que les performances du discriminateur sur de faux exemples. Par conséquent, nous marquerons toutes les couches du discriminateur comme non entraînables lorsqu'il fait partie du modèle GAN afin qu'elles ne puissent pas être mises à jour et surentraînées sur de faux exemples.
Lors de la formation du générateur via ce modèle GAN intégré, il y a un autre changement important. Nous voulons que le discriminateur pense que les échantillons produits par le générateur sont réels et non faux. Par conséquent, lorsque le générateur sera formé dans le cadre du modèle GAN, nous marquerons les échantillons générés comme réels (classe 1).
On peut imaginer que le discriminateur classera alors les échantillons générés comme non réels (classe 0) ou avec une faible probabilité d'être réels (0,3 ou 0,5). Le processus de rétropropagation utilisé pour mettre à jour les poids du modèle verra cela comme une erreur importante et mettra à jour les poids du modèle (c'est-à-dire uniquement les poids dans le générateur) pour corriger cette erreur, ce qui permettra au générateur de mieux générer de faux échantillons plausibles.
Rendons cela concret.
- Entrées : point dans l'espace latent, par ex. un vecteur à cinq éléments de nombres aléatoires gaussiens.
- Résultats : classification binaire, probabilité que l'échantillon soit réel (ou faux).
La fonction define_gan() ci-dessous prend comme arguments les modèles générateur et discriminateur déjà définis et crée le nouveau troisième modèle logique englobant ces deux modèles. Les poids dans le discriminateur sont marqués comme ne pouvant pas être entraînés, ce qui affecte uniquement les poids vus par le modèle GAN et non par le modèle discriminateur autonome.
Le modèle GAN utilise ensuite la même fonction binaire de perte d'entropie croisée que le discriminateur et la version efficace d'Adam de la descente de gradient stochastique.
# define the combined generator and discriminator model, for updating the generator
def define_gan(generator, discriminator):
# make weights in the discriminator not trainable
discriminator.trainable = False
# connect them
model = Sequential()
# add generator
model.add(generator)
# add the discriminator
model.add(discriminator)
# compile model
model.compile(loss='binary_crossentropy', optimizer='adam')
return model
Rendre le discriminateur impossible à entraîner est une astuce astucieuse de l'API Keras.
La propriété trainable impacte le modèle lors de sa compilation. Le modèle discriminateur a été compilé avec des couches pouvant être entraînées, par conséquent les poids du modèle dans ces couches seront mis à jour lorsque le modèle autonome sera mis à jour via des appels à train_on_batch().
Le modèle discriminateur a été marqué comme non entraînable, ajouté au modèle GAN et compilé. Dans ce modèle, les poids du modèle discriminateur ne peuvent pas être entraînés et ne peuvent pas être modifiés lorsque le modèle GAN est mis à jour via des appels à train_on_batch().
Ce comportement est décrit dans la documentation de l'API Keras ici :
- Comment puis-je « geler » les calques Keras ?
L'exemple complet de création du discriminateur, du générateur et du modèle composite est répertorié ci-dessous.
# demonstrate creating the three models in the gan
from keras.models import Sequential
from keras.layers import Dense
from keras.utils.vis_utils import plot_model
# define the standalone discriminator model
def define_discriminator(n_inputs=2):
model = Sequential()
model.add(Dense(25, activation='relu', kernel_initializer='he_uniform', input_dim=n_inputs))
model.add(Dense(1, activation='sigmoid'))
# compile model
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
# define the standalone generator model
def define_generator(latent_dim, n_outputs=2):
model = Sequential()
model.add(Dense(15, activation='relu', kernel_initializer='he_uniform', input_dim=latent_dim))
model.add(Dense(n_outputs, activation='linear'))
return model
# define the combined generator and discriminator model, for updating the generator
def define_gan(generator, discriminator):
# make weights in the discriminator not trainable
discriminator.trainable = False
# connect them
model = Sequential()
# add generator
model.add(generator)
# add the discriminator
model.add(discriminator)
# compile model
model.compile(loss='binary_crossentropy', optimizer='adam')
return model
# size of the latent space
latent_dim = 5
# create the discriminator
discriminator = define_discriminator()
# create the generator
generator = define_generator(latent_dim)
# create the gan
gan_model = define_gan(generator, discriminator)
# summarize gan model
gan_model.summary()
# plot gan model
plot_model(gan_model, to_file='gan_plot.png', show_shapes=True, show_layer_names=True)
L’exécution de l’exemple crée d’abord un résumé du modèle composite.
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
sequential_2 (Sequential) (None, 2) 122
_________________________________________________________________
sequential_1 (Sequential) (None, 1) 101
=================================================================
Total params: 223
Trainable params: 122
Non-trainable params: 101
_________________________________________________________________
Un tracé du modèle est également créé et nous pouvons voir que le modèle attend un point à cinq éléments dans l'espace latent en entrée et prédira une seule étiquette de classification en sortie.
Remarque, la création de ce tracé suppose que les bibliothèques pydot et graphviz soient installées. Si cela pose un problème, vous pouvez commenter l'instruction d'importation pour la fonction plot_model et l'appel à la fonction plot_model().
La formation du modèle composite implique de générer un lot de points dans l'espace latent via la fonction generate_latent_points() de la section précédente, et les étiquettes class=1 et d'appeler train_on_batch(). fonction.
La fonction train_gan() ci-dessous le démontre, même si elle est plutôt inintéressante car seul le générateur sera mis à jour à chaque époque, laissant le discriminateur avec les poids de modèle par défaut.
# train the composite model
def train_gan(gan_model, latent_dim, n_epochs=10000, n_batch=128):
# manually enumerate epochs
for i in range(n_epochs):
# prepare points in latent space as input for the generator
x_gan = generate_latent_points(latent_dim, n_batch)
# create inverted labels for the fake samples
y_gan = ones((n_batch, 1))
# update the generator via the discriminator's error
gan_model.train_on_batch(x_gan, y_gan)
Au lieu de cela, il faut d'abord mettre à jour le modèle discriminateur avec des échantillons réels et faux, puis mettre à jour le générateur via le modèle composite.
Cela nécessite de combiner des éléments de la fonction train_discriminator() définie dans la section discriminateur et de la fonction train_gan() définie ci-dessus. Cela nécessite également que la fonction generate_fake_samples() utilise le modèle générateur pour générer de faux échantillons au lieu de générer des nombres aléatoires.
La fonction de train complète pour la mise à jour du modèle discriminateur et du générateur (via le modèle composite) est listée ci-dessous.
# train the generator and discriminator
def train(g_model, d_model, gan_model, latent_dim, n_epochs=10000, n_batch=128):
# determine half the size of one batch, for updating the discriminator
half_batch = int(n_batch / 2)
# manually enumerate epochs
for i in range(n_epochs):
# prepare real samples
x_real, y_real = generate_real_samples(half_batch)
# prepare fake examples
x_fake, y_fake = generate_fake_samples(g_model, latent_dim, half_batch)
# update discriminator
d_model.train_on_batch(x_real, y_real)
d_model.train_on_batch(x_fake, y_fake)
# prepare points in latent space as input for the generator
x_gan = generate_latent_points(latent_dim, n_batch)
# create inverted labels for the fake samples
y_gan = ones((n_batch, 1))
# update the generator via the discriminator's error
gan_model.train_on_batch(x_gan, y_gan)
Nous avons presque tout ce dont nous avons besoin pour développer un GAN pour notre fonction unidimensionnelle.
Un aspect restant est l’évaluation du modèle.
Évaluation des performances du GAN
Généralement, il n'existe aucun moyen objectif d'évaluer les performances d'un modèle GAN.
Dans ce cas spécifique, nous pouvons concevoir une mesure objective pour les échantillons générés car nous connaissons le véritable domaine d'entrée sous-jacent et la fonction cible et pouvons calculer une mesure d'erreur objective.
Néanmoins, nous ne calculerons pas ce score d’erreur objectif dans ce tutoriel. Au lieu de cela, nous utiliserons l'approche subjective utilisée dans la plupart des applications GAN. Plus précisément, nous utiliserons le générateur pour générer de nouveaux échantillons et les inspecter par rapport à des échantillons réels du domaine.
Tout d'abord, nous pouvons utiliser la fonction generate_real_samples() développée dans la partie discriminateur ci-dessus pour générer des exemples réels. La création d'un nuage de points de ces exemples créera la forme en U familière de notre fonction cible.
# generate n real samples with class labels
def generate_real_samples(n):
# generate inputs in [-0.5, 0.5]
X1 = rand(n) - 0.5
# generate outputs X^2
X2 = X1 * X1
# stack arrays
X1 = X1.reshape(n, 1)
X2 = X2.reshape(n, 1)
X = hstack((X1, X2))
# generate class labels
y = ones((n, 1))
return X, y
Ensuite, nous pouvons utiliser le modèle générateur pour générer le même nombre de faux échantillons.
Cela nécessite d'abord de générer le même nombre de points dans l'espace latent via la fonction generate_latent_points() développée dans la section générateur ci-dessus. Ceux-ci peuvent ensuite être transmis au modèle générateur et utilisés pour générer des échantillons qui peuvent également être tracés sur le même nuage de points.
# generate points in latent space as input for the generator
def generate_latent_points(latent_dim, n):
# generate points in the latent space
x_input = randn(latent_dim * n)
# reshape into a batch of inputs for the network
x_input = x_input.reshape(n, latent_dim)
return x_input
La fonction generate_fake_samples() ci-dessous génère ces faux échantillons et l'étiquette de classe associée de 0 qui sera utile plus tard.
# use the generator to generate n fake examples, with class labels
def generate_fake_samples(generator, latent_dim, n):
# generate points in latent space
x_input = generate_latent_points(latent_dim, n)
# predict outputs
X = generator.predict(x_input)
# create class labels
y = zeros((n, 1))
return X, y
Le fait de tracer les deux échantillons sur le même graphique permet de les comparer directement pour voir si les mêmes domaines d'entrée et de sortie sont couverts et si la forme attendue de la fonction cible a été capturée de manière appropriée, au moins subjectivement.
La fonction summarize_performance() ci-dessous peut être appelée à tout moment pendant la formation pour créer un nuage de points de points réels et générés afin d'avoir une idée de la capacité actuelle du modèle générateur.
# plot real and fake points
def summarize_performance(generator, latent_dim, n=100):
# prepare real samples
x_real, y_real = generate_real_samples(n)
# prepare fake examples
x_fake, y_fake = generate_fake_samples(generator, latent_dim, n)
# scatter plot real and fake data points
pyplot.scatter(x_real[:, 0], x_real[:, 1], color='red')
pyplot.scatter(x_fake[:, 0], x_fake[:, 1], color='blue')
pyplot.show()
Nous pouvons également nous intéresser en même temps aux performances du modèle discriminateur.
Plus précisément, nous souhaitons savoir dans quelle mesure le modèle discriminateur peut identifier correctement les échantillons réels et faux. Un bon modèle générateur devrait confondre le modèle discriminateur, ce qui entraînerait une précision de classification plus proche de 50 % sur des exemples réels et faux.
Nous pouvons mettre à jour la fonction summarize_performance() pour prendre également le discriminateur et le numéro de l'époque actuelle comme arguments et signaler l'exactitude sur l'échantillon d'exemples réels et faux.
# evaluate the discriminator and plot real and fake points
def summarize_performance(epoch, generator, discriminator, latent_dim, n=100):
# prepare real samples
x_real, y_real = generate_real_samples(n)
# evaluate discriminator on real examples
_, acc_real = discriminator.evaluate(x_real, y_real, verbose=0)
# prepare fake examples
x_fake, y_fake = generate_fake_samples(generator, latent_dim, n)
# evaluate discriminator on fake examples
_, acc_fake = discriminator.evaluate(x_fake, y_fake, verbose=0)
# summarize discriminator performance
print(epoch, acc_real, acc_fake)
# scatter plot real and fake data points
pyplot.scatter(x_real[:, 0], x_real[:, 1], color='red')
pyplot.scatter(x_fake[:, 0], x_fake[:, 1], color='blue')
pyplot.show()
Cette fonction peut ensuite être appelée périodiquement lors de l'entraînement.
Par exemple, si nous choisissons d’entraîner les modèles sur 10 000 itérations, il peut être intéressant de vérifier les performances du modèle toutes les 2 000 itérations.
Nous pouvons y parvenir en paramétrant la fréquence de l'enregistrement via l'argument n_eval et en appelant la fonction summarize_performance() depuis le train() fonctionner après le nombre approprié d’itérations.
La version mise à jour de la fonction train() avec cette modification est répertoriée ci-dessous.
# train the generator and discriminator
def train(g_model, d_model, gan_model, latent_dim, n_epochs=10000, n_batch=128, n_eval=2000):
# determine half the size of one batch, for updating the discriminator
half_batch = int(n_batch / 2)
# manually enumerate epochs
for i in range(n_epochs):
# prepare real samples
x_real, y_real = generate_real_samples(half_batch)
# prepare fake examples
x_fake, y_fake = generate_fake_samples(g_model, latent_dim, half_batch)
# update discriminator
d_model.train_on_batch(x_real, y_real)
d_model.train_on_batch(x_fake, y_fake)
# prepare points in latent space as input for the generator
x_gan = generate_latent_points(latent_dim, n_batch)
# create inverted labels for the fake samples
y_gan = ones((n_batch, 1))
# update the generator via the discriminator's error
gan_model.train_on_batch(x_gan, y_gan)
# evaluate the model every n_eval epochs
if (i+1) % n_eval == 0:
summarize_performance(i, g_model, d_model, latent_dim)
Exemple complet de formation du GAN
Nous avons maintenant tout ce dont nous avons besoin pour former et évaluer un GAN sur la fonction unidimensionnelle que nous avons choisie.
L’exemple complet est répertorié ci-dessous.
# train a generative adversarial network on a one-dimensional function
from numpy import hstack
from numpy import zeros
from numpy import ones
from numpy.random import rand
from numpy.random import randn
from keras.models import Sequential
from keras.layers import Dense
from matplotlib import pyplot
# define the standalone discriminator model
def define_discriminator(n_inputs=2):
model = Sequential()
model.add(Dense(25, activation='relu', kernel_initializer='he_uniform', input_dim=n_inputs))
model.add(Dense(1, activation='sigmoid'))
# compile model
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
# define the standalone generator model
def define_generator(latent_dim, n_outputs=2):
model = Sequential()
model.add(Dense(15, activation='relu', kernel_initializer='he_uniform', input_dim=latent_dim))
model.add(Dense(n_outputs, activation='linear'))
return model
# define the combined generator and discriminator model, for updating the generator
def define_gan(generator, discriminator):
# make weights in the discriminator not trainable
discriminator.trainable = False
# connect them
model = Sequential()
# add generator
model.add(generator)
# add the discriminator
model.add(discriminator)
# compile model
model.compile(loss='binary_crossentropy', optimizer='adam')
return model
# generate n real samples with class labels
def generate_real_samples(n):
# generate inputs in [-0.5, 0.5]
X1 = rand(n) - 0.5
# generate outputs X^2
X2 = X1 * X1
# stack arrays
X1 = X1.reshape(n, 1)
X2 = X2.reshape(n, 1)
X = hstack((X1, X2))
# generate class labels
y = ones((n, 1))
return X, y
# generate points in latent space as input for the generator
def generate_latent_points(latent_dim, n):
# generate points in the latent space
x_input = randn(latent_dim * n)
# reshape into a batch of inputs for the network
x_input = x_input.reshape(n, latent_dim)
return x_input
# use the generator to generate n fake examples, with class labels
def generate_fake_samples(generator, latent_dim, n):
# generate points in latent space
x_input = generate_latent_points(latent_dim, n)
# predict outputs
X = generator.predict(x_input)
# create class labels
y = zeros((n, 1))
return X, y
# evaluate the discriminator and plot real and fake points
def summarize_performance(epoch, generator, discriminator, latent_dim, n=100):
# prepare real samples
x_real, y_real = generate_real_samples(n)
# evaluate discriminator on real examples
_, acc_real = discriminator.evaluate(x_real, y_real, verbose=0)
# prepare fake examples
x_fake, y_fake = generate_fake_samples(generator, latent_dim, n)
# evaluate discriminator on fake examples
_, acc_fake = discriminator.evaluate(x_fake, y_fake, verbose=0)
# summarize discriminator performance
print(epoch, acc_real, acc_fake)
# scatter plot real and fake data points
pyplot.scatter(x_real[:, 0], x_real[:, 1], color='red')
pyplot.scatter(x_fake[:, 0], x_fake[:, 1], color='blue')
pyplot.show()
# train the generator and discriminator
def train(g_model, d_model, gan_model, latent_dim, n_epochs=10000, n_batch=128, n_eval=2000):
# determine half the size of one batch, for updating the discriminator
half_batch = int(n_batch / 2)
# manually enumerate epochs
for i in range(n_epochs):
# prepare real samples
x_real, y_real = generate_real_samples(half_batch)
# prepare fake examples
x_fake, y_fake = generate_fake_samples(g_model, latent_dim, half_batch)
# update discriminator
d_model.train_on_batch(x_real, y_real)
d_model.train_on_batch(x_fake, y_fake)
# prepare points in latent space as input for the generator
x_gan = generate_latent_points(latent_dim, n_batch)
# create inverted labels for the fake samples
y_gan = ones((n_batch, 1))
# update the generator via the discriminator's error
gan_model.train_on_batch(x_gan, y_gan)
# evaluate the model every n_eval epochs
if (i+1) % n_eval == 0:
summarize_performance(i, g_model, d_model, latent_dim)
# size of the latent space
latent_dim = 5
# create the discriminator
discriminator = define_discriminator()
# create the generator
generator = define_generator(latent_dim)
# create the gan
gan_model = define_gan(generator, discriminator)
# train model
train(generator, discriminator, gan_model, latent_dim)
L’exécution de l’exemple rapporte les performances du modèle toutes les 2 000 itérations d’entraînement (lots) et crée un tracé.
Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.
On voit que le processus de formation est relativement instable. La première colonne indique le numéro d'itération, la seconde la précision de classification du discriminateur pour les exemples réels et la troisième colonne la précision de classification du discriminateur pour les (faux) exemples générés.
Dans ce cas, nous pouvons voir que le discriminateur reste relativement confus quant aux exemples réels et que les performances en matière d’identification de faux exemples varient.
1999 0.45 1.0
3999 0.45 0.91
5999 0.86 0.16
7999 0.6 0.41
9999 0.15 0.93
J'omettrai de fournir ici les cinq parcelles créées par souci de concision ; nous n’en examinerons que deux.
Le premier tracé est créé après 2 000 itérations et montre des échantillons réels (rouges) et faux (bleus). Le modèle fonctionne initialement mal avec un groupe de points générés uniquement dans le domaine d'entrée positif, bien qu'avec la bonne relation fonctionnelle.
Le deuxième graphique montre le réel (rouge) et le faux (bleu) après 10 000 itérations.
Ici, nous pouvons voir que le modèle générateur fait un travail raisonnable en générant des échantillons plausibles, avec les valeurs d'entrée dans le bon domaine entre [-0,5 et 0,5] et les valeurs de sortie montrant la relation X^2, ou proche de celle-ci.
Rallonges
Cette section répertorie quelques idées pour étendre le didacticiel que vous souhaiterez peut-être explorer.
- Architecture du modèle. Expérimentez avec des architectures de modèles alternatives pour le discriminateur et le générateur, telles que plus ou moins de nœuds, de couches et des fonctions d'activation alternatives telles que ReLU qui fuit.
- Mise à l'échelle des données. Expérimentez avec des fonctions d'activation alternatives telles que la tangente hyperbolique (tanh) et toute mise à l'échelle requise des données d'entraînement.
- Fonction de cible alternative. Expérimentez avec une fonction cible alternative, telle qu'une simple onde sinusoïdale, une distribution gaussienne, une fonction quadratique différente ou même une fonction polynomiale multimodale.
Si vous explorez l'une de ces extensions, j'aimerais le savoir.
Publiez vos découvertes dans les commentaires ci-dessous.
Lectures complémentaires
Cette section fournit plus de ressources sur le sujet si vous souhaitez approfondir.
API
- API Keras
- Comment puis-je « geler » les calques Keras ?
- API MatplotLib
- API numpy.random.rand
- API numpy.random.randn
- API numpy.zeros
- API numpy.ones
- API numpy.hstack
Résumé
Dans ce didacticiel, vous avez découvert comment développer un réseau contradictoire génératif à partir de zéro pour une fonction unidimensionnelle.
Concrètement, vous avez appris :
- L’avantage de développer un réseau contradictoire génératif à partir de zéro pour une fonction simple unidimensionnelle.
- Comment développer des modèles de discriminateur et de générateur distincts, ainsi qu'un modèle composite pour entraîner le générateur via le comportement prédictif du discriminateur.
- Comment évaluer subjectivement les échantillons générés dans le contexte d'exemples réels du domaine du problème.
Avez-vous des questions ?
Posez vos questions dans les commentaires ci-dessous et je ferai de mon mieux pour y répondre.