#!/usr/bin/env python # coding: utf-8 # # Linear basis function models with PyMC4 # In[1]: import logging import pymc4 as pm import numpy as np import arviz as az import tensorflow as tf import tensorflow_probability as tfp print(pm.__version__) print(tf.__version__) print(tfp.__version__) # Mute Tensorflow warnings ... logging.getLogger('tensorflow').setLevel(logging.ERROR) # ## Linear basis function models # # The following is a PyMC4 implementation of [Bayesian regression with linear basis function models](https://nbviewer.jupyter.org/github/krasserm/bayesian-machine-learning/blob/dev/bayesian-linear-regression/bayesian_linear_regression.ipynb). To recap, a linear regression model is a linear function of the parameters but not necessarily of the input. Input $x$ can be expanded with a set of non-linear basis functions $\phi_j(x)$, where $(\phi_1(x), \dots, \phi_M(x))^T = \boldsymbol\phi(x)$, for modeling a non-linear relationship between input $x$ and a function value $y$. # # $$ # y(x, \mathbf{w}) = w_0 + \sum_{j=1}^{M}{w_j \phi_j(x)} = w_0 + \mathbf{w}_{1:}^T \boldsymbol\phi(x) \tag{1} # $$ # # For simplicity I'm using a scalar input $x$ here. Target variable $t$ is given by the deterministic function $y(x, \mathbf{w})$ and Gaussian noise $\epsilon$. # # $$ # t = y(x, \mathbf{w}) + \epsilon \tag{2} # $$ # # Here, we can choose between polynomial and Gaussian basis functions for expanding input $x$. # In[6]: from functools import partial from scipy.stats import norm def polynomial_basis(x, power): return x ** power def gaussian_basis(x, mu, sigma): return norm(loc=mu, scale=sigma).pdf(x).astype(np.float32) def _expand(x, bf, bf_args): return np.stack([bf(x, bf_arg) for bf_arg in bf_args], axis=1) def expand_polynomial(x, degree=3): return _expand(x, bf=polynomial_basis, bf_args=range(1, degree + 1)) def expand_gaussian(x, mus=np.linspace(0, 1, 9), sigma=0.3): return _expand(x, bf=partial(gaussian_basis, sigma=sigma), bf_args=mus) # Choose between polynomial and Gaussian expansion # (by switching the comment on the following two lines) expand = expand_polynomial #expand = expand_gaussian # For example, to expand two input values `[0.5, 1.5]` into a polynomial design matrix of degree `3` we can use # In[7]: expand_polynomial(np.array([0.5, 1.5]), degree=3) # The power of `0` is omitted here and covered by a $w_0$ in the model. # ## Example dataset # # The example dataset consists of `N` noisy samples from a sinusoidal function `f`. # In[8]: import matplotlib.pyplot as plt get_ipython().run_line_magic('matplotlib', 'inline') from bayesian_linear_regression_util import ( plot_data, plot_truth ) def f(x, noise=0): """Sinusoidal function with optional Gaussian noise.""" return 0.5 + np.sin(2 * np.pi * x) + np.random.normal(scale=noise, size=x.shape) # Number of samples N = 10 # Constant noise noise = 0.3 # Noisy samples x = np.linspace(0, 1, N, dtype=np.float32) t = f(x, noise=noise) # Noise-free ground truth x_test = np.linspace(0, 1, 100).astype(np.float32) y_true = f(x_test) plot_data(x, t) plot_truth(x_test, y_true) # ## Implementation with PyMC4 # # ### Model definition # The model definition directly follows from Eq. $(1)$ and Eq. $(2)$ with normal priors over parameters. The size of parameter vector `w_r` ($\mathbf{w}_{1:}$ in Eq. $(1)$) is determined by the number of basis functions and set via the `batch_stack` parameter. With the above default settings, it is 3 for polynomial expansion and 9 for Gaussian expansion. # In[9]: import tensorflow as tf @pm.model def model(Phi, t, sigma=noise): """Linear model generator. Args: - Phi: design matrix (N,M) - t: noisy target values (N,) - sigma: known noise of t """ w_0 = yield pm.Normal(name='w_0', loc=0, scale=10) w_r = yield pm.Normal(name='w_r', loc=0, scale=10, batch_stack=Phi.shape[1]) mu = w_0 + tf.tensordot(w_r, Phi.T, axes=1) yield pm.Normal(name='t_obs', loc=mu, scale=sigma, observed=t) # ### Inference # Tensorflow will automatically run inference on a GPU if available. With the current version of PyMC4, inference on a GPU is quite slow compared to a multi-core CPU (need to investigate that in more detail). To enforce inference on a CPU set environment variable `CUDA_VISIBLE_DEVICES` to an empty value. There is no progress bar visible yet during sampling but the following shouldn't take longer than a 1 minute. # In[10]: trace = pm.sample(model(expand(x), t), num_chains=3, burn_in=100, num_samples=1000) # In[11]: az.plot_trace(trace); # In[12]: az.plot_posterior(trace, var_names="model/w_0"); az.plot_posterior(trace, var_names="model/w_r"); # ### Prediction # # To obtain posterior predictive samples for a test set `x_test` we simply call the model generator function again with the expanded test set. This is a nice improvement over PyMC3 which required to setup a shared Theano variable for setting test set values. Target values are ignored during predictive sampling, only the shape of the target array `t` matters. # In[13]: draws_posterior = pm.sample_posterior_predictive(model(expand(x_test), t=np.zeros_like(x_test)), trace, inplace=False) draws_posterior.posterior_predictive # The predictive mean and standard deviation is obtained by averaging over chains (axis `0`) and predictive samples (axis `1`) for each of the 100 data points in `x_test` (axis `2`). # In[14]: predictive_samples = draws_posterior.posterior_predictive.data_vars['model/t_obs'].values m = np.mean(predictive_samples, axis=(0, 1)) s = np.std(predictive_samples, axis=(0, 1)) # These statistics can be used to plot model predictions and their uncertainties (together with the ground truth and the noisy training dataset). # In[15]: plt.fill_between(x_test, m + s, m - s, alpha = 0.5, label='Predictive std. dev.') plt.plot(x_test, m, label='Predictive mean'); plot_data(x, t) plot_truth(x_test, y_true, label=None) plt.legend(); # Try running the example again with Gaussian expansion i.e. setting `expand = expand_gaussian` and see how it compares to polynomial expansion. Also try running with a different number of basis functions by overriding the default arguments of `expand_polynomial` and `expand_gaussian`. You can find more PyMC4 examples in the [notebooks](https://github.com/pymc-devs/pymc4/tree/master/notebooks) diretory of the PyMC4 project.