Ga naar hoofdinhoud

Auto-encoders implementeren in Keras: tutorial

In deze tutorial leer je meer over auto-encoders en hoe je convolutionele en denoising auto-encoders bouwt met de notMNIST-dataset in Keras.
· 15 min lezen

Verken met AI

In het algemeen kun je auto-encoders beschouwen als een unsupervised leertechniek, omdat je geen expliciete labels nodig hebt om het model te trainen. Alles wat je nodig hebt om een auto-encoder te trainen, is ruwe inputdata.

In deze tutorial leer je over auto-encoders in deep learning en implementeer je een convolutionele en denoising auto-encoder in Python met Keras. Je werkt met de NotMNIST-alfabetdataset als voorbeeld.

Kort samengevat behandel je vandaag de volgende onderwerpen:

  • Eerst leer je meer over auto-encoders: wat ze zijn, hoe ze zich verhouden tot technieken voor dimensiereductie en de verschillende typen van dit algoritme;
  • Vervolgens focus je op de convolutionele auto-encoder: je ziet wat dit type auto-encoder doet en hoe je hem kunt opbouwen. Daarna implementeer je er zelf een! Je leert hoe je data in ubyte gzip-formaat laadt, vervolgens verken je de data, preprocess je de data, pas je de data in een model, visualiseer je de trainings- en validatieverliesgrafiek en voorspel je tot slot op de testset.
  • Daarna maak je kennis met de denoising auto-encoder en zie je hoe je er een kunt implementeren: je leert ruis aan de beelden toe te voegen. Vervolgens voer je deze beelden in je deep learning-model en train je het! Tot slot voorspel je op de ruisachtige testbeelden.

Auto-encoder

Zoals je in de introductie las, is een auto-encoder een unsupervised machine learning-algoritme dat een afbeelding als input neemt en probeert deze te reconstrueren met minder bits vanuit de bottleneck, ook wel de latente ruimte genoemd. De afbeelding wordt bij de bottleneck grotendeels gecomprimeerd. De compressie in auto-encoders wordt bereikt door het netwerk een tijd te trainen; naarmate het leert, probeert het de inputafbeelding zo goed mogelijk te representeren in de bottleneck. Algemene beeldcompressie-algoritmen zoals JPEG en JPEG lossless comprimeren afbeeldingen zonder enige vorm van training en doen dat behoorlijk goed.

Auto-encoders lijken op technieken voor dimensiereductie zoals Principal Component Analysis (PCA). Ze projecteren data van een hogere dimensie naar een lagere dimensie met lineaire transformaties en proberen de belangrijke kenmerken van de data te behouden terwijl ze de niet-essentiële delen verwijderen.

Het belangrijkste verschil tussen auto-encoders en PCA zit echter in de transformatie: zoals je al las, gebruikt PCA lineaire transformaties, terwijl auto-encoders niet-lineaire transformaties gebruiken.

Nu je een beetje begrip hebt van auto-encoders, laten we de term opsplitsen en er wat intuïtie bij krijgen!

autoencoder keras

De bovenstaande afbeelding is een vanilla auto-encoder met twee lagen en één verborgen laag. In deep learning-terminologie zul je vaak merken dat de inputlaag niet wordt meegeteld bij het bepalen van het totale aantal lagen in een architectuur. Het totaal aantal lagen in een architectuur bestaat alleen uit het aantal verborgen lagen en de outputlaag.

Zoals in de afbeelding te zien is, hebben de input- en outputlagen hetzelfde aantal neuronen.

Neem een voorbeeld. Je voert een afbeelding met slechts vijf pixelwaarden in de auto-encoder, die door de encoder wordt gecomprimeerd naar drie pixelwaarden bij de bottleneck (middelste laag) of latente ruimte. Met deze drie waarden probeert de decoder de vijf pixelwaarden of eigenlijk de inputafbeelding te reconstrueren die je als input aan het netwerk hebt gegeven.

In de praktijk zitten er meer verborgen lagen tussen de input en de output.

autoencoder python keras

Een auto-encoder kun je in drie delen opdelen

  • Encoder: dit deel van het netwerk comprimeert of downsamplet de input naar een kleiner aantal bits. De ruimte die door dit kleinere aantal bits wordt vertegenwoordigd, wordt vaak de latente ruimte of bottleneck genoemd. De bottleneck heet ook wel het "maximale compressiepunt", omdat de input hier maximaal gecomprimeerd is. De gecomprimeerde bits die de oorspronkelijke input representeren, worden samen een “encoding” van de input genoemd.
  • Decoder: dit deel van het netwerk probeert de input te reconstrueren met alleen de encoding van de input. Als de decoder de input exact kan reconstrueren zoals die aan de encoder werd gevoerd, kun je zeggen dat de encoder de beste encodings voor de input kan produceren waarmee de decoder goed kan reconstrueren!

Er zijn verschillende soorten auto-encoders, zoals de convolutionele auto-encoder, denoising auto-encoder, variational auto-encoder en sparse auto-encoder. Zoals je in de introductie las, focus je in deze tutorial alleen op de convolutionele en denoising varianten.Convolutionele auto-encoders in Python met Keras

