1 /*
2 SPDX-FileCopyrightText: 2014-2017 Max Planck Society.
3 All rights reserved.
4
5 SPDX-License-Identifier: BSD-3-Clause
6 */
7
8 /**
9 * @file
10 * @date 2014-2017
11 * @copyright Max Planck Society
12 *
13 * @author Edgar D. Klenske <edgar.klenske@tuebingen.mpg.de>
14 * @author Stephan Wenninger <stephan.wenninger@tuebingen.mpg.de>
15 * @author Raffi Enficiaud <raffi.enficiaud@tuebingen.mpg.de>
16 *
17 * @brief The GP class implements the Gaussian Process functionality.
18 */
19
20 #include <cstdint>
21
22 #include "gaussian_process.h"
23 #include "math_tools.h"
24 #include "covariance_functions.h"
25
26 // A functor for special orderings
27 struct covariance_ordering
28 {
covariance_orderingcovariance_ordering29 covariance_ordering(Eigen::VectorXd const& cov) : covariance_(cov){}
operator ()covariance_ordering30 bool operator()(int a, int b) const
31 {
32 return (covariance_[a] > covariance_[b]);
33 }
34 Eigen::VectorXd const& covariance_;
35 };
36
GP()37 GP::GP() : covFunc_(nullptr), // initialize pointer to null
38 covFuncProj_(nullptr), // initialize pointer to null
39 data_loc_(Eigen::VectorXd()),
40 data_out_(Eigen::VectorXd()),
41 data_var_(Eigen::VectorXd()),
42 gram_matrix_(Eigen::MatrixXd()),
43 alpha_(Eigen::VectorXd()),
44 chol_gram_matrix_(Eigen::LDLT<Eigen::MatrixXd>()),
45 log_noise_sd_(-1E20),
46 use_explicit_trend_(false),
47 feature_vectors_(Eigen::MatrixXd()),
48 feature_matrix_(Eigen::MatrixXd()),
49 chol_feature_matrix_(Eigen::LDLT<Eigen::MatrixXd>()),
50 beta_(Eigen::VectorXd())
51 { }
52
GP(const covariance_functions::CovFunc & covFunc)53 GP::GP(const covariance_functions::CovFunc& covFunc) :
54 covFunc_(covFunc.clone()),
55 covFuncProj_(nullptr),
56 data_loc_(Eigen::VectorXd()),
57 data_out_(Eigen::VectorXd()),
58 data_var_(Eigen::VectorXd()),
59 gram_matrix_(Eigen::MatrixXd()),
60 alpha_(Eigen::VectorXd()),
61 chol_gram_matrix_(Eigen::LDLT<Eigen::MatrixXd>()),
62 log_noise_sd_(-1E20),
63 use_explicit_trend_(false),
64 feature_vectors_(Eigen::MatrixXd()),
65 feature_matrix_(Eigen::MatrixXd()),
66 chol_feature_matrix_(Eigen::LDLT<Eigen::MatrixXd>()),
67 beta_(Eigen::VectorXd())
68 { }
69
GP(const double noise_variance,const covariance_functions::CovFunc & covFunc)70 GP::GP(const double noise_variance,
71 const covariance_functions::CovFunc& covFunc) :
72 covFunc_(covFunc.clone()),
73 covFuncProj_(nullptr),
74 data_loc_(Eigen::VectorXd()),
75 data_out_(Eigen::VectorXd()),
76 data_var_(Eigen::VectorXd()),
77 gram_matrix_(Eigen::MatrixXd()),
78 alpha_(Eigen::VectorXd()),
79 chol_gram_matrix_(Eigen::LDLT<Eigen::MatrixXd>()),
80 log_noise_sd_(std::log(noise_variance)),
81 use_explicit_trend_(false),
82 feature_vectors_(Eigen::MatrixXd()),
83 feature_matrix_(Eigen::MatrixXd()),
84 chol_feature_matrix_(Eigen::LDLT<Eigen::MatrixXd>()),
85 beta_(Eigen::VectorXd())
86 { }
87
~GP()88 GP::~GP()
89 {
90 delete this->covFunc_; // tidy up since we are responsible for the covFunc.
91 delete this->covFuncProj_; // tidy up since we are responsible for the covFuncProj.
92 }
93
GP(const GP & that)94 GP::GP(const GP& that) :
95 covFunc_(nullptr), // initialize to nullptr, clone later
96 covFuncProj_(nullptr), // initialize to nullptr, clone later
97 data_loc_(that.data_loc_),
98 data_out_(that.data_out_),
99 data_var_(that.data_var_),
100 gram_matrix_(that.gram_matrix_),
101 alpha_(that.alpha_),
102 chol_gram_matrix_(that.chol_gram_matrix_),
103 log_noise_sd_(that.log_noise_sd_),
104 use_explicit_trend_(that.use_explicit_trend_),
105 feature_vectors_(that.feature_vectors_),
106 feature_matrix_(that.feature_matrix_),
107 chol_feature_matrix_(that.chol_feature_matrix_),
108 beta_(that.beta_)
109 {
110 covFunc_ = that.covFunc_->clone();
111 covFuncProj_ = that.covFuncProj_->clone();
112 }
113
setCovarianceFunction(const covariance_functions::CovFunc & covFunc)114 bool GP::setCovarianceFunction(const covariance_functions::CovFunc& covFunc)
115 {
116 // can only set the covariance function if training dataset is empty
117 if (data_loc_.size() != 0 || data_out_.size() != 0)
118 return false;
119 delete covFunc_; // initialized to zero, so delete is safe
120 covFunc_ = covFunc.clone();
121
122 return true;
123 }
124
enableOutputProjection(const covariance_functions::CovFunc & covFuncProj)125 void GP::enableOutputProjection(const covariance_functions::CovFunc& covFuncProj)
126 {
127 delete covFuncProj_;// initialized to zero, so delete is safe
128 covFuncProj_ = covFuncProj.clone();
129 }
130
disableOutputProjection()131 void GP::disableOutputProjection()
132 {
133 delete covFuncProj_; // initialized to zero, so delete is safe
134 covFuncProj_ = nullptr;
135 }
136
operator =(const GP & that)137 GP& GP::operator=(const GP& that)
138 {
139 if (this != &that)
140 {
141 covariance_functions::CovFunc* temp = covFunc_; // store old pointer...
142 covFunc_ = that.covFunc_->clone(); // ... first clone ...
143 delete temp; // ... and then delete.
144
145 // copy the rest
146 data_loc_ = that.data_loc_;
147 data_out_ = that.data_out_;
148 data_var_ = that.data_var_;
149 gram_matrix_ = that.gram_matrix_;
150 alpha_ = that.alpha_;
151 chol_gram_matrix_ = that.chol_gram_matrix_;
152 log_noise_sd_ = that.log_noise_sd_;
153 }
154 return *this;
155 }
156
drawSample(const Eigen::VectorXd & locations) const157 Eigen::VectorXd GP::drawSample(const Eigen::VectorXd& locations) const
158 {
159 return drawSample(locations,
160 math_tools::generate_normal_random_matrix(locations.rows(), locations.cols()));
161 }
162
drawSample(const Eigen::VectorXd & locations,const Eigen::VectorXd & random_vector) const163 Eigen::VectorXd GP::drawSample(const Eigen::VectorXd& locations,
164 const Eigen::VectorXd& random_vector) const
165 {
166 Eigen::MatrixXd prior_covariance;
167 Eigen::MatrixXd kernel_matrix;
168 Eigen::LLT<Eigen::MatrixXd> chol_kernel_matrix;
169 Eigen::MatrixXd samples;
170
171 // we need the prior covariance for both, prior and posterior samples.
172 prior_covariance = covFunc_->evaluate(locations, locations);
173 kernel_matrix = prior_covariance;
174
175 if (gram_matrix_.cols() == 0) // no data, i.e. only a prior
176 {
177 kernel_matrix = prior_covariance + JITTER * Eigen::MatrixXd::Identity(
178 prior_covariance.rows(), prior_covariance.cols());
179 }
180 else // we have some data
181 {
182 Eigen::MatrixXd mixed_covariance;
183 mixed_covariance = covFunc_->evaluate(locations, data_loc_);
184 Eigen::MatrixXd posterior_covariance;
185 posterior_covariance = prior_covariance - mixed_covariance *
186 (chol_gram_matrix_.solve(mixed_covariance.transpose()));
187 kernel_matrix = posterior_covariance + JITTER * Eigen::MatrixXd::Identity(
188 posterior_covariance.rows(), posterior_covariance.cols());
189 }
190 chol_kernel_matrix = kernel_matrix.llt();
191
192 // Draw sample: s = chol(K)*x, where x is a random vector
193 samples = chol_kernel_matrix.matrixL() * random_vector;
194
195 // add the measurement noise on return
196 return samples + std::exp(log_noise_sd_) *
197 math_tools::generate_normal_random_matrix(samples.rows(), samples.cols());
198 }
199
infer()200 void GP::infer()
201 {
202 assert(data_loc_.rows() > 0 && "Error: the GP is not yet initialized!");
203
204 // The data covariance matrix
205 Eigen::MatrixXd data_cov = covFunc_->evaluate(data_loc_, data_loc_);
206
207 // swapping in the Gram matrix is faster than directly assigning it
208 gram_matrix_.swap(data_cov); // store the new data_cov as gram matrix
209 if (data_var_.rows() == 0) // homoscedastic
210 {
211 gram_matrix_ += (std::exp(2 * log_noise_sd_) + JITTER) *
212 Eigen::MatrixXd::Identity(gram_matrix_.rows(), gram_matrix_.cols());
213 }
214 else // heteroscedastic
215 {
216 gram_matrix_ += data_var_.asDiagonal();
217 }
218
219 // compute the Cholesky decomposition of the Gram matrix
220 chol_gram_matrix_ = gram_matrix_.ldlt();
221
222 // pre-compute the alpha, which is the solution of the chol to the data
223 alpha_ = chol_gram_matrix_.solve(data_out_);
224
225 if (use_explicit_trend_)
226 {
227 feature_vectors_ = Eigen::MatrixXd(2, data_loc_.rows());
228 // precompute necessary matrices for the explicit trend function
229 feature_vectors_.row(0) = Eigen::MatrixXd::Ones(1,data_loc_.rows()); // instead of pow(0)
230 feature_vectors_.row(1) = data_loc_.array(); // instead of pow(1)
231
232 feature_matrix_ = feature_vectors_ * chol_gram_matrix_.solve(feature_vectors_.transpose());
233 chol_feature_matrix_ = feature_matrix_.ldlt();
234
235 beta_ = chol_feature_matrix_.solve(feature_vectors_) * alpha_;
236 }
237 }
238
infer(const Eigen::VectorXd & data_loc,const Eigen::VectorXd & data_out,const Eigen::VectorXd & data_var)239 void GP::infer(const Eigen::VectorXd& data_loc,
240 const Eigen::VectorXd& data_out,
241 const Eigen::VectorXd& data_var /* = EigenVectorXd() */)
242 {
243 data_loc_ = data_loc;
244 data_out_ = data_out;
245 if (data_var.rows() > 0)
246 {
247 data_var_ = data_var;
248 }
249 infer(); // updates the Gram matrix and its Cholesky decomposition
250 }
251
inferSD(const Eigen::VectorXd & data_loc,const Eigen::VectorXd & data_out,const int n,const Eigen::VectorXd & data_var,const double prediction_point)252 void GP::inferSD(const Eigen::VectorXd& data_loc,
253 const Eigen::VectorXd& data_out,
254 const int n, const Eigen::VectorXd& data_var /* = EigenVectorXd() */,
255 const double prediction_point /*= std::numeric_limits<double>::quiet_NaN()*/)
256 {
257 Eigen::VectorXd covariance;
258
259 Eigen::VectorXd prediction_loc(1);
260 if ( math_tools::isNaN(prediction_point) )
261 {
262 // if none given, use the last datapoint as prediction reference
263 prediction_loc = data_loc.tail(1);
264 }
265 else
266 {
267 prediction_loc << prediction_point;
268 }
269
270 // calculate covariance between data and prediction point for point selection
271 covariance = covFunc_->evaluate(data_loc, prediction_loc);
272
273 // generate index vector
274 std::vector<int> index(covariance.size(), 0);
275 for (size_t i = 0 ; i != index.size() ; i++) {
276 index[i] = i;
277 }
278
279 // sort indices with respect to covariance value
280 std::sort(index.begin(), index.end(),
281 covariance_ordering(covariance)
282 );
283
284 bool use_var = data_var.rows() > 0; // true means heteroscedastic noise
285
286 if (n < data_loc.rows()) {
287 std::vector<double> loc_arr(n);
288 std::vector<double> out_arr(n);
289 std::vector<double> var_arr(n);
290
291 for (int i = 0; i < n; ++i)
292 {
293 loc_arr[i] = data_loc[index[i]];
294 out_arr[i] = data_out[index[i]];
295 if (use_var)
296 {
297 var_arr[i] = data_var[index[i]];
298 }
299 }
300
301 data_loc_ = Eigen::Map<Eigen::VectorXd>(loc_arr.data(),n,1);
302 data_out_ = Eigen::Map<Eigen::VectorXd>(out_arr.data(),n,1);
303 if (use_var)
304 {
305 data_var_ = Eigen::Map<Eigen::VectorXd>(var_arr.data(),n,1);
306 }
307 }
308 else // we can use all points and don't neet to select
309 {
310 data_loc_ = data_loc;
311 data_out_ = data_out;
312 if (use_var)
313 {
314 data_var_ = data_var;
315 }
316 }
317 infer();
318 }
319
clearData()320 void GP::clearData()
321 {
322 gram_matrix_ = Eigen::MatrixXd();
323 chol_gram_matrix_ = Eigen::LDLT<Eigen::MatrixXd>();
324 data_loc_ = Eigen::VectorXd();
325 data_out_ = Eigen::VectorXd();
326 }
327
predict(const Eigen::VectorXd & locations,Eigen::VectorXd * variances) const328 Eigen::VectorXd GP::predict(const Eigen::VectorXd& locations, Eigen::VectorXd* variances /*=nullptr*/) const
329 {
330 // The prior covariance matrix (evaluated on test points)
331 Eigen::MatrixXd prior_cov = covFunc_->evaluate(locations, locations);
332
333 if (data_loc_.rows() == 0) // check if the data is empty
334 {
335 if (variances != nullptr)
336 {
337 (*variances) = prior_cov.diagonal();
338 }
339 return Eigen::VectorXd::Zero(locations.size());
340 }
341 else
342 {
343 // Calculate mixed covariance matrix (test and data points)
344 Eigen::MatrixXd mixed_cov = covFunc_->evaluate(locations, data_loc_);
345
346 // Calculate feature matrix for linear feature
347 Eigen::MatrixXd phi(2, locations.rows());
348 if (use_explicit_trend_)
349 {
350 phi.row(0) = Eigen::MatrixXd::Ones(1,locations.rows()); // instead of pow(0)
351 phi.row(1) = locations.array(); // instead of pow(1)
352
353 return predict(prior_cov, mixed_cov, phi, variances);
354 }
355 return predict(prior_cov, mixed_cov, Eigen::MatrixXd(), variances);
356 }
357 }
358
predictProjected(const Eigen::VectorXd & locations,Eigen::VectorXd * variances) const359 Eigen::VectorXd GP::predictProjected(const Eigen::VectorXd& locations, Eigen::VectorXd* variances /*=nullptr*/) const
360 {
361 // use the suitable covariance function, depending on whether an
362 // output projection is used or not.
363 covariance_functions::CovFunc* covFunc = nullptr;
364 if (covFuncProj_ == nullptr)
365 {
366 covFunc = covFunc_;
367 }
368 else
369 {
370 covFunc = covFuncProj_;
371 }
372
373 assert(covFunc != nullptr);
374
375 // The prior covariance matrix (evaluated on test points)
376 Eigen::MatrixXd prior_cov = covFunc->evaluate(locations, locations);
377
378 if (data_loc_.rows() == 0) // check if the data is empty
379 {
380 if (variances != nullptr)
381 {
382 (*variances) = prior_cov.diagonal();
383 }
384 return Eigen::VectorXd::Zero(locations.size());
385 }
386 else
387 {
388 // The mixed covariance matrix (test and data points)
389 Eigen::MatrixXd mixed_cov = covFunc->evaluate(locations, data_loc_);
390
391 Eigen::MatrixXd phi(2, locations.rows());
392 if (use_explicit_trend_)
393 {
394 // calculate the feature vectors for linear regression
395 phi.row(0) = Eigen::MatrixXd::Ones(1,locations.rows()); // instead of pow(0)
396 phi.row(1) = locations.array(); // instead of pow(1)
397
398 return predict(prior_cov, mixed_cov, phi, variances);
399 }
400 return predict(prior_cov, mixed_cov, Eigen::MatrixXd(), variances);
401 }
402 }
403
predict(const Eigen::MatrixXd & prior_cov,const Eigen::MatrixXd & mixed_cov,const Eigen::MatrixXd & phi,Eigen::VectorXd * variances) const404 Eigen::VectorXd GP::predict(const Eigen::MatrixXd& prior_cov, const Eigen::MatrixXd& mixed_cov,
405 const Eigen::MatrixXd& phi /*=Eigen::MatrixXd()*/, Eigen::VectorXd* variances /*=nullptr*/)
406 const
407 {
408
409 // calculate GP mean from precomputed alpha vector
410 Eigen::VectorXd m = mixed_cov * alpha_;
411
412 // precompute K^{-1} * mixed_cov
413 Eigen::MatrixXd gamma = chol_gram_matrix_.solve(mixed_cov.transpose());
414
415 Eigen::MatrixXd R;
416
417 // include fixed-features in the calculations
418 if (use_explicit_trend_)
419 {
420 R = phi - feature_vectors_ * gamma;
421
422 m += R.transpose() * beta_;
423 }
424
425 if (variances != nullptr)
426 {
427 // calculate GP variance
428 Eigen::MatrixXd v = prior_cov - mixed_cov * gamma;
429
430 // include fixed-features in the calculations
431 if (use_explicit_trend_)
432 {
433 assert(R.size() > 0);
434 v += R.transpose() * chol_feature_matrix_.solve(R);
435 }
436
437 (*variances) = v.diagonal();
438 }
439 return m;
440 }
441
setHyperParameters(const Eigen::VectorXd & hyperParameters)442 void GP::setHyperParameters(const Eigen::VectorXd& hyperParameters)
443 {
444 assert(hyperParameters.rows() == covFunc_->getParameterCount() + covFunc_->getExtraParameterCount() + 1 &&
445 "Wrong number of hyperparameters supplied to setHyperParameters()!");
446 log_noise_sd_ = hyperParameters[0];
447 covFunc_->setParameters(hyperParameters.segment(1, covFunc_->getParameterCount()));
448 covFunc_->setExtraParameters(hyperParameters.tail(covFunc_->getExtraParameterCount()));
449 if (data_loc_.rows() > 0)
450 {
451 infer();
452 }
453
454 // if the projection kernel is set, set the parameters there as well.
455 if (covFuncProj_ != nullptr)
456 {
457 covFuncProj_->setParameters(hyperParameters.segment(1, covFunc_->getParameterCount()));
458 covFuncProj_->setExtraParameters(hyperParameters.tail(covFunc_->getExtraParameterCount()));
459 }
460 }
461
getHyperParameters() const462 Eigen::VectorXd GP::getHyperParameters() const
463 {
464 Eigen::VectorXd hyperParameters(covFunc_->getParameterCount() + covFunc_->getExtraParameterCount() + 1);
465 hyperParameters << log_noise_sd_, covFunc_->getParameters(), covFunc_->getExtraParameters();
466 return hyperParameters;
467 }
468
enableExplicitTrend()469 void GP::enableExplicitTrend()
470 {
471 use_explicit_trend_ = true;
472 }
473
disableExplicitTrend()474 void GP::disableExplicitTrend()
475 {
476 use_explicit_trend_ = false;
477 }
478