22. Black-box Variational Inference#

This chapter is adapted from AM207 (Harvard, Fall 2021). Watch the recorded lecture.

Hide code cell content

def generate_data(N, y_var=1.):
    '''generate training data with a gap, testing data uniformly sampled'''
    #training x
    x_train = np.hstack((np.linspace(-10, -5, N), np.linspace(5, 10, N)))
    #function relating x and y
#     f = lambda x:  0.01 * x**3
    f = lambda x: 2 * x
    #y is equal to f(x) plus gaussian noise
    y_train = f(x_train) + np.random.normal(0, y_var**0.5, 2 * N)

    ## generate testing data
    #nubmer of testing points
    N_test = 100
    #testing x
    x_test = np.linspace(-10, 10, N_test)
    y_test = f(x_test) + np.random.normal(0, y_var**0.5, N_test)

    return x_train, y_train, x_test, y_test

def bayesian_polynomial_regression(x, y, x_test, y_test, prior_var, y_var, ax, S=100, poly_degree=10):
    '''visualize the posterior predictive of a Bayesian polynomial regression model'''
    poly = PolynomialFeatures(poly_degree)
    #transform training x: add polynomial features
    x_poly = poly.fit_transform(x.reshape((-1, 1)))
    #transform x_test: add polynomial features
    x_test_poly = poly.fit_transform(x_test.reshape((-1, 1)))
    #Gaussian log pdf
    gaussian_log_pdf = lambda mu, sigma_sq, x: -0.5 * (np.log(2 * np.pi * sigma_sq) + (x - mu)**2 / sigma_sq)

    #reshape y into 2D array
    y_matrix = y.reshape((-1, 1))

    #define the covariance and precision matrices of the prior on the weights
    prior_variance = np.diag(prior_var * np.ones((x_poly.shape[1], )))
    prior_precision = np.linalg.inv(prior_variance)

    #defining the posterior variance
    joint_variance = np.linalg.inv(prior_precision + 1. / y_var * x_poly.T.dot(x_poly))
    #defining the posterior mean
    joint_mean = joint_variance.dot(x_poly.T.dot(y_matrix)) * 1. / y_var

    #sampling S points from the posterior
    posterior_samples = np.random.multivariate_normal(joint_mean.flatten(), joint_variance, size=S)
    #sampling S points from the posterior predictive
    y_predict_noiseless = np.array([x_test_poly.dot(sample) for sample in posterior_samples])
    y_predict_bayes = y_predict_noiseless + np.random.normal(0, y_var**0.5, size=(S, len(x_test)))
    
    #compute log likelihood for the test data
    log_likelihood_bayes = []
    for n in range(len(y_test)):
        log_likelihood_bayes.append(gaussian_log_pdf(y_predict_noiseless[:, n], y_var, y_test[n]).mean())
    log_likelihood_bayes = np.array(log_likelihood_bayes)
    
    #compute the 95 percentiles of the posterior predictives
    ub_bayes = np.percentile(y_predict_bayes, 97.5, axis=0)
    lb_bayes = np.percentile(y_predict_bayes, 2.5, axis=0)
    
    #visualize the posterior predictive distribution
    ax.scatter(x, y, color='red', s=10, alpha=0.5, label='train data')
    ax.fill_between(x_test, ub_bayes, lb_bayes, color='blue', alpha=0.2, label="test log-likelihood: {}".format(np.round(np.sum(log_likelihood_bayes), 4)))
    ax.set_title('posterior predictive distribution of bayesian regression model with prior variance of {}'.format(prior_var))
    ax.legend(loc='best')
    ax.set_xlim([-10, 10])
    
    return ax, joint_variance, joint_mean

def black_box_variational_inference(logprob, D, num_samples):
    
    """
    Implements http://arxiv.org/abs/1401.0118, and uses the
    local reparameterization trick from http://arxiv.org/abs/1506.02557
    code taken from:
    https://github.com/HIPS/autograd/blob/master/examples/black_box_svi.py
    """

    def unpack_params(params):
        # Variational dist is a diagonal Gaussian.
        mean, log_std = params[:D], params[D:]
        return mean, log_std

    def gaussian_entropy(log_std):
        return 0.5 * D * (1.0 + np.log(2*np.pi)) + np.sum(log_std)

    rs = npr.RandomState(0)
    def variational_objective(params, t):
        """Provides a stochastic estimate of the variational lower bound."""
        mean, log_std = unpack_params(params)
        samples = rs.randn(num_samples, D) * np.exp(log_std) + mean
        lower_bound = gaussian_entropy(log_std) + np.mean(logprob(samples, t))
        return -lower_bound

    gradient = grad(variational_objective)

    return variational_objective, gradient, unpack_params

Hide code cell content

def variational_inference(Sigma_W, sigma_y, y_train, x_train, forward, S, max_iteration, step_size, verbose):
    '''implements wrapper for variational inference via bbb for bayesian regression'''
    D = Sigma_W.shape[0]
    Sigma_W_inv = np.linalg.inv(Sigma_W)
    Sigma_W_det = np.linalg.det(Sigma_W)
    variational_dim = D
    
    #define the log prior on the model parameters
    def log_prior(W):
        constant_W = -0.5 * (D * np.log(2 * np.pi) + np.log(Sigma_W_det))
        exponential_W = -0.5 * np.diag(np.dot(np.dot(W, Sigma_W_inv), W.T))
        log_p_W = constant_W + exponential_W
        return log_p_W

    #define the log likelihood
    def log_lklhd(W):
        S = W.shape[0]
        constant = (-np.log(sigma_y) - 0.5 * np.log(2 * np.pi)) * N
        exponential = -0.5 * sigma_y**-2 * np.sum((y_train.reshape((1, 1, N)) - forward(W, x_train))**2, axis=2).flatten()
        return constant + exponential

    #define the log joint density
    log_density = lambda w, t: log_lklhd(w) + log_prior(w)

    #build variational objective.
    objective, gradient, unpack_params = black_box_variational_inference(log_density, D, num_samples=S)

    def callback(params, t, g):
        if verbose:
            if  t % 100 == 0:
                print("Iteration {} lower bound {}; gradient mag: {}".format(t, -objective(params, t), np.linalg.norm(gradient(params, t))))

    print("Optimizing variational parameters...")
    #initialize variational parameters
    init_mean = np.ones(D)
    init_log_std = -100 * np.ones(D)
    init_var_params = np.concatenate([init_mean, init_log_std])
    
    #perform gradient descent using adam (a type of gradient-based optimizer)
    variational_params = adam(gradient, init_var_params, step_size=step_size, num_iters=max_iteration, callback=callback)
    
    return variational_params 