Omdat je inputdata uit afbeeldingen bestaat, is het een goed idee om een convolutionele auto-encoder te gebruiken. Het is geen auto-encoder-variant, maar eerder een traditionele auto-encoder met convolutionele lagen erbovenop: je vervangt in feite volledig verbonden lagen door convolutionele lagen. Convolutionele lagen, samen met max-pooling-lagen, zetten de input om van breed (een 28 x 28-afbeelding) en dun (één kanaal of grijswaarden) naar klein (7 x 7-afbeelding in de latente ruimte) en dik (128 kanalen).

Maak je geen zorgen als je het bovenstaande idee nog niet helemaal begrijpt! Het tweede deel van deze tutorial, waarin je focust op de implementatie hiervan, zal hopelijk al je twijfels wegnemen (als je die hebt!).

Tip: als je meer wilt weten over convolutionele neurale netwerken, bekijk dan deze tutorial.

Dit helpt het netwerk om visuele kenmerken uit de afbeeldingen te halen en zo een nauwkeurigere representatie in de latente ruimte te krijgen. Het reconstructieproces gebruikt upsampling en convoluties, die bekendstaan als een decoder. Het downsamplen is het proces waarin de afbeelding wordt gecomprimeerd naar een lage dimensie, ook wel een encoder genoemd.

Het is belangrijk om op te merken dat de encoder vooral de inputafbeelding comprimeert; bijvoorbeeld: als je inputafbeelding de afmeting 176 x 176 x 1 (~30976) heeft, dan kan het maximale compressiepunt een dimensie van 22 x 22 x 512 (~247808) hebben. In dit geval begon je met een grijswaardenafbeelding van 176 x 176 en door die door een paar convolutionele lagen en precies drie max-pooling-lagen te sturen, is je afbeelding uiteindelijk gedownsampeld naar een dimensie van 22 x 22, maar het aantal kanalen is toegenomen van 1 naar 512. Zoals hierboven vermeld, werd je input van breed (176 x 176) en dun (1) klein (22x22) en dik (512).

De data laden

De notMNIST-dataset is een beeldherkenningsdataset van lettertypeglyphs voor de letters A tot en met J. Ze lijkt sterk op de klassieke MNIST-dataset, die afbeeldingen bevat van handgeschreven cijfers 0 tot en met 9: in dit geval vind je dat de NotMNIST-dataset bestaat uit 28x28 grijswaardenafbeeldingen van 70.000 letters van A - J in totaal 10 categorieën, en 6.000 afbeeldingen per categorie.

Tip: als je wilt leren hoe je een Multi-Layer Perceptron (MLP) voor classificatietaken met de MNIST-dataset implementeert, bekijk dan deze tutorial.

De NotMNIST-dataset is niet vooraf gedefinieerd in het Keras- of TensorFlow-framework, dus je zult de data moeten downloaden van deze bron. De data wordt gedownload in ubyte.gzip-formaat, maar maak je daar nog geen zorgen over! Je leert zo hoe je bytestream-formaten leest en ze omzet naar een NumPy-array. Laten we beginnen!

Het netwerk wordt getraind op een Nvidia Tesla K40, dus als je op een GPU traint en Jupyter Notebook gebruikt, moet je drie extra regels code toevoegen waarin je de CUDA device order en CUDA visible devices specificeert met een module genaamd os.

In de onderstaande code stel je in feite omgevingsvariabelen in in de notebook met os.environ. Het is goed om het volgende te doen vóór je Keras initialiseert, om de Keras-backend TensorFlow te beperken tot de eerste GPU. Als de machine waarop je traint een GPU op 0 heeft, gebruik dan 0 in plaats van 1. Je kunt dat controleren door een eenvoudig commando in je terminal uit te voeren: bijvoorbeeld nvidia-smi

import os
os.environ["CUDA_DEVICE_ORDER"]="PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"]="1" #model will be trained on GPU 1

Vervolgens importeer je alle vereiste modules zoals numpy, matplotlib en vooral keras, aangezien je dat framework in deze tutorial gebruikt!

import keras
from matplotlib import pyplot as plt
import numpy as np
import gzip
%matplotlib inline
from keras.layers import Input,Conv2D,MaxPooling2D,UpSampling2D
from keras.models import Model
from keras.optimizers import RMSprop
Using TensorFlow backend.

Hier definieer je een functie die het gzip-bestand opent en het bestand leest met bytestream.read(). Je geeft de afbeeldingsdimensie en het totale aantal afbeeldingen door aan deze functie. Vervolgens zet je met np.frombuffer() de string die is opgeslagen in variabele buf om naar een NumPy-array van type float32.

Daarna vorm je de array om naar een driedimensionale array of tensor, waarbij de eerste dimensie het aantal afbeeldingen is en de tweede en derde dimensie de dimensies van de afbeelding zijn. Tot slot retourneer je de NumPy-array data.

def extract_data(filename, num_images):
    with gzip.open(filename) as bytestream:
        bytestream.read(16)
        buf = bytestream.read(28 * 28 * num_images)
        data = np.frombuffer(buf, dtype=np.uint8).astype(np.float32)
        data = data.reshape(num_images, 28,28)
        return data

Je roept nu de functie extract_data() aan door de trainings- en testbestanden door te geven, samen met hun bijbehorende aantal afbeeldingen:

train_data = extract_data('train-images-idx3-ubyte.gz', 60000)
test_data = extract_data('t10k-images-idx3-ubyte.gz', 10000)

