01 · AutoencodersPrimer 1 · 3 min

Compress. Reconstruct. Compare.

Why this mattersLearn useful image features without class labels.

Try thisFollow one image. Notice where its information is compressed.

MNIST input digit 3Input · 784 pixels
PCA reconstruction of the same digitReconstruction

Visual example: a linear autoencoder (PCA). The notebook trains a neural autoencoder.

Follow one image

Encode

The encoder turns 784 pixel values into 32 numbers. These numbers form the latent code.

The training target is the input image itself. Digit labels are not needed.

Check your understanding

What stops the network from simply copying every pixel?

Two views of an autoencoder
Keenan Crane · CC0. Left: spaces and maps. Right: network layers.
Keenan Crane · CC0. Left: spaces and maps. Right: network layers.
Keras autoencoder diagram.
Keras autoencoder diagram.
Notebook source · 5 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 01 · Notebook text
# COMP90086 Workshop 9
Cell 02 · Notebook text
In this workshop, we will discuss generative models for images.

Table of Contents

- Autoencoders
    - Basic Autoencoder
    - Convolutional Autoencoder for Image Denoising
    - (Extra bonus) Convolutional Variational Autoencoder 
- GANs
    - Play with GANs in your browser
    - DCGAN
Cell 03 · Python · import matplotlib.pyplot as plt
PYTHON · RUN IN JUPYTER
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
import os
import time

from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.models import Model

from IPython import display

print("TensorFlow version: ", tf.__version__)
Stored output
TensorFlow version:  2.17.0
Cell 04 · Notebook text
# 1. Autoencoders

Adapted from the Keras Blog and TensorFlow.org tutorials:
<table class="tfo-notebook-buttons" align="left">
    <td>
    <a target="_blank" href="https://blog.keras.io/building-autoencoders-in-keras.html">
    <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/ae/Keras_logo.svg/240px-Keras_logo.svg.png" width=32/>
    View source on blog.keras.io</a>
  </td>   
    <td>
    <a target="_blank" href="https://www.tensorflow.org/tutorials/generative/autoencoder">
    <img src="https://www.tensorflow.org/images/tf_logo_32px.png" />
    View source on TensorFlow.org</a>
  </td>
  <td>
    <a target="_blank" href="https://colab.research.google.com/github/tensorflow/docs/blob/master/site/en/tutorials/generative/autoencoder.ipynb">
    <img src="https://www.tensorflow.org/images/colab_logo_32px.png" />
    Run in Google Colab</a>
  </td>
  <td>
    <a target="_blank" href="https://github.com/tensorflow/docs/blob/master/site/en/tutorials/generative/autoencoder.ipynb">
    <img src="https://www.tensorflow.org/images/GitHub-Mark-32px.png" />
    View source on GitHub</a>
  </td>
</table>
Cell 05 · Notebook text
An autoencoder is a special type of neural network that is trained to copy its input to its output. For example, given an image of a handwritten digit, an autoencoder first encodes the image into a lower dimensional latent representation, then decodes the latent representation back to an image. An autoencoder learns to compress the data while minimizing the reconstruction error. 

![autoencoder](https://blog.keras.io/img/ae/autoencoder_schema.jpg)
<center>Image source: the Keras Blog</center>

We will introduce autoencoders with three examples: basic autoencoder, image denoising, and convolutional variational autoencoder.
01 · AutoencodersPrimer 2 · 4 min

What does the code represent?

Why this mattersUnderstand which images the decoder can produce.

Try thisSelect high and low density. Compare how much training evidence each region has.

Representation diagram
Data space manifold and latent space of an autoencoder

Select a label to explore the diagram. Figures: Keenan Crane, CC0.

Read the representation diagram

Image x

Each 28 × 28 image is a point in a space with 784 coordinates.

The maps and their limits

Encoder: $z=f_\theta(x)$. Decoder: $\hat{x}=g_\phi(z)$.

$$\mathcal M=g_\phi(\mathbb R^k),\qquad \dim(\mathcal M)\leq k$$

Training encourages $g_\phi(f_\theta(x))\approx x$ on the data. It does not guarantee an identity map everywhere on $\mathcal M$.

The output lies on the decoder manifold. The input may not. Zero training loss alone does not prove overfitting; compare held-out results.

High and low density have no hard boundary. A basic autoencoder does not define a simple sampling distribution.

Check your understanding

Why can a random latent code produce a poor image?

01 · AutoencodersNotebook lab · 3 min

Prepare the digit images

Why this mattersGive the network consistent pixel values and shapes.

Try thisFollow one image from 28 × 28 pixels to a 784-value vector.

60,000training images
10,000test images
28 × 28greyscale pixels
From an image to a training vector

Load

Load the training and test splits with keras.datasets.mnist.load_data(). Labels range from 0 to 9.

MNIST examples from the notebook. Top: test images. Bottom: reconstructions.
MNIST examples from the notebook. Top: test images. Bottom: reconstructions.
Setup and data code
PYTHON · RUN IN JUPYTER
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
import os
import time

from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.models import Model

from IPython import display

print("TensorFlow version: ", tf.__version__)
PYTHON · RUN IN JUPYTER
# Since we only need images from the dataset to encode and decode,
# we won't use the labels.
(train_data, _), (test_data, test_labels) = keras.datasets.mnist.load_data()

# Normalize and reshape the data
train_data = train_data.astype('float32') / 255.
test_data = test_data.astype('float32') / 255.

x_train = train_data.reshape((len(train_data), np.prod(train_data.shape[1:])))
x_test = test_data.reshape((len(test_data), np.prod(test_data.shape[1:])))

print("train_images shape:", x_train.shape)
print("test_images shape:", x_test.shape)
Check your understanding

Why are the labels kept out of the training target?

Notebook source · 3 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 06 · Notebook text
## Load the MNIST dataset and Prepare the data
We will train the basic autoencoder using the MNIST dataset. 

MNIST is a dataset that consists of images of handwritten digits:
* the input data are images of handwritten digits (28×28 pixels with a single 8-bit channel)
* the target is a label in the set $\{0, 1, \ldots, 9\}$

The data is already split into training and test sets. The training set contains 60,000 instances and the test set contains 10,000 instances.
Cell 07 · Notebook text
Let's prepare our input data. We're using MNIST digits. We load the data into NumPy arrays using a built-in function from Keras, and we're discarding the labels (since we're only interested in encoding/decoding the input images).
Cell 08 · Python · (train_data, _), (test_data, test_labels) = keras.datasets.mnist.load_
PYTHON · RUN IN JUPYTER
# Since we only need images from the dataset to encode and decode,
# we won't use the labels.
(train_data, _), (test_data, test_labels) = keras.datasets.mnist.load_data()

# Normalize and reshape the data
train_data = train_data.astype('float32') / 255.
test_data = test_data.astype('float32') / 255.

x_train = train_data.reshape((len(train_data), np.prod(train_data.shape[1:])))
x_test = test_data.reshape((len(test_data), np.prod(test_data.shape[1:])))

print("train_images shape:", x_train.shape)
print("test_images shape:", x_test.shape)
Stored output
train_images shape: (60000, 784)
test_images shape: (10000, 784)
01 · AutoencodersNotebook lab · 5 min

Train the basic autoencoder

Why this mattersCheck whether training improves reconstruction on held-out images.

Try thisPlay the loss chart. Compare the training and validation curves.

Build and train

784 → 32

Dense(32, activation="relu") produces the latent code.

Watch the loss fallRecorded notebook values
1 / 20

BCE and MSE

Why this formula? Use these losses to compare predicted pixels with target pixels. A smaller value means a closer reconstruction under that loss.

Both losses measure reconstruction error. This notebook uses BCE with sigmoid outputs.

$$\mathrm{BCE}=-\frac1D\sum_{d=1}^D[x_d\log\hat{x}_d+(1-x_d)\log(1-\hat{x}_d)]$$$$\mathrm{MSE}=\frac1D\sum_{d=1}^D(x_d-\hat{x}_d)^2$$

$D=784$ pixels. BCE treats each pixel as a Bernoulli mean. MSE is the squared pixel error.

