Recherche de site Web

Autoencodeurs Adversariaux (avec Pytorch)


Introduction

« La majeure partie de l'apprentissage humain et animal est un apprentissage non supervisé. Si l'intelligence était un gâteau, l'apprentissage non supervisé serait le gâteau [de base], l'apprentissage supervisé serait le glaçage du gâteau, et l'apprentissage par renforcement serait la cerise sur le gâteau. Nous savons comment faire le glaçage et la cerise, mais nous ne savons pas comment faire le gâteau. »

Directeur de la recherche en IA chez Facebook, le professeur Yann LeCunn mentionne à plusieurs reprises cette analogie dans ses conférences. Par apprentissage non supervisé, il fait référence à la « capacité d'une machine à modéliser l'environnement, à prédire des futurs possibles et à comprendre comment le monde fonctionne en l'observant et en y agissant. »

Les modèles génératifs profonds sont l'une des techniques qui tentent de résoudre le problème de l'apprentissage non supervisé en apprentissage automatique. Dans ce cadre, un système d'apprentissage automatique est obligé de découvrir la structure cachée au sein de données non étiquetées. Les modèles génératifs profonds ont de nombreuses applications répandues, telles que l'estimation de densité, le débruitage d'images/audio, la compression, la compréhension de scènes, l'apprentissage de représentations et la classification semi-supervisée, parmi beaucoup d'autres.

Les autoencodeurs variationnels (VAEs) nous permettent de formaliser ce problème dans le cadre des modèles graphiques probabilistes où nous maximisons une borne inférieure sur la log-vraisemblance des données. Dans cet article, nous examinerons une architecture récemment développée, les autoencodeurs adversariaux, qui s'inspirent des VAEs, mais nous offrent plus de flexibilité dans la manière dont nous mappons nos données dans une dimension latente (si cela n’est pas clair pour l’instant, ne vous inquiétez pas, nous reviendrons sur cette idée tout au long de l'article). Une des idées les plus intéressantes concernant les autoencodeurs adversariaux est la manière d'imposer une distribution prioritaire à la sortie d'un réseau de neurones en utilisant l'apprentissage adversarial.

Si vous voulez mettre vos mains dans le code Pytorch, n'hésitez pas à visiter le dépôt GitHub. Tout au long de l'article, nous couvrirons d'abord quelques notions sur les autoencodeurs de débruitage et les autoencodeurs variationnels, puis nous passerons aux autoencodeurs adversariaux, une implémentation Pytorch, la procédure d'entraînement suivie et quelques expériences concernant le démêlage et l'apprentissage semi-supervisé en utilisant le jeu de données MNIST.

Contexte

Autoencodeurs de débruitage (dAE)

La version la plus simple d'un autoencodeur est celle dans laquelle nous entraînons un réseau à reconstruire son entrée. En d'autres termes, nous voudrions que le réseau apprenne d'une manière ou d'une autre la fonction identité $f(x)=x$. Pour que ce problème ne soit pas trivial, nous imposons au réseau de passer par une couche intermédiaire (espace latent) dont la dimensionnalité est beaucoup plus faible que celle de l'entrée. Avec cette condition de goulot d'étranglement, le réseau doit compresser l'information de l'entrée. Le réseau est donc divisé en deux parties : l'encodeur reçoit l'entrée et crée une représentation latente ou cachée de celle-ci, et le décodeur prend cette représentation intermédiaire et tente de reconstruire l'entrée. La perte d'un autoencodeur est appelée perte de reconstruction, et peut être définie simplement comme l'erreur quadratique entre l'entrée et les échantillons générés :

$$L_R (x, x’)=||x - x’||^2$$

Une autre perte de reconstruction largement utilisée lorsque l'entrée est normalisée pour être dans la plage $[0,1]^N$est la perte d'entropie croisée.

Autoencodeurs variationnels (VAE)