Evenzo definieer je een functie om labels te extraheren die het gzip-bestand opent en het bestand leest met bytestream.read(), waaraan je de labeldimensie (1) en het totale aantal afbeeldingen doorgeeft. Vervolgens zet je met np.frombuffer() de string die is opgeslagen in variabele buf om naar een NumPy-array van type int64.

Dit keer hoef je de array niet te reshapen, omdat de variabele labels een kolomvector van dimensie 60.000 x 1 zal teruggeven. Tot slot retourneer je de NumPy-array labels.

def extract_labels(filename, num_images):
    with gzip.open(filename) as bytestream:
        bytestream.read(8)
        buf = bytestream.read(1 * num_images)
        labels = np.frombuffer(buf, dtype=np.uint8).astype(np.int64)
        return labels

Je roept nu de functie voor het extraheren van labels aan door de trainings- en testlabelfiles door te geven, samen met hun bijbehorende aantal afbeeldingen:

train_labels = extract_labels('train-labels-idx1-ubyte.gz',60000)
test_labels = extract_labels('t10k-labels-idx1-ubyte.gz',10000)

Als je de trainings- en testdata eenmaal hebt geladen, ben je klaar om de data te analyseren om wat intuïtie te krijgen over de dataset waarmee je in deze tutorial gaat werken!

Data verkennen

Laten we nu analyseren hoe afbeeldingen in de dataset eruitzien en ook de dimensies van de afbeeldingen bekijken met behulp van het NumPy-arrayattribuut .shape:

# Shapes of training set
print("Training set (images) shape: {shape}".format(shape=train_data.shape))

# Shapes of test set
print("Test set (images) shape: {shape}".format(shape=test_data.shape))
Training set (images) shape: (60000, 28, 28)
Test set (images) shape: (10000, 28, 28)

Uit de bovenstaande output zie je dat de trainingsdata een vorm heeft van 60000 x 28 x 28, omdat er 60.000 trainingsvoorbeelden zijn, elk een matrix van 28 x 28. Evenzo heeft de testdata een vorm van 10000 x 28 x 28, omdat er 10.000 testvoorbeelden zijn.

Let op dat je in deze taak de trainings- en testlabels niet zult gebruiken. De taak gaat alleen over de trainings- en testafbeeldingen. Maar voor verkenningsdoeleinden, wat je een betere intuïtie over de data kan geven, maak je wel gebruik van de labels.

Laten we een woordenboek maken dat klassennamen met hun bijbehorende categorische klasselabels bevat:

# Create dictionary of target classes
label_dict = {
 0: 'A',
 1: 'B',
 2: 'C',
 3: 'D',
 4: 'E',
 5: 'F',
 6: 'G',
 7: 'H',
 8: 'I',
 9: 'J',
}

Bekijk nu een paar afbeeldingen in je dataset:

plt.figure(figsize=[5,5])

# Display the first image in training data
plt.subplot(121)
curr_img = np.reshape(train_data[0], (28,28))
curr_lbl = train_labels[0]
plt.imshow(curr_img, cmap='gray')
plt.title("(Label: " + str(label_dict[curr_lbl]) + ")")

# Display the first image in testing data
plt.subplot(122)
curr_img = np.reshape(test_data[0], (28,28))
curr_lbl = test_labels[0]
plt.imshow(curr_img, cmap='gray')
plt.title("(Label: " + str(label_dict[curr_lbl]) + ")")
<matplotlib.text.Text at 0x7f3ec73db240>

voorbeeldafbeeldingen uit trainings- en testdata

De output van bovenstaande twee plots zijn een van de voorbeeldafbeeldingen uit zowel de trainings- als testdata, en deze afbeeldingen hebben respectievelijk een klasselabel 5 of F en 3 of D. Op dezelfde manier hebben andere letters verschillende labels, maar vergelijkbare letters hebben hetzelfde label. Dit betekent dat alle 6.000 afbeeldingen van klasse F een klasselabel 5 hebben.

Data preprocessing

De afbeeldingen in de dataset zijn inderdaad grijswaardenafbeeldingen met pixelwaarden van 0 tot 255 en een dimensie van 28 x 28. Voordat je de data in het model voert, is het heel belangrijk om te preprocessen. Je zet eerst elke 28 x 28-afbeelding van de train- en testset om naar een matrix van 28 x 28 x 1, die je in het netwerk kunt voeren:

train_data = train_data.reshape(-1, 28,28, 1)
test_data = test_data.reshape(-1, 28,28, 1)
train_data.shape, test_data.shape
((60000, 28, 28, 1), (10000, 28, 28, 1))

Controleer vervolgens het datatype van de trainings- en test-NumPy-arrays; dit moet float32 zijn. Zo niet, dan moet je het omzetten naar dit formaat, maar omdat je dat al hebt gedaan tijdens het inlezen van de data, hoeft dat nu niet meer. Je moet de pixelwaarden ook herschalen naar het bereik 0 - 1 inclusief. Laten we dat doen!

Vergeet niet de datatypes van de trainings- en testdata te verifiëren:

train_data.dtype, test_data.dtype
(dtype('float32'), dtype('float32'))

Herschalen van de trainings- en testdata met de maximale pixelwaarde van de trainings- en testdata:

np.max(train_data), np.max(test_data)
(255.0, 255.0)
train_data = train_data / np.max(train_data)
test_data = test_data / np.max(test_data)