Implementation
Hourglass autoencoder with mean squared reconstruction loss
Input $x$ is compared to output $\hat{x}$. That self-comparison is the "auto" in autoencoder.
Keras blog schema: digit, encoder, code, decoder, reconstructed digit
Francois Chollet, Keras blog. Same story: compress, then reconstruct.
Check your understanding

What do you pass as x and y in fit()?

Notebook source · 11 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 09 · Notebook text
## (1) Basic autoencoder

Let's start simple, with a single fully-connected neural layer as encoder and as decoder.


Define an autoencoder with two Dense layers: an `encoder`, which compresses the images into a 32 dimensional latent vector, and a `decoder`, that reconstructs the original image from the latent space.
Cell 10 · Python · latent_dim = 32 # 32 floats -> compression of factor 24.5, assuming t
PYTHON · RUN IN JUPYTER
# This is the size of our encoded representations
latent_dim = 32  # 32 floats -> compression of factor 24.5, assuming the input is 784 floats

# This is our input image
input_img = keras.Input(shape=(784,))
# "encoded" is the encoded representation of the input
encoded = layers.Dense(latent_dim, activation='relu')(input_img)
# "decoded" is the lossy reconstruction of the input
decoded = layers.Dense(784, activation='sigmoid')(encoded)

# This model maps an input to its reconstruction
autoencoder = keras.Model(input_img, decoded)
Cell 11 · Notebook text
Let's also create a separate encoder model:
Cell 12 · Python · encoder = keras.Model(input_img, encoded)
PYTHON · RUN IN JUPYTER
# This model maps an input to its encoded representation
encoder = keras.Model(input_img, encoded)
Cell 13 · Notebook text
As well as the decoder model:
Cell 14 · Python · encoded_input = keras.Input(shape=(latent_dim,))
PYTHON · RUN IN JUPYTER
# This is our encoded (32-dimensional) input
encoded_input = keras.Input(shape=(latent_dim,))
# Retrieve the last layer of the autoencoder model
decoder_layer = autoencoder.layers[-1]
# Create the decoder model
decoder = keras.Model(encoded_input, decoder_layer(encoded_input))
Cell 15 · Notebook text
Now let's train our autoencoder to reconstruct MNIST digits.