Les autoencodeurs variationnels imposent une deuxième contrainte sur la manière de construire la représentation cachée. Désormais, le code latent a une distribution a priori définie par conception $p(x)$. En d'autres termes, l'encodeur ne peut pas utiliser librement tout l'espace latent mais doit restreindre les codes cachés produits pour qu'ils soient probables selon cette distribution a priori $p(x)$. Par exemple, si la distribution a priori sur le code latent est une distribution gaussienne avec une moyenne de 0 et un écart type de 1, alors générer un code latent avec une valeur de 1000 devrait être vraiment improbable.

Cela peut être considéré comme un second type de régularisation sur la quantité d'information qui peut être stockée dans le code latent. L'avantage de cela repose sur le fait que nous pouvons maintenant utiliser le système comme un modèle génératif. Pour créer un nouvel échantillon provenant de la distribution de données $p(x)$, il nous suffit d'échantillonner à partir de $p(z)$et de faire passer cet échantillon à travers le décodeur pour reconstruire une nouvelle image. Si cette condition n'est pas imposée, alors le code latent peut se distribuer librement dans l'espace latent et il n'est donc pas possible d'échantillonner un code latent valide pour produire une sortie de manière directe.

Afin de faire respecter cette propriété, un second terme est ajouté à la fonction de perte sous la forme d'une divergence de Kullback-Leibler (KL) entre la distribution créée par l'encodeur et la distribution a priori. Puisque le VAE est basé sur une interprétation probabiliste, la perte de reconstruction utilisée est la perte d'entropie croisée mentionnée précédemment. En réunissant tout cela, nous obtenons,

$$L(x, x’)=L_R (x, x’) + KL(q(z|x)||p(z))$$ou $$L(x, x’)=- \sum _{k=1} ^{N} x _k \log(x’ _k) + (1- x _k) \log(1 - x’ _k) + KL(q(z|x)|| p(z))$$

Où $q(z|x)$est l'encodeur de notre réseau et $p(z)$est la distribution a priori imposée sur le code latent. Maintenant, cette architecture peut être entraînée conjointement en utilisant la rétropropagation.

Autoencodeurs adversariaux (AAE)

AAE en tant que modèle génératif

L'un des principaux inconvénients des autoencodeurs variationnels est que l'intégrale du terme de divergence KL n'a pas de solution analytique sous forme fermée, sauf pour un petit nombre de distributions. De plus, il n'est pas simple d'utiliser des distributions discrètes pour le code latent $z$. Cela s'explique par le fait que la rétropropagation à travers des variables discrètes n'est généralement pas possible, ce qui rend le modèle difficile à entraîner efficacement. Une approche pour faire cela dans le cadre du VAE a été introduite ici.

Les autoencodeurs adversariaux évitent d'utiliser le divergence KL complètement en utilisant l'apprentissage adversarial. Dans cette architecture, un nouveau réseau est entraîné pour prédire de manière discriminative si un échantillon provient du code caché de l'autoencodeur ou de la distribution a priori $p(z)$déterminée par l'utilisateur. La perte de l'encodeur est maintenant composée de la perte de reconstruction plus la perte donnée par le réseau discriminateur.

L'image montre schématiquement comment fonctionnent les AAEs lorsque nous utilisons une loi gaussienne comme code latent (bien que l'approche soit générique et puisse utiliser n'importe quelle distribution). La première ligne est équivalente à un autoencodeur. Tout d'abord, un échantillon $z$est tiré selon le réseau générateur $q(z|x)$, cet échantillon est ensuite envoyé au décodeur qui génère $x’$à partir de $z$. La perte de reconstruction est calculée entre $x$et $x’$et le gradient est rétropropagé à travers $p$et $q$en conséquence et leurs poids sont mis à jour.

Figure 1. Architecture de base d'un AAE. La rangée supérieure est un autoencodeur tandis que la rangée inférieure est un réseau antagoniste qui force la sortie de l'encodeur à suivre la distribution $p(z)$.

Dans la partie régularisation adversariale, le discriminateur reçoit $z$ distribué selon $q(z|x)$ et $z’$ échantillonné à partir de la véritable distribution a priori $p(z)$ et attribue une probabilité à chacun de provenir de $p(z)$. La perte encourue est rétropropagée à travers le discriminateur pour mettre à jour ses poids. Ensuite, le processus est répété et le générateur met à jour ses paramètres.

