Skip to content

Commit

Permalink
Merge e9eefd4 into d373bba
Browse files Browse the repository at this point in the history
  • Loading branch information
fonnesbeck committed Feb 19, 2017
2 parents d373bba + e9eefd4 commit 09f8dc8
Show file tree
Hide file tree
Showing 6 changed files with 449 additions and 139 deletions.
337 changes: 215 additions & 122 deletions docs/source/notebooks/GP-introduction.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pymc3/distributions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from .distribution import NoDistribution
from .distribution import TensorType
from .distribution import draw_values
from .distribution import generate_samples

from .mixture import Mixture
from .mixture import NormalMixture
Expand Down
2 changes: 2 additions & 0 deletions pymc3/gp/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from . import cov
from . import mean
from .gp import GP, sample_gp
34 changes: 17 additions & 17 deletions pymc3/gp/cov.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def __init__(self, input_dim, active_dims=None):
self.active_dims = np.array(active_dims)
assert len(active_dims) == input_dim

def K(self, X, Z):
def __call__(self, X, Z):
R"""
Evaluate the covariance function.
Expand Down Expand Up @@ -78,14 +78,14 @@ def __init__(self, factor_list):
self.factor_list.append(factor)

class Add(Combination):
def K(self, X, Z=None):
def __call__(self, X, Z=None):
return reduce((lambda x, y: x + y),
[k.K(X, Z) if isinstance(k, Covariance) else k for k in self.factor_list])
[k(X, Z) if isinstance(k, Covariance) else k for k in self.factor_list])

class Prod(Combination):
def K(self, X, Z=None):
def __call__(self, X, Z=None):
return reduce((lambda x, y: x * y),
[k.K(X, Z) if isinstance(k, Covariance) else k for k in self.factor_list])
[k(X, Z) if isinstance(k, Covariance) else k for k in self.factor_list])


class Stationary(Covariance):
Expand Down Expand Up @@ -129,7 +129,7 @@ class ExpQuad(Stationary):
k(x, x') = \mathrm{exp}\left[ -\frac{(x - x')^2}{2 \ell^2} \right]
"""

def K(self, X, Z=None):
def __call__(self, X, Z=None):
X, Z = self._slice(X, Z)
return tt.exp( -0.5 * self.square_dist(X, Z))

Expand All @@ -148,7 +148,7 @@ def __init__(self, input_dim, lengthscales, alpha, active_dims=None):
self.lengthscales = lengthscales
self.alpha = alpha

def K(self, X, Z=None):
def __call__(self, X, Z=None):
X, Z = self._slice(X, Z)
return tt.power((1.0 + 0.5 * self.square_dist(X, Z) * (1.0 / self.alpha)), -1.0 * self.alpha)

Expand All @@ -162,7 +162,7 @@ class Matern52(Stationary):
k(x, x') = \left(1 + \frac{\sqrt{5(x - x')^2}}{\ell} + \frac{5(x-x')^2}{3\ell^2}\right) \mathrm{exp}\left[ - \frac{\sqrt{5(x - x')^2}}{\ell} \right]
"""

def K(self, X, Z=None):
def __call__(self, X, Z=None):
X, Z = self._slice(X, Z)
r = self.euclidean_dist(X, Z)
return (1.0 + np.sqrt(5.0) * r + 5.0 / 3.0 * tt.square(r)) * tt.exp(-1.0 * np.sqrt(5.0) * r)
Expand All @@ -177,7 +177,7 @@ class Matern32(Stationary):
k(x, x') = \left(1 + \frac{\sqrt{3(x - x')^2}}{\ell}\right)\mathrm{exp}\left[ - \frac{\sqrt{3(x - x')^2}}{\ell} \right]
"""

def K(self, X, Z=None):
def __call__(self, X, Z=None):
X, Z = self._slice(X, Z)
r = self.euclidean_dist(X, Z)
return (1.0 + np.sqrt(3.0) * r) * tt.exp(-np.sqrt(3.0) * r)
Expand All @@ -192,7 +192,7 @@ class Exponential(Stationary):
k(x, x') = \mathrm{exp}\left[ -\frac{||x - x'||}{2\ell^2} \right]
"""

def K(self, X, Z=None):
def __call__(self, X, Z=None):
X, Z = self._slice(X, Z)
return tt.exp(-0.5 * self.euclidean_dist(X, Z))

Expand All @@ -205,7 +205,7 @@ class Cosine(Stationary):
k(x, x') = \mathrm{cos}\left( \frac{||x - x'||}{ \ell^2} \right)
"""

def K(self, X, Z=None):
def __call__(self, X, Z=None):
X, Z = self._slice(X, Z)
return tt.cos(np.pi * self.euclidean_dist(X, Z))

Expand All @@ -222,7 +222,7 @@ def __init__(self, input_dim, c, active_dims=None):
Covariance.__init__(self, input_dim, active_dims)
self.c = c

def K(self, X, Z=None):
def __call__(self, X, Z=None):
X, Z = self._slice(X, Z)
Xc = tt.sub(X, self.c)
if Z is None:
Expand All @@ -245,8 +245,8 @@ def __init__(self, input_dim, c, d, offset, active_dims=None):
self.d = d
self.offset = offset

def K(self, X, Z=None):
return tt.power(Linear.K(self, X, Z) + self.offset, self.d)
def __call__(self, X, Z=None):
return tt.power(Linear(self, X, Z) + self.offset, self.d)


class WarpedInput(Covariance):
Expand Down Expand Up @@ -274,12 +274,12 @@ def __init__(self, input_dim, cov_func, warp_func, args=None, active_dims=None):
self.args = args
self.cov_func = cov_func