def variational_polynomial_regression(Sigma_W, sigma_y, x_test, y_test, x_train, y_train, forward, ax, posterior_sample_size=100, S=2000, max_iteration=2000, step_size=1e-2, verbose=True):
    '''perform bayesian regression: infer posterior, visualize posterior predictive, compute log-likelihood'''
    D = Sigma_W.shape[0]
    
    #approximate posterior with mean-field gaussian
    variational_params = variational_inference(Sigma_W, sigma_y, y_train, x_train, forward, S, max_iteration, step_size, verbose)
    
    #sample from the variational posterior
    var_means = variational_params[:D]
    var_variance = np.diag(np.exp(variational_params[D:])**2)
    posterior_samples = np.random.multivariate_normal(var_means, var_variance, size=posterior_sample_size)

    #predict on x_test
    y_predict_noiseless = forward(posterior_samples, x_test)
    y_pred = y_predict_noiseless + np.random.normal(0, sigma_y**0.5, size=(posterior_sample_size, len(x_test)))

    #Gaussian log pdf
    gaussian_log_pdf = lambda mu, sigma_sq, x: -0.5 * (np.log(2 * np.pi * sigma_sq) + (x - mu)**2 / sigma_sq)

    #compute log likelihood for the test data
    log_likelihood = []
    for n in range(len(y_test)):
        log_likelihood.append(gaussian_log_pdf(y_predict_noiseless[:, n], sigma_y, y_test[n]).mean())
    log_likelihood = np.array(log_likelihood).sum()

    #compute the 95 percentiles of the posterior predictives
    ub_bayes = np.percentile(y_pred, 97.5, axis=0)
    lb_bayes = np.percentile(y_pred, 2.5, axis=0)
    
    #visualize the posterior predictive distribution
    ax.scatter(x_train, y_train, color='red', s=10, alpha=0.5, label='train data')
    ax.fill_between(x_test, ub_bayes, lb_bayes, color='green', alpha=0.2, label='test log-likelihood:{}'.format(np.round(log_likelihood, 4)))
    ax.set_title('mean-field Gaussian variational approximation of the posterior')
    ax.legend(loc='best')
    
    return ax, var_variance, var_means

In this chapter

  1. Review of Neural Network Models

  2. Bayesian Neural Networks

  3. Black-box Variational Inference

Hide code cell content

### Import basic libraries
import numpy
import autograd.numpy as np
import autograd.numpy.random as npr
import autograd.scipy.stats.multivariate_normal as mvn
import autograd.scipy.stats.norm as norm
from autograd import grad
from autograd.misc.optimizers import adam
import numpy
import scipy as sp
import pandas as pd
import sklearn as sk
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
import math
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib import rc
from IPython.display import HTML
from IPython.display import YouTubeVideo
%matplotlib inline

22.1. Review of Neural Network Models#

How Would You Parameterize a Non-linear Trend?#

In non-linear regression, we are interested in modeling observed outcome \(Y^{(n)}\) as a non-linear function of observed covariates \(\mathbf{X}^{(n)}\):

\[\begin{split}\begin{aligned} \mu &= g_{\mathbf{w}}(\mathbf{X}^{(n)})\\ Y^{(n)}&\sim \mathcal{N}(\mu, \sigma^2) \end{aligned}\end{split}\]

But it’s not easy to think of a function class \(g(x)\) can capture the trend in the data (e.g. polynomial or sinusoidal)?

../_images/fig12.png

Representing Arbitrarily Complex Functions#

Motto: neural networks build up a complex function \(g\) by composing simple nonlinear functions. We represent neural networks as layered directed graphs where each node \(i\) in the \(l\)-th layer represents the function \(f\left(\sum_{j}w^{l-1}_{ji} h^{l-1}_j\right)\), \(h^{l-1}_j\) being the hidden nodes from the \(l-1\)-th layer.

../_images/fig5.png

This is a neural network. We denote the weights of the neural network collectively by \(\mathbf{W}\). The non-linear function \(f\) is called the activation function.

Design Choices: Depth or Width#

Ideally, we want our architecture to be able to express a number of very complex functions, since we don’t know what is appropriate for our data. So what architecture is more effective for expressing complex functions?

Neural Networks Regression#

Model for Regression: \(Y^{(n)}\sim \mathcal{N}(\mu, \sigma^2)\), \(\mu = g_\mathbf{W}(\mathbf{X}^{(n)})\), where \(g_\mathbf{W}\) is a neural network with parameters \(\mathbf{W}\).

Training Objective: find \(\mathbf{W}\) to maximize the likelihood of our data. This is equivalent to minimizing the Mean Square Error,

\[ \min_{\mathbf{W}}\, \mathrm{MSE}(\mathbf{W}) = \frac{1}{N}\sum^N_{n=1} \left(y_n - g_\mathbf{W}(x_n)\right)^2 \]

Optimizing the Training Objective: The main challenge of optimizing the objective is computing the gradient of a neural network. Luckily, the gradient can be computed in an algorithmic way using the chain rule, working from the output node backwards towards the input. For example, the derivative with respect to the hidden node \(h^l_i\) is:

\[ \frac{\partial \mathrm{MSE}}{\partial h^l_i} = \sum_{j}\underbrace{\frac{\partial \mathrm{MSE}}{\partial h^{l+1}_j}}_{\text{derivative from layer $l\quad$}} \underbrace{\frac{\partial h^{l+1}_j}{\partial h^{l}_i}}_{\text{derivative of $f\left(\sum_{k}w_k h_k\right)$}} \]

Using this backwards gradient computation, we can optimize a neural network with respect to the MSE using gradient descent.

The Maximum Likelihood Objective is Non-Convex for Neural Networks#

Unfortunately, the likelihood and MSE functions for neural network regression models are not convex! This means that just because your gradient is zero, it doesn’t mean you’ve optimized anything.

../_images/loss_landscape.jpg

Neural Network Regression vs Linear Regression#

Linear models are easy to interpret. Once we’ve found the MLE of the model parameters, we can formulate scientific hypotheses about the relationship between the outcome \(Y\) and the covariates \(\mathbf{X}\):