First, we'll configure our model to use a per-pixel binary crossentropy loss, and the Adam optimizer:
Cell 16 · Python · autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
PYTHON · RUN IN JUPYTER
autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
Cell 17 · Notebook text
Now let's train our autoencoder for 20 epochs:
Cell 18 · Python · autoencoder.fit(x_train, x_train,
PYTHON · RUN IN JUPYTER
autoencoder.fit(x_train, x_train,
                epochs=20,
                batch_size=256,
                shuffle=True,
                validation_data=(x_test, x_test))
Stored output
Epoch 1/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.3786 - val_loss: 0.1932
Epoch 2/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.1828 - val_loss: 0.1548
Epoch 3/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.1506 - val_loss: 0.1345
Epoch 4/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.1324 - val_loss: 0.1215
Epoch 5/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.1209 - val_loss: 0.1131
Epoch 6/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.1133 - val_loss: 0.1076
Epoch 7/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.1079 - val_loss: 0.1037
Epoch 8/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.1041 - val_loss: 0.1006
Epoch 9/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.1013 - val_loss: 0.0984
Epoch 10/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.0993 - val_loss: 0.0968
Epoch 11/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.0979 - val_loss: 0.0957
Epoch 12/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.0970 - val_loss: 0.0951
Epoch 13/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.0963 - val_loss: 0.0947
Epoch 14/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.0959 - val_loss: 0.0943
Epoch 15/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.0955 - val_loss: 0.0941
Epoch 16/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.0953 - val_loss: 0.0939
Epoch 17/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.0950 - val_loss: 0.0938
Epoch 18/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.0950 - val_loss: 0.0936
Epoch 19/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.0950 - val_loss: 0.0936
Epoch 20/20
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - loss: 0.0948 - val_loss: 0.0935
<keras.src.callbacks.history.History at 0x30d6affd0>
Cell 19 · Notebook text
After 20 epochs, the autoencoder seems to reach a stable train/validation loss value of about 0.09. We can try to visualize the reconstructed inputs and the encoded representations.
01 · AutoencodersNotebook lab · 4 min

Read the reconstructions and codes

Why this mattersCheck what the model preserves and how it groups similar inputs.

Try thisCompare original and reconstructed strokes. Then inspect nearby colours in t-SNE.

Top: original test digits. Bottom: neural autoencoder reconstructions after 20 epochs.
Top: original test digits. Bottom: neural autoencoder reconstructions after 20 epochs.
Inspect the result

Encode test data

Run encoder.predict(x_test). Each image becomes a vector with 32 values.

Stored t-SNE of the 32-D test codes. Colour indicates the digit label.
Stored t-SNE of the 32-D test codes. Colour indicates the digit label.

t-SNE is a view of the code, not the code itself. Large distances and cluster sizes can be misleading.

A linear baseline: PCA

A linear autoencoder trained with squared error can learn the PCA subspace. This result does not apply to every autoencoder loss.

PCA with k = 2. This differs from the neural model in both dimension and model type.
PCA with k = 2. This differs from the neural model in both dimension and model type.
Decode a grid of 2-D PCA codes. This is a linear baseline, not a VAE.
Decode a grid of 2-D PCA codes. This is a linear baseline, not a VAE.
Explained variance in the PCA baseline.
Explained variance in the PCA baseline.
Check your understanding

Do overlapping colours mean the autoencoder failed?

Notebook source · 7 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 20 · Python · encoded_imgs = encoder.predict(x_test)
PYTHON · RUN IN JUPYTER
# Encode and decode some digits
# Note that we take them from the *test* set
encoded_imgs = encoder.predict(x_test)
decoded_imgs = decoder.predict(encoded_imgs)
Stored output
313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 261us/step
313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 291us/step
Cell 21 · Notebook text
Define a `display` function to display ten random images.
Cell 22 · Python · def display(array1, array2):
PYTHON · RUN IN JUPYTER
def display(array1, array2):
    """
    Displays ten random images from each one of the supplied arrays.
    """

    n = 10 # How many digits we will display

    indices = np.random.randint(len(array1), size=n)
    images1 = array1[indices, :]
    images2 = array2[indices, :]

    plt.figure(figsize=(20, 4))
    for i, (image1, image2) in enumerate(zip(images1, images2)):
        # Display original
        ax = plt.subplot(2, n, i + 1)
        plt.imshow(image1.reshape(28, 28))
        plt.title("original")
        plt.gray()
        ax.get_xaxis().set_visible(False)
        ax.get_yaxis().set_visible(False)
        
        # Display reconstruction
        ax = plt.subplot(2, n, i + 1 + n)
        plt.imshow(image2.reshape(28, 28))
        plt.gray()
        plt.title("reconstructed")
        ax.get_xaxis().set_visible(False)
        ax.get_yaxis().set_visible(False)

    plt.show()
Cell 23 · Python · display(x_test, decoded_imgs)
PYTHON · RUN IN JUPYTER
# Display the test data and the reconstructed results
display(x_test, decoded_imgs)
Stored output
Stored notebook output · cell 23
Stored notebook output · cell 23
<Figure size 2000x400 with 20 Axes>
Cell 24 · Notebook text
Here's what we get. The top row is the original digits, and the bottom row is the reconstructed digits. The autoencoder is able to recreate digits quite accurately, although there is some information loss.
Cell 25 · Notebook text
### Visualise the latent space

The autoencoder has learned a latent representation of the images, which is a compressed representation (in this case, just 32 values) that represent the variation in the MNIST images. We can use [TSNE](https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html) to visualise the latent space. This is a form of dimensionality-reduction that tries to preserve the distances between points as much as possible -- points that are nearby in the latent space will be nearby in the 2D plot. Different digits are shown in different colours.
Cell 26 · Python · from sklearn.manifold import TSNE
PYTHON · RUN IN JUPYTER
from sklearn.manifold import TSNE

# using t-SNE to visualise the latent space 
tsne = TSNE(n_components=2) 
X_tsne = tsne.fit_transform(encoded_imgs) 

plt.figure(figsize=(10, 6))
plt.scatter(X_tsne[:,0], X_tsne[:,1], c= test_labels, cmap='rainbow')
plt.colorbar()
plt.xticks([])
plt.yticks([])
plt.show()
Stored output
Stored notebook output · cell 26
Stored notebook output · cell 26
<Figure size 1000x600 with 2 Axes>
01 · AutoencodersExercise 1 · 5 min

How small can the code be?

Why this mattersChoose a code size that balances detail and compression.

Try thisReduce k. Predict which stroke details will disappear.

Explore the bottleneck: $784\to k\to 784$ PCA teaching demo

Change k below. Watch the reconstructed digit and its pixel error. All values in this visual come from PCA.

Workshop bottleneckDrag $k$
32
784input pixels
32latent values
24.5compression $n/k$

The notebook uses k = 32. This interactive example uses PCA.

Run the notebook experiment

Choose k

Try 2, 8, 16, 32, and 64. Change latent_dim before building the model.

Exercise 1 · predict, then compare

How does k affect reconstruction and digit clusters? Why?

Compare all five PCA results
PCA reconstructions for k = 2, 8, 16, 32, and 64. These are not retrained neural models.
PCA reconstructions for k = 2, 8, 16, 32, and 64. These are not retrained neural models.
Notebook source · 2 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 27 · Notebook text
### Exercise 1: Investigate the latent space representation of MNIST digits

Try changing the size of the latent space. How does this affect the reconstruction results and the organisation of digits in the latent space? Why?

You may notice the representations of different digits overlap in the latent space, particularly when the latent space is very small. Which digits cluster together in the latent space? Do these clusters make sense?
Cell 28 · Python · Exercise space
PYTHON · RUN IN JUPYTER
# Empty exercise cell in the source notebook.
02 · DenoisingNotebook lab · 3 min

Add noise. Keep the target clean.

Why this mattersCreate training pairs that teach the model to remove noise.

Try thisIncrease the noise. Notice that the clean target stays fixed.

Corrupt a digitnotebook uses $0.4$
0.40

Left: clean $x$. Middle: noisy $\tilde{x}$. Right: the target is still $x$, not $\tilde{x}$.

Make a noisy training pair

Reshape

Use images with shape (N, 28, 28, 1). Convolutions preserve the image layout.

Noise equation
$$\tilde{x}=\mathrm{clip}(x+0.4\varepsilon,0,1),\qquad\varepsilon\sim\mathcal N(0,I)$$
PYTHON · RUN IN JUPYTER
def noise(array):
    """
    Adds random noise to each image in the supplied array.
    """

    noise_factor = 0.4
    noisy_array = array + noise_factor * np.random.normal(
        loc=0.0, scale=1.0, size=array.shape
    )

    return np.clip(noisy_array, 0.0, 1.0)
Notebook output. Top: clean images. Bottom: noisy inputs. The reused plot title says “reconstructed”.
Notebook output. Top: clean images. Bottom: noisy inputs. The reused plot title says “reconstructed”.
Check your understanding

If the input is noisy, should the target also be noisy?

Notebook source · 4 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 29 · Notebook text
## (2) Convolutional autoencoder for image denoising

An autoencoder can also be trained to remove noise from images. In the following section, you will create a noisy version of the MNIST dataset by applying random noise to each image. You will then train a deep convolutional autoencoder for image denoising, mapping noisy digits images from the MNIST dataset to clean digits images.

Below we define a `noise` function to add random noise to each image.
Cell 30 · Python · def noise(array):
PYTHON · RUN IN JUPYTER
def noise(array):
    """
    Adds random noise to each image in the supplied array.
    """

    noise_factor = 0.4
    noisy_array = array + noise_factor * np.random.normal(
        loc=0.0, scale=1.0, size=array.shape
    )

    return np.clip(noisy_array, 0.0, 1.0)
Cell 31 · Notebook text
Let's reuse the dataset we loaded earlier.
Cell 32 · Python · x_train = np.reshape(train_data, (len(train_data), 28, 28, 1))
PYTHON · RUN IN JUPYTER
# Reshape the data
x_train = np.reshape(train_data, (len(train_data), 28, 28, 1))
x_test = np.reshape(test_data, (len(test_data), 28, 28, 1))

# Create a copy of the data with added noise
noisy_train_data = noise(x_train)
noisy_test_data = noise(x_test)

# Display the train data and a version of it with added noise
display(x_train, noisy_train_data)
Stored output
Stored notebook output · cell 32
Stored notebook output · cell 32
<Figure size 2000x400 with 20 Axes>
02 · DenoisingNotebook lab · 4 min

Learn to remove the noise

Why this mattersRecover image structure from a corrupted input.

Try thisFollow the feature-map sizes. Compare noisy inputs with the clean estimates.

Follow the convolutional shapes

28 × 28 × 1

Input: one noisy greyscale image. A 3 × 3 convolution produces 32 feature maps.

Stored result after two epochs. Top: noisy test images. Bottom: predicted clean images.
Stored result after two epochs. Top: noisy test images. Bottom: predicted clean images.
2training epochs
128batch size
0.0970final validation BCE

Fit noisy_train_data → x_train. Validate noisy_test_data → x_test. Training may take time; use the stored result to discuss the outcome.

Check your understanding

How does this task differ from the basic autoencoder?

Notebook source · 5 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 33 · Notebook text
Can our autoencoder learn to recover the original digits? Let's find out.

**Build the autoencoder.**

Compared to the previous convolutional autoencoder, in order to improve the quality of the reconstructed, we'll use a slightly different model with more filters per layer:

We are going to use the Functional API to build our convolutional autoencoder.
Cell 34 · Python · input = layers.Input(shape=(28, 28, 1))
PYTHON · RUN IN JUPYTER
input = layers.Input(shape=(28, 28, 1))

# Encoder
x = layers.Conv2D(32, (3, 3), activation="relu", padding="same")(input)
x = layers.MaxPooling2D((2, 2), padding="same")(x)
x = layers.Conv2D(32, (3, 3), activation="relu", padding="same")(x)
x = layers.MaxPooling2D((2, 2), padding="same")(x)

# Decoder
x = layers.Conv2DTranspose(32, (3, 3), strides=2, activation="relu", padding="same")(x)
x = layers.Conv2DTranspose(32, (3, 3), strides=2, activation="relu", padding="same")(x)
x = layers.Conv2D(1, (3, 3), activation="sigmoid", padding="same")(x)

# Autoencoder
autoencoder = Model(input, x)
autoencoder.compile(optimizer="adam", loss="binary_crossentropy")
autoencoder.summary()
Stored output
Model: "functional_3"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ input_layer_2 (InputLayer)      │ (None, 28, 28, 1)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d (Conv2D)                 │ (None, 28, 28, 32)     │           320 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d (MaxPooling2D)    │ (None, 14, 14, 32)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_1 (Conv2D)               │ (None, 14, 14, 32)     │         9,248 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d_1 (MaxPooling2D)  │ (None, 7, 7, 32)       │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_transpose                │ (None, 14, 14, 32)     │         9,248 │
│ (Conv2DTranspose)               │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_transpose_1              │ (None, 28, 28, 32)     │         9,248 │
│ (Conv2DTranspose)               │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_2 (Conv2D)               │ (None, 28, 28, 1)      │           289 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 28,353 (110.75 KB)
 Trainable params: 28,353 (110.75 KB)
 Non-trainable params: 0 (0.00 B)
Cell 35 · Notebook text
Now we can train our autoencoder using the noisy data as our input and the clean data as our target. We want our autoencoder to learn how to denoise the images.

**This may take a while, please refer to your tutor's demonstration of the results during the workshop.**
Cell 36 · Python · autoencoder.fit(
PYTHON · RUN IN JUPYTER
autoencoder.fit(
    x=noisy_train_data,
    y=x_train,
    epochs=2,
    batch_size=128,
    shuffle=True,
    validation_data=(noisy_test_data, x_test),
)
Stored output
Epoch 1/2
469/469 ━━━━━━━━━━━━━━━━━━━━ 16s 33ms/step - loss: 0.2862 - val_loss: 0.1020
Epoch 2/2
469/469 ━━━━━━━━━━━━━━━━━━━━ 16s 35ms/step - loss: 0.1013 - val_loss: 0.0970
<keras.src.callbacks.history.History at 0x36391ac70>
Cell 37 · Python · predictions = autoencoder.predict(noisy_test_data)
PYTHON · RUN IN JUPYTER
predictions = autoencoder.predict(noisy_test_data)
display(noisy_test_data, predictions)
Stored output
313/313 ━━━━━━━━━━━━━━━━━━━━ 2s 5ms/step
Stored notebook output · cell 37
Stored notebook output · cell 37
<Figure size 2000x400 with 20 Axes>
03 · Variational autoencodersBonus primer · 3 min

Give latent space a sampling rule

Why this mattersGive the decoder a useful rule for drawing new latent codes.

Try thisDraw several codes. Look for samples far from the example training codes.

Draw a random $z$ from $\mathcal{N}(0,I)$
Vanilla AE codes

The prior lands in a hole. $g$ must extrapolate.

VAE: regularised codes

The same prior now hits the cloud. $g$ interpolates.

Schematic codes, not trained model outputs. The circle marks a typical region of a Gaussian prior. The Gaussian also has mass outside it.

From reconstruction to generation

Basic AE

Each input maps to one code. Training does not specify a simple distribution for new codes.

Regularisation supports sampling. It does not guarantee a good image at every latent point.

Check your understanding

Where does a new code come from when a VAE generates an image?

Notebook bonus and optional tutorials

The notebook introduces the idea and links to a standalone example. It does not include VAE training cells.

TensorFlow convolutional VAE · Run in Colab · VAE Explainer

Notebook source · 2 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 38 · Notebook text
## (3) Extra bonus: Convolutional Variational Autoencoder
Cell 39 · Notebook text
Variational autoencoders are a slightly more modern and interesting take on autoencoding.

What is a variational autoencoder? It's a type of autoencoder with added constraints on the encoded representations being learned. More precisely, it is an autoencoder that learns a latent variable model for its input data. So instead of letting your neural network learn an arbitrary function, you are learning the parameters of a probability distribution modeling your data. If you sample points from this distribution, you can generate new input data samples: a VAE is a "generative model".

Because a VAE is a more complex example, your could refer to the standalone script below:

<table class="tfo-notebook-buttons" align="left">
  <td>
    <a target="_blank" href="https://www.tensorflow.org/tutorials/generative/cvae">
    <img src="https://www.tensorflow.org/images/tf_logo_32px.png" />
    View on TensorFlow.org</a>
  </td>
  <td>
    <a target="_blank" href="https://colab.research.google.com/github/tensorflow/docs/blob/master/site/en/tutorials/generative/cvae.ipynb">
    <img src="https://www.tensorflow.org/images/colab_logo_32px.png" />
    Run in Google Colab</a>
  </td>

</table>
03 · Variational autoencodersBonus primer · 4 min

Sample without breaking the gradient

Why this mattersLearn distribution parameters while still sampling random codes.

Try thisMove the mean or spread. Compare the sampled code, decoded image, and KL cost.

The reparameterisation trick

Predict μ, σ

The encoder predicts a mean and a standard deviation for each latent coordinate.

Sample a code, then decodePCA decoder · VAE sampling demo

Move the mean and variance. Resample independent noise. The output uses a two-component PCA decoder.

0.40
-0.30
-0.80
0.70
−0.30

A real PCA decoder maps the two coordinates to pixels. Each coordinate is scaled by its training standard deviation. This is not a trained VAE.

KL (stay near the prior)
0.87$z_1=μ_1+σε_1$
-0.09$z_2=μ_2+σε_2$
0.21$D_{\mathrm{KL}}$ (1-D slice)

Dashed $=p(z)$. Solid $=q_\phi(z\mid x)$. KL is smallest when μ = 0 and σ = 1. Moving away from that point raises KL.

The same facts as formulas

Why this formula? Reconstruction keeps the output close to the input. KL keeps the encoded distribution close to the prior so sampling is useful.

From the notebook (Keras / Chollet): a VAE learns the parameters of a distribution on the latent space, not an arbitrary code. Sample that distribution, then decode.

$$q_\phi(z\mid x)=\mathcal{N}\bigl(\mu_\phi(x),\,\mathrm{diag}\,\sigma_\phi^2(x)\bigr)$$ $$z=\mu_\phi(x)+\sigma_\phi(x)\odot\varepsilon,\qquad \varepsilon\sim\mathcal{N}(0,I)$$ $$\mathcal{L}_{\mathrm{VAE}}=\underbrace{-\mathbb{E}_{q_\phi(z\mid x)}[\log p_\theta(x\mid z)]}_{\text{reconstruction}}+\underbrace{D_{\mathrm{KL}}\bigl(q_\phi(z\mid x)\,\|\,p(z)\bigr)}_{\text{regulariser}}$$ $$D_{\mathrm{KL}}=\frac{1}{2}\bigl(\mu^2+\sigma^2-\log\sigma^2-1\bigr)$$
Check your understanding

Which values minimise the KL term in the one-dimensional example?

04 · GANsPrimer 4 · 4 min

Generate. Judge. Improve.

Why this mattersLearn to generate images from feedback rather than pixel targets.

Try thisPlay the game, then select mode collapse. Check how much of the data is covered.

Watch the generator cover the data

Teal: real data. Orange: generated points. This is a teaching simulation, not a trained network. Play, then try mode collapse.

0simulation step
0.50mean D on fakes
lowring coverage
Two networks learn together

Generator G

Start with random noise z. Map it to a candidate image G(z).

Check your understanding

Can generated images look good while the model performs poorly?

GAN Lab and model comparison

Open GAN Lab for a separate live training tool.

AE: reconstruct an input. VAE: reconstruct and sample a prior. GAN: generate from noise using a learned discriminator.

VAEs can produce softer images. GANs can produce fine detail, but training may be unstable. These are common tendencies, not fixed rules.

Notebook source · 4 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 40 · Notebook text
# 2. Generative Adversarial Networks (GANs)
Cell 41 · Notebook text
## (1) Warm-up [Play with GANs in your browser!](https://poloclub.github.io/ganlab/)
Cell 42 · Notebook text
### GANs, Autoencoders and VAEs
Generative adversarial networks (GANs) are algorithmic architectures that use two neural networks, pitting one against the other (thus the “adversarial”) in order to generate new, synthetic instances of data that can pass for real data. They are used widely in image generation, video generation and voice generation.

It may be useful to compare generative adversarial networks to other neural networks, such as autoencoders and variational autoencoders.

Autoencoders encode input data as vectors. They create a hidden, or compressed, representation of the raw data. They are useful in dimensionality reduction; that is, the vector serving as a hidden representation compresses the raw data into a smaller number of salient dimensions. Autoencoders can be paired with a so-called decoder, which allows you to reconstruct input data based on its hidden representation.

Variational autoencoders are generative algorithm that add an additional constraint to encoding the input data, namely that the hidden representations are normalized. Variational autoencoders are capable of both compressing data like an autoencoder and synthesizing data like a GAN. However, while GANs generate data in fine, granular detail, images generated by VAEs tend to be more blurred.

(Source of the above introduction: [Pathmind](https://wiki.pathmind.com/generative-adversarial-network-gan))
Cell 43 · Notebook text
## (2) DCGAN

We will introduce how to generate images of handwritten digits using a [Deep Convolutional Generative Adversarial Network](https://arxiv.org/abs/1511.06434) (DCGAN). We will still use the MNIST dataset to train the generator and the discriminator. The generator will generate handwritten digits resembling the MNIST data.

Adapted from TensorFlow.org tutorials:
<table class="tfo-notebook-buttons" align="left">
  <td>
    <a target="_blank" href="https://www.tensorflow.org/tutorials/generative/dcgan">
    <img src="https://www.tensorflow.org/images/tf_logo_32px.png" />
    View source on TensorFlow.org</a>
  </td>
  <td>
    <a target="_blank" href="https://colab.research.google.com/github/tensorflow/docs/blob/master/site/en/tutorials/generative/dcgan.ipynb">
    <img src="https://www.tensorflow.org/images/colab_logo_32px.png" />
    Run in Google Colab</a>
  </td>

</table>
04 · GANsNotebook lab · 3 min

Prepare images for DCGAN

Why this mattersStop the discriminator from relying on a simple pixel-scale mismatch.

Try thisMove the pixel slider. Compare the AE range with the GAN range.

The GAN data pipeline

Reload

Reload the original MNIST arrays. Avoid reusing the flattened or noisy arrays by accident.

One pixel, two normalisations
128
0.502AE: x / 255
0.004GAN: (x − 127.5) / 127.5
tanhgenerator output
Check your understanding

Why must real and generated images use the same range?

Notebook source · 3 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 44 · Notebook text
### Load and prepare the dataset

Let's reimport the dataset to omit the modifications made earlier.
Cell 45 · Python · (x_train, _), (x_test, _) = tf.keras.datasets.mnist.load_data()
PYTHON · RUN IN JUPYTER
(x_train, _), (x_test, _) = tf.keras.datasets.mnist.load_data()
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1).astype('float32')
x_train = (x_train - 127.5) / 127.5  # Normalize the images to [-1, 1]

x_test = x_test.reshape(x_test.shape[0], 28, 28, 1).astype('float32')
x_test = (x_test - 127.5) / 127.5  # Normalize the images to [-1, 1]

print("train_images shape:", x_train.shape)
print("test_images shape:", x_test.shape)
Stored output
train_images shape: (60000, 28, 28, 1)
test_images shape: (10000, 28, 28, 1)
Cell 46 · Python · BUFFER_SIZE = 60000
PYTHON · RUN IN JUPYTER
BUFFER_SIZE = 60000
BATCH_SIZE = 256

# Batch and shuffle the data
train_dataset = tf.data.Dataset.from_tensor_slices(x_train).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
04 · GANsNotebook lab · 5 min

Build the generator and discriminator

Why this mattersSee how noise becomes an image, and an image becomes a score.

Try thisStep through both networks. Track width, height, and channels.

Generator · grow a 100-value noise vector

100 → 7 × 7 × 256

A Dense layer expands z. Batch normalisation and LeakyReLU follow. Reshape to a feature map.

Discriminator · shrink an image to one score

28 × 28 × 1

Take a real image or a generated image.

Before training
The notebook’s first generated image. The generator has not been trained at this point.
The notebook’s first generated image. The generator has not been trained at this point.

The untrained discriminator returns a score near zero. That score is not a probability.

Check your understanding

Why does the discriminator end with one value?

Notebook source · 9 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 47 · Notebook text
### Create the models

A GAN is made of two parts: a "generator" model that maps points in the latent space to points in image space, a "discriminator" model, a classifier that can tell the difference between real images (from the training dataset) and fake images (the output of the generator network).

<img style="float: ;" src="https://www.tensorflow.org/images/gan/dcgan.gif" width=600 height=500>
<center>(Image courtesy: tensorflow.org)</center>
Cell 48 · Notebook text
A GAN training loop looks like this:

- (1) Train the generator. 
    - Sample random points in the latent space. 
    - Turn the points into fake images via the "generator" network. 
    - Get a batch of real images and combine them with the generated images. 
    - Train the "generator" model to "fool" the discriminator and classify the fake images as real.

- (2) Train the discriminator. 
    - Sample a batch of random points in the latent space. 
    - Turn the points into fake images via the "generator" model. 
    - Get a batch of real images and combine them with the generated images. 
    - Train the "discriminator" model to classify generated vs. real images.

Let's implement this training loop. 

#### The Generator

The generator uses `tf.keras.layers.Conv2DTranspose` (upsampling) layers to produce an image from a seed (random noise). Start with a `Dense` layer that takes this seed as input, then upsample several times until you reach the desired image size of 28x28x1. Notice the `tf.keras.layers.LeakyReLU` activation for each layer, except the output layer which uses tanh.
Cell 49 · Python · def make_generator_model():
PYTHON · RUN IN JUPYTER
def make_generator_model():
    model = tf.keras.Sequential()
    model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(100,)))
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU())

    model.add(layers.Reshape((7, 7, 256)))
    assert model.output_shape == (None, 7, 7, 256)  # Note: None is the batch size

    model.add(layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False))
    assert model.output_shape == (None, 7, 7, 128)
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU())

    model.add(layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False))
    assert model.output_shape == (None, 14, 14, 64)
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU())

    model.add(layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh'))
    assert model.output_shape == (None, 28, 28, 1)

    return model
Cell 50 · Notebook text
Use the (as yet untrained) generator to create an image.
Cell 51 · Python · generator = make_generator_model()
PYTHON · RUN IN JUPYTER
generator = make_generator_model()

noise = tf.random.normal([1, 100])
generated_image = generator(noise, training=False)

plt.imshow(generated_image[0, :, :, 0], cmap='gray')
Stored output
/Users/thixuanmayl/anaconda3/envs/py39/lib/python3.9/site-packages/keras/src/layers/core/dense.py:87: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead.
  super().__init__(activity_regularizer=activity_regularizer, **kwargs)
<matplotlib.image.AxesImage at 0x3637e7b50>
Stored notebook output · cell 51
Stored notebook output · cell 51
<Figure size 640x480 with 1 Axes>
Cell 52 · Notebook text
#### The Discriminator
Then, create the discriminator meant to classify fake vs real digits. The discriminator is a CNN-based image classifier.
Cell 53 · Python · def make_discriminator_model():
PYTHON · RUN IN JUPYTER
def make_discriminator_model():
    model = tf.keras.Sequential()
    model.add(layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same',
                                     input_shape=[28, 28, 1]))
    model.add(layers.LeakyReLU())
    model.add(layers.Dropout(0.3))

    model.add(layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
    model.add(layers.LeakyReLU())
    model.add(layers.Dropout(0.3))

    model.add(layers.Flatten())
    model.add(layers.Dense(1))

    return model
Cell 54 · Notebook text
Use the (as yet untrained) discriminator to classify the generated images as real or fake. The model will be trained to output positive values for real images, and negative values for fake images.
Cell 55 · Python · discriminator = make_discriminator_model()
PYTHON · RUN IN JUPYTER
discriminator = make_discriminator_model()
decision = discriminator(generated_image)
print (decision)
Stored output
tf.Tensor([[-0.00164244]], shape=(1, 1), dtype=float32)
/Users/thixuanmayl/anaconda3/envs/py39/lib/python3.9/site-packages/keras/src/layers/convolutional/base_conv.py:107: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead.
  super().__init__(activity_regularizer=activity_regularizer, **kwargs)
04 · GANsNotebook lab · 5 min

Follow one training step

Why this mattersGive each network a training signal that matches its task.

Try thisCompare the fake-image target in the generator loss and discriminator loss.

The notebook training step

Sample noise

Draw a batch of 100-dimensional normal vectors. The generator creates fake images.

Discriminator

Separate real from fake

Real → 1
Generated → 0

Generator

Make generated images pass

Generated → 1
Update G through D

Losses and the GAN objective

Why this formula? These losses tell each network which direction to improve. D learns to separate real and fake; G learns to make fakes pass.

$$L_D=\mathrm{BCE}(1,D(x))+\mathrm{BCE}(0,D(G(z)))$$$$L_G=\mathrm{BCE}(1,D(G(z)))$$

These BCE calls use from_logits=True. The generator uses the non-saturating loss.

In the original minimax objective below, D denotes a probability:

$$\min_G\max_D\;\mathbb E_x[\log D(x)]+\mathbb E_z[\log(1-D(G(z)))]$$
Check your understanding

Why use target 1 for fake images in the generator loss?

Notebook source · 11 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 56 · Notebook text
#### Define the loss and optimizers
Define loss functions and optimizers for both models.
Cell 57 · Python · cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)
PYTHON · RUN IN JUPYTER
# This method returns a helper function to compute cross entropy loss
cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)
Cell 58 · Notebook text
Discriminator loss

