Apprendimento semi-supervisionato con reti avversarie generative (GAN)

Se avete mai sentito parlare o studiato il deep learning, probabilmente avrete sentito parlare di MNIST, SVHN, ImageNet, PascalVoc e altri. Ognuno di questi set di dati ha una cosa in comune. Sono costituiti da centinaia e migliaia di dati etichettati. In altre parole, queste raccolte sono composte da coppie (x,y) in cui (x) è il dato grezzo, ad esempio una matrice di immagine, e (y) è una descrizione di ciò che quel punto di dati (x) rappresenta.

Prendiamo come esempio il set di dati MNIST. Ciascuno dei 60.000 punti dati è una coppia (input, label). input è un’immagine in scala di grigi 28×28 e label è un segno che rappresenta l’input. Per il dataset MNIST, un’immagine di input può essere uno zero, uno, due, tre e così via fino a 10 possibili categorie.

MNIST pixel representation.

L’uso più comune di dataset come questi è quello di sviluppare modelli supervisionati. Per addestrare questi algoritmi, di solito forniamo un’enorme quantità di campioni di dati.

L’apprendimento supervisionato è stato al centro della maggior parte delle ricerche nel deep learning. Tuttavia, la necessità di creare modelli in grado di apprendere da un numero inferiore di dati sta aumentando rapidamente.

In quest’ottica, l’apprendimento semi-supervisionato è una tecnica che prevede l’utilizzo di dati etichettati e non etichettati per addestrare un classificatore.

Questo tipo di classificatore prende una piccola porzione di dati etichettati e una quantità molto maggiore di dati non etichettati (provenienti dallo stesso dominio). L’obiettivo è quello di combinare queste fonti di dati per addestrare una rete neurale a convoluzione profonda (Deep Convolution Neural Networks, DCNN) per apprendere una funzione dedotta in grado di mappare un nuovo punto di dati al risultato desiderato.

In questa frontiera, presentiamo un modello GAN per classificare i numeri civici di street view utilizzando un set di addestramento etichettato molto piccolo. Infatti, il modello utilizza circa l’1,3% delle etichette di addestramento originali di SVHN, cioè 1000 (mille) esempi etichettati. Utilizziamo alcune delle tecniche descritte nel documento Improved Techniques for Training GANs di OpenAI.

Se non avete familiarità con le GAN per la generazione di immagini, consultate A Short Introduction to Generative Adversarial Networks. Questo articolo fa riferimento ad alcuni dei contenuti descritti in quell’opera. Il codice completo è disponibile qui. Altrimenti, iscrivetevi al nostro corso ad hoc interamente dedicato ai modelli generativi!

 

Intuizione

Quando abbiamo costruito una GAN per la generazione di immagini, abbiamo addestrato contemporaneamente il generatore e il discriminatore. Dopo l’addestramento, abbiamo potuto scartare il discriminatore perché lo abbiamo usato solo per addestrare il generatore.

Semi-supervided learning GAN architecture for an 11 class classification problem.

 

Per l’apprendimento semi-supervisionato, dobbiamo trasformare il discriminatore in un classificatore multiclasse. Questo nuovo modello deve essere in grado di generalizzare bene sul set di test, anche se non abbiamo molti esempi etichettati per l’addestramento.

Inoltre, questa volta, al termine dell’addestramento, possiamo effettivamente buttare via il generatore. Si noti che i ruoli sono cambiati. Ora il generatore viene utilizzato solo per aiutare il discriminatore durante l’addestramento.

In altre parole, il generatore agisce come un’altra fonte di informazioni da cui il discriminatore ottiene dati di addestramento non etichettati. Come vedremo, questi dati non etichettati sono fondamentali per migliorare le prestazioni del discriminatore.

Inoltre, per una normale GAN a generazione di immagini, il discriminatore ha un solo ruolo. Calcolare la probabilità che i suoi input siano reali o meno – chiamiamolo il problema del GAN.

Tuttavia, per trasformare il discriminatore in un classificatore semi-supervisionato, oltre al problema GAN, il discriminatore deve anche apprendere le probabilità di ciascuna delle classi del dataset originale.

In altre parole, per ogni immagine in ingresso, il discriminatore deve apprendere le probabilità che sia una, due, tre e così via.

Ricordiamo che per un discriminatore GAN di generazione di immagini, abbiamo una singola unità sigmoide in uscita. Questo valore rappresenta la probabilità che un’immagine in ingresso sia reale (valore vicino a 1) o falsa (valore vicino a 0).

