<?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="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2025-11-08T21:48:22+00:00</updated><id>/feed.xml</id><title type="html">Random blog posts about machine learning in Julia</title><subtitle>Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.</subtitle><entry><title type="html">A Beginner’s Guide to DataLoaders in Julia with MLUtils.jl</title><link href="/julia/deep-learning/dataloaders/2025/11/08/dataloaders.html" rel="alternate" type="text/html" title="A Beginner’s Guide to DataLoaders in Julia with MLUtils.jl" /><published>2025-11-08T04:00:00+00:00</published><updated>2025-11-08T04:00:00+00:00</updated><id>/julia/deep-learning/dataloaders/2025/11/08/dataloaders</id><content type="html" xml:base="/julia/deep-learning/dataloaders/2025/11/08/dataloaders.html"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>Data Loaders are critical tools to work efficiently with deep learning.
They are the glue that bring the data to your machine learning model. Doing this efficiently
and without errors is important for successful training of any model.</p>

<p>Datasets and DataLoaders are concepts that encapsulate the logic for preparing the data to be
input in your model. In their basic form, they prepare small batches of data for ingestion by
reshaping individual samples into matrix form. More involved applications may load 
images from files, augment them through flipws, crops, rotations.</p>

<p>The good news is that in Julia, <a href="https://juliaml.github.io/MLUtils.jl">MLUtils</a> has you covered for 
all your data loading needs. In this blog post we’ll dive into how to create both Datasets and DataLoaders.
We start out with a simple-as-possible example and then walk through a more realistic example.</p>

<h1 id="data-loaders-in-easy-mode">Data loaders in easy mode</h1>
<p>Let’s start by writing a simple as possible data loader to illustrate the core concepts of data loading
using the DataLoader. The task we are looking at is somewhat related to image classification, but simplified.</p>

<p>Our data may be a bunch of images, like 1,000 of them. Each of the images is assumed to be in one of
ten classes. A mock up of that data is something like</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">X_train</span> <span class="o">=</span> <span class="n">randn</span><span class="x">(</span><span class="kt">Float32</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">1_000</span><span class="x">)</span>
<span class="n">Y_train</span> <span class="o">=</span> <span class="n">randn</span><span class="x">(</span><span class="mi">1</span><span class="o">:</span><span class="mi">10</span><span class="x">,</span> <span class="mi">1_000</span><span class="x">)</span>
</code></pre></div></div>

<p>Here, <code class="language-plaintext highlighter-rouge">X_train</code> are 1_000 samples of 10x10 matrices. These are mocked up as normally distributed Float32
data with shape (10, 10, 1_000), corresponding to (width, height, sample). Julia stores arrays in column-major,
so in our case the data for width is consecutive in memory. The last dimension, the sample dimension varies slowest.
The targets are mocked as 10_000 samples of random integers between 1 and 10, indicated by the <code class="language-plaintext highlighter-rouge">1:10</code> syntax.</p>

<p>A nice thing about Julia is that you often get away without defining custom classes. In our simlpe case,
we can get away with defining our dataset as</p>
<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">data</span> <span class="o">=</span> <span class="x">(</span><span class="n">X_train</span><span class="x">,</span> <span class="n">Y_train</span><span class="x">)</span>
</code></pre></div></div>

<p>No custom class definition needed. The dataset is just a tuple of two vectors.</p>

<p>Now we move on to the data loader. The job of the data loader is to sample from our dataset. It should
do batching, i.e. loading multiple (x,y) samples in one go, and random shuffling.
For this simple dataset, we just need to create a DataLoader:</p>
<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">loader</span> <span class="o">=</span> <span class="n">DataLoader</span><span class="x">(</span><span class="n">data</span><span class="x">,</span> <span class="n">batch_size</span> <span class="o">=</span> <span class="mi">3</span><span class="x">)</span>
</code></pre></div></div>