Laten we verifiëren dat de maximale waarde van de train- en testdata 1,0 is na het herschalen!

np.max(train_data), np.max(test_data)
(1.0, 1.0)

Na dit alles is het belangrijk om de data op te splitsen. Om je model goed te laten generaliseren, splits je de trainingsdata in twee delen: een train- en een validatieset. Je traint je model op 80% van de data en valideert op 20% van de overgebleven trainingsdata.

Dit helpt ook om de kans op overfitting te verkleinen, omdat je je model valideert op data die het niet heeft gezien tijdens de trainingsfase.

Je kunt de module train_test_split van scikit-learn gebruiken om de data goed te verdelen:

from sklearn.model_selection import train_test_split
train_X,valid_X,train_ground,valid_ground = train_test_split(train_data,
                                                             train_data, 
                                                             test_size=0.2, 
                                                             random_state=13)

Let op dat je voor deze taak geen trainings- en testlabels nodig hebt. Daarom geef je de trainingsafbeeldingen twee keer door. Je trainingsafbeeldingen fungeren zowel als input als als ground truth, vergelijkbaar met de labels die je hebt bij een classificatietaak.

Nu ben je klaar om het netwerk te definiëren en de data erin te voeren. Dus zonder verder oponthoud: door naar de volgende stap!

De convolutionele auto-encoder

De afbeeldingen hebben formaat 28 x 28 x 1 ofwel een 784-dimensionale vector. Je zet de afbeeldingsmatrix om naar een array, schaalt die tussen 0 en 1, vormt hem om naar 28 x 28 x 1 en voert dit als input aan het netwerk.

Ook gebruik je een batchgrootte van 128; een hogere batchgrootte van 256 of 512 is ook prima, het hangt af van het systeem waarop je je model traint. Dit draagt sterk bij aan het bepalen van de leerparameters en beïnvloedt de voorspellingsnauwkeurigheid. Je traint je netwerk 50 epochs.

batch_size = 128
epochs = 50
inChannel = 1
x, y = 28, 28
input_img = Input(shape = (x, y, inChannel))

Zoals eerder besproken, is de auto-encoder opgedeeld in twee delen: een encoder en een decoder.

Encoder

  • De eerste laag heeft 32 filters van 3 x 3, gevolgd door een downsampling- (max-pooling-)laag,
  • De tweede laag heeft 64 filters van 3 x 3, gevolgd door nog een downsampling-laag,
  • De laatste laag van de encoder heeft 128 filters van 3 x 3.



Decoder

  • De eerste laag heeft 128 filters van 3 x 3, gevolgd door een upsampling-laag,/li>
  • De tweede laag heeft 64 filters van 3 x 3, gevolgd door nog een upsampling-laag,
  • De laatste laag van de encoder heeft 1 filter van 3 x 3.

De max-poolinglaag downsamplet de input telkens met een factor twee, terwijl de upsamplinglaag de input telkens met een factor twee upsamplet.

Let op: Het aantal filters, de filtergrootte, het aantal lagen, het aantal epochs waarop je je model traint: het zijn allemaal hyperparameters en moeten worden bepaald op basis van je eigen intuïtie. Je kunt vrij experimenteren door met deze hyperparameters te spelen en de prestaties van je model te meten. Zo leer je stap voor stap de kunst van deep learning!

def autoencoder(input_img):
    #encoder
    #input = 28 x 28 x 1 (wide and thin)
    conv1 = Conv2D(32, (3, 3), activation='relu', padding='same')(input_img) #28 x 28 x 32
    pool1 = MaxPooling2D(pool_size=(2, 2))(conv1) #14 x 14 x 32
    conv2 = Conv2D(64, (3, 3), activation='relu', padding='same')(pool1) #14 x 14 x 64
    pool2 = MaxPooling2D(pool_size=(2, 2))(conv2) #7 x 7 x 64
    conv3 = Conv2D(128, (3, 3), activation='relu', padding='same')(pool2) #7 x 7 x 128 (small and thick)

    #decoder
    conv4 = Conv2D(128, (3, 3), activation='relu', padding='same')(conv3) #7 x 7 x 128
    up1 = UpSampling2D((2,2))(conv4) # 14 x 14 x 128
    conv5 = Conv2D(64, (3, 3), activation='relu', padding='same')(up1) # 14 x 14 x 64
    up2 = UpSampling2D((2,2))(conv5) # 28 x 28 x 64
    decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(up2) # 28 x 28 x 1
    return decoded

Nadat het model is gemaakt, moet je het compileren met de optimizer RMSProp.

Tip: bekijk deze tutorial van Andrew Ng over RMSProp als je er meer over wilt weten.

Let op dat je ook het type verlies moet specificeren via het argument loss. In dit geval is dat de mean squared error, omdat het verlies na elke batch wordt berekend tussen de batch met voorspelde output en de ground truth met behulp van mean squared error, pixel voor pixel:

autoencoder = Model(input_img, autoencoder(input_img))
autoencoder.compile(loss='mean_squared_error', optimizer = RMSprop())

Laten we de lagen die je in de bovenstaande stap hebt gemaakt visualiseren met de samenvattingsfunctie; deze toont het aantal parameters (gewichten en biases) in elke laag en ook het totaal aantal parameters in je model.