\[\begin{aligned} \widehat{\text{income}} = 2 * \text{education (yr)} + 3.1 * \text{married} - 1.5 * \text{gaps in work history} \end{aligned}\]

What do the weights of a neural network tell you about the relationship between the covariates and the outcome?

../_images/fig5.png

Generalization Error and Bias/Variance#

Complex models have low bias – they can model a wide range of functions, given enough samples.

But complex models like neural networks can use their ‘extra’ capacity to explain non-meaningful features of the training data that are unlikely to appear in the test data (i.e. noise). These models have high variance – they are very sensitive to small changes in the data distribution, leading to drastic performance decrease from train to test settings.

22.2. Bayesian Neural Networks#

Bayesian Polynomial Regression is Bayesian Linear Regression#

A Bayesian polynomial regression model uses a polynomial of degree \(D\) to capture the relationship between \(X\in \mathbb{R}\) and \(Y\):

\[\begin{split}\begin{aligned} \mathbf{W} &\sim \mathcal{N}(0, \sigma^2_W \mathbf{I})\\ \mu^{(n)}&= w_0 + w_1 X^{(n)} + w_2 (X^{(n)})^2 + \ldots + w_D(X^{(n)})^D\\ Y^{(n)} &\sim \mathcal{N}(\mu^{(n)}, \sigma^2_Y) \end{aligned}\end{split}\]

Rather than considering the polynomial as a non-linear function of \(X\), we can see it as a linear function of the vector \(\mathbf{X} = [1, X, X^2, \ldots, X^D] \in \mathbb{R}^{D+1}\):

\[\begin{split}\begin{aligned} \mathbf{W} &\sim \mathcal{N}(0, \sigma^2_W \mathbf{I})\\ \mu^{(n)}&= \mathbf{W}^\top \mathbf{X}^{(n)}\\ Y^{(n)} &\sim \mathcal{N}(\mu^{(n)}, \sigma^2_Y) \end{aligned}\end{split}\]

This means that for a Bayesian polynomial regression model, the posterior \(p(\mathbf{X} | \text{Data})\) is a multivariate Gaussian (just as in the case of Bayesian linear regression, see the Chapter 1 problem set).

Bayesian Neural Networks#

A Bayesian neural network (BNN) is a Bayesian model for regression that uses a neural network to capture the relationship between \(\mathbf{X}\) and \(Y\):

\[\begin{split}\begin{aligned} \mathbf{W} &\sim \mathcal{N}(0, \sigma^2_W \mathbf{I})\\ \mu^{(n)}&= g_{\mathbf{W}}(\mathbf{X}^{(n)})\\ Y^{(n)} &\sim \mathcal{N}(\mu^{(n)}, \sigma^2_Y) \end{aligned}\end{split}\]

Unfortunately, the posterior of a neural network is multimodal and very complex, posing a challenge for samplers. Furthermore, training data for BNNs are typically large, this makes gradient-based samplers like HMC extremely inefficient – in every leap-frog iteration of HMC, the gradient \(\frac{\partial U(q)}{\partial q}\) requires an evaluation over the entire training data set.

Further reading:

  1. NeuTra-lizing Bad Geometry in Hamiltonian Monte Carlo Using Neural Transport

  2. Stochastic Gradient Hamiltonian Monte Carlo

22.3. Black-box Variational Inference#

Review of Variational Inference#

Goal: given a target posterior distribution \(p(\psi | Y_1, \ldots, Y_N)\), \(\psi \in \mathbb{R}^I\) we want to find a distribution \(q(\psi |\lambda^*)\) in a family of distributions \(Q = \{q(\psi |\lambda) | \lambda \in \Lambda \}\) such that \(q(\psi |\lambda^*)\) best approximates \(p\).

Design Choices: we need to choose:

A. (Variational family) a family \(Q\) of candidate distributions for approximating \(p\). The members of \(Q\) are called the variational distributions.

Our Choice: we assume that the joint \(q(\psi)\) factorizes completely over each dimension of \(\psi\), i.e. \(q(\psi)= \prod_{i=1}^I q(\psi_i | \lambda_i)\). This is called the mean field assumption. What can go wrong with this design choice?

B. (Divergence measure) a divergence measure to quantify the difference between \(p\) and \(q\).

Our Choice:

\[D_{\text{KL}}(q(\psi | \lambda) \| p(\psi | Y_1, \ldots, Y_N)) = \mathbb{E}_{\psi \sim q(\psi|\lambda)}\left[\log\left( \frac{q(\psi | \lambda)}{p(\psi | Y_1, \ldots, Y_N)} \right) \right]\]

What can go wrong with this design choice?

Variational Inference as Optimization#

Let’s formalize variational inference for a target distribution \(p(\psi)\): find a \(q(\psi|\lambda^*)\) such that

\[\begin{aligned} \lambda^* = \underset{\lambda}{\text{argmin}}\; D_{\text{KL}}(q(\psi|\lambda) \| p(\psi|Y_1, \ldots, Y_N))) = \underset{\lambda}{\text{argmin}}\; \mathbb{E}_{\psi \sim q(\psi|\lambda)}\left[\log\left(\frac{q(\psi | \lambda)}{p(\psi|Y_1, \ldots, Y_N))}\right) \right] \end{aligned}\]

Recall that for EM, we had proved that minimizing the KL is equivalent to maximizing the ELBO (for which it is easier to compute the gradient). We will do the same here:

\[\begin{split}\begin{aligned} \underset{\lambda}{\min}D_{\text{KL}}(q(\psi|\lambda) \| p(\psi|Y_1, \ldots, Y_N))) \overset{\text{equiv}}{\equiv}& \underset{\lambda}{\max} -D_{\text{KL}}(q(\psi|\lambda) \| p(\psi|Y_1, \ldots, Y_N))) \\ =& \underset{\lambda}{\max} -\mathbb{E}_{\psi \sim q(\psi|\lambda)}\left[\log\left(\frac{q(\psi | \lambda)}{p(\psi|Y_1, \ldots, Y_N))} \right)\right] \\ =& \underset{\lambda}{\max}\underbrace{\mathbb{E}_{\psi \sim q(\psi|\lambda)}\left[\log\left(\frac{p(\psi, Y_1, \ldots, Y_N))}{q(\psi | \lambda)} \right)\right]}_{ELBO(\lambda)} \\ &- \log p(Y_1, \ldots, Y_N). \end{aligned}\end{split}\]

