<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://thomaswarford.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://thomaswarford.github.io/" rel="alternate" type="text/html" /><updated>2026-09-04T00:55:29+00:00</updated><id>https://thomaswarford.github.io/feed.xml</id><title type="html">Thomas Warford</title><subtitle>PhD student at the University of Cambridge, focused on using machine learning to accelerate the development of materials.</subtitle><author><name>Thomas Warford</name></author><entry><title type="html">Discovering Digits with Convolutional Autoencoders</title><link href="https://thomaswarford.github.io/2023/10/22/convolutional-autoencoders.html" rel="alternate" type="text/html" title="Discovering Digits with Convolutional Autoencoders" /><published>2023-10-22T00:00:00+00:00</published><updated>2023-10-22T00:00:00+00:00</updated><id>https://thomaswarford.github.io/2023/10/22/convolutional-autoencoders</id><content type="html" xml:base="https://thomaswarford.github.io/2023/10/22/convolutional-autoencoders.html"><![CDATA[<table>
  <thead>
    <tr>
      <th style="text-align: center"><img src="/assets/images/cae/tsne.png" alt="t-SNE latent space representation of MNIST digits learned by convolutional autoencoder" /></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><em>Each colour corresponds with a different digit — the seperation between digits has been learned!</em></td>
    </tr>
  </tbody>
</table>

<p>Autoencoders are neural networks which take an image as input, and are trained to reproduce that image. Their usefulness comes from from their middle layer activations having fewer dimensions than the original data, effectively compressing the input data. These activations can be thought of as vectors in a latent space, and the positions of the vectors in this space can be really interesting.</p>

<p>Although not created with an autoenoder, the learned vectors of words created by word2vec illustrate how interesting this can be, for instance “king=queen-woman+man”.</p>

<h2 id="the-architecture">The Architecture</h2>

<p>Convolutional layers are great at extracting features from images, and by making stride-2 convolutions the width and height of images is halved.</p>

<p>Here’s the pytorch code for a simple convolutional layer, simply a convolution followed by an optional activation layer.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">conv</span><span class="p">(</span>
    <span class="n">ni</span><span class="p">,</span> <span class="c1"># input channels (3 for rgb image)
</span>    <span class="n">nf</span><span class="p">,</span> <span class="c1"># output channels
</span>    <span class="n">ks</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="c1"># kernal size (ks * ks)
</span>    <span class="n">act</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span> <span class="c1">#whether we add an activation layer
</span>
    <span class="n">layers</span> <span class="o">=</span> <span class="p">[</span><span class="n">nn</span><span class="p">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="n">ni</span><span class="p">,</span> <span class="n">nf</span><span class="p">,</span> <span class="n">ks</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="n">ks</span><span class="o">//</span><span class="mi">2</span><span class="p">)]</span>
    <span class="k">if</span> <span class="n">act</span><span class="p">:</span>
        <span class="n">layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">())</span>
    
    <span class="k">return</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span><span class="o">*</span><span class="n">layers</span><span class="p">)</span>
</code></pre></div></div>
<p>Many of these convolutional layers put together forms an encoder, which generates the latent representation.</p>

<p>Here’s a “deconvolutional” layer. The upsampling increases the width and height of images by 2 whilst the stride-1 convolution has no effect on image dimensions.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">deconv</span><span class="p">(</span>
    <span class="n">ni</span><span class="p">,</span> <span class="c1"># input channels (3 for rgb image)