In altre parole, dal punto di vista del discriminatore, valori prossimi a 1 significano che i campioni provengono probabilmente dall’insieme di allenamento. Allo stesso modo, un valore vicino a 0 indica una maggiore probabilità che i campioni provengano dalla rete del generatore.

Utilizzando questa probabilità, il discriminatore è in grado di inviare un segnale al generatore. Questo segnale permette al generatore di adattare i suoi parametri durante l’addestramento, migliorando le sue capacità di creare immagini realistiche.

Dobbiamo convertire il discriminatore (del GAN precedente) in un classificatore a 11 classi. Per farlo, possiamo trasformare la sua uscita sigmoide in una softmax con 11 uscite di classe. Le prime 10 per le probabilità delle singole classi del set di dati SVHN (da zero a nove) e l’undicesima classe per tutte le immagini false provenienti dal generatore.

Si noti che se si imposta la probabilità dell’undicesima classe a 0, la somma delle prime 10 probabilità rappresenta la stessa probabilità calcolata con la funzione sigmoide.

Infine, dobbiamo impostare le perdite in modo tale che il discriminatore possa fare entrambe le cose:

– (i) aiutare il generatore di apprendimento a produrre immagini realistiche. A tal fine, dobbiamo istruire il discriminatore a distinguere tra campioni reali e falsi.

– (ii) utilizzare le immagini del generatore, insieme ai dati di addestramento etichettati e non etichettati, per aiutare a classificare il set di dati.

In sintesi, il discriminatore dispone di tre diverse fonti di dati di addestramento.

– Immagini reali con etichette. Si tratta di coppie di immagini etichettate come in un normale problema di classificazione supervisionata.

– Immagini reali senza etichette. Per queste, il classificatore impara solo che le immagini sono reali.

– Immagini dal generatore. Per queste, il discriminatore impara a classificare come false.

La combinazione di queste diverse fonti di dati renderà il classificatore in grado di apprendere da una prospettiva più ampia. Questo, a sua volta, consente al modello di eseguire inferenze in modo molto più preciso di quanto non farebbe utilizzando solo i 1000 esempi etichettati per l’addestramento.

 

Generatore

Il generatore segue un’implementazione molto standard descritta nel documento DCGAN. Questo approccio consiste nel prendere in input un vettore casuale z. Lo rimodella in un tensore 4D e lo sottopone a una sequenza di convoluzioni di trasposizione, normalizzazione batch (BN) e operazioni di leaky ReLU.

Questa sequenza di calcoli aumenta le dimensioni spaziali del vettore di ingresso e ne riduce il numero di canali. Di conseguenza, la rete produce una forma di tensore RGB 32x32x3 schiacciata tra i valori di -1 e 1 attraverso la funzione tangente iperbolica.

def generator(z, output_dim, reuse=False, alpha=0.2, training=True, size_mult=128):
    with tf.variable_scope('generator', reuse=reuse):
        # First fully connected layer
        x1 = tf.layers.dense(z, 4 * 4 * size_mult * 4)
        # Reshape it to start the convolutional stack
        x1 = tf.reshape(x1, (-1, 4, 4, size_mult * 4))
        x1 = tf.layers.batch_normalization(x1, training=training)
        x1 = tf.maximum(alpha * x1, x1)

        x2 = tf.layers.conv2d_transpose(x1, size_mult * 2, 5, strides=2, padding='same')
        x2 = tf.layers.batch_normalization(x2, training=training)
        x2 = tf.maximum(alpha * x2, x2)

        x3 = tf.layers.conv2d_transpose(x2, size_mult, 5, strides=2, padding='same')
        x3 = tf.layers.batch_normalization(x3, training=training)
        x3 = tf.maximum(alpha * x3, x3)

        # Output layer
        logits = tf.layers.conv2d_transpose(x3, output_dim, 5, strides=2, padding='same')

        out = tf.tanh(logits)

        return out

Discriminatore

Il discriminatore, ora un classificatore multiclasse, è la rete più importante. In questo caso, abbiamo impostato un’architettura DCGAN simile, in cui utilizziamo una pila di convoluzioni con BN e ReLU.

Per ridurre le dimensioni dei vettori di caratteristiche, utilizziamo le convoluzioni a stridore. Si noti che non tutte le convoluzioni eseguono questo tipo di calcolo. Quando vogliamo mantenere intatte le dimensioni del vettore di caratteristiche, usiamo strides di 1, altrimenti usiamo strides di 2. Infine, per stabilizzare l’apprendimento, facciamo un uso estensivo di BN (tranne che nel primo strato della rete).