Thus, the variational objective can be rephrased as maximizing the \(ELBO\).

Variational Inference for Bayesian Neural Networks#

Consider a Bayesian neural network:

\[\begin{split}\begin{aligned} \mathbf{W} &\sim \mathcal{N}(0, \sigma^2_W \mathbf{I})\\ Y^{(n)} &\sim \mathcal{N}(g_{\mathbf{W}}(\mathbf{X}^{(n)}), \sigma^2_Y) \end{aligned}\end{split}\]

and a mean-field Gaussian variational family:

\[ q(\mathbf{W} | \mu, \Sigma) = \mathcal{N}(\mathbf{W}; \mu, \Sigma) \]

where \(\Sigma\) is a diagonal matrix.

The ELBO is:

\[\begin{split}\begin{aligned} ELBO(\mu, \Sigma) &= \mathbb{E}_{\mathbf{W} \sim q(\mathbf{W} | \mu, \Sigma)} \left[ \log \left( \frac{p(\mathbf{W}) \prod_{n=1}^N p(Y^{(n)} | \mathbf{X}^{(n)}, \mathbf{W})}{q(\mathbf{W} | \mu, \Sigma)} \right) \right]\\ &= \mathbb{E}_{\mathbf{W} \sim q(\mathbf{W} | \mu, \Sigma)} \left[ \log \left( \frac{\mathcal{N}(\mathbf{W}; 0, \sigma^2_W \mathbf{I}) \prod_{n=1}^N \mathcal{N}(Y^{(n)}; g_{\mathbf{W}}(\mathbf{X}^{(n)}), \sigma^2_Y)}{\mathcal{N}(\mathbf{W}; \mu, \Sigma)} \right) \right]. \end{aligned}\end{split}\]

To find the optimal variational parameters such that \(\mu^*, \Sigma^* = \mathrm{argmax}\, ELBO(\mu, \Sigma)\), we need to compute the gradient of the ELBO:

\[\begin{aligned} \nabla_{\mu, \Sigma}\,\underbrace{\mathbb{E}_{\mathbf{W} \sim q(\mathbf{W} | \mu, \Sigma)} \left[ \log \left( \frac{p(\mathbf{W}) \prod_{n=1}^N p(Y^{(n)} | \mathbf{X}^{(n)}, \mathbf{W})}{q(\mathbf{W} | \mu, \Sigma)} \right) \right]}_{ELBO(\mu, \Sigma)}. \end{aligned}\]

Black-Box Variational Inference#

The Black-box Variational Inference (BBVI) algorithm for BNN’s:

  1. Initialization: pick an initial value \(\mu^{(0)}, \Sigma^{(0)}\)

  2. Gradient Ascent: repeat:

    1. Approximate the gradient

      \[\begin{split}\begin{aligned} \nabla_{\mu, \Sigma} \, ELBO(\mu, \Sigma) &= \mathbb{E}_{\mathbf{W} \sim q(\mathbf{W} | \mu, \Sigma)}\left[ \nabla_{\mu, \Sigma}\, q(\mathbf{W} | \mu, \Sigma) * \log \left( \frac{p(\mathbf{W}) \prod_{n=1}^N p(Y^{(n)} | \mathbf{X}^{(n)}, \mathbf{W})}{q(\mathbf{W} | \mu, \Sigma)} \right) \right]\\ &\approx\underbrace{\frac{1}{S}\sum_{s=1}^S \nabla_{\mu, \Sigma}\, \log q(\mathbf{W}^s | \mu, \Sigma) * \log \left( \frac{p(\mathbf{W}^s) \prod_{n=1}^N p(Y^{(n)} | \mathbf{X}^{(n)}, \mathbf{W}^s)}{q(\mathbf{W}^s | \mu, \Sigma)} \right)}_{{\text{score function gradient}}}, \end{aligned}\end{split}\]

      where \(\mathbf{W}^s\sim q(\mathbf{W} | \mu^{\text{current}}, \Sigma^{\text{current}})\).

    2. Update parameters \((\mu^{\text{current}}, \Sigma^{\text{current}}) \leftarrow (\mu^{\text{current}}, \Sigma^{\text{current}}) + \eta * {\text{score function gradient}}\)

Variance of the Gradient Estimate#

In Black-Box Variational Inference, we estimate the gradient using Monte Carlo estimation (i.e. the score function gradient approximation):

\[ \nabla_{\mu, \Sigma} \, ELBO(\mu, \Sigma) \approx \frac{1}{S} {\sum_{s=1}^S \nabla_{\mu, \Sigma}\,\log q(\mathbf{W}^s | \mu, \Sigma) * \log \left( \frac{p(\mathbf{W}^s) \prod_{n=1}^N p(Y^{(n)} | \mathbf{X}^{(n)}, \mathbf{W}^s)}{q(\mathbf{W}^s | \mu, \Sigma)} \right)}, \]

where \(\mathbf{W}^s\sim q(\mathbf{W} | \mu^{\text{current}}, \Sigma^{\text{current}})\).

As in the case with every MC estimate, we worry about variance. As it turns out, the score function gradient approximation has very high variance. This leads to slow convergence for gradient descent. To mitigate this, we need to employ a number of variance reduction methods.

In the original paper “Black-Box Variational Inference”, the authors implement control variates and Rao-Blackwellization.

Gradient of the ELBO with the Reparametrization Trick#

An alternative to using the log-derivative trick to computing the gradient of the ELBO is to use the reparametrization trick.

We note that since \(q(\mathbf{W} | \mu, \Sigma) = \mathcal{N}(\mathbf{W};\mu, \Sigma )\), sampling \(W\sim q(\mathbf{W} | \mu, \Sigma)\) is equivalent to sampling \(\epsilon \sim \mathcal{N}(0, \mathbf{I})\) and then transforming the sample \(\mathbf{W} = \epsilon^\top \Sigma^{1/2} + \mu\), where \(\mathbf{I}\) and \(\Sigma\) have the same dimensions.

Thus, we can rewrite the ELBO:

../_images/reparametrized_grad.png

Black-Box Variational Inference with the Reparametrization Trick#

