ptgp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ptgp/__init__.py +62 -0
- ptgp/_version.py +24 -0
- ptgp/conditionals.py +72 -0
- ptgp/gp/__init__.py +11 -0
- ptgp/gp/svgp.py +299 -0
- ptgp/gp/unapproximated.py +69 -0
- ptgp/gp/vfe.py +92 -0
- ptgp/idata.py +189 -0
- ptgp/inducing.py +723 -0
- ptgp/inducing_fourier.py +380 -0
- ptgp/kernels/__init__.py +21 -0
- ptgp/kernels/base.py +69 -0
- ptgp/kernels/categorical.py +101 -0
- ptgp/kernels/combination.py +66 -0
- ptgp/kernels/nonstationary.py +166 -0
- ptgp/kernels/stationary.py +116 -0
- ptgp/kl.py +59 -0
- ptgp/likelihoods/__init__.py +8 -0
- ptgp/likelihoods/base.py +98 -0
- ptgp/likelihoods/bernoulli.py +48 -0
- ptgp/likelihoods/gaussian.py +66 -0
- ptgp/likelihoods/negative_binomial.py +44 -0
- ptgp/likelihoods/poisson.py +41 -0
- ptgp/likelihoods/student_t.py +40 -0
- ptgp/linalg/__init__.py +2 -0
- ptgp/linalg/operator.py +4 -0
- ptgp/linalg/ops.py +5 -0
- ptgp/linalg/rewrites.py +4 -0
- ptgp/mean.py +40 -0
- ptgp/objectives.py +327 -0
- ptgp/optim/__init__.py +30 -0
- ptgp/optim/api.py +214 -0
- ptgp/optim/optimizers.py +140 -0
- ptgp/optim/schedules.py +75 -0
- ptgp/optim/training.py +1251 -0
- ptgp/rewrites.py +172 -0
- ptgp/utils.py +412 -0
- ptgp-0.1.0.dist-info/METADATA +165 -0
- ptgp-0.1.0.dist-info/RECORD +41 -0
- ptgp-0.1.0.dist-info/WHEEL +4 -0
- ptgp-0.1.0.dist-info/licenses/LICENSE +21 -0
ptgp/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from ptgp import (
|
|
5
|
+
gp,
|
|
6
|
+
idata,
|
|
7
|
+
inducing,
|
|
8
|
+
kernels,
|
|
9
|
+
likelihoods,
|
|
10
|
+
mean,
|
|
11
|
+
objectives,
|
|
12
|
+
optim,
|
|
13
|
+
rewrites, # noqa: F401 (registers PyTensor rewrites at import)
|
|
14
|
+
utils,
|
|
15
|
+
)
|
|
16
|
+
from ptgp.inducing_fourier import FourierFeatures1D
|
|
17
|
+
from ptgp.optim.api import FitResult, fit, predict
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _StdoutHandler(logging.StreamHandler):
|
|
21
|
+
"""Handler that resolves sys.stdout at emit time, not at init time."""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
super().__init__()
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def stream(self):
|
|
28
|
+
return sys.stdout
|
|
29
|
+
|
|
30
|
+
@stream.setter
|
|
31
|
+
def stream(self, _):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_logger = logging.getLogger("ptgp")
|
|
36
|
+
if not _logger.handlers:
|
|
37
|
+
_handler = _StdoutHandler()
|
|
38
|
+
_handler.setFormatter(logging.Formatter("%(message)s"))
|
|
39
|
+
_logger.addHandler(_handler)
|
|
40
|
+
_logger.setLevel(logging.INFO)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"FourierFeatures1D",
|
|
44
|
+
"FitResult",
|
|
45
|
+
"fit",
|
|
46
|
+
"gp",
|
|
47
|
+
"idata",
|
|
48
|
+
"inducing",
|
|
49
|
+
"kernels",
|
|
50
|
+
"likelihoods",
|
|
51
|
+
"mean",
|
|
52
|
+
"objectives",
|
|
53
|
+
"optim",
|
|
54
|
+
"predict",
|
|
55
|
+
"utils",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
from ptgp._version import __version__
|
|
61
|
+
except ImportError:
|
|
62
|
+
__version__ = "0.0.0+unknown"
|
ptgp/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
ptgp/conditionals.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import pytensor.assumptions as pta
|
|
2
|
+
import pytensor.tensor as pt
|
|
3
|
+
|
|
4
|
+
# Diagonal jitter added to Kmm before Cholesky / inversion, to keep it PSD
|
|
5
|
+
# under floating-point noise. Matches GPflow / GPJax / PyMC defaults of 1e-6.
|
|
6
|
+
_DEFAULT_JITTER = 1e-6
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def conditional_whitened(A_white, Knn, f, q_sqrt=None, full_cov=False):
|
|
10
|
+
"""Posterior conditional in whitened parameterisation.
|
|
11
|
+
|
|
12
|
+
A_white : (M, N), satisfies A_white = R^{-1} @ Kmn where R @ R.T = Kmm.
|
|
13
|
+
Knn : (N,) prior diagonal if ``full_cov=False``, else (N, N) prior covariance.
|
|
14
|
+
f : (M,) variational mean (whitened: prior on v is N(0, I)).
|
|
15
|
+
q_sqrt : (M, K), optional.
|
|
16
|
+
full_cov: if True, return full (N, N) covariance; else (N,) marginals.
|
|
17
|
+
"""
|
|
18
|
+
fmean = A_white.T @ f
|
|
19
|
+
if full_cov:
|
|
20
|
+
fvar = Knn - A_white.T @ A_white
|
|
21
|
+
else:
|
|
22
|
+
fvar = Knn - pt.sum(A_white**2, axis=0)
|
|
23
|
+
if q_sqrt is not None:
|
|
24
|
+
B = A_white.T @ q_sqrt
|
|
25
|
+
if full_cov:
|
|
26
|
+
fvar = fvar + B @ B.T
|
|
27
|
+
else:
|
|
28
|
+
fvar = fvar + pt.sum(B**2, axis=1)
|
|
29
|
+
return fmean, fvar
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def conditional_unwhitened(A, Kmn, Knn, f, q_sqrt=None, full_cov=False):
|
|
33
|
+
"""Posterior conditional in unwhitened parameterisation.
|
|
34
|
+
|
|
35
|
+
A : (M, N), satisfies A = Kmm^{-1} @ Kmn.
|
|
36
|
+
Kmn : (M, N) cross-covariance.
|
|
37
|
+
Knn : (N,) prior diagonal if ``full_cov=False``, else (N, N) prior covariance.
|
|
38
|
+
f : (M,) variational mean (prior on u is N(0, Kmm)).
|
|
39
|
+
full_cov: if True, return full (N, N) covariance; else (N,) marginals.
|
|
40
|
+
"""
|
|
41
|
+
fmean = A.T @ f
|
|
42
|
+
if full_cov:
|
|
43
|
+
fvar = Knn - A.T @ Kmn
|
|
44
|
+
else:
|
|
45
|
+
fvar = Knn - pt.sum(A * Kmn, axis=0)
|
|
46
|
+
if q_sqrt is not None:
|
|
47
|
+
B = A.T @ q_sqrt
|
|
48
|
+
if full_cov:
|
|
49
|
+
fvar = fvar + B @ B.T
|
|
50
|
+
else:
|
|
51
|
+
fvar = fvar + pt.sum(B**2, axis=1)
|
|
52
|
+
return fmean, fvar
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def base_conditional(Kmn, Kmm, Knn, f, q_sqrt=None, white=False, full_cov=False):
|
|
56
|
+
"""Back-compat wrapper. Materialises Kmm; use the helpers above directly
|
|
57
|
+
when you have a structured Kuu_solve / Kuu_sqrt_solve."""
|
|
58
|
+
# Add jitter to keep Kmm PSD under float noise — matches GPflow / PyMC default.
|
|
59
|
+
# Re-annotate after the addition: PyTensor canonicalizes ``Kmm + c·I`` into a
|
|
60
|
+
# ``set_subtensor`` on the diagonal, which our PSD-inference rules don't see
|
|
61
|
+
# through. The mathematical identity (PSD + c·I PSD ⇒ PSD) is sound.
|
|
62
|
+
Kmm = pta.assume(
|
|
63
|
+
Kmm + _DEFAULT_JITTER * pt.eye(Kmm.shape[-1], dtype=Kmm.dtype),
|
|
64
|
+
positive_definite=True,
|
|
65
|
+
symmetric=True,
|
|
66
|
+
)
|
|
67
|
+
if white:
|
|
68
|
+
L = pt.linalg.cholesky(Kmm)
|
|
69
|
+
A_white = pt.linalg.solve(L, Kmn)
|
|
70
|
+
return conditional_whitened(A_white, Knn, f, q_sqrt, full_cov=full_cov)
|
|
71
|
+
A = pt.linalg.inv(Kmm) @ Kmn
|
|
72
|
+
return conditional_unwhitened(A, Kmn, Knn, f, q_sqrt, full_cov=full_cov)
|
ptgp/gp/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from ptgp.gp.svgp import SVGP, VariationalParams, init_variational_params
|
|
2
|
+
from ptgp.gp.unapproximated import Unapproximated
|
|
3
|
+
from ptgp.gp.vfe import VFE
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"Unapproximated",
|
|
7
|
+
"VFE",
|
|
8
|
+
"SVGP",
|
|
9
|
+
"VariationalParams",
|
|
10
|
+
"init_variational_params",
|
|
11
|
+
]
|
ptgp/gp/svgp.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytensor.assumptions as pta
|
|
5
|
+
import pytensor.tensor as pt
|
|
6
|
+
|
|
7
|
+
from ptgp.conditionals import conditional_unwhitened, conditional_whitened
|
|
8
|
+
from ptgp.kl import gauss_kl, gauss_kl_structured
|
|
9
|
+
from ptgp.mean import Zero
|
|
10
|
+
from ptgp.objectives import elbo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _softplus_lower_triangular(flat, M):
|
|
14
|
+
"""Build M×M lower-triangular matrix from a flat vector, with softplus on the diagonal.
|
|
15
|
+
|
|
16
|
+
The flat vector has length M·(M+1)/2 in row-major lower-triangular layout
|
|
17
|
+
(matching ``np.tril_indices(M)``). Diagonal entries are passed through
|
|
18
|
+
``pt.softplus`` so the result is a lower-triangular Cholesky factor with
|
|
19
|
+
strictly positive diagonal at every optimizer step.
|
|
20
|
+
"""
|
|
21
|
+
rows, cols = np.tril_indices(M)
|
|
22
|
+
L = pt.set_subtensor(pt.zeros((M, M))[rows, cols], flat)
|
|
23
|
+
diag_idx = np.arange(M)
|
|
24
|
+
L = pt.set_subtensor(L[diag_idx, diag_idx], pt.softplus(L[diag_idx, diag_idx]))
|
|
25
|
+
return L
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _matrix_to_softplus_flat_init(L_init, M):
|
|
29
|
+
"""Inverse of ``_softplus_lower_triangular`` at init time.
|
|
30
|
+
|
|
31
|
+
Given an M×M lower-triangular Cholesky factor with strictly positive
|
|
32
|
+
diagonal, return the flat vector ``flat`` such that
|
|
33
|
+
``_softplus_lower_triangular(flat, M)`` evaluates to ``L_init``.
|
|
34
|
+
"""
|
|
35
|
+
if L_init.shape != (M, M):
|
|
36
|
+
raise ValueError(f"L_init must have shape ({M}, {M}); got {L_init.shape}.")
|
|
37
|
+
diag_vals = np.diag(L_init)
|
|
38
|
+
if np.any(diag_vals <= 0):
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"L_init must have strictly positive diagonal (it represents a "
|
|
41
|
+
"Cholesky factor under softplus parameterization)."
|
|
42
|
+
)
|
|
43
|
+
rows, cols = np.tril_indices(M)
|
|
44
|
+
flat = L_init[rows, cols].astype(np.float64)
|
|
45
|
+
diag_positions = np.cumsum(np.arange(1, M + 1)) - 1
|
|
46
|
+
flat[diag_positions] = np.log(np.expm1(diag_vals.astype(np.float64)))
|
|
47
|
+
return flat
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclasses.dataclass(frozen=True)
|
|
51
|
+
class VariationalParams:
|
|
52
|
+
"""Symbolic variational parameters for SVGP.
|
|
53
|
+
|
|
54
|
+
Attributes
|
|
55
|
+
----------
|
|
56
|
+
q_mu : TensorVariable
|
|
57
|
+
Symbolic variational mean, shape ``(M,)``. Pass to ``SVGP`` via
|
|
58
|
+
``variational_params=...``.
|
|
59
|
+
q_sqrt : TensorVariable
|
|
60
|
+
Symbolic Cholesky factor of the variational covariance, shape ``(M, M)``,
|
|
61
|
+
annotated ``lower_triangular=True``. Pass to ``SVGP`` via
|
|
62
|
+
``variational_params=...``.
|
|
63
|
+
extra_vars : tuple of TensorVariable
|
|
64
|
+
Trainable leaves underlying ``q_mu`` and ``q_sqrt``.
|
|
65
|
+
extra_init : tuple of ndarray
|
|
66
|
+
Initial values for ``extra_vars``, in matching order.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
q_mu: object
|
|
70
|
+
q_sqrt: object
|
|
71
|
+
extra_vars: tuple = ()
|
|
72
|
+
extra_init: tuple = ()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def init_variational_params(M, q_mu_init=None, q_sqrt_init=None):
|
|
76
|
+
"""Build symbolic variational parameters with GPJax-style parameterization.
|
|
77
|
+
|
|
78
|
+
``q_sqrt`` is stored as a flat vector of length ``M·(M+1)/2`` and
|
|
79
|
+
materialised by filling a lower-triangular matrix and applying
|
|
80
|
+
``pt.softplus`` to the diagonal — guarantees a true Cholesky factor
|
|
81
|
+
(lower-triangular with strictly positive diagonal) at every optimizer step.
|
|
82
|
+
``q_mu`` is stored as a length-``M`` flat vector.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
M : int
|
|
87
|
+
Number of inducing points.
|
|
88
|
+
q_mu_init : ndarray of shape (M,), optional
|
|
89
|
+
Initial variational mean. Defaults to zeros.
|
|
90
|
+
q_sqrt_init : ndarray of shape (M, M), optional
|
|
91
|
+
Initial Cholesky factor. Must be lower-triangular with positive diagonal.
|
|
92
|
+
Defaults to the identity matrix.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
VariationalParams
|
|
97
|
+
|
|
98
|
+
Examples
|
|
99
|
+
--------
|
|
100
|
+
>>> vp = init_variational_params(M=8)
|
|
101
|
+
>>> svgp = SVGP(kernel=..., likelihood=..., inducing_variable=..., variational_params=vp)
|
|
102
|
+
>>> train_step, _, _ = compile_training_step(
|
|
103
|
+
... elbo,
|
|
104
|
+
... svgp,
|
|
105
|
+
... X,
|
|
106
|
+
... y,
|
|
107
|
+
... model=model,
|
|
108
|
+
... extra_vars=vp.extra_vars,
|
|
109
|
+
... extra_init=vp.extra_init,
|
|
110
|
+
... )
|
|
111
|
+
"""
|
|
112
|
+
if q_mu_init is None:
|
|
113
|
+
q_mu_init = np.zeros(M, dtype=np.float64)
|
|
114
|
+
else:
|
|
115
|
+
q_mu_init = np.asarray(q_mu_init, dtype=np.float64)
|
|
116
|
+
if q_mu_init.shape != (M,):
|
|
117
|
+
raise ValueError(f"q_mu_init must have shape ({M},); got {q_mu_init.shape}.")
|
|
118
|
+
if q_sqrt_init is None:
|
|
119
|
+
q_sqrt_init = np.eye(M, dtype=np.float64)
|
|
120
|
+
else:
|
|
121
|
+
q_sqrt_init = np.asarray(q_sqrt_init, dtype=np.float64)
|
|
122
|
+
|
|
123
|
+
n_lower = M * (M + 1) // 2
|
|
124
|
+
q_mu = pt.vector("q_mu", shape=(M,), dtype="float64")
|
|
125
|
+
q_sqrt_flat = pt.vector("q_sqrt_flat", shape=(n_lower,), dtype="float64")
|
|
126
|
+
q_sqrt = pta.assume(
|
|
127
|
+
_softplus_lower_triangular(q_sqrt_flat, M),
|
|
128
|
+
lower_triangular=True,
|
|
129
|
+
)
|
|
130
|
+
flat_init = _matrix_to_softplus_flat_init(q_sqrt_init, M)
|
|
131
|
+
return VariationalParams(
|
|
132
|
+
q_mu=q_mu,
|
|
133
|
+
q_sqrt=q_sqrt,
|
|
134
|
+
extra_vars=(q_mu, q_sqrt_flat),
|
|
135
|
+
extra_init=(q_mu_init, flat_init),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class SVGP:
|
|
140
|
+
"""Stochastic Variational Gaussian Process.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
kernel : Kernel
|
|
145
|
+
Covariance function.
|
|
146
|
+
mean : callable, optional
|
|
147
|
+
Mean function (default: Zero()).
|
|
148
|
+
likelihood : Likelihood
|
|
149
|
+
Observation likelihood.
|
|
150
|
+
inducing_variable : InducingVariables
|
|
151
|
+
Inducing point locations (or structured inducing variables).
|
|
152
|
+
variational_params : VariationalParams
|
|
153
|
+
Symbolic variational parameters and their backing trainable
|
|
154
|
+
leaves. Construct via :func:`init_variational_params` (recommended)
|
|
155
|
+
or build a :class:`VariationalParams` directly for custom
|
|
156
|
+
parameterizations. ``vp.extra_vars`` / ``vp.extra_init`` should be
|
|
157
|
+
passed to ``compile_training_step``.
|
|
158
|
+
whiten : bool
|
|
159
|
+
If True, use whitened variational parameterization (default True).
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
default_objective = staticmethod(elbo)
|
|
163
|
+
predict_needs_data = False
|
|
164
|
+
|
|
165
|
+
def __init__(
|
|
166
|
+
self,
|
|
167
|
+
kernel,
|
|
168
|
+
mean=None,
|
|
169
|
+
likelihood=None,
|
|
170
|
+
inducing_variable=None,
|
|
171
|
+
variational_params=None,
|
|
172
|
+
whiten=True,
|
|
173
|
+
):
|
|
174
|
+
if variational_params is None:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
"SVGP requires variational_params. Construct via "
|
|
177
|
+
"ptgp.gp.init_variational_params(M) and pass as "
|
|
178
|
+
"variational_params=..., or build a VariationalParams directly."
|
|
179
|
+
)
|
|
180
|
+
self.kernel = kernel
|
|
181
|
+
self.mean = mean if mean is not None else Zero()
|
|
182
|
+
self.likelihood = likelihood
|
|
183
|
+
self.inducing_variable = inducing_variable
|
|
184
|
+
self.whiten = whiten
|
|
185
|
+
self.variational_params = variational_params
|
|
186
|
+
self.q_mu = variational_params.q_mu
|
|
187
|
+
self.q_sqrt = variational_params.q_sqrt
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def extra_vars(self):
|
|
191
|
+
return (
|
|
192
|
+
*self.variational_params.extra_vars,
|
|
193
|
+
*self.inducing_variable.extra_vars,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def extra_init(self):
|
|
198
|
+
return (
|
|
199
|
+
*self.variational_params.extra_init,
|
|
200
|
+
*self.inducing_variable.extra_init,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def predict_marginal(self, X, incl_lik=False):
|
|
204
|
+
"""Posterior marginal mean and variance at each point in X.
|
|
205
|
+
|
|
206
|
+
Returns the per-point posterior — correlations between test
|
|
207
|
+
points are discarded. Use ``predict_joint`` for the full (N, N)
|
|
208
|
+
covariance or ``predict_f_samples`` to draw smooth function samples.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
X : tensor, shape (N, D)
|
|
213
|
+
incl_lik : bool
|
|
214
|
+
If True, push through the likelihood's predictive mean/var
|
|
215
|
+
(observation-space uncertainty).
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
mean : tensor, shape (N,)
|
|
220
|
+
var : tensor, shape (N,)
|
|
221
|
+
"""
|
|
222
|
+
ind, kernel = self.inducing_variable, self.kernel
|
|
223
|
+
Kmn = ind.K_uf(kernel, X)
|
|
224
|
+
Knn_diag = kernel.diag(X)
|
|
225
|
+
if self.whiten:
|
|
226
|
+
A_w = ind.Kuu_sqrt_solve(kernel, Kmn)
|
|
227
|
+
fmean, fvar = conditional_whitened(A_w, Knn_diag, self.q_mu, self.q_sqrt)
|
|
228
|
+
else:
|
|
229
|
+
A = ind.Kuu_solve(kernel, Kmn)
|
|
230
|
+
fmean, fvar = conditional_unwhitened(A, Kmn, Knn_diag, self.q_mu, self.q_sqrt)
|
|
231
|
+
fmean = fmean + self.mean(X)
|
|
232
|
+
if incl_lik:
|
|
233
|
+
return self.likelihood.predict_mean_and_var(fmean, fvar)
|
|
234
|
+
return fmean, fvar
|
|
235
|
+
|
|
236
|
+
def predict_joint(self, X):
|
|
237
|
+
"""Posterior joint mean and full covariance of the latent f at X.
|
|
238
|
+
|
|
239
|
+
The diagonal of the returned covariance equals the variance from
|
|
240
|
+
``predict_marginal``; the off-diagonals capture correlations
|
|
241
|
+
between test points, which are needed to draw smooth samples.
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
X : tensor, shape (N, D)
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
mean : tensor, shape (N,)
|
|
250
|
+
cov : tensor, shape (N, N)
|
|
251
|
+
"""
|
|
252
|
+
ind, kernel = self.inducing_variable, self.kernel
|
|
253
|
+
Kmn = ind.K_uf(kernel, X)
|
|
254
|
+
Knn = kernel(X)
|
|
255
|
+
if self.whiten:
|
|
256
|
+
A_w = ind.Kuu_sqrt_solve(kernel, Kmn)
|
|
257
|
+
fmean, fcov = conditional_whitened(A_w, Knn, self.q_mu, self.q_sqrt, full_cov=True)
|
|
258
|
+
else:
|
|
259
|
+
A = ind.Kuu_solve(kernel, Kmn)
|
|
260
|
+
fmean, fcov = conditional_unwhitened(A, Kmn, Knn, self.q_mu, self.q_sqrt, full_cov=True)
|
|
261
|
+
fmean = fmean + self.mean(X)
|
|
262
|
+
return fmean, fcov
|
|
263
|
+
|
|
264
|
+
def predict_f_samples(self, X, epsilon, jitter=1e-6):
|
|
265
|
+
"""Draw samples of the latent f at X from the joint posterior.
|
|
266
|
+
|
|
267
|
+
Samples are produced via a Cholesky transform of caller-supplied
|
|
268
|
+
iid-standard-normal noise. The caller owns the RNG — the function
|
|
269
|
+
itself is deterministic.
|
|
270
|
+
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
X : tensor, shape (N, D)
|
|
274
|
+
epsilon : tensor, shape (S, N)
|
|
275
|
+
iid N(0, 1) draws. ``S`` is the number of samples.
|
|
276
|
+
jitter : float
|
|
277
|
+
Added to the diagonal of the posterior covariance before
|
|
278
|
+
Cholesky, for numerical stability (default 1e-6).
|
|
279
|
+
|
|
280
|
+
Returns
|
|
281
|
+
-------
|
|
282
|
+
samples : tensor, shape (S, N)
|
|
283
|
+
"""
|
|
284
|
+
fmean, fcov = self.predict_joint(X)
|
|
285
|
+
N = X.shape[0]
|
|
286
|
+
L = pt.linalg.cholesky(fcov + jitter * pt.eye(N))
|
|
287
|
+
return fmean[None, :] + epsilon @ L.T
|
|
288
|
+
|
|
289
|
+
def prior_kl(self):
|
|
290
|
+
"""KL divergence KL[q(u) || p(u)]."""
|
|
291
|
+
if self.whiten:
|
|
292
|
+
return gauss_kl(self.q_mu, self.q_sqrt, K=None)
|
|
293
|
+
ind, kernel = self.inducing_variable, self.kernel
|
|
294
|
+
return gauss_kl_structured(
|
|
295
|
+
self.q_mu,
|
|
296
|
+
self.q_sqrt,
|
|
297
|
+
K_solve=lambda rhs: ind.Kuu_solve(kernel, rhs),
|
|
298
|
+
K_logdet=ind.Kuu_logdet(kernel),
|
|
299
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import pytensor.tensor as pt
|
|
2
|
+
|
|
3
|
+
from ptgp.likelihoods import Gaussian
|
|
4
|
+
from ptgp.mean import Zero
|
|
5
|
+
from ptgp.objectives import marginal_log_likelihood
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Unapproximated:
|
|
9
|
+
"""Exact (unapproximated) Gaussian process.
|
|
10
|
+
|
|
11
|
+
The observation model is Gaussian; parameterize the noise via ``sigma``.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
kernel : Kernel
|
|
16
|
+
Covariance function.
|
|
17
|
+
mean : callable, optional
|
|
18
|
+
Mean function (default: ``Zero()``).
|
|
19
|
+
sigma : tensor or PyMC random variable
|
|
20
|
+
Observation noise standard deviation.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
extra_vars = ()
|
|
24
|
+
extra_init = ()
|
|
25
|
+
default_objective = staticmethod(marginal_log_likelihood)
|
|
26
|
+
predict_needs_data = True
|
|
27
|
+
|
|
28
|
+
def __init__(self, kernel, mean=None, sigma=None):
|
|
29
|
+
"""Store the kernel and mean; build a Gaussian likelihood from sigma."""
|
|
30
|
+
self.kernel = kernel
|
|
31
|
+
self.mean = mean if mean is not None else Zero()
|
|
32
|
+
self.likelihood = Gaussian(sigma)
|
|
33
|
+
|
|
34
|
+
def predict_marginal(self, X_new, X_train, y_train, incl_lik=False):
|
|
35
|
+
"""Posterior marginal mean and variance at each point in X_new.
|
|
36
|
+
|
|
37
|
+
Returns the per-point posterior; correlations between test points
|
|
38
|
+
are discarded.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
X_new : tensor, shape (N*, D)
|
|
43
|
+
X_train : tensor, shape (N, D)
|
|
44
|
+
y_train : tensor, shape (N,)
|
|
45
|
+
incl_lik : bool
|
|
46
|
+
If True, include likelihood noise in the predictions.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
mean : tensor, shape (N*,)
|
|
51
|
+
var : tensor, shape (N*,)
|
|
52
|
+
"""
|
|
53
|
+
Knn = self.kernel(X_train)
|
|
54
|
+
sigma = self.likelihood.sigma
|
|
55
|
+
sigma2_train = sigma**2 * pt.ones(X_train.shape[0]) # (N,); scalar broadcasts
|
|
56
|
+
Knn_noisy = Knn + pt.diag(sigma2_train)
|
|
57
|
+
Kns = self.kernel(X_train, X_new) # (N, N*)
|
|
58
|
+
Kss_diag = self.kernel.diag(X_new)
|
|
59
|
+
|
|
60
|
+
Knn_inv = pt.linalg.inv(Knn_noisy)
|
|
61
|
+
|
|
62
|
+
mu_train = self.mean(X_train)
|
|
63
|
+
fmean = self.mean(X_new) + Kns.T @ Knn_inv @ (y_train - mu_train)
|
|
64
|
+
fvar = Kss_diag - pt.sum(Kns * (Knn_inv @ Kns), axis=0)
|
|
65
|
+
|
|
66
|
+
if incl_lik:
|
|
67
|
+
sigma_new = self.likelihood.sigma_at(X_train, X_new)
|
|
68
|
+
return fmean, fvar + sigma_new**2
|
|
69
|
+
return fmean, fvar
|
ptgp/gp/vfe.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import pytensor.tensor as pt
|
|
2
|
+
|
|
3
|
+
from ptgp.likelihoods import Gaussian
|
|
4
|
+
from ptgp.mean import Zero
|
|
5
|
+
from ptgp.objectives import collapsed_elbo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VFE:
|
|
9
|
+
"""Variational Free Energy (SGPR) sparse Gaussian Process.
|
|
10
|
+
|
|
11
|
+
Uses Titsias' collapsed bound — inducing variables are analytically
|
|
12
|
+
integrated out. The observation model is Gaussian; parameterize the noise
|
|
13
|
+
via ``sigma``.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
kernel : Kernel
|
|
18
|
+
Covariance function.
|
|
19
|
+
mean : callable, optional
|
|
20
|
+
Mean function (default: ``Zero()``).
|
|
21
|
+
sigma : tensor or PyMC random variable
|
|
22
|
+
Observation noise standard deviation.
|
|
23
|
+
inducing_variable : InducingVariables
|
|
24
|
+
Inducing point locations.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
default_objective = staticmethod(collapsed_elbo)
|
|
28
|
+
predict_needs_data = True
|
|
29
|
+
|
|
30
|
+
def __init__(self, kernel, mean=None, sigma=None, inducing_variable=None):
|
|
31
|
+
"""Store the kernel, mean, and inducing variable; build a Gaussian likelihood from sigma."""
|
|
32
|
+
if not hasattr(inducing_variable, "Z"):
|
|
33
|
+
raise TypeError(
|
|
34
|
+
f"VFE requires inducing variables with a .Z attribute "
|
|
35
|
+
f"(got {type(inducing_variable).__name__}). "
|
|
36
|
+
f"Use SVGP for structured inducing variables like FourierFeatures1D."
|
|
37
|
+
)
|
|
38
|
+
self.kernel = kernel
|
|
39
|
+
self.mean = mean if mean is not None else Zero()
|
|
40
|
+
self.likelihood = Gaussian(sigma)
|
|
41
|
+
self.inducing_variable = inducing_variable
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def extra_vars(self):
|
|
45
|
+
return tuple(self.inducing_variable.extra_vars)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def extra_init(self):
|
|
49
|
+
return tuple(self.inducing_variable.extra_init)
|
|
50
|
+
|
|
51
|
+
def predict_marginal(self, X_new, X_train, y_train, incl_lik=False):
|
|
52
|
+
"""Posterior marginal mean and variance at each point in X_new.
|
|
53
|
+
|
|
54
|
+
Returns the per-point posterior; correlations between test points
|
|
55
|
+
are discarded.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
X_new : tensor, shape (N*, D)
|
|
60
|
+
X_train : tensor, shape (N, D)
|
|
61
|
+
y_train : tensor, shape (N,)
|
|
62
|
+
incl_lik : bool
|
|
63
|
+
If True, include likelihood noise in the predictions.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
mean : tensor, shape (N*,)
|
|
68
|
+
var : tensor, shape (N*,)
|
|
69
|
+
"""
|
|
70
|
+
Z = self.inducing_variable.Z
|
|
71
|
+
sigma = self.likelihood.sigma
|
|
72
|
+
sigma2_vec = sigma**2 * pt.ones(X_train.shape[0]) # (N,); scalar broadcasts
|
|
73
|
+
|
|
74
|
+
Kuu = self.kernel(Z) # (M, M)
|
|
75
|
+
Kuf = self.kernel(Z, X_train) # (M, N)
|
|
76
|
+
Kus = self.kernel(Z, X_new) # (M, N*)
|
|
77
|
+
Kss_diag = self.kernel.diag(X_new)
|
|
78
|
+
|
|
79
|
+
diff = y_train - self.mean(X_train)
|
|
80
|
+
Kuf_laminv = Kuf / sigma2_vec[None, :] # each column ÷ σᵢ²
|
|
81
|
+
Sigma = Kuu + Kuf_laminv @ Kuf.T
|
|
82
|
+
Sigma_inv = pt.linalg.inv(Sigma)
|
|
83
|
+
alpha = Sigma_inv @ Kuf_laminv @ diff
|
|
84
|
+
|
|
85
|
+
fmean = self.mean(X_new) + Kus.T @ alpha
|
|
86
|
+
Kuu_inv = pt.linalg.inv(Kuu)
|
|
87
|
+
fvar = Kss_diag - pt.sum(Kus * ((Kuu_inv - Sigma_inv) @ Kus), axis=0)
|
|
88
|
+
|
|
89
|
+
if incl_lik:
|
|
90
|
+
sigma_new = self.likelihood.sigma_at(X_train, X_new)
|
|
91
|
+
return fmean, fvar + sigma_new**2
|
|
92
|
+
return fmean, fvar
|