Nous pouvons maintenant utiliser la perte subie par le générateur du réseau antagoniste (qui est l'encodeur de l'autoencodeur) au lieu d'une divergence KL pour qu'il apprenne à produire des échantillons selon la distribution $p(z)$. Cette modification nous permet d'utiliser un ensemble plus large de distributions comme priors pour le code latent.

La perte du discriminateur est

$$L_D=- \frac{1}{m} \sum _{k=1} ^m \log(D(z’)) + \log(1 - D(z))$$(Graphique)

où $m$ est la taille du mini-lot, $z$ est généré par l'encodeur et $z’$ est un échantillon du véritable a priori.

Pour le générateur adversarial, nous avons

$$L_G=- \frac{1}{m} \sum _{k=1} ^m log(D(z))$$(Plot)

En regardant les équations et les graphiques, vous devriez vous convaincre que la perte définie de cette manière obligera le discriminateur à être capable de reconnaître les échantillons faux tout en poussant le générateur à tromper le discriminateur.

Définition du réseau

Avant d'entrer dans la procédure d'entraînement utilisée pour ce modèle, nous examinons comment mettre en œuvre ce que nous avons jusqu'à présent dans Pytorch. Pour les réseaux encodeur, décodeur et discriminateur, nous utiliserons de simples réseaux neuronaux à propagation avant avec trois couches d'état caché de 1000 unités, des fonctions non linéaires ReLU et un dropout avec une probabilité de 0,2.

#Encoder
class Q_net(nn.Module):
    def __init__(self):
        super(Q_net, self).__init__()
        self.lin1 = nn.Linear(X_dim, N)
        self.lin2 = nn.Linear(N, N)
        self.lin3gauss = nn.Linear(N, z_dim)
    def forward(self, x):
        x = F.droppout(self.lin1(x), p=0.25, training=self.training)
        x = F.relu(x)
        x = F.droppout(self.lin2(x), p=0.25, training=self.training)
        x = F.relu(x)
        xgauss = self.lin3gauss(x)
        return xgauss
# Decoder
class P_net(nn.Module):
    def __init__(self):
        super(P_net, self).__init__()
        self.lin1 = nn.Linear(z_dim, N)
        self.lin2 = nn.Linear(N, N)
        self.lin3 = nn.Linear(N, X_dim)
    def forward(self, x):
        x = self.lin1(x)
        x = F.dropout(x, p=0.25, training=self.training)
        x = F.relu(x)
        x = self.lin2(x)
        x = F.dropout(x, p=0.25, training=self.training)
        x = self.lin3(x)
        return F.sigmoid(x)
# Discriminator
class D_net_gauss(nn.Module):
    def __init__(self):
        super(D_net_gauss, self).__init__()
        self.lin1 = nn.Linear(z_dim, N)
        self.lin2 = nn.Linear(N, N)
        self.lin3 = nn.Linear(N, 1)
    def forward(self, x):
        x = F.dropout(self.lin1(x), p=0.2, training=self.training)
        x = F.relu(x)
        x = F.dropout(self.lin2(x), p=0.2, training=self.training)
        x = F.relu(x)
        return F.sigmoid(self.lin3(x))

Quelques points à noter à partir de ces définitions. Tout d'abord, puisque la sortie de l'encodeur doit suivre une distribution gaussienne, nous n'utilisons pas de non-linéarités dans sa dernière couche. La sortie du décodeur possède une non-linéarité sigmoïde, ceci est dû au fait que nous utilisons des entrées normalisées de manière à ce que leurs valeurs soient comprises entre 0 et 1. La sortie du réseau discriminateur est simplement un nombre compris entre 0 et 1 représentant la probabilité que l'entrée provienne de la véritable distribution a priori.

Une fois que les classes de réseaux sont définies, nous créons une instance de chacune et définissons les optimiseurs à utiliser. Afin d'avoir de l'indépendance dans la procédure d'optimisation pour l'encodeur (qui est également le générateur du réseau antagoniste), nous définissons deux optimiseurs pour cette partie du réseau comme suit :