The Black-box Variational Inference (BBVI) with the reparametrization trick or Bayes By Backprop algorithm for BNN’s:

  1. Initialization: pick an initial value \(\mu^{(0)}, \Sigma^{(0)}\)

  2. Gradient Ascent: repeat:

    1. Approximate the gradient

      \[\begin{split}\begin{aligned} \nabla_{\mu, \Sigma} \, ELBO(\mu, \Sigma) \approx& \small\frac{1}{S} \sum_{s=1}^S \nabla_{\mu, \Sigma} \log \left[p(\epsilon_s^\top \Sigma^{1/2} + \mu) \prod_{n=1}^N p(Y^{(n)} | \mathbf{X}^{(n)}, \epsilon_s^\top \Sigma^{1/2} + \mu)\right] \\ &+ \nabla_{\mu, \Sigma}\underbrace{-\mathbb{E}_{\mathbf{W} \sim \mathcal{N}(\mu, \Sigma )}\left[\log \mathcal{N}(\mathbf{W};\mu, \Sigma ) \right]}_{\text{Guassian entropy: has closed form}}, \end{aligned}\end{split}\]

      where \(\epsilon_s \sim \mathcal{N}(0, \mathbf{I})\).

    2. Update parameters \((\mu^{\text{current}}, \Sigma^{\text{current}}) \leftarrow (\mu^{\text{current}}, \Sigma^{\text{current}}) + \eta * {\text{reparametrization gradient}}\)

Hide code cell content

##set random seed
rand_state = 0
random = np.random.RandomState(rand_state)

##generate training data
#number of points in each of the two segments of the domain
N = 20
#output variance
y_var = 1.
x_train, y_train, x_test, y_test = generate_data(N, y_var)

##transform covariates for polynomial regression
poly = PolynomialFeatures(1)
#transform x: add polynomial features
x_poly = poly.fit_transform(x_train.reshape((-1, 1)))
#transform x_test: add polynomial features
x_test_poly = poly.fit_transform(x_test.reshape((-1, 1)))

##define dimensions
N = x_poly.shape[0]
D = x_poly.shape[1]

##define variances
sigma_y = 1.**2
weight_noise = 5**2
Sigma_W = weight_noise * np.eye(D)

##polynomial function
def forward(w, x):
    x_poly = poly.fit_transform(x.reshape((-1, 1))) 
    return np.dot(w, x_poly.T)

BBVI for Bayesian Linear Regression (Posterior Predictives)#

posterior_sample_size = 100
##visualize the posterior predictive of a bayesian polynomial regression model
#prior variance
prior_var = 5**2
fig, ax = plt.subplots(1, 2, figsize=(20, 5))
ax[0], joint_variance, joint_mean = bayesian_polynomial_regression(x_train, y_train, x_test, y_test, prior_var, y_var, ax[0], S=posterior_sample_size, poly_degree=1)
ax[1], var_variance, var_mean = variational_polynomial_regression(Sigma_W, sigma_y, x_test, y_test, x_train, y_train, forward, ax[1], posterior_sample_size=posterior_sample_size, S=4000, max_iteration=2000, step_size=1e-1, verbose=False)
plt.show()
Optimizing variational parameters...
../_images/5a4ed04ee41011d3147442dcb70648211b9e61d52d2682c2d133f27770ba432a.png

BBVI for Bayesian Linear Regression (Posteriors)#

Hide code cell content

#define points that include most of the probability mass of the pdf
x, y = numpy.mgrid[-0.33:0.4:.005, 1.925:2.025:.005]
pos = numpy.dstack((x, y))
#get the value of the target pdf at those points
z_p = sp.stats.multivariate_normal(joint_mean.flatten(), joint_variance).pdf(pos)
#get the value of the variational pdf at those points
z_q = sp.stats.multivariate_normal(var_mean, var_variance).pdf(pos)
#plot the target density against variational densities
fig, ax = plt.subplots(1, 1, figsize=(10, 10))
ax.contourf(x, y, z_p, levels=14, cmap='Reds', alpha=0.8)
ax.contour(x, y, z_p, levels=14, cmap='Reds', alpha=0.8)
ax.contour(x, y, z_q, levels=14, cmap='Blues', alpha=1.)
ax.set_title('Approximate posterior (blue) vs actual posterior (red)')
plt.show()
../_images/2815a4f62269da0c8c92208752d6d700a1cc0ec9614e485f039e7cc658df1822.png

Hide code cell content

##set random seed
rand_state = 0
random = np.random.RandomState(rand_state)

##generate training data
#number of points in each of the two segments of the domain
N = 20
#output variance
y_var = 1.
x_train, y_train, x_test, y_test = generate_data(N, y_var)

##transform covariates for polynomial regression
poly = PolynomialFeatures(3)
#transform x: add polynomial features
x_poly = poly.fit_transform(x_train.reshape((-1, 1)))
#transform x_test: add polynomial features
x_test_poly = poly.fit_transform(x_test.reshape((-1, 1)))

##define dimensions
N = x_poly.shape[0]
D = x_poly.shape[1]

##define variances
sigma_y = 1.**2
weight_noise = 5**2
Sigma_W = weight_noise * np.eye(D)

##polynomial function
def forward(w, x):
    x_poly = poly.fit_transform(x.reshape((-1, 1))) 
    return np.dot(w, x_poly.T)

Hide code cell source

posterior_sample_size = 100
##visualize the posterior predictive of a bayesian polynomial regression model
#prior variance
prior_var = 5**2
fig, ax = plt.subplots(1, 2, figsize=(20, 5))
ax[0], joint_variance, joint_mean = bayesian_polynomial_regression(x_train, y_train, x_test, y_test, prior_var, y_var, ax[0], S=posterior_sample_size, poly_degree=1)
ax[1], var_variance, var_mean = variational_polynomial_regression(Sigma_W, sigma_y, x_test, y_test, x_train, y_train, forward, ax[1], posterior_sample_size=posterior_sample_size, S=4000, max_iteration=2000, step_size=1e-1, verbose=False)
plt.show()
Optimizing variational parameters...
../_images/1c6c7c79185acbc51808222a0cb365f6a49a20bcb8b63949d2c28da4963c33cc.png