autoencoder.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_6 (InputLayer)         (None, 28, 28, 1)         0         
_________________________________________________________________
conv2d_25 (Conv2D)           (None, 28, 28, 32)        320       
_________________________________________________________________
max_pooling2d_9 (MaxPooling2 (None, 14, 14, 32)        0         
_________________________________________________________________
conv2d_26 (Conv2D)           (None, 14, 14, 64)        18496     
_________________________________________________________________
max_pooling2d_10 (MaxPooling (None, 7, 7, 64)          0         
_________________________________________________________________
conv2d_27 (Conv2D)           (None, 7, 7, 128)         73856     
_________________________________________________________________
conv2d_28 (Conv2D)           (None, 7, 7, 128)         147584    
_________________________________________________________________
up_sampling2d_9 (UpSampling2 (None, 14, 14, 128)       0         
_________________________________________________________________
conv2d_29 (Conv2D)           (None, 14, 14, 64)        73792     
_________________________________________________________________
up_sampling2d_10 (UpSampling (None, 28, 28, 64)        0         
_________________________________________________________________
conv2d_30 (Conv2D)           (None, 28, 28, 1)         577       
=================================================================
Total params: 314,625
Trainable params: 314,625
Non-trainable params: 0
_________________________________________________________________

Het is eindelijk tijd om het model te trainen met de fit()-functie van Keras! Het model traint 50 epochs. De fit()-functie retourneert een history-object; Door het resultaat hiervan op te slaan in fashion_train, kun je het later gebruiken om de verliesfunctie te plotten tussen training en validatie, wat je helpt om de prestaties van je model visueel te analyseren.

Train het model

autoencoder_train = autoencoder.fit(train_X, train_ground, batch_size=batch_size,epochs=epochs,verbose=1,validation_data=(valid_X, valid_ground))
Train on 48000 samples, validate on 12000 samples
Epoch 1/50
48000/48000 [==============================] - 16s - loss: 0.0368 - val_loss: 0.0132
Epoch 2/50
48000/48000 [==============================] - 15s - loss: 0.0101 - val_loss: 0.0085
Epoch 3/50
48000/48000 [==============================] - 15s - loss: 0.0071 - val_loss: 0.0081
Epoch 4/50
48000/48000 [==============================] - 15s - loss: 0.0057 - val_loss: 0.0056
Epoch 5/50
48000/48000 [==============================] - 15s - loss: 0.0048 - val_loss: 0.0051
Epoch 6/50
48000/48000 [==============================] - 15s - loss: 0.0043 - val_loss: 0.0039
Epoch 7/50
48000/48000 [==============================] - 15s - loss: 0.0038 - val_loss: 0.0039
Epoch 8/50
48000/48000 [==============================] - 15s - loss: 0.0035 - val_loss: 0.0039
Epoch 9/50
48000/48000 [==============================] - 15s - loss: 0.0032 - val_loss: 0.0030
Epoch 10/50
48000/48000 [==============================] - 15s - loss: 0.0030 - val_loss: 0.0029
Epoch 11/50
48000/48000 [==============================] - 15s - loss: 0.0029 - val_loss: 0.0026
Epoch 12/50
48000/48000 [==============================] - 15s - loss: 0.0027 - val_loss: 0.0025
Epoch 13/50
48000/48000 [==============================] - 15s - loss: 0.0026 - val_loss: 0.0028
Epoch 14/50
48000/48000 [==============================] - 15s - loss: 0.0025 - val_loss: 0.0022
Epoch 15/50
48000/48000 [==============================] - 15s - loss: 0.0024 - val_loss: 0.0024
Epoch 16/50
48000/48000 [==============================] - 16s - loss: 0.0023 - val_loss: 0.0027
Epoch 17/50
48000/48000 [==============================] - 15s - loss: 0.0023 - val_loss: 0.0022
Epoch 18/50
48000/48000 [==============================] - 16s - loss: 0.0022 - val_loss: 0.0025
Epoch 19/50
48000/48000 [==============================] - 16s - loss: 0.0022 - val_loss: 0.0022
Epoch 20/50
48000/48000 [==============================] - 15s - loss: 0.0021 - val_loss: 0.0022
Epoch 21/50
48000/48000 [==============================] - 16s - loss: 0.0021 - val_loss: 0.0020
Epoch 22/50
48000/48000 [==============================] - 16s - loss: 0.0020 - val_loss: 0.0019
Epoch 23/50
48000/48000 [==============================] - 16s - loss: 0.0020 - val_loss: 0.0021
Epoch 24/50
48000/48000 [==============================] - 16s - loss: 0.0020 - val_loss: 0.0018
Epoch 25/50
48000/48000 [==============================] - 16s - loss: 0.0019 - val_loss: 0.0020
Epoch 26/50
48000/48000 [==============================] - 16s - loss: 0.0019 - val_loss: 0.0020
Epoch 27/50
48000/48000 [==============================] - 16s - loss: 0.0019 - val_loss: 0.0017
Epoch 28/50
48000/48000 [==============================] - 16s - loss: 0.0018 - val_loss: 0.0018
Epoch 29/50
48000/48000 [==============================] - 16s - loss: 0.0018 - val_loss: 0.0019
Epoch 30/50
48000/48000 [==============================] - 16s - loss: 0.0018 - val_loss: 0.0017
Epoch 31/50
48000/48000 [==============================] - 16s - loss: 0.0018 - val_loss: 0.0019
Epoch 32/50
48000/48000 [==============================] - 16s - loss: 0.0017 - val_loss: 0.0018
Epoch 33/50
48000/48000 [==============================] - 16s - loss: 0.0017 - val_loss: 0.0017
Epoch 34/50
48000/48000 [==============================] - 15s - loss: 0.0017 - val_loss: 0.0018
Epoch 35/50
48000/48000 [==============================] - 15s - loss: 0.0017 - val_loss: 0.0019
Epoch 36/50
48000/48000 [==============================] - 15s - loss: 0.0017 - val_loss: 0.0016
Epoch 37/50
48000/48000 [==============================] - 15s - loss: 0.0017 - val_loss: 0.0017
Epoch 38/50
48000/48000 [==============================] - 15s - loss: 0.0016 - val_loss: 0.0019
Epoch 39/50
48000/48000 [==============================] - 15s - loss: 0.0016 - val_loss: 0.0015
Epoch 40/50
48000/48000 [==============================] - 15s - loss: 0.0016 - val_loss: 0.0017
Epoch 41/50
48000/48000 [==============================] - 15s - loss: 0.0016 - val_loss: 0.0017
Epoch 42/50
48000/48000 [==============================] - 15s - loss: 0.0016 - val_loss: 0.0014
Epoch 43/50
48000/48000 [==============================] - 15s - loss: 0.0016 - val_loss: 0.0018
Epoch 44/50
48000/48000 [==============================] - 15s - loss: 0.0016 - val_loss: 0.0015
Epoch 45/50
48000/48000 [==============================] - 15s - loss: 0.0016 - val_loss: 0.0014
Epoch 46/50
48000/48000 [==============================] - 16s - loss: 0.0015 - val_loss: 0.0016
Epoch 47/50
48000/48000 [==============================] - 16s - loss: 0.0015 - val_loss: 0.0017
Epoch 48/50
48000/48000 [==============================] - 16s - loss: 0.0015 - val_loss: 0.0015
Epoch 49/50
48000/48000 [==============================] - 16s - loss: 0.0015 - val_loss: 0.0016
Epoch 50/50
48000/48000 [==============================] - 16s - loss: 0.0015 - val_loss: 0.0020

Eindelijk! Je hebt het model 50 epochs getraind op Not-MNIST. Laten we nu de verliesgrafiek tussen training en validatie plotten om de modelprestatie te visualiseren.

Training versus validatie-verliesdiagram

loss = autoencoder_train.history['loss']
val_loss = autoencoder_train.history['val_loss']
epochs = range(epochs)
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()

training en validatie verliesplot

Je ziet dat de validatie- en trainingsverlies in de pas lopen. Dit toont aan dat je model niet overfit: de validatieverlies daalt en stijgt niet, en er is zelden een grote kloof tussen trainings- en validatieverlies.

Daarom kun je zeggen dat de generalisatiecapaciteit van je model goed is.

Tot slot is het tijd om de testafbeeldingen te reconstrueren met de functie predict() van Keras en te zien hoe goed je model in staat is om te reconstrueren op de testdata.

Voorspellen op testdata

Je laat het getrainde model voorspellen op alle 10.000 testafbeeldingen en plot een paar van de gereconstrueerde afbeeldingen om te visualiseren hoe goed je model de testafbeeldingen kan reconstrueren.

pred = autoencoder.predict(test_data)
pred.shape
(10000, 28, 28, 1)
plt.figure(figsize=(20, 4))
print("Test Images")
for i in range(10):
    plt.subplot(2, 10, i+1)
    plt.imshow(test_data[i, ..., 0], cmap='gray')
    curr_lbl = test_labels[i]
    plt.title("(Label: " + str(label_dict[curr_lbl]) + ")")
plt.show()    
plt.figure(figsize=(20, 4))
print("Reconstruction of Test Images")
for i in range(10):
    plt.subplot(2, 10, i+1)
    plt.imshow(pred[i, ..., 0], cmap='gray')  
plt.show()
Test Images

testafbeeldingen

Reconstruction of Test Images

reconstructie van testafbeeldingen

Uit de bovenstaande figuren kun je afleiden dat je model fantastisch werk heeft geleverd bij het reconstrueren van de testafbeeldingen die je met het model hebt voorspeld. Althans visueel gezien lijken de test- en gereconstrueerde afbeeldingen bijna exact op elkaar.

Denoising auto-encoder

Een denoising auto-encoder probeert een representatie (latente ruimte of bottleneck) te leren die robuust is tegen ruis. Je voegt ruis toe aan een afbeelding en voert vervolgens de ruisachtige afbeelding als input aan het encooder-gedeelte van je netwerk. Het encoder-gedeelte van de auto-encoder transformeert de afbeelding naar een andere ruimte die de letters probeert te behouden maar de ruis verwijdert.

Maar hoe verwijdert het de ruis precies?

Tijdens het trainen definieer je een verliesfunctie, vergelijkbaar met de root mean squared error die je eerder in de convolutionele auto-encoder had gedefinieerd. Bij elke iteratie van de training berekent het netwerk een verlies tussen de ruisachtige afbeelding die door de decoder wordt teruggegeven en de ground truth (denoisy-afbeelding) en probeert het dat verlies of verschil tussen de gereconstrueerde afbeelding en de originele ruisvrije afbeelding te minimaliseren. Met andere woorden: het netwerk leert een 7 x 7 x 128-ruimte die ruisvrije encodings van de data bevat waarop je het netwerk traint!

Implementatie van denoising auto-encoder

Om te zien hoe dit in Python werkt, gebruik je dezelfde NotMNIST-dataset als in het eerste deel van deze tutorial. Dat betekent dat je geen extra datapreprocessing hoeft te doen, want dat is al gedaan! Eén belangrijke preprocessingstap in dit deel is echter het toevoegen van ruis aan de train-, validatie- en testafbeeldingen. Laten we dat eerst snel doen!

Ruis toevoegen aan afbeeldingen

Laten we eerst een noise factor definiëren, een hyperparameter. De noise factor wordt vermenigvuldigd met een willekeurige matrix met een gemiddelde van 0,0 en standaarddeviatie van 1,0. Deze matrix trekt monsters uit een normale (Gaussische) verdeling. De vorm van de willekeurige normale array zal gelijk zijn aan de vorm van de data waaraan je de ruis toevoegt.

Ter vereenvoudiging: de variabele train_X heeft de vorm 48000 x 28 x 28 x 1. De willekeurige normale array zal dus ook een vorm hebben die lijkt op train_X; alleen dan kun je twee arrays optellen, omdat ze dan dezelfde dimensies hebben.

noise_factor = 0.5
x_train_noisy = train_X + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=train_X.shape)
x_valid_noisy = valid_X + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=valid_X.shape)
x_test_noisy = test_data + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=test_data.shape)
x_train_noisy = np.clip(x_train_noisy, 0., 1.)
x_valid_noisy = np.clip(x_valid_noisy, 0., 1.)
x_test_noisy = np.clip(x_test_noisy, 0., 1.)

np.clip() zal alle negatieve waarden op nul afkappen en alle waarden groter dan één op één. Je wilt immers dat je pixelwaarden tussen nul en één liggen. En er is een kans dat na het introduceren van ruis in de data het bereik van pixelwaarden iets verandert, dus om veilig te zijn is het een goede gewoonte om de pixelwaarden te clippen.

De ruisachtige afbeeldingen visualiseren

plt.figure(figsize=[5,5])

# Display the first image in training data
plt.subplot(121)
curr_img = np.reshape(x_train_noisy[1], (28,28))
plt.imshow(curr_img, cmap='gray')

# Display the first image in testing data
plt.subplot(122)
curr_img = np.reshape(x_test_noisy[1], (28,28))
plt.imshow(curr_img, cmap='gray')
<matplotlib.image.AxesImage at 0x7f3ec6b20e48>

ruisachtige afbeeldingen

Nu je de ruisachtige data hebt, kun je die in het netwerk voeren en zien hoe de ruis "magisch" uit de afbeeldingen wordt verwijderd!

Denoising auto-encoder-netwerk

Zoals eerder besproken, is de auto-encoder verdeeld in twee delen: de encoder en de decoder. De architectuur die je gaat bouwen, ziet er als volgt uit:

Encoder

  • De eerste laag heeft 32-3 x 3-filters, gevolgd door een downsampling- (max-pooling-)laag,
  • De tweede laag heeft 64-3 x 3-filters, gevolgd door nog een downsampling-laag,
  • De laatste laag van de encoder heeft 128-3 x 3-filters.

Decoder

  • De eerste laag heeft 128-3 x 3-filters, gevolgd door een upsampling-laag,
  • De tweede laag heeft 64-3 x 3-filters, gevolgd door nog een upsampling-laag,
  • De laatste laag van de encoder heeft 1-3 x 3-filter.
batch_size = 128
epochs = 20
inChannel = 1
x, y = 28, 28
input_img = Input(shape = (x, y, inChannel))
def autoencoder(input_img):
    #encoder
    conv1 = Conv2D(32, (3, 3), activation='relu', padding='same')(input_img)
    pool1 = MaxPooling2D(pool_size=(2, 2))(conv1)
    conv2 = Conv2D(64, (3, 3), activation='relu', padding='same')(pool1)
    pool2 = MaxPooling2D(pool_size=(2, 2))(conv2)
    conv3 = Conv2D(128, (3, 3), activation='relu', padding='same')(pool2)

    #decoder
    conv4 = Conv2D(128, (3, 3), activation='relu', padding='same')(conv3)
    up1 = UpSampling2D((2,2))(conv4)
    conv5 = Conv2D(64, (3, 3), activation='relu', padding='same')(up1)
    up2 = UpSampling2D((2,2))(conv5)
    decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(up2)
    return decoded
autoencoder = Model(input_img, autoencoder(input_img))
autoencoder.compile(loss='mean_squared_error', optimizer = RMSprop())

Trainen

Als je je herinnert: bij het trainen van de convolutionele auto-encoder voerde je de trainingsafbeeldingen twee keer in, omdat de input en de ground truth hetzelfde waren. Bij de denoising auto-encoder voer je echter de ruisachtige afbeeldingen als input, terwijl je ground truth de denoisy-afbeeldingen blijven waarop je de ruis had toegepast. Alleen dan kan het netwerk een verlies berekenen tussen de ruisachtige en de echte denoisy-afbeeldingen.

autoencoder_train = autoencoder.fit(x_train_noisy, train_X, batch_size=batch_size,epochs=epochs,verbose=1,validation_data=(x_valid_noisy, valid_X))
Train on 48000 samples, validate on 12000 samples
Epoch 1/20
48000/48000 [==============================] - 15s - loss: 0.0531 - val_loss: 0.0268
Epoch 2/20
48000/48000 [==============================] - 14s - loss: 0.0243 - val_loss: 0.0217
Epoch 3/20
48000/48000 [==============================] - 14s - loss: 0.0207 - val_loss: 0.0200
Epoch 4/20
48000/48000 [==============================] - 14s - loss: 0.0190 - val_loss: 0.0184
Epoch 5/20
48000/48000 [==============================] - 14s - loss: 0.0179 - val_loss: 0.0183
Epoch 6/20
48000/48000 [==============================] - 14s - loss: 0.0171 - val_loss: 0.0178
Epoch 7/20
48000/48000 [==============================] - 14s - loss: 0.0165 - val_loss: 0.0161
Epoch 8/20
48000/48000 [==============================] - 14s - loss: 0.0160 - val_loss: 0.0166
Epoch 9/20
48000/48000 [==============================] - 14s - loss: 0.0157 - val_loss: 0.0157
Epoch 10/20
48000/48000 [==============================] - 14s - loss: 0.0153 - val_loss: 0.0159
Epoch 11/20
48000/48000 [==============================] - 14s - loss: 0.0151 - val_loss: 0.0153
Epoch 12/20
48000/48000 [==============================] - 14s - loss: 0.0148 - val_loss: 0.0154
Epoch 13/20
48000/48000 [==============================] - 14s - loss: 0.0146 - val_loss: 0.0151
Epoch 14/20
48000/48000 [==============================] - 14s - loss: 0.0145 - val_loss: 0.0152
Epoch 15/20
48000/48000 [==============================] - 14s - loss: 0.0143 - val_loss: 0.0163
Epoch 16/20
48000/48000 [==============================] - 14s - loss: 0.0141 - val_loss: 0.0152
Epoch 17/20
48000/48000 [==============================] - 14s - loss: 0.0140 - val_loss: 0.0149
Epoch 18/20
48000/48000 [==============================] - 14s - loss: 0.0139 - val_loss: 0.0153
Epoch 19/20
48000/48000 [==============================] - 14s - loss: 0.0138 - val_loss: 0.0152
Epoch 20/20
48000/48000 [==============================] - 14s - loss: 0.0137 - val_loss: 0.0150

Train versus validatie-verliesdiagram

loss = autoencoder_train.history['loss']
val_loss = autoencoder_train.history['val_loss']
epochs = range(epochs)
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()

training en validatie verliesgrafiek 2

Uit de bovenstaande grafiek kun je afleiden dat het model bij sommige epochs overfit, terwijl het het grootste deel van de tijd in de pas loopt. Je kunt de prestaties van het model zeker proberen te verbeteren door er wat complexiteit aan toe te voegen zodat het verlies verder kan dalen; train het langer en beslis dan.

Voorspellen op de testdataset

pred = autoencoder.predict(x_test_noisy)
plt.figure(figsize=(20, 4))
print("Test Images")
for i in range(10,20,1):
    plt.subplot(2, 10, i+1)
    plt.imshow(test_data[i, ..., 0], cmap='gray')
    curr_lbl = test_labels[i]
    plt.title("(Label: " + str(label_dict[curr_lbl]) + ")")
plt.show()    
plt.figure(figsize=(20, 4))
print("Test Images with Noise")
for i in range(10,20,1):
    plt.subplot(2, 10, i+1)
    plt.imshow(x_test_noisy[i, ..., 0], cmap='gray')
plt.show()    

plt.figure(figsize=(20, 4))
print("Reconstruction of Noisy Test Images")
for i in range(10,20,1):
    plt.subplot(2, 10, i+1)
    plt.imshow(pred[i, ..., 0], cmap='gray')  
plt.show()
Test Images

Testafbeeldingen 2

Test Images with Noise

Testafbeeldingen met ruis

Reconstruction of Noisy Test Images

Reconstructie van ruisachtige testafbeeldingen

Het lijkt erop dat een vrij eenvoudige netwerkarchitectuur in slechts 20 epochs behoorlijk goed werk heeft geleverd bij het verwijderen van ruis uit de testafbeeldingen, of niet?

Ga verder!

Deze tutorial was een goede start om auto-encoders zowel theoretisch als praktisch te begrijpen. Als je het makkelijk kon volgen of zelfs met een beetje extra moeite: goed gedaan! Bekijk DataCamp's Keras Tutorial: Deep Learning in Python-tutorial en Introduction to Deep Learning in Python-cursus.

In de volgende tutorial leer je hoe je afbeeldingen from scratch inleest, analyseert, preprocesset en ze invoert in het model met behulp van een vingerafdrukdataset. Je leert ook hoe je medische beelden van T-1-modus inleest en reconstrueert met een auto-encoder!

Er is nog veel te behandelen, dus waarom zou je DataCamp's Deep Learning in Python-cursus niet volgen, als je dat nog niet hebt gedaan? Je leert vanaf de basis en groeit langzaam naar het beheersen van het deep learning-domein. Het zal ongetwijfeld een onmisbare bron zijn wanneer je leert werken met convolutionele neurale netwerken in Python, hoe je gezichten, objecten enz. detecteert.

Onderwerpen

Gerelateerd