MUQ  0.4.3
CustomProposal.cpp
Go to the documentation of this file.
1 /***
2 
3 ### Overview
4 This example demonstrates how to manually specify the proposal covariance in a
5 simple random walk proposal.
6 
7 ### MUQ MCMC Interfaces
8 There are two ways to specify MCMC samplers in MUQ. The first, which is
9 demonstrated in the Example1_Gaussian example, specifies the proposal variance
10 and other algorithmic parameters through a boost property tree. A lower level
11 interface also exists, which allows users to manually specify the proposal
12 distribution and transition kernel. In this latter setting, the boost property
13 tree is then used only for chain-level settings like the number of steps,
14 burn in, and print levels.
15 
16 
17 */
20 
26 
28 
29 #include <boost/property_tree/ptree.hpp>
30 
31 namespace pt = boost::property_tree;
32 using namespace muq::Modeling;
33 using namespace muq::SamplingAlgorithms;
34 using namespace muq::Utilities;
35 
36 
37 int main(){
38 
39  /***
40  ### 1. Define the target density and set up the sampling problem
41 
42  The `Gaussian` class in MUQ provides a definition of a multivariate Gaussian
43  distribution. The distribution is completely defined by a mean vector and a
44  covariance (or precision) matrix. In MUQ, modeling components like a Gaussian
45  probability density, are represented as children of the `ModPiece` base class.
46  However, interpreting the Gaussian as as a `ModPiece` is ambiguous. The
47  Gaussian class could be used to define represent a random variable "ModPiece"
48  that returns a random realization of the multivariate Gaussian when evaluated.
49  Or the Gaussian class could define a "ModPiece" that evaluates the log
50  probability density function of the Gaussian distribution. To select one of
51  these two use cases, the MUQ `Gaussian` class has member functions `AsDensity()`
52  and `AsVariable()` that return a shared_ptr to a class that evaluates the log PDF,
53  or a class that draws a random sample, respectively. These functions are
54  implemented in the `Distribution` class, which is a parent of the `Gaussian`
55  class.
56 
57  The AbstractSamplingProblem base class and its
58  children, like the SamplingProblem class, define the interface between
59  sampling algorithms like MCMC and the models and densities they work with.
60 
61  */
62  /***
63  Define the Target Density:
64  */
65  Eigen::VectorXd mu(2);
66  mu << 1.0, 2.0;
67 
68  Eigen::MatrixXd cov(2,2);
69  cov << 1.0, 0.8,
70  0.8, 1.5;
71 
72  auto targetDensity = std::make_shared<Gaussian>(mu, cov)->AsDensity(); // standard normal Gaussian
73 
74  /***
75  Create the Sampling Problem:
76  */
77  auto problem = std::make_shared<SamplingProblem>(targetDensity);
78 
79  /***
80  ### 3. Construct the RWM proposal
81  Let $x_k$ denote the $k^{th}$ state in a Markov chain. At step $k$,
82  the Random Walk Metropolis algorithm draws a random realization of the proposal
83  random variable $x^\prime = x_k + z$, where $z\sim q(z)$ is a random
84  variable distributed according to the proposal distribution $q(z)$. The
85  proposed point is then accepted or rejected using the Metropolis-Hastings rule.
86 
87  The MUQ RWM implementation allows users to specify the proposal distributions
88  $q(z)$ either through options in a boost property_tree (see the Example1_Gaussian
89  example) or manually (demonstrated below). Here, we employ the latter approach
90  and manually specify an instance of MUQ's `MHProposal` class, which is then
91  combined with the `MHKernel` Markov transition kernel to define the RWM algorithm.
92  */
93 
94  /***
95  Define the proposal distribution.
96  */
97  Eigen::VectorXd propMu = Eigen::VectorXd::Zero(2);
98 
99  // Set the proposal covariance to be the optimal scaling of the target covariance
100  Eigen::MatrixXd propCov = (2.4*2.4/std::sqrt(2))*cov;
101 
102  auto propDist = std::make_shared<Gaussian>(propMu, propCov);
103 
104  /***
105  Use the Gaussian proposal distribution to define an MCMC proposal class.
106  */
107  pt::ptree propOpts;
108  auto proposal = std::make_shared<MHProposal>(propOpts, problem, propDist);
109 
110  /***
111  Construct the Metropolis-Hastings (MH) Markov transition kernel using the
112  proposal.
113 
114  MUQ can perform blockwise updates of target densities that have multiple
115  vector-valued inputs (e.g., parameters and hyperparameters). The
116  `SingleChainMCMC` class employed below therefore expects a transition kernel
117  for each parameter block. These kernels are passed to the `SingleChainMCMC`
118  constructor as a `std::vector` of transition kernels. Here, we only have a
119  single kernel but we store the kernel in a vector to match the interface
120  expected by `SingleChainMCMC`.
121  */
122  pt::ptree kernOpts;
123  std::vector<std::shared_ptr<TransitionKernel>> kernels(1);
124  kernels.at(0) = std::make_shared<MHKernel>(kernOpts, problem, proposal);
125 
126  /***
127  Use the kernel to define a single chain MCMC algorithm.
128  */
129  pt::ptree chainOpts;
130  chainOpts.put("NumSamples", 1e4); // number of MCMC steps
131  chainOpts.put("BurnIn", 1e3);
132  chainOpts.put("PrintLevel",3);
133 
134  auto mcmc = std::make_shared<SingleChainMCMC>(chainOpts,kernels);
135 
136  /***
137  ### 3. Run the MCMC algorithm
138  We are now ready to run the MCMC algorithm. Here we start the chain at the
139  target densities mean. The resulting samples are returned in an instance
140  of the SampleCollection class, which internally holds the steps in the MCMC chain
141  as a vector of SamplingStates.
142  */
143  Eigen::VectorXd startPt = mu;
144  std::shared_ptr<SampleCollection> samps = mcmc->Run(startPt);
145 
146  /***
147  ### 4. Run multiple chains to assess convergence
148 
149  */
150  unsigned int numChains = 4;
151  std::vector<std::shared_ptr<SampleCollection>> chains(numChains);
152  chains.at(0) = samps;
153 
154  for(unsigned int i=1; i<numChains; ++i){
155 
156  // Construct a new sampler.
157  mcmc = std::make_shared<SingleChainMCMC>(chainOpts,kernels);
158 
159  // Generate a "diffuse" starting point for the chain
160  startPt = std::sqrt(1.5) * RandomGenerator::GetNormal(2);
161 
162  // Run the sampler
163  chains.at(i) = mcmc->Run(startPt);
164  }
165 
166  /***
167  The $\hat{R}$ statistic compares the intra- and inter-chain variances to assess
168  whether the chains have converged to the same distribution (the target distribution).
169  Here we use the `Diagnostics` class in MUQ to compute $\hat{R}$ following the split and
170  rank-transformed techniques described in [[Vehtari et al., 2021](https://arxiv.org/abs/1903.08008)].
171  If $\hat{R}$ is close to $1$ (e.g., $\hat{R}<1.01$), then the chains are past burn in and
172  are likely generating samples of the posterior.
173  */
174 
175  Eigen::VectorXd rhat = Diagnostics::Rhat(chains);
176  std::cout << "\nRhat:\n" << rhat.transpose() << std::endl;
177 
178 
179  /***
180  ### 5. Analyze the results
181 
182  The SampleCollection class provides several functions for computing sample moments.
183  For example, here we compute the mean, variance, and third central moment.
184  While the third moment is actually a tensor, here we only return the marginal
185  values, i.e., $\mathbb{E}_x[(x_i-\mu_i)^3]$ for each $i$.
186  */
187  Eigen::VectorXd sampMean = samps->Mean();
188  std::cout << "\nSample Mean:\n" << sampMean.transpose() << std::endl;
189 
190  Eigen::VectorXd sampVar = samps->Variance();
191  std::cout << "\nSample Variance:\n" << sampVar.transpose() << std::endl;
192 
193  Eigen::MatrixXd sampCov = samps->Covariance();
194  std::cout << "\nSample Covariance:\n" << sampCov << std::endl;
195 
196  Eigen::VectorXd sampMom3 = samps->CentralMoment(3);
197  std::cout << "\nSample Third Moment:\n" << sampMom3.transpose() << std::endl << std::endl;
198 
199  /***
200  ### 6. Inspect the sample meta data.
201 
202  In addition to storing the state of the MCMC chain, MUQ may also store extra
203  information stored by the transition kernel or proposal. This "metadata"
204  is store in the `SampleCollection` and can be accessed using the `GetMeta`
205  function of the `SampleCollection` class. It is also possible to list what
206  metadata is available using the `ListMeta` function.` Below, we first list
207  all of the available metadata and then extract the log of the target density.
208 
209  Note that the `GetMeta` function returns a matrix of doubles regardless of the
210  size or type of the metadata. Each column of the matrix corresponds to a sample
211  and each row of the matrix to a component of the (possibly) vector-valued
212  metadata variable. For scalar values, like the log density, there will only
213  be a single row.
214 
215  */
216 
217  // List the name of any metadata stored by the samples
218  std::cout << "Available MetaData: " << std::endl;
219  for(auto& metaKey : samps->ListMeta())
220  std::cout << " \"" << metaKey << "\"" << std::endl;
221  std::cout << std::endl;
222 
223  // Extract the log-density of the target distribution at each sample
224  Eigen::MatrixXd logTargetDens = samps->GetMeta("LogTarget");
225 
226  // Compute the maximum log target density and store the sample index where it occured
227  double maxLogDens;
228  unsigned int maxRow, maxCol;
229  maxLogDens = logTargetDens.maxCoeff(&maxRow, &maxCol);
230 
231  std::cout << "From MetaData:" << std::endl;
232  std::cout << " p* = max log(p(x)) = " << maxLogDens << std::endl;
233  std::cout << " x* = argmax p(x) = " << samps->at(maxCol)->state.at(0).transpose() << std::endl;
234 
235  /***
236  ### 7. Extract samples as a matrix for further processing.
237 
238  In some cases you will want to extract a matrix of the MCMC states from the
239  `SampleCollection` object. As shown below, that can be easily accomplished
240  using the `AsMatrix` function in the `SampleCollection` class.
241  */
242  Eigen::MatrixXd sampMat = samps->AsMatrix();
243 
244  std::cout << "\nMean using eigen = " << sampMat.rowwise().mean().transpose() << std::endl;
245 
246  return 0;
247 }
int main()