torch.manual_seed(10)
Q, P = Q_net() = Q_net(), P_net(0)     # Encoder/Decoder
D_gauss = D_net_gauss()                # Discriminator adversarial
if torch.cuda.is_available():
    Q = Q.cuda()
    P = P.cuda()
    D_cat = D_gauss.cuda()
    D_gauss = D_net_gauss().cuda()
# Set learning rates
gen_lr, reg_lr = 0.0006, 0.0008
# Set optimizators
P_decoder = optim.Adam(P.parameters(), lr=gen_lr)
Q_encoder = optim.Adam(Q.parameters(), lr=gen_lr)
Q_generator = optim.Adam(Q.parameters(), lr=reg_lr)
D_gauss_solver = optim.Adam(D_gauss.parameters(), lr=reg_lr)
Procédure de formation

La procédure d'entraînement pour cette architecture pour chaque mini-lot est effectuée comme suit :

1) Effectuez un passage avant à travers la partie encodeur/décodeur, calculez la perte de reconstruction et mettez à jour les paramètres des réseaux encodeur Q et décodeur P.

    z_sample = Q(X)
    X_sample = P(z_sample)
    recon_loss = F.binary_cross_entropy(X_sample + TINY,
                                        X.resize(train_batch_size, X_dim) + TINY)
    recon_loss.backward()
    P_decoder.step()
    Q_encoder.step()

2) Créez une représentation latente z=Q(x) et prenez un échantillon z’ du préalable p(z), faites passer chacun à travers le discriminateur et calculez le score attribué à chacun (D(z) et D(z’)).

    Q.eval()
    z_real_gauss = Variable(torch.randn(train_batch_size, z_dim) * 5)   # Sample from N(0,5)
    if torch.cuda.is_available():
        z_real_gauss = z_real_gauss.cuda()
    z_fake_gauss = Q(X)

3) Calculez la perte dans le discriminateur et rétropropagez-la à travers le réseau du discriminateur pour mettre à jour ses poids. En code,

    # Compute discriminator outputs and loss
    D_real_gauss, D_fake_gauss = D_gauss(z_real_gauss), D_gauss(z_fake_gauss)
    D_loss_gauss = -torch.mean(torch.log(D_real_gauss + TINY) + torch.log(1 - D_fake_gauss + TINY))
    D_loss.backward()       # Backpropagate loss
    D_gauss_solver.step()   # Apply optimization step

4) Calculez la perte du réseau générateur et mettez à jour le réseau Q en conséquence.

# Generator
Q.train()   # Back to use dropout
z_fake_gauss = Q(X)
D_fake_gauss = D_gauss(z_fake_gauss)

G_loss = -torch.mean(torch.log(D_fake_gauss + TINY))
G_loss.backward()
Q_generator.step()
Génération d'images

Nous essayons maintenant de visualiser comment l'AAE encode les images dans une représentation latente gaussienne 2-D avec un écart type de 5. Pour cela, nous entraînons d'abord le modèle avec un état caché 2-D. Ensuite, nous générons des points uniformes sur cet espace latent de (-10,-10) (coin supérieur gauche) à (10,10) (coin inférieur droit) et les faisons passer à travers le réseau décodeur.

Espace latent. Reconstructions d'images tout en explorant uniformément l'espace latent 2D de -10 à 10 sur les axes $x$ et $y$.

AAE pour apprendre des représentations désentrelacées

Une représentation intermédiaire idéale des données serait celle qui capture les facteurs causals sous-jacents de variation ayant généré les données observées. Yoshua Bengio et ses collègues notent dans cet article que « nous aimerions que nos représentations démêlent les facteurs de variation. Différents facteurs explicatifs des données tendent à changer indépendamment les uns des autres dans la distribution d'entrée ». Ils mentionnent également « que l'approche la plus robuste pour l'apprentissage des caractéristiques est de démêler autant de facteurs que possible, en ne perdant que le minimum d'information sur les données de manière pratique ».

Dans le cas des données MNIST (qui est un grand ensemble de données de chiffres manuscrits), nous pouvons définir deux facteurs causals sous-jacents, d'une part, nous avons le chiffre généré, et d'autre part, le style ou la manière de l'écrire.

