# -*- coding: utf-8 -*-
"""Tut5 - Autoencoder_dim_Reduction.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1a3w3t01APX9l9RoRbJT9H4Frw3bUv1hK

#Autoencoder With Neural Networks

"Autoencoding" is a data compression algorithm where the compression and decompression functions are 1) data-specific, 2) lossy, and 3) learned automatically from examples rather than engineered by a human. Additionally, in almost all contexts where the term "autoencoder" is used, the compression and decompression functions are implemented with unsupervised neural networks (i.e., no class labels or labeled data).

1) Autoencoders are data-specific, which means that they will only be able to compress data similar to what they have been trained on. This is different from, say, the MPEG-2 Audio Layer III (MP3) compression algorithm, which only holds assumptions about "sound" in general, but not about specific types of sounds.An autoencoder trained on pictures of faces would do a rather poor job of compressing pictures of trees, because the features it would learn would be face-specific.

2) Autoencoders are lossy, which means that the decompressed outputs will be degraded compared to the original inputs 

3) Autoencoders are learned automatically from data examples, which is a useful property: it means that it is easy to train specialized instances of the algorithm that will perform well on a specific type of input. It doesn't require any new engineering, just appropriate training data.

To build an autoencoder, you need three things: an encoding function, a decoding function, and a distance function between the amount of information loss between the compressed representation of your data and the decompressed representation (i.e. a "loss" function). 

Autoencoders are typically used for:

* Dimensionality reduction (i.e., think PCA but more powerful/intelligent).
* Denoising (ex., removing noise and preprocessing images to improve OCR accuracy)
* Anomaly/outlier detection (ex., detecting mislabeled data points in a dataset or detecting when an input data point falls well outside our typical data distribution).

Today two interesting practical applications of autoencoders are data denoising (which we feature later in this post), and dimensionality reduction for data visualization. With appropriate dimensionality and sparsity constraints, autoencoders can learn data projections that are more interesting than PCA or other basic techniques.

![alt text](https://www.pyimagesearch.com/wp-content/uploads/2020/02/keras_autoencoder_arch_flow.png)

We input a digit to the autoencoder.
The encoder subnetwork creates a latent representation of the digit. This latent representation is substantially smaller (in terms of dimensionality) than the input.
The decoder subnetwork then reconstructs the original digit from the latent representation.
"""

#Import libraries for neural network, numpy for matrix operations, and matplotlib for visualization
#Keras library is a high level API for deep leanring and neural networks

from keras.datasets import mnist
from keras.layers import Input, Dense
from keras.models import Model
from keras import backend as b
from keras import initializers,regularizers
from keras.optimizers import SGD,Adam
import numpy as np
import matplotlib.pyplot as plt

"""Loading the MNIST dataset images and not their labels. We want to reconstruct the images as output of the autoencoder and hence we do not need labels.
Creating a training set and test set and normalizing the data for better training.
"""

(X_train, _), (X_test, _) = mnist.load_data()

X_train = X_train.astype('float32')/255.
X_test = X_test.astype('float32')/255.

X_train = X_train.reshape(len(X_train), np.prod(X_train.shape[1:]))
X_test = X_test.reshape(len(X_test), np.prod(X_test.shape[1:]))
print(X_train.shape)
print(X_test.shape)

"""Our input image is a 2D matrix of size 28x28. When we represented it as a vector it has a dimension of 784. We will encode it to 32 and the decode it back to the original dimension of 784

![alt text](https://miro.medium.com/max/806/1*Widu_pWRjbGvbbmZ5IiT4Q.png)

Encoded image will have a compression rate of 784/32=24.5

![alt text](https://wiki.pathmind.com/images/wiki/deep_autoencoder.png)
"""

b.clear_session()
#We need to take the input image of dimension 784 and convert it to keras tensors.
input_img= Input(shape=(784,))
#We now create the encoder and the decoder based on the figure above.
#Input image will be Encoded to 32 units and the activation will be relu and the input is the tensors of input image
encoded = Dense(units=32, activation='relu')(input_img)
#Decoder will have 784 units as it needs to reconstruct the input image back to its original dimension.
#As decoded is the output layer of the autoencoder we will use sigmoid activation function. encoded image will be the input to the decoder
#Sigmoid allows us to model the output as the presence of a white pixel or not. 
decoded = Dense(units=784, activation='sigmoid')(encoded)
#We now create the autoencoder with input as the input image and output as the decoder
autoencoder=Model(input_img, decoded)
autoencoder.summary()

"""We train the autoencoder end to end, which has both the encoder and decoder parts. """

#We can also extract the encoder which takes input as input images and the output is the encoded image.
encoder = Model(input_img, encoded)
#let see the structure of the encoded model
encoder.summary()
#We now compile the autoencoder model with adadelta optimizer. 
#As pixels have a value of 0 or 1 we use binary_crossentropy as the loss function and the metrics used for the performance of the model is accuracy
#i.e., the number of pixels that are correctly predicted. Alternativelly, we can think of each output node as the probability of having a white pixel. 
autoencoder.compile(optimizer='adam', loss='binary_crossentropy', metrics=['binary_accuracy'])

#We now train the autoencoder using the training data with 20 epochs and batch size of 256. We also shuffle the training data
autoencoder.fit(X_train, X_train,
                epochs=20,
                batch_size=256,
                shuffle=True,
                validation_data=(X_test, X_test)
                )

"""During training it is important to look at the loss. It should decrease. Similarly we should observe that the accuracy has increased.
Finally, you should also keep an eye at validation losses and accuracies that they follow the same trends to identify overfitting.

#Visualizing the Encoding

Predicting the test set. We want to view the encoded images as well as the reconstructed images so we fit the test data on both autoencoder as well as encoder
"""

encoded_imgs = encoder.predict(X_test)
predicted = autoencoder.predict(X_test)

"""Let’s plot the original input, encoded images and the reconstructed images using matplotlib

"""

plt.figure(figsize=(40, 4))
for i in range(10):
    # display original
    ax = plt.subplot(3, 20, i + 1)
    plt.imshow(X_test[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
    
    # display encoded image
    ax = plt.subplot(3, 20, i + 1 + 20)
    plt.imshow(encoded_imgs[i].reshape(8,4))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
    # display reconstruction
    ax = plt.subplot(3, 20, 2*20 +i+ 1)
    plt.imshow(predicted[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
    
plt.show()

"""Notice that the outputs are similar with the input. Furthermore, encodings of similar numbers seem to share some patterns. The outputs are fuzzy though. We could try a deeper network or train for longer.

The representations learned by autoencoders can be used for downstream tasks such as classification or regression. Furthermore, autoencoder variants provide ways to model and learn distributions so that they can be used as generative models (e.g.,  Variational autoencoders (VAEs)).
"""