This method quantifies how well the discriminator is able to distinguish real images from fakes. It compares the discriminator's predictions on real images to an array of 1s, and the discriminator's predictions on fake (generated) images to an array of 0s.
Cell 59 · Python · def discriminator_loss(real_output, fake_output):
PYTHON · RUN IN JUPYTER
def discriminator_loss(real_output, fake_output):
    real_loss = cross_entropy(tf.ones_like(real_output), real_output)
    fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
    total_loss = real_loss + fake_loss
    return total_loss
Cell 60 · Notebook text
Generator loss

The generator's loss quantifies how well it was able to trick the discriminator. Intuitively, if the generator is performing well, the discriminator will classify the fake images as real (or 1). Here, compare the discriminators decisions on the generated images to an array of 1s.
Cell 61 · Python · def generator_loss(fake_output):
PYTHON · RUN IN JUPYTER
def generator_loss(fake_output):
    return cross_entropy(tf.ones_like(fake_output), fake_output)
Cell 62 · Notebook text
The discriminator and the generator optimizers are different since you will train two networks separately.
Cell 63 · Python · generator_optimizer = tf.keras.optimizers.Adam(1e-4)
PYTHON · RUN IN JUPYTER
generator_optimizer = tf.keras.optimizers.Adam(1e-4)
discriminator_optimizer = tf.keras.optimizers.Adam(1e-4)
Cell 68 · Notebook text
#### Define the training loop
Cell 70 · Notebook text
The training loop begins with generator receiving a random seed as input. That seed is used to produce an image. The discriminator is then used to classify real images (drawn from the training set) and fakes images (produced by the generator). The loss is calculated for each of these models, and the gradients are used to update the generator and discriminator.
Cell 71 · Python · @tf.function
PYTHON · RUN IN JUPYTER
# Notice the use of `tf.function`
# This annotation causes the function to be "compiled".
@tf.function
def train_step(images):
    noise = tf.random.normal([BATCH_SIZE, noise_dim])

    with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
        generated_images = generator(noise, training=True)

        real_output = discriminator(images, training=True)
        fake_output = discriminator(generated_images, training=True)

        gen_loss = generator_loss(fake_output)
        disc_loss = discriminator_loss(real_output, fake_output)

    gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
    gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)

    generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
    discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))