</span>    <span class="n">nf</span><span class="p">,</span> <span class="c1"># output channels
</span>    <span class="n">ks</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="c1"># kernal size (ks * ks)
</span>    <span class="n">act</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span> <span class="c1">#whether we add an activation layer)
</span>    
    <span class="n">layers</span> <span class="o">=</span> <span class="p">[</span><span class="n">nn</span><span class="p">.</span><span class="n">UpsamplingNearest2d</span><span class="p">(</span><span class="n">scale_factor</span><span class="o">=</span><span class="mi">2</span><span class="p">),</span> 
                <span class="n">nn</span><span class="p">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="n">ni</span><span class="p">,</span> <span class="n">nf</span><span class="p">,</span> <span class="n">ks</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="n">ks</span><span class="o">//</span><span class="mi">2</span><span class="p">)]</span>
    
    <span class="k">if</span> <span class="n">act</span><span class="p">:</span> <span class="n">layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">())</span>
    
    <span class="k">return</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span><span class="o">*</span><span class="n">layers</span><span class="p">)</span>
</code></pre></div></div>
<p>A series of deconvolutional layers together serves as our encoder, building up the reproduced image from our latent representation.</p>

<p>Here’s the full architecture:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Autoencoder</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">n_latent</span><span class="o">=</span><span class="mi">128</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">encode</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">ZeroPad2d</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span> <span class="c1"># 32x32
</span>            <span class="n">conv</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span> <span class="c1"># 16x16
</span>            <span class="n">nn</span><span class="p">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span> <span class="c1"># 16x16
</span>            <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
            <span class="n">conv</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span> <span class="c1"># 8x8
</span>            <span class="n">conv</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">16</span><span class="p">),</span> <span class="c1"># 4x4
</span>            <span class="n">nn</span><span class="p">.</span><span class="n">Flatten</span><span class="p">(),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="mi">16</span><span class="o">*</span><span class="mi">4</span><span class="o">*</span><span class="mi">4</span><span class="p">,</span> <span class="n">n_latent</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Tanh</span><span class="p">()</span>
        <span class="p">)</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">decode_linear</span><span class="o">=</span><span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">n_latent</span><span class="p">,</span> <span class="mi">16</span><span class="o">*</span><span class="mi">4</span><span class="o">*</span><span class="mi">4</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">()</span>
        <span class="p">)</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">decode</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
            <span class="n">deconv</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span> <span class="c1"># 8x8
</span>            <span class="n">deconv</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span> <span class="c1">#16x16
</span>            <span class="n">nn</span><span class="p">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span> <span class="c1"># 16x16
</span>            <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
            <span class="n">deconv</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="n">act</span><span class="o">=</span><span class="bp">False</span><span class="p">),</span> <span class="c1">#32x32
</span>            <span class="n">nn</span><span class="p">.</span><span class="n">ZeroPad2d</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">),</span> <span class="c1">#28x28
</span>            <span class="n">nn</span><span class="p">.</span><span class="n">Sigmoid</span><span class="p">()</span>
        <span class="p">)</span>
        
    
    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
        <span class="n">output</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">output</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">decode_linear</span><span class="p">(</span><span class="n">output</span><span class="p">)</span>
        <span class="n">output</span> <span class="o">=</span> <span class="n">output</span><span class="p">.</span><span class="n">view</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="n">output</span><span class="p">)</span>
</code></pre></div></div>

<p>Note the addition of a linear layer and Tanh activation to the encoder. I opted to use Tanh to make the latent vectors easier to work with.</p>

<p>You can check out the notebook here: <a href="https://www.kaggle.com/code/thomaswarford/mnist-autoencoder-clustering">https://www.kaggle.com/code/thomaswarford/mnist-autoencoder-clustering</a></p>

<h2 id="results">Results</h2>

<p>The autoencoder was trained on the MNIST handwritten digits dataset, with a latent vector of size 128.</p>

<p>Here are the resulting reproductions of letters:</p>

<table>
  <tr>
    <td><img src="/assets/images/cae/7_targ.png" alt="original 7" style="width: 100%; image-rendering: pixelated;" /></td>
    <td><img src="/assets/images/cae/7_recon.png" alt="reconstructed 7" style="width: 100%; image-rendering: pixelated;" /></td>
  </tr>
  <tr>
    <td colspan="2">Original and reproduction of the letter 7.</td>
  </tr>
