pcntoolkit 0.32.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.
@@ -0,0 +1,245 @@
1
+ from __future__ import print_function
2
+ from __future__ import division
3
+
4
+ import numpy as np
5
+ import torch
6
+
7
+
8
+ class GPRRFA:
9
+ """Random Feature Approximation for Gaussian Process Regression
10
+
11
+ Estimation and prediction of Bayesian linear regression models
12
+
13
+ Basic usage::
14
+
15
+ R = GPRRFA()
16
+ hyp = R.estimate(hyp0, X, y)
17
+ ys,s2 = R.predict(hyp, X, y, Xs)
18
+
19
+ where the variables are
20
+
21
+ :param hyp: vector of hyperparmaters.
22
+ :param X: N x D data array
23
+ :param y: 1D Array of targets (length N)
24
+ :param Xs: Nte x D array of test cases
25
+ :param hyp0: starting estimates for hyperparameter optimisation
26
+
27
+ :returns: * ys - predictive mean
28
+ * s2 - predictive variance
29
+
30
+ The hyperparameters are::
31
+
32
+ hyp = [ log(sn), log(ell), log(sf) ] # hyp is a numpy array
33
+
34
+ where sn^2 is the noise variance, ell are lengthscale parameters and
35
+ sf^2 is the signal variance. This provides an approximation to the
36
+ covariance function::
37
+
38
+ k(x,z) = x'*z + sn2*exp(0.5*(x-z)'*Lambda*(x-z))
39
+
40
+ where Lambda = diag((ell_1^2, ... ell_D^2))
41
+
42
+ Written by A. Marquand
43
+ """
44
+
45
+ def __init__(self, hyp=None, X=None, y=None, n_feat=None,
46
+ n_iter=100, tol=1e-3, verbose=False):
47
+
48
+ self.hyp = np.nan
49
+ self.nlZ = np.nan
50
+ self.tol = tol # not used at present
51
+ self.Nf = n_feat
52
+ self.n_iter = n_iter
53
+ self.verbose = verbose
54
+ self._n_restarts = 5
55
+
56
+ if (hyp is not None) and (X is not None) and (y is not None):
57
+ self.post(hyp, X, y)
58
+
59
+ def _numpy2torch(self, X, y=None, hyp=None):
60
+
61
+ if type(X) is torch.Tensor:
62
+ pass
63
+ elif type(X) is np.ndarray:
64
+ X = torch.from_numpy(X)
65
+ else:
66
+ ValueError('Unknown data type (X)')
67
+ X = X.double()
68
+
69
+ if y is not None:
70
+ if type(y) is torch.Tensor:
71
+ pass
72
+ elif type(y) is np.ndarray:
73
+ y = torch.from_numpy(y)
74
+ else:
75
+ raise ValueError('Unknown data type (y)')
76
+
77
+ if len(y.shape) == 1:
78
+ y.resize_(y.shape[0], 1)
79
+ y = y.double()
80
+
81
+ if hyp is not None:
82
+ if type(hyp) is torch.Tensor:
83
+ pass
84
+ else:
85
+ hyp = torch.tensor(hyp, requires_grad=True)
86
+
87
+ return X, y, hyp
88
+
89
+ def get_n_params(self, X):
90
+
91
+ return X.shape[1] + 2
92
+
93
+ def post(self, hyp, X, y):
94
+ """ Generic function to compute posterior distribution.
95
+
96
+ This function will save the posterior mean and precision matrix as
97
+ self.m and self.A and will also update internal parameters (e.g.
98
+ N, D and the prior covariance (Sigma) and precision (iSigma).
99
+ """
100
+
101
+ # make sure all variables are the right type
102
+ X, y, hyp = self._numpy2torch(X, y, hyp)
103
+
104
+ self.N, self.Dx = X.shape
105
+
106
+ # ensure the number of features is specified (use 75% as a default)
107
+ if self.Nf is None:
108
+ self.Nf = int(0.75 * self.N)
109
+
110
+ self.Omega = torch.zeros((self.Dx, self.Nf), dtype=torch.double)
111
+ for f in range(self.Nf):
112
+ self.Omega[:, f] = torch.exp(hyp[1:-1]) * \
113
+ torch.randn((self.Dx, 1), dtype=torch.double).squeeze()
114
+
115
+ XO = torch.mm(X, self.Omega)
116
+ self.Phi = torch.exp(hyp[-1])/np.sqrt(self.Nf) * \
117
+ torch.cat((torch.cos(XO), torch.sin(XO)), 1)
118
+
119
+ # concatenate linear weights
120
+ self.Phi = torch.cat((self.Phi, X), 1)
121
+ self.D = self.Phi.shape[1]
122
+
123
+ if self.verbose:
124
+ print("estimating posterior ... | hyp=", hyp)
125
+
126
+ self.A = torch.mm(torch.t(self.Phi), self.Phi) / torch.exp(2*hyp[0]) + \
127
+ torch.eye(self.D, dtype=torch.double)
128
+ self.m = torch.mm(torch.solve(torch.t(self.Phi), self.A)[0], y) / \
129
+ torch.exp(2*hyp[0])
130
+
131
+ # save hyperparameters
132
+ self.hyp = hyp
133
+
134
+ # update optimizer iteration count
135
+ if hasattr(self, '_iterations'):
136
+ self._iterations += 1
137
+
138
+ def loglik(self, hyp, X, y):
139
+ """ Function to compute compute log (marginal) likelihood """
140
+ X, y, hyp = self._numpy2torch(X, y, hyp)
141
+
142
+ # always recompute the posterior
143
+ self.post(hyp, X, y)
144
+
145
+ # logdetA = 2*torch.sum(torch.log(torch.diag(torch.cholesky(self.A))))
146
+ try:
147
+ # compute the log determinants in a numerically stable way
148
+ logdetA = 2 * \
149
+ torch.sum(torch.log(torch.diag(torch.cholesky(self.A))))
150
+ except Exception as e:
151
+ print("Warning: Estimation of posterior distribution failed")
152
+ print(e)
153
+ # nlZ = torch.tensor(1/np.finfo(float).eps)
154
+ nlZ = torch.tensor(np.nan)
155
+ self._optim_failed = True
156
+ return nlZ
157
+
158
+ # compute negative marginal log likelihood
159
+ nlZ = -0.5 * (self.N*torch.log(1/torch.exp(2*hyp[0])) -
160
+ self.N*np.log(2*np.pi) -
161
+ torch.mm(torch.t(y - torch.mm(self.Phi, self.m)),
162
+ (y - torch.mm(self.Phi, self.m))) /
163
+ torch.exp(2*hyp[0]) -
164
+ torch.mm(torch.t(self.m), self.m) - logdetA)
165
+
166
+ if self.verbose:
167
+ print("nlZ= ", nlZ, " | hyp=", hyp)
168
+
169
+ # save marginal likelihood
170
+ self.nlZ = nlZ
171
+ return nlZ
172
+
173
+ def dloglik(self, hyp, X, y):
174
+ """ Function to compute derivatives """
175
+
176
+ print("derivatives not available")
177
+
178
+ return
179
+
180
+ def estimate(self, hyp0, X, y, optimizer='lbfgs'):
181
+ """ Function to estimate the model """
182
+
183
+ if type(hyp0) is torch.Tensor:
184
+ hyp = hyp0
185
+ hyp0.requires_grad_()
186
+ else:
187
+ hyp = torch.tensor(hyp0, requires_grad=True)
188
+ # save the starting values
189
+ self.hyp0 = hyp
190
+
191
+ if optimizer.lower() == 'lbfgs':
192
+ opt = torch.optim.LBFGS([hyp])
193
+ else:
194
+ raise ValueError("Optimizer " + " not implemented")
195
+ self._iterations = 0
196
+
197
+ def closure():
198
+ opt.zero_grad()
199
+ nlZ = self.loglik(hyp, X, y)
200
+ if not torch.isnan(nlZ):
201
+ nlZ.backward()
202
+ return nlZ
203
+
204
+ for r in range(self._n_restarts):
205
+ self._optim_failed = False
206
+
207
+ nlZ = opt.step(closure)
208
+
209
+ if self._optim_failed:
210
+ print("optimization failed. retrying (", r+1, "of",
211
+ self._n_restarts, ")")
212
+ hyp = torch.randn_like(hyp, requires_grad=True)
213
+ self.hyp0 = hyp
214
+ else:
215
+ print("Optimzation complete after", self._iterations,
216
+ "evaluations. Function value =",
217
+ nlZ.detach().numpy().squeeze())
218
+ break
219
+
220
+ return self.hyp.detach().numpy()
221
+
222
+ def predict(self, hyp, X, y, Xs):
223
+ """ Function to make predictions from the model """
224
+
225
+ X, y, hyp = self._numpy2torch(X, y, hyp)
226
+ Xs, *_ = self._numpy2torch(Xs)
227
+
228
+ if (hyp != self.hyp).all() or not (hasattr(self, 'A')):
229
+ self.post(hyp, X, y)
230
+
231
+ # generate prediction tensors
232
+ XsO = torch.mm(Xs, self.Omega)
233
+ Phis = torch.exp(hyp[-1])/np.sqrt(self.Nf) * \
234
+ torch.cat((torch.cos(XsO), torch.sin(XsO)), 1)
235
+ # add linear component
236
+ Phis = torch.cat((Phis, Xs), 1)
237
+
238
+ ys = torch.mm(Phis, self.m)
239
+
240
+ # compute diag(Phis*(Phis'\A)) avoiding computing off-diagonal entries
241
+ s2 = torch.exp(2*hyp[0]) + \
242
+ torch.sum(Phis * torch.t(torch.solve(torch.t(Phis), self.A)[0]), 1)
243
+
244
+ # return output as numpy arrays
245
+ return ys.detach().numpy().squeeze(), s2.detach().numpy().squeeze()