La finestra di convoluzione 2D (kernel o filtro) è impostata per avere una larghezza e un’altezza di 3 in tutte le convoluzioni. Inoltre, si noti che abbiamo alcuni strati con dropout. È importante capire che il nostro discriminatore si comporta (in parte) come qualsiasi altro classificatore normale. Per questo motivo, può soffrire degli stessi problemi di qualsiasi classificatore se non è ben progettato.

Quando si addestra un classificatore di grandi dimensioni su un insieme di dati molto limitato, uno degli inconvenienti più probabili che si possono incontrare è l’immenso rischio di overfitting. Una cosa da osservare nei classificatori “sovrallenati” è che in genere mostrano una notevole differenza tra l’errore di addestramento (più piccolo) e l’errore di test (più alto).

Questa situazione dimostra che il modello ha fatto un buon lavoro nel catturare la struttura del set di dati di addestramento. Tuttavia, poiché crede troppo nei dati di addestramento, non riesce a generalizzare per gli esempi non visti.

Per evitare ciò, facciamo un uso estensivo della regolarizzazione attraverso il dropout. Anche per il primo strato della rete.

Alla fine, invece di applicare uno strato completamente connesso in cima allo stack di convoluzione, eseguiamo il Global Average Pooling (GAP). In GAP, si fa la media delle dimensioni spaziali di un vettore di caratteristiche. Questa operazione ha l’effetto di ridurre le dimensioni del tensore a un unico valore.

Ad esempio, supponiamo che dopo una serie di convoluzioni, otteniamo un tensore di forma [BATCH_SIZE, 8, 8, NUM_CHANNELS]. Per applicare GAP, prendiamo il valore medio sulla fetta del tensore [8×8]. Si ottiene così un tensore di forma [BATCH_SIZE, 1, 1, NUM_CHANNELS] che può essere rimodellato in [BATCH_SIZE, NUM_CHANNELS].

In Network in Network, gli autori descrivono alcuni vantaggi di GAP rispetto ai tradizionali strati completamente connessi. Questi includono: maggiore robustezza per la traslazione spaziale e minori problemi di overfitting. Dopo GAP, applichiamo uno strato completamente connesso per produrre i logit finali. Questi hanno forma [BATCH_SIZE, NUM_CLASSES] e corrispondono ai valori finali non scalati delle classi.

Per ottenere le probabilità di classificazione, alimentiamo i logit attraverso la funzione softmax. Tuttavia, abbiamo ancora bisogno di un modo per rappresentare la probabilità che un’immagine in ingresso sia reale piuttosto che falsa. In altre parole, dobbiamo ancora tenere conto del problema della classificazione binaria di una GAN regolare.

Sappiamo che i logit sono in termini di valori di probabilità softmax. Tuttavia, abbiamo bisogno di un modo per rappresentarli anche come logiti sigmoidi. Sappiamo che la probabilità che un ingresso sia reale corrisponde alla somma di tutti i logit della classe reale. In questo modo, possiamo inserire questi valori in una funzione LogSumExp che modellerà il valore della classificazione binaria. Successivamente, il risultato di LogSumExp viene inviato a una funzione sigmoide.

Per evitare problemi numerici, possiamo utilizzare la funzione LogSumExp di Tensorflow. Questa routine previene i problemi di sovra/sotto flusso che possono verificarsi quando LogSumExp incontra valori molto estremi, positivi o negativi.# This function is more numerically stable than log(sum(exp(input))).
# It avoids overflows caused by taking the exp of large inputs and underflows
# caused by taking the log of small inputs.
gan_logits = tf.reduce_logsumexp(class_logits, 1)