04 · GANsNotebook lab · 4 min

Track training and restore weights

Why this mattersCompare training progress and reuse learned weights.

Try thisCompare the fixed-seed frames. Then check what the stored notebook run actually shows.

Trained DCGAN 4 by 4 digit grid
TensorFlow tutorial frames. Select a stage to inspect it.
From training loop to saved model

Use a fixed seed

Keep 16 noise vectors with 100 values each. Reuse them to view changes after each epoch.

Stored notebook result
The saved notebook grid is noise-like. It does not demonstrate successful generation.
The saved notebook grid is noise-like. It does not demonstrate successful generation.

The stored output does not prove why this happened. Check the path, restored variables, and training state. A missing checkpoint can raise an error.

Run the full training loop
PYTHON · RUN IN JUPYTER
train(train_dataset, EPOCHS)
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))

The notebook defines a helper named display. This replaces the earlier IPython module name. Before running the GAN loop, use the fix below and replace both display.clear_output calls.

PYTHON · RUN IN JUPYTER
from IPython import display as ipy_display
# In train(), replace display.clear_output(wait=True) with:
ipy_display.clear_output(wait=True)
Verify a checkpoint before using it
PYTHON · RUN IN JUPYTER
checkpoint_path = "./pretrained_checkpoints/ckpt-2"
if not tf.io.gfile.exists(checkpoint_path + ".index"):
    raise FileNotFoundError(checkpoint_path)