</table>
<p><br /></p>
<table>
  <tr>
    <td><img src="/assets/images/cae/9_targ.png" alt="original 9" style="width: 100%; image-rendering: pixelated;" /></td>
    <td><img src="/assets/images/cae/9_recon.png" alt="reconstructed 9" style="width: 100%; image-rendering: pixelated;" /></td>
  </tr>
  <tr>
    <td colspan="2">Original and reproduction of letter 9.</td>
  </tr>
</table>

<p>Now our autoencoder is trained, we can use the encoder to “vectorize” images. <a href="https://en.wikipedia.org/wiki/T-distributed_stochastic_neighbor_embedding">t-SNE</a> let’s us visualize these length-128 vectors in 2 dimensions. Here is the resulting plot, where each colour represents a different digit.</p>

<p><img src="/assets/images/cae/tsne.png" alt="t-SNE 2D projection of learned digit clusters" /></p>

<p>There we go! The separation between different digits is quite impressive, considering we never told the network the labels explicitly.</p>

<h2 id="improvements">Improvements</h2>

<p>Considering the simplicity of this architecture, it does pretty well. However, applying autoencoders to larger, more complicated images will require some refinements.</p>

<p>Currently my lab partner and I are training an autoencoder to recreate band structure plots similar to the one below and are getting blurry white images as a result.</p>

<p><img src="/assets/images/cae/bandstructure.png" alt="fluorine iodide band structure plot" /></p>

<p>This might suggest that our activations are tending towards zero as you go through the layers — this could be fixed by batch normalisation or LSUV. We’re also going to look at ResNet architectures for inspiration.</p>

<h3 id="acknowledgements">Acknowledgements</h3>