<p>DataLoaders are iterable, and we can look at the first item like so:</p>
<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">julia</span><span class="o">&gt;</span> <span class="x">(</span><span class="n">x_first</span><span class="x">,</span> <span class="n">y_first</span><span class="x">)</span> <span class="o">=</span> <span class="n">first</span><span class="x">(</span><span class="n">loader</span><span class="x">);</span>
<span class="n">julia</span><span class="o">&gt;</span> <span class="n">size</span><span class="x">(</span><span class="n">x_first</span><span class="x">)</span>
<span class="x">(</span><span class="mi">10</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">3</span><span class="x">)</span>
<span class="n">julia</span><span class="o">&gt;</span> <span class="n">size</span><span class="x">(</span><span class="n">y_first</span><span class="x">)</span>
<span class="x">(</span><span class="mi">3</span><span class="x">,)</span>
</code></pre></div></div>

<p>When we iterate over the dataloader, we can use individual samples. Here is the iteration skeleton:</p>
<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">julia</span><span class="o">&gt;</span> <span class="k">for</span> <span class="x">(</span><span class="n">x</span><span class="x">,</span> <span class="n">y</span><span class="x">)</span> <span class="k">in</span> <span class="n">loader</span>
            <span class="nd">@show</span> <span class="n">size</span><span class="x">(</span><span class="n">x</span><span class="x">),</span> <span class="n">size</span><span class="x">(</span><span class="n">y</span><span class="x">)</span>
       <span class="k">end</span>
<span class="x">[</span><span class="o">...</span><span class="x">]</span>
<span class="x">(</span><span class="n">size</span><span class="x">(</span><span class="n">x</span><span class="x">),</span> <span class="n">size</span><span class="x">(</span><span class="n">y</span><span class="x">))</span> <span class="o">=</span> <span class="x">((</span><span class="mi">10</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">3</span><span class="x">),</span> <span class="x">(</span><span class="mi">3</span><span class="x">,))</span>
<span class="x">(</span><span class="n">size</span><span class="x">(</span><span class="n">x</span><span class="x">),</span> <span class="n">size</span><span class="x">(</span><span class="n">y</span><span class="x">))</span> <span class="o">=</span> <span class="x">((</span><span class="mi">10</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">1</span><span class="x">),</span> <span class="x">(</span><span class="mi">1</span><span class="x">,))</span>
</code></pre></div></div>

<p>Note that in the last iteration, the DataLoader only returns vectors with a single sample.
This is the loader watching out for us and stopping at the end of the data. We have 1_000 samples,
and using <code class="language-plaintext highlighter-rouge">batchsize=3</code>, the last iteration can only feature a single sample. This is because
1000 / 3 = 333.33333 and the dataset is exhausted after the first sample in the last iteration.</p>

<p>We can confirm this by querying the length of the dataloader:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>julia&gt; length(loader)
334
</code></pre></div></div>

<p>That’s it. In conclusion, for a simple in-memory dataset creating a tuple <code class="language-plaintext highlighter-rouge">data = (X, Y)</code> is all you need.
The DataLoader does the rest.</p>

<h1 id="a-more-realistic-example">A more realistic example</h1>
<p>Now let’s look at a more involved case where we want to read text data from a file and tokenize it.
This sample is inspired by Andrej Karpathy’s <a href="https://www.youtube.com/watch?v=kCc8FmEb1nY">NanoGPT video</a>,
<a href="https://github.com/karpathy/ng-video-lecture">github</a> <a href="https://colab.research.google.com/drive/1JMLa53HDuA-i7ZBmqV7ZnA3c_fvtXnx-?usp=sharing">collab</a>.</p>

<p>For this application, it’s convenient to encapsulate information on the DataSet into a struct. This struct
collects the length of the text, a block size for data loading, dictionaries that map characters to tokens
and vice-versa, and a vector of all the tokens. The members important for the data loading logic are 
<code class="language-plaintext highlighter-rouge">block_size</code> and <code class="language-plaintext highlighter-rouge">data</code>. While <code class="language-plaintext highlighter-rouge">data</code> holds the data vector itself, <code class="language-plaintext highlighter-rouge">block_size</code> defines the length of
an individual sample.</p>

<p>In addition, we give the struct a constructors that reads all lines in the text file, creates an array of
unique characters, and dictionaries to map from character to token and a dictionary to map from token to character.
While the dictionaries are not necessary for the data loading logic to work, they are included here for completeness.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>struct NanoDataset
    block_size::Int64               # How long an observation is. This is important for the dataloader
    ch_to_int::Dict{Char, Int64}    # Maps chars to tokens
    int_to_ch::Dict{Int64, Char}    # Maps tokens to chars
    data::Vector{Int64}             # The tokens

    function NanoDataset(filename::String, block_size::Int64)
        lines = readlines(filename)             # Read all lines
        _out = [c for l ∈ lines for c ∈ l]      # Create char array
        chars = sort!(unique(_out))             # Sort all chars occuring in the dataset
        push!(chars, '\n')                      # Add the \n character that we stripped when only looking at lines
        ch_to_int = Dict(val =&gt; ix for (ix, val) in enumerate(chars))   # Mapping of chars to int
        int_to_ch = Dict(ix =&gt; val for (ix, val) in enumerate(chars))


        all_tokens = [ch_to_int[s] for s in join(lines, "\n")]

        new(block_size, ch_to_int, int_to_ch, all_tokens)
    end
end
</code></pre></div></div>

<p>To create this dataset we run</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ds</span> <span class="o">=</span> <span class="n">NanoDataset</span><span class="x">(</span><span class="n">FILENAME</span><span class="x">,</span> <span class="mi">16</span><span class="x">)</span>
</code></pre></div></div>

<p>Great, now moving on to the DataLoader. To make the dataloader work, we have to implement the <code class="language-plaintext highlighter-rouge">numobs</code> and
<code class="language-plaintext highlighter-rouge">getobs</code> interface as described in the 
<a href="https://juliaml.github.io/MLUtils.jl/stable/api/#MLUtils.DataLoader">Documentation</a>. This basically means
that we have to make DataLoader know how to get the length of the dataset and how to get a single observation.</p>

<p>Fortunately, the MLUtils documentation tells us how to implement both. <a href="https://juliaml.github.io/MLUtils.jl/stable/api/#MLCore.numobs">numobs</a> just requires to specialize <code class="language-plaintext highlighter-rouge">Base.length</code> for our type. This function returns how
many observations (that is the number of samples) there are in the dataset.</p>

<p>We implement this like</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Base.length(d::NanoDataset) = length(d.data) - d.block_size - 1
</code></pre></div></div>

<p>which makes <code class="language-plaintext highlighter-rouge">numobs</code> work for our dataset:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">julia</span><span class="o">&gt;</span> <span class="n">numobs</span><span class="x">(</span><span class="n">ds</span><span class="x">)</span>
<span class="mi">1115375</span>
</code></pre></div></div>
<p>Next, we’ll get <code class="language-plaintext highlighter-rouge">getobs</code> working.</p>

<p>For the easy case, an individual sample of <code class="language-plaintext highlighter-rouge">X_train</code> can be accessed by <code class="language-plaintext highlighter-rouge">X_train[:,:,42]</code>. That is, array 
indexing is the access. For <code class="language-plaintext highlighter-rouge">NanoDataset</code> we need to define how to get a single observation. We do this
by specializing <code class="language-plaintext highlighter-rouge">Base.getindex</code> on our dataset:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>function Base.getindex(d::NanoDataset, i::Int)
    1 &lt;= i &lt;= length(d) - d.block_size - 1|| throw(ArgumentError("Index is out of bounds"))
    return (d.data[i:i+d.block_size-1], d.data[i+1:i+d.block_size])
</code></pre></div></div>

<p>So when we access ds through <code class="language-plaintext highlighter-rouge">[]</code> braces, we get two vectors: The input <code class="language-plaintext highlighter-rouge">X</code> and the target <code class="language-plaintext highlighter-rouge">Y</code>. <code class="language-plaintext highlighter-rouge">X</code> is 
a sequence of tokens of length <code class="language-plaintext highlighter-rouge">block_size</code> and <code class="language-plaintext highlighter-rouge">Y</code> is this same sequence, shifted by a single index.</p>

<p>I’d like to note here, that <code class="language-plaintext highlighter-rouge">Base.getindex</code> is a good place to get fancy. For image loading, this
function may load data from a file. Or apply augmentations through 
<a href="https://github.com/Evizero/Augmentor.jl">Augmentor.jl</a>. The sky (or ChatGPT) is the limit!</p>

<p>Now we can access samples in our dataset like</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>julia&gt; ds[1]
([18, 47, 56, 57, 58, 1, 15, 47, 58, 47, 64, 43, 52, 10, 65, 14], [47, 56, 57, 58, 1, 15, 47, 58, 47, 64, 43, 52, 10, 65, 14, 43])
</code></pre></div></div>
<p>Note that Y is just X shifted by one index, just as implemented in <code class="language-plaintext highlighter-rouge">getindex</code>.
Before we define a dataloader, let’s implement one more function that allows us to load minibatchs:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MLUtils.getobs(d::NanoDataset, i::AbstractArray{&lt;:Integer}) = [getobs(d, ii) for ii in i]
</code></pre></div></div>

<p>This function defines to dispatch calls where requested samples are given by <code class="language-plaintext highlighter-rouge">i::AbstractArray{&lt;:Integer}</code>
to multiple calls of <code class="language-plaintext highlighter-rouge">getobs</code>. For example, loading the first two samples returns this:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">julia</span><span class="o">&gt;</span> <span class="n">getobs</span><span class="x">(</span><span class="n">d</span><span class="x">,</span> <span class="x">[</span><span class="mi">1</span><span class="x">,</span><span class="mi">2</span><span class="x">])</span>
<span class="mi">2</span><span class="o">-</span><span class="n">element</span> <span class="kt">Vector</span><span class="x">{</span><span class="kt">Tuple</span><span class="x">{</span><span class="kt">Vector</span><span class="x">{</span><span class="kt">Int64</span><span class="x">},</span> <span class="kt">Vector</span><span class="x">{</span><span class="kt">Int64</span><span class="x">}}}</span><span class="o">:</span>
 <span class="x">([</span><span class="mi">18</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">56</span><span class="x">,</span> <span class="mi">57</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">1</span><span class="x">,</span> <span class="mi">15</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">64</span><span class="x">,</span> <span class="mi">43</span><span class="x">,</span> <span class="mi">52</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">65</span><span class="x">,</span> <span class="mi">14</span><span class="x">],</span> <span class="x">[</span><span class="mi">47</span><span class="x">,</span> <span class="mi">56</span><span class="x">,</span> <span class="mi">57</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">1</span><span class="x">,</span> <span class="mi">15</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">64</span><span class="x">,</span> <span class="mi">43</span><span class="x">,</span> <span class="mi">52</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">65</span><span class="x">,</span> <span class="mi">14</span><span class="x">,</span> <span class="mi">43</span><span class="x">])</span>
 <span class="x">([</span><span class="mi">47</span><span class="x">,</span> <span class="mi">56</span><span class="x">,</span> <span class="mi">57</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">1</span><span class="x">,</span> <span class="mi">15</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">64</span><span class="x">,</span> <span class="mi">43</span><span class="x">,</span> <span class="mi">52</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">65</span><span class="x">,</span> <span class="mi">14</span><span class="x">,</span> <span class="mi">43</span><span class="x">],</span> <span class="x">[</span><span class="mi">56</span><span class="x">,</span> <span class="mi">57</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">1</span><span class="x">,</span> <span class="mi">15</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">64</span><span class="x">,</span> <span class="mi">43</span><span class="x">,</span> <span class="mi">52</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">65</span><span class="x">,</span> <span class="mi">14</span><span class="x">,</span> <span class="mi">43</span><span class="x">,</span> <span class="mi">44</span><span class="x">])</span>

</code></pre></div></div>

<p>Fantastic, now we have everything in place to create a DataLoader. We can create a dataloader that
loads 2 batches at the same time:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">julia</span><span class="o">&gt;</span> <span class="n">dl</span> <span class="o">=</span> <span class="n">DataLoader</span><span class="x">(</span><span class="n">d</span><span class="x">,</span> <span class="n">batchsize</span><span class="o">=</span><span class="mi">2</span><span class="x">)</span>
<span class="mi">557688</span><span class="o">-</span><span class="n">element</span> <span class="n">DataLoader</span><span class="x">(</span><span class="o">::</span><span class="n">NanoDataset</span><span class="x">,</span> <span class="n">batchsize</span><span class="o">=</span><span class="mi">2</span><span class="x">)</span>
  <span class="n">with</span> <span class="n">first</span> <span class="n">element</span><span class="o">:</span>
  <span class="mi">2</span><span class="o">-</span><span class="n">element</span> <span class="kt">Vector</span><span class="x">{</span><span class="kt">Tuple</span><span class="x">{</span><span class="kt">Vector</span><span class="x">{</span><span class="kt">Int64</span><span class="x">},</span> <span class="kt">Vector</span><span class="x">{</span><span class="kt">Int64</span><span class="x">}}}</span>

<span class="n">julia</span><span class="o">&gt;</span> <span class="x">(</span><span class="n">x</span><span class="x">,</span><span class="n">y</span><span class="x">)</span> <span class="o">=</span> <span class="n">first</span><span class="x">(</span><span class="n">dl</span><span class="x">)</span>
<span class="mi">2</span><span class="o">-</span><span class="n">element</span> <span class="kt">Vector</span><span class="x">{</span><span class="kt">Tuple</span><span class="x">{</span><span class="kt">Vector</span><span class="x">{</span><span class="kt">Int64</span><span class="x">},</span> <span class="kt">Vector</span><span class="x">{</span><span class="kt">Int64</span><span class="x">}}}</span><span class="o">:</span>
 <span class="x">([</span><span class="mi">18</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">56</span><span class="x">,</span> <span class="mi">57</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">1</span><span class="x">,</span> <span class="mi">15</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">64</span><span class="x">,</span> <span class="mi">43</span><span class="x">,</span> <span class="mi">52</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">65</span><span class="x">,</span> <span class="mi">14</span><span class="x">],</span> <span class="x">[</span><span class="mi">47</span><span class="x">,</span> <span class="mi">56</span><span class="x">,</span> <span class="mi">57</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">1</span><span class="x">,</span> <span class="mi">15</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">64</span><span class="x">,</span> <span class="mi">43</span><span class="x">,</span> <span class="mi">52</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">65</span><span class="x">,</span> <span class="mi">14</span><span class="x">,</span> <span class="mi">43</span><span class="x">])</span>
 <span class="x">([</span><span class="mi">47</span><span class="x">,</span> <span class="mi">56</span><span class="x">,</span> <span class="mi">57</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">1</span><span class="x">,</span> <span class="mi">15</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">64</span><span class="x">,</span> <span class="mi">43</span><span class="x">,</span> <span class="mi">52</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">65</span><span class="x">,</span> <span class="mi">14</span><span class="x">,</span> <span class="mi">43</span><span class="x">],</span> <span class="x">[</span><span class="mi">56</span><span class="x">,</span> <span class="mi">57</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">1</span><span class="x">,</span> <span class="mi">15</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">58</span><span class="x">,</span> <span class="mi">47</span><span class="x">,</span> <span class="mi">64</span><span class="x">,</span> <span class="mi">43</span><span class="x">,</span> <span class="mi">52</span><span class="x">,</span> <span class="mi">10</span><span class="x">,</span> <span class="mi">65</span><span class="x">,</span> <span class="mi">14</span><span class="x">,</span> <span class="mi">43</span><span class="x">,</span> <span class="mi">44</span><span class="x">])</span>

</code></pre></div></div>

<p>The data loader returns a vector of lenth 2, each element of the vector a tuple of vectors.
Ideally for deep learning, we’d like a matrix of size (16,2) though. Remember the first dimension is the sequence
length, dimensions 2 is the batch dimension. The DataLoader can do this by setting <code class="language-plaintext highlighter-rouge">collate=true</code>:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">julia</span><span class="o">&gt;</span> <span class="n">dl</span> <span class="o">=</span> <span class="n">DataLoader</span><span class="x">(</span><span class="n">d</span><span class="x">,</span> <span class="n">batchsize</span><span class="o">=</span><span class="mi">2</span><span class="x">,</span> <span class="n">collate</span><span class="o">=</span><span class="nb">true</span><span class="x">)</span>
<span class="mi">557688</span><span class="o">-</span><span class="n">element</span> <span class="n">DataLoader</span><span class="x">(</span><span class="o">::</span><span class="n">NanoDataset</span><span class="x">,</span> <span class="n">batchsize</span><span class="o">=</span><span class="mi">2</span><span class="x">,</span> <span class="n">collate</span><span class="o">=</span><span class="kt">Val</span><span class="x">{</span><span class="nb">true</span><span class="x">}())</span>
  <span class="n">with</span> <span class="n">first</span> <span class="n">element</span><span class="o">:</span>
  <span class="x">(</span><span class="mi">16</span><span class="n">×2</span> <span class="kt">Matrix</span><span class="x">{</span><span class="kt">Int64</span><span class="x">},</span> <span class="mi">16</span><span class="n">×2</span> <span class="kt">Matrix</span><span class="x">{</span><span class="kt">Int64</span><span class="x">},)</span>

<span class="n">julia</span><span class="o">&gt;</span> <span class="x">(</span><span class="n">x</span><span class="x">,</span><span class="n">y</span><span class="x">)</span> <span class="o">=</span> <span class="n">first</span><span class="x">(</span><span class="n">dl</span><span class="x">)</span>
<span class="x">([</span><span class="mi">18</span> <span class="mi">47</span><span class="x">;</span> <span class="mi">47</span> <span class="mi">56</span><span class="x">;</span> <span class="n">…</span> <span class="x">;</span> <span class="mi">65</span> <span class="mi">14</span><span class="x">;</span> <span class="mi">14</span> <span class="mi">43</span><span class="x">],</span> <span class="x">[</span><span class="mi">47</span> <span class="mi">56</span><span class="x">;</span> <span class="mi">56</span> <span class="mi">57</span><span class="x">;</span> <span class="n">…</span> <span class="x">;</span> <span class="mi">14</span> <span class="mi">43</span><span class="x">;</span> <span class="mi">43</span> <span class="mi">44</span><span class="x">])</span>

<span class="n">julia</span><span class="o">&gt;</span> <span class="n">x</span>
<span class="mi">16</span><span class="n">×2</span> <span class="kt">Matrix</span><span class="x">{</span><span class="kt">Int64</span><span class="x">}</span><span class="o">:</span>
 <span class="mi">18</span>  <span class="mi">47</span>
 <span class="mi">47</span>  <span class="mi">56</span>
 <span class="mi">56</span>  <span class="mi">57</span>
 <span class="mi">57</span>  <span class="mi">58</span>
 <span class="mi">58</span>   <span class="mi">1</span>
  <span class="mi">1</span>  <span class="mi">15</span>
 <span class="mi">15</span>  <span class="mi">47</span>
 <span class="mi">47</span>  <span class="mi">58</span>
 <span class="mi">58</span>  <span class="mi">47</span>
 <span class="mi">47</span>  <span class="mi">64</span>
 <span class="mi">64</span>  <span class="mi">43</span>
 <span class="mi">43</span>  <span class="mi">52</span>
 <span class="mi">52</span>  <span class="mi">10</span>
 <span class="mi">10</span>  <span class="mi">65</span>
 <span class="mi">65</span>  <span class="mi">14</span>
 <span class="mi">14</span>  <span class="mi">43</span>
</code></pre></div></div>

<p>Now the DataLoader returns the desired matrix. In addition, DataLoader supports shuffling and multithreading,
just add <code class="language-plaintext highlighter-rouge">shuffle=true, parallel=true</code> to the parameter list.</p>

<h1 id="where-to-go-from-here">Where to go from here</h1>

<p>In this tutorial we looked at how to use DataLoaders in Julia through two examples. In the first example,
we had in-memory data that can be passed as a tuple into the DataLoader. That just worked.
In the second example, we had to implement the <code class="language-plaintext highlighter-rouge">numobs</code>, <code class="language-plaintext highlighter-rouge">getobs</code> interface to make it work. This is a
just a bit more work, but allows to work with arbitrary custom data. The <code class="language-plaintext highlighter-rouge">getobs</code> implementation is where
to hook in lazy-loading from disk, augmentations, and so on.</p>

<p>Now that we looked at some examples on data loading, here are some things to try next</p>
<ul>
  <li>Try building your own Dataloader and use it in a <a href="https://lux.csail.mit.edu/">Lux</a> or <a href="https://fluxml.ai/">Flux</a> training loop.</li>
  <li>Play around with image augmentation using <a href="https://evizero.github.io/Augmentor.jl/stable/">Augmentor</a></li>
  <li>Try using threads and measure the speed-up compared to single-threaded loading.</li>
</ul>]]></content><author><name></name></author><category term="julia" /><category term="deep-learning" /><category term="dataloaders" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Backpropagation through Poisson solvers</title><link href="/julia/zygote/2022/02/19/backprop-poisson.html" rel="alternate" type="text/html" title="Backpropagation through Poisson solvers" /><published>2022-02-19T04:00:00+00:00</published><updated>2022-02-19T04:00:00+00:00</updated><id>/julia/zygote/2022/02/19/backprop-poisson</id><content type="html" xml:base="/julia/zygote/2022/02/19/backprop-poisson.html"><![CDATA[<p>Continuing from the <a href="https://rkube.github.io/julia/zygote/2022/02/16/backprop-sim.html">previous example</a> this tutorial shows how to use automatic 
differentiaton with physics-inspired calculations. Let’s say we have
a profile that can be parameterized by a free parameter. From this profile,
we calculate another expression. And then we want to find the maximum
of this expression as a function of the free parameter.</p>

<p>To be more precise, let’s say we have a quantity \(\rho\) with a profile that has a profile
with a peak whose amplitude is parameterized by the parameter \(t\):</p>

\[\begin{align}
    \rho_t(x) = -\sin^2(t) \exp \left(  \frac{\left(x-x_0\right)}{2\sigma^2} \right)
        \times \frac{\sigma^2 - (x - x_0)^2}{\sigma^4}
\end{align}\]

<p>The plot below shows how the peak of the amplitude varies with \(t\):
<img src="/assets/images/autodiff_examples/rho_with_t.png" alt="Profile variation with t" /></p>

<p>We see that for \(t = 0.2\ldots2.0\) the peak decreases in amplitude and increases
as \(t\) increases from 2.0 to 4.0.</p>

<p>To complicate things a bit, we would like to know how a quantity that is derived from this
profile depends on \(t\). In particular we want to</p>

<ol>
  <li>Calculate \(\rho_t = \frac{\partial^2 \phi_t}{\partial x^2}\)</li>
  <li>Then maximize \(\int\limits_{0}^{L_x} \phi_t(x)^2 \, \mathrm{d}x\)</li>
</ol>

<p>The ρ profile used here is after all a manufactured solution to the Poisson equation for</p>

\[\begin{align}
    \phi_t(x) = \sin^2(t) \exp \left(  \frac{\left(x-x_0\right)}{2\sigma^2} \right).
\end{align}\]

<p>Then we only have to ask <a href="https://www.wolframalpha.com/input?i=Integrate%5B+%28Sin%5Bt%5D%5E2+Exp%5B%28x-x0%29%5E2+%2F+2+%2F+s%5D%29%5E2%2C+%7Bx%2C0%2CL%7D%5D">Wolfram Alpha</a> to
give us</p>

\[\begin{align}
    I(t) = \int \limits_{0}^{L_x} \phi_t(x)^2 = \frac{\sqrt{2 \sigma} \sin(t)^4}{s} 
    \left[ \mathrm{erfi}\left( \frac{L-x_0}{\sqrt{\sigma}} \right) + 
      \mathrm{erfi}\left( \frac{x_0}{\sqrt{\sigma}}\right)\right].
\end{align}\]

<p>But in this tutorial the focus is on using automatic differentiation for such tasks. 
In particular, we can use the approach presented here when we lack an analytic
solution for ϕ. We can also use the approach presented here when we lack an 
analytic expression for ρ, as long as we can generate ρ given a value for t.</p>

<p>So let’s draft a game-plan. First we choose a discretization of the domain [0:Lx] using Nx
grid points, equidistantially spaced with \(\triangle x\). Then we pick a value of
t and</p>
<ol>
  <li>Calculate \(\rho_t(x)\).</li>
  <li>Invert the poisson equation to find \(\phi_t(x)\).</li>
  <li>Calculate the integral \(I(t) = \int \phi_t(x)^2 \mathrm{d}x\).</li>
</ol>

<p>Starting it off, we import relevant libraries, in particular Zygote, and define parameters
for the grid and the profile:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="n">Zygote</span>
<span class="k">using</span> <span class="n">FFTW</span><span class="o">:</span> <span class="n">fft</span><span class="x">,</span> <span class="n">ifft</span>
<span class="k">using</span> <span class="n">LinearAlgebra</span>
<span class="k">using</span> <span class="n">BenchmarkTools</span>

<span class="n">Lx</span> <span class="o">=</span> <span class="mi">2</span><span class="nb">π</span>
<span class="n">Nx</span> <span class="o">=</span> <span class="mi">128</span>
<span class="n">Δx</span> <span class="o">=</span> <span class="n">Lx</span> <span class="o">/</span> <span class="n">Nx</span>
<span class="n">n0</span> <span class="o">=</span> <span class="mf">0.0</span>
<span class="n">μ</span> <span class="o">=</span> <span class="nb">π</span>
<span class="n">σ</span> <span class="o">=</span> <span class="mf">0.25</span>
<span class="n">t</span> <span class="o">=</span> <span class="mf">1.0</span>
</code></pre></div></div>

<p>Since we later ask for the \(\partial_t I(t)\) we write a function that calculates \(I(t)\):</p>
<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span><span class="nf"> int_prof</span><span class="x">(</span><span class="n">t</span><span class="x">)</span>
    <span class="c"># This calculates I(t)</span>
    <span class="n">xrg</span> <span class="o">=</span> <span class="n">Δx</span> <span class="o">*</span> <span class="x">(</span><span class="mi">0</span><span class="o">:</span><span class="mi">1</span><span class="o">:</span><span class="x">(</span><span class="n">Nx</span><span class="o">-</span><span class="mi">1</span><span class="x">))</span>
    <span class="n">ρ</span> <span class="o">=</span> <span class="n">ρ_prof</span><span class="x">(</span><span class="n">xrg</span><span class="x">,</span> <span class="mf">1.0</span><span class="x">,</span> <span class="n">t</span><span class="x">,</span> <span class="n">μ</span><span class="x">,</span> <span class="n">σ</span><span class="x">)</span>
    <span class="n">ϕ</span> <span class="o">=</span> <span class="n">invert_poisson</span><span class="x">(</span><span class="n">ρ</span><span class="x">,</span> <span class="n">Nx</span><span class="x">,</span> <span class="n">Lx</span><span class="x">)</span>
    <span class="n">ϕ</span> <span class="o">=</span> <span class="n">ϕ</span> <span class="o">.-</span> <span class="n">mean</span><span class="x">(</span><span class="n">ϕ</span><span class="x">)</span>
    <span class="n">sum</span><span class="x">(</span><span class="n">ϕ</span><span class="o">.^</span><span class="mi">2</span><span class="x">)</span>
<span class="k">end</span>
</code></pre></div></div>
<p>That’s it. Now we can call Zygote’s <code class="language-plaintext highlighter-rouge">gradient</code> function on <code class="language-plaintext highlighter-rouge">int_prof(t)</code> to take calculate
\(\partial_t I(t)\). Now there are some more details to explore. In the next section
we digress into Poisson solvers. Feel free to skip this section and head straight to
the results to see how well AD performs on this relatively simple forward model.</p>

<h1 id="poisson-solvers">Poisson Solvers</h1>
<p>Given \(\rho_t\), we want to solve the Poisson equation</p>

\[\begin{align}
    \rho_t(x) &amp; = \frac{\partial^2 \phi_t)x)}{\partial x^2} \\
    \phi_t(0) &amp; = \phi_t(L)
\end{align}\]

<p>for \(\phi_t\) on the domain \(x \in [0:L]\). We don’t even pretend that we
have any class and use periodic boundary conditions. We want the solution on
the grid points \(x_i = i \triangle x\), where \(\triangle x = L_x / N_x\)
and \(N_x\) denotes the number of grid points.</p>

<p>For this task wwe can either follow this 
<a href="http://www-m16.ma.tum.de/foswiki/pub/M16/Allgemeines/StefanPossanner/Poisson1D_FD.html#19">excellent tutorial</a> 
or use a spectral solver, described for example 
<a href="https://atmos.washington.edu/~breth/classes/AM585/lect/FS_2DPoisson.pdf">here</a>.</p>

<p>A simple spectral solver for can be implemented like this:</p>
<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Simple spectral solver for Laplace equation</span>
<span class="k">function</span><span class="nf"> invert_laplace_spectral</span><span class="x">(</span><span class="n">y</span><span class="o">::</span><span class="kt">Array</span><span class="x">{</span><span class="kt">Float64</span><span class="x">},</span> <span class="n">Nz</span><span class="x">,</span> <span class="n">Lz</span><span class="x">)</span>
    <span class="n">k</span> <span class="o">=</span> <span class="x">[</span><span class="mf">1e100</span><span class="x">;</span> <span class="mi">1</span><span class="o">:</span><span class="x">(</span><span class="n">Nz</span> <span class="o">÷</span> <span class="mi">2</span><span class="x">);</span> <span class="o">-</span><span class="x">(</span><span class="n">Nz</span> <span class="o">÷</span> <span class="mi">2</span> <span class="o">-</span> <span class="mi">1</span><span class="x">)</span><span class="o">:-</span><span class="mi">1</span><span class="x">]</span> <span class="o">.*</span> <span class="mi">2</span><span class="nb">π</span><span class="o">/</span><span class="n">Lz</span>
	<span class="n">y_ft</span> <span class="o">=</span> <span class="o">-</span><span class="n">fft</span><span class="x">(</span><span class="n">y</span><span class="x">)</span> <span class="o">./</span> <span class="n">k</span> <span class="o">./</span> <span class="n">k</span>
    <span class="n">y_ft</span> <span class="o">=</span> <span class="n">y_ft</span> <span class="o">.*</span> <span class="x">[</span><span class="mi">0</span><span class="x">;</span> <span class="n">ones</span><span class="x">(</span><span class="n">Nz</span> <span class="o">-</span> <span class="mi">1</span><span class="x">)]</span>
    <span class="n">d2y</span> <span class="o">=</span> <span class="n">real</span><span class="x">(</span><span class="n">ifft</span><span class="x">(</span><span class="n">y_ft</span><span class="x">))</span>
<span class="k">end</span>
</code></pre></div></div>
<p>The first line assembles a vector of wave numbers. The solution to Poisson’s allows an arbitrary offset. Setting the element of the
wave numbers vector, that corresponds to the zero frequency, to a large number
,  <code class="language-plaintext highlighter-rouge">k[1] = 1^100</code>, we fix the mean of the solution to zero since we later divide by <code class="language-plaintext highlighter-rouge">k</code>.<br />
We then take the Fourier transformation, divide by \(k^{2}\) and explicitly
zero out the zero-mode. The syntax is a bit cumbersome, we perform a point-wise
multiplication with an array, since Zygote does not support array mutation. 
Finally we take the real part of the inverse transformation.</p>

<p>A particlar issue with Zygote is that it can’t handle code that mutates arrays. 
The following text snippet shows a way of how to assemble an array and a way that will fail:</p>
<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="x">[</span><span class="mi">0</span><span class="x">;</span> <span class="n">ones</span><span class="x">(</span><span class="n">Nz</span> <span class="o">-</span> <span class="mi">1</span><span class="x">)]</span>   <span class="c"># This will work with Zygote</span>

<span class="n">tt</span> <span class="o">=</span> <span class="n">ones</span><span class="x">(</span><span class="n">Nz</span><span class="x">)</span>       
<span class="n">tt</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span> <span class="o">=</span> <span class="mi">0</span>           <span class="c"># This will not work with Zygote</span>
</code></pre></div></div>

<p>Alternatively, we can implement a finite difference solver for Poisson’s equation. 
Below is the implementation for periodic boundary conditions adapted from
<a href="http://www-m16.ma.tum.de/foswiki/pub/M16/Allgemeines/StefanPossanner/Poisson1D_FD.html#19">herel</a></p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span><span class="nf"> invert_laplace</span><span class="x">(</span><span class="n">y</span><span class="x">,</span>  <span class="n">Nz</span><span class="x">,</span> <span class="n">Lz</span><span class="x">)</span>
    <span class="n">Δz</span> <span class="o">=</span> <span class="n">Lz</span> <span class="o">/</span> <span class="n">Nz</span>
    <span class="n">invΔz²</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="n">Δz</span> <span class="o">/</span> <span class="n">Δz</span>

    <span class="n">A0</span> <span class="o">=</span> <span class="n">Zygote</span><span class="o">.</span><span class="n">Buffer</span><span class="x">(</span><span class="n">zeros</span><span class="x">(</span><span class="n">Nz</span><span class="x">,</span> <span class="n">Nz</span><span class="x">))</span>
    <span class="k">for</span> <span class="n">n</span> <span class="n">∈</span> <span class="mi">1</span><span class="o">:</span><span class="n">Nz</span>
        <span class="k">for</span> <span class="n">m</span> <span class="n">∈</span> <span class="mi">1</span><span class="o">:</span><span class="n">Nz</span>
            <span class="n">A0</span><span class="x">[</span><span class="n">n</span><span class="x">,</span><span class="n">m</span><span class="x">]</span> <span class="o">=</span> <span class="mf">0.0</span>
        <span class="k">end</span>
    <span class="k">end</span>
    <span class="k">for</span> <span class="n">n</span> <span class="n">∈</span> <span class="mi">2</span><span class="o">:</span><span class="n">Nz</span>
        <span class="n">A0</span><span class="x">[</span><span class="n">n</span><span class="x">,</span> <span class="n">n</span><span class="x">]</span> <span class="o">=</span> <span class="o">-</span><span class="mf">2.0</span> <span class="o">*</span> <span class="n">invΔz²</span>
        <span class="n">A0</span><span class="x">[</span><span class="n">n</span><span class="x">,</span> <span class="n">n</span><span class="o">-</span><span class="mi">1</span><span class="x">]</span> <span class="o">=</span> <span class="n">invΔz²</span>
        <span class="n">A0</span><span class="x">[</span><span class="n">n</span><span class="o">-</span><span class="mi">1</span><span class="x">,</span> <span class="n">n</span><span class="x">]</span> <span class="o">=</span> <span class="n">invΔz²</span>
    <span class="k">end</span>
    <span class="n">A0</span><span class="x">[</span><span class="mi">1</span><span class="x">,</span> <span class="mi">1</span><span class="x">]</span> <span class="o">=</span> <span class="mf">1.0</span>
    <span class="n">A0</span><span class="x">[</span><span class="mi">1</span><span class="x">,</span> <span class="mi">2</span><span class="x">]</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="n">A0</span><span class="x">[</span><span class="n">Nz</span><span class="x">,</span><span class="mi">1</span><span class="x">]</span> <span class="o">=</span> <span class="n">invΔz²</span>

    <span class="n">A</span> <span class="o">=</span> <span class="n">copy</span><span class="x">(</span><span class="n">A0</span><span class="x">)</span>
    <span class="n">yvec</span> <span class="o">=</span> <span class="n">vcat</span><span class="x">([</span><span class="mf">0.0</span><span class="x">],</span> <span class="n">y</span><span class="x">[</span><span class="mi">2</span><span class="o">:</span><span class="n">Nz</span><span class="x">])</span>
    <span class="n">ϕ_num</span> <span class="o">=</span> <span class="n">A</span> <span class="o">\</span> <span class="n">yvec</span>
<span class="k">end</span>
</code></pre></div></div>

<p>After calculating sample spacing, the code constructs a Matrix for the
finite difference Laplace operator. Here we use 
<a href="https://fluxml.ai/Zygote.jl/latest/utils/#Zygote.Buffer">Zygote.Buffer</a>
which allows us to use array mutation syntax. Again, code that is to be
differentiated with Zygote can’t mutate arrays. After assembling the matrix
we copy it into a matrix, augment the input vector and solve the linear system.</p>

<h2 id="differentiation-through-poisson-solvers">Differentiation through Poisson solvers</h2>

<p>The code below shows how I set up the problem. I choose \(\phi\) and 
\(\rho\) as a manufactured solution. This way I can compare the 
numerical solution for \(\phi_t\) to <code class="language-plaintext highlighter-rouge">ϕ_prof</code>. A Gaussian solution is
also a craving test case for a spectral solver. Since the Fourier 
transformation of a Gaussian is a Gaussian, all Fourier modes will be
non-negative. This way one can pick up errors where a mode is at a wrong
place in an array.</p>

<p>The functions <code class="language-plaintext highlighter-rouge">int_prof_fd(t)</code> and <code class="language-plaintext highlighter-rouge">int_prof_sp(t)</code> implement the three
prong workflow described above. In particular their only argument is <code class="language-plaintext highlighter-rouge">t</code>.
This way we can pass the whole workflow to <code class="language-plaintext highlighter-rouge">Zygote.gradient</code> and this way
get the derivative of I with respect to t, $$\partial_t I$.</p>

<p>Now let’s set up the code:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="n">ϕ_prof</span><span class="x">(</span><span class="n">xrg</span><span class="x">,</span> <span class="n">n0</span><span class="x">,</span> <span class="n">t</span><span class="x">,</span> <span class="n">μ</span><span class="x">,</span> <span class="n">σ</span><span class="x">)</span> <span class="o">=</span> <span class="n">n0</span> <span class="o">.+</span> <span class="n">sin</span><span class="x">(</span><span class="n">t</span><span class="x">)</span><span class="o">.^</span><span class="mi">2</span> <span class="o">.*</span> <span class="n">exp</span><span class="o">.</span><span class="x">(</span><span class="o">-</span><span class="x">(</span><span class="n">xrg</span> <span class="o">.-</span> <span class="n">μ</span><span class="x">)</span><span class="o">.^</span><span class="mi">2</span> <span class="o">./</span> <span class="mf">2.0</span> <span class="o">./</span> <span class="n">σ</span> <span class="o">./</span> <span class="n">σ</span><span class="x">)</span>
<span class="n">ρ_prof</span><span class="x">(</span><span class="n">xrg</span><span class="x">,</span> <span class="n">n0</span><span class="x">,</span> <span class="n">t</span><span class="x">,</span> <span class="n">μ</span><span class="x">,</span> <span class="n">σ</span><span class="x">)</span> <span class="o">=</span> <span class="o">-</span><span class="n">sin</span><span class="x">(</span><span class="n">t</span><span class="x">)</span><span class="o">^</span><span class="mi">2</span> <span class="o">*</span> <span class="n">exp</span><span class="o">.</span><span class="x">(</span><span class="o">-</span><span class="x">(</span><span class="n">xrg</span> <span class="o">.-</span> <span class="n">μ</span><span class="x">)</span><span class="o">.^</span><span class="mi">2</span> <span class="o">/</span> <span class="mi">2</span> <span class="o">/</span> <span class="n">σ</span> <span class="o">/</span> <span class="n">σ</span><span class="x">)</span> <span class="o">.*</span> <span class="x">(</span><span class="n">σ</span><span class="o">^</span><span class="mi">2</span> <span class="o">.-</span> <span class="x">(</span><span class="n">xrg</span> <span class="o">.-</span> <span class="n">μ</span><span class="x">)</span><span class="o">.^</span><span class="mi">2</span><span class="x">)</span> <span class="o">/</span> <span class="x">(</span><span class="n">σ</span><span class="o">^</span><span class="mi">4</span><span class="x">)</span>

<span class="n">Lx</span> <span class="o">=</span> <span class="mi">2</span><span class="nb">π</span>
<span class="n">Nx</span> <span class="o">=</span> <span class="mi">128</span>
<span class="n">Δx</span> <span class="o">=</span> <span class="n">Lx</span> <span class="o">/</span> <span class="n">Nx</span>
<span class="n">n0</span> <span class="o">=</span> <span class="mf">0.0</span>
<span class="n">μ</span> <span class="o">=</span> <span class="nb">π</span>
<span class="n">σ</span> <span class="o">=</span> <span class="mf">0.25</span>
<span class="n">t</span> <span class="o">=</span> <span class="mf">1.0</span>

<span class="k">function</span><span class="nf"> int_prof_fd</span><span class="x">(</span><span class="n">t</span><span class="x">)</span>
    <span class="n">xrg</span> <span class="o">=</span> <span class="n">Δx</span> <span class="o">*</span> <span class="x">(</span><span class="mi">0</span><span class="o">:</span><span class="mi">1</span><span class="o">:</span><span class="x">(</span><span class="n">Nx</span><span class="o">-</span><span class="mi">1</span><span class="x">))</span>
    <span class="n">ρ</span> <span class="o">=</span> <span class="n">ρ_prof</span><span class="x">(</span><span class="n">xrg</span><span class="x">,</span> <span class="mf">1.0</span><span class="x">,</span> <span class="n">t</span><span class="x">,</span> <span class="n">μ</span><span class="x">,</span> <span class="n">σ</span><span class="x">)</span>
    <span class="n">ϕ</span> <span class="o">=</span> <span class="n">invert_laplace</span><span class="x">(</span><span class="n">ρ</span><span class="x">,</span> <span class="n">Nx</span><span class="x">,</span> <span class="n">Lx</span><span class="x">)</span>
    <span class="n">ϕ</span> <span class="o">=</span> <span class="n">ϕ</span> <span class="o">.-</span> <span class="n">mean</span><span class="x">(</span><span class="n">ϕ</span><span class="x">)</span>
    <span class="n">sum</span><span class="x">(</span><span class="n">ϕ</span><span class="o">.^</span><span class="mi">2</span><span class="x">)</span>
<span class="k">end</span>

<span class="k">function</span><span class="nf"> int_prof_sp</span><span class="x">(</span><span class="n">t</span><span class="x">)</span>
    <span class="n">xrg</span> <span class="o">=</span> <span class="n">Δx</span> <span class="o">*</span> <span class="x">(</span><span class="mi">0</span><span class="o">:</span><span class="mi">1</span><span class="o">:</span><span class="x">(</span><span class="n">Nx</span><span class="o">-</span><span class="mi">1</span><span class="x">))</span>
    <span class="n">ρ</span> <span class="o">=</span> <span class="n">ρ_prof</span><span class="x">(</span><span class="n">xrg</span><span class="x">,</span> <span class="mf">1.0</span><span class="x">,</span> <span class="n">t</span><span class="x">,</span> <span class="n">μ</span><span class="x">,</span> <span class="n">σ</span><span class="x">)</span>
    <span class="n">ϕ</span> <span class="o">=</span> <span class="n">invert_laplace_spectral</span><span class="x">(</span><span class="n">ρ</span><span class="x">,</span> <span class="n">Nx</span><span class="x">,</span> <span class="n">Lx</span><span class="x">)</span>
    <span class="n">sum</span><span class="x">(</span><span class="n">ϕ</span><span class="o">.^</span><span class="mi">2</span><span class="x">)</span>
<span class="k">end</span>

<span class="n">trg</span> <span class="o">=</span> <span class="mf">0.0</span><span class="o">:</span><span class="mf">0.01</span><span class="o">:</span><span class="mi">2</span><span class="nb">π</span>
<span class="n">phi2_fd</span> <span class="o">=</span> <span class="n">zeros</span><span class="x">(</span><span class="n">length</span><span class="x">(</span><span class="n">trg</span><span class="x">))</span>
<span class="n">phi2_sp</span> <span class="o">=</span> <span class="n">zeros</span><span class="x">(</span><span class="n">length</span><span class="x">(</span><span class="n">trg</span><span class="x">))</span>

<span class="n">phi2_grad_fd</span> <span class="o">=</span> <span class="n">zeros</span><span class="x">(</span><span class="n">length</span><span class="x">(</span><span class="n">trg</span><span class="x">))</span>
<span class="n">phi2_grad_sp</span> <span class="o">=</span> <span class="n">zeros</span><span class="x">(</span><span class="n">length</span><span class="x">(</span><span class="n">trg</span><span class="x">))</span>

<span class="k">for</span> <span class="n">idx</span> <span class="n">∈</span> <span class="mi">1</span><span class="o">:</span><span class="n">length</span><span class="x">(</span><span class="n">trg</span><span class="x">)</span>
    <span class="n">t</span> <span class="o">=</span> <span class="n">trg</span><span class="x">[</span><span class="n">idx</span><span class="x">]</span>
    <span class="n">phi2_fd</span><span class="x">[</span><span class="n">idx</span><span class="x">]</span> <span class="o">=</span> <span class="n">int_prof_fd</span><span class="x">(</span><span class="n">t</span><span class="x">)</span>
    <span class="n">phi2_grad_fd</span><span class="x">[</span><span class="n">idx</span><span class="x">]</span> <span class="o">=</span> <span class="n">gradient</span><span class="x">(</span><span class="n">int_prof_fd</span><span class="x">,</span> <span class="n">t</span><span class="x">)[</span><span class="mi">1</span><span class="x">]</span>

    <span class="n">phi2_sp</span><span class="x">[</span><span class="n">idx</span><span class="x">]</span> <span class="o">=</span> <span class="n">int_prof_sp</span><span class="x">(</span><span class="n">t</span><span class="x">)</span>
    <span class="n">phi2_grad_sp</span><span class="x">[</span><span class="n">idx</span><span class="x">]</span> <span class="o">=</span> <span class="n">real</span><span class="x">(</span><span class="n">gradient</span><span class="x">(</span><span class="n">int_prof_sp</span><span class="x">,</span> <span class="n">t</span><span class="x">)[</span><span class="mi">1</span><span class="x">])</span>
<span class="k">end</span>
</code></pre></div></div>

<p>As a sanity check we first compare the output of the forward model when
using the finite difference and spectral solver.</p>

<p><img src="/assets/images/autodiff_examples/I_t_FD_SP_solvers.png" alt="Comparison of forward model using finite difference and spectral solvers" /></p>

<p>For the resolution I chose, \(\triangle x\) is approximately 0.5 in this
example, both solvers perform well.</p>

<p><img src="/assets/images/autodiff_examples/dIdt_FD_AD.png" alt="Comparison of dI/dt obtained through AD and finite differences" /></p>

<p>A final thing to look at is speed. Using 
<a href="https://github.com/JuliaCI/BenchmarkTools.jl">Benchmarktool</a>
we can evaluate the performance of backpropagating through either the finite difference and 
the spectral solver:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="n">BenchmarkTools</span>
<span class="n">julia</span><span class="o">&gt;</span> <span class="nd">@benchmark</span> <span class="n">gradient</span><span class="x">(</span><span class="n">int_prof_sp</span><span class="x">,</span> <span class="n">t</span><span class="x">)[</span><span class="mi">1</span><span class="x">]</span>
<span class="n">BenchmarkTools</span><span class="o">.</span><span class="n">Trial</span><span class="o">:</span> 
  <span class="n">memory</span> <span class="n">estimate</span><span class="o">:</span>  <span class="mf">141.64</span> <span class="n">KiB</span>
  <span class="n">allocs</span> <span class="n">estimate</span><span class="o">:</span>  <span class="mi">752</span>
  <span class="o">--------------</span>
  <span class="n">minimum</span> <span class="n">time</span><span class="o">:</span>     <span class="mf">110.582</span> <span class="n">μs</span> <span class="x">(</span><span class="mf">0.00</span><span class="o">%</span> <span class="n">GC</span><span class="x">)</span>
  <span class="n">median</span> <span class="n">time</span><span class="o">:</span>      <span class="mf">118.785</span> <span class="n">μs</span> <span class="x">(</span><span class="mf">0.00</span><span class="o">%</span> <span class="n">GC</span><span class="x">)</span>
  <span class="n">mean</span> <span class="n">time</span><span class="o">:</span>        <span class="mf">139.315</span> <span class="n">μs</span> <span class="x">(</span><span class="mf">9.20</span><span class="o">%</span> <span class="n">GC</span><span class="x">)</span>
  <span class="n">maximum</span> <span class="n">time</span><span class="o">:</span>     <span class="mf">12.583</span> <span class="n">ms</span> <span class="x">(</span><span class="mf">68.76</span><span class="o">%</span> <span class="n">GC</span><span class="x">)</span>
  <span class="o">--------------</span>
  <span class="n">samples</span><span class="o">:</span>          <span class="mi">10000</span>
  <span class="n">evals</span><span class="o">/</span><span class="n">sample</span><span class="o">:</span>     <span class="mi">1</span>

<span class="n">julia</span><span class="o">&gt;</span> <span class="nd">@benchmark</span> <span class="n">gradient</span><span class="x">(</span><span class="n">int_prof_fd</span><span class="x">,</span> <span class="n">t</span><span class="x">)[</span><span class="mi">1</span><span class="x">]</span>
<span class="n">BenchmarkTools</span><span class="o">.</span><span class="n">Trial</span><span class="o">:</span> 
  <span class="n">memory</span> <span class="n">estimate</span><span class="o">:</span>  <span class="mf">6.07</span> <span class="n">MiB</span>
  <span class="n">allocs</span> <span class="n">estimate</span><span class="o">:</span>  <span class="mi">170758</span>
  <span class="o">--------------</span>
  <span class="n">minimum</span> <span class="n">time</span><span class="o">:</span>     <span class="mf">28.093</span> <span class="n">ms</span> <span class="x">(</span><span class="mf">0.00</span><span class="o">%</span> <span class="n">GC</span><span class="x">)</span>
  <span class="n">median</span> <span class="n">time</span><span class="o">:</span>      <span class="mf">32.338</span> <span class="n">ms</span> <span class="x">(</span><span class="mf">0.00</span><span class="o">%</span> <span class="n">GC</span><span class="x">)</span>
  <span class="n">mean</span> <span class="n">time</span><span class="o">:</span>        <span class="mf">35.562</span> <span class="n">ms</span> <span class="x">(</span><span class="mf">3.07</span><span class="o">%</span> <span class="n">GC</span><span class="x">)</span>
  <span class="n">maximum</span> <span class="n">time</span><span class="o">:</span>     <span class="mf">104.050</span> <span class="n">ms</span> <span class="x">(</span><span class="mf">0.00</span><span class="o">%</span> <span class="n">GC</span><span class="x">)</span>
  <span class="o">--------------</span>
  <span class="n">samples</span><span class="o">:</span>          <span class="mi">141</span>
  <span class="n">evals</span><span class="o">/</span><span class="n">sample</span><span class="o">:</span>     <span class="mi">1</span>
</code></pre></div></div>

<p>The mean time it takes to backprop through the finite difference solver is about 250 times
larger than for the spectral solver. Given that the spectral solver is cheaper to evaluate,
it doesn’t require to solve a linear system, this is not completely unexpected.</p>]]></content><author><name></name></author><category term="julia" /><category term="zygote" /><summary type="html"><![CDATA[Continuing from the previous example this tutorial shows how to use automatic differentiaton with physics-inspired calculations. Let’s say we have a profile that can be parameterized by a free parameter. From this profile, we calculate another expression. And then we want to find the maximum of this expression as a function of the free parameter.]]></summary></entry><entry><title type="html">Backpropagation through numerical calculations</title><link href="/julia/zygote/2022/02/16/backprop-sim.html" rel="alternate" type="text/html" title="Backpropagation through numerical calculations" /><published>2022-02-16T11:00:00+00:00</published><updated>2022-02-16T11:00:00+00:00</updated><id>/julia/zygote/2022/02/16/backprop-sim</id><content type="html" xml:base="/julia/zygote/2022/02/16/backprop-sim.html"><![CDATA[<p>Differentiable programming makes all code amenable to gradient based
optimization. This harmless statement has broad implications. Think for 
example about a physical simulation where a differential equation is
integrated in time. Given some initial conditions, boundary conditions,
and parameters, the simulator approximates how the system evolves in time.
Now lets say you simulate a wave in a swimming pool that moves towards
a wall. Your goal is to find out how fast the wave has to be launched as
to swap over the edge it is travelling to. With differentiable programming,
you could define a target metric and optimize it with respect to the
initial condition, here the speed with which the wave is launched.</p>

<p>To get started with differentiable programming we are looking at a very
simple example. We are given a density profile n on a finite domain:</p>

\[\begin{align}
n(z) = n_0 + \sin(t) \exp\left( \frac{z-z_0}{2\sigma}\right).
\end{align}\]

<p>The background density is \(n_0\) on which a Gaussian density peak,
centered around \(z_0\) modulated. The amplitude of the peak is given by
\(\sin(t)\). And we are interested on how the integral of the profile over
the domain varies with t. Of course this can be calculated analytically:</p>

\[\begin{align}
\int \limits_{-L_z}^{L_z} n(z')\, \mathrm{d}z' = L_z n_0 + \sin(t)^2 \sqrt{2\pi} \sigma \mathrm{erf} \left( \frac{1}{\sqrt{2}\sigma} \right)
\end{align}\]

<p>To study the dependence of this integral on the amplitude parameter t we
evaluate the derivative:</p>

\[\begin{align}
\frac{\partial N}{\partial t} = L_z \sqrt{2\pi} \sigma \mathrm{erf}\left( \frac{1}{\sqrt{2}\sigma} \right) \sin(t) \cos(t).
\end{align}\]

<p>To find the amplitude t that maximizes N we can now just set \(\partial N/\partial t = 0\) and solve for t. But in a situation where we are not so
lucky and have an analytic expression we may want to use automatic
differntiation. Let’s do this in Julia.</p>

<h1 id="automatic-differentiation-in-julia">Automatic differentiation in Julia</h1>

<p>First things first - import <a href="https://github.com/FluxML/Zygote.jl">Zygote</a> to do automatic differentiation and
the plots package as well as the error function erf:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="n">Zygote</span>
<span class="k">using</span> <span class="n">Plots</span>
<span class="k">using</span> <span class="n">SpecialFunctions</span><span class="o">:</span> <span class="n">erf</span>
</code></pre></div></div>

<p>Now we define a function that gives our density profile:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gen_prof</span><span class="x">(</span><span class="n">xrg</span><span class="x">,</span> <span class="n">n0</span><span class="x">,</span> <span class="n">t</span><span class="x">,</span> <span class="n">z0</span><span class="x">,</span> <span class="n">σ</span><span class="x">)</span> <span class="o">=</span> <span class="n">n0</span> <span class="o">.+</span> <span class="n">sin</span><span class="x">(</span><span class="n">t</span><span class="x">)</span><span class="o">.^</span><span class="mi">2</span> <span class="o">.*</span> <span class="n">exp</span><span class="o">.</span><span class="x">(</span><span class="o">-</span><span class="x">(</span><span class="n">zrg</span> <span class="o">.-</span> <span class="n">z0</span><span class="x">)</span><span class="o">.^</span><span class="mi">2</span> <span class="o">./</span> <span class="mf">2.0</span> <span class="o">./</span> <span class="n">σ</span> <span class="o">./</span> <span class="n">σ</span><span class="x">)</span>
</code></pre></div></div>

<p>For automatic differentiation to do its magic we need to numerically integrate
the profile.</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span><span class="nf"> int_prof</span><span class="x">(</span><span class="n">t</span><span class="x">)</span>
    <span class="n">my_prof</span> <span class="o">=</span> <span class="n">gen_prof</span><span class="x">(</span><span class="n">z0</span><span class="o">:</span><span class="n">Δz</span><span class="o">:</span><span class="n">z1</span><span class="x">,</span> <span class="n">n0</span><span class="x">,</span> <span class="n">t</span><span class="x">,</span> <span class="n">μ</span><span class="x">,</span> <span class="n">σ</span><span class="x">)</span>
    <span class="n">my_sum</span> <span class="o">=</span> <span class="n">sum</span><span class="x">(</span><span class="n">my_prof</span><span class="x">)</span> <span class="o">*</span> <span class="n">Δz</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This function generates a vector which holds values of the profile at the points
starting at \(z_0\) to \(z_1\), spaced regularly with \(\triangle z\). The
parameters \(\mu\) and \(\sigma\) are captured from the context where the function is
called from later. After generating the profile vector we numerically integrate the
profile using the rectangle rule.</p>

<p>Now we have all the ingredients to calculate \(\partial N/\partial t\) using 
automatic differentiation. In code, we first define the necessary parameters
and allocate vectors for the t’s where we want the derivative:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">n0</span> <span class="o">=</span> <span class="mf">1.0</span>
<span class="n">σ</span> <span class="o">=</span> <span class="mf">0.2</span>
<span class="n">x0</span> <span class="o">=</span> <span class="o">-</span><span class="mf">1.0</span>
<span class="n">x1</span> <span class="o">=</span> <span class="mf">1.0</span>
<span class="n">Δx</span> <span class="o">=</span> <span class="mf">0.01</span>
<span class="n">n0</span> <span class="o">=</span> <span class="mf">1.0</span>
<span class="n">μ</span> <span class="o">=</span> <span class="n">x0</span> <span class="o">+</span> <span class="x">(</span><span class="n">x1</span> <span class="o">-</span> <span class="n">x0</span><span class="x">)</span> <span class="o">/</span> <span class="mf">2.0</span>
<span class="n">σ</span> <span class="o">=</span> <span class="x">(</span><span class="n">x1</span> <span class="o">-</span> <span class="n">x0</span><span class="x">)</span> <span class="o">/</span> <span class="mf">10.0</span>

<span class="c"># Perform numerical integration of the profile for a range of t's:</span>
<span class="n">t_vals</span> <span class="o">=</span> <span class="mf">0.0</span><span class="o">:</span><span class="mf">0.05</span><span class="o">:</span><span class="mf">6.5</span>
<span class="n">sum_vals</span> <span class="o">=</span> <span class="n">similar</span><span class="x">(</span><span class="n">t_vals</span><span class="x">)</span>
<span class="n">sum_vals_grad</span> <span class="o">=</span> <span class="n">similar</span><span class="x">(</span><span class="n">t_vals</span><span class="x">)</span>
</code></pre></div></div>

<p>To find the gradient, we first evaluate <code class="language-plaintext highlighter-rouge">int_prof</code> for a given value of t.
Then we call <code class="language-plaintext highlighter-rouge">gradient(int_prof, t)</code> on this call. The return value is just the
gradient \(\partial N / \partial t\). Thist functionality is just Zygote’s
<a href="https://fluxml.ai/Zygote.jl/latest/#Taking-Gradients-1">gradient</a> doing it’s 
work but on a more complicated example than in the documentation. Note that
<code class="language-plaintext highlighter-rouge">gradient</code> returns a tuple where the actual gradient is the first element.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>for tidx ∈ tvals
    t = t_vals[tidx]
    sum_vals[tidx] = int_prof(t)
    sum_vals_grad[tidx] = gradient(int_prof, t)[1]
end
</code></pre></div></div>

<p>The gradient calculated using automatic differentiation can be compared to
the analytic formula:</p>
<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dNdt_anl</span> <span class="o">=</span> <span class="mf">2.</span> <span class="o">*</span> <span class="n">sqrt</span><span class="x">(</span><span class="mi">2</span><span class="nb">π</span><span class="x">)</span> <span class="o">*</span> <span class="n">σ</span> <span class="o">.*</span> <span class="n">sin</span><span class="o">.</span><span class="x">(</span><span class="n">t_vals</span><span class="x">)</span> <span class="o">.*</span> <span class="n">cos</span><span class="o">.</span><span class="x">(</span><span class="n">t_vals</span><span class="x">)</span> <span class="o">.*</span> <span class="n">erf</span><span class="x">(</span><span class="mi">1</span> <span class="o">/</span> <span class="n">√</span><span class="x">(</span><span class="mi">2</span><span class="x">)</span> <span class="o">/</span> <span class="n">σ</span> <span class="x">)</span>

<span class="n">p</span> <span class="o">=</span> <span class="n">plot</span><span class="x">(</span><span class="n">t_vals</span><span class="x">,</span> <span class="n">sum_vals_grad</span><span class="x">,</span> <span class="n">label</span><span class="o">=</span><span class="s">"∂ ∫n(z,t) dz / ∂t (autodiff)."</span><span class="x">)</span>
<span class="n">plot!</span><span class="x">(</span><span class="n">p</span><span class="x">,</span> <span class="n">t_vals</span><span class="x">,</span> <span class="n">dNdt_anl</span><span class="x">,</span> <span class="n">label</span><span class="o">=</span><span class="s">"∂ ∫n(z,t) dz / ∂t(analytical)."</span><span class="x">,</span> <span class="n">xlabel</span><span class="o">=</span><span class="s">"t"</span><span class="x">)</span>
</code></pre></div></div>

<p><img src="/assets/images/autodiff_examples/dNdt.png" alt="Comparing analytical derivative with derivative found by automatic
differentiation" /></p>

<p>As shown in the plot, the analytical derivative and the one calculated by
automatic differentiation are almost identical.</p>

<p><img src="/assets/images/autodiff_examples/dNdt_Error.png" alt="Comparing analytical derivative with derivative found by automatic
differentiation" /></p>

<p>And plotting the error, we find differences of the order of \(10^{-8}\) to
\(10^{-9}\).</p>

<h1 id="summary">Summary</h1>
<p>In this blog post we looked at a very simple use-case for automatic differentiation.
We have a mathematical expression that we know depends on a paramter and we wish
to find the optimum value of this expression. Using automatic differentiation we 
now can calculate the gradient of that expression with respect to the parameter.
This approach allows one to find the optimize the expression with resepct to the
value.</p>

<p>A common procedure for this optimization is gradient descent, used in deep learning.
Instead of a optimizing a neural network, automatic differentiation can be applied
to arbitrary code. So we can optimize a much broader class of items than just 
neural networks.</p>]]></content><author><name></name></author><category term="julia" /><category term="zygote" /><summary type="html"><![CDATA[Differentiable programming makes all code amenable to gradient based optimization. This harmless statement has broad implications. Think for example about a physical simulation where a differential equation is integrated in time. Given some initial conditions, boundary conditions, and parameters, the simulator approximates how the system evolves in time. Now lets say you simulate a wave in a swimming pool that moves towards a wall. Your goal is to find out how fast the wave has to be launched as to swap over the edge it is travelling to. With differentiable programming, you could define a target metric and optimize it with respect to the initial condition, here the speed with which the wave is launched.]]></summary></entry><entry><title type="html">Training GANs in Julia’s Flux</title><link href="/julia/gan/2021/11/08/training-gans.html" rel="alternate" type="text/html" title="Training GANs in Julia’s Flux" /><published>2021-11-08T00:00:00+00:00</published><updated>2021-11-08T00:00:00+00:00</updated><id>/julia/gan/2021/11/08/training-gans</id><content type="html" xml:base="/julia/gan/2021/11/08/training-gans.html"><![CDATA[<p>In order to effectively run machine learning experiments we need a fast
turn-around time for model training. So simply implementing the model is not
the only thing we need to worry about. We also want to be able to change the
hyperparameters in a convenient way. This could either be through a configuration
file or through command line arguments. This post demonstrates how I train
a <a href="https://fluxml.ai/tutorials/2021/10/14/vanilla-gan.html">vanilla GAN</a> on the
MNIST dataset. It is not about GAN theory, for this the original paper by
Goodfellow et al. [[1]] is a good starting point. Instead I focus on how to
structure the code and subtle implementation issues I came across when writing
the code. You can find the current version of the code on <a href="https://github.com/rkube/mnist_gan">github</a>.</p>

<h2 id="project-structure">Project structure</h2>
<p>I am taking a starting point in the vanilla GAN implementation on the 
<a href="https://fluxml.ai/tutorials/2021/10/14/vanilla-gan.html">FluxML website</a>. This
implementation works and the trained generator indeed generates images that
look indistinguishable from images belonging to the MNIST dataset.
But how do we arrive there? Why are the learning rates chosen as \(\eta = 2 \times 10^{-4}\)? IS the <code class="language-plaintext highlighter-rouge">leakyrelu</code> the optimal activation function or does it perform
on-par with <code class="language-plaintext highlighter-rouge">relu</code> in some regime? To answer these questions we need a code that 
quickly allows us to change these parameters.</p>

<p>And while we are at it, lets bundle the code together with its dependencies in a
Julia package. This allows us to conveniently a package dependencies to the code.
Taken together, the code and well defined dependencies make the behaviour reproducible.  The Julia documentation gives a comprehensive introduction on
packages <a href="https://pkgdocs.julialang.org/v1/creating-packages/">here</a>.</p>

<p>In order to run the code in the project I first checkout the code from github,
then enter the repository and then execute the runme script:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">$</span> <span class="n">git</span> <span class="n">checkout</span> <span class="n">https</span><span class="o">://</span><span class="n">github</span><span class="o">.</span><span class="n">com</span><span class="o">/</span><span class="n">rkube</span><span class="o">/</span><span class="n">mnist_gan</span><span class="o">.</span><span class="n">git</span>
<span class="o">$</span> <span class="n">cd</span> <span class="n">mnist_gan</span>
<span class="o">$</span> <span class="n">julia</span> <span class="o">--</span><span class="n">project</span><span class="o">=.</span> <span class="n">src</span><span class="o">/</span><span class="n">runme</span><span class="o">.</span><span class="n">jl</span> <span class="o">--</span><span class="n">activation</span><span class="o">=</span><span class="n">ADAM</span> <span class="o">--</span><span class="n">train_k</span><span class="o">=</span><span class="mi">8</span> <span class="o">...</span>
</code></pre></div></div>

<p>All packages installed in the project are local to this project and don’t interfere
with packages installed in the general environment. This allows for example to
specify for certain version numbers and will give us producibility of our results.</p>

<h2 id="code-structure">Code structure</h2>
<p>The code is structued as a standard Julia project. The root folder layout looks
like this</p>

<pre><code class="language-.">├── Manifest.toml
├── Project.toml
├── README.md
└── src
    ├── Manifest.toml
    ├── mnist_gan.jl
    ├── models.jl
    ├── Project.toml
    ├── runme.jl
    └── training.jl
</code></pre>

<p>The root folder contains <code class="language-plaintext highlighter-rouge">Manifest.toml</code> and <code class="language-plaintext highlighter-rouge">Project.toml</code> which include information
about dependencies, versions, package names. More information is given in the
<a href="https://pkgdocs.julialang.org/v1/toml-files/">Pkg.jl documentation</a>.</p>

<p>The <code class="language-plaintext highlighter-rouge">src</code> folder contains all source codes files. In particular it contains a
<code class="language-plaintext highlighter-rouge">mnist_gan.jl</code> file. This is named after the package name and in the simple case
here only twofines the package as a module, includes all other modules and
my two source files</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="n">mnist_gan</span>

<span class="k">using</span> <span class="n">NNlib</span>
<span class="k">using</span> <span class="n">Flux</span>
<span class="k">using</span> <span class="n">Zygote</span>
<span class="k">using</span> <span class="n">Base</span><span class="o">:</span><span class="n">Fix2</span>

<span class="c"># All GAN models</span>
<span class="n">include</span><span class="x">(</span><span class="s">"models.jl"</span><span class="x">)</span>
<span class="c"># Functions used for training</span>
<span class="n">include</span><span class="x">(</span><span class="s">"training.jl"</span><span class="x">)</span>
<span class="k">end</span> <span class="c">#module</span>
</code></pre></div></div>

<p>As additional structure I put the models in <code class="language-plaintext highlighter-rouge">models.jl</code> and training functions in
<code class="language-plaintext highlighter-rouge">training.jl</code>.</p>

<h2 id="command-line-arguments">Command line arguments</h2>
<p>To quickly train the GAN with specific hyperparameters one can either read the
hyperparameters from a configuration file or pass them through the command line.
Here we do the second approach. To comfortably parse command line arguments I’m
using (ArgParse.jl)[https://argparsejl.readthedocs.io/en/latest/argparse.html].</p>

<p>Condensing to only single argument, my code looks like this:</p>
<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="n">ArgParse</span>

<span class="n">s</span> <span class="o">=</span> <span class="n">ArgParseSettings</span><span class="x">()</span>
<span class="nd">@add_arg_table</span> <span class="n">s</span> <span class="k">begin</span>
    <span class="s">"--lr_dscr"</span>
        <span class="n">help</span> <span class="o">=</span> <span class="s">"Learning rate for the discriminator. Default = 0.0002"</span>
        <span class="n">arg_type</span> <span class="o">=</span> <span class="kt">Float64</span>
        <span class="n">default</span> <span class="o">=</span> <span class="mf">0.0002</span>

<span class="n">args</span> <span class="o">=</span> <span class="n">parse_args</span><span class="x">(</span><span class="n">s</span><span class="x">)</span>
</code></pre></div></div>
<p>That’s it. Now I can access the single command line arguments via <code class="language-plaintext highlighter-rouge">args[lr_dscr]</code>.</p>

<h2 id="logging">Logging</h2>
<p>Keeping track of the model performance while training is crucial when performing
parameter scans. For the vanilla GAN alone I defined 10 parameters that can be
varied. Letting each parameter assume only two distinct values this allows for
1024 combinations. Julia’s [logging facilities(https://github.com/JuliaLogging)
provide means to systematicallylog model training for a large hyperparameter scan.</p>

<p>In particular, we can use <a href="https://github.com/JuliaLogging/TensorBoardLogger.jl">TensorBoardLogger.jl</a>. <a href="https://www.tensorflow.org/tensorboard">TensorBoard</a>
provides a visualization of training and includes numerous useful features, such
as visualization of loss curves, displaying of model output images and more. To
use TensorBoardLogger.jl in my code I have to include the module, instantiate
a logger. Then I can easily log my experiments:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Import the modules</span>
<span class="k">using</span> <span class="n">TensorBoardLogger</span>
<span class="o">...</span>
<span class="c"># Instantiate TensorBoardLogger</span>
<span class="c"># Let's log the hyperparamters of the current run. </span>
<span class="n">dir_name</span> <span class="o">=</span> <span class="n">join</span><span class="x">([</span><span class="s">"</span><span class="si">$(k)</span><span class="s">_</span><span class="si">$(v)</span><span class="s">"</span> <span class="k">for</span> <span class="x">(</span><span class="n">k</span><span class="x">,</span> <span class="n">v</span><span class="x">)</span> <span class="k">in</span> <span class="n">a</span><span class="x">])</span>
<span class="n">tb_logger</span> <span class="o">=</span> <span class="n">TBLogger</span><span class="x">(</span><span class="s">"logs/"</span> <span class="o">*</span> <span class="n">dir_name</span><span class="x">)</span>
<span class="n">with_logger</span><span class="x">(</span><span class="n">tb_logger</span><span class="x">)</span> <span class="k">do</span>
    <span class="nd">@info</span> <span class="s">"hyperparameters"</span> <span class="n">args</span>
<span class="k">end</span>

<span class="c"># Wrap the main training loop in a with clause to enable logging</span>
<span class="n">lossvec_gen</span> <span class="o">=</span> <span class="n">zeros</span><span class="x">(</span><span class="kt">Float32</span><span class="x">,</span> <span class="n">args</span><span class="x">[</span><span class="s">"num_iterations"</span><span class="x">])</span>
<span class="n">lossvec_dscr</span> <span class="o">=</span> <span class="n">zeros</span><span class="x">(</span><span class="kt">Float32</span><span class="x">,</span> <span class="x">[</span><span class="s">"num_iterations"</span><span class="x">])</span>

<span class="n">with_logger</span><span class="x">(</span><span class="n">tb_logger</span><span class="x">)</span> <span class="k">do</span>
    <span class="k">for</span> <span class="n">n</span> <span class="n">∈</span> <span class="n">args</span><span class="x">[</span><span class="s">"num_iterations"</span><span class="x">]</span><span class="n">e</span>
        <span class="c"># Do machine learning ...</span>
        <span class="o">...</span>
        <span class="c"># Code to log PNG images to tensorboard, inside the main training loop</span>
        <span class="k">if</span> <span class="n">n</span> <span class="o">%</span> <span class="n">args</span><span class="x">[</span><span class="s">"output_period"</span><span class="x">]</span> <span class="o">==</span> <span class="mi">0</span>
            <span class="n">noise</span> <span class="o">=</span> <span class="n">randn</span><span class="x">(</span><span class="n">args</span><span class="x">[</span><span class="s">"latent_dim"</span><span class="x">],</span> <span class="mi">4</span><span class="x">)</span> <span class="o">|&gt;</span> <span class="n">gpu</span><span class="x">;</span>
            <span class="n">fake_img</span> <span class="o">=</span> <span class="n">reshape</span><span class="x">(</span><span class="n">generator</span><span class="x">(</span><span class="n">noise</span><span class="x">),</span> <span class="mi">28</span><span class="x">,</span> <span class="mi">4</span><span class="o">*</span><span class="mi">28</span><span class="x">)</span> <span class="o">|&gt;</span> <span class="n">cpu</span><span class="x">;</span>
            <span class="c"># I need to clip pixel values to [0.0; 1.0]</span>
            <span class="n">fake_img</span><span class="x">[</span><span class="n">fake_img</span> <span class="o">.&gt;</span> <span class="mf">1.0</span><span class="x">]</span> <span class="o">.=</span> <span class="mf">1.0</span>
            <span class="n">fake_img</span><span class="x">[</span><span class="n">fake_img</span> <span class="o">.&lt;</span> <span class="o">-</span><span class="mf">1.0</span><span class="x">]</span> <span class="o">.=</span> <span class="o">-</span><span class="mf">1.0</span>
            <span class="n">fake_img</span> <span class="o">=</span> <span class="x">(</span><span class="n">fake_img</span> <span class="o">.+</span> <span class="mf">1.0</span><span class="x">)</span> <span class="o">.*</span> <span class="mf">0.5</span>
            <span class="c"># </span>
            <span class="n">log_image</span><span class="x">(</span><span class="n">tb_logger</span><span class="x">,</span> <span class="s">"generatedimage"</span><span class="x">,</span> <span class="n">fake_img</span><span class="x">,</span> <span class="n">ImageFormat</span><span class="x">(</span><span class="mi">202</span><span class="x">))</span>
        <span class="k">end</span>
        <span class="c"># Log the generato and discriminator loss</span>
        <span class="nd">@info</span> <span class="s">"test"</span> <span class="n">loss_generator</span><span class="o">=</span><span class="n">lossvec_gen</span><span class="x">[</span><span class="n">n</span><span class="x">]</span> <span class="n">loss_discriminator</span><span class="o">=</span><span class="n">lossvec_dscr</span><span class="x">[</span><span class="n">n</span><span class="x">]</span>
    <span class="k">end</span> <span class="c"># for</span>
<span class="k">end</span> <span class="c">#  Logger</span>
</code></pre></div></div>

<p>First, I’m generating a string from all keys and values defined in the command
line argument dictionary. Later this will allow me to filter these arguments.
Then I’m logging the <code class="language-plaintext highlighter-rouge">args</code> dictionary, which contains the hyperparameters of
the current experiment. Then I’m generating a fake image using the generator
and log it as well. Here I need to clip the pixel values to [0.0; 1.0]. Since
the Generator is trained on images with pixel values between -1.0 and 1.0 I need
to transform the pixel space. Note that he last argument to the call in 
<code class="language-plaintext highlighter-rouge">log_image</code> encodes the layout of the <code class="language-plaintext highlighter-rouge">fake_img</code> array. I had to look up the
available encodings via</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">?</span>
</code></pre></div></div>

<h2 id="loss-functions-on-the-fly">Loss functions on-the-fly</h2>
<p>To resolve the correct loss function from command line arguments I’m using the
getfield method. To make it a little more convoluted, we also need to distinguish
between loss functions that take an additional, tunable parameterr
like <code class="language-plaintext highlighter-rouge">celu</code>, <code class="language-plaintext highlighter-rouge">elu</code>, <code class="language-plaintext highlighter-rouge">leakyrelu</code> and <code class="language-plaintext highlighter-rouge">trelu</code>,  and loss functions who do not.
The following code block shows how to map a string that encodes the function name
to the actual function using <code class="language-plaintext highlighter-rouge">getfield</code>. To create a closure over an optional
parameter I’m using <code class="language-plaintext highlighter-rouge">Fix2</code>. The code below is from <code class="language-plaintext highlighter-rouge">models.jl</code></p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span><span class="nf"> get_vanilla_discriminator</span><span class="x">(</span><span class="n">args</span><span class="x">)</span>
    <span class="o">...</span>
    <span class="k">if</span> <span class="n">args</span><span class="x">[</span><span class="s">"activation"</span><span class="x">]</span> <span class="k">in</span> <span class="x">[</span><span class="s">"celu"</span><span class="x">,</span> <span class="s">"elu"</span><span class="x">,</span> <span class="s">"leakyrelu"</span><span class="x">,</span> <span class="s">"trelu"</span><span class="x">]</span>
        <span class="c"># Now continue: We want to use Base.Fix2</span>
        <span class="n">act</span> <span class="o">=</span> <span class="n">Fix2</span><span class="x">(</span><span class="n">getfield</span><span class="x">(</span><span class="n">NNlib</span><span class="x">,</span> <span class="kt">Symbol</span><span class="x">(</span><span class="n">args</span><span class="x">[</span><span class="s">"activation"</span><span class="x">])),</span> <span class="kt">Float32</span><span class="x">(</span><span class="n">args</span><span class="x">[</span><span class="s">"activation_alpha"</span><span class="x">]))</span>
    <span class="k">else</span>
        <span class="n">act</span> <span class="o">=</span> <span class="n">getfield</span><span class="x">(</span><span class="n">NNlib</span><span class="x">,</span> <span class="kt">Symbol</span><span class="x">(</span><span class="n">args</span><span class="x">[</span><span class="s">"activation"</span><span class="x">]));</span>
    <span class="k">end</span>

    <span class="k">return</span> <span class="n">Chain</span><span class="x">(</span><span class="n">Dense</span><span class="x">(</span><span class="mi">28</span> <span class="o">*</span> <span class="mi">28</span><span class="x">,</span> <span class="mi">1024</span><span class="x">,</span> <span class="n">act</span><span class="x">),</span> 
        <span class="o">...</span><span class="x">);</span>
</code></pre></div></div>

<p>I found out that can have an impact on performance how I pass the activation
function as an argument to the dense layer. By passing only the function, the
implementation of Dense handles how the activation function is applied to the linear
transformation. This is how it should be. If I manually prescribe how to apply
the broadcast I find slower performance:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">julia</span><span class="o">&gt;</span> <span class="n">d1</span> <span class="o">=</span> <span class="n">Dense</span><span class="x">(</span><span class="mi">100</span><span class="x">,</span> <span class="mi">100</span><span class="x">,</span> <span class="n">act</span><span class="x">)</span>
<span class="n">Dense</span><span class="x">(</span><span class="mi">100</span><span class="x">,</span> <span class="mi">100</span><span class="x">,</span> <span class="n">relu</span><span class="x">)</span>  <span class="c"># 10_100 parameters</span>

<span class="n">julia</span><span class="o">&gt;</span> <span class="nd">@btime</span> <span class="n">d1</span><span class="x">(</span><span class="n">randn</span><span class="x">(</span><span class="kt">Float32</span><span class="x">,</span> <span class="mi">100</span><span class="x">,</span> <span class="mi">100</span><span class="x">));</span>
  <span class="mf">163.863</span> <span class="n">μs</span> <span class="x">(</span><span class="mi">6</span> <span class="n">allocations</span><span class="o">:</span> <span class="mf">117.33</span> <span class="n">KiB</span><span class="x">)</span>

<span class="n">julia</span><span class="o">&gt;</span> <span class="n">d2</span> <span class="o">=</span> <span class="n">Dense</span><span class="x">(</span><span class="mi">100</span><span class="x">,</span> <span class="mi">100</span><span class="x">,</span> <span class="n">x</span> <span class="o">-&gt;</span> <span class="n">act</span><span class="x">(</span><span class="n">x</span><span class="x">))</span>
<span class="n">Dense</span><span class="x">(</span><span class="mi">100</span><span class="x">,</span> <span class="mi">100</span><span class="x">,</span> <span class="c">#5)  # 10_100 parameters</span>

<span class="n">julia</span><span class="o">&gt;</span> <span class="nd">@btime</span> <span class="n">d2</span><span class="x">(</span><span class="n">randn</span><span class="x">(</span><span class="kt">Float32</span><span class="x">,</span> <span class="mi">100</span><span class="x">,</span> <span class="mi">100</span><span class="x">));</span>
  <span class="mf">3.041</span> <span class="n">ms</span> <span class="x">(</span><span class="mi">20016</span> <span class="n">allocations</span><span class="o">:</span> <span class="mf">430.30</span> <span class="n">KiB</span><span class="x">)</span>
</code></pre></div></div>

<p>So manually prescribing how to perform the broadcast is about 20 times slower.
Instead, I let the code above return a function that Flux knows how to apply a 
broadcast on.</p>

<h2 id="running-a-parameter-scan">Running a parameter scan</h2>
<p>Now we are set up to run a parameter scan. For this I generate runscripts
where I vary my command line arguments. The resulting scripts look like this</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#SBATCH things

cd /location/of/the/repo
julia --project=. --lr_dscr=0.0002 --lr_gen=0.0002 --batch_size=8 --num_iterations=10000 --latent_dim=100 --optimizer=ADAM --activation=leakyrelu --activation_alpha=0.2 --train_k=8 --prob_dropout=0.3 --output_period=250
</code></pre></div></div>

<p>Of course the arguments vary across the scripts. After crunching all the numbers,
the log file directory is populated with the tensorboard log files. The next
blog post will discuss how the results look like and how to pick the best
hyperparameters.</p>

<h2 id="references">References</h2>
<p><a id="1">[1]</a>
I. Goodfellow et al. <a href="https://arxiv.org/abs/1406.2661">Generative Adversarial Networks</a></p>]]></content><author><name></name></author><category term="julia" /><category term="gan" /><summary type="html"><![CDATA[In order to effectively run machine learning experiments we need a fast turn-around time for model training. So simply implementing the model is not the only thing we need to worry about. We also want to be able to change the hyperparameters in a convenient way. This could either be through a configuration file or through command line arguments. This post demonstrates how I train a vanilla GAN on the MNIST dataset. It is not about GAN theory, for this the original paper by Goodfellow et al. [[1]] is a good starting point. Instead I focus on how to structure the code and subtle implementation issues I came across when writing the code. You can find the current version of the code on github.]]></summary></entry><entry><title type="html">Backpropagating through QR decomposition</title><link href="/jekyll/update/2021/07/06/backpropagating-through-qr.html" rel="alternate" type="text/html" title="Backpropagating through QR decomposition" /><published>2021-07-06T05:56:22+00:00</published><updated>2021-07-06T05:56:22+00:00</updated><id>/jekyll/update/2021/07/06/backpropagating-through-qr</id><content type="html" xml:base="/jekyll/update/2021/07/06/backpropagating-through-qr.html"><![CDATA[<p>One of the most useful decompositions of Linear Algebra is the QR decomposition.
This decomposition is particularly important when we are interested in the 
vector space spanned by the columns of a matrix A. Formally, we write the QR
decomposition of \(A \in \mathbb{R}^{m \times n}\), where m ≥ n, as</p>

\[\begin{align}
A = QR = 
\left[
\begin{array}{c|c|c|c}
     &amp;     &amp;        &amp; \\
 q_1 &amp; q_2 &amp; \cdots &amp; q_n \\ 
     &amp;     &amp;        &amp; 
\end{array}
\right]
%
\left[
    \begin{array}{cccc}
    r_{1,1} &amp; r_{1,2} &amp; \cdots &amp; r_{1,n} \\ 
    0       &amp; r_{2,2} &amp; \cdots &amp; r_{2,n} \\ 
    0       &amp; 0       &amp; \ddots &amp; \vdots  \\
    0       &amp; \cdots  &amp;        &amp; r_{n,n}
    \end{array}
\right].
\end{align}\]

<p>Where Q is an orthogonal m-by-n matrix, i.e. the columns of Q are orthogonal 
\(q_{i} \cdot q_{j} = \delta_{i,j}\). R is a square n-by-n matrix.
Using the decomposition, we can reconstruct the columns of A as</p>

\[\begin{align}
a_1 &amp; = r_{1,1} q_1 \\ 
a_2 &amp; = r_{1,2} q_1 + r{2,2} q_2 \\ 
    &amp; \cdots \\ 
a_n &amp; = \sum_{j=1}^{n} r_{j,n} q_j.
\end{align}\]

<p>Internally, Julia treats QR-factorized matrices through a packed format <a href="#1">[1]</a>.
This format does not store the matrices Q and R explicitly, but using a packed format.
All matrix algebra and arithmetic is implemented through methods that expect
the packed format as input. And the QR decomposition itself is calling the LAPACK
method
<a href="https://github.com/JuliaLang/julia/blob/018977209bb4fd707ec61c59dfd31860abaa6717/stdlib/LinearAlgebra/src/qr.jl#L283">geqrt</a> which
returns just this packed format.</p>

<p>A computational graph where one may want to backpropagate through a QR factorization
may look similar to this one:
<img src="/assets/images/autodiff/backprop_qr.png" alt="Computational graph with QR factorization" /></p>

<p>While the incoming gradients, \(\bar{f} \partial f / \partial Q\) and
\(\bar{f} \partial f / \partial R\) depend on \(f\), the gradients for the QR
decomposition \(\bar{Q} \partial Q / \partial A\) and \(\bar{R} \partial R / \partial A\) are defined through the QR factorization. While these can be in principle
computed through an automatic differentiation framework, it can be beneficial to
implement a pullback. For one, this saves compilation time before first execution.
An additional benefit is less memory use, as the gradients will be propagated through
fewer functions. A pullback for the QR factorization may thus also aid numerical
stability, as fewer accumulations and propagations are performed.</p>

<p>Formulas for the pullback of the QR factorization are given in <a href="#2">[2]</a>,
<a href="#3">[3]</a> and <a href="#4">[4]</a>. Pytorch for example implements the method described in <a href="#3">[3]</a>, see <a href="https://github.com/pytorch/pytorch/blob/b162d95e461a5ea22f6840bf492a5dbb2ebbd151/torch/csrc/autograd/FunctionsManual.cpp">here</a>.</p>

<p>In this blog post, we implement the pullback for the QR factorization in Julia.
Specifically, we implement a so-called <a href="https://juliadiff.org/ChainRulesCore.jl/dev/index.html#frule-and-rrule">rrule</a> for <a href="https://juliadiff.org/ChainRulesCore.jl/dev/index.html">ChainRules.jl</a>. While you are here, please take a moment and read
the documentation of this package. It is very well written and helped me tremendously
to understand how automatic differentiation works and is implemented n Julia.
Also, if you want to skip ahead, <a href="https://gist.github.com/rkube/ccdd21b8009e5be281f3870a0caec47c">here</a> is my example implementation of the
QR pullback for ChainRules.</p>

<p>Now, let’s take a look at the code and how to implement a custom rrule. The first
thing we need to look at is the <a href="https://github.com/JuliaLang/julia/blob/018977209bb4fd707ec61c59dfd31860abaa6717/stdlib/LinearAlgebra/src/qr.jl#L37">definition of the QR struct</a></p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span><span class="nc"> QR</span><span class="x">{</span><span class="n">T</span><span class="x">,</span><span class="n">S</span><span class="o">&lt;:</span><span class="kt">AbstractMatrix</span><span class="x">{</span><span class="n">T</span><span class="x">}}</span> <span class="o">&lt;:</span> <span class="kt">Factorization</span><span class="x">{</span><span class="n">T</span><span class="x">}</span>
    <span class="n">factors</span><span class="o">::</span><span class="n">S</span>
    <span class="n">τ</span><span class="o">::</span><span class="kt">Vector</span><span class="x">{</span><span class="n">T</span><span class="x">}</span>

    <span class="k">function</span><span class="nf"> QR</span><span class="x">{</span><span class="n">T</span><span class="x">,</span><span class="n">S</span><span class="x">}(</span><span class="n">factors</span><span class="x">,</span> <span class="n">τ</span><span class="x">)</span> <span class="k">where</span> <span class="x">{</span><span class="n">T</span><span class="x">,</span><span class="n">S</span><span class="o">&lt;:</span><span class="kt">AbstractMatrix</span><span class="x">{</span><span class="n">T</span><span class="x">}}</span>
        <span class="n">require_one_based_indexing</span><span class="x">(</span><span class="n">factors</span><span class="x">)</span>
        <span class="n">new</span><span class="x">{</span><span class="n">T</span><span class="x">,</span><span class="n">S</span><span class="x">}(</span><span class="n">factors</span><span class="x">,</span> <span class="n">τ</span><span class="x">)</span>
    <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>There are two fields in the structure, <em>factors</em> and <em>τ</em>.  The matrices Q and R are
returned through an accompanying <a href="https://github.com/JuliaLang/julia/blob/018977209bb4fd707ec61c59dfd31860abaa6717/stdlib/LinearAlgebra/src/qr.jl#L462">getproperty</a> function:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span><span class="nf"> getproperty</span><span class="x">(</span><span class="n">F</span><span class="o">::</span><span class="kt">QR</span><span class="x">,</span> <span class="n">d</span><span class="o">::</span><span class="kt">Symbol</span><span class="x">)</span>
    <span class="n">m</span><span class="x">,</span> <span class="n">n</span> <span class="o">=</span> <span class="n">size</span><span class="x">(</span><span class="n">F</span><span class="x">)</span>
    <span class="k">if</span> <span class="n">d</span> <span class="o">===</span> <span class="o">:</span><span class="n">R</span>
        <span class="k">return</span> <span class="n">triu!</span><span class="x">(</span><span class="n">getfield</span><span class="x">(</span><span class="n">F</span><span class="x">,</span> <span class="o">:</span><span class="n">factors</span><span class="x">)[</span><span class="mi">1</span><span class="o">:</span><span class="n">min</span><span class="x">(</span><span class="n">m</span><span class="x">,</span><span class="n">n</span><span class="x">),</span> <span class="mi">1</span><span class="o">:</span><span class="n">n</span><span class="x">])</span>
    <span class="k">elseif</span> <span class="n">d</span> <span class="o">===</span> <span class="o">:</span><span class="n">Q</span>
        <span class="k">return</span> <span class="n">QRPackedQ</span><span class="x">(</span><span class="n">getfield</span><span class="x">(</span><span class="n">F</span><span class="x">,</span> <span class="o">:</span><span class="n">factors</span><span class="x">),</span> <span class="n">F</span><span class="o">.</span><span class="n">τ</span><span class="x">)</span>
    <span class="k">else</span>
        <span class="n">getfield</span><span class="x">(</span><span class="n">F</span><span class="x">,</span> <span class="n">d</span><span class="x">)</span>
    <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>For the QR pullback we first need to implement a pullback for the <em>getproperty</em>
function. The pullback for this function only propagates incoming gradients
backwards. Incoming gradients are described using a <a href="https://juliadiff.org/ChainRulesCore.jl/stable/api.html#ChainRulesCore.Tangent">Tangent</a>. This struct can only
have fields that are present in the parameter type, in our case 
<a href="https://docs.julialang.org/en/v1/stdlib/LinearAlgebra/#LinearAlgebra.QRCompactWY">LinearAlgebra.QRCompactWY</a>. This struct has fields <em>factors</em> and <em>τ</em> as discussed
above. Now the incoming gradients would be \(\bar{Q}\) and \(\bar{R}\). Thus
the pullback needs to map between these two. Thus the pullback can be implemented
like this:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">function</span><span class="nf"> ChainRulesCore.rrule</span><span class="x">(</span><span class="o">::</span><span class="n">typeof</span><span class="x">(</span><span class="n">getproperty</span><span class="x">),</span> <span class="n">F</span><span class="o">::</span><span class="n">LinearAlgebra</span><span class="o">.</span><span class="n">QRCompactWY</span><span class="x">,</span> <span class="n">d</span><span class="o">::</span><span class="kt">Symbol</span><span class="x">)</span> 
    <span class="k">function</span><span class="nf"> getproperty_qr_pullback</span><span class="x">(</span><span class="n">Ȳ</span><span class="x">)</span>
        <span class="n">∂factors</span> <span class="o">=</span> <span class="k">if</span> <span class="n">d</span> <span class="o">===</span> <span class="o">:</span><span class="n">Q</span>
            <span class="n">Ȳ</span>
        <span class="k">else</span>
            <span class="nb">nothing</span>
        <span class="k">end</span>

        <span class="n">∂T</span> <span class="o">=</span> <span class="k">if</span> <span class="n">d</span> <span class="o">===</span> <span class="o">:</span><span class="n">R</span>
            <span class="n">Ȳ</span>
        <span class="k">else</span>
            <span class="nb">nothing</span>
        <span class="k">end</span>

        <span class="n">∂F</span> <span class="o">=</span> <span class="n">Tangent</span><span class="x">{</span><span class="n">LinearAlgebra</span><span class="o">.</span><span class="n">QRCompactWY</span><span class="x">}(;</span> <span class="n">factors</span><span class="o">=</span><span class="n">∂factors</span><span class="x">,</span> <span class="n">T</span><span class="o">=</span><span class="n">∂T</span><span class="x">)</span>
        <span class="k">return</span> <span class="x">(</span><span class="n">NoTangent</span><span class="x">(),</span> <span class="n">∂F</span><span class="x">)</span>
    <span class="k">end</span>

    <span class="k">return</span> <span class="n">getproperty</span><span class="x">(</span><span class="n">F</span><span class="x">,</span> <span class="n">d</span><span class="x">),</span> <span class="n">getproperty_qr_pullback</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Notice the call signature of the <code class="language-plaintext highlighter-rouge">rrule</code>. The first argument is always
<em>::typeof(functionname)</em>, where <em>functionname</em> is the name of the function that
we want a custom rrule for. Following this argument are the actual arguments
that one normally passes to <em>functionname</em>.</p>

<p>Finally we need to implement the pullback that actually performs the relevant
calculations. The typical way of implementing custom pullbacks with
<a href="https://juliadiff.org/ChainRulesCore.jl/dev/">ChainRules</a> is write a function
that calculates that returns a tuple containing the result of the forward pass,
as well as the pullback \(\mathcal{B}^{x}_{f}(\bar{y})\). Doing it this way allows
the pullback to re-use results from the forward pass. In other words, by defining
the pullback as a function in the forward pass allows to easily use cached results.
There is more discussion on the reason behind this design choice in the
<a href="https://juliadiff.org/ChainRulesCore.jl/dev/FAQ.html#Why-does-rrule-return-the-primal-function-evaluation?">ChainRules documentation</a>.</p>

<p>Returning to the pullback for the QR factorization, here is a possible implementation:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">function</span><span class="nf"> ChainRules.rrule</span><span class="x">(</span><span class="o">::</span><span class="n">typeof</span><span class="x">(</span><span class="n">qr</span><span class="x">),</span> <span class="n">A</span><span class="o">::</span><span class="kt">AbstractMatrix</span><span class="x">{</span><span class="n">T</span><span class="x">})</span> <span class="k">where</span> <span class="x">{</span><span class="n">T</span><span class="x">}</span> 
    <span class="kt">QR</span> <span class="o">=</span> <span class="n">qr</span><span class="x">(</span><span class="n">A</span><span class="x">)</span>
    <span class="n">m</span><span class="x">,</span> <span class="n">n</span> <span class="o">=</span> <span class="n">size</span><span class="x">(</span><span class="n">A</span><span class="x">)</span>
    <span class="k">function</span><span class="nf"> qr_pullback</span><span class="x">(</span><span class="n">Ȳ</span><span class="o">::</span><span class="n">Tangent</span><span class="x">)</span>
        <span class="k">function</span><span class="nf"> qr_pullback_square_deep</span><span class="x">(</span><span class="n">Q̄</span><span class="x">,</span> <span class="n">R̄</span><span class="x">,</span> <span class="n">A</span><span class="x">,</span> <span class="n">Q</span><span class="x">,</span> <span class="n">R</span><span class="x">)</span>
            <span class="n">M</span> <span class="o">=</span> <span class="n">R̄</span><span class="o">*</span><span class="n">R</span><span class="err">'</span> <span class="o">-</span> <span class="n">Q</span><span class="err">'</span><span class="o">*</span><span class="n">Q̄</span>
            <span class="c"># M &lt;- copyltu(M)</span>
            <span class="n">M</span> <span class="o">=</span> <span class="n">triu</span><span class="x">(</span><span class="n">M</span><span class="x">)</span> <span class="o">+</span> <span class="n">transpose</span><span class="x">(</span><span class="n">triu</span><span class="x">(</span><span class="n">M</span><span class="x">,</span><span class="mi">1</span><span class="x">))</span>
            <span class="n">Ā</span> <span class="o">=</span> <span class="x">(</span><span class="n">Q̄</span> <span class="o">+</span> <span class="n">Q</span> <span class="o">*</span> <span class="n">M</span><span class="x">)</span> <span class="o">/</span> <span class="n">R</span><span class="err">'</span>
        <span class="k">end</span> 
        <span class="n">Q̄</span> <span class="o">=</span> <span class="n">Ȳ</span><span class="o">.</span><span class="n">factors</span>
        <span class="n">R̄</span> <span class="o">=</span> <span class="n">Ȳ</span><span class="o">.</span><span class="n">T</span> 
        <span class="n">Q</span> <span class="o">=</span> <span class="kt">QR</span><span class="o">.</span><span class="n">Q</span>
        <span class="n">R</span> <span class="o">=</span> <span class="kt">QR</span><span class="o">.</span><span class="n">R</span>
        <span class="k">if</span> <span class="n">m</span> <span class="o">≥</span> <span class="n">n</span> 
            <span class="n">Q̄</span> <span class="o">=</span> <span class="n">Q̄</span> <span class="k">isa</span> <span class="n">ChainRules</span><span class="o">.</span><span class="n">AbstractZero</span> <span class="o">?</span> <span class="n">Q̄</span> <span class="o">:</span> <span class="nd">@view</span> <span class="n">Q̄</span><span class="x">[</span><span class="o">:</span><span class="x">,</span> <span class="n">axes</span><span class="x">(</span><span class="n">Q</span><span class="x">,</span> <span class="mi">2</span><span class="x">)]</span> 
            <span class="n">Ā</span> <span class="o">=</span> <span class="n">qr_pullback_square_deep</span><span class="x">(</span><span class="n">Q̄</span><span class="x">,</span> <span class="n">R̄</span><span class="x">,</span> <span class="n">A</span><span class="x">,</span> <span class="n">Q</span><span class="x">,</span> <span class="n">R</span><span class="x">)</span>
        <span class="k">else</span>
            <span class="c"># partition A = [X | Y]</span>
            <span class="c"># X = A[1:m, 1:m]</span>
            <span class="n">Y</span> <span class="o">=</span> <span class="n">A</span><span class="x">[</span><span class="mi">1</span><span class="o">:</span><span class="n">m</span><span class="x">,</span> <span class="n">m</span> <span class="o">+</span> <span class="mi">1</span><span class="o">:</span><span class="k">end</span><span class="x">]</span>
    
            <span class="c"># partition R = [U | V], and we don't need V</span>
            <span class="n">U</span> <span class="o">=</span> <span class="n">R</span><span class="x">[</span><span class="mi">1</span><span class="o">:</span><span class="n">m</span><span class="x">,</span> <span class="mi">1</span><span class="o">:</span><span class="n">m</span><span class="x">]</span>
            <span class="k">if</span> <span class="n">R̄</span> <span class="k">isa</span> <span class="n">ChainRules</span><span class="o">.</span><span class="n">AbstractZero</span>
                <span class="n">V̄</span> <span class="o">=</span> <span class="n">zeros</span><span class="x">(</span><span class="n">size</span><span class="x">(</span><span class="n">Y</span><span class="x">))</span>
                <span class="n">Q̄_prime</span> <span class="o">=</span> <span class="n">zeros</span><span class="x">(</span><span class="n">size</span><span class="x">(</span><span class="n">Q</span><span class="x">))</span>
                <span class="n">Ū</span> <span class="o">=</span> <span class="n">R̄</span> 
            <span class="k">else</span>
                <span class="c"># partition R̄ = [Ū | V̄]</span>
                <span class="n">Ū</span> <span class="o">=</span> <span class="n">R̄</span><span class="x">[</span><span class="mi">1</span><span class="o">:</span><span class="n">m</span><span class="x">,</span> <span class="mi">1</span><span class="o">:</span><span class="n">m</span><span class="x">]</span>
                <span class="n">V̄</span> <span class="o">=</span> <span class="n">R̄</span><span class="x">[</span><span class="mi">1</span><span class="o">:</span><span class="n">m</span><span class="x">,</span> <span class="n">m</span> <span class="o">+</span> <span class="mi">1</span><span class="o">:</span><span class="k">end</span><span class="x">]</span>
                <span class="n">Q̄_prime</span> <span class="o">=</span> <span class="n">Y</span> <span class="o">*</span> <span class="n">V̄</span><span class="err">'</span>
            <span class="k">end</span> 

            <span class="n">Q̄_prime</span> <span class="o">=</span> <span class="n">Q̄</span> <span class="k">isa</span> <span class="n">ChainRules</span><span class="o">.</span><span class="n">AbstractZero</span> <span class="o">?</span> <span class="n">Q̄_prime</span> <span class="o">:</span> <span class="n">Q̄_prime</span> <span class="o">+</span> <span class="n">Q̄</span> 

            <span class="n">X̄</span> <span class="o">=</span> <span class="n">qr_pullback_square_deep</span><span class="x">(</span><span class="n">Q̄_prime</span><span class="x">,</span> <span class="n">Ū</span><span class="x">,</span> <span class="n">A</span><span class="x">,</span> <span class="n">Q</span><span class="x">,</span> <span class="n">U</span><span class="x">)</span>
            <span class="n">Ȳ</span> <span class="o">=</span> <span class="n">Q</span> <span class="o">*</span> <span class="n">V̄</span> 
            <span class="c"># partition Ā = [X̄ | Ȳ]</span>
            <span class="n">Ā</span> <span class="o">=</span> <span class="x">[</span><span class="n">X̄</span> <span class="n">Ȳ</span><span class="x">]</span>
        <span class="k">end</span> 
        <span class="k">return</span> <span class="x">(</span><span class="n">NoTangent</span><span class="x">(),</span> <span class="n">Ā</span><span class="x">)</span>
    <span class="k">end</span> 
    <span class="k">return</span> <span class="kt">QR</span><span class="x">,</span> <span class="n">qr_pullback</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The part of the rrule that calculates the foward pass just calculates the QR 
decomposition and returns the result of that. The pullback consumes the
incoming gradients, \(\bar{R}\) and \(\bar{Q}\), which are stored as defined 
in the pullback for <em>getproperty</em>. The actual calculation for the case where
the dimensions of the matrix A are equal, m=n, and the case where A is tall and
skinny, m&gt;n are different. But they can re-use some code. That is what the
local function <em>qr_pullback_square_deep</em> does. The rest is mostly matrix slicing 
and the occasional matrix product. The implementation is borrows strongly from
pytorch’s autograd <a href="https://github.com/pytorch/pytorch/blob/b162d95e461a5ea22f6840bf492a5dbb2ebbd151/torch/csrc/autograd/FunctionsManual.cpp">implementation</a>.</p>

<p>Finally, I need to verify that my implemenation calculates correct results. For
this,  I define two small test functions which each work on exactly one output of the 
QR factorization, either the matrix Q or the matrix R. Then I generate some random
input and compare the gradient calculated through reverse-mode AD in Zygote
to the gradient calculated by FiniteDifferences. If they are approximately equal,
I take the result to be correct.</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code>

<span class="n">V1</span> <span class="o">=</span> <span class="n">rand</span><span class="x">(</span><span class="kt">Float32</span><span class="x">,</span> <span class="x">(</span><span class="mi">4</span><span class="x">,</span> <span class="mi">4</span><span class="x">));</span>

<span class="k">function</span><span class="nf"> f1</span><span class="x">(</span><span class="n">V</span><span class="x">)</span> <span class="k">where</span> <span class="n">T</span>
    <span class="n">Q</span><span class="x">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">qr</span><span class="x">(</span><span class="n">V</span><span class="x">)</span>
    <span class="k">return</span> <span class="n">sum</span><span class="x">(</span><span class="n">Q</span><span class="x">)</span>
<span class="k">end</span>

<span class="k">function</span><span class="nf"> f2</span><span class="x">(</span><span class="n">V</span><span class="x">)</span> <span class="k">where</span> <span class="n">T</span>
    <span class="n">_</span><span class="x">,</span> <span class="n">R</span> <span class="o">=</span> <span class="n">qr</span><span class="x">(</span><span class="n">V</span><span class="x">)</span>
    <span class="k">return</span> <span class="n">sum</span><span class="x">(</span><span class="n">R</span><span class="x">)</span>
<span class="k">end</span>


<span class="n">res1_V1_ad</span> <span class="o">=</span> <span class="n">Zygote</span><span class="o">.</span><span class="n">gradient</span><span class="x">(</span><span class="n">f1</span><span class="x">,</span> <span class="n">V1</span><span class="x">)</span>
<span class="n">res1_V1_fd</span> <span class="o">=</span> <span class="n">FiniteDifferences</span><span class="o">.</span><span class="n">grad</span><span class="x">(</span><span class="n">central_fdm</span><span class="x">(</span><span class="mi">5</span><span class="x">,</span><span class="mi">1</span><span class="x">),</span> <span class="n">f1</span><span class="x">,</span> <span class="n">V1</span><span class="x">)</span>
<span class="nd">@assert</span> <span class="n">res1_V1_ad</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span> <span class="n">≈</span> <span class="n">res1_V1_fd</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span>
</code></pre></div></div>

<p><a id="1">[1]</a>
Schreiber et al. <a href="https://doi.org/10.1137/0910005">A Storage-Efficient WY Representation for Products of Householder Transformations</a></p>

<p><a id="2">[2]</a>
HJ Liao, JG Liu et al. <a href="https://arxiv.org/abs/1903.09650">Differentiable Programming Tensor Networks</a></p>

<p><a id="3">[3]</a>
M. Seeger, A. Hetzel et al. <a href="https://arxiv.org/abs/1710.08717">Auto-Differenting Linear Algebra</a></p>

<p><a id="4">[4]</a>
Walter and Lehman <a href="https://arxiv.org/abs/1001.1654">Walter and Lehmann, 2018, Algorithmic Differentiation of Linear Algebra Functions with Application in Optimum Experimental Design</a></p>]]></content><author><name></name></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[One of the most useful decompositions of Linear Algebra is the QR decomposition. This decomposition is particularly important when we are interested in the vector space spanned by the columns of a matrix A. Formally, we write the QR decomposition of \(A \in \mathbb{R}^{m \times n}\), where m ≥ n, as]]></summary></entry><entry><title type="html">Automatic differentiation - Reverse Mode</title><link href="/julia/autodiff/2021/05/16/autodiff2.html" rel="alternate" type="text/html" title="Automatic differentiation - Reverse Mode" /><published>2021-05-16T18:00:00+00:00</published><updated>2021-05-16T18:00:00+00:00</updated><id>/julia/autodiff/2021/05/16/autodiff2</id><content type="html" xml:base="/julia/autodiff/2021/05/16/autodiff2.html"><![CDATA[<p>This post is the follow-up of my post on <a href="2021-05-10-autodiff1.md">forward mode AD</a>.
There I motivated motivated the use of automatic differentiation by noting that it is helpful for
gradient-baseed optimization. In this post I will discuss how reverse mode allows us to
take the derivative of a function output with respect to basically an arbitrary number
of parameters in one sweep. To do this comfortably, one often evaluates <em>local</em> 
derivatives, whose use is facilitated by introducing the bar notation.</p>

<p>To see this, let’s go the other way around. Given our computational graph we
start at the output \(yy\) and calculate derivatives with respect to intermediate
values. For convenience we define the notation</p>

\[\begin{align}
    \bar{u} = \frac{\partial y}{\partial u}
\end{align}\]

<p>that is, the symbol under the bar is the variable we wish to take the derivative with
respect to. Now let’s dive right in and take derivatives from the example. We start
out with</p>

\[\begin{align}
    \bar{v}_6 &amp; = \frac{\partial y}{\partial v_6} = 1 \\
    \bar{v}_5 &amp; = \frac{\partial y}{\partial v_5} = \frac{\partial y}{\partial v_6} \frac{\partial v_6}{\partial v_5} = \bar{v}_6 \frac{\partial v_6(v_4, v_5)}{\partial v_5} = \bar{v}_6 v_4 \\
\end{align}\]

<p>The first expression is often called the seed gradient and is trivial to evaluate.
In order to evaluate \(\bar{v}_5\) we had to use the chain-rule.</p>

<p>Continuing, we now have to evaluate \(\bar{v}_4\). Let’s do it first using the
chain rule:</p>

\[\begin{align}
    \frac{\partial y}{\partial v_4} &amp; = 
        \frac{\partial y}{\partial v_6} \left(
            \frac{\partial v_6}{\partial v_5} \frac{\partial v_5}{\partial v_4} +
            \frac{\partial v_6}{\partial v_4}
        \right) \\ 
        &amp; = v_4 (-1) + v_5.
\end{align}\]

<p>where we have used the chain rule and that \(v_6 = v_6(v_5(v_4), v_4)\). Looking at
the computational graph, we see that we had to split the product using a plus-sign
precisely at a position where there are multiple paths from the origin \(y\) to
the target node \(v_4\), one via \(v_6\) and one via \(v_5\). Now as trivial as
this example is, real-world programs are often much more complicated. And to
calculate the partial derivatives, the formulas can become ever more complex.</p>

<p>Now the bar notation is here to make our life easier. Essentially, it gives us an
easy way to replace chain-rule evaluation with local values that are stored in
all child-nodes of the target node \(v_i\) in \(\bar{v}_i\). Put in another way,
they are used as cache-values when traverseing the graph from right-to-left, as we 
do in reverse-mode AD. This is illustrated in the sketch below:</p>

<p><img src="/assets/images/autodiff/reverse_mode_graph.png" alt="Coputational graph for reverse-mode autodiff" /></p>

<p>We see that there are two gradients incoming to the \(v_4\) node:
\(\bar{v}_5 \partial v_5 / \partial v_4\) and \(\bar{v}_6 \partial v_6 / \partial v_4\).
Assuming that \(\bar{v}_6\) and \(\bar{v}_5\) are already evaluated, we just need the
partial derivatives \(\partial v_5 / \partial v_4\) and 
\(\partial v_6 / \partial v_4\) to find \(\bar{v}_4\):</p>

\[\begin{align}
\bar{v}_4 = \bar{v}_5 \frac{\partial v_5}{\partial v_4} + \bar{v}_6 \frac{\partial v_6}{\partial v_4} = \bar{v}_5 (-1) + \bar{v}_6 v_5.
\end{align}\]

<p>And indeed, we have recovered the same rule as when applying the chain rule.
When we continue to calculate derivatives \(\bar{v}_i\), we see that this
notation becomes very handy. For example, using the chain rule we have</p>

\[\begin{align}
\bar{v}_3 = \frac{\partial y}{v_3} =  \bar{v}_6 \frac{\partial v_6(v_5(v_4(v_1,v_3), v_4(v_3, v_1)))}{\partial v_3} 
\end{align}\]

<p>where we again need to use the product rule. Using the bar-notation however, the
derivative becomes trivial</p>

\[\begin{align}
    \bar{v}_3 = \bar{v}_4 \frac{\partial v_4}{v_3} = \bar{v}_4 v_1,
\end{align}\]

<p>given that both \(\bar{v}_4\) and \(v_1\) have been evaluated. But in typical
reverse-mode use this is the case, as we have completed one forward pass through
the graph and we have traversed the barred values from right to left.</p>

<p>For completeness sake, let’s calculate the Reverse-mode AD evaluation trace as we did
for the forward mode.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Reverse-mode AD evaluation trace</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">\(v_{-1} = x_1 = 1.0\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_0 = x_2 = 1.1\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_1 = v_0 v_{-1} = (1.1) (1.0) = 1.1\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_2 = exp(v_1) = 3.004\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_3 = cos(v_0) = 0.4536\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_4 = v_1 v_3 = 0.4990\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_5 = v_2 - v_4 = (3.004) - (0.4990) = 2.5052\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_6 = v_4 v_5 = (0.4990) (2.5052) = 1.2500\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(\bar{v}_6 = \frac{\partial y}{\partial v_6} = 1\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(\bar{v}_5 = \frac{\partial y}{\partial v_5} = \bar{v}_6 v_4 = (1) (0.4990) = 0.4990\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(\bar{v}_4 = \frac{\partial y}{\partial v_4} = \bar{v}_6 \frac{\partial v_6}{\partial v_4} + \bar{v}_5 \frac{\partial v_5}{v_4} = \bar{v}_6 v_5 + \bar{v}_5 (-1) = (1)(2.5052) + (0.4990)(-1) = 2.006\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(\bar{v}_3 = \frac{\partial y}{v_3} = \bar{v}_4 \frac{\partial v_4}{\partial v_3} = \bar{v}_4 v_1 = (2.006) * (1.1) = 2.2069\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(\bar{v}_2 = \bar{v}_5 \frac{\partial v_5}{\partial v_2} = \bar{v}_5 (1) = 0.4990\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(\bar{v}_1 = \bar{v}_2 \frac{\partial v_2}{\partial v_1} + \bar{v}_4 \frac{\partial v_4}{\partial v_1} = \bar{v}_2 \exp(v_1) + \bar{v}_4 v_3 = (0.4990) (3.004) + (2.006)(0.4536) = 2.4090\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(\bar{v}_0 = \bar{v}_3 \frac{\partial v_3}{\partial v_0} + \bar{v}_1 \frac{\partial v_1}{\partial v_0} = \bar{v}_3 (- \sin v_0) + \bar{v}_1 v_{-1} = (2.2069) (-0.8912) + (2.4090)(1) = 4.3758\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(\bar{v}_{-1} = \bar{v}_1 \frac{\partial v_1}{\partial v_{-1}} = \bar{v}_1 v_0 = (2.4090) * (1.1) = 2.6500\)</td>
    </tr>
  </tbody>
</table>

<p>As a first step, we are evaluating all the un-barred quantities \(v_i\) before we begin the
backward pass, where we evaluate all the \(\bar{v}_i\). We also see that we need the values
of the child nodes to calculate the barred values. For example, to calculate \(\bar{v}_3\) 
we need the value of \(v_1\), which is a child-node of \(v_4\), when evaluating the
partial derivative \(\partial v_4 / \partial v_1\).</p>

<p>We also see that this procedure would allow us to calculate the derivatives \(\partial y / \partial v_i\) for an arbitrary number of \(v_i\)s in a single reverse pass. This is the 
exactly the behaviour that makes reverse-mode automatic differentiation so powerful in
context of gradient-based optimization. In that situation we want to take the derivative of
a scalar loss function with respect to a possible large amount of parameters.</p>

<h2 id="reverse-mode-ad-in-higher-dimensions-jacobian-vector-products">Reverse-mode AD in higher dimensions: Jacobian-Vector products</h2>

<p>We are often working in higher dimensional spaces, hidden layers in multilayer perceptrons
for example can have hundreds or even thousands of features. It is therefore instructive to take
a look how reverse-mode AD works in this setting. For this we are looking at a new example:</p>

\[\begin{align}
x \in \mathbb{R}^{n} \quad f: \mathbb{R}^{n} \rightarrow \mathbb{R}^{m} \quad g: \mathbb{R}^{m} \rightarrow \mathbb{R}^{l} \quad u \in \mathbb{R}^{m} \quad y \in \mathbb{R}^{l} \\ 
u = f(x) \quad y = g(u) = g(f(x))
\end{align}\]

<p>The computational graph for the program \(y = g(f(x))\) is just</p>

<p><img src="/assets/images/autodiff/reverse_mode_example2.png" alt="Computational graph for simple example with gradient backprop" /></p>

<p>Now the formulas for the higher-dimensional case arise naturally from the ones derived in the
previous section. Since we are discussing reverse mode, we start at the right. The
bar quantities for each element of the first hidden layer is given by:</p>

\[\begin{align}
    \bar{u}_j &amp; = \sum_{i} \bar{y}_i \frac{\partial y_i}{\partial u_j} \\
\end{align}\]

<p>Using vector notation, this equation can be written as</p>

\[\begin{align}
    \left[ \bar{y}_1 \ldots \bar{y}_l \right] 
    \left[ 
        \begin{matrix}
            \frac{\partial y_1}{\partial u_1} &amp; \ldots &amp; \frac{\partial y_1}{\partial u_m} \\ 
            \vdots                            &amp; \ddots &amp; \vdots \\ 
            \frac{\partial y_l}{\partial u_1} &amp; \ldots &amp; \frac{\partial y_l}{\partial u_m}
        \end{matrix}
        
        \right]
    &amp; = \left[ \bar{u}_1 \ldots \bar{u}_m \right]
\end{align}\]

<p>Now \(\bar{f}\) is the vector that we get as an output from that calculation and will be
propagated left-wards. We obtain it by calculating a vector-jacobian product. In practice,
one usually does not calculate the jacobian matrix, but it is more efficient to calculate
the vjp directly.</p>

<p>Moving leftwards to the graph, we use the same procedure to calculate \(\bar{x}\):</p>

\[\begin{align}
    \left[ \bar{u}_1 \ldots \bar{u}_n \right] 
    \left[ 
        \begin{matrix}
            \frac{\partial u_1}{\partial x_1} &amp; \ldots &amp; \frac{\partial u_1}{\partial x_n} \\ 
            \vdots                            &amp; \ddots &amp; \vdots \\ 
            \frac{\partial u_m}{\partial x_1} &amp; \ldots &amp; \frac{\partial u_m}{\partial x_n}
        \end{matrix}
        
        \right]
    &amp; = \left[ \bar{x}_1 \ldots \bar{x}_n \right]
\end{align}\]

<p>Combining these results we find</p>

\[\begin{align}
    \bar{x} = \bar{y} \left[  \left\{ \frac{\partial y_i}{\partial u_j} \right\}_{i,j} \right] \left[ \left\{ \frac{\partial u_i}{\partial x_j} \right\}_{r,s} \right].
\end{align}\]

<p>This equation is evaluated left-to-right, so that it requires to calculate only vector-matrix products.</p>

<p>As a final note, these vector-jacobian products are often called pullbacks in the
context of automatic differentiation software. A notation used for them is</p>

\[\begin{align}
    \bar{u} = \sum_{i} \bar{y}_i \frac{\partial y_i}{\partial u} = \mathcal{B}^{u}_{f}(\bar{y}).
\end{align}\]

<p>Let’s unpack the expression \(\mathcal{B}^{u}_{f}(\bar{y})\). The upper index denotes simply
that the pullback is a function of \(u\), since it needs the values from the forward-pass
at the node to be evaluated. The lower index \(f\) denotes that it depends on the mapping 
\(f\), since this is the mapping from \(x\) to \(u: f(x) = u\). Finally, the argument
\(\bar{y}\) denotes that the pullback needs the incoming gradient from the right.</p>

<p>In practice, automatic differentiation software uses the chain rule to split the computational
graph of a program into finer units until it can identify a pullback for a segment of the
computational graph. In some cases this may be the desired way to work and in some cases,
a user-defined backpropagation rule may be desireable.</p>

<h2 id="summary">Summary</h2>
<p>We have introduced reverse-mode automatic differentiation. By starting with a gradient
at the end of the computational graph, this mode allows to quickly calculate the
sensitivity of an output with respect to an arbitrary large amount of intermediate values.
To aid the necessary computations for this, the bar-notation has been introduced.
Finally, we motivated that reverse-mode AD only needs to calculate vector-jacobian products
and introduced the notation of pullback as the primitive object of reverse-mode AD.
And finally, please check out the references below which I used for this blog-post.</p>

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

<p><a id="1"> [1]</a>
C. Rackauckas <a href="https://mitmath.github.io/18337/lecture10/estimation_identification">18.337J/6.338J: Parallel Computing and Scientific Machine Learning</a></p>

<p><a id="2"> [2]</a>
S. Radcliffe <a href="https://sidsite.com/posts/autodiff">Autodiff in python</a></p>

<p><a id="3">[3]</a>
R. Grosse <a href="https://www.cs.toronto.edu/~rgrosse/courses/csc321_2018/slides/lec06.pdf">Lecture notes CSC321</a></p>

<p><a id="4"> [4]</a>
A. Griewank, A. Walther - Evaluating Derivatives - SIAM(2008)</p>]]></content><author><name></name></author><category term="julia" /><category term="autodiff" /><summary type="html"><![CDATA[This post is the follow-up of my post on forward mode AD. There I motivated motivated the use of automatic differentiation by noting that it is helpful for gradient-baseed optimization. In this post I will discuss how reverse mode allows us to take the derivative of a function output with respect to basically an arbitrary number of parameters in one sweep. To do this comfortably, one often evaluates local derivatives, whose use is facilitated by introducing the bar notation.]]></summary></entry><entry><title type="html">Automatic differentiation - Forward Mode</title><link href="/julia/autodiff/2021/05/10/autodiff1.html" rel="alternate" type="text/html" title="Automatic differentiation - Forward Mode" /><published>2021-05-10T14:00:00+00:00</published><updated>2021-05-10T14:00:00+00:00</updated><id>/julia/autodiff/2021/05/10/autodiff1</id><content type="html" xml:base="/julia/autodiff/2021/05/10/autodiff1.html"><![CDATA[<p>Automatic differentiation allows one to take derivatives of computer programs which
calculate numerical values. The name refers to a set of techniques that produce a 
transformed computer program which in turn calculates various derivatives of these values.</p>

<p>To see why this is extremely useful, consider a traditional data fitting problem.
We have a model \(f_\theta: \mathbb{R}^m \rightarrow \mathbb{R}^n\), where
\(\theta\) are the model parameters and observations \(y_i \in \mathbb{R}^n\)
taken at \(x_i \in \mathbb{R}^{m}\). To fit the model on the data we can
use a loss function, for example the mean-squared error</p>

\[\begin{align}
\mathcal{L} = \sum_{i} \left( f_\theta(x_i) - y_i \right)^2
\end{align}\]

<p>and tune the parameters \(\theta\) in order to minimize \(\mathcal{L}\). This process
is also called learning or solving an inverse problem.</p>

<p>A common method to tune the parameters \(\theta\) is gradient descent. Given a learning
rate \(\eta\) one iterates</p>

\[\theta \leftarrow \theta - \eta \frac{\partial \mathcal{L}}{\partial \theta}\]

<p>until \(\mathcal{L}\) is approximately at a local minimum. Applying the chain rule, we 
can expand the derivative as</p>

\[\frac{\partial \mathcal{L}}{\partial \theta} = \sum_i 2 \left( f_\theta(x_i) - y_i \right) 
\frac{\partial f_\theta(x_i)}{\partial \theta}.\]

<p>To perform the optization procedure we need to calculate the derivatives described
by the last term in the equation above. This is where automatic differentiation 
comes into play.</p>

<h1 id="a-toy-example">A Toy Example</h1>
<p>To better understand what automatic differentiation does and how it works let us consider
the toy example</p>

\[f(x_1, x_2) = x_1 x_2 \cos(x_2) \left[ \exp(x_1 x_2) - 1 \right] = y.\]

<p>In the following we will investigate how to calculate the derivative of \(f\) with respect
to its inputs \(x_1\) and \(x_2\). In the context of the previous section, we can think of
\(x_1\) as a parameter \(\theta\) that we wish to tone. While the method we present here
is as simple as possible - real-world examples of tunable models often have thousands,
millions, or even billions of parameters - it serves well to introduce the fundamental way
in which automatic differentiation works. For the work below we are following <a href="#1">[1]</a>
closely.</p>

<p>This mathematical formula for \(f\) can be decomposed into a succession of elementary functions,
like \(+\), \(\sin\), \(\exp\), \(*\). That is, we can define intermediate variables like 
\(v_1(a,b) = a \cdot b\), \(v_2(a) = \cos a\) etc, which are only defined once. Each of these
variables is also initialized by applying a simple expression to previous variables.
Applying this procedure to our toy example gives us the following sequence:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">A simple program broken up into intermediate values</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">\(v_{-1} = x_1\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_{0} = x_2\)</td>
    </tr>
  </tbody>
  <tbody>
    <tr>
      <td style="text-align: center">\(v_{1} = v_{-1} \cdot  v_{0}\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_{2} = \exp v_1\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_{3} = \cos v_0\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_{4} = v_1 \cdot v_3\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_{5} = v_2 - v_4\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_6 = v_4 v_5\)</td>
    </tr>
  </tbody>
  <tbody>
    <tr>
      <td style="text-align: center">\(y = v_6\)</td>
    </tr>
  </tbody>
</table>

<p>We now define the so-called <em>Tangential Derivative</em> \(\dot{v_i} = \partial v_i / \partial x_1\). 
For the tangential derivative, we are fixing the denominator to \(x_1\) so that it describes the sensitivity of the functions
output with respect to the given input. Let us calculate this derivative for the v’s now:</p>

\[\begin{align}
\dot{v}_1 &amp; = \frac{\partial v_1}{\partial x_1} = v_0 \frac{\partial v_{-1}}{\partial v_{-1}} = v_0 \\ 
\dot{v}_2 &amp; = \frac{\partial v_2}{\partial v_1} \dot{v}_1 = v_0 \exp v_1\\ 
\dot{v}_3 &amp; = \frac{\partial v_3}{\partial v_0} \dot{v}_0 = 0  \\ 
\dot{v}_4 &amp; = \frac{\partial v_4}{\partial v_1} \dot{v}_1 + \frac{\partial v_4}{\partial v_3} \dot{v}_3 = v_3 v_0  \\ 
\dot{v}_5 &amp; = \frac{\partial v_5}{\partial v_2} \dot{v}_2 + \frac{\partial v_5}{\partial v_4} \dot{v}_4 = \dot{v}_2 - \dot{v}_4  \\ 
\dot{v}_6 &amp; = \frac{\partial v_6}{\partial v_5} \dot{v}_5 + \frac{\partial v_6}{\partial v_4} \dot{v}_4 = \dot{v}_5 v_4 + \dot{v}_4 v_5  \\ 
\end{align}\]

<p>Note that we have to use \(\dot{v}_{-1} = 1\) and \(\dot{v}_{0} = 0\) in order to calculate \(v\dot{v}_1\). That
is, the seed determines which part of the sum is non-zero. From the expressions above we can alos see that we
will need to evaluate the derivatives starting at \(v_1\). That is, we begin at the start of the function,
calculate simple derivatives and push this calculation through. Therefore this is also called <em>forward</em> mode automatic differentiation.</p>

<p>In practice, forward mode AD is often implemented by operator overloading. An implementation
would need seed an initial derivative \(x_i = 1\) and then calculate the program and the derivatives simultaneously.
This approach works for our toy example as we see from the evaluation trace shown below:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Forward-mode AD evaluation trace</th>
      <th style="text-align: center"> </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">\(v_{-1} = x_1 = 1.0\)</td>
      <td style="text-align: center">\(\dot{v}_{-1} = 1.0\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_{0} = x_2 = 1.1\)</td>
      <td style="text-align: center">\(\dot{v}_{0} = 0.0\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_1 = v_0 v_{-1} = (1.1) (1.0) = 1.1\)</td>
      <td style="text-align: center">\(\dot{v}_{1} = v_{0} = (1.1)\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_{4} = v_1 v_3 = (1.1) (0.4536) = 0.4990\)</td>
      <td style="text-align: center">\(\dot{v}_4 = v_3 \dot{v}_1 = (0.4536) * (1.1) = 0.499\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_{5} = v_2 - v_4 = (3.004) - (0.4990) = 2.5052\)</td>
      <td style="text-align: center">\(\dot{v}_5 = \dot{v}_2 - \dot{v}_4 = 3.305 - 0.499 = 2.851\)</td>
    </tr>
    <tr>
      <td style="text-align: center">\(v_6 = v_4 v_5 = (0.4990) (2.7180) = 1.2500\)</td>
      <td style="text-align: center">\(\dot{v}_6 = v_4 \dot{v}_5 + \dot{v}_4 v_5 = (0.499) (2.851) + (0.499) (2.505) = 2.650\)</td>
    </tr>
  </tbody>
</table>

<p>It is illustrative to visualize how the values and derivatives are propagated in the computational graph.
<img src="/assets/images/autodiff/forwarddiff_graph.png" alt="Coputational graph for forward-mode autodiff" /></p>

<p>Here the intermediate values are shown in yellow and the gradient values are shown in turquoise. Note that
both, values and derivatives are propagated together from the left, the input, to the output on the right.
The implication of this is that forward-mode autodiff can calculate the sensitivities of all outputs y
with respect to one input x in a multidimensional setting.</p>

<h2 id="summary">Summary</h2>
<p>Automatic differentiation is a set of tools that allows to calculate the derivatives of
computer programs that return numerical values. Here we looked at the so-called forward mode of
automatic differentiation. It works by de-composing a program into simple functions for which
we know the derivative. We can initialize a program and set the input variable for which we want
to know the output’s derivatives for. Then we can calculate the function value and the derivative
of the output with respect to the chosen value in one sweep.</p>

<p>This works well for situations where we have a small number of inputs and a large number of outputs.
But in data-fitting problems we often end up with the reverse situation: We have a large number
of outputs and few, or maybe only a single output. In these situations one may want to use
reverse-mode automatic differentiation.</p>

<h2 id="references">References</h2>
<p><a id="1"> [1]</a>
A. Griewank, A. Walther - Evaluating Derivatives - SIAM(2008)</p>]]></content><author><name></name></author><category term="julia" /><category term="autodiff" /><summary type="html"><![CDATA[Automatic differentiation allows one to take derivatives of computer programs which calculate numerical values. The name refers to a set of techniques that produce a transformed computer program which in turn calculates various derivatives of these values.]]></summary></entry><entry><title type="html">Machine-learned preconditioners - Part 2</title><link href="/jekyll/update/2021/04/09/nn-precond-2.html" rel="alternate" type="text/html" title="Machine-learned preconditioners - Part 2" /><published>2021-04-09T05:56:22+00:00</published><updated>2021-04-09T05:56:22+00:00</updated><id>/jekyll/update/2021/04/09/nn-precond-2</id><content type="html" xml:base="/jekyll/update/2021/04/09/nn-precond-2.html"><![CDATA[<p>This post is a follow-up from the last one. But now we are focusing on solving
a non-linear system of the form</p>

\[F(x) = 0\]

<p>using a quasi-Newton method. You find the definition on
<a href="https://en.wikipedia.org/wiki/Quasi-Newton_method">Wikipedia</a>. Basically it
is Newton’s method but you replace the Jacobian with an approximation.
In this blog post we are considering a simple two-dimensional system.
Using Newton’s method for this example is trivial and leads to a converged
solution after only two or three steps, since it converges quadratically.
A quasi-Newton method on the other hand converges only linearly. And we
aim to improve on that convergence behaviour by using a machine-learned
preconditioner. Now, preconditioners are usually used to accelerate convergence of 
linear solvers, like GMRES. And I don’t claim that they are generally useful for
non-linear systems. So we do it just to see if this works in principle.</p>

<h1 id="quasi-newton-iteration">Quasi-Newton iteration</h1>

<p>We consider the simple two-dimensional system</p>

\[\begin{align}
F_1(x_1, x_2) = x_1 + \alpha x_1 x_2 - s_1 &amp; = 0 \\ 
F_2(x_1, x_2) = \beta x_1 x_2 + x_2 - s_2 &amp; = 0
\end{align}\]

<p>where \(\alpha\) and \(\beta\) are of the order 0.01 and the source terms are 
\(s_{1,2} \sim \mathcal{N}(1, 10^{-2})\). Picking the source terms from a
Normal distribution allows us to consider a large number of instances of this
problem. Later we will use this to generate training and test sets.</p>

<p>Newton’s method can be used to find the root of $F(x)$. Starting out at an
initial guess $x_0$, it updates the guess by guiding it along its gradient:
\(F(x_0 + \delta x) \approx F(x_0) + J(x) \delta x \approx 0\).  Here 
\(J^{-1}(x)\) is the inverse of an approximation on the Jacobian Matrix whose
entries are is defined as \((J(x))_{ij} = \partial F_i / \partial x_j\).
Solving for \(\delta x\) gives the update \(\delta x = J^{-1}(x) F(x)\). Now the 
Jacobian matrix of the system can be easily calculated. For the quasi-Newton 
method we consider the approximation</p>

\[\widetilde{J}(x) = \begin{pmatrix}
1 + \alpha x_2  &amp; 1 \\ 
1      &amp; 1 + \beta x_1
\end{pmatrix}\]

<p>where the off-diagonal elements of the original Jacobian have been modified 
as \(\partial F_1 / \partial x_2 = \alpha x_1 \approx 1\) and 
\(\partial F_2 / \partial x_1 = \beta x_2 \approx 1\).</p>

<p>A quasi-Newton aims to find the root of \(F(x)=0\) using the same iteration scheme
but using an approximation to the Jacobian instead
\(\begin{align}
x_{k+1} &amp; = x_k - \widetilde{J}^{-1}(x) f(x).
\end{align}\)</p>

<p>If you use the true Jacobian you have Newton’s method and the rate of 
convergence is quadratic. If you don’t, you have a quasi-newton method and the 
rate of convergence is linear. How can we accelerate convergence in this
case?</p>

<p>To accelerate convergence, we can use a Preconditioner \(P^{-1}\). This is
a invertible matrix that we squeeze into the update by inserting a one:</p>

\[\begin{align}
J(x_k) \delta x_k &amp;= -F(x_k) \Leftrightarrow \\
\left( J(x_k)P^{-1} \right) \left( P \delta x_k \right) &amp;= -F(x_k)
\end{align}\]

<p>The update from \(x_k \rightarrow x_{k+1}\) then needs to calculated in
two steps.</p>

<ul>
  <li>Calculate \(\delta w = \left( \widetilde{J} \right)^{-1} P^{-1} \left(F(x_k) \right)\)</li>
  <li>Calculate \(\delta x_{k} = P^{-1} \delta w\).</li>
  <li>Update \(x_{k+1} = x_{k} + \delta_x\).</li>
</ul>

<p>That is, we apply \(P^{-1}\) twice. A closed form of \(P\) is not required.
Next we will see how to implement \(P^{-1}\) as a neural network and
how to train it using differentiable code.</p>

<h1 id="neural-network-preconditioner">Neural-Network preconditioner</h1>
<p>We parameterize the preconditioner with a Multilayer perceptron as 
\(P^{-1}_{\theta}: \mathbb{R}^{m} \rightarrow \mathbb{R}^2\):</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="n">Flux</span>
<span class="n">dim</span> <span class="o">=</span> <span class="mi">2</span>
<span class="n">Pinv</span> <span class="o">=</span> <span class="n">Chain</span><span class="x">(</span><span class="n">Dense</span><span class="x">(</span><span class="mi">2</span><span class="o">*</span><span class="n">dim</span> <span class="o">+</span> <span class="mi">2</span><span class="x">,</span> <span class="mi">50</span><span class="x">,</span> <span class="n">celu</span><span class="x">),</span> <span class="n">Dense</span><span class="x">(</span><span class="mi">50</span><span class="x">,</span> <span class="mi">50</span><span class="x">,</span> <span class="n">celu</span><span class="x">),</span> <span class="n">Dense</span><span class="x">(</span><span class="mi">50</span><span class="x">,</span> <span class="n">dim</span><span class="x">))</span>
</code></pre></div></div>

<p>Here we allow for \(m &gt; 2\) to pass additional inputs to the Network. We are
also using CeLU activation functions to avoid having a non-differentiable point
at \(x=0\) and we are choosing a simple 3-layer feed-forward architecture to start
out with.</p>

<p>While this gives us a parameterization we now have to find out how to train
the network. The task of the preconditioner is to minimize the residual
after \(n_\mathrm{i}\) iterations of Newtons method. The following loss
function tells us how well \(P^{-1}\) is doing over a number of examples
\(F(x) = 0\), where the RHS terms \(s_1, s_2 \sim \mathcal{N}\left(2, 0.01\right)\).</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sqnorm</span><span class="x">(</span><span class="n">x</span><span class="x">)</span> <span class="o">=</span> <span class="n">sum</span><span class="x">(</span><span class="n">abs2</span><span class="x">,</span> <span class="n">x</span><span class="x">)</span> 


<span class="k">function</span><span class="nf"> loss2</span><span class="x">(</span><span class="n">batch</span><span class="x">)</span>
<span class="c"># batch is a 2d-array. dim1: index within batch. dim2: indices the batch</span>
    <span class="c"># Loss of the current sample. Defined as the norm of f(x) after nᵢ Newton iterations</span>
    <span class="n">loss_local</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="c"># Batch-size</span>
    <span class="n">batch_size</span> <span class="o">=</span> <span class="n">size</span><span class="x">(</span><span class="n">batch</span><span class="x">)[</span><span class="mi">2</span><span class="x">]</span>
    <span class="c"># Iterate over source vectors in the current mini-batch</span>
    <span class="k">for</span> <span class="n">sidx</span> <span class="k">in</span> <span class="mi">1</span><span class="o">:</span><span class="n">batch_size</span>
        <span class="c"># Grab the current source terms </span>
        <span class="n">s</span> <span class="o">=</span> <span class="n">batch</span><span class="x">[</span><span class="o">:</span><span class="x">,</span> <span class="n">sidx</span><span class="x">]</span>
        <span class="c"># Define a RHS</span>
        <span class="k">function</span><span class="nf"> f</span><span class="x">(</span><span class="n">x</span><span class="x">)</span> 
            <span class="n">F</span> <span class="o">=</span> <span class="n">Zygote</span><span class="o">.</span><span class="n">Buffer</span><span class="x">(</span><span class="n">zeros</span><span class="x">(</span><span class="n">dim</span><span class="x">))</span>
            <span class="n">F</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span> <span class="o">=</span> <span class="n">x</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span> <span class="o">+</span> <span class="n">α</span><span class="o">*</span><span class="n">x</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span><span class="o">*</span><span class="n">x</span><span class="x">[</span><span class="mi">2</span><span class="x">]</span> <span class="o">-</span> <span class="n">s</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span>
            <span class="n">F</span><span class="x">[</span><span class="mi">2</span><span class="x">]</span> <span class="o">=</span> <span class="n">β</span> <span class="o">*</span> <span class="n">x</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span> <span class="o">*</span> <span class="n">x</span><span class="x">[</span><span class="mi">2</span><span class="x">]</span> <span class="o">+</span> <span class="n">x</span><span class="x">[</span><span class="mi">2</span><span class="x">]</span> <span class="o">-</span> <span class="n">s</span><span class="x">[</span><span class="mi">2</span><span class="x">]</span>
            <span class="k">return</span> <span class="n">copy</span><span class="x">(</span><span class="n">F</span><span class="x">)</span>
        <span class="k">end</span>

        <span class="c"># run num_steps of Newton iteration for the current RHS</span>
        <span class="n">sol</span> <span class="o">=</span> <span class="n">picard</span><span class="x">(</span><span class="n">x</span><span class="x">,</span> <span class="n">x0</span><span class="x">,</span> <span class="n">Pinv</span><span class="x">,</span> <span class="n">s</span><span class="x">,</span> <span class="mi">1</span><span class="x">)</span>
        <span class="n">loss_local</span> <span class="o">+=</span> <span class="n">norm</span><span class="x">(</span><span class="n">f</span><span class="x">(</span><span class="n">sol</span><span class="x">))</span>
    <span class="k">end</span>

    <span class="k">return</span> <span class="n">log</span><span class="x">(</span><span class="n">loss_local</span><span class="x">)</span> <span class="o">/</span> <span class="n">batch_size</span>
<span class="k">end</span>
</code></pre></div></div>

<p>One has to be careful in evaluating the preconditioner within calls to the
Picard iteration. After each iteration, the input \(f(x)\) will decrease by
an order of magnitude. Neural Networks work best of the input is of order
one on the other hand. Thus we need to find a way to provide input of order
one to \(P^{-1}\) at each iteration. In the routine below the input is</p>

<ul>
  <li>\(f(x) / \vert f(x) \vert_{2}\) - Input \(f(x)\), scaled to its L² norm</li>
  <li>\(s\) - The source terms in \(F(x) = 0\).</li>
  <li>\(\log \vert f(x) \vert\) - Gives the order of magnitude of the input</li>
  <li>\(i\) - The current iteration</li>
</ul>

<p>The first term represents the desired input, re-scaled to be order unity, and
the logarithm \(\log \vert f(x) \vert\) estimates the order of magnitude of this 
term. For example for \(\vert f(x) \vert = 10^{-5} \rightarrow \log \vert f(x) \vert \approx -11.5\). I also pass the source term \(s\) and the 
current Picard iteration \(i\) into \(P^{-1}\).</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span><span class="nf"> picard</span><span class="x">(</span><span class="n">f</span><span class="x">,</span> <span class="n">x</span><span class="x">,</span> <span class="n">P⁻¹</span><span class="x">,</span> <span class="n">s</span><span class="x">,</span> <span class="n">num_steps</span><span class="x">)</span>
<span class="c"># Run num_Steps of Newton iteration</span>
<span class="c"># P⁻¹: Preconditioner</span>
<span class="c"># s: source terms in F(x) + s = 0</span>

    <span class="k">for</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">1</span><span class="o">:</span><span class="n">num_steps</span>
        <span class="c"># P⁻¹ receives the following inputs</span>
        <span class="c"># f(x) / norm(f(x)) : Scaled input</span>
        <span class="c"># s                 : source term</span>
        <span class="c"># log(norm(f(x)))   : Order of magnitude of f(x)</span>
        <span class="c"># i                 : Iteration number I</span>
        <span class="n">δw</span> <span class="o">=</span> <span class="n">pinv</span><span class="x">(</span><span class="n">J</span><span class="x">(</span><span class="n">x</span><span class="x">))</span> <span class="o">*</span> <span class="n">P⁻¹</span><span class="x">(</span><span class="n">vcat</span><span class="x">(</span><span class="n">f</span><span class="x">(</span><span class="n">x</span><span class="x">)</span> <span class="o">/</span> <span class="n">norm</span><span class="x">(</span><span class="n">f</span><span class="x">(</span><span class="n">x</span><span class="x">)),</span> <span class="n">s</span><span class="x">,</span> <span class="n">log</span><span class="x">(</span><span class="n">norm</span><span class="x">(</span><span class="n">f</span><span class="x">(</span><span class="n">x</span><span class="x">))),</span> <span class="n">i</span><span class="x">))</span>
        <span class="n">δx</span> <span class="o">=</span> <span class="n">P⁻¹</span><span class="x">(</span><span class="n">vcat</span><span class="x">(</span><span class="n">δw</span> <span class="o">/</span> <span class="n">norm</span><span class="x">(</span><span class="n">δw</span><span class="x">),</span> <span class="n">s</span><span class="x">,</span> <span class="n">log</span><span class="x">(</span><span class="n">norm</span><span class="x">(</span><span class="n">δw</span><span class="x">)),</span> <span class="n">i</span><span class="x">))</span>
        <span class="n">x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">-</span> <span class="n">δx</span>
    <span class="k">end</span>
    <span class="k">return</span> <span class="n">x</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I am generating 100 training samples which I divide into 10 mini-batches
for training. Similarly, the test-set is 100 examples. Here I don’t use
a validation set but instead only consider the loss on the training set.
While training I am monitoring the loss on the training set. If it has not
decreased for 5 epochs, I’m halfing the learning rate.</p>

<p>In order to optimize the neural network as to give a smaller residual
after one iteration one needs to calculate the gradients of the loss
with respect to the parameters of the neural network. Zygote allows us
to do this using only a single call to gradient:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   
<span class="c"># Start training loop</span>
<span class="n">loss_vec</span> <span class="o">=</span> <span class="n">zeros</span><span class="x">(</span><span class="n">num_epochs</span><span class="x">)</span>
<span class="n">η_vec</span> <span class="o">=</span> <span class="n">zeros</span><span class="x">(</span><span class="n">num_epochs</span><span class="x">)</span>
<span class="c"># Counts how many steps we haven't improved the loss function.</span>
<span class="n">stall_thresh</span> <span class="o">=</span> <span class="mi">5</span>

<span class="k">for</span> <span class="n">epoch</span> <span class="k">in</span> <span class="mi">1</span><span class="o">:</span><span class="n">num_epochs</span>
    <span class="c"># Randomly shuffly the training examples</span>
    <span class="n">batch_idx</span> <span class="o">=</span> <span class="n">shuffle</span><span class="x">(</span><span class="mi">1</span><span class="o">:</span><span class="n">num_train</span><span class="x">)</span>
    <span class="k">for</span> <span class="n">batch</span> <span class="k">in</span> <span class="mi">1</span><span class="o">:</span><span class="n">num_batch</span>
        <span class="c"># Get random indices for the current batch</span>
        <span class="n">this_batch</span> <span class="o">=</span> <span class="n">batch_idx</span><span class="x">[(</span><span class="n">batch</span> <span class="o">-</span> <span class="mi">1</span><span class="x">)</span> <span class="o">*</span> <span class="n">batch_size</span> <span class="o">+</span> <span class="mi">1</span><span class="o">:</span><span class="n">batch</span> <span class="o">*</span> <span class="n">batch_size</span><span class="x">]</span>
        <span class="n">grads</span> <span class="o">=</span> <span class="n">Flux</span><span class="o">.</span><span class="n">gradient</span><span class="x">(</span><span class="n">Flux</span><span class="o">.</span><span class="n">params</span><span class="x">(</span><span class="n">P⁻¹</span><span class="x">))</span> <span class="k">do</span> 
            <span class="n">l</span> <span class="o">=</span> <span class="n">loss2</span><span class="x">(</span><span class="n">source_train</span><span class="x">[</span><span class="o">:</span><span class="x">,</span> <span class="n">this_batch</span><span class="x">])</span>
        <span class="k">end</span>
        <span class="k">for</span> <span class="n">p</span> <span class="k">in</span> <span class="n">Flux</span><span class="o">.</span><span class="n">params</span><span class="x">(</span><span class="n">P⁻¹</span><span class="x">)</span>
            <span class="n">Flux</span><span class="o">.</span><span class="n">Optimise</span><span class="o">.</span><span class="n">update!</span><span class="x">(</span><span class="n">p</span><span class="x">,</span> <span class="o">-</span><span class="n">η</span> <span class="o">*</span> <span class="n">grads</span><span class="x">[</span><span class="n">p</span><span class="x">])</span>
        <span class="k">end</span>
        <span class="n">Zygote</span><span class="o">.</span><span class="n">ignore</span><span class="x">()</span> <span class="k">do</span> 
            <span class="n">loss_vec</span><span class="x">[</span><span class="n">epoch</span><span class="x">]</span> <span class="o">=</span> <span class="n">loss2</span><span class="x">(</span><span class="n">source_train</span><span class="x">[</span><span class="o">:</span><span class="x">,</span> <span class="n">this_batch</span><span class="x">])</span>
            <span class="n">η_vec</span><span class="x">[</span><span class="n">epoch</span><span class="x">]</span> <span class="o">=</span> <span class="n">η</span>
            <span class="k">if</span> <span class="n">mod</span><span class="x">(</span><span class="n">epoch</span><span class="x">,</span> <span class="mi">10</span><span class="x">)</span> <span class="o">==</span> <span class="mi">0</span>
                <span class="n">println</span><span class="x">(</span><span class="s">"Epoch </span><span class="si">$(epoch) </span><span class="s">loss </span><span class="si">$</span><span class="s">(loss_vec[epoch])  η </span><span class="si">$</span><span class="s">(η)"</span><span class="x">)</span>
            <span class="k">end</span>
        <span class="k">end</span>
    <span class="k">end</span>

    <span class="k">if</span> <span class="x">(</span><span class="n">epoch</span> <span class="o">&gt;</span> <span class="n">stall_thresh</span><span class="x">)</span> <span class="o">&amp;&amp;</span> <span class="x">(</span><span class="n">loss_vec</span><span class="x">[</span><span class="n">epoch</span><span class="x">]</span> <span class="o">&gt;</span> <span class="n">mean</span><span class="x">(</span><span class="n">loss_vec</span><span class="x">[</span><span class="n">epoch</span> <span class="o">-</span> <span class="n">stall_thresh</span><span class="o">:</span><span class="n">epoch</span><span class="x">]))</span>
        <span class="kd">global</span> <span class="n">η</span> <span class="o">*=</span> <span class="mf">0.5</span>
        <span class="n">println</span><span class="x">(</span><span class="s">"    new η = </span><span class="si">$</span><span class="s">(η)   "</span><span class="x">)</span>
    <span class="k">end</span>
    <span class="k">if</span> <span class="n">η</span> <span class="o">&lt;</span> <span class="mf">1e-10</span>
        <span class="n">break</span> 
    <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Training the preconditioner is a bit tricky. I had to play quiet a bit with
the layout of the network, the learning rate, and the batch size to get useful
results. One setup that works is</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">x0</span> <span class="o">=</span> <span class="x">[</span><span class="mf">1.9</span><span class="x">;</span> <span class="mf">2.2</span><span class="x">]</span>
<span class="n">α</span> <span class="o">=</span> <span class="mf">0.03</span>
<span class="n">β</span> <span class="o">=</span> <span class="o">-</span><span class="mf">0.07</span>

<span class="n">Pinv</span> <span class="o">=</span> <span class="n">Chain</span><span class="x">(</span><span class="n">Dense</span><span class="x">(</span><span class="mi">2</span><span class="o">*</span><span class="n">dim</span> <span class="o">+</span> <span class="mi">2</span><span class="x">,</span> <span class="mi">100</span><span class="x">,</span> <span class="n">celu</span><span class="x">),</span> <span class="n">Dense</span><span class="x">(</span><span class="mi">100</span><span class="x">,</span> <span class="mi">200</span><span class="x">,</span> <span class="n">celu</span><span class="x">),</span> <span class="n">Dense</span><span class="x">(</span><span class="mi">200</span><span class="x">,</span> <span class="mi">100</span><span class="x">,</span> <span class="n">celu</span><span class="x">),</span> <span class="n">Dense</span><span class="x">(</span><span class="mi">100</span><span class="x">,</span> <span class="n">dim</span><span class="x">))</span>

<span class="n">num_train</span> <span class="o">=</span> <span class="mi">100</span>
<span class="n">num_epochs</span> <span class="o">=</span> <span class="mi">100</span>
<span class="n">num_batch</span> <span class="o">=</span> <span class="mi">10</span>
<span class="n">num_test</span> <span class="o">=</span> <span class="mi">100</span>
</code></pre></div></div>

<p>Let’s look at the loss on the training set:</p>

<p><img src="/assets/images/nn_precond/training_loss.png" alt="Training loss" /></p>

<p>For the first few epochs, there is no significant training. But around epochs
10 and then 20, there are significant drops. After 20, training proceeds uniform
and the loss function flattens out around epoch 60. Learning-rate scheduling
is essential here, it is halfed around the steep drops. Finally we end up with
an L² loss of about exp(-4)≈0.01 per sample.</p>

<p>We can evaluate the performance of the learned preconditioner by testing
it on unseen examples:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">or</span> <span class="n">idx</span> <span class="k">in</span> <span class="mi">1</span><span class="o">:</span><span class="n">num_test</span>
    <span class="c"># Load the source vector from the test set</span>
    <span class="n">s</span> <span class="o">=</span> <span class="n">source_test</span><span class="x">[</span><span class="o">:</span><span class="x">,</span> <span class="n">idx</span><span class="x">]</span>
    <span class="c"># Define the non-linear system with the current source vector</span>
    <span class="k">function</span><span class="nf"> f</span><span class="x">(</span><span class="n">x</span><span class="x">)</span>
        <span class="n">F</span> <span class="o">=</span> <span class="n">zeros</span><span class="x">(</span><span class="n">dim</span><span class="x">)</span>
        <span class="n">F</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span> <span class="o">=</span> <span class="n">x</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span> <span class="o">+</span> <span class="n">α</span> <span class="o">*</span> <span class="n">x</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span> <span class="o">*</span> <span class="n">x</span><span class="x">[</span><span class="mi">2</span><span class="x">]</span> <span class="o">-</span> <span class="n">s</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span>
        <span class="n">F</span><span class="x">[</span><span class="mi">2</span><span class="x">]</span> <span class="o">=</span> <span class="n">β</span> <span class="o">*</span> <span class="n">x</span><span class="x">[</span><span class="mi">1</span><span class="x">]</span> <span class="o">*</span> <span class="n">x</span><span class="x">[</span><span class="mi">2</span><span class="x">]</span> <span class="o">+</span> <span class="n">x</span><span class="x">[</span><span class="mi">2</span><span class="x">]</span> <span class="o">-</span> <span class="n">s</span><span class="x">[</span><span class="mi">2</span><span class="x">]</span>
        <span class="k">return</span> <span class="n">F</span>
    <span class="k">end</span>

    <span class="c"># Run 1 iteration with ML preconditioner, store solution after 1 iteration in x1_ml</span>
    <span class="n">x1_ml</span> <span class="o">=</span> <span class="n">newton</span><span class="x">(</span><span class="n">f</span><span class="x">,</span> <span class="n">x0</span><span class="x">,</span> <span class="n">P⁻¹</span><span class="x">,</span> <span class="n">s</span><span class="x">,</span> <span class="mi">1</span><span class="x">)</span>
    <span class="c"># Run 1 iteration without preconditioner, store solution after 1 iteration in x1_no</span>
    <span class="n">x1_no</span> <span class="o">=</span> <span class="n">newton_no</span><span class="x">(</span><span class="n">f</span><span class="x">,</span> <span class="n">x0</span><span class="x">,</span> <span class="mi">1</span><span class="x">)</span>

    <span class="c"># Run 5 more newton iterations, starting at x1_ml</span>
    <span class="n">vsteps_ml</span> <span class="o">=</span> <span class="n">map</span><span class="x">(</span><span class="n">n</span> <span class="o">-&gt;</span> <span class="n">norm</span><span class="x">(</span><span class="n">f</span><span class="x">(</span><span class="n">newton_no</span><span class="x">(</span><span class="n">f</span><span class="x">,</span> <span class="n">x1_ml</span><span class="x">,</span> <span class="n">n</span><span class="x">))),</span> <span class="mi">0</span><span class="o">:</span><span class="mi">5</span><span class="x">)</span>
    <span class="c"># Run 5 more newton iterations, starting at x1_no</span>
    <span class="n">vsteps_no</span> <span class="o">=</span> <span class="n">map</span><span class="x">(</span><span class="n">n</span> <span class="o">-&gt;</span> <span class="n">norm</span><span class="x">(</span><span class="n">f</span><span class="x">(</span><span class="n">newton_no</span><span class="x">(</span><span class="n">f</span><span class="x">,</span> <span class="n">x1_no</span><span class="x">,</span> <span class="n">n</span><span class="x">))),</span> <span class="mi">0</span><span class="o">:</span><span class="mi">5</span><span class="x">)</span>
<span class="k">end</span>

</code></pre></div></div>

<p>The first iteration is performed using the preconditioner, followed by 5
iterations without preconditioning. This is compared to the norm of the
residuals in 6 iterations performed without preconditioner. The plot below
shows the convergence history of 100 test samples. Green line show ML-preconditioned iterations, red lines show the iteration history without
peconditioner.</p>

<p><img src="/assets/images/nn_precond/100-200-100.png" alt="Residual Loss" /></p>

<p>We  find that the Neural Network can act as a preconditioner in the first iteration.
The residual norm after one iteration shows large scatter, almost two orders of
maagnitude. After one accelerated initial step, the iteration continues with
linear rate of convergence for all test samples. That is at the same rate as the
un-preconditioned samples, shown in red here.</p>

<p>In summary, yes, we can use a machine-learned preconditioner to accelerate
Picard iteration to solve non-linear systems. It is a bit tricky to learn and
I only showed how to do the first iteration. So there are still many details that
have not been explored. I would also like to add that for the case here, Newton’s
method is the way to go. It has a quadratic rate of convergence - much faster than
the linear rate of Picard iteration. For the samples here, residuals are machine precision after about 3 to 4 iterations. But if that is not an option, using a
Neural Network can accelerate your Picard iteration.</p>]]></content><author><name></name></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[This post is a follow-up from the last one. But now we are focusing on solving a non-linear system of the form]]></summary></entry><entry><title type="html">Making sense of Julia’s Zygote</title><link href="/jekyll/update/2021/03/02/julias-zygote.html" rel="alternate" type="text/html" title="Making sense of Julia’s Zygote" /><published>2021-03-02T15:56:22+00:00</published><updated>2021-03-02T15:56:22+00:00</updated><id>/jekyll/update/2021/03/02/julias-zygote</id><content type="html" xml:base="/jekyll/update/2021/03/02/julias-zygote.html"><![CDATA[<p>Soon comes a blog-post on Zygote, adjoints, backprop and such.</p>]]></content><author><name></name></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[Soon comes a blog-post on Zygote, adjoints, backprop and such.]]></summary></entry><entry><title type="html">Machine-learned preconditioners - Part 1</title><link href="/jekyll/update/2021/03/02/neural-networks-and-iterative-solvers.html" rel="alternate" type="text/html" title="Machine-learned preconditioners - Part 1" /><published>2021-03-02T15:56:22+00:00</published><updated>2021-03-02T15:56:22+00:00</updated><id>/jekyll/update/2021/03/02/neural-networks-and-iterative-solvers</id><content type="html" xml:base="/jekyll/update/2021/03/02/neural-networks-and-iterative-solvers.html"><![CDATA[<h1 id="what-is-a-preconditioner">What is a preconditioner?</h1>

<p>A common task in scientific computing is to solve a system of linear equations</p>

\[Ax = b\]

<p>with a matrix \(A \in \mathcal{R}^{d \times d}\) that gives the coefficients of the 
system, a right-hand-side vector \(b \in \mathcal{R}^{d}\),
and a solution vector \(x \in \mathcal{R}^{d}\).</p>

<p>Instead of solving the linear problem directly, one often solves 
the preconditioned problem. We write this problem by inserting a one in the
linear system and re-parenthesing:</p>

<p>\(\left( A P^{-1} \right) \left( P x \right) = b\)
In this equation we have changed the coefficient matrix from \(A\) to
\(A P^{-1}\) and the solution vector is now \(P x\) instead of \(x\). To
retrieve the original vector, simply calculate \(x = P^{-1}y\). The matrix
\(P^{-1}\) is called the preconditioner.</p>

<p>The dependence of \(P^{-1}\) on \(x\) is by choice. There is no deeper reason
for why the preconditioner should depend on the initial guess for the iteraion.
We choose to include this dependency here to foreshadow later applications, where
such a dependency may be useful. In practice, this choice here is rather limiting.
Since the matrix \(A_0 P^{-1}\) needs to be positive definite, and by construction
only \(\widetilde{b}\) is positive definite, the method as constructed here is only
valid for \(x \approx 0\).</p>

<p>The Gauss-Seidel method is an iterative method to find the solution of
a linear system. Given an initial guess \(x_0\), update the entries of this
vector following an iterative scheme and after some, or many, iterations, \(x_0\)
solves the equation \(A x_0 = b\). 
<a href="https://en.wikipedia.org/wiki/Gauss%E2%80%93Seidel_method">Wikipedia</a> has a nice 
and instructive page on this scheme.</p>

<p>A good preconditioner gives you a converged solution after fewer iterations.
In other words, with a good preconditioner you are closer to the true solution
of the linear system after N iterations than you are with a bad preconditioner.
Choosing a good preconditioner depends on the problem at hand and can become
a dark art. Let’s make it even more dark and train a neural network to be
a preconditioner.</p>

<h1 id="modeling-preconditioner-as-a-neural-network">Modeling preconditioner as a neural network</h1>

<p>As a first step, let’s model \(P^{-1}\) as a multi-layer perceptron 
(technically a single-layer):</p>

\[P^{-1} = \sigma \left( W x + \widetilde{b} \right)\]

<p>with a weight matrix \(W \in \mathcal{R}^{d^2 \times d}\), a bias
\(\widetilde{b} \in \mathcal{R}^{d^2}\), and a ReLU \(\sigma\). With
\(x \in \mathcal{R}^{d}\) the matrix dimensions are chosen such that \(P^{-1}\)
is of dimension \(d^2\). Simply reshape it to \(d \times d\) to have it act like
a matrix.</p>

<p>Here I’m discussing a simple test case and am working with the Gauss-Seidel
iterative solver. For real problem one would probably use a different iterative
algorithm, but it serves as a proof-of-concept. Anyway, for Gauss-Seidel to work,
the system coefficient matrix \(AP^{-1}\) needs to be positive-definite. 
How do we do this for our neural network? A simple hack is to consider only 
vectors whose entries are close to zero.</p>

<p>That way we get away with requiring only \(\widetilde{b}\) to be
positive-definite. We get the last property by letting 
\(\widetilde{b} = b_0 b_0^{T}\) and sampling the entries of \(b_0\) as
\(b_{ij} \sim \mathcal{N}(0, 1)\). In a similar fashion, we sample the entries
of the weight-matrix \(W\) as \(w_{i,j} \sim \mathcal{N}(0,1)\).</p>

<h1 id="how-can-we-train-a-preconditioner">How can we train a preconditioner</h1>

<p>To make this preconditioner useful, the weights \(W\) and bias term \(b\) need
to be optimized such that the residual after \(N\) iterations is as small
as possible. And this needs to be true for all vectors from a training set.
Using automatic differentiation, we can train \(W\) and \(b\) using gradient
descent like this:</p>

<ul>
  <li>Pick an initial guess \(x_0\) with \(x_{0,i} \sim \mathcal{N}(0, 0.01)\)</li>
  <li>Build the preconditioner \(P^{-1} = \sigma(W, x_0, \widetilde{b})\)</li>
  <li>Calculate 5 Gauss-Seidel steps. Let’s call the solution here \(y_5\).</li>
  <li>Un-apply the preconditioner: \(x_5 = P^{-1} y_5\)</li>
  <li>Calculate the distance to the true solution \(\mathcal{L} = \frac{1}{N} \sum_{i=1}^{N} \left( x_{\text{true}, i} - x_{5,i} \right)^2\)</li>
  <li>Calculate the gradients \(\nabla_{W} \mathcal{L}\) and
\(\nabla_{B} \mathcal{L}\)</li>
  <li>Update the weight matrix and bias using gradient descent: \(W \leftarrow W - \alpha \nabla_{W} \mathcal{L}\) and \(b \leftarrow b - \alpha \nabla_{b} \mathcal{L}\). Here \(\alpha\) is the learning rate</li>
</ul>

<p>The approach here is to directly back-propagate from the loss function \(\mathcal{L}\), through
the numerical solver, to the parameters of the preconditioner \(W\) and \(b\).
We are not working with an offline training and test-data set, but the data is
taken directly from the numerical calculations. This way we directly capture the
reaction of the numerical solver to updates proposed by gradient descent.</p>

<p>The derivatives \(\nabla_\theta \mathcal{L}\) can be calculate using automatic differentiation
packges, such as <a href="https://fluxml.ai/Zygote.jl/latest/">Zygote</a>.</p>

<h3 id="implementation">Implementation</h3>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c"># Test differentiation through control flow</span>

<span class="c"># Use a iterative conjugate solver with preconditioner</span>

<span class="k">using</span> <span class="n">Random</span>
<span class="k">using</span> <span class="n">Zygote</span>
<span class="k">using</span> <span class="n">LinearAlgebra</span>
<span class="k">using</span> <span class="n">Distributions</span>
<span class="k">using</span> <span class="n">NNlib</span>
</code></pre></div></div>

<p>I copy-and-pasted the Gauss-Seidel code from wikipedia:</p>
<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span><span class="nf"> gauss_seidel</span><span class="x">(</span><span class="n">A</span><span class="x">,</span> <span class="n">b</span><span class="x">,</span> <span class="n">x</span><span class="x">,</span> <span class="n">niter</span><span class="x">)</span>
    <span class="c"># This is from https://en.wikipedia.org/wiki/Gauss%E2%80%93Seidel_method</span>
    <span class="n">x_int</span> <span class="o">=</span> <span class="n">Zygote</span><span class="o">.</span><span class="n">Buffer</span><span class="x">(</span><span class="n">x</span><span class="x">)</span>
    <span class="n">x_int</span><span class="x">[</span><span class="o">:</span><span class="x">]</span> <span class="o">=</span> <span class="n">x</span><span class="x">[</span><span class="o">:</span><span class="x">]</span>
    <span class="k">for</span> <span class="n">n</span> <span class="n">∈</span> <span class="mi">1</span><span class="o">:</span><span class="n">niter</span>
        <span class="k">for</span> <span class="n">j</span> <span class="n">∈</span> <span class="mi">1</span><span class="o">:</span><span class="n">size</span><span class="x">(</span><span class="n">A</span><span class="x">,</span> <span class="mi">1</span><span class="x">)</span>
            <span class="n">x_int</span><span class="x">[</span><span class="n">j</span><span class="x">]</span> <span class="o">=</span> <span class="x">(</span><span class="n">b</span><span class="x">[</span><span class="n">j</span><span class="x">]</span> <span class="o">-</span> <span class="n">A</span><span class="x">[</span><span class="n">j</span><span class="x">,</span> <span class="o">:</span><span class="x">]</span><span class="err">'</span> <span class="o">*</span> <span class="n">x_int</span><span class="x">[</span><span class="o">:</span><span class="x">]</span> <span class="o">+</span> <span class="n">A</span><span class="x">[</span><span class="n">j</span><span class="x">,</span> <span class="n">j</span><span class="x">]</span> <span class="o">*</span> <span class="n">x_int</span><span class="x">[</span><span class="n">j</span><span class="x">])</span> <span class="o">/</span> <span class="n">A</span><span class="x">[</span><span class="n">j</span><span class="x">,</span> <span class="n">j</span><span class="x">]</span>
        <span class="k">end</span>
    <span class="k">end</span>
    <span class="k">return</span> <span class="n">copy</span><span class="x">(</span><span class="n">x_int</span><span class="x">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The block below sets things up.</p>
<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="n">Random</span><span class="o">.</span><span class="n">seed!</span><span class="x">(</span><span class="mi">1</span><span class="x">)</span>
<span class="n">dim</span> <span class="o">=</span> <span class="mi">4</span>

<span class="c"># Define a matrix</span>
<span class="n">A0</span> <span class="o">=</span> <span class="x">[</span><span class="mf">10.0</span> <span class="o">-</span><span class="mf">1.0</span> <span class="mf">2.0</span> <span class="mf">0.0</span><span class="x">;</span> <span class="o">-</span><span class="mi">1</span> <span class="mi">11</span> <span class="o">-</span><span class="mi">1</span> <span class="mi">3</span><span class="x">;</span> <span class="mi">2</span> <span class="o">-</span><span class="mi">1</span> <span class="mi">10</span> <span class="o">-</span><span class="mi">1</span><span class="x">;</span> <span class="mi">0</span> <span class="mi">3</span> <span class="o">-</span><span class="mi">1</span> <span class="mi">8</span><span class="x">]</span>
<span class="c"># Define the RHS</span>
<span class="n">b0</span> <span class="o">=</span> <span class="x">[</span><span class="mf">6.0</span><span class="x">;</span> <span class="mf">25.0</span><span class="x">;</span> <span class="o">-</span><span class="mf">11.0</span><span class="x">;</span> <span class="mf">15.0</span><span class="x">]</span>
<span class="c"># This is the true solution</span>
<span class="n">x_true</span> <span class="o">=</span> <span class="x">[</span><span class="mf">1.0</span><span class="x">,</span> <span class="mf">2.0</span><span class="x">,</span> <span class="o">-</span><span class="mf">1.0</span><span class="x">,</span> <span class="mf">1.0</span><span class="x">]</span>

<span class="c"># Define the size of the training and test set</span>
<span class="n">N_train</span> <span class="o">=</span> <span class="mi">100</span>
<span class="n">N_test</span> <span class="o">=</span> <span class="mi">10</span>

<span class="c"># Define an initial state. Draw this from a narrow distribution around zero</span>
<span class="c"># We need to do this so that the preconditioner eigenvalues are positive</span>
<span class="n">x0</span> <span class="o">=</span> <span class="n">rand</span><span class="x">(</span><span class="n">Normal</span><span class="x">(</span><span class="mf">0.0</span><span class="x">,</span> <span class="mf">0.01</span><span class="x">),</span> <span class="x">(</span><span class="n">N_train</span><span class="x">,</span> <span class="n">dim</span><span class="x">))</span>

<span class="c"># Define an MLP. This will later be our preconditioner</span>
<span class="c"># The output should be a matrix and we work with 2dim as the size for the MLP</span>
<span class="n">W</span> <span class="o">=</span> <span class="n">rand</span><span class="x">(</span><span class="n">dim</span><span class="o">*</span><span class="n">dim</span><span class="x">,</span> <span class="n">dim</span><span class="x">)</span>
<span class="c"># For Gauss-Seidel to work, the matrix A*P⁻¹ needs to be positive semi-definite.</span>
<span class="n">bmat</span> <span class="o">=</span> <span class="n">rand</span><span class="x">(</span><span class="n">dim</span><span class="x">,</span> <span class="n">dim</span><span class="x">)</span>
<span class="c"># We know that any matrix A*A' is positive semi-definite</span>
<span class="n">bvec</span> <span class="o">=</span> <span class="n">reshape</span><span class="x">(</span><span class="n">bmat</span> <span class="o">*</span> <span class="n">bmat</span><span class="err">'</span><span class="x">,</span> <span class="x">(</span><span class="n">dim</span> <span class="o">*</span> <span class="n">dim</span><span class="x">))</span>
<span class="c"># Now Wx + bmat is positive semi-definite if x is very small</span>
<span class="n">P</span><span class="x">(</span><span class="n">x</span><span class="x">,</span> <span class="n">W</span><span class="x">,</span> <span class="n">b</span><span class="x">)</span> <span class="o">=</span> <span class="n">NNlib</span><span class="o">.</span><span class="n">relu</span><span class="o">.</span><span class="x">(</span><span class="n">reshape</span><span class="x">(</span><span class="n">W</span><span class="o">*</span><span class="n">x</span> <span class="o">.+</span> <span class="n">b</span><span class="x">,</span> <span class="x">(</span><span class="n">dim</span><span class="x">,</span> <span class="n">dim</span><span class="x">)))</span>
<span class="c"># Positive-definite means positive Eigenvalues. We should check this.</span>
<span class="nd">@show</span> <span class="n">eigvals</span><span class="x">(</span><span class="n">A0</span><span class="o">*</span><span class="n">P</span><span class="x">(</span><span class="n">x0</span><span class="x">[</span><span class="mi">0</span><span class="x">,</span><span class="o">:</span><span class="x">],</span> <span class="n">W</span><span class="x">,</span> <span class="n">bvec</span><span class="x">))</span>
</code></pre></div></div>

<p>This function will serve as our loss function. It evaluates the NN preconditioner and then runs
some Gauss-Seidel iterations. Finally, the iterative solution
approximation is transformed back by applying \(P^{-1}\).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>function loss_fun(W, bmat, A0, b0, y0, niter=5)
    # W - Weight matrix for NN-preconditioner
    # bmat - Bias vector for NN preconditioner
    # A0: Linear system coefficient matrix
    # b0: RHS of linear system
    # y0 - initial guess for Linear system. Strictly, this is x0. But we call it the same
    # assuming that P⁻¹x0 = x0.
    # niter - Number of Gauss-Seidel iterations to perform
    
    loss = 0.0
    nsamples = size(y0)[1]
    
    for idx ∈ 1:nsamples:
        # Evaluate the preconditioner
        P⁻¹ = P(y0[idx, :], W, reshape(bmat * bmat', (dim * dim)))
        # Initial guess
        # Now we solve A(Px)⁻¹y = rhs for y with 3 Gauss-Seidel iterations
        y_sol = gauss_seidel(A0 * P⁻¹, b0, y0[idx, :], niter)
        # And reconstruct x
        x = P⁻¹ * y_sol
        loss += norm(x - x_true) / length(x)
    end

    return loss / nsamples
end
</code></pre></div></div>

<p>Now comes the fun part. Zygote calculates the partial derivatives of the
loss function with respect to its input, in our case \(W\) and \(b\). 
Given the gradients, we can actually update \(W\) and \(b\).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Number of epochs to train
num_epochs = 10
loss_arr = zeros(num_epochs)
# Store the Weight and bias matrix at each iteration
W_arr = zeros((size(W)..., num_epochs))
bmat_arr = zeros((size(bmat)..., num_epochs))
# Learning rate
α = 0.005

for epoch ∈ 1:num_epochs
    loss_arr[epoch], grad = Zygote.pullback(loss_fun, W, bmat, A0, b0, x0)
    res = grad(1.0)
    # Gradient descent
    global W -= α * res[1]
    global bmat -= α * res[2]
    # Store W and b
    W_arr[:, :, epoch] = W
    bmat_arr[:, :, epoch] = bmat
end
</code></pre></div></div>

<p>Finally, let’s evaluate the performance</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># This functions returns a vector with the residual of the iterative
# solution to the true solution at each step
function eval_performance(W, bmat, A0, y0, b0, niter=20)
    # Instantiate the preconditioner with the initial guess
    P⁻¹ = P(y0, W, reshape(bmat * bmat', (dim * dim)))
    y_sol = copy(y0)
    loss_vec = zeros(niter)
    for n ∈ 1:niter      
        # Initial guess
        # Now we solve (Px)⁻¹y = rhs for y with 3 Gauss-Seidel iterations
        y_sol = gauss_seidel(A0 * P⁻¹, b0, y_sol, niter)
        # And reconstruct x
        x = P⁻¹ * y_sol
        loss_vec[n] = norm(x - x_true) / length(x)
    end

    return loss_vec
end

# Get the residual at each Gauss-Seidel iteration
sol_err = zeros(20, num_epochs)
for i ∈ 1:num_epochs
    # Calculate the loss at each iteration, averaged over the training data
    sol_avg = zeros(20)
    for idx ∈ 1:N_test
        sol_here[:] += eval_performance(W_arr[:, :, i], bmat_arr[:, :, i], A0, x0, b0)
    end
    sol_err[:, i] = sol_here[:] / N_test
end

</code></pre></div></div>

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

<p>A plot says many words, so here we go</p>

<p><img src="/assets/images/nn_precond/GS_iteration_error.png" alt="Trained preconditioner" /></p>

<p>The plot shows the average residual to the true solution vector as a function of
Gauss-Seidel iterations. Training for 1 epoch, the residual decreases as a power law
for all 20 GS iterations. The longer we train the preconditioner, the faster
the residual decreases. Remember that we trained only for 5 iterations. But in the plot
we see that preconditioner GS scheme proceeds at an accelerated rate of convergence,
even after the fifth iteration. So for this toy example, the NN preconditioner performs
quiet well.</p>

<p>Finally, a word on what we learn. Since we only take small, non-zero vectors, we update mostly
the bias term and not the weight matrix. We can verify this in the code:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">julia</span><span class="o">&gt;</span> <span class="x">(</span><span class="n">W_arr</span><span class="x">[</span><span class="o">:</span><span class="x">,</span> <span class="o">:</span><span class="x">,</span> <span class="k">end</span><span class="x">]</span> <span class="o">-</span> <span class="n">W_arr</span><span class="x">[</span><span class="o">:</span><span class="x">,</span> <span class="o">:</span><span class="x">,</span> <span class="mi">1</span><span class="x">])</span>
<span class="mi">16</span><span class="n">×4</span> <span class="kt">Array</span><span class="x">{</span><span class="kt">Float64</span><span class="x">,</span><span class="mi">2</span><span class="x">}</span><span class="o">:</span>
  <span class="mf">9.2298e-7</span>    <span class="mf">3.56583e-6</span>    <span class="mf">9.63813e-6</span>   <span class="o">-</span><span class="mf">3.62526e-6</span>
  <span class="mf">7.02612e-6</span>   <span class="mf">1.83826e-6</span>    <span class="mf">4.87793e-6</span>    <span class="mf">1.06628e-5</span>
 <span class="o">-</span><span class="mf">1.18359e-5</span>  <span class="o">-</span><span class="mf">5.52383e-6</span>   <span class="o">-</span><span class="mf">1.00905e-5</span>   <span class="o">-</span><span class="mf">2.49149e-5</span>
  <span class="mf">4.2548e-6</span>   <span class="o">-</span><span class="mf">4.93141e-7</span>   <span class="o">-</span><span class="mf">5.37068e-6</span>    <span class="mf">1.46914e-5</span>
 <span class="o">-</span><span class="mf">4.80919e-6</span>  <span class="o">-</span><span class="mf">1.97348e-5</span>   <span class="o">-</span><span class="mf">5.14656e-5</span>    <span class="mf">1.82806e-5</span>
 <span class="o">-</span><span class="mf">2.99721e-5</span>  <span class="o">-</span><span class="mf">1.85244e-5</span>   <span class="o">-</span><span class="mf">4.37366e-5</span>   <span class="o">-</span><span class="mf">3.33036e-5</span>
  <span class="mf">5.28254e-5</span>   <span class="mf">5.17799e-5</span>    <span class="mf">0.000115429</span>   <span class="mf">8.27112e-5</span>
 <span class="o">-</span><span class="mf">1.70783e-5</span>  <span class="o">-</span><span class="mf">1.11531e-5</span>   <span class="o">-</span><span class="mf">8.03195e-6</span>   <span class="o">-</span><span class="mf">5.73307e-5</span>
  <span class="mf">6.09361e-6</span>   <span class="mf">3.89891e-5</span>    <span class="mf">8.93109e-5</span>   <span class="o">-</span><span class="mf">3.54379e-5</span>
  <span class="mf">5.58404e-5</span>   <span class="mf">5.60942e-5</span>    <span class="mf">0.000112264</span>   <span class="mf">6.67546e-5</span>
 <span class="o">-</span><span class="mf">8.3024e-5</span>   <span class="o">-</span><span class="mf">0.000129324</span>  <span class="o">-</span><span class="mf">0.00020506</span>   <span class="o">-</span><span class="mf">0.000139751</span>
  <span class="mf">2.75378e-5</span>   <span class="mf">3.47865e-5</span>    <span class="mf">1.79561e-5</span>    <span class="mf">0.000101882</span>
 <span class="o">-</span><span class="mf">2.89687e-6</span>  <span class="o">-</span><span class="mf">2.14357e-5</span>   <span class="o">-</span><span class="mf">4.965e-5</span>      <span class="mf">2.24125e-5</span>
 <span class="o">-</span><span class="mf">3.26029e-5</span>  <span class="o">-</span><span class="mf">3.11671e-5</span>   <span class="o">-</span><span class="mf">5.59135e-5</span>   <span class="o">-</span><span class="mf">4.08413e-5</span>
  <span class="mf">5.10635e-5</span>   <span class="mf">7.48976e-5</span>    <span class="mf">0.000101988</span>   <span class="mf">9.20182e-5</span>
 <span class="o">-</span><span class="mf">1.64133e-5</span>  <span class="o">-</span><span class="mf">1.88765e-5</span>   <span class="o">-</span><span class="mf">3.41413e-6</span>   <span class="o">-</span><span class="mf">6.0346e-5</span>
</code></pre></div></div>

<p>The average matrix element of W has changed only little during learning, on average by
about 0.00001. Looking at how much the elements of b have changed during training</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">julia</span><span class="o">&gt;</span> <span class="x">(</span><span class="n">bmat_arr</span><span class="x">[</span><span class="o">:</span><span class="x">,</span> <span class="o">:</span><span class="x">,</span> <span class="k">end</span><span class="x">]</span><span class="o">*</span><span class="n">bmat_arr</span><span class="x">[</span><span class="o">:</span><span class="x">,</span> <span class="o">:</span><span class="x">,</span> <span class="mi">1</span><span class="x">]</span><span class="err">'</span><span class="x">)</span> <span class="o">-</span> <span class="x">(</span><span class="n">bmat_arr</span><span class="x">[</span><span class="o">:</span><span class="x">,</span> <span class="o">:</span><span class="x">,</span> <span class="mi">1</span><span class="x">]</span><span class="o">*</span><span class="n">bmat_arr</span><span class="x">[</span><span class="o">:</span><span class="x">,</span> <span class="o">:</span><span class="x">,</span> <span class="mi">1</span><span class="x">]</span><span class="err">'</span><span class="x">)</span>
<span class="mi">4</span><span class="n">×4</span> <span class="kt">Array</span><span class="x">{</span><span class="kt">Float64</span><span class="x">,</span><span class="mi">2</span><span class="x">}</span><span class="o">:</span>
 <span class="o">-</span><span class="mf">0.000674214</span>   <span class="mf">0.00163171</span>   <span class="mf">0.0012703</span>    <span class="mf">0.000217951</span>
  <span class="mf">0.00631572</span>    <span class="mf">0.00148005</span>  <span class="o">-</span><span class="mf">0.0105803</span>    <span class="mf">0.00372173</span>
 <span class="o">-</span><span class="mf">0.050246</span>     <span class="o">-</span><span class="mf">0.0453774</span>   <span class="o">-</span><span class="mf">0.0181303</span>   <span class="o">-</span><span class="mf">0.0511274</span>
  <span class="mf">0.011839</span>      <span class="mf">0.0128648</span>   <span class="o">-</span><span class="mf">0.00398132</span>   <span class="mf">0.00899787</span>
</code></pre></div></div>

<p>we find that they have changed more, on average by a factor of 100 more than entries of W.
But as discussed earlier, including x0 in the preconditioner is a modeling choice which
one does not have to make.</p>

<h2 id="conclusions">Conclusions</h2>
<p>To summarize, we proposed to use a Neural Network to accelerate an iterative solver
for linear systems by acting as a preconditioner matrix. We propose that the weights
of the neural network can be optimized by automatic differentiation in reverse mode.
By putting the solver in the loop, the training and inference steps couple to the
simulation in a very simple way.</p>

<p>One drawback of the method as written here is that we are limiting ourselfes to initial
guesses \(x \sim 0\). This is due to the requirement of the Gauss-Seidel scheme that the
linear system is positive definite. In more practical settings this can be circumvented
by either using different parameters to be passed to \(P\) than \(x\). Alternatively
one can use an iterative solver that doesn’t pose such restrictions, such as Jacobian-Free
Newton Krylov or Conjugate Gradient etc.</p>]]></content><author><name></name></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[What is a preconditioner?]]></summary></entry></feed>