def discriminator(x, reuse=False, alpha=0.2, drop_rate=0., num_classes=10, size_mult=64):
    with tf.variable_scope('discriminator', reuse=reuse):
        x = tf.layers.dropout(x, rate=drop_rate/2.5)

        # Input layer is ?x32x32x3
        x1 = tf.layers.conv2d(x, size_mult, 3, strides=2, padding='same')
        relu1 = tf.maximum(alpha * x1, x1)
        relu1 = tf.layers.dropout(relu1, rate=drop_rate) # [?x16x16x?]

        x2 = tf.layers.conv2d(relu1, size_mult, 3, strides=2, padding='same')
        bn2 = tf.layers.batch_normalization(x2, training=True) # [?x8x8x?]
        relu2 = tf.maximum(alpha * bn2, bn2)

        x3 = tf.layers.conv2d(relu2, size_mult, 3, strides=2, padding='same') # [?x4x4x?]
        bn3 = tf.layers.batch_normalization(x3, training=True)
        relu3 = tf.maximum(alpha * bn3, bn3)
        relu3 = tf.layers.dropout(relu3, rate=drop_rate)

        x4 = tf.layers.conv2d(relu3, 2 * size_mult, 3, strides=1, padding='same') # [?x4x4x?]
        bn4 = tf.layers.batch_normalization(x4, training=True)
        relu4 = tf.maximum(alpha * bn4, bn4)

        x5 = tf.layers.conv2d(relu4, 2 * size_mult, 3, strides=1, padding='same') # [?x4x4x?]
        bn5 = tf.layers.batch_normalization(x5, training=True)
        relu5 = tf.maximum(alpha * bn5, bn5)

        x6 = tf.layers.conv2d(relu5, 2 * size_mult, 3, strides=2, padding='same') # [?x2x2x?]
        bn6 = tf.layers.batch_normalization(x6, training=True)
        relu6 = tf.maximum(alpha * bn6, bn6)
        relu6 = tf.layers.dropout(relu6, rate=drop_rate)
# Flatten it by global average pooling
# In global average pooling, for every feature map we take the average over all the spatial
# domain and return a single value
# In: [BATCH_SIZE,HEIGHT X WIDTH X CHANNELS] --> [BATCH_SIZE, CHANNELS]
features = tf.reduce_mean(relu7, axis=[1,2])

# Set class_logits to be the inputs to a softmax distribution over the different classes
class_logits = tf.layers.dense(features, num_classes)
# Get the probability that the input is real rather than fake
out = tf.nn.softmax(class_logits) # class probabilities for the 10 real classes
# This function is more numerically stable than log(sum(exp(input))).
# It avoids overflows caused by taking the exp of large inputs and underflows
# caused by taking the log of small inputs.
gan_logits = tf.reduce_logsumexp(class_logits, 1)

Perdita del modello

Come abbiamo detto, possiamo dividere la perdita del discriminatore in due parti. Una che rappresenta il problema GAN, la perdita non supervisionata. E l’altra che calcola le probabilità delle singole classi reali, la perdita supervisionata.

Per quanto riguarda la perdita non supervisionata, il discriminatore deve distinguere tra le immagini di addestramento reali e le immagini false del generatore.

Come per una GAN normale, il discriminatore riceve per metà del tempo immagini non etichettate dall’insieme di addestramento e per l’altra metà immagini immaginarie non etichettate dal generatore.

In entrambi i casi si tratta di un problema di classificazione binaria. Poiché vogliamo un valore di probabilità vicino a 1 per le immagini reali e vicino a 0 per quelle irreali, possiamo utilizzare la funzione di entropia incrociata sigmoidea per calcolare la perdita.

Per le immagini provenienti dal set di addestramento, massimizziamo la loro probabilità di essere reali assegnando etichette di 1. Per le immagini fabbricate, provenienti dal generatore, massimizziamo la loro probabilità di essere reali. Per le immagini fabbricate provenienti dal generatore, massimizziamo la loro probabilità di essere false assegnando loro etichette di 0.

Per la perdita supervisionata, dobbiamo utilizzare i logit del discriminatore. Poiché si tratta di un problema di classificazione multiclasse, possiamo utilizzare la funzione di cross entropy softmax con le etichette reali che abbiamo a disposizione.

Si noti che questa parte è simile a qualsiasi altro modello di classificazione. Alla fine, la perdita del discriminatore è la somma della perdita supervisionata e della perdita non supervisionata. Inoltre, poiché stiamo facendo finta di non avere la maggior parte delle etichette, dobbiamo ignorarle nella perdita supervisionata. Per fare ciò, moltiplichiamo la perdita per la variabile maschere che indica quale insieme di etichette è disponibile per l’uso.

Come descritto nel documento Improved Techniques for Training GANs, utilizziamo la corrispondenza delle caratteristiche per la perdita del generatore.

Come descrivono gli autori:

La corrispondenza delle caratteristiche è il concetto di penalizzazione dell’errore assoluto medio tra il valore medio di un insieme di caratteristiche sui dati di addestramento e i valori medi di quell’insieme di caratteristiche sui campioni generati.

Per fare ciò, prendiamo un insieme di statistiche (i momenti) da due fonti diverse e le forziamo a essere simili.

In primo luogo, prendiamo la media delle caratteristiche estratte dal discriminatore durante l’elaborazione di un minibatch di formazione reale.

