Skip to content

Commit f14e1b6

Browse files
authored
Merge pull request #3337 from florence-bockting/laplace/return-cov-chol
Add `laplace_latent_solve()` and `laplace_latent_tol_solve()`
2 parents b98567a + 031e4a0 commit f14e1b6

4 files changed

Lines changed: 198 additions & 9 deletions

File tree

stan/math/mix/functor/laplace_base_rng.hpp

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <stan/math/mix/functor/laplace_marginal_density.hpp>
66
#include <stan/math/prim/prob/multi_normal_cholesky_rng.hpp>
77
#include <stan/math/prim/prob/multi_normal_rng.hpp>
8+
#include <stan/math/prim/fun/cholesky_decompose.hpp>
89

910
namespace stan {
1011
namespace math {
@@ -15,11 +16,19 @@ namespace math {
1516
* theta ~ Normal(theta | 0, Sigma(phi, x))
1617
* y ~ pi(y | theta, eta)
1718
*
18-
* returns a multivariate normal random variate sampled
19+
* By default, returns a multivariate normal random variate sampled
1920
* from the Laplace approximation of p(theta_pred | y, phi, x_pred).
21+
* If `ReturnMeanAndCovCholesky` is true, instead of drawing a sample this
22+
* returns the posterior mean and the Cholesky factor of the posterior
23+
* covariance of that same Laplace approximation, as a
24+
* `std::tuple<Eigen::VectorXd, Eigen::MatrixXd>`.
2025
* Note that while the data is observed at x (train_tuple), the new samples
2126
* are drawn for covariates x_pred (pred_tuple).
2227
* To sample the "original" theta's, set pred_tuple = train_tuple.
28+
* @tparam ReturnMeanAndCovCholesky If false (default), draw and return a
29+
* random variate from the approximate posterior. If true, return a tuple
30+
* containing the posterior mean and the lower-triangular Cholesky factor of
31+
* the posterior covariance instead of a sample.
2332
* @tparam LLFunc Type of likelihood function.
2433
* @tparam LLArgs Tuple of arguments types of likelihood function.
2534
* \laplace_common_template_args
@@ -31,13 +40,15 @@ namespace math {
3140
* \rng_arg
3241
* \msg_arg
3342
*/
34-
template <typename LLFunc, typename LLArgs, typename CovarFun,
35-
typename CovarArgs, bool InitTheta, typename RNG,
43+
template <bool ReturnMeanAndCovCholesky = false, typename LLFunc,
44+
typename LLArgs, typename CovarFun, typename CovarArgs,
45+
bool InitTheta, typename RNG,
3646
require_t<is_all_arithmetic_scalar<CovarArgs, LLArgs>>* = nullptr>
37-
inline Eigen::VectorXd laplace_base_rng(
38-
LLFunc&& ll_fun, LLArgs&& ll_args, CovarFun&& covariance_function,
39-
CovarArgs&& covar_args, const laplace_options<InitTheta>& options, RNG& rng,
40-
std::ostream* msgs) {
47+
inline auto laplace_base_rng(LLFunc&& ll_fun, LLArgs&& ll_args,
48+
CovarFun&& covariance_function,
49+
CovarArgs&& covar_args,
50+
const laplace_options<InitTheta>& options,
51+
RNG& rng, std::ostream* msgs) {
4152
Eigen::MatrixXd covariance_train = stan::math::apply(
4253
[msgs, &covariance_function](auto&&... args) {
4354
return covariance_function(std::forward<decltype(args)>(args)..., msgs);
@@ -51,7 +62,12 @@ inline Eigen::VectorXd laplace_base_rng(
5162
= md_est.L.template triangularView<Eigen::Lower>().solve(
5263
md_est.W_r * covariance_train);
5364
Eigen::MatrixXd Sigma = covariance_train - V_dec.transpose() * V_dec;
54-
return multi_normal_rng(std::move(mean_train), std::move(Sigma), rng);
65+
if constexpr (ReturnMeanAndCovCholesky) {
66+
Eigen::MatrixXd Sigma_chol = cholesky_decompose(Sigma);
67+
return std::make_tuple(std::move(mean_train), std::move(Sigma_chol));
68+
} else {
69+
return multi_normal_rng(std::move(mean_train), std::move(Sigma), rng);
70+
}
5571
} else {
5672
Eigen::MatrixXd Sigma
5773
= covariance_train
@@ -60,7 +76,12 @@ inline Eigen::VectorXd laplace_base_rng(
6076
- md_est.W_r
6177
* md_est.LU.solve(covariance_train * md_est.W_r))
6278
* covariance_train;
63-
return multi_normal_rng(std::move(mean_train), std::move(Sigma), rng);
79+
if constexpr (ReturnMeanAndCovCholesky) {
80+
Eigen::MatrixXd Sigma_chol = cholesky_decompose(Sigma);
81+
return std::make_tuple(std::move(mean_train), std::move(Sigma_chol));
82+
} else {
83+
return multi_normal_rng(std::move(mean_train), std::move(Sigma), rng);
84+
}
6485
}
6586
}
6687

stan/math/mix/prob.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <stan/math/mix/prob/laplace_latent_poisson_log_rng.hpp>
66
#include <stan/math/mix/prob/laplace_latent_neg_binomial_2_log_rng.hpp>
77
#include <stan/math/mix/prob/laplace_latent_rng.hpp>
8+
#include <stan/math/mix/prob/laplace_latent_solve.hpp>
89
#include <stan/math/mix/prob/laplace_marginal.hpp>
910
#include <stan/math/mix/prob/laplace_marginal_neg_binomial_2_log_lpmf.hpp>
1011
#include <stan/math/mix/prob/laplace_marginal_bernoulli_logit_lpmf.hpp>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#ifndef STAN_MATH_MIX_PROB_LAPLACE_LATENT_SOLVE_HPP
2+
#define STAN_MATH_MIX_PROB_LAPLACE_LATENT_SOLVE_HPP
3+
4+
#include <stan/math/mix/functor/laplace_base_rng.hpp>
5+
6+
namespace stan {
7+
namespace math {
8+
9+
namespace internal {
10+
// Placeholder RNG type: used whenever laplace_base_rng's
11+
// sampling branch is compiled out via `if constexpr`. In this case
12+
// no rng is needed as no sampling is performed.
13+
struct laplace_unused_rng {};
14+
15+
} // namespace internal
16+
17+
/**
18+
* In a latent gaussian model,
19+
*
20+
* theta ~ Normal(0, Sigma(phi))
21+
* y ~ p(y|theta,phi)
22+
*
23+
* returns the posterior mean and Cholesky factor from the Laplace
24+
* approximation to p(theta|y,phi), where the log likelihood is given by L_f.
25+
* @tparam LLFunc Type of likelihood function.
26+
* @tparam LLArgs Tuple of arguments types of likelihood function.
27+
* \laplace_common_template_args
28+
* @param ll_fun Likelihood function.
29+
* @param ll_args Arguments for likelihood function.
30+
* \laplace_common_args
31+
* @param[in] hessian_block_size Block size for the Hessian approximation with
32+
* respect to the latent gaussian variable theta.
33+
* \laplace_options
34+
* \msg_arg
35+
*/
36+
template <typename LLFunc, typename LLArgs, typename CovarFun,
37+
typename CovarArgs, typename OpsTuple>
38+
inline auto laplace_latent_tol_solve(LLFunc&& ll_fun, LLArgs&& ll_args,
39+
int hessian_block_size,
40+
CovarFun&& covariance_function,
41+
CovarArgs&& covar_args, OpsTuple&& ops,
42+
std::ostream* msgs) {
43+
auto options
44+
= internal::tuple_to_laplace_options(std::forward<OpsTuple>(ops));
45+
options.hessian_block_size = hessian_block_size;
46+
internal::laplace_unused_rng unused_rng;
47+
return laplace_base_rng<true>(std::forward<LLFunc>(ll_fun),
48+
std::forward<LLArgs>(ll_args),
49+
std::forward<CovarFun>(covariance_function),
50+
std::forward<CovarArgs>(covar_args),
51+
std::move(options), unused_rng, msgs);
52+
}
53+
54+
/**
55+
* In a latent gaussian model,
56+
*
57+
* theta ~ Normal(0, Sigma(phi))
58+
* y ~ p(y|theta,phi)
59+
*
60+
* returns the posterior mean and Cholesky factor
61+
* from the Laplace approximation of p(theta | y, phi).
62+
* @tparam LLFunc Type of likelihood function.
63+
* @tparam LLArgs Tuple of arguments types of likelihood function.
64+
* \laplace_common_template_args
65+
* @tparam RNG A valid boost rng type
66+
* @param ll_fun Likelihood function.
67+
* @param ll_args Arguments for likelihood function.
68+
* \laplace_common_args
69+
* @param[in] hessian_block_size Block size for the Hessian approximation with
70+
* respect to the latent gaussian variable theta.
71+
* \msg_arg
72+
*/
73+
template <typename LLFunc, typename LLArgs, typename CovarFun,
74+
typename CovarArgs>
75+
inline auto laplace_latent_solve(LLFunc&& ll_fun, LLArgs&& ll_args,
76+
int hessian_block_size,
77+
CovarFun&& covariance_function,
78+
CovarArgs&& covar_args, std::ostream* msgs) {
79+
auto options = laplace_options_default{hessian_block_size};
80+
internal::laplace_unused_rng unused_rng;
81+
return laplace_base_rng<true>(std::forward<LLFunc>(ll_fun),
82+
std::forward<LLArgs>(ll_args),
83+
std::forward<CovarFun>(covariance_function),
84+
std::forward<CovarArgs>(covar_args),
85+
std::move(options), unused_rng, msgs);
86+
}
87+
88+
} // namespace math
89+
} // namespace stan
90+
91+
#endif
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#include <stan/math.hpp>
2+
#include <stan/math/mix.hpp>
3+
#include <test/unit/math/laplace/laplace_utility.hpp>
4+
5+
#include <boost/random/mersenne_twister.hpp>
6+
7+
#include <gtest/gtest.h>
8+
#include <stdexcept>
9+
#include <vector>
10+
11+
namespace {
12+
struct poisson_log_likelihood {
13+
template <typename Theta>
14+
auto operator()(const Theta& theta, const std::vector<int>& y,
15+
std::ostream* pstream) const {
16+
return stan::math::poisson_log_lpmf(y, theta);
17+
}
18+
};
19+
} // namespace
20+
21+
TEST_F(laplace_count_two_dim_diag_test, latent_solve_mean_and_cov) {
22+
using stan::math::laplace_latent_solve;
23+
auto [mean_est, chol_est]
24+
= laplace_latent_solve(poisson_log_likelihood{}, std::forward_as_tuple(y),
25+
1, stan::math::test::diagonal_kernel_functor{},
26+
std::forward_as_tuple(phi(0), phi(1)), nullptr);
27+
constexpr double tol = 1e-6;
28+
EXPECT_EQ(2, mean_est.size());
29+
EXPECT_NEAR(theta_root(0), mean_est(0), tol);
30+
EXPECT_NEAR(theta_root(1), mean_est(1), tol);
31+
EXPECT_NEAR(0.0, chol_est(0, 1), 1e-12); // check lower triangular matrix
32+
Eigen::MatrixXd Sigma_est = chol_est * chol_est.transpose();
33+
EXPECT_NEAR(K_laplace(0, 0), Sigma_est(0, 0), tol);
34+
EXPECT_NEAR(K_laplace(1, 1), Sigma_est(1, 1), tol);
35+
EXPECT_NEAR(K_laplace(0, 1), Sigma_est(0, 1), tol);
36+
EXPECT_NEAR(K_laplace(1, 0), Sigma_est(1, 0), tol);
37+
}
38+
39+
TEST_F(laplace_count_two_dim_diag_test, latent_tol_solve_mean_and_cov) {
40+
using stan::math::laplace_latent_tol_solve;
41+
constexpr double tolerance = 1e-12;
42+
constexpr int max_num_steps = 1000;
43+
constexpr int hessian_block_size = 1;
44+
constexpr int solver = 1;
45+
constexpr int max_steps_line_search = 0;
46+
auto [mean_est, chol_est] = laplace_latent_tol_solve(
47+
poisson_log_likelihood{}, std::forward_as_tuple(y), hessian_block_size,
48+
stan::math::test::diagonal_kernel_functor{},
49+
std::forward_as_tuple(phi(0), phi(1)),
50+
std::make_tuple(theta_0, tolerance, max_num_steps, solver,
51+
max_steps_line_search, true),
52+
nullptr);
53+
constexpr double tol = 1e-6;
54+
EXPECT_EQ(2, mean_est.size());
55+
EXPECT_NEAR(theta_root(0), mean_est(0), tol);
56+
EXPECT_NEAR(theta_root(1), mean_est(1), tol);
57+
EXPECT_NEAR(0.0, chol_est(0, 1), 1e-12); // check lower triangular matrix
58+
Eigen::MatrixXd Sigma_est = chol_est * chol_est.transpose();
59+
EXPECT_NEAR(K_laplace(0, 0), Sigma_est(0, 0), tol);
60+
EXPECT_NEAR(K_laplace(1, 1), Sigma_est(1, 1), tol);
61+
EXPECT_NEAR(K_laplace(0, 1), Sigma_est(0, 1), tol);
62+
EXPECT_NEAR(K_laplace(1, 0), Sigma_est(1, 0), tol);
63+
}
64+
65+
TEST_F(laplace_count_two_dim_diag_test,
66+
latent_solve_singular_covariance_throws) {
67+
using stan::math::laplace_latent_solve;
68+
EXPECT_THROW(({
69+
laplace_latent_solve(
70+
poisson_log_likelihood{}, std::forward_as_tuple(y), 1,
71+
stan::math::test::diagonal_kernel_functor{},
72+
std::forward_as_tuple(0.0, phi(1)), // singular covariance
73+
nullptr);
74+
}),
75+
std::domain_error);
76+
}

0 commit comments

Comments
 (0)