Approche supervisée

Dans cette partie, nous allons un pas plus loin que l'architecture précédente et essayons d'imposer une certaine structure dans le code latent $z$. En particulier, nous voulons que l'architecture soit capable de séparer l'information de classe du style de trace dans un scénario entièrement supervisé. Pour ce faire, nous étendons l'architecture précédente à celle de la figure ci-dessous. Nous partageons la dimension latente en deux parties : la première $z$ est analogue à celle que nous avions dans l'exemple précédent ; la seconde partie du code caché est maintenant un vecteur one-hot $y$ indiquant l'identité du chiffre présenté à l'autoencodeur.

Architecture d'autoencodeur adversarial supervisé.

Dans ce contexte, le décodeur utilise le vecteur one-hot $y$ et le code caché $z$ pour reconstruire l'image originale. Il revient à l'encodeur la tâche de coder l'information de style dans $z$. Sur l'image ci-dessous, nous pouvons voir le résultat de l'entraînement de cette architecture avec 10 000 échantillons MNIST étiquetés. La figure montre les images reconstruites dans lesquelles, pour chaque ligne, le code caché $z$ est fixé à une valeur particulière et l'étiquette de classe $y$ varie de 0 à 9. En effet, le style est maintenu à travers les colonnes.

Reconstruction d'image en explorant le code latent $y$ tout en gardant $z$ fixe de gauche à droite.

##### Approche semi-supervisée Comme dernière expérience, nous examinons une alternative pour obtenir des résultats de désentrelacement similaires dans le cas où nous n'avons que quelques échantillons d'informations étiquetées. Nous pouvons modifier l'architecture précédente afin que l'AAE produise un code latent composé par la concaténation d'un vecteur $y$ indiquant la classe ou l'étiquette (en utilisant un Softmax) et d'une variable latente continue $z$ (en utilisant une couche linéaire). Puisque nous voulons que le vecteur $y$ se comporte comme un vecteur one-hot, nous l'imposons à suivre une distribution catégorique en utilisant un second réseau adversaire avec le discriminateur $D_{cat}$. L'encodeur est maintenant $q(z, y|x)$. Le décodeur utilise à la fois l'étiquette de classe et le code latent continu pour reconstruire l'image.

Architecture d'autoencodeur adversarial semi-supervisé.

Les données non étiquetées contribuent à la procédure d'entraînement en améliorant la façon dont l'encodeur crée le code caché en fonction de la perte de reconstruction et en améliorant les réseaux générateur et discriminateur pour lesquels aucune information étiquetée n'est nécessaire.

Désentrelacement avec une approche semi-supervisée.

Il convient de noter que maintenant, non seulement nous pouvons générer des images avec moins d'informations étiquetées, mais nous pouvons également classer les images pour lesquelles nous n'avons pas d'étiquettes en regardant le code latent $y$ et en choisissant celui avec la valeur la plus élevée. Avec les paramètres actuels, la perte de classification est d'environ 3 % en utilisant 100 échantillons étiquetés et 47 000 échantillons non étiquetés.

Sur l'entraînement GPU

Enfin, nous faisons une courte comparaison du temps d'entraînement pour ce dernier algorithme sur deux GPU différents et un CPU sur la plateforme Paperspace. Même si cette architecture n'est pas très compliquée et est composée de quelques couches linéaires, l'amélioration du temps d'entraînement est énorme lorsqu'on utilise l'accélération GPU. Le temps d'entraînement pour 500 époques passe de presque 4 heures sur CPU à environ 9 minutes en utilisant la Nvidia Quadro M4000, et encore réduit à 6 minutes avec la Nvidia Quadro P5000.

Temps d'entraînement avec et sans accélération GPU

Plus de matériel

  • Qu'est-ce qu'un autoencodeur variationnel (Tutoriel)
  • Auto-encoding Variational Bayes (article original)
  • Autoencodeurs adversariaux (article original)
  • Construire des machines qui imaginent et raisonnent : principes et applications des modèles génératifs profonds