22.4. Summary#

  1. (Width vs Depth) In a NN architecture, we control the width and the depth of the network. What is the effect of increasing each (independently) on the function that we learn?

  1. (Linear vs Neural Network Models) While both simple and complex models can be interpreted and successfully trained, there are trade-offs between using a complex vs a simple model!

    • Easily interpreted vs interpretations are harder to come by

    • Interpretations are more faithful vs interpretations can be more misleading

    • Training is easier (because objective function is convex) vs training is much harder (because objective function is extremely non-convex)

  2. (Bayesian Neural Networks) Deep Bayes refers to the subfield of ML wherein we try to combine deep learning with Bayesian learning. A Bayesian Neural Network is one example of a deep Bayesian model.

    • Why do we want to make our NNs Bayesian?

    • How do we make NNs Bayesian?

    • How do we sample from the posterior?

      • Will sampling from the posterior using HMC going to work?

      • Can we do variational inference? Now we can - with 4 pieces of technology:

        • algebra tricks to get the gradient past the expectation in the ELBO

        • Monte Carlo estimate of the expectation in the ELBO

        • automatic differentiation

        • gradient descent

        \[\begin{aligned} \nabla_{\mu, \Sigma}\,\underbrace{\mathbb{E}_{\mathbf{W} \sim q(\mathbf{W} | \mu, \Sigma)} \left[ \log \left( \frac{p(\mathbf{W}) \prod_{n=1}^N p(Y^{(n)} | \mathbf{X}^{(n)}, \mathbf{W})}{q(\mathbf{W} | \mu, \Sigma)} \right) \right]}_{ELBO(\mu, \Sigma)}. \end{aligned}\]

Variational Inference for BNNs#

The Black-box Variational Inference (BBVI) algorithm for BNN’s:

  1. Initialization: pick an initial value \(\mu^{(0)}, \Sigma^{(0)}\)

  2. Gradient Ascent: repeat:

    1. Approximate the gradient

      \[\begin{split}\begin{aligned} \nabla_{\mu, \Sigma} \, ELBO(\mu, \Sigma) &= \nabla_{\mu, \Sigma}\,\underbrace{\mathbb{E}_{\mathbf{W} \sim q(\mathbf{W} | \mu, \Sigma)} \left[ \log \left( \frac{p(\mathbf{W}) \prod_{n=1}^N p(Y^{(n)} | \mathbf{X}^{(n)}, \mathbf{W})}{q(\mathbf{W} | \mu, \Sigma)} \right) \right]}_{ELBO(\mu, \Sigma)}\\ &= \underbrace{\mathbb{E}_{\mathbf{W} \sim q(\mathbf{W} | \mu, \Sigma)}\left[ \nabla_{\mu, \Sigma}\, q(\mathbf{W} | \mu, \Sigma) * \log \left( \frac{p(\mathbf{W}) \prod_{n=1}^N p(Y^{(n)} | \mathbf{X}^{(n)}, \mathbf{W})}{q(\mathbf{W} | \mu, \Sigma)} \right) \right]}_{\text{score function gradient}}\\ &\approx\underbrace{\frac{1}{S}\sum_{s=1}^S \nabla_{\mu, \Sigma}\, \log q(\mathbf{W}^s | \mu, \Sigma) * \log \left( \frac{p(\mathbf{W}^s) \prod_{n=1}^N p(Y^{(n)} | \mathbf{X}^{(n)}, \mathbf{W}^s)}{q(\mathbf{W}^s | \mu, \Sigma)} \right)}_{{\text{MC approx. of score function gradient}}}, \end{aligned}\end{split}\]

      where \(\mathbf{W}^s\sim q(\mathbf{W} | \mu^{\text{current}}, \Sigma^{\text{current}})\).

    2. Update parameters \((\mu^{\text{current}}, \Sigma^{\text{current}}) \leftarrow (\mu^{\text{current}}, \Sigma^{\text{current}}) + \eta * {\text{(MC approx. of score function gradient)}}\)

The reparametrization gradient is another way to push the gradient past the expectation in the gradient of the ELBO.

We note that since \(q(\mathbf{W} | \mu, \Sigma) = \mathcal{N}(\mathbf{W};\mu, \Sigma )\), sampling \(W\sim q(\mathbf{W} | \mu, \Sigma)\) is equivalent to sampling \(\epsilon \sim \mathcal{N}(0, \mathbf{I})\) and then transforming the sample \(\mathbf{W} = \epsilon^\top \Sigma^{1/2} + \mu\), where \(\mathbf{I}\) and \(\Sigma\) have the same dimensions.

Thus, we can rewrite the ELBO:

../_images/reparametrized_grad.png

The Black-box Variational Inference (BBVI) with the reparametrization trick algorithm for BNN’s:

  1. Initialization: pick an initial value \(\mu^{(0)}, \Sigma^{(0)}\)

  2. Gradient Ascent: repeat:

    1. Approximate the gradient

      \[\begin{split}\begin{aligned} \nabla_{\mu, \Sigma} \, ELBO(\mu, \Sigma) \approx& \small\frac{1}{S} \sum_{s=1}^S \nabla_{\mu, \Sigma} \log \left[p(\epsilon_s^\top \Sigma^{1/2} + \mu) \prod_{n=1}^N p(Y^{(n)} | \mathbf{X}^{(n)}, \epsilon_s^\top \Sigma^{1/2} + \mu)\right] \\ &+ \nabla_{\mu, \Sigma}\underbrace{-\mathbb{E}_{\mathbf{W} \sim \mathcal{N}(\mu, \Sigma )}\left[\log \mathcal{N}(\mathbf{W};\mu, \Sigma ) \right]}_{\text{Guassian entropy: has closed form}}, \end{aligned}\end{split}\]

      where \(\epsilon_s \sim \mathcal{N}(0, \mathbf{I})\).

    2. Update parameters \((\mu^{\text{current}}, \Sigma^{\text{current}}) \leftarrow (\mu^{\text{current}}, \Sigma^{\text{current}}) + \eta * {\text{reparametrization gradient}}\)

22.5. Exercises#

These problems were originally assigned as homework. Data files are in the book’s data/ folder. Worked solutions, where available, are collapsed beneath the questions they answer; try each problem before expanding them.

Problem Set: Bayesian Neural Networks#

Hide code cell content

from autograd import numpy as np
from autograd import grad
from autograd.misc.optimizers import adam, sgd
from autograd import scipy as sp
import pandas as pd
import numpy
import matplotlib.pyplot as plt
import sys
%matplotlib inline

Hide code cell content