In secondo luogo, calcoliamo i momenti nello stesso modo, ma ora per quando un minibatch composto da immagini false provenienti dal generatore viene analizzato dal discriminatore.

Infine, con queste due serie di momenti, la perdita del generatore è la differenza assoluta media tra i due. In altre parole, come sottolinea il documento:

Addestriamo il generatore a far corrispondere i valori attesi delle caratteristiche a uno strato intermedio del discriminatore.

Sebbene la perdita di feature-matching abbia buone prestazioni nel compito di apprendimento semi-supervisionato, le immagini prodotte dal generatore non sono buone come quelle create nell’ultimo post.

Sample images created by the generator network using the feature matching loss.

 

Nel documento Improved Techniques for Training GANs, OpenAI riporta risultati all’avanguardia per l’apprendimento di classificazioni semi-supervisionate su MNIST, CIFAR-10 e SVHN.

La nostra implementazione raggiunge precisioni di addestramento e di test rispettivamente del 93% e del 68%. Questi risultati sono migliori di quelli riportati nell’articolo del NIPS 2014, che ha ottenuto circa il 64%.

Questo notebook non è destinato a dimostrare le migliori pratiche come le tecniche di convalida incrociata. Utilizza solo alcune delle tecniche descritte nell’articolo originale di OpenAI.

Il quaderno si basa sul programma di nanodiploma Udacity Deep learning Fundamentals in cui mi sono laureato.

# Here we compute `d_loss`, the loss for the discriminator.
# This should combine two different losses:
# 1. The loss for the GAN problem, where we minimize the cross-entropy for the binary
#    real-vs-fake classification problem.
tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=gan_logits_on_data,
                                                        labels=tf.ones_like(gan_logits_on_data) * (1 - smooth)))

fake_data_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=gan_logits_on_samples,
                                                    labels=tf.zeros_like(gan_logits_on_samples)))

# This way, the unsupervised
unsupervised_loss = real_data_loss + fake_data_loss
#  2. The loss for the SVHN digit classification problem, where we minimize the cross-entropy
#     for the multi-class softmax. For this one we use the labels. Don't forget to ignore
#     use `label_mask` to ignore the examples that we are pretending are unlabeled for the
#     semi-supervised learning problem.
y = tf.squeeze(y)
suppervised_loss = tf.nn.softmax_cross_entropy_with_logits(logits=class_logits_on_data,
                                                              labels=tf.one_hot(y, num_classes, dtype=tf.float32))

label_mask = tf.squeeze(tf.to_float(label_mask))

# ignore the labels that we pretend does not exist for the loss
suppervised_loss = tf.reduce_sum(tf.multiply(suppervised_loss, label_mask))

# get the mean
suppervised_loss = suppervised_loss / tf.maximum(1.0, tf.reduce_sum(label_mask))
d_loss = unsupervised_loss + suppervised_loss
# Here we set `g_loss` to the "feature matching" loss invented by Tim Salimans at OpenAI.
# This loss consists of minimizing the absolute difference between the expected features
# on the data and the expected features on the generated samples.
# This loss works better for semi-supervised learning than the tradition GAN losses.# Make the Generator output features that are on average similar to the features
# that are found by applying the real data to the discriminatordata_moments = tf.reduce_mean(data_features, axis=0)
sample_moments = tf.reduce_mean(sample_features, axis=0)
g_loss = tf.reduce_mean(tf.abs(data_moments - sample_moments))pred_class = tf.cast(tf.argmax(class_logits_on_data, 1), tf.int32)
eq = tf.equal(tf.squeeze(y), pred_class)
correct = tf.reduce_sum(tf.to_float(eq))
masked_correct = tf.reduce_sum(label_mask * tf.to_float(eq))

Conclusioni

Molte ricerche considerano l’apprendimento non supervisionato come l’anello mancante per i sistemi di IA generali.

Per superare questi ostacoli, sono fondamentali i tentativi di risolvere problemi già consolidati utilizzando dati meno etichettati. In questo scenario, le GAN rappresentano una reale alternativa per l’apprendimento di compiti complicati con campioni meno etichettati.

Tuttavia, il divario di prestazioni tra apprendimento supervisionato e semi-supervisionato è ancora lontano dall’essere uguale. Ma possiamo certamente aspettarci che questo divario si riduca man mano che entrano in gioco nuovi approcci.

 

Articolo originale di Thalles Silva

https://medium.com/towards-data-science/semi-supervised-learning-with-gans-9f3cb128c5e

Share:

Contenuti
Torna in alto