<p>Thanks for reading. I learnt about autoencoders, including the (de)convolutional layers above, from fast.ai, as well as the tricks I’ll need to make them better. The kaggle notebook linked above uses some of <a href="https://www.kaggle.com/code/evananders/digit-recognizer-02-fine-tune-fastai-vision-mode">Evan Anders’</a> code to load in the data.</p>]]></content><author><name>Thomas Warford</name></author><summary type="html"><![CDATA[An exploration of unsupervised representation learning and latent space clustering on MNIST digits using convolutional autoencoders in PyTorch.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://thomaswarford.github.io/assets/images/cae/tsne.png" /><media:content medium="image" url="https://thomaswarford.github.io/assets/images/cae/tsne.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Multi-GPU Physics Based Hamiltonian Monte Carlo</title><link href="https://thomaswarford.github.io/2022/09/15/multi-gpu-hamiltonian-monte-carlo.html" rel="alternate" type="text/html" title="Multi-GPU Physics Based Hamiltonian Monte Carlo" /><published>2022-09-15T00:00:00+00:00</published><updated>2022-09-15T00:00:00+00:00</updated><id>https://thomaswarford.github.io/2022/09/15/multi-gpu-hamiltonian-monte-carlo</id><content type="html" xml:base="https://thomaswarford.github.io/2022/09/15/multi-gpu-hamiltonian-monte-carlo.html"><![CDATA[<div class="editors-note">
    <strong>Editor's Note:</strong> This was my PRACE Summer of HPC 2022 final report, written with Bruno Rodriguez Carrillo under the mentorship of Anton Lebedev at the Hartree Centre. The PDF with all other reports can be found [here](https://web.archive.org/web/20250324162031/https://summerofhpc.prace-ri.eu/wp-content/uploads/2022/10/SoHPC2022_final_reports.pdf). The work ultimately resulted in [this conference paper](https://doi.org/10.1007/978-3-031-36030-5_48).
</div>

<p>HMC is widely used in probabilistic programming, as part of fitting many-parameter models. Our implementation — created from scratch — is scalable, portable and based on Physics.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center"><img src="/assets/images/hmc/hmc.gif" alt="HMC proposals on a parabolic potential" /></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><em>HMC exploring a bowl-shaped potential. Accepted proposals are shown in blue, rejected ones in red.</em></td>
    </tr>
  </tbody>
</table>

<p>Generating samples from a given probability distribution function is a surprisingly difficult task, and Hamiltonian Monte Carlo (HMC) solves this challenge in an elegant way. HMC is widely used in probabilistic programming packages, such as <a href="https://mc-stan.org/docs/reference-manual/index.html">Stan</a> and <a href="https://num.pyro.ai/en/stable/mcmc.html">Numpyro</a>, which use the algorithm to sample parameters from posterior probability distributions, which govern the distribution of model parameters given some data. The size and number of parameters of some statistical models, such as those in epidemiology, justifies parallelized HMC schemes.</p>

<p>There were two principal goals for this project. First, we had to implement from scratch in Python the HMC algorithm as presented and discussed in previous works. For such a purpose, we opted for a physics based approach, as this allowed for a more intuitive interpretation and future extension of the method. We assumed we were given a system with particles moving in a multidimensional space and these do not interact with each other. In addition, each particle had its own mass and the system could be heated up and cooled down by considering the temperature as a parameter.</p>

<p>Secondly, we adapted our implementation with the goal of it being able to run on multi-CPU or GPU clusters by simply changing one line of code.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center"><img src="/assets/images/hmc/metropolis-hastings.gif" alt="Proposals generated by the Metropolis-Hastings algorithm" /></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><em>Proposals generated by the Metropolis-Hastings algorithm. Note the high rejection rate and high correlation, even with 2 dimensions.</em></td>
    </tr>
  </tbody>
</table>

<h2 id="theory">Theory</h2>

<p>To begin with, let us assume that we have a model, and some data which is supposed to be explained by the model. Through Bayesian inference the probability distribution of the model parameters can be deduced. Generating parameter samples from this distribution is difficult or impossible via inverse transform sampling, but can be done via HMC.</p>

<h3 id="bayes-rule">Bayes Rule</h3>

<p>We recall that Bayes theorem states that given the <em>prior</em> probability \(p(\mathbf{q})\) and a set of observed data \(y\) whose <em>likelihood</em> is \(p(y \mid \mathbf{q})\), then the <em>posterior</em> probability \(p(\mathbf{q} \mid y)\) is</p>

\[p(\mathbf{q} \mid y) = \frac{p(\mathbf{q}) \, p(y \mid \mathbf{q})}{p(y)},\]

<p>where \(p(y)\) is a normalizing constant, which may be ignored. Typically, \(p(\mathbf{q} \mid y)\) corresponds to the distribution we want to sample from, which we will call \(\pi(q)\) from now on. HMC samples from this distribution for the parameters, as outlined below. This is done to calculate expectation values for the parameters of the distribution, along with standard deviations.</p>

<h3 id="markov-chain-sampling-methods">Markov Chain Sampling Methods</h3>

<p>Let us assume we are interested in generating samples from a probability distribution function or density function \(\pi(\mathbf{q})\) with parameter \(\mathbf{q}\) on a \(D\)-dimensional space, \(Q\). Furthermore, \(\pi(\mathbf{q})\) is not easily invertible or is not available in closed form.</p>

<p>The subset \(T \subset Q\) where the product \(\pi(\mathbf{q}) \, d\mathbf{q}\) such that \(\mathbf{q} \in T\) is non-negligible is called the typical set. This region is where we want to sample from. Notably the volume of \(T\) relative to the region surrounding \(T\) decreases as \(D\) increases.</p>

<p>There exist many sampling techniques to generate points from \(\pi(\mathbf{q})\) and one of the most widely used techniques is a class of statistical methods called Markov Chain Monte Carlo (MCMC). These methods, intuitively, consist of randomly navigating through the space spanned by parameter \(\mathbf{q}\), commonly called <em>parameter space</em>.</p>

<p>Most MCMC methods work by generating the proposals for the next sample (\(\mathbf{q}_{n+1}\)) in proximity to the current sample (\(\mathbf{q}_n\)). For instance, the Gaussian Metropolis-Hastings method involves taking a random Gaussian step in parameter space to get the next proposal, which we denote by \(\mathbf{q}^\prime\). The proposal is then accepted or rejected with probability \(\pi(\mathbf{q}^\prime)/\pi(\mathbf{q}_n)\). The rejection of samples at a lower probability density causes the walk to “stay on track”, and for computational resources to be focused on the typical set, where the density of the target distribution is significant.</p>

<p>This simple Metropolis-Hastings method, shown above, struggles with high-dimensional distributions, when \(D\) is large, as the volume surrounding \(T\) is much larger than the volume of \(T\) itself, meaning most proposals are in regions of \(Q\) where \(\pi(\mathbf{q}) \, d\mathbf{q}\) is negligible and are, therefore, rejected. This is not efficient at best and totally infeasible if \(D\) is sufficiently high.</p>

<h3 id="from-markov-chains-to-particles">From Markov Chains to Particles</h3>

<p>This is where HMC steps in. Work on the random motion of smoke particles in air (Brownian motion) by Einstein and others established that the motion of particles in a gas can be modeled using Markov chains. Inverting this, some Markov chains can be considered as particles. Suppose that we are given a particle whose position vector \(\mathbf{q}\) is on a \(D\)-dimensional space; keep in mind that \(\mathbf{q}\) is the parameter we are interested in. HMC doubles the parameter space \(Q\) by adding an extra parameter, which we refer to as <em>momentum</em>, \(\mathbf{p}\). Then, we have a joint probability distribution function depending on \(\mathbf{q}\) and \(\mathbf{p}\), that is, \(\pi(\mathbf{q}, \mathbf{p}) = \pi(\mathbf{p} \mid \mathbf{q}) \, \pi(\mathbf{q})\).</p>

<p>Notably, in physics we often sample from the <em>canonical distribution</em>, which is given by</p>

\[\pi(\mathbf{q}, \mathbf{p}) = \exp\bigl(-H(\mathbf{q}, \mathbf{p})\bigr),\]

<p>where \(H(\mathbf{q}, \mathbf{p})\) is the Hamiltonian or energy value at \((\mathbf{q}, \mathbf{p})\).</p>

<p>We notice that \(H(\mathbf{q}, \mathbf{p})\) can be written as the sum of kinetic \(K(\mathbf{p}, \mathbf{q})\) and potential energy \(V(\mathbf{q})\) as follows:</p>

\[H(\mathbf{q}, \mathbf{p}) = -\log \pi(\mathbf{q}, \mathbf{p}) = -\log \pi(\mathbf{p} \mid \mathbf{q}) - \log \pi(\mathbf{q}) = K(\mathbf{p}, \mathbf{q}) + V(\mathbf{q}).\]

<p>This means that each point in the parameter space is assigned a potential energy \(V(\mathbf{q}) = -\ln{\pi(\mathbf{q})}\). To illustrate this, imagine a 2D Gaussian distribution \(\pi(\mathbf{q}) = \exp(-q_1^2 - q_2^2)\) (lacking normalization). This has a bowl shaped parabolic potential energy function \(V(\mathbf{q}) = q_1^2 + q_2^2\). We set \(K(\mathbf{p}, \mathbf{q}) = (\mathbf{p}^{T} \cdot \mathbf{p}) / 2m\). Such a choice of \(K(\mathbf{p}, \mathbf{q})\) is usually implemented as discussed in the references. Besides, we state that the Hamiltonian captures the “geometrical information” of \(T\) and that \(K(\mathbf{p}, \mathbf{q})\) is non-unique.</p>

<p>To go from one sample to the next, we give the “particle”, whose mass is \(m\), a random momentum \(\mathbf{p}\) from a Maxwell-Boltzmann distribution, which approximates the motion of particles in an ideal gas. In practice this means momentum is drawn from a multivariate normal distribution with a mean of zero and the standard deviation for each coordinate as given below:</p>

\[\sigma_{p_i}^2 = \sigma_{p_{i+1}}^2 = \dots = m k_b T,\]

<p>for \(i = 1, \dots, D-1\).</p>

<p>We then simulate the evolution of the system for a certain number of time-steps, which requires solving Hamilton’s equations for \(\mathbf{p}, \mathbf{q}\). This is done using a symplectic integrator — which conserves the Hamiltonian, i.e. energy — such as the leapfrog method. We recall that the force \(F\) acting upon the particle is the negative of the gradient of the potential with respect to position; that is</p>

\[F = -\nabla V(\mathbf{q}).\]

<p>To concretize the evolution, imagine a hockey puck being given a random momentum in a large, parabolic bowl. In an ideal world, this method of generating proposals negates the need for an acceptance/rejection step, for reasons outlined in the references. However, in reality, a small number of proposals are rejected due to numerical integration errors. Nonetheless, with a suitable time-step most proposals are accepted and samples have a lower correlation than those produced by Metropolis-Hastings.</p>

<h2 id="methods">Methods</h2>

<p>The implementation was made using Google’s jax, which is NumPy accelerated on GPU. Jax features just-in-time compilation, automatic differentiation and parallelization of evaluation by means of vectorization via <code class="language-plaintext highlighter-rouge">vmap</code>. MPI was used to run the program on multiple GPUs, where <code class="language-plaintext highlighter-rouge">vmap</code> was used to run multiple particles in parallel as illustrated below.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center"><img src="/assets/images/hmc/flow.png" alt="Parallel architecture" /></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><em>Parallel architecture.</em></td>
    </tr>
  </tbody>
</table>

<p>Numpyro played an important role in the project, as it was used to set up the probabilistic models and calculate the log posterior distribution from observations. The posteriors were then sampled from using our HMC kernel.</p>

<h2 id="results">Results</h2>

<p>We achieved the initial goal of fitting probabilistic models on multiple GPUs. The implementation is also extremely portable, as it can be run on CPUs or GPUs easily, with no requirement for all GPUs to be of the same architecture. The figure below shows the scaling of the implementation with the number of particles on a single CPU, a single GPU and two GPUs of different architectures.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center"><img src="/assets/images/hmc/scaling.png" alt="Runtime vs number of particles on CPU, one GPU and two GPUs" /></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><em>A GTX 1080Ti and RTX 3050 were used, inferring the bias of two coins.</em></td>
    </tr>
  </tbody>
</table>

<p>The speedup of the algorithm with the number of V100 GPUs used is plotted below, whilst the change in run time on one GPU as the number of particles increases is shown after it. The weak scaling in both plots is a consequence of the model being far too small to fully utilize the GPUs, meaning much of the run time is overhead. We limited ourselves to small models which can be solved analytically because this allowed us to verify the correctness of the results obtained with our implementation.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center"><img src="/assets/images/hmc/speedup.png" alt="Speedup vs number of V100 GPUs" /></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><em>Speedup vs number of V100 GPUs for various numbers of particles.</em></td>
    </tr>
  </tbody>
</table>

<table>
  <thead>
    <tr>
      <th style="text-align: center"><img src="/assets/images/hmc/scaling_2.png" alt="Speedup vs number of particles on one V100 GPU" /></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><em>Speedup vs Number of Particles on one V100 GPU.</em></td>
    </tr>
  </tbody>
</table>

<h2 id="discussionconclusions">Discussion/Conclusions</h2>

<p>Overall we are pleased with our progress and the results so far have been promising. In the future, we plan to fit some larger models and explore how tuning physical parameters can affect our performance.</p>

<p>One promising avenue is simulated annealing. Let us say we have a bimodal distribution function (i.e. a pdf with two local maxima). To start we set momentum’s with a high temperature, meaning the entire potential landscape (and hopefully each mode) is more readily explored. We gradually lower the temperature before generating samples from the chains, so the tails of the distribution are not over represented.</p>

<p>Also, it is worth noting that current results have been obtained with code that has not yet been optimized. Profiling reveals significant host-device communication which, in combination with the small models used, explains the limited scalability observed so far. Eliminating these transfers and increasing the model size in the coming weeks should lead to major improvements in scalability.</p>

<h3 id="project-details">Project details</h3>

<ul>
  <li><strong>Project title:</strong> Multi-GPU Physics Based Hamiltonian Monte Carlo (PRACE SoHPC Project ID 2216)</li>
  <li><strong>Site:</strong> Hartree Centre — STFC, United Kingdom</li>
  <li><strong>Authors:</strong> Bruno Rodriguez Carrillo (Mexico), Thomas Warford (UK)</li>
  <li><strong>Mentor:</strong> Anton Lebedev, Hartree Centre — STFC, UK</li>
  <li><strong>Software applied:</strong> <a href="https://github.com/google/jax">jax</a>, <a href="https://github.com/mpi4jax/mpi4jax">mpi4jax</a>, <a href="https://github.com/pyro-ppl/numpyro">numpyro</a></li>
</ul>

<h3 id="acknowledgements">Acknowledgements</h3>

<p>Thanks to Anton Lebedev for mentorship and contribution to code. Thanks to Hartree Centre and PRACE.</p>

<h3 id="references">References</h3>

<ul>
  <li><a href="https://mc-stan.org/docs/reference-manual/index.html">Stan Reference Manual</a>, Version 2.30, Chapter 15.</li>
  <li><a href="https://num.pyro.ai/en/stable/mcmc.html">Numpyro Documentation</a>.</li>
  <li>Betancourt, M. (2017). A conceptual introduction to Hamiltonian Monte Carlo. arXiv preprint arXiv:1701.02434.</li>
  <li>Hoffman, M. D., &amp; Gelman, A. (2014). The No-U-Turn sampler: adaptively setting path lengths in Hamiltonian Monte Carlo. J. Mach. Learn. Res., 15(1), 1593-1623.</li>
</ul>]]></content><author><name>Thomas Warford</name></author><summary type="html"><![CDATA[Implementing Hamiltonian Monte Carlo from scratch in jax, with a physics-based formulation that runs across multiple CPUs and GPUs by changing a single line of code.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://thomaswarford.github.io/assets/images/hmc/hmc-still.png" /><media:content medium="image" url="https://thomaswarford.github.io/assets/images/hmc/hmc-still.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">What is Hamiltonian Monte Carlo?</title><link href="https://thomaswarford.github.io/2022/08/09/what-is-hamiltonian-monte-carlo.html" rel="alternate" type="text/html" title="What is Hamiltonian Monte Carlo?" /><published>2022-08-09T00:00:00+00:00</published><updated>2022-08-09T00:00:00+00:00</updated><id>https://thomaswarford.github.io/2022/08/09/what-is-hamiltonian-monte-carlo</id><content type="html" xml:base="https://thomaswarford.github.io/2022/08/09/what-is-hamiltonian-monte-carlo.html"><![CDATA[<div class="editors-note">
    <strong>Editor's Note:</strong> This post was originally written for the PRACE Summer of HPC blog in August 2022, during my placement at the Hartree Centre. The original blog post can be found [here](https://web.archive.org/web/20260903190835/https://summerofhpc.prace-ri.eu/what-is-hamiltonian-monte-carlo/). The work ultimately resulted in [this conference paper](https://doi.org/10.1007/978-3-031-36030-5_48).
</div>

<table>
  <thead>
    <tr>
      <th style="text-align: center"><img src="/assets/images/hmc-intro/donut.gif" alt="HMC generating samples from a donut-shaped distribution" /></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><em>Elegant animation of HMC generating samples from a donut-shaped distribution, from <a href="https://tcbegley.com/blog/mcmc-part-2">Tom Begley’s blog post</a>. Thanks for granting me permission to use it!</em></td>
    </tr>
  </tbody>
</table>

<p>Generating random variables from an arbitrary distribution is a surprisingly difficult task, and Hamiltonian Monte Carlo (HMC) solves this challenge in an elegant way. Let’s consider a multivariate target distribution \(\pi(\vec{q})\), with parameters \(\vec{q}\).</p>

<p>Most Markov Chain Monte Carlo (MCMC) methods work by generating the proposals for the next sample (\(\vec{q}_{n+1}\)) from the current sample (\(\vec{q}_n\)). For instance, an MCMC method could take a random Gaussian step in parameter space to get the next proposal. The proposal is then accepted or rejected with probability determined by the probability density at the current and the proposed positions. The rejection of samples at a lower density causes the walk to “stay on track”, and for computational resources to be focused on the typical set, where the density of the target distribution is significant.</p>

<p>This simple MCMC method struggles with high-dimensional distributions, as the volume surrounding the typical set is much larger than the volume of the typical set, meaning most propositions are in areas of low density and are therefore rejected. This isn’t very efficient. We can reduce the step-size to increase the acceptance rate, but this causes samples to be highly correlated.</p>

<p>This is where HMC steps in. Each point in the parameter space \(\vec{q}\) is assigned a potential energy \(u(\vec{q})=-\ln{\pi(\vec{q})}\). To concretize this, imagine a 2D Gaussian distribution \(\pi(q_1, q_2) = \exp(-q_1^2-q_2^2)\). This has a bowl shaped parabolic potential energy function.</p>

<p>To go from one sample to the next, we give the “particle” a random momentum \(\vec{p}\) and simulate the evolution of the system for a certain number of time-steps. To concretize this, imagine a hockey puck being given a random momentum in a large, parabolic bowl. In an ideal world, this method of generating proposals negates the need for an acceptance/rejection step, for reasons outlined in the references below. However, in reality, a small number of proposals are rejected due to numerical integration errors. Nonetheless, with a suitable time-step most proposals are accepted and samples have a lower correlation than those produced by simple MCMC.</p>

<p>Up until this point, Bruno and I have been working to implement and parallelize this algorithm with the help of our mentor Anton. In <a href="/2022/09/15/multi-gpu-hamiltonian-monte-carlo.html">the next post</a> I talk about how we’ve been using jax, a python module which massively speeds up numpy on CPUs and GPUs.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center"><img src="/assets/images/hmc-intro/chains.png" alt="Two HMC chains exploring a potential" /></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><em>Two HMC chains exploring a potential. Each chain runs on a different CPU core.</em></td>
    </tr>
  </tbody>
</table>

<h3 id="references">References</h3>

<ul>
  <li><a href="https://tcbegley.com/blog/mcmc-part-2">Tom Begley’s blog post</a> — thanks for granting me permission to use the animation above!</li>
  <li>Betancourt, M. (2017). <a href="https://arxiv.org/abs/1701.02434">A Conceptual Introduction to Hamiltonian Monte Carlo</a>. arXiv preprint arXiv:1701.02434.</li>
</ul>]]></content><author><name>Thomas Warford</name></author><summary type="html"><![CDATA[An intuitive introduction to Hamiltonian Monte Carlo: why simple MCMC struggles in high dimensions, and how treating parameters as particles rolling in a potential fixes it.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://thomaswarford.github.io/assets/images/hmc-intro/chains.png" /><media:content medium="image" url="https://thomaswarford.github.io/assets/images/hmc-intro/chains.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>