class Feedforward:
    def __init__(self, architecture, random=None, weights=None):
        self.params = {'H': architecture['width'],
                       'L': architecture['hidden_layers'],
                       'D_in': architecture['input_dim'],
                       'D_out': architecture['output_dim'],
                       'activation_type': architecture['activation_fn_type'],
                       'activation_params': architecture['activation_fn_params']}

        self.D = (  (architecture['input_dim'] * architecture['width'] + architecture['width'])
                  + (architecture['output_dim'] * architecture['width'] + architecture['output_dim'])
                  + (architecture['hidden_layers'] - 1) * (architecture['width']**2 + architecture['width'])
                 )

        if random is not None:
            self.random = random
        else:
            self.random = np.random.RandomState(0)

        self.h = architecture['activation_fn']

        if weights is None:
            self.weights = self.random.normal(0, 1, size=(1, self.D))
        else:
            self.weights = weights

        self.objective_trace = np.empty((1, 1))
        self.weight_trace = np.empty((1, self.D))


    def forward(self, weights, x):
        ''' Forward pass given weights and input '''
        H = self.params['H']
        D_in = self.params['D_in']
        D_out = self.params['D_out']

        assert weights.shape[1] == self.D

        if len(x.shape) == 2:
            assert x.shape[0] == D_in
            x = x.reshape((1, D_in, -1))
        else:
            assert x.shape[1] == D_in

        weights = weights.T


        #input to first hidden layer
        W = weights[:H * D_in].T.reshape((-1, H, D_in))
        b = weights[H * D_in:H * D_in + H].T.reshape((-1, H, 1))
        input = self.h(np.matmul(W, x) + b)
        index = H * D_in + H

        assert input.shape[1] == H

        #additional hidden layers
        for _ in range(self.params['L'] - 1):
            before = index
            W = weights[index:index + H * H].T.reshape((-1, H, H))
            index += H * H
            b = weights[index:index + H].T.reshape((-1, H, 1))
            index += H
            output = np.matmul(W, input) + b
            input = self.h(output)

            assert input.shape[1] == H

        #output layer
        W = weights[index:index + H * D_out].T.reshape((-1, D_out, H))
        b = weights[index + H * D_out:].T.reshape((-1, D_out, 1))
        output = np.matmul(W, input) + b
        assert output.shape[1] == self.params['D_out']

        return output

    def make_objective(self, x_train, y_train, reg_param):

        def objective(W, t):
            squared_error = np.linalg.norm(y_train - self.forward(W, x_train), axis=1)**2
            if reg_param is None:
                sum_error = np.sum(squared_error)
                return sum_error
            else:
                mean_error = np.mean(squared_error) + reg_param * np.linalg.norm(W)
                return mean_error

        return objective, grad(objective)

    def fit(self, x_train, y_train, params, reg_param=None):

        assert x_train.shape[0] == self.params['D_in']
        assert y_train.shape[0] == self.params['D_out']

        ### make objective function for training
        self.objective, self.gradient = self.make_objective(x_train, y_train, reg_param)

        ### set up optimization
        step_size = 0.01
        max_iteration = 5000
        check_point = 100
        weights_init = self.weights.reshape((1, -1))
        mass = None
        optimizer = 'adam'
        random_restarts = 5

        if 'step_size' in params.keys():
            step_size = params['step_size']
        if 'max_iteration' in params.keys():
            max_iteration = params['max_iteration']
        if 'check_point' in params.keys():
            self.check_point = params['check_point']
        if 'init' in params.keys():
            weights_init = params['init']
        if 'call_back' in params.keys():
            call_back = params['call_back']
        if 'mass' in params.keys():
            mass = params['mass']
        if 'optimizer' in params.keys():
            optimizer = params['optimizer']
        if 'random_restarts' in params.keys():
            random_restarts = params['random_restarts']

        def call_back(weights, iteration, g):
            ''' Actions per optimization step '''
            objective = self.objective(weights, iteration)
            self.objective_trace = np.vstack((self.objective_trace, objective))
            self.weight_trace = np.vstack((self.weight_trace, weights))
            if iteration % check_point == 0:
                print("Iteration {} lower bound {}; gradient mag: {}".format(iteration, objective, np.linalg.norm(self.gradient(weights, iteration))))

        ### train with random restarts
        optimal_obj = 1e16
        optimal_weights = self.weights

        for i in range(random_restarts):
            if optimizer == 'adam':
                adam(self.gradient, weights_init, step_size=step_size, num_iters=max_iteration, callback=call_back)
            local_opt = np.min(self.objective_trace[-100:])
            if local_opt < optimal_obj:
                opt_index = np.argmin(self.objective_trace[-100:])
                self.weights = self.weight_trace[-100:][opt_index].reshape((1, -1))
            weights_init = self.random.normal(0, 1, size=(1, self.D))

        self.objective_trace = self.objective_trace[1:]
        self.weight_trace = self.weight_trace[1:]

Bayesian Neural Network Regression#

In this problem set you will explore Bayesian models for regression with neural networks – essentially, you will place priors on the network parameters \(\mathbf{W}\) and infer the posterior \(p(\mathbf{W}|\text{Data})\). These Bayesian models are called Bayesian neural networks. The data for this regression problem is in HW8_data.csv. You are provided an implementation of a neural network as well as an example of how to use it – but feel free to implement your own version!

Hide code cell content

#read in the data
data = pd.read_csv('../data/HW8_data.csv')
x_train = data['x'].values.reshape((1, -1))
y_train = data['y'].values.reshape((1, -1))
data.head()

Let’s instantiate a neural network with 1-hidden layer, 5-hidden nodes and relu activation:

Hide code cell content

###relu activation
activation_fn_type = 'relu'
activation_fn = lambda x: np.maximum(np.zeros(x.shape), x)


###neural network model design choices
width = 5
hidden_layers = 1
input_dim = 1
output_dim = 1

architecture = {'width': width,
               'hidden_layers': hidden_layers,
               'input_dim': input_dim,
               'output_dim': output_dim,
               'activation_fn_type': 'relu',
               'activation_fn_params': 'rate=1',
               'activation_fn': activation_fn}

#set random state to make the experiments replicable
rand_state = 0
random = np.random.RandomState(rand_state)

#instantiate a Feedforward neural network object
nn = Feedforward(architecture, random=random)

Fit the neural network to the data and visualize the fit.

Hide code cell content

###define design choices in gradient descent
params = {'step_size':1e-3, 
          'max_iteration':15000, 
          'random_restarts':1}

#fit my neural network to minimize MSE on the given data
nn.fit(x_train, y_train, params)

Hide code cell content

