{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# CSC412/2506 Assignment 3: Variational Auto Encoders\n",
"\n",
"In this assignment we will learn how to preform efficient inference and learning in directed graphical models with continuous latent variables.We will use stochastic variational inference with automatic differentiation (SADVI) to approximate intractible posterior distributions. \n",
"We will implement the two gradient estimators discussed in lecture, Score Function and Reparamterization, and experimentally demonstrate their properties such as biasedness and variance. \n",
"We will use the reparameterization gradient estimators to optimize the ELBO of our latent variable model.\n",
"\n",
"You can use automatic differentiation in your code.\n",
"You may also use a machine learning framework to specify the encoder and decoder neural networks, and provide gradientent optimizers such as ADAM.\n",
"However, you may not use any probabilistic modelling elements for these frameworks.\n",
"In particular, sampling from and evaluating densities under distributions must be written by you."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Implementing the VAE [20pts]\n",
"\n",
"In this assignment we will implement and investigate the Variational Auto Encoder on Binarized MNIST digits detailed in [Auto-Encoding Variational Bayes](https://arxiv.org/pdf/1312.6114.pdf) by Kingma and Welling (2013). Before starting, read this paper. In particular, we will implement model as described in Appendix C."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Load and Prepare Data\n",
"\n",
"Load the MNIST dataset, binarize the images, split into a training dataset of 10000 images and a test set of 10000 images. Also partition the training set into minibatches of size M=100."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# You may use the script provided in A2 or dataloaders provided by framework\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Distributions [5pts]\n",
"\n",
"Implement code to sample from and evaluate the log-pdf of diagonal multivariate gaussians $\\mathcal{N}(x|\\mu, \\sigma^2 I)$ and Bernoulli distributions. For sampling from these distributions, you have access to samples from uniform and unit Gaussians, (`rand` and `randn`). Make sure you test you've implemented these correctly by comparing to standard packages!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# sampler from Diagonal Gaussian x~N(μ,σ^2 I) (hint: use reparameterization trick here)\n",
"\n",
"# sampler from Bernoulli\n",
"\n",
"# log-pdf of x under Diagonal Gaussian N(x|μ,σ^2 I)\n",
"\n",
"# log-pdf of x under Bernoulli \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Defining Model Architecture [5pts]\n",
"\n",
"Implement the model as defined in Appendix C. The MLPs will have a single hidden layer with Dh=500 hidden units. The dimensionality of the latent space will be Dz=2 for visualization purposes later.\n",
"\n",
"Note that the output of the encoder will be $[\\mu,\\log\\sigma^2]$. Why not ouput $\\sigma^2$ directly? Keep this in mind when you sample from the distribution using your Diagonal Gaussian sampler."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Set latent dimensionality=2 and number of hidden units=500.\n",
"\n",
"# Define MLP for recognition model / \"encoder\"\n",
"# Provides parameters for q(z|x)\n",
"\n",
"# Define sample from recognition model\n",
"# Samples z ~ q(z|x)\n",
"\n",
"# Define MLP for generative model / \"decoder\"\n",
"# Provides parameters for distribution p(x|z)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Variational Objective [7pts]\n",
"\n",
"Here we will use the log-pdfs, the encoder, gaussian sampler, and decoder to define the Monte Carlo estimator for the mean of the ELBO over the minibatch."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# log_q(z|x) logprobability of z under approximate posterior N(μ,σ^2)\n",
"\n",
"# log_p_z(z) log probability of z under prior\n",
"\n",
"# log_p(x|z) - conditional probability of data given latents.\n",
"\n",
"# Monte Carlo Estimator of mean ELBO with Reparameterization over M minibatch samples.\n",
"# This is the average ELBO over the minibatch\n",
"# Unlike the paper, do not use the closed form KL between two gaussians,\n",
"# Following eq (2), use the above quantities to estimate ELBO as discussed in lecture\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Optimize with Gradient Descent\n",
"\n",
"Minimize the -ELBO with ADAM optimizer. You may use the optimizer provided by your framework"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Load Saved Model Parameters (if you've already trained)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Set up ADAM optimizer\n",
"\n",
"# Train for ~200 epochs (1 epoch = all minibatches in traindata)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Save Optimized Model Parameters\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Report ELBO on Training and Test Set [3pts]"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# ELBO on training set\n",
"\n",
"# ELBO on test set\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Numerically Computing Intractable Integrals [10pts]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Numerical Integration over Latent Space [5pts]\n",
"\n",
"Since we chose a low dimensional latent space, we are able to perform [numerical integration](https://en.wikipedia.org/wiki/Riemann_sum) to evaluate integrals which are intractible in higher dimension. \n",
"\n",
"For instance, we will use this to integrate over the latent space. e.g. the $$p(z|x) = \\frac{p(x|z)*p(z)}{p(x)}= \\frac{p(x|z)*p(z)}{\\int p(x|z)*p(z) dz}$$\n",
"\n",
"We want to numerically compute that integral. However, since we are parameterizing $\\log p(x|z)$ and $\\log p(z)$ we will have\n",
"$$\\log p(z|x) = \\log p(x|z) + \\log p(z) - \\log \\int \\exp [\\log p(x|z)+ \\log p(z)] dz$$\n",
"\n",
"You will write code which computes $\\log \\int \\exp \\log f(z) dz$ given an equally spaced grid of $\\log f(z)$s as input.\n",
"Note that if we approximate that integral with a numerical sum, in order for it to be numerically stable we will need `logsumexp`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Implement log sum exp\n",
"\n",
"# Implement stable numerical integration \n",
"# over a 2d grid of equally spaced (delta_z) evaluations logf(x)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Compare Numerical Log-Likelihood to ELBO [5pts]\n",
"\n",
"We can use the numerical integration to compute the log-likeihood of a element in our dataset under our model. We can then compare the numerical integration to the estimate given by the ELBO."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Define the delta_z to be the spacing of the grid (I used delta_z = 0.1)\n",
"\n",
"# Define a grid of delta_z spaced points [-4,4]x[-4,4]\n",
"\n",
"# Sample an x from the data to evaluate the likelhiood\n",
"\n",
"# Compute log_p(x|z)+log_p(z) for every point on the grid\n",
"\n",
"# Using your numerical integration code\n",
"# integrate log_p(x|z)+log_p(z) over z to find log_p(x)\n",
"\n",
"# Check that your numerical integration is correct \n",
"# by integrating log_p(x|z)+log_p(z) - log_p(x)\n",
"# If you've successfully normalized this should integrate to 0 = log 1\n",
"\n",
"# Now compute the ELBO on x\n",
"\n",
"# Try this for multiple samples of x\n",
"# note that the ELBO is a lower bound to the true log_p(x)!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Data Space Visualizations [10pts]\n",
"\n",
"In this section we will investigate our model by visualizing the distributions over data given by the generative model, samples from these distributions, and reconstructions of the data."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Write a function to reshape 784 array into a 28x28 image for plotting\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Samples from the generative model [5pts]\n",
"\n",
"Here you will sample from the generative model using ancestral sampling. \n",
"\n",
"* First sample a z from the prior. \n",
"* Then use the generative model to parameterize a bernoulli distribution over x given z. Plot this distribution.\n",
"* Then sample x from the distribution. Plot this sample.\n",
"\n",
"Do this for 10 samples z from the prior.\n",
"\n",
"Concatenate all your plots into one 10x2 figure where the first column is the distribution over x and the second column is a sample from this distribution. Each row will be a new sample from the prior."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Sample 10 z from prior\n",
"\n",
"# For each z, plot p(x|z)\n",
"\n",
"# Sample x from p(x|z) \n",
"\n",
"# Concatenate plots into a figure\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Reconstructions of data [5pts]\n",
"\n",
"Here we will investigate the VAEs ability to reconstruct 10 inputs from the data. For each input ou will\n",
"\n",
"* Plot the input $x$\n",
"* Use the recognition network to encode $x$ to the parameters for a distribution $q(z|x)$\n",
"* Sample $z \\sim q(z|x)$\n",
"* Use the generative model to decode to the parameters for distribution $p(x|z)$. Plot this\n",
"* Sample $\\tilde x \\sim p(x|z)$. Plot this\n",
"\n",
"Then you will concatenate all your plots into a 10x3 figure where the first column is the input data, the second column is the distribution over x, the third column is a reconstruction of the input. Each row will be a new sample from the data."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Sample 10 xs from the data, plot.\n",
"\n",
"# For each x, encode to distribution q(z|x)\n",
"\n",
"# For each x, sample distribution z ~ q(z|x)\n",
"\n",
"# For each z, decode to distribution p(x̃|z), plot.\n",
"\n",
"# For each x, sample from the distribution x̃ ~ p(x̃|z), plot.\n",
"\n",
"# Concatenate all plots into a figure.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Latent Space Visualizations [15pts]\n",
"\n",
"In this section we will investigate our model by visualizing the latent space through various methods. These will include encoding the data, decoding along a grid, and linearly interpolating between encdoded data."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Latent embedding of data [5pts]\n",
"\n",
"One way to understand what is represented in the latent space is to consider where it encodes elements of the data. Here we will produce a scatter plot in the latent space, where each point in the plot will be the mean vector for the distribution $q(z|x)$ given by the encoder. Further, we will colour each point in the plot by the class label for the input data. \n",
"\n",
"Hopefully our latent space will have learned to distinguish between elements from different classes, even though we never provided class labels to the model!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Encode the training data\n",
"\n",
"# Take the mean vector of each encoding\n",
"\n",
"# Plot these mean vectors in the latent space with a scatter\n",
"# Colour each point depending on the class label\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Decoding along a lattice [5pts]\n",
"\n",
"We can also understand the \"learned manifold\" by plotting the generative distribution $p(x|z)$ for each point along a grid in the latent space. We will replicate figure 4b in the paper."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Create a 20x20 equally spaced grid of z's\n",
"# (use the previous figure to help you decide appropriate bounds for the grid)\n",
"\n",
"# For each z on the grid plot the generative distribution over x\n",
"\n",
"# concatenate these plots to a lattice of distributions\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Interpolate between two classes [5pts]\n",
"\n",
"A common technique to assess latent representations is to interpolate between two points.\n",
"\n",
"Here we will encode 3 pairs of data points with different classes. Then we will linearly interpolate between the mean vectors of their encodings. We will plot the generative distributions along the linear interpolation."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Function which gives linear interpolation z_α between za and zb\n",
"\n",
"# Sample 3 pairs of data with different classes\n",
"\n",
"# Encode the data in each pair, and take the mean vectors\n",
"\n",
"# Linearly interpolate between these mean vectors\n",
"\n",
"# Along the interpolation, plot the distributions p(x|z_α)\n",
"\n",
"# Concatenate these plots into one figure\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Posteriors and Stochastic Variational Inference [20pts]\n",
"\n",
"Here we will use numerical integration to plot the \"true\" posterior $p(z|x)$ which is generally intractable. We will compare the intractable true posterior to the variational approximate posterior given by the recognition model $q(z|x)$.\n",
"\n",
"Then we will use the generative model to perform inference other inference tasks. In particular, we will see that the purpose of the encoder was only to make training the generative model tractable, and that we can do inference using the generative model completely without the encoder. To illustrate this we will perform the inference task of producing a generative distribution over the bottom half of the digit conditioned on the top half. We will achieve this with stochastic variational inference."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plotting Posteriors [5pts]\n",
"\n",
"Here we will plot the true posterior by evaluating $\\log p(x|z)+\\log p(z)$ on an equally spaced grid over z then numerically integrating over this grid to find the log-normalizer $\\log p(x)$. This will give us the intractable true posterior $p(z|x)$.\n",
"\n",
"Then we will compare the true posterior to the approximate posterior given by the recognition model $q(z|x)$. Does the recognition model produce a good approximate posterior to the intractable true posterior?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Sample an element x from the dataset to plot posteriors for\n",
"\n",
"# Define a grid of equally spaced points in z\n",
"# The grid needs to be fine enough that the plot is nice\n",
"# To keep the integration tractable \n",
"# I reccomend centering your grid at the mean of q(z|x)\n",
"\n",
"# Evaluate log_p(x|z) + log_p(z) for every z on the grid\n",
"\n",
"# Numerically integrate log_p(x|z) + log_p(z) to get log_p(x)\n",
"\n",
"# Produce a grid of normalized log_p(z|x)\n",
"\n",
"# Plot the contours of p(z|x) (note, not log)\n",
"\n",
"# Evaluate log_q(z|x) recognition network for every z on grid\n",
"\n",
"# Plot the contours of q(z|x) on previous plot\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## True posterior for top of digit [5pts]\n",
"\n",
"In this question we will plot the \"true\" posterior given only the top of the image, $p(z|x_{top})$. \n",
"\n",
"Realize that the generative model gives a Bernoulli distribution over each pixel in the image. We can easily evaluate the likelihood of only the top of an image by evaluating under only those corresponding dimensions of the generative model."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Function which returns only the top half of a 28x28 array\n",
"# This will be useful for plotting, as well as selecting correct bernoulli params\n",
"\n",
"# log_p(x_top | z) (hint: select top half of 28x28 bernoulli param array)\n",
"\n",
"# Sample an element from the data set and take only its top half: x_top\n",
"\n",
"# Define a grid of equally spaced points in z\n",
"\n",
"# Evaluate log_p(x_top | z) + log_p(z) for every z on grid\n",
"\n",
"# Numerically integrate to get log_p(x_top)\n",
"\n",
"# Normalize to produce grid of log_p(z|x_top)\n",
"\n",
"# Plot the contours of p(z|x_top)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Learn approximate posterior for top of digit with Stochastic Variational Inference [10 pts]\n",
"\n",
"In this question we will see how we can use SVI to learn an approximate posterior to $p(z|x_{top})$ which we just obtained through intractable integration.\n",
"\n",
"Note that we can't just use our recognition model, because our encoder doesn't know what to do with only top halfs of images. Instead, we will initialize a variational distribution $q(z) = \\mathcal{N}(z| \\mu,\\sigma^2 I)$ and optimize the ELBO to minimize the KL divergence between it and the true distribution."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Initialize parameters μ and logσ for variational distribution q(z)\n",
"\n",
"# Define mean ELBO over M samples z ~ q(z)\n",
"# using log_p(z), log_p(x_top | z), and q(z|x_top)\n",
"\n",
"# Loss for SVI is -1*ELBO\n",
"\n",
"# Set up ADAM to optimize μ and logσ^2\n",
"\n",
"# Optimize for a few iterations until convergence (you can use a larger stepsize here)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# On previous plot of contours of p(z|x_top) plot the optimized q(z)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Sample z ~ q(z)\n",
"\n",
"# Use generative model p(x|z) to produce distribution over x\n",
"\n",
"# Extract the bottom half of this generative distribution: p(x_bot| z)\n",
"\n",
"# Concatenate the x_top and p(x_bot | z) and plot.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Investigating Gradient Estimators [Bonus 5pts]\n",
"\n",
"In this part we will experimentally investigate the difference in variances between the gradient estimates given by the Reparameterization and Score-Function gradient estimators.\n",
"\n",
"Comment on their mean and variances"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Use Reparameterization Gradient Estimator\n",
"# to estimate gradient of mean ELBO wrt μ over M minibatch samples\n",
"# hint: this will involve just taking gradients through the code used to train\n",
"\n",
"# Use Score-Function Gradient Estimator\n",
"# to estimate gradient of mean ELBO wrt μ over M minibatch samples\n",
"# make sure you are not useing the reparameterization trick to sample z from q\n",
"# you should only be taking gradients through log_q(z|x), no gradients through ELBO or z\n",
"\n",
"# Consider the gradients wrt the first component of μ\n",
"# Produce two histograms in two different subplots\n",
"# First show the distribution of gradients given by Reparameterization estimator\n",
"# Second show the distribution of gradients given by Score Function Estimator\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}