# Build both models before checking restored model variables.
_ = generator(tf.zeros([1, 100]), training=False)
_ = discriminator(tf.zeros([1, 28, 28, 1]), training=False)
status = checkpoint.restore(checkpoint_path)
status.assert_existing_objects_matched()
predictions = generator(seed, training=False)

Added diagnostic code. Optimiser slot checks may require the slots to be created first.

Check your understanding

Why keep the same noise seed for progress images?

Notebook source · 11 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 64 · Notebook text
#### Save checkpoints

This notebook also demonstrates how to save and restore models, which can be helpful in case a long running training task is interrupted.


Cell 65 · Python · checkpoint_dir = './training_checkpoints'
PYTHON · RUN IN JUPYTER
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(generator_optimizer=generator_optimizer,
                                 discriminator_optimizer=discriminator_optimizer,
                                 generator=generator,
                                 discriminator=discriminator)
Cell 66 · Notebook text
#### Generate and save images
Cell 67 · Python · def generate_and_save_images(model, epoch, test_input):
PYTHON · RUN IN JUPYTER
def generate_and_save_images(model, epoch, test_input):
  # Notice `training` is set to False.
  # This is so all layers run in inference mode (batchnorm).
    predictions = model(test_input, training=False)

    fig = plt.figure(figsize=(4, 4))

    for i in range(predictions.shape[0]):
        plt.subplot(4, 4, i+1)
        plt.imshow(predictions[i, :, :, 0] * 127.5 + 127.5, cmap='gray')
        plt.axis('off')

    plt.savefig('image_at_epoch_{:04d}.png'.format(epoch))
    plt.show()
Cell 69 · Python · EPOCHS = 50 # In practice you need at least 20 epochs to generate nice
PYTHON · RUN IN JUPYTER
EPOCHS = 50 # In practice you need at least 20 epochs to generate nice digits.
noise_dim = 100
num_examples_to_generate = 16

# You will reuse this seed overtime (so it's easier)
# to visualize progress after each epoch
seed = tf.random.normal([num_examples_to_generate, noise_dim])
Cell 72 · Python · def train(dataset, epochs):
PYTHON · RUN IN JUPYTER
def train(dataset, epochs):
    for epoch in range(epochs):
        start = time.time()

        for image_batch in dataset:
            train_step(image_batch)

        # Produce images for each epoch
        display.clear_output(wait=True)
        generate_and_save_images(generator,
                                 epoch + 1,
                                 seed)

        # Save the model every 15 epochs
        if (epoch + 1) % 15 == 0:
            checkpoint.save(file_prefix = checkpoint_prefix)

        print ('Time for epoch {} is {} sec'.format(epoch + 1, time.time()-start))

    # Generate after the final epoch
    display.clear_output(wait=True)
    generate_and_save_images(generator,
                             epochs,
                             seed)
Cell 73 · Notebook text
### Train the model
Call the `train()` method defined above to train the generator and discriminator simultaneously. Note, training GANs can be tricky. It's important that the generator and discriminator do not overpower each other (e.g., that they train at a similar rate).

At the beginning of the training, the generated images look like random noise. As training progresses, the generated digits will look increasingly real. After about 50 epochs, they resemble MNIST digits. 

To train your own model, run:

`train(train_dataset, EPOCHS)`

To restore the latest checkpoint with your own model, run:

`checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))`

To save time, we will use the prepared checkpoints directly in the workshop.
Cell 74 · Python · import logging
PYTHON · RUN IN JUPYTER
# Optional: Disable Tensorflow verbose logging output
import logging
tf.get_logger().setLevel(logging.ERROR) # or any {DEBUG, INFO, WARNING, ERROR, CRITICAL}
Cell 75 · Python · checkpoint.restore("./pretrained_checkpoints/ckpt-2") #about 100 epoch
PYTHON · RUN IN JUPYTER
## Restore the pre-trained checkpoints
# checkpoint.restore("./pretrained_checkpoints/ckpt-1") #about 50 epochs
checkpoint.restore("./pretrained_checkpoints/ckpt-2") #about 100 epochs
Stored output
<tensorflow.python.checkpoint.checkpoint.CheckpointLoadStatus at 0x352bf8ca0>
Cell 76 · Python · predictions = generator(seed, training=False)
PYTHON · RUN IN JUPYTER
predictions = generator(seed, training=False)
Cell 77 · Python · fig = plt.figure(figsize=(4, 4))
PYTHON · RUN IN JUPYTER
# Display the results
fig = plt.figure(figsize=(4, 4))

for i in range(predictions.shape[0]):
    plt.subplot(4, 4, i+1)
    plt.imshow(predictions[i, :, :, 0], cmap='gray')
    plt.axis('off')

plt.show()
Stored output
Stored notebook output · cell 77
Stored notebook output · cell 77
<Figure size 400x400 with 16 Axes>
05 · Test the generatorExercise 2 · 4 min

What does the discriminator score?

Why this mattersCheck what the discriminator has learned to separate.

Try thisCompare real and fake logits. Use the images to interpret the scores.

Discriminator logits