def K(self, X, Z=None):
def __call__(self, X, Z=None):
X, Z = self._slice(X, Z)
if Z is None:
return self.cov_func.K(self.w(X, self.args), Z)
return self.cov_func(self.w(X, self.args), Z)
else:
return self.cov_func.K(self.w(X, self.args), self.w(Z, self.args))
return self.cov_func(self.w(X, self.args), self.w(Z, self.args))


def handle_args(func, args):
Expand Down
128 changes: 128 additions & 0 deletions pymc3/gp/gp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import numpy as np
from scipy import stats
from tqdm import tqdm

from theano.tensor.nlinalg import matrix_inverse, det
import theano.tensor as tt
import theano

from .mean import Zero
from ..distributions import MvNormal, Continuous, draw_values, generate_samples
from ..model import modelcontext


__all__ = ['GP', 'sample_gp']

class GP(Continuous):
"""Gausian process
Parameters
----------
mean_func : Mean
Mean function of Gaussian process
cov_func : Covariance
Covariance function of Gaussian process
sigma : scalar or array
Observation noise (defaults to zero)
"""
def __init__(self, mean_func=None, cov_func=None, sigma=0, *args, **kwargs):
super(GP, self).__init__(*args, **kwargs)

if mean_func is None:
self.M = Zero()
else:
self.M = mean_func

if cov_func is None:
raise ValueError('A covariance function must be specified for GPP')
self.K = cov_func

self.sigma = sigma

def random(self, point=None, size=None, **kwargs):
X = kwargs.pop('X')
mu, cov = draw_values([self.M(X).squeeze(), self.K(X) + np.eye(X.shape[0])*self.sigma**2], point=point)

def _random(mean, cov, size=None):
return stats.multivariate_normal.rvs(
mean, cov, None if size == mean.shape else size)

samples = generate_samples(_random,
mean=mu, cov=cov,
dist_shape=self.shape,
broadcast_shape=mu.shape,
size=size)
return samples

def logp(self, X, Y):
mu = self.M(X)
Sigma = self.K(X) + tt.eye(X.shape[0])*self.sigma**2

return MvNormal.dist(mu, Sigma).logp(Y)


def sample_gp(trace, gp, X_values, samples=None, obs_noise=True, model=None, random_seed=None, progressbar=True):
"""Generate samples from a posterior Gaussian process.
Parameters
----------
trace : backend, list, or MultiTrace
Trace generated from MCMC sampling.
gp : Gaussian process object
The GP variable to sample from.
X_values : array
Grid of values at which to sample GP.
samples : int
Number of posterior predictive samples to generate. Defaults to the
length of `trace`
obs_noise : bool
Flag for including observation noise in sample. Defaults to True.
model : Model
Model used to generate `trace`. Optional if in `with` context manager.
random_seed : integer > 0
Random number seed for sampling.
progressbar : bool
Flag for showing progress bar.
Returns
-------
Array of samples from posterior GP evaluated at Z.
"""
model = modelcontext(model)

if samples is None:
samples = len(trace)

if random_seed:
np.random.seed(random_seed)

if progressbar:
indices = tqdm(np.random.randint(0, len(trace), samples), total=samples)
else:
indices = np.random.randint(0, len(trace), samples)

K = gp.distribution.K

data = [v for v in model.observed_RVs if v.name==gp.name][0].data

X = data['X']
Y = data['Y']
Z = X_values

S_xz = K(X, Z)
S_zz = K(Z)
if obs_noise:
S_inv = matrix_inverse(K(X) + tt.eye(X.shape[0])*gp.distribution.sigma**2)
else:
S_inv = matrix_inverse(K(X))

# Posterior mean
m_post = tt.dot(tt.dot(S_xz.T, S_inv), Y)
# Posterior covariance
S_post = S_zz - tt.dot(tt.dot(S_xz.T, S_inv), S_xz)

gp_post = MvNormal.dist(m_post, S_post, shape=Z.shape[0])

samples = [gp_post.random(point=trace[idx]) for idx in indices]

return np.array(samples)
86 changes: 86 additions & 0 deletions pymc3/gp/mean.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import theano.tensor as tt

__all__ = ['Zero', 'Constant']

class Mean(object):
"""
Base class for mean functions
"""

def __call__(self, X):
R"""
Evaluate the mean function.
Parameters
----------
X : The training inputs to the mean function.
"""
raise NotImplementedError

def __add__(self, other):
return Add(self, other)

def __mul__(self, other):
return Prod(self, other)

class Zero(Mean):
def __call__(self, X):
return tt.zeros(X.shape, dtype='float32').squeeze()

class Constant(Mean):
"""
Constant mean function for Gaussian process.
Parameters
----------
c : variable, array or integer
Constant mean value
"""

def __init__(self, c=0):
Mean.__init__(self)
self.c = c

def __call__(self, X):
return tt.tile(tt.stack(self.c), X.shape).squeeze()

class Linear(Mean):

def __init__(self, coeffs, intercept=0):
"""
Linear mean function for Gaussian process.
Parameters
----------
coeffs : variables
Linear coefficients
intercept : variable, array or integer
Intercept for linear function (Defaults to zero)
"""
Mean.__init__(self)
self.b = intercept
self.A = coeffs

def __call__(self, X):
return tt.dot(X, self.A) + self.b


class Add(Mean):
def __init__(self, first_mean, second_mean):
Mean.__init__(self)
self.m1 = first_mean
self.m2 = second_mean

def __call__(self, X):
return tt.add(self.m1(X), self.m2(X))


class Prod(Mean):
def __init__(self, first_mean, second_mean):
Mean.__init__(self)
self.m1 = first_mean
self.m2 = second_mean

def __call__(self, X):
return tt.mul(self.m1(X), self.m2(X))

0 comments on commit 09f8dc8

Please sign in to comment.