#test x-values
x_test = np.linspace(-8, 8, 100).reshape((1, -1))
#predict on the test x-values
y_test_pred = nn.forward(nn.weights, x_test)
#visualize the function learned by the neural network
plt.scatter(x_train.flatten(), y_train.flatten(), color='black', label='data')
plt.plot(x_test.flatten(), y_test_pred.flatten(), color='red', label='learned neural network function')
plt.legend(loc='best')
plt.show()

Exercise: Part I: Inference for Bayesian Neural Networks Using HMC

  1. (The Bayesian Neural Network Model) We will implement the following Bayesian model for the data:

    \[\begin{split}\begin{aligned} \mathbf{W} &\sim \mathcal{N}(0, 5^2 \mathbf{I}_{D\times D})\\ \mu^{(n)} &= g_{\mathbf{W}}(\mathbf{X}^{(n)})\\ Y^{(n)} &\sim \mathcal{N}(\mu^{(n)}, 0.5^2)\\ \end{aligned}\end{split}\]

    where \(g_{\mathbf{W}}\) is a neural network with parameters \(\mathbf{W}\) represented as a vector in \(\mathbb{R}^{D}\) with \(D\) being the total number of parameters (including biases).

    Implement the log of the joint distribution in autograd’s version of numpy, i.e. implement \(\log \left[p(\mathbf{W})\prod_{n=1}^N p(Y^{(n)} |\mathbf{X}^{(n)} , \mathbf{W}) \right]\). Hint: you’ll need to write out the log of the various Gaussian pdf’s and implement their formulae using autograd’s numpy functions.

  1. (Sample from the Posterior) Use HMC to sample from the posterior of the Bayesian neural network in Question 1 with 1 hidden layer and 5 hidden nodes. Start with the following settings for your sampler:

params = {'step_size':1e-3, 
          'leapfrog_steps':50, 
          'total_samples':10000, 
          'burn_in':.1, 
          'thinning_factor':2,
          'position_init': nn.weights}

Note that you should initialize with the MLE model, otherwise convergence may be slow. Please feel free to tweak these design choices as you see fit!

  1. (Visualize the Posterior Predictive) Visualize 100 samples, randomly selected, from your posterior samples of \(\mathbf{W}\), by plotting their predicted values plus a random noise \(\epsilon \sim \mathcal{N}(0, 0.5^2)\) at 100 equally spaced x-values between -8 and 8:

x_test = np.linspace(-8, 8, 100)
y_test = nn.forward(sample, x_test.reshape((1, -1))) 
y_test += np.random.normal(0, 0.5, size=y_test.shape)

where sample is a posterior sample of \(\mathbf{W}\).

  1. (Model Evaluation) Discuss the fit of your model to the data. Discuss also what the posterior predictive tell you about the aleatoric and epistemic uncertainty of the model. Are these uncertainties what you’d want (think about want kind of uncertainties you want and where)?

Exercise: Part II: Inference for BNNs Using Black-Box Variational Inference with the Reparametrization Trick

  1. (BBVI with the Reparametrization Trick) Implement BBVI with the reparametrization trick for approximating an arbitrary posterior \(p(w| \text{Data})\) by an isotropic Gaussian \(\mathcal{N}(\mu, \Sigma)\), where \(\Sigma\) is a diagonal matrix. See the derivation earlier in this chapter or the example code from autograd’s github repo.

    Unit Test: You should check that your implementation is correct by approximating the posterior of the following Bayesian logistic regression model:

    \[\begin{split}\begin{aligned} w &\sim \mathcal{N}(0, 1)\\ Y^{(n)} &\sim Ber(\text{sigm}(wX^{(n)} + 10)) \end{aligned}\end{split}\]

    where \(w\), \(Y^{(n)}\), \(X^{(n)}\) are a real scalar valued random variables, and where the data consists of a single observation \((Y=1, X=-20)\).

    The true posterior \(p(w | Y=1, X=-20)\) should look like the following (i.e. the true posterior is left-skewed):

../_images/logistic_posterior.png

Your mean-field variational approximation should be a Gaussian with mean around -0.321 and standard deviation around 0.876.

  1. (Variational Inference for BNNs) For the Bayesian model you implemented in Part I, Questions 1 and 2, use BBVI with the reparametrization trick to approximate the posterior of the Bayesian neural network with a mean-field Gaussian variational family (i.e. an isotropic Gaussian). Please set learning rate and maximum iteration choices as you see fit!

  1. (Visualize the Posterior Predictive) Visualize 100 samples \(\mathbf{W}^s\) from your approximate posterior of \(\mathbf{W}\) by plotting the neural network outputs with weight \(\mathbf{W}^s\) plus a random noise \(\epsilon \sim \mathcal{N}(0, 0.5^2)\) at 100 equally spaced x-values between -8 and 8:

x_test = np.linspace(-8, 8, 100)
y_test = nn.forward(sample, x_test.reshape((1, -1))) 
y_test += np.random.normal(0, 0.5, size=y_test.shape)

where sample is a sample from the approximate posterior of \(\mathbf{W}\).

  1. (Model Evaluation) Discuss the fit of your model to the data. Discuss also what the posterior predictive tell you about the aleatoric and epistemic uncertainty of the model. Are these uncertainties what you’d want (think about want kind of uncertainties you want and where)?

Exercise: Part III: Comparison

  1. (Model Evaluation) Compare the posterior predictive visualization from BBVI with the reparametrization trick to the one you obtained using HMC. Can you say whether or not your posterior approximation is good? How does approximating the posterior effect our estimation of epistemic and aleatoric uncertainty?

  1. (Quality of Posterior Vs Quality of Posterior Predictive) Based on your convergence diagnostics of your HMC sampler, do you believe that you’ve obtained a set of representative samples from the BNN posterior? Why or why not?

    Based on your understanding of BNN posteriors, do you believe that a mean-field Gaussian is a good approximation of the BNN posterior? Why or why not?

    Which set of samples, HMC or VI, do you believe better captures the BNN posterior?

    Based on the empirical results from above, do you believe there is a clear positive relationship between posterior approximation and quality of posterior predictive uncertainties - i.e. is it true that the more I capture of the BNN posterior the better the quality of my posterior predictive uncertainties?

  1. (Speed and Scalability) Compare the two inference methods, HMC and BBVI, and discuss the scalability of each with respect to the number of model parameters and number of data points (how does increasing the number of model parameters or the number of data points affect each inference method?).

22.6. Further Reading#

Variational Inference for Bayesian Neural Networks