Zero is the centre line. Left: favours fake. Right: favours real. The scale is fixed across both views.

All ten real scores were slightly negative. Fake scores sat near zero. That is not a working critic. It matches an unrestored $D$.

Compare the scores

Prepare images

Use x_test[:10] for real images. Generate ten fakes from ten random latent vectors.

Exercise 2 · compare and explain

Should every generated image always receive a negative score?

Stored image pairs
Top: real test images. Bottom: stored generated images.
Top: real test images. Bottom: stored generated images.
Notebook source · 5 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 78 · Notebook text
### Exercise 2: Check the Discriminator output for real and fake images

Run some real images (from a test set) and fake images (N random samples from the Generator) through the Discriminator and check their scores. If the Discriminator is well-trained, it should output positive values for real images, and negative values for fake images.
Cell 79 · Python · real_images = x_test[:10]
PYTHON · RUN IN JUPYTER
# Choose 10 real images for test set and generate 10 fake images from the Generator
real_images = x_test[:10]
noise = tf.random.normal([10, noise_dim])
fake_images = generator(noise, training=False)

display(real_images, fake_images.numpy())

# Run these real/fake images rough the Discriminator and check their scores.
# your code

Stored output
Stored notebook output · cell 79
Stored notebook output · cell 79
<Figure size 2000x400 with 20 Axes>
Cell 80 · Notebook text
The top row is the real images for the test set, and the bottom row is the fake images generated from the Generator.
Cell 81 · Notebook text
### Solution
Cell 82 · Python · real_output = discriminator(x_test[:10], training=False)
PYTHON · RUN IN JUPYTER
real_output = discriminator(x_test[:10], training=False)
fake_output = discriminator(fake_images, training=False)

print ("Scores for real images",real_output)
print ("Scores for fake images",fake_output)
Stored output
Scores for real images tf.Tensor(
[[-0.1127525 ]
 [-0.10259311]
 [-0.08745752]
 [-0.10899768]
 [-0.03588801]
 [-0.08322508]
 [-0.114327  ]
 [-0.07023349]
 [-0.03052524]
 [-0.08232151]], shape=(10, 1), dtype=float32)
Scores for fake images tf.Tensor(
[[ 6.1758328e-07]
 [ 3.8085919e-04]
 [-3.0598999e-03]
 [ 1.0995279e-03]
 [-1.1941220e-03]
 [-1.2221746e-04]
 [ 7.4083486e-04]
 [-2.5569103e-03]
 [-2.0648669e-03]
 [-1.3346837e-03]], shape=(10, 1), dtype=float32)
05 · Test the generatorExercise 3 · 5 min

Is the generator copying?

Why this mattersLook for evidence that generated images copy the training data.

Try thisCompare a query with its three closest matches. Similar is not the same as identical.

Try nearest-neighbour searchMNIST teaching demo

The query is a held-out digit or its PCA reconstruction. Each search was computed across all 60,000 training images. This is not a GAN result.

Search for possible copies

Generate one image

Draw one noise vector. Compute gen = generator(noise, training=False).

Exercise 3 · implement and inspect

How would you find the three closest training images?

Original solution figures
Stored generated query: noise-like output.
Stored generated query: noise-like output.
The three matches displayed by the original notebook code.
The three matches displayed by the original notebook code.
Notebook source · 4 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 83 · Notebook text
### Exercise 3: Check that generated images are not just copies of the training set

Is the Generator simply repeating images from its training set? Or has it learned how to create new images?

To check this, generate one random image from the Generator. Compute its similarity (e.g., sum of square pixel difference) to every image in the training set. Return the closest match (or 3 closest matches).
Cell 84 · Python · from sklearn.metrics import mean_squared_error, r2_score
PYTHON · RUN IN JUPYTER
from sklearn.metrics import mean_squared_error, r2_score
import heapq
from tqdm import tqdm

# your code
Cell 85 · Notebook text
### Solution
Cell 86 · Python · noise = tf.random.normal([1,100])
PYTHON · RUN IN JUPYTER
# Generate one random image from the Generator
noise = tf.random.normal([1,100])
gen = generator(noise, training=False)

plt.imshow(gen[0, :, :, 0] * 127.5 + 127.5, cmap='gray')
plt.title("Generated Image")
plt.axis('off')
plt.show()

# Compute its similarity to every image in the training set. 
# We use sum of square pixel difference as the similarity here.
similarity = []
for i in tqdm(range(len(x_train))):
    mse = tf.reduce_mean(tf.square(gen - tf.squeeze(x_train[i])))
    heapq.heappush(similarity, (mse,i))

# Find the three smallest value, i.e. the three most similar index
k = 3
results = []
for _ in range(k):
    results.append(heapq.heappop(similarity))

# Display the three closest matches found
for i, (_,idx) in enumerate(results):
    plt.subplot(1,3,i+1)
    plt.imshow(np.squeeze(x_train[idx]))
    plt.axis('off')
    
plt.title("The 3 closest matches in the training set")
plt.show()
Stored output
Stored notebook output · cell 86
Stored notebook output · cell 86
<Figure size 640x480 with 1 Axes>
100%|███████████████████████████████████| 60000/60000 [00:06<00:00, 9546.72it/s]
Stored notebook output · cell 86
Stored notebook output · cell 86
<Figure size 640x480 with 3 Axes>
05 · Test the generatorExercise 4 · 4 min

Walk through latent space

Why this mattersTest whether nearby latent codes produce gradual, plausible changes.

Try thisMove from A to B. Watch the intermediate images, not just the endpoints.

Move from A to BLinear PCA demo

Decode interpolated code

Interpolate decoded pixels

0.00
AB

For an affine PCA decoder, these two paths are equal before clipping. A nonlinear generator can produce a different path.

Run the GAN latent walk

Choose endpoints

Draw two random vectors A and B in the 100-dimensional input space.

Exercise 4 · inspect the path

What would continuity and completeness look like along the walk?

Stored walk and baseline comparisons
Stored notebook walk: fifty noise-like outputs.
Stored notebook walk: fifty noise-like outputs.
PCA latent interpolation between codes of a 3 and an 8.
PCA latent interpolation between codes of a 3 and an 8.
Raw-pixel interpolation between the original 3 and 8. Its endpoints differ from the decoded PCA endpoints.
Raw-pixel interpolation between the original 3 and 8. Its endpoints differ from the decoded PCA endpoints.
Notebook source · 4 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 87 · Notebook text
### Exercise 4: Do a linear walk through the latent space
In a well-trained generative model, the latent space should be **continuous** and **complete**. Continuous means that similar images are nearby in the latent space. Complete means that all points in the latent space correspond to real images.

A good way to check this is to do a linear walk through the latent space. Select two random points (A,B) in the Generator's input space. Get N equally-spaced points from A to B (e.g., from `numpy.linspace`) and generate an image for each of those input points.

If the GAN is well-trained, the images should show a smooth transition from A to B (indicating that the latent space is continuous) and every step between those points should look like a realistic image (indicating that the latent space is complete).
Cell 88 · Python · Exercise space
PYTHON · RUN IN JUPYTER
# your code
Cell 89 · Notebook text
### Solution
Cell 90 · Python · noise = tf.random.normal([2, 100])
PYTHON · RUN IN JUPYTER
# Select two random points in the Generator's input space.
noise = tf.random.normal([2, 100])

# Get 50 equally-spaced points from A to B
walking_latent = np.linspace(noise[0], noise[1], 50)
walking_latent_images = generator(walking_latent, training=False)

fig = plt.figure(figsize=(10, 5))

for i in range(walking_latent_images.shape[0]):
    plt.subplot(5, 10, i+1)
    plt.imshow(walking_latent_images[i, :, :, 0] * 127.5 + 127.5, cmap='gray')
    plt.axis('off')
Stored output
Stored notebook output · cell 90
Stored notebook output · cell 90
<Figure size 1000x500 with 50 Axes>
05 · Test the generatorExercise 5 · 7 min

How many different images?

Why this mattersDetect a generator that repeats a small set of images.

Try thisFirst count birthday pairs. Then replace birthdays with possible generated images.

Start with the intuition

23 people. A surprisingly likely match.

A match can occur between any two people. Each new person adds several new pairs.

