Here I test out LMC on a simple problem: fitting a linear model with Gaussian intrinsic scatter to data.
Jake VanderPlas has an excellent example featuring several other Python MCMC packages here, and for ease of comparison I have set up an identical data set to analyze below.
In [1]:
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['xtick.labelsize'] = 'x-large'
plt.rcParams['ytick.labelsize'] = 'x-large'
import corner
%matplotlib inline
In [2]:
np.random.seed(42)
true_alpha = 25.0
true_beta = 0.5
true_sigma = 10.0
xs = 100.0 * np.random.random(20)
ys = true_alpha + true_beta * xs
# It's a bit weird to add scatter in x like this and then not
# account for it in the model, but for consistency...
# Anyway, with such a shallow slope, it doesn't throw off the fit
# too much.
xs = np.random.normal(xs, 10.0)
ys = np.random.normal(ys, true_sigma)
In [3]:
plt.plot(xs, ys, 'o');
plt.xlabel('x', fontsize=14);
plt.ylabel('y', fontsize=14);
In [4]:
# Following JV, define separate functions for the prior, likelihood
# and posterior.
# I'll be lazy and define the parameter objects at global scope.
# (The actual declarations of alpha, beta and sigma will come later.)
# In a complex fitting code, this would be very bad practice, but
# for simple scripts like this it should be fine.
# Similarly, I will just use the global `xs` and `ys` data.
# Consequently, these functions do not need arguments!
def lnPrior():
if sigma() <= 0.0:
return -np.inf
# NB "uniform in angle" slope prior as in the example we're following
# Jeffreys prior for the scatter
return -1.5*np.log(1 + beta()** 2) - np.log(sigma())
def lnLikelihood():
ymod = alpha() + beta()*xs
return np.sum( -0.5*((ys - ymod) / sigma())**2 - np.log(sigma()) )
# A quirk of LMC is that the log_posterior function must have 1 argument
# but, given the choices above, we won't actually use it.
def lnPosterior(junk=None):
return lnPrior() + lnLikelihood()
In [5]:
import lmc
In [6]:
# Here we define Parameter objects for each free parameter, and bundle them
# into a ParameterSpace that knows about the posterior function.
# Each Parameter is given a starting value and a guess at the appropriate step size.
alpha = lmc.Parameter(25.0, 1.0, 'alpha')
beta = lmc.Parameter(0.5, 0.05, 'beta')
sigma = lmc.Parameter(10.0, 1.0, 'sigma')
space = lmc.ParameterSpace([alpha, beta, sigma], lnPosterior)
In [7]:
# Run a few chains and save them in separate dictionaries.
# There will be vector named by each Parameter.name, as well as one for the corresponding
# log-posterior values.
# We're using all the default setting here, which means that each chain will adapt its
# proposal distribution, but we're not taking advantage of LMC's parallel features. That would
# certainly make things a bit faster. Running the chains in parallel instead of serially provides
# an obvious speedup, but in addition each one would converge to the posterior more quickly.
Nchains = 4
Nsteps = 26000 # to get 1e5 total samples after removing 1e3 burn-in
chains = [lmc.dictBackend() for j in range(Nchains)]
for ch in chains:
v = lmc.Vehicle(space, ch)
%time v(1,Nsteps)
In [8]:
plt.rcParams['figure.figsize'] = (12.0, 3.0)
for ch in chains:
plt.plot(ch['alpha'], 'o', ms=3.0);
plt.xlabel('step', fontsize=14);
plt.ylabel('alpha', fontsize=14);
In [9]:
plt.rcParams['figure.figsize'] = (12.0, 3.0)
for ch in chains:
plt.plot(ch['beta'], 'o', ms=3.0);
plt.xlabel('step', fontsize=14);
plt.ylabel('beta', fontsize=14);
In [10]:
plt.rcParams['figure.figsize'] = (12.0, 3.0)
for ch in chains:
plt.plot(ch['sigma'], 'o', ms=3.0);
plt.xlabel('step', fontsize=14);
plt.ylabel('sigma', fontsize=14);
Let's zoom in on the beginning a bit
In [11]:
plt.rcParams['figure.figsize'] = (12.0, 3.0)
for ch in chains:
plt.plot(np.array(ch['alpha'])[range(1000)], 'o', ms=3.0);
plt.xlabel('step', fontsize=14);
plt.ylabel('alpha', fontsize=14);
In [12]:
plt.rcParams['figure.figsize'] = (12.0, 3.0)
for ch in chains:
plt.plot(np.array(ch['beta'])[range(1000)], 'o', ms=3.0);
plt.xlabel('step', fontsize=14);
plt.ylabel('beta', fontsize=14);
In [13]:
plt.rcParams['figure.figsize'] = (12.0, 3.0)
for ch in chains:
plt.plot(np.array(ch['sigma'])[range(1000)], 'o', ms=3.0);
plt.xlabel('step', fontsize=14);
plt.ylabel('sigma', fontsize=14);
In [14]:
burn = 1000
post = np.array([np.concatenate([np.array(ch[k])[np.arange(burn+1, len(ch[k]))] for ch in chains])
for k in [p.name for p in space]]).T
post.shape
Out[14]:
In [15]:
corner.corner(post[:,range(3)], labels=[p.name for p in space], bins=50, color='b',
truths=[true_alpha, true_beta, true_sigma], truth_color='r',show_titles=True);