By using this website, you agree to the cookie policy

Blog Publications About πŸŒ™

DIY Metropolis-Hastings

This post walks through a simple implementation of the Metropolis-Hastings algorithm.

Background

If you haven’t read my previous post on pseudorandom number generation, be sure to check it out since we’ll be reusing the pseudorandom number generator discussed there.

Metropolis-Hastings is a Markov chain Monte Carlo technique to sample from distributions for which we don’t know how to perform direct sampling. In particular, we can use this algorithm to sample from a distribution without a known normalization constant.

For this post, we’ll sample from a Gaussian distribution without needing to know the Gaussian normalization constant of 1Οƒ2Ο€\frac 1 {\sigma \sqrt {2 \pi}}Οƒ2π​1​.

Here is the high-level summary of how we will accomplish this:

  1. Set initial sample x0=0x_0 = 0x0​=0 (this is arbitrary)
  2. Propose new sample xproposedx_{proposed}xproposed​
  3. Use g(x)g(x)g(x) which is proportional to target distribution f(x)f(x)f(x) to probabilistically accept or reject xproposedx_{proposed}xproposed​ over x0x_0x0​
  4. Set x1x_1x1​ equal to the value of x0x_0x0​ or xproposedx_{proposed}xproposed​ based on the outcome of step 3.
  5. Repeat steps 2-4 to determine the next sample, x2x_2x2​, and continue repeating for x3,x4,...,xNx_3, x_4, ..., x_Nx3​,x4​,...,xN​.

Sample Proposal

Of the 5 steps above, steps 2 and 3, sample proposal and sample selection, are the least straightforward and will be explained in this section and the next. In the sample proposal step, we generate a new sample which might be returned as the next output from the algorithm while in the sample selection step we probabilistically return either the newly proposed sample or the sample which was returned in the previous iteration of the algorithm.

Sample proposal can be straight forward, and for this tutorial we’ll use a very simple method. Given a previous sample value of xtx_txt​, the proposed sample will have a value of

xproposed=xt+U[βˆ’12,12]x_{proposed} = x_t + \mathcal{U}_{[- \frac 1 2,\frac 1 2]}xproposed​=xt​+U[βˆ’21​,21​]​

where U[βˆ’12,12]\mathcal{U}_{[- \frac 1 2,\frac 1 2]}U[βˆ’21​,21​]​ is a continuous uniform random variable sampled between βˆ’12- \frac 1 2βˆ’21​ and 12\frac 1 221​. For the first iteration of Metropolis-Hastings, x0x_0x0​ will be an arbitrary value such as 000 but should ideally have a probability >0\gt 0>0 for the target distribution.

The equivalent Python code for sample proposal is simple:

def propose_sample(current_sample: float) -> float:
    return current_sample + uniform() - 0.5


proposed_sample = propose_sample(x0)

uniform() returns a continuous random variable with values between 000 and 111 (U[0,1]\mathcal{U}_{[0,1]}U[0,1]​), and the code behind it can be read about here.

Sample Selection

In step 3, sample selection, we probabilistically choose either xproposedx_{proposed}xproposed​ or xtx_txt​ to be the next sample value xt+1x_{t+1}xt+1​.

In order to perform sample selection, we need a function g(x)g(x)g(x) which is proportional to our distribution of interest f(x)f(x)f(x). I.e. g(x)∝f(x)g(x) \propto f(x)g(x)∝f(x) such that:

f(a)f(b)=cg(a)cg(b)=g(a)g(b)\frac {f(a)} {f(b)} = \frac {cg(a)} {cg(b)} = \frac {g(a)} {g(b)}f(b)f(a)​=cg(b)cg(a)​=g(b)g(a)​

Since the target distribution from which we’d like to sample is the Gaussian distribution f(x)=1Οƒ2Ο€eβˆ’12(xβˆ’ΞΌΟƒ)2f(x)=\frac 1 {\sigma \sqrt {2 \pi}} e^{-\frac 1 2 ({\frac {x - \mu} \sigma})^2}f(x)=Οƒ2π​1​eβˆ’21​(Οƒxβˆ’ΞΌβ€‹)2, we’ll choose g(x)=eβˆ’12(xβˆ’ΞΌΟƒ)2g(x)=e^{-\frac 1 2 ({\frac {x - \mu} \sigma})^2}g(x)=eβˆ’21​(Οƒxβˆ’ΞΌβ€‹)2.

To determine whether xt+1x_{t+1}xt+1​ takes the value of xtx_txt​ or xproposedx_{proposed}xproposed​, we sample a value uuu between 0 and 1 from a continuous uniform distribution U[0,1]\mathcal{U}_{[0,1]}U[0,1]​ and let xt+1=xtx_{t+1}=x_txt+1​=xt​ if u≀g(xt)g(xproposed)u \leq \frac {g(x_t)} {g(x_{proposed})}u≀g(xproposed​)g(xt​)​ else xt+1=xproposedx_{t+1}=x_{proposed}xt+1​=xproposed​.

This logic can be seen in the following Python code:

def score(x: float, mu: float = 0, sigma: float = 1) -> float:
    norm_x = (x - mu) / sigma
    return math.exp(-(norm_x ** 2) / 2)


def get_next_sample(current_sample: float, current_sample_score: float) -> Tuple[float, float]:
    # Calculate proposed value for x_{t+1}
    proposed_sample = propose_sample(current_sample)
    proposed_sample_score = score(proposed_sample)

    # NOTE: This code was written with simplicity in mind, but there is no reason to
    # sample from the uniform distribution if proposed_sample_score > self.current_sample_score
    if uniform() <= (proposed_sample_score / current_sample_score):
        current_sample = proposed_sample
        current_sample_score = proposed_sample_score

    return current_sample, current_sample_score


# x0 is the arbitrary starting point
x0 = 0
x0_score = score(x0)

x1, x1_score = get_next_sample(x0, x0_score)
x2, x2_score = get_next_sample(x1, x1_score)

Visualization

Lastly, we’ll implement a stateful class to generate samples:

class MetropolisHastings:
    def __init__(self, x0: float = 0) -> None:
        self.sample = x0
        self.sample_score = score(self.sample)

    def __call__(self) -> float:
        self.sample, self.sample_score = get_next_sample(self.sample, self.sample_score)
        return self.sample

Now we can generate a set of samples and make sure the distribution looks as expected.

def gen_samples(f: Callable[[], float]) -> Iterator[float]:
    for _ in tqdm(range(1000000)):
        yield f()

metropolis = MetropolisHastings()
gaussian_samples = list(gen_samples(metropolis))

Sure enough, our Gaussian samples look pretty good.

_config.yml

That’s all for this post! If you have any feedback, please feel free to reach out.

Published on 3 March 2022

If you enjoy this blog, please consider supporting me by buying one of my books. πŸ“š
(click here)