23
Pairs of people whose birthdays could match

One dot = one person. One line = one chance to match. Lines do not show actual shared birthdays.

Chances add up253possible pairs
Match person 15.9%
Match any pair50.7%

Why it feels surprising

We often think about one person’s birthday. The test compares every pair. With 23 people, there are 23 × 22 / 2 = 253 pairs.

Assume independent birthdays, 365 equally likely days, and no leap day. The bars use the exact probability under these assumptions.

Why count “no match” first?

Person 1 can have any birthday. Person 2 must avoid 1 day. Person 3 must avoid 2 days. Continue this pattern.

365 / 365×364 / 365×363 / 365× …(365 − N + 1) / 365

This product is the chance that all birthdays differ. Subtract it from 1 to get the chance of at least one match.

$$P(\text{match})=1-\prod_{i=0}^{N-1}\left(1-\frac{i}{365}\right)$$

We do not treat the pair events as independent. Multiplying the “new birthday” chances gives the exact result.

Now apply the same idea to a GAN

People become samples. Birthdays become outputs.

Birthday problemN people

Draw birthdays from 365 possible days.

Generator testN generated images

Draw from S possible distinct outputs.

A collision is a repeated output. If repeats appear in small batches, the generator may have limited variety.

S is the support size: the number of possible outputs in this simplified model. Real GAN outputs need a clear near-duplicate rule.

Test the idea: smaller support, earlier repeats

Keep N fixed and reduce S. Watch the repeat probability rise. This estimates a batch-level chance, not a count from one grid.

50
2500
38.7%$P(\mathrm{collision})$
2500$N^2$ rule of thumb

Synthetic sample. Larger red dots mark repeated categories.

Why N²? A batch has N(N − 1) / 2 pairs. That count grows roughly as N². Under uniform sampling, a 50% repeat chance occurs when N is on the order of √S.

The birthday test

Choose N

The notebook starts with N = 50. Generate N independent samples.

Exercise 5 · sample and estimate

If half of the N = 50 batches contain a duplicate, how large is the support?

Collision formula and notebook grid
$$P(\mathrm{collision})\approx1-\exp\left(-\frac{N(N-1)}{2S}\right)$$

The calculator uses this formula. The coloured dots are a synthetic example of repeated categories.

Stored N = 50 grid. Noise-like outputs do not establish useful image diversity.
Stored N = 50 grid. Noise-like outputs do not establish useful image diversity.

Arora & Zhang: the birthday test for GANs

Notebook source · 2 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 91 · Notebook text
### Exercise 5: Implement [the birthday paradox test](https://arxiv.org/abs/1706.08224)

Ideally, a generative model should be able to produce a wide variety of images. However, in practice GANs usually show some amount of "mode collapse" -- the Generator can only produce a limited number of images. In severe cases of mode collapse, a Generator will only learn to produce a single image.

Less-severe cases of mode collapse are hard to detect, but one way to check for this is the [the birthday paradox test](https://arxiv.org/abs/1706.08224). Choose a value of N and generate N random images. See if you can spot any duplicate images in the set of N. The value of N at which you start seeing duplicates gives you an approximation of the number of images the GAN can produce. If you have a 50% chance of duplicates in a set of N, that suggests that the Generator produces approximately N^2 unique different images.
Cell 92 · Python · N = 50
PYTHON · RUN IN JUPYTER
N = 50

noise = tf.random.normal([N, noise_dim])
generated_images = generator(noise, training=False)

fig = plt.figure(figsize=(10, 5))

for i in range(generated_images.shape[0]):
    plt.subplot(5, 10, i+1)
    plt.imshow(generated_images[i, :, :, 0] * 127.5 + 127.5, cmap='gray')
    plt.axis('off')
Stored output
Stored notebook output · cell 92
Stored notebook output · cell 92
<Figure size 1000x500 with 50 Axes>
05 · Test the generatorExercise 6 · 8 min

Quality can rise as diversity falls

Why this mattersSeparate image recognisability from variety across a batch.

Try thisChange classifier confidence and class coverage. Watch when the score rises or falls.

The notebook’s training story: quality rises, variety falls
Inspect three possible stagesIllustrative training story

Inception Score · visual intuition

Clear predictions. A varied batch.

A classifier gives each image class probabilities. The score compares each image’s prediction with the batch average.

100%
3 of 3

Each image · p(y | x)

A narrow prediction means “I recognise this”.

Batch average · p(y)

Average the probabilities across all six images.

Inception-style score3.001 to 3 in this three-class example

Variety across the batch

Uncertainty per image

Entropy measures spread. We want a broad batch average and focused individual predictions.

Both ingredients are present

What can a high score miss?

The example uses two different handwritten images per class.

A classifier can stay confident even when a generator repeats images. This score does not test whether images are copied from the training set.

Use the birthday test and nearest-neighbour checks alongside it. Classifier confidence is a useful signal, not proof of visual quality.

Connect the picture to the formula

Purpose: reward confident predictions for individual images and broad class coverage across the batch.

$$\mathrm{IS}=\exp\left(\frac1N\sum_{i=1}^{N}D_{\mathrm{KL}}\bigl(p(y\mid x_i)\,\|\,p(y)\bigr)\right)$$

xᵢ: one generated image. y: a class. p(y | xᵢ): the classifier’s probabilities for that image. p(y): their average over the batch.

KL measures how much each prediction differs from the batch average. The exponential turns the average KL into the reported score.

$$\log\mathrm{IS}=H(p(y))-\frac1N\sum_i H(p(y\mid x_i))$$

Entropy H measures uncertainty. We want high class variety in the batch and low uncertainty for each image. Values here use natural logarithms, measured in nats.

If every image has the same probability vector, the two terms cancel. The score is 1.

Teaching example: real MNIST images with chosen probability vectors. No classifier runs here. The original Inception Score uses an Inception classifier; a digit classifier gives an analogous score for MNIST.

Sources: Salimans et al. · Barratt & Sharma on limitations.

Exercise 6 · part i

i. How could the developer measure quality?

Exercise 6 · part ii

ii. How could the developer measure diversity?

Exercise 6 · part iii

iii. Why could diversity drop later in training?

Notebook source · 2 cells

Exact source and saved outputs. Python runs in Jupyter. Page controls use local teaching demos.

Cell 93 · Notebook text
### Exercise 6

A Generative Adversarial Network is being trained. The developer observes that initially the Generator is creating images that look like patterned snow.  After some training, it is starting to generate blurry images that start to look like the training data. Later the quality of the generated  images increases but the diversity drops.

i) How might the developer measure quality?

A: Inception score

ii) How might the developer measure diversity?

A: Birthday paradox

iii) Why might diversity drop during the later part of training?

A: Discriminator overfits
Cell 94 · Python · Exercise space
PYTHON · RUN IN JUPYTER
# Empty exercise cell in the source notebook.
06 · ReviewRecap · 3 min

Choose the model. Choose the check.

Why this mattersChoose an evaluation check that answers your actual question.

Try thisMatch each model to its training signal. Then choose a check for repeated images.

Recall the four tasks

Autoencoder

Clean image → latent code → reconstruction. Check detail and reconstruction loss.

Autoencoder VAE GAN (DCGAN)
Maps $x\to z\to\hat{x}$ $x\to(\mu,\sigma)\to z\to\hat{x}$ $z\to G(z)$; $D$ scores images
Latent Unconstrained code $q_\phi(z\mid x)$ near $\mathcal{N}(0,I)$ Prior $z\sim\mathcal{N}(0,I)$ only
Loss $\ell(x,\hat{x})$ reconstruction $+$ KL $\min_G\max_D V(D,G)$
Sampling No simple $p(z)$ Draw $z\sim p(z)$, decode Draw $z\sim p(z)$, generate
Typical look Slightly blurry copy Blurrier, more regular Often sharp; training can be unstable
Failure Lost detail; overfitting Unused latent codes; excessive blur Mode collapse; $D$ overfitting
Workshop check Recon grid + t-SNE; vary $k$ ELBO terms; 2-D latent grid Scores, neighbours, walk, birthday
Check your understanding

A generator repeats one convincing digit. Which checks would reveal the problem?

Optional tools and references

Download the full solution notebook ↓