<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2020-09-22T15:14:06+00:00</updated><id>/feed.xml</id><title type="html">Francesco Cremonesi’s Blog</title><subtitle>Data scientist and machine learning enthusiast. PhD in  computational neuroscience. Natural skeptic. I'm interested in applying data models for good, improving healthcare, environmental sustainability and fairness in society.
</subtitle><entry><title type="html">Reading large datasets for training on Colab</title><link href="/2020/09/18/colab-io-bench.html" rel="alternate" type="text/html" title="Reading large datasets for training on Colab" /><published>2020-09-18T14:13:00+00:00</published><updated>2020-09-18T14:13:00+00:00</updated><id>/2020/09/18/colab-io-bench</id><content type="html" xml:base="/2020/09/18/colab-io-bench.html">&lt;h2 id=&quot;large-datasets-on-colab&quot;&gt;Large datasets on Colab&lt;/h2&gt;

&lt;p&gt;Google Colab provides a fantastic way for anyone to access a powerful GPU runtime on the cloud, especially tailored for exploring and training machine learning models.
However, the issue still remains of making sure the training data that you need is available to your models within a reasonable latency.
For small datasets, a common approach is to simply store your data on your local computer, and upload it to the Colab runtime everytime via the internet.
This approach is not feasible when datasets become large: in our experience, it can take up to 6 hours to upload a &amp;lt;2 GB dataset to the Colab environment.&lt;/p&gt;

&lt;p&gt;We need a faster way to access our training data, possibly by storing it already on the cloud to exploit better bandwidth.
We consider three approaches:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;em&gt;GDrive&lt;/em&gt;: using Google Drive for storage, and mounting it in the Colab runtime;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;GCSFuse&lt;/em&gt;: using Google Cloud Storage Buckets, and mounting them with gcsfuse;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;GCS Manual&lt;/em&gt;: using Google Cloud Storage Buckets, and transferring the data with the storage api.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;main-results&quot;&gt;Main results&lt;/h2&gt;

&lt;p&gt;The results of this investigation can be summarized as follows:&lt;/p&gt;

&lt;hr /&gt;

&lt;ol&gt;
  &lt;li&gt;the major bottleneck for the performance of reading the training dataset is the time required to transfer the files from their remote location to the Colab environment during the first epoch;&lt;/li&gt;
  &lt;li&gt;geographic location plays an important role in affecting the performance, while choosing a cpu, gpu or tpu runtime does not;&lt;/li&gt;
  &lt;li&gt;after the first epoch of training, caching the files locally on the Colab runtime significantly improves the performance, to the point that other factors (such as gpu transfers) may become the bottlenecks to training performance.&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;p&gt;More specifically, in terms of the performance of the approaches considered here, we found that:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;GCSFuse does not seem to aggressively exploit the read-only nature of ML workflows, meaning that subsequent reads after the first are almost as slow as the first one;&lt;/li&gt;
  &lt;li&gt;The GCS Manual approach offers the best performance, but requires payment of a low fee and that you refresh your Colab environment enough times to land in the geographic region that you need;&lt;/li&gt;
  &lt;li&gt;GDrive offers good performance for free, but can be a bit unstable when dealing with folders containing lots of files.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;performance-analysis&quot;&gt;Performance analysis&lt;/h2&gt;

&lt;p&gt;When you create a new Colab runtime, it is attached to an hard drive that does not contain your data.
Hence, to access the contents of your files, the data needs to be transferred to the Colab runtime, at least once for every file read.
In the third approach described above, this transfer happens when we manually copy the files, but do not be fooled: it happens with the other approaches too, even if it is handled automatically under the hood by the software libraries.&lt;/p&gt;

&lt;p&gt;In machine learning workflows, re-reading a file happens very often during training, for example at every epoch.
Retransferring the data everytime you want to re-read a file would be costly, so a more efficient strategy is to &lt;em&gt;cache&lt;/em&gt; the file contents on the Colab runtime filesystem.
This is what’s implicitly happening in the third approach, which works well because you already know that those files have not been changed on the remote server.
In the first and second approach you are instead relying on the library’s implementations to handle this cache optimization for you.&lt;/p&gt;

&lt;p&gt;To measure the performance of reading files, we use read bandwidth in Megabytes per second.
In practice, we measure the amount of MB read and the time it took to read them separately, and plot their ratio.
We caution against taking these numbers as an absolute indication of real-world performance, although they are valid to have a ballpark idea.
The reason is that other operations, such as opening the file, can contribute significant overhead (especially for small files).&lt;/p&gt;

&lt;h3 id=&quot;gcsfuse-does-not-provide-optimal-re-read-performance&quot;&gt;Gcsfuse does not provide optimal re-read performance&lt;/h3&gt;

&lt;p&gt;We measured the speed of re-reading files for the three approaches described above.
While the GCS Manual approach and Google Drive appear to exploit some sort of caching to improve the performance, we observed that gcsfuse’s speed was not improved in subsequent reads of the files.
The Figure below shows the boxplots for the file read bandwidth, distinguishing the first transfer from the remote servers versus a second (and subsequent) reads.
Note the different scales on the x-axis.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/io_bench_first_second_read.png&quot; alt=&quot;first_vs_second_read&quot; /&gt;&lt;/p&gt;

&lt;p&gt;During the first read, all bandwidths are the same.
The first read is likely bounded by the speed of transferring the file contents from their cloud locations over the internet.
The second read is more than 1000x faster for GDrive and the Manual approach, while for GCSFuse the speed remains roughly the same.&lt;/p&gt;

&lt;p&gt;In this benchmark, we did our best to ensure that file contents were not kept in memory between the first and second reads, to simulate the case where one would loop over the whole dataset for each epoch, effectively clearing the memory.
It remains unclear why the second-read bandwidth of the GDrive approach is worse than the manual approach, but it might be possible that GDrive is performing some checks or contacting the remote server.&lt;/p&gt;

&lt;h2 id=&quot;geographic-region-affects-performance&quot;&gt;Geographic region affects performance&lt;/h2&gt;

&lt;p&gt;The closer the remote servers are to where the colab is physically running, the better we expect the performance to be.
We measured the first-read bandwidth and found that geographic proximity can give a 2x-10x boost to performance.
The Figure below shows the read bandwidth broken down by region where the Colab runtime is executing.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/io_bench_regions.png&quot; alt=&quot;regions&quot; /&gt;&lt;/p&gt;

&lt;p&gt;For the GCSFuse and GCS Manual approaches, performance was improved when the colab runtime was running in the Netherlands.
This is consistent with the fact that my GCS Bucket is located in the EU Multiregion.
The GDrive approach, on the other hand, has better performance when the Colab runtime is in the US.
One can only assume that GDrive servers are located there.&lt;/p&gt;

&lt;h2 id=&quot;runtime-type-does-not-affect-performance&quot;&gt;Runtime type does not affect performance&lt;/h2&gt;

&lt;p&gt;As expected, we also confirm that runtime type does not affect first read bandwidth.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/io_bench_rt_type.png&quot; alt=&quot;rt_type&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This confirms our understanding that the performance of reading files for the first time is bounded by the time required to transfer the file contents over the internet.&lt;/p&gt;

&lt;h2 id=&quot;implementation-guide-storing-and-reading-your-data-from-the-cloud&quot;&gt;Implementation guide: storing and reading your data from the cloud&lt;/h2&gt;

&lt;p&gt;This section presents the code and some technical details for implementation.&lt;/p&gt;

&lt;h3 id=&quot;gdrive-google-drive-for-storage-and-mounting&quot;&gt;&lt;em&gt;GDrive&lt;/em&gt;: Google Drive for storage and mounting&lt;/h3&gt;

&lt;p&gt;I used the UI to upload my dataset to my personal Google Drive.
Then, the code to mount your drive in the Colab runtime is relatively simple.&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;google.colab&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;drive&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;drive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'/content/gdrive'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Note that calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drive.mount&lt;/code&gt; will prompt you for an authorization code, for which you manually need to click on a link.
After authentication and successful mounting, you are able to access all the files on your Google Drive from the Colab runtime.&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;images&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;listdir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'/content/gdrive/path/to/training/data'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'r'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;readlines&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Google Drive can be a bit clumsy and &lt;a href=&quot;https://research.google.com/colaboratory/faq.html&quot;&gt;unstable&lt;/a&gt; when dealing with folders containing many files.
An efficient approach to uploading large datasets to Drive is to upload a zipped folder, and unzip it directly on the Drive.
Sometimes, reading the files would fail with an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OSError&lt;/code&gt; exception or a timeout.
I found that listing the directories containing the training data and catching the exceptions in that moment with the following code can help in later on making the training process more stable.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;retry = True
while retry:
  retry=False
  try:
    next(os.walk('/content/gdrive/path/to/training/data'))
  except StopIteration:
    print('Exception Raised. Retry')
    retry = True
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;gcsfuse-cloud-storage-and-gcsfuse&quot;&gt;&lt;em&gt;GCSFuse&lt;/em&gt;: Cloud Storage and gcsfuse&lt;/h3&gt;

&lt;p&gt;You can use the &lt;a href=&quot;https://cloud.google.com/storage&quot;&gt;Google Cloud Storage&lt;/a&gt; service to store your dataset.
This is a viable option even if you’re not within a corporation or a well-funded research institution, as the prices are not so high.
I’ve been storing about 10 GB of data for a couple of datasets, and paying less than 3 EUR per month.
A simple way of accessing your data from Colab is then to mount the Storage Bucket on the runtime, using &lt;a href=&quot;https://cloud.google.com/storage/docs/gcs-fuse&quot;&gt;gcsfuse&lt;/a&gt;.
First, you need to install the tool&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;deb http://packages.cloud.google.com/apt gcsfuse-bionic main&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/gcsfuse.list
&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;apt &lt;span class=&quot;nt&quot;&gt;-qq&lt;/span&gt; update
&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;apt &lt;span class=&quot;nt&quot;&gt;-qq&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;gcsfuse
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Then you need to authenticate yourself and prepare the directory&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;google.colab&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authenticate_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;os&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;makedirs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'/content/bucket-data'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chdir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'/content'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Now you are ready to mount the drive&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;gcsfuse &lt;span class=&quot;nt&quot;&gt;--implicit-dirs&lt;/span&gt; my_bucket_name bucket-data
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--implicit-dirs&lt;/code&gt; option is really important, as it tells gcsfuse to recreate the directory structure from the Cloud Storage Bucket (very important if, for example, train and validation datasets are distinguished by being in different folders).
Note that gcsfuse seems to have some caching options, but when I mounted the drive with the options below I found no significant performance difference&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# finer control on caching&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;gcsfuse &lt;span class=&quot;nt&quot;&gt;--implicit-dirs&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--stat-cache-ttl&lt;/span&gt; 5h &lt;span class=&quot;nt&quot;&gt;--type-cache-ttl&lt;/span&gt; 5h &lt;span class=&quot;nt&quot;&gt;--stat-cache-capacity&lt;/span&gt; 65536 ml_datasets_checco_1 bucket-data
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;After a successful mount, it is again possible to open the files as usual&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;images&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;listdir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'/content/bucket-data/my/path'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'r'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;readlines&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;gcs-manual-cloud-storage-and-manual-file-copy&quot;&gt;&lt;em&gt;GCS Manual&lt;/em&gt;: Cloud Storage and manual file copy&lt;/h3&gt;

&lt;p&gt;For performance reasons, we devised a third option in which we copy the files manually from the Storage Bucket using the provided API.
Authentication and some preparation is again required&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;google.colab&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authenticate_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;os&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'/content/gcs-api/my/data'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;makedirs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'/content/gcs-api/my/data'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Then the API can be used to retrieve the files&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;google.cloud&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storage&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'my_project_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;list_blobs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'my_bucket_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;download_to_filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'/content/gcs-api/my/data/this_file_name.ext'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As shown above, performance is affected by the region of your GCS Bucket as well as the region where the Colab runtime is executing.
The former can be found out through the Google Cloud Console, while the latter can be found using&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;import requests
ipinfo = requests.get('http://ipinfo.io')
region = ipinfo.json()['country'] + ': ' + ipinfo.json()['region']
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;code&quot;&gt;Code&lt;/h2&gt;

&lt;p&gt;The code for the benchmarks can be found &lt;a href=&quot;https://github.com/sharkovsky/sharkovsky.github.io/blob/master/code/IO_bench.ipynb&quot;&gt;here&lt;/a&gt;, while the analysis code is &lt;a href=&quot;https://github.com/sharkovsky/sharkovsky.github.io/blob/master/code/IO_bench_analysis.ipynb&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We used python’s line profiler &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%lprun&lt;/code&gt; to extract timings for single lines of code.
For GDrive and GCSFuse, the read benchmark looks like this:&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;N&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;50&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;images&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;listdir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'/content/gdrive/path/to/data'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;N&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;filename&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;images&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'/content/gdrive/My Drive/ML_data/snakes/valid/venomous/'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filename&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'rb'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seek&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;length_of_file&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tell&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seek&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;length_of_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;To obtain the read bandwidth, we divided the total size of the files in MB by the sum of all the timings for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;f.read&lt;/code&gt; function call, as measured by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lprun&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For GCS Manual, the benchmark looks like this:&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;N&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;50&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;list_blobs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'gcs_bucket_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;N&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;filepath&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'/content/gcs-api/'&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'/'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'/'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:])&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;download_to_filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filepath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here we measured instead the time of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b.download_to_filename&lt;/code&gt; function.
Technically this doesn’t include reading the file, but once it has been downloaded the file contents can be considered to be in memory, and reading them right away would happen at lightining speed compared to the cost of transferring them from the remote servers.&lt;/p&gt;</content><author><name></name></author><summary type="html">Large datasets on Colab</summary></entry><entry><title type="html">My first attempt at publishing a python package</title><link href="/2019/08/18/python-package.html" rel="alternate" type="text/html" title="My first attempt at publishing a python package" /><published>2019-08-18T09:45:10+00:00</published><updated>2019-08-18T09:45:10+00:00</updated><id>/2019/08/18/python-package</id><content type="html" xml:base="/2019/08/18/python-package.html">&lt;p&gt;Lately I wanted to publish my first package to the &lt;a href=&quot;https://pypi.org/&quot;&gt;PyPI&lt;/a&gt; repository, but I encountered some difficulties which I’d like to share here.
The main problems I encountered are:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;[SOLVED] &lt;a href=&quot;#specifying-a-github-repo-as-a-dependency&quot;&gt;how to&lt;/a&gt; specify a github repository as part of the dependencies for your package;&lt;/li&gt;
  &lt;li&gt;[SOLVED] &lt;a href=&quot;#testing-the-distribution&quot;&gt;how to&lt;/a&gt; test your package distribution;&lt;/li&gt;
  &lt;li&gt;[WORKAROUND] &lt;a href=&quot;#publising-the-package&quot;&gt;how to&lt;/a&gt; publish your package when you have specified a github repository as dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-package-doi4bib&quot;&gt;The package: doi4bib&lt;/h2&gt;

&lt;p&gt;In the frantic last days before handing in my thesis, someone told me that reviewers really appreciate it when clickable DOI links are placed in the references.
Getting this in the latex manuscript was as simple as using the &lt;a href=&quot;https://ctan.org/pkg/doi?lang=en&quot;&gt;doi&lt;/a&gt; package, but I quickly realized that the DOI information was missing from most of my references!
So I quickly hacked together a utility that would read a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bib&lt;/code&gt; file and add the DOI information where needed.&lt;/p&gt;

&lt;p&gt;This python tool can be found on my &lt;a href=&quot;https://github.com/sharkovsky/doi4bib&quot;&gt;github page&lt;/a&gt;.
It makes extensive use of other people’s work, namely the &lt;a href=&quot;https://github.com/OpenAPC/openapc-de/blob/master/python/import_dois.py&quot;&gt;OpenAPC DOI Importer&lt;/a&gt; and aclement’s &lt;a href=&quot;https://github.com/aclements/biblib&quot;&gt;biblib&lt;/a&gt; library for parsing bib files.&lt;/p&gt;

&lt;h2 id=&quot;specifying-dependencies&quot;&gt;Specifying dependencies&lt;/h2&gt;

&lt;p&gt;As I mentioned, my tool relies on two dependencies: the &lt;a href=&quot;https://github.com/OpenAPC/openapc-de/blob/master/python/import_dois.py&quot;&gt;OpenAPC DOI Importer&lt;/a&gt; and aclement’s &lt;a href=&quot;https://github.com/aclements/biblib&quot;&gt;biblib&lt;/a&gt; library.&lt;/p&gt;

&lt;h3 id=&quot;openapc-doi-importer&quot;&gt;OpenAPC DOI Importer&lt;/h3&gt;

&lt;p&gt;Since the dependency in this case was for a single file from the repository, I simply copied that file and stripped parts of it that weren’t relevant to me.
This may not be the best practice in general, but for such a simple situation it works quite well.&lt;/p&gt;

&lt;h3 id=&quot;aclements-biblib-library&quot;&gt;Aclement’s biblib library&lt;/h3&gt;

&lt;p&gt;In this case, I needed the specify the dependency on a full library.
In the ideal case, there would be a python package &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my-dependency&lt;/code&gt; which can be specified in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setyp.py&lt;/code&gt; file as such:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# file setup.py
setup(...
    install_requires=['my-dependency',...]
    ···]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Unfortunately, this was not the case.
Even more unfortunately, a conflict occurred:&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;a package with the same name but completely different API exists on PyPI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Given the bad luck that always accompanies programmers who are starting out, the existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;biblib&lt;/code&gt; &lt;a href=&quot;https://pypi.org/project/biblib/&quot;&gt;package&lt;/a&gt; on PyPI did not work for me.
I urge you to test it out for yourself, as it offers similar functionalities to what I describe here.&lt;/p&gt;

&lt;p&gt;So the first problem I encountered is that I needed to explictly point to aclement’s repository as a dependency.&lt;/p&gt;

&lt;h4 id=&quot;specifying-a-github-repo-as-a-dependency&quot;&gt;Specifying a github repo as a dependency&lt;/h4&gt;

&lt;p&gt;The basic idea is that you need to use the &lt;a href=&quot;https://www.python.org/dev/peps/pep-0508/&quot;&gt;PEP-508&lt;/a&gt; syntax.
The thing that worked for me was to specify the dependency both in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;install_requires&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dependency_links&lt;/code&gt;, as such:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# file setup.py
setup(...
    install_requires=[
        'biblib@git+https://github.com/aclements/biblib.git#egg=biblib-0.1.0',
        ...
    ],
    dependency_links=[
        'git+https://github.com/aclements/biblib.git#egg=biblib-0.1.0',
    ...
    ]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In my case, it was necessary to embed the github url in both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;install_requires&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dependency_links&lt;/code&gt; because otherwise the created wheel did not have the knowledge of the url, and during a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip install&lt;/code&gt; would routinely fall back to the other existing package with the same name.&lt;/p&gt;

&lt;p&gt;The syntax is&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    &amp;lt;package name&amp;gt; @ git+&amp;lt;URL&amp;gt;#egg=&amp;lt;package name&amp;gt;-&amp;lt;version&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can read the details in the “&lt;a href=&quot;https://setuptools.readthedocs.io/en/latest/setuptools.html#id15&quot;&gt;Dependencies that aren’t in PyPI&lt;/a&gt;” section of the setuptools docs.&lt;/p&gt;

&lt;h2 id=&quot;testing-the-distribution&quot;&gt;Testing the distribution&lt;/h2&gt;

&lt;p&gt;Before publishing your package you probably want to test it.
One nice service is provided by &lt;a href=&quot;https://packaging.python.org/guides/using-testpypi/&quot;&gt;TestPyPI&lt;/a&gt; which allows you to try out the distribution tools and process without worrying about affecting the real index.&lt;/p&gt;

&lt;p&gt;However, I often found that I could run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python setup.py install&lt;/code&gt; locally without issue, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip install&lt;/code&gt; from the testpypi repository would fail.
So another quick test that allowed me to reproduce at least some of the issues I was having with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip&lt;/code&gt; was to create a clean virtual environment and pip-installing the newly created wheel directly.
Thus my workflow was, &lt;em&gt;before testing publication to TestPyPI&lt;/em&gt;:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# in package directory
python setup.py sdist bdist_wheel
virtualenv venv
source venv/bin/activate
pip install -e dist/doi4bib-0.1.10-py2.py3-none-any.whl
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I found that the last pip install command often reproduced the errors I got when trying to install from TestPyPI.&lt;/p&gt;

&lt;h2 id=&quot;publishing-the-package&quot;&gt;Publishing the package&lt;/h2&gt;

&lt;p&gt;This is where I ran into a wall.
I managed to publish several versions of the package to TestPyPI, but none of them could later on be installed correctly via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip install&lt;/code&gt;.
The main problem was always that the incorrect &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;biblib&lt;/code&gt; package was picked up by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip&lt;/code&gt;.
Finally, as described &lt;a href=&quot;specifying-a-github-repo-as-a-dependency&quot;&gt;above&lt;/a&gt; I managed to create a wheel with the correct url to the github repository.
However, this has &lt;a href=&quot;https://stackoverflow.com/a/54894359&quot;&gt;the side-effect&lt;/a&gt; of making the package impossible to upload to TestPyPI and PyPI.
Attempts to upload fail with&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;HTTPError: 400 Client Error: Invalid value for requires_dist
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For the moment, it seems that PyPI &lt;a href=&quot;https://github.com/pypa/pip/issues/6301&quot;&gt;doesn’t accept&lt;/a&gt; direct references, period.
Therefore, I opted for a simple, yet effective, option.
For now, I will only publish my package directly through github link, using the now familiar &lt;a href=&quot;https://www.python.org/dev/peps/pep-0508/&quot;&gt;PEP-508&lt;/a&gt; syntax:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pip install -e git+https://github.com/sharkovsky/doi4bib@0.1.10#egg=doi4bib-0.1.10
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;</content><author><name></name></author><summary type="html">Lately I wanted to publish my first package to the PyPI repository, but I encountered some difficulties which I’d like to share here. The main problems I encountered are: [SOLVED] how to specify a github repository as part of the dependencies for your package; [SOLVED] how to test your package distribution; [WORKAROUND] how to publish your package when you have specified a github repository as dependency.</summary></entry><entry><title type="html">A Simplified Model for Load Imbalance in Distributed Spiking Neural Networks</title><link href="/2018/03/20/load-imbalance.html" rel="alternate" type="text/html" title="A Simplified Model for Load Imbalance in Distributed Spiking Neural Networks" /><published>2018-03-20T09:45:10+00:00</published><updated>2018-03-20T09:45:10+00:00</updated><id>/2018/03/20/load-imbalance</id><content type="html" xml:base="/2018/03/20/load-imbalance.html">&lt;p&gt;Simulating networks of spiking neurons at large scale requires the implementation of some strategy to distribute neurons across different compute nodes.
As the network evolves during the simulation some compute nodes may be required, at a given time instant, to process more spikes than other neurons, simply because of the chaotic activity of the network.
This introduces a negative effect on simulation performance called load imbalance, by which the total wallclock time of the simulation is increased because some compute nodes must waste cycles waiting for others (who were tasked with a larger amount of computation) to finish.&lt;/p&gt;

&lt;p&gt;The load balance problem has been tackled differently in the literature.
The developers of &lt;a href=&quot;http://www.nest-simulator.org/&quot;&gt;NEST&lt;/a&gt;, one of the state-of-the-art spiking point-neuron simulators available open-source, have &lt;a href=&quot;https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5820465/&quot;&gt;recently stated&lt;/a&gt; that &lt;em&gt;“one needs to investigate how to map the spatial structure of a neuronal network to the topology of modern HPC systems. This is not an obvious design choice as the currently employed round-robin distribution scheme is crucial for load balancing. Any non-random distribution of neurons could incur significant performance penalties due to unbalanced work load.”&lt;/em&gt;
As part of a software engineering effort, the group of Modha et al. have developed a &lt;a href=&quot;https://people.eecs.berkeley.edu/~demmel/cs267_Spr10/Lectures/RajAnanthanarayanan_SC07-pap402.pdf&quot;&gt;closed-source simulator&lt;/a&gt; based on non-blocking communication that is able partially to mitigate the load balancing problem.
Finally, a &lt;a href=&quot;https://www.frontiersin.org/articles/10.3389/fninf.2014.00076/full&quot;&gt;2014 study&lt;/a&gt; from Gerstner’s lab found that synaptic plasticity can greatly impact simulation performance by introducing a significant load imbalance among parallel processes.&lt;/p&gt;

&lt;p&gt;In this post, I will introduce a simple model that allows to get an idea of the potential load imbalance that may arise during a simulation under some simplified assumptions.&lt;/p&gt;

&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;

&lt;p&gt;Under reasonable assumptions, the load imbalance \(\Lambda = max~\{W_p\} - min~\{W_p\}\) arising from the different spiking workloads in a distributed spiking neural network simulation can be estimated by&lt;/p&gt;

\[Pr( \Lambda = k ) = \sum_{x=0}^\infty Pr( max~\{W_p\} = x, min~\{W_p\} = x - k ),\]

&lt;p&gt;where \(W_p i.i.d.\) and&lt;/p&gt;

\[Pr( max~\{W_p\} = x, min~\{W_p\} = y ) =
\begin{cases}
0, &amp;amp;x&amp;lt;y \\
[ F_W(y) - F_W(y-1)]^P, &amp;amp;x = y \\
[ F_W(y+1) - F_W(y-1)]^P - [ F_W(y+1) - F_W(y)]^P - [ F_W(y) - F_W(y-1)]^P, &amp;amp; x = y+1\\
[ F_W(x) - F_W(y-1)]^P + [ F_W(x-1) - F_W(y)]^P - [ F_W(x) - F_W(y)]^P - [ F_W(x-1) - F_W(y-1)]^P, &amp;amp;x &amp;gt; y + 1
\end{cases}\]

&lt;p&gt;where \(F_W\) is the cdf of \(W_p\).&lt;/p&gt;

&lt;h4 id=&quot;application&quot;&gt;Application&lt;/h4&gt;

&lt;p&gt;This result was obtained with the application to the distributed simulation of spiking neural networks in mind.
In this case \(W_p\) represents the number of spikes that a given parallel process must &lt;em&gt;integrate&lt;/em&gt; in a network minimum delay period.
According to the model defined below, \(W_p \sim Poiss( \frac{NK}{P}f\delta)\).&lt;/p&gt;

&lt;p&gt;However, the problem is framed in the general setting of &lt;em&gt;finding the distribution of the difference between the maximum and minimum of a collection of i.i.d variables.&lt;/em&gt;&lt;/p&gt;

&lt;h4 id=&quot;validation&quot;&gt;Validation&lt;/h4&gt;

&lt;p&gt;I tested the formula above for \(P=16\) and different values of the Poisson parameter \(\mu\).
The numerical experiments yield counts that are very similar to those obtained during the formula.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/load_imbalance_valid.png&quot; alt=&quot;validation&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I have observed for \(\mu &amp;gt; 20\) results start being incorrect.
&lt;del&gt;I suspect this may be due to roundoff errors.&lt;/del&gt;
This is due to the fact that I approximated the range of values for the sum in the definition of the pmf of \(\Lambda\).
In the new version of the code I have called this variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;range_for_convolution&lt;/code&gt; and set it to a much larger value.
With a value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;range_for_convolution=200.&lt;/code&gt;, results appear correct at least for \(\mu \leq 100\).
Obviously, this results in a much more computationally expensive process.
In the future, it would be interesting to explore better ways of fixing this problem.&lt;/p&gt;

&lt;p&gt;The code for generating this image is provided &lt;a href=&quot;https://github.com/sharkovsky/sharkovsky.github.io/blob/master/code/LoadBal.ipynb&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;problem-definition&quot;&gt;Problem Definition&lt;/h2&gt;

&lt;p&gt;As is often done, I will simplify the spiking activity of a neuron and consider it as a poisson process with certain firing rate \(f\).
Moreover, I will introduce the simplifying assumption that all neurons are &lt;strong&gt;independent&lt;/strong&gt; from each other.
Note that this is a very unrealistic assumption, however it is often done in literature and in any case one may assume that if the neurons are randomly distributed across compute nodes it may still be a valid approximation.
Finally, I will assume for simplicity that all neurons have the same firing rate.
Again, this is not a very realistic assumption.
However this assumption can provide a best-case scenario where the load imbalance originates only from the chaotic and possibly stochastic nature of the spiking neural network, and if  we introduce additional imbalance by, for example, having different firing rates than the resulting load imbalance could only get worse.&lt;/p&gt;

&lt;p&gt;Let me introduce some notation:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;\(N\) is the total number of neurons in the simulation;&lt;/li&gt;
  &lt;li&gt;\(P\) is the number of parallel distributed processes;&lt;/li&gt;
  &lt;li&gt;\(f\) is the firing rate of each neuron;&lt;/li&gt;
  &lt;li&gt;\(K\) is the number of synapses per neuron;&lt;/li&gt;
  &lt;li&gt;\(\delta\) is the interval of simulated time at the end of which we wish to assess the potential load imbalance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The number of spikes that a neuron must process at a given instant can be modeled by assuming that each of the neuron’s synapses receives events independently from the others and according to a Poisson process as described above.
In the case of multisynaptic connections or of small networks this may not be a realistic assumption, but in the case of a large, random network this may be a good approximation.&lt;/p&gt;

&lt;p&gt;As a consequence of the assumptions above and assuming that neurons are randomly and evenly distributed across compute nodes, we have the following model.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;The number of spikes to be processed by all the neurons pertaining to a given parallel process is distributed like a Poisson variable:
\(W \sim Poiss(\frac{NK}{P}f\delta).\)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Moreover, since we want to estimate the load imbalance, we are interested in the difference between the parallel process with the maximum number of spikes to process and the one with the minimum.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;We define the load imbalance as
\(\Lambda = max~W - min~W\)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;where the \(max\) and \(min\) are taken over the different parallel processes.&lt;/p&gt;

&lt;h3 id=&quot;probabilistic-setting&quot;&gt;Probabilistic Setting&lt;/h3&gt;

&lt;p&gt;We can frame the problem in a general probabilistic setting as:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Given a set of \(P\) i.i.d. variables \(\{W_p\}\),&lt;/em&gt;
&lt;em&gt;what is the distribution of \(\Lambda = max~\{W_p\} - min~\{W_p\}\)?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is an interesting problem for two reason: first it requires estimating the joint distribution of the maximum and minimum of a set of i.i.d. variables; second it requires estimating the distribution of the difference of two variables that are &lt;em&gt;not independent&lt;/em&gt;.&lt;/p&gt;

&lt;h4 id=&quot;marginal-distributions-of-the-minimum-and-maximum&quot;&gt;Marginal Distributions of the Minimum and Maximum&lt;/h4&gt;

&lt;p&gt;Finding the distribution of the maximum (minimum) of a set of i.i.d. variables is relatively trivial.
The idea is to estimate the cumulative distribution function (cdf) of the maximum (minimum) using an insight into the properties of these functions.
For the maximum, we have that if the maximum should be smaller than a certain value \(x\), than &lt;em&gt;all&lt;/em&gt; the variables in the set should be smaller than \(x\), which translates to \(Pr( max~\{W_p\} \leq x ) = Pr( W_0 \leq x \cap W_1 \leq x \cap \dots )\).&lt;/p&gt;

&lt;p&gt;For the minimum, we use some transformations to obtain that
\(Pr( min~\{W_p\} \leq y ) = 1 - Pr( W_0 &amp;gt; y \cap W_1 &amp;gt; y \cap \dots )\).&lt;/p&gt;

&lt;p&gt;If \(F_W\) is the cdf of \(W\), the insights above lead to&lt;/p&gt;

\[\begin{align*}
Pr( max~\{W_p\} \leq x ) &amp;amp;= F_W(x)^P, \\
Pr( min~\{W_p\} \leq y ) &amp;amp;= 1 - [ 1 - F_W(y) ]^P.
\end{align*}\]

&lt;h4 id=&quot;joint-distribution-of-the-minimum-and-maximum&quot;&gt;Joint Distribution of the Minimum and Maximum&lt;/h4&gt;

&lt;p&gt;Finding the joint distribution is not so trivial.
This stack exchange &lt;a href=&quot;https://math.stackexchange.com/questions/1242784/the-maximum-and-minimum-of-five-independent-uniform-random-variables&quot;&gt;answer&lt;/a&gt; provides the correct path to follow, but contains some typos.
These stack &lt;a href=&quot;https://stats.stackexchange.com/questions/220/how-is-the-minimum-of-a-set-of-random-variables-distributed&quot;&gt;exchange&lt;/a&gt; &lt;a href=&quot;https://math.stackexchange.com/questions/565624/the-joint-density-of-the-max-and-min-of-two-independent-exponentials&quot;&gt;answers&lt;/a&gt; also provides some insight, but are applied only to special cases.
For completeness and correctness, I will repeat the procedure here.&lt;/p&gt;

&lt;p&gt;Given a collection of P i.i.d. variables \(\{W_p\}\), we seek the joint distribution of \(( max~\{W_p\}, min~\{W_p\})\).&lt;/p&gt;

&lt;p&gt;By drawing a little diagram, you can convince yourself that&lt;/p&gt;

\[Pr( max~\{W_p\} \leq x, min~\{W_p\} \leq y ) = Pr( max~\{W_p\} \leq x ) - Pr( max~\{W_p\} \leq x, min~\{W_p\} &amp;gt; y ).\]

&lt;p&gt;From the marginals above, we have that \(Pr( max~\{W_p\} \leq x ) = F_W(x)^P\).
To estimate the other term, we need to use the same insight as for the marginals: assuming \(y &amp;lt; x\), if the minimum should be larger than \(y\) and the maximum should be less or equal to \(x\), than &lt;em&gt;all&lt;/em&gt; the variables in \(\{W_p\}\) should respect this property.&lt;/p&gt;

\[\begin{align*}
Pr( max~\{W_p\}\leq x, min~\{W_p\} &amp;gt; y ) &amp;amp;= Pr( W_0 &amp;gt; y \cap W_0 \cap \leq x \cap W_1 &amp;gt; y \cap W_1 \cap \leq x \cap \dots ) \\
&amp;amp;= [ F_W(x) - F_W(y) ]^P.
\end{align*}\]

&lt;p&gt;On the other hand, if \(y \geq x\), then \(Pr( max~\{W_p\}\leq x, min~\{W_p\} &amp;gt; y ) = 0\).&lt;/p&gt;

&lt;p&gt;Therefore, we have that&lt;/p&gt;

\[Pr( max~\{W_p\} \leq x, min~\{W_p\} \leq y ) =
\begin{cases}
F_W(x)^P - [ F_W(x) - F_W(y) ]^P, &amp;amp; y &amp;lt; x \\
F_W(x)^P, &amp;amp; y \geq x.
\end{cases}\]

&lt;h4 id=&quot;joint-probability-mass-function&quot;&gt;Joint Probability Mass Function&lt;/h4&gt;

&lt;p&gt;The stack exchange &lt;a href=&quot;https://math.stackexchange.com/questions/1242784/the-maximum-and-minimum-of-five-independent-uniform-random-variables&quot;&gt;answer&lt;/a&gt; cited above proceeds to compute the pmf directly by differentiation.
Given that the variables of interest here are discrete and non-negative, I found that this approach did not work.
Instead, I will use the following relationship that is valid for integer, non-negative variables:&lt;/p&gt;

\[\begin{align*}
Pr(X=x,Y=y) &amp;amp;= Pr( x-1 &amp;lt; X \leq x, y-1 &amp;lt; Y \leq y) \\
 &amp;amp;= F_{(X,Y)}(x,y) - F_{(X,Y)}(x-1,y) - F_{(X,Y)}(x,y-1) + F_{(X,Y)}(x-1,y-1)
\end{align*}\]

&lt;p&gt;After some computations and taking special care of the corner cases we have:&lt;/p&gt;

\[Pr( max~\{W_p\} = x, min~\{W_p\} = y ) =
\begin{cases}
0, &amp;amp;x&amp;lt;y \\
[ F_W(y) - F_W(y-1)]^P, &amp;amp;x = y \\
[ F_W(y+1) - F_W(y-1)]^P - [ F_W(y+1) - F_W(y)]^P - [ F_W(y) - F_W(y-1)]^P, &amp;amp; x = y+1\\
[ F_W(x) - F_W(y-1)]^P + [ F_W(x-1) - F_W(y)]^P - [ F_W(x) - F_W(y)]^P - [ F_W(x-1) - F_W(y-1)]^P, &amp;amp;x &amp;gt; y + 1
\end{cases}\]

&lt;h4 id=&quot;distribution-of-the-difference&quot;&gt;Distribution of the Difference&lt;/h4&gt;

&lt;p&gt;Finally we are ready to compute our quantity of interest.
Here, special care must be taken to pick the correct formula for the difference of two &lt;strong&gt;non-independent&lt;/strong&gt; variables.
This formula is a special case of the &lt;a href=&quot;https://en.wikipedia.org/wiki/Multivariate_random_variable#Operations_on_random_vectors&quot;&gt;invertible mappings&lt;/a&gt; formula (thank you &lt;a href=&quot;https://ch.linkedin.com/in/francescocasalegno&quot;&gt;Francesco Casalegno&lt;/a&gt; for pointing this out).
According to the formula we have&lt;/p&gt;

\[Pr( max~\{W_p\} - min~\{W_p\} = k ) = \sum_{x=0}^\infty Pr( max~\{W_p\} = x, min~\{W_p\} = x - k )\]</content><author><name></name></author><summary type="html">Simulating networks of spiking neurons at large scale requires the implementation of some strategy to distribute neurons across different compute nodes. As the network evolves during the simulation some compute nodes may be required, at a given time instant, to process more spikes than other neurons, simply because of the chaotic activity of the network. This introduces a negative effect on simulation performance called load imbalance, by which the total wallclock time of the simulation is increased because some compute nodes must waste cycles waiting for others (who were tasked with a larger amount of computation) to finish.</summary></entry><entry><title type="html">Pratical Boltzmann Machines</title><link href="/2017/06/30/practical-boltzmann.html" rel="alternate" type="text/html" title="Pratical Boltzmann Machines" /><published>2017-06-30T11:53:10+00:00</published><updated>2017-06-30T11:53:10+00:00</updated><id>/2017/06/30/practical-boltzmann</id><content type="html" xml:base="/2017/06/30/practical-boltzmann.html">&lt;p&gt;This post collects a couple of tricks that I found useful for implementing, training and debugging Boltzmann Machines.
If you’re more interested in the theory, checkout my &lt;a href=&quot;/2017/06/23/boltzmann-machine.html&quot;&gt;Boltzmann Machines&lt;/a&gt; (BMs) post.&lt;/p&gt;

&lt;p&gt;In what follows, I have tried to collect as many doubts as possible among those that came up while implementing a BM (see  ).&lt;/p&gt;

&lt;h4 id=&quot;everything-is-so-slow&quot;&gt;Everything is so slow&lt;/h4&gt;

&lt;p&gt;Yes.
There doesn’t seem to be much you can do about that.
Training BMs is a costly business, and is very slow.
And after getting a residence permit in Switzerland, believe me, I know what slow means.&lt;/p&gt;

&lt;h4 id=&quot;neuron-activations&quot;&gt;Neuron activations&lt;/h4&gt;

&lt;p&gt;On the internet I have seen some people use \({0,1}\) as activation values, and others use \({-1,1}\).
From the little that I can tell, \({0,1}\) seem to be used more often in practice, whereas \({-1,1}\) appears often in the theoretical presentations.
Moreover, \({0,1}\) has a biological interpretation, corresponding to whether the neuron is active/inactive during that stimulus presentation.
However, \({-1,1}\) could somehow be related to excitatory/inhibitory neurons.
For both approaches all the neuron dynamics, learning rules and so on remain the same, so in the end it shouldn’t make much of a difference.
However, who knows if it makes a difference in terms of convergence speed?&lt;/p&gt;

&lt;h4 id=&quot;debugging-your-energy-computation&quot;&gt;Debugging your energy computation&lt;/h4&gt;

&lt;p&gt;You can reproduce the analytical example from &lt;a href=&quot;http://www.cs.toronto.edu/~tijmen/csc321/slides/lecture_slides_lec11.pdf&quot;&gt;slide 35&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;initialize a Boltzmann machine with 2 visible and 2 hidden units;&lt;/li&gt;
  &lt;li&gt;initialize the weight matrix with&lt;/li&gt;
&lt;/ul&gt;

\[W = \begin{matrix}
   0 &amp;amp; 0 &amp;amp; 2 &amp;amp; 0 \\
   0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 1 \\
   2 &amp;amp; 0 &amp;amp; 0 &amp;amp; -1 \\
   0 &amp;amp; 1 &amp;amp; -1 &amp;amp; 0 \end{matrix}\]

&lt;p&gt;and&lt;/p&gt;

\[\mathbf{b} = \mathbf{0}\]

&lt;ul&gt;
  &lt;li&gt;reproduce the table in the slide (also in my BM &lt;a href=&quot;/2017/06/23/boltzmann-machine.html&quot;&gt;blog post&lt;/a&gt; ), computing the energy for every combination of visible and hidden unit values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See the &lt;a href=&quot;https://github.com/sharkovsky/sharkovsky.github.io/blob/master/code/BM.ipynb&quot;&gt;code&lt;/a&gt; under the section “Debugging your energy computation” for a prototypical implementation.&lt;/p&gt;

&lt;h4 id=&quot;debugging-your-samplingsimulated-annealing-step&quot;&gt;Debugging your sampling/simulated annealing step&lt;/h4&gt;

&lt;p&gt;You can use the same analytical example from above.
In this case, I suggest computing the conditioned probabilities ( e.g. given that \(\mathbf{h} = \left [ 1,1 \right ]^T\) ).&lt;/p&gt;

&lt;p&gt;Looking at the relevant rows of the table from &lt;a href=&quot;http://www.cs.toronto.edu/~tijmen/csc321/slides/lecture_slides_lec11.pdf&quot;&gt;slide 35&lt;/a&gt;:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;v1&lt;/th&gt;
      &lt;th&gt;v2&lt;/th&gt;
      &lt;th&gt;h1&lt;/th&gt;
      &lt;th&gt;h2&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;-Energy&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;exp( -Energy)&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;joint probability \(p(\mathbf{v}, \mathbf{h})\)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;7.39&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.186&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;2.72&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.069&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.025&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;-1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.37&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.009&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;allows us to compute the conditioned partition function&lt;/p&gt;

\[Z_{|[1,1]} = 7.39 + 2.72 + 1 + 0.37 = 11.48\]

&lt;p&gt;and the conditioned probabilities&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;v1&lt;/th&gt;
      &lt;th&gt;v2&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;conditioned probability&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;7.39/11.48 = 0.643&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;2.72/11.48 = 0.237&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1/11.48 = 0.087&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.37/11.48 = 0.032&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Then you let your MCMC chain run to convergence (i.e. the BM is dreaming) many times, but always &lt;strong&gt;clamping the hidden state&lt;/strong&gt; to \([1,1]\), and you compute a histogram of the outcomes.&lt;/p&gt;

&lt;p&gt;In my &lt;a href=&quot;https://github.com/sharkovsky/sharkovsky.github.io/blob/master/code/BM.ipynb&quot;&gt;code&lt;/a&gt; you’ll find an implementation of this idea under the section “Test sampling/dreaming procedure”, although I was super lazy and computed the probability of \(v1+v2\) (a scalar) instead of the vector \([v1,v2]\), so that I could use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;numpy.histogram&lt;/code&gt; easily.&lt;/p&gt;

&lt;p&gt;A second way to do this is to randomly initialize the network at every time, and making sure that the joint probabilities respect the ones given in the table.&lt;/p&gt;

&lt;p&gt;Finally, a third way is to randomly initialize the hidden state to a randomly chosen value, and checking whether the outcome of the dream step respect the marginal probabilities of \(\mathbf{v}\).
To be honest, though, I’m not sure if this way is correct, because it sounds like you are making an assumption that the posterior probability of \(\mathbf{h}\) is uniform.&lt;/p&gt;

&lt;h4 id=&quot;simulated-annealing-schedule&quot;&gt;Simulated annealing schedule&lt;/h4&gt;

&lt;p&gt;Here again I have found some contradictory information on the internet.
I have seen both the phrase&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;as the temperature becomes a very small value&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;as the temperature tends to one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the end, the interpretation that I settled on is the following: as the temperature tends to 0, Boltzmann Machines become a deterministic model very similar to Hopfield nets.
I don’t think this is generally what you want, so I settled on the idea that the temperature should start at a value \(T_0 &amp;gt;&amp;gt; 1\) and tend to \(T_{end} = 1\).&lt;/p&gt;

&lt;p&gt;Keep in mind that if your starting temperature is too large than the first few iterations will be essentially random.
This can be useful if you have a large Boltzmann Machine for which the randomly chosen initial value could be very far the steady-state distribution, but can be annoying for small Boltzmann Machines.&lt;/p&gt;

&lt;p&gt;Another concept that I struggled with for some time is where the simulated annealing is used: is it “outside” the training loop, or “inside”?
In the end, I decided for the following interpretation: simulated annealing is used to help with the optimization process related to inference, and &lt;strong&gt;not&lt;/strong&gt; the gradient descent on the parameters.
Therefore, simulated annealing happens every time you do an inference step, i.e. every time you process a new data point or every iteration of the negative phase.
In this interpretation, the simulated annealing happens &lt;em&gt;inside&lt;/em&gt; the training loop, but &lt;em&gt;outside&lt;/em&gt; the Gibbs sampling loop.&lt;/p&gt;

&lt;p&gt;A few tips that I found useful are:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;if your input data is rescaled between -1 and 1 and you have a small-medium BM, then a decent starting value for the temperature can be around 20;&lt;/li&gt;
  &lt;li&gt;temperature should decrease pretty slowly: I found that making jumps larger than 5 was making convergence harder;&lt;/li&gt;
  &lt;li&gt;running about 20-40 Gibbs sampling loops per temperature value turned out to be pretty good for me;&lt;/li&gt;
  &lt;li&gt;running at least double the number of Gibbs sampling loops at the final temperature, to make sure everything is stable, seemed to help.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;training&quot;&gt;Training&lt;/h4&gt;

&lt;p&gt;Training has been, in my experience, extremely slow.
Learning rates larger than 0.1 seemed to pretty consistently end up exploding the model, and often I ended up using \(\eta = 0.01\) or even \(\eta = 0.005\).&lt;/p&gt;

&lt;p&gt;In the &lt;a href=&quot;https://github.com/sharkovsky/sharkovsky.github.io/blob/master/code/BM.ipynb&quot;&gt;code&lt;/a&gt; I knew the exact solution so I could compute the error.
Convergence was extremely slow:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/BM_convergence.png&quot; alt=&quot;BM convergence&quot; /&gt;&lt;/p&gt;

&lt;p&gt;and what’s more, even though the error curve looks pretty promising, the model itself does not yet reproduce the probability distribution in a particularly satisfactory way:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;condition&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;\(p(\mathbf{v} = [1,1])\)&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;\(p(\mathbf{v} = [1,0]~or~\mathbf{v} = [0,1] )\)&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;\(p(\mathbf{v} = [0,0])\)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;analytical model \(W_{analytic}\)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.15&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.48&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.37&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;setting \(W=W_{analytic}\)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.16&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.47&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.37&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;random initial conditions&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.29&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.51&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.2&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;after convergence&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.16&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.5&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.34&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id=&quot;discussion&quot;&gt;Discussion&lt;/h4&gt;

&lt;p&gt;My experience with understanding and training BMs has been somewhat difficult.
There is a lot of information on the internet for RBMs, but not so much original BMs.
However, the latter are the more &lt;em&gt;biologically realistic&lt;/em&gt;, which is ultimately what I was interested in.&lt;/p&gt;

&lt;p&gt;I have reached the point where the lowering of my standards due to exhaustion has allowed the current status quo to be sufficient for me to feel satisfied.
So overall, a good experience and a Goldmanesque metaphor for life.&lt;/p&gt;</content><author><name></name></author><summary type="html">This post collects a couple of tricks that I found useful for implementing, training and debugging Boltzmann Machines. If you’re more interested in the theory, checkout my Boltzmann Machines (BMs) post.</summary></entry><entry><title type="html">Boltzmann Machines</title><link href="/2017/06/23/boltzmann-machine.html" rel="alternate" type="text/html" title="Boltzmann Machines" /><published>2017-06-23T11:53:10+00:00</published><updated>2017-06-23T11:53:10+00:00</updated><id>/2017/06/23/boltzmann-machine</id><content type="html" xml:base="/2017/06/23/boltzmann-machine.html">&lt;p&gt;In this post belonging to a series dedicated to machine learning methods that are in some way &lt;em&gt;closer&lt;/em&gt; to biological networks than your run-of-the-mill fully connected network I will present Boltzmann Machines (BMs).
These neural networks are characterized by some properties that make them seem &lt;em&gt;biologically plausible&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;arbitrary recurrent connectivity;&lt;/li&gt;
  &lt;li&gt;inference is equivalent to energy minimization;&lt;/li&gt;
  &lt;li&gt;binary (and possibly stochastic) neuron dynamics;&lt;/li&gt;
  &lt;li&gt;unsupervised learning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For more details on why BMs can be considered biologically plausible, see my &lt;a href=&quot;/2017/06/20/bio-backprop.html&quot;&gt;other post&lt;/a&gt;.
Obviously, we are far from understanding how the brain works, and these are all mathematical abstractions only loosely related to the physical/biological phenomenon.&lt;/p&gt;

&lt;p&gt;The most prominent author of Boltzmann machine related papers is G. &lt;a href=&quot;http://www.scholarpedia.org/article/Boltzmann_machine&quot;&gt;Hinton&lt;/a&gt; (see his &lt;a href=&quot;https://www.cs.toronto.edu/~hinton/absps/guideTR.pdf&quot;&gt;practical guide&lt;/a&gt; ), but Y. &lt;a href=&quot;http://papers.nips.cc/paper/3048-greedy-layer-wise-training-of-deep-networks.pdf&quot;&gt;Bengio&lt;/a&gt;, I. &lt;a href=&quot;https://papers.nips.cc/paper/5024-multi-prediction-deep-boltzmann-machines.pdf&quot;&gt;Goodfellow&lt;/a&gt;, A. &lt;a href=&quot;http://www.cs.utoronto.ca/~kriz/learning-features-2009-TR.pdf&quot;&gt;Krizhevsky&lt;/a&gt; and others have made significant contributions.
Moreover, although I had to wander all the way to the second page of Google’s results to find it, &lt;a href=&quot;http://gorayni.blogspot.ch/2014/06/boltzmann-machines.html&quot;&gt;Gorayni’s blog post&lt;/a&gt; cleared many of my doubts about the actual implementation, and the &lt;a href=&quot;http://www.inference.org.uk/itprnn/book.pdf&quot;&gt;MacKay book&lt;/a&gt; he mentions turned out to be a gem of clarity for the theoretical aspects.&lt;/p&gt;

&lt;h4 id=&quot;why-boltzmann-machines&quot;&gt;Why Boltzmann machines?&lt;/h4&gt;

&lt;p&gt;Boltzmann machines are a &lt;em&gt;generative model&lt;/em&gt;.
This means that they can learn, in an unsupervised way, the probability distribution of your data \(p(data)\), or in a supervised way \(p(data | label)\).
As always, Andrew Ng’s &lt;a href=&quot;http://cs229.stanford.edu/notes/cs229-notes2.pdf&quot;&gt;explanation&lt;/a&gt; is worth a thousand of mine.&lt;/p&gt;

&lt;p&gt;Moreover, there are three ways (see &lt;a href=&quot;https://www.cs.toronto.edu/~hinton/absps/guideTR.pdf&quot;&gt;chapter 16&lt;/a&gt; ) in which one could use Boltzmann machines as a &lt;em&gt;discriminative model&lt;/em&gt; instead: train a classifier on top of a subset of the neurons, train a different BM for each class, or train a single BM with two sets of input neurons.&lt;/p&gt;

&lt;h3 id=&quot;network-topology&quot;&gt;Network topology&lt;/h3&gt;

&lt;p&gt;Boltzmann machines are characterized by arbitrary recurrent, &lt;strong&gt;symmetric&lt;/strong&gt; connections.
Usually, self connections are also disallowed.&lt;/p&gt;

&lt;p&gt;The neurons (or units) are split in two non-overlapping groups: &lt;em&gt;visible&lt;/em&gt; and &lt;em&gt;hidden&lt;/em&gt; units.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/boltzmann-net-topology.svg&quot; alt=&quot;boltzmann-topology&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This distinction is important because it allows the boltzmann machine to learn a &lt;em&gt;latent&lt;/em&gt; representation of the data, so the BM can learn not only \(p(data)\) but also \(p(data|hidden~units)\).
Moreover, hidden units allow the Boltzmann machine to model distributions over visible state vectors that cannot be modelled by direct pairwise interactions between the visible units (see section &lt;a href=&quot;http://www.scholarpedia.org/article/Boltzmann_machine&quot;&gt;learning with hidden units&lt;/a&gt;).
This distinction is also at the basis of &lt;a href=&quot;https://en.wikipedia.org/wiki/Restricted_Boltzmann_machine&quot;&gt;Restricted Boltzmann Machines&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;inference-as-energy-minimization&quot;&gt;Inference as energy minimization&lt;/h3&gt;

&lt;p&gt;This is the idea that the inference process consists of fixing some part of the network and letting the rest &lt;em&gt;evolve&lt;/em&gt; naturally to a state of lower energy.
This contrasts with a more static view of neural networks, where the activation of each neuron is computed only once, statically, as a function of the input values.
This point of view is not specific to BMs, actually is used to define a broad range of models under the umbrella term &lt;a href=&quot;http://www.iro.umontreal.ca/~bengioy/ift6266/H14/ftml-sec5.pdf&quot;&gt;energy based models&lt;/a&gt; and in particular was used before in &lt;a href=&quot;https://en.wikipedia.org/wiki/Hopfield_network&quot;&gt;Hopfield networks&lt;/a&gt;.
Y. Le Cun’s &lt;a href=&quot;http://yann.lecun.com/exdb/publis/pdf/lecun-06.pdf&quot;&gt;tutorial&lt;/a&gt; is a good starting point for understanding energy based models.&lt;/p&gt;

&lt;p&gt;I still find it difficult to understand the relationship between inference and energy in an intuitive way.
I will now try to explain the explanation that I’ve been giving to myself:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;in a stochastic neural network, performing inference means letting the network evolve, possibly under some constraints, until a state that is in some sense stable is reached;&lt;/li&gt;
  &lt;li&gt;in this sense, inference is then equivalent to sampling from the probability distribution of possibile network states;&lt;/li&gt;
  &lt;li&gt;the relationship between the probability distribution of the network states and the energy has a &lt;a href=&quot;https://www.quora.com/How-are-energy-based-models-in-deep-learning-related-to-probability&quot;&gt;simple formulation&lt;/a&gt;
\(p(\mathbf{x}) = \frac{e^{ -E(\mathbf{x}) }}{Z}\);&lt;/li&gt;
  &lt;li&gt;given the formula above, states with lower energy have a higher probability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that in this setting energies can have negative values.&lt;/p&gt;

&lt;p&gt;Finally, the example on &lt;a href=&quot;http://www.cs.toronto.edu/~tijmen/csc321/slides/lecture_slides_lec11.pdf&quot;&gt;slide 35&lt;/a&gt; is what really made the idea click for me.
This exmaple is very useful to understand how &lt;strong&gt;connection weights define a probability distribution&lt;/strong&gt;.
Consider the following, very simple, Boltzmann machine with an 2-dimensional input vector \(\mathbf{v} = (v1,v2)\) and two hidden units.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/weights-def-proba.svg&quot; alt=&quot;weights define probability&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Then we can compute the probability distribution \(p(\mathbf{v})\) induced by the weights by considering all possible combinations of inputs and hidden units, following this process:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;compute the energy associated with every network configuration (formula in section below);&lt;/li&gt;
  &lt;li&gt;compute the &lt;em&gt;partition function&lt;/em&gt;, i.e. the normalizing factor \(Z\), as the sum over all possible combinations \(Z = \sum\limits_{\mathbf{v},\mathbf{h}} e^{-E(\mathbf{v},\mathbf{h})}\);&lt;/li&gt;
  &lt;li&gt;compute the probability associated with a particular network configuration \(( \mathbf{v_0}, \mathbf{h_0})\)as \(\frac{ e^{-E(\mathbf{v_0}, \mathbf{h_0})} }{Z}\);&lt;/li&gt;
  &lt;li&gt;the probability distribution of the input data is given by the total probability \(p(\mathbf{v}) = \sum\limits_{\mathbf{h}} p(\mathbf{v}, \mathbf{h})\).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And we can build a table exactly as in &lt;a href=&quot;http://www.cs.toronto.edu/~tijmen/csc321/slides/lecture_slides_lec11.pdf&quot;&gt;slide 35&lt;/a&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;v1&lt;/th&gt;
      &lt;th&gt;v2&lt;/th&gt;
      &lt;th&gt;h1&lt;/th&gt;
      &lt;th&gt;h2&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;-Energy&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;exp( -Energy)&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;joint probability \(p(\mathbf{v}, \mathbf{h})\)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;7.39&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.186&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;7.39&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.186&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;2.72&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.069&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.025&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;2.72&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.069&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;7.39&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.186&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.025&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.025&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.025&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.025&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;2.72&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.069&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.025&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;-1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0.37&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.009&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.025&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.025&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.025&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;which allows us to compute the marginalized probabilities&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;\(\mathbf{v}\)&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;marginal probability \(p(\mathbf{v})\)&lt;/th&gt;
      &lt;th&gt;formula&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;(1,1)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.466&lt;/td&gt;
      &lt;td&gt;.186 + .186 + .069 + .025&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(1,0)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.305&lt;/td&gt;
      &lt;td&gt;.069 + .186 + .025 + .025&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(0,1)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.144&lt;/td&gt;
      &lt;td&gt;.025 + .025 + .069 + .025&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(0,0)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;.084&lt;/td&gt;
      &lt;td&gt;.009 + .025 + .025 + .025&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;definition-of-energy&quot;&gt;Definition of energy&lt;/h3&gt;

&lt;p&gt;How did we compute the enrgy values in the table above?
The formula is actually pretty simple: consider the symmetric connection weight matrix \(W\), biases \(\mathbf{b}\) and the state vector \(\mathbf{s} = (\mathbf{v}, \mathbf{h})\), then the (scalar) energy is given by:&lt;/p&gt;

\[E = -\frac{1}{2}\mathbf{s}^TW\mathbf{s} - \mathbf{b}^T\mathbf{s}.\]

&lt;p&gt;N.B. vectors are column vectors here.
To see this formula in action, consider the BM above.
Then&lt;/p&gt;

\[W = \begin{matrix}
0 &amp;amp; 0 &amp;amp; 2 &amp;amp; 0 \\
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 1 \\
2 &amp;amp; 0 &amp;amp; 0 &amp;amp; -1 \\
0 &amp;amp; 1 &amp;amp; -1 &amp;amp; 0 \end{matrix},\]

&lt;p&gt;and the biases are all zero.
The energy of configuration \(\mathbf{s} = ( 1, 0, 1, 1)\) would be \(E = -\frac{1}{2} ( 2w_{13} + 2w_{14} + 2w_{34}) = -2 - 0 - (-1) = -1\).&lt;/p&gt;

&lt;h3 id=&quot;the-neuron-dynamics-during-inference&quot;&gt;The neuron dynamics during inference&lt;/h3&gt;

&lt;p&gt;BM neurons can take only two values: 1 and 0.
The crucial idea behind letting BMs states’ evolve is that it must be done in a sequential way, &lt;em&gt;one neuron at a time&lt;/em&gt;.
In theory, one can select a candidate transition for the \(i^{th}\) neuron, compute the difference in energy between the original and the transitioned state, and decide whether to reject or keep the transition based on the probability&lt;/p&gt;

&lt;p&gt;\(p ( keep~transition) = \frac{1}{1 + e^{E(transition) - E(original)} }\).&lt;/p&gt;

&lt;p&gt;Incidentally, this is what I do in my code.&lt;/p&gt;

&lt;p&gt;Otherwise, the &lt;a href=&quot;https://en.wikipedia.org/wiki/Boltzmann_machine&quot;&gt;wikipedia page&lt;/a&gt; gives a nice derivation of the formula for how energy relates to the probability of the \(i^{th}\) neuron being on or off and which also explains where the formula for the above probability comes from.
The gist is the following: we rely on the property of &lt;a href=&quot;https://en.wikipedia.org/wiki/Boltzmann_distribution&quot;&gt;Boltzmann distributions&lt;/a&gt; that the energy of a state is proportional to the negative log probability of that state:&lt;/p&gt;

&lt;p&gt;\(E( \mathbf{s}_i = 0 ) - E( \mathbf{s}_i = 1 ) = ln( p( \mathbf{s}_i = 1 ) ) - ln( p( \mathbf{s}_i = 0 ) )\),&lt;/p&gt;

&lt;p&gt;and by doing some algebra we get to&lt;/p&gt;

\[p( \mathbf{s}_i = 1 ) = \frac{1}{1 + e^{ E( \mathbf{s}_i = 1 ) - E( \mathbf{s}_i = 0 ) } } .\]

&lt;p&gt;So far we have stated the formulas that allow to decide whether the state transition of a single neuron should happen, so how does this fit in the global picture?&lt;/p&gt;

&lt;h1 id=&quot;enter-mcmc&quot;&gt;Enter MCMC&lt;/h1&gt;

&lt;p&gt;No, MCMC is not a rapper suffering from &lt;a href=&quot;https://en.wikipedia.org/wiki/Palilalia&quot;&gt;Palilalia&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://theclevermachine.wordpress.com/2012/11/19/a-gentle-introduction-to-markov-chain-monte-carlo-mcmc/&quot;&gt;Markov Chain Monte Carlo (MCMC)&lt;/a&gt; methods, and in particular &lt;a href=&quot;https://theclevermachine.wordpress.com/2012/11/05/mcmc-the-gibbs-sampler/&quot;&gt;Gibbs sampling&lt;/a&gt;, are the answer to the above problem.
The idea is the following: the overall state of the network’s dynamics follow a Markov Chain, where each state transition’s probability is defined as above; to compute the dynamics of the network, we need to run this Markov Chain to convergence.
The concept of convergence here is defined mystically as the moment when the probability distribution of the states does not depend anymore on the initial value, but is only a function of the energy of each state.
In practice, I have not found a single resource explaining how to understand whether your network has &lt;em&gt;converged&lt;/em&gt; or not, and in  a later section I’ll explain how I did it for a very very very simple case.&lt;/p&gt;

&lt;p&gt;To run the Markov Chain to convergence, we use the MCMC method which consists in repeatedly sampling from the distribution of possible states.
In particular, we repeatedly use Gibbs sampling:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;until &lt;em&gt;convergence&lt;/em&gt;, do:
    &lt;ul&gt;
      &lt;li&gt;for each neuron in the network, do:
        &lt;ul&gt;
          &lt;li&gt;compute transition probability for this neuron, &lt;strong&gt;keeping the rest fixed&lt;/strong&gt;&lt;/li&gt;
          &lt;li&gt;randomly decide to keep or reject transition according to probability&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As we said before, given the mystical definition of &lt;em&gt;convergence&lt;/em&gt;, we often run the above loop for a fixed number of iterations.
As a cheaper alternative, we can resort to simulated annealing instead.&lt;/p&gt;

&lt;h1 id=&quot;simulated-annealing&quot;&gt;Simulated annealing&lt;/h1&gt;

&lt;p&gt;Running the MCMC to convergence everytime can be very resource consuming.
To mitigate this problem, we adopt an optimization technique called &lt;a href=&quot;https://en.wikipedia.org/wiki/Simulated_annealing&quot;&gt;simulated annealing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Simulated annealing introduces the concept of &lt;em&gt;temperature&lt;/em&gt; \(T\), a value which modifies the transition probability to:&lt;/p&gt;

\[p ( keep~transition) = \frac{1}{1 + e^{ \frac{ E(transition) - E(original)}{T}} }.\]

&lt;p&gt;This is how the the Markov Chain loop is modified in the presence of simulated annealing:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;given an &lt;em&gt;annealing schedule&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;for each (temperature value \(T\), number of iterations \(N\)) tuple in the &lt;em&gt;annealing schedule&lt;/em&gt;, do:
    &lt;ul&gt;
      &lt;li&gt;repeat the full Gibbs sampling procedure \(N\) times, at a temperature value of \(T\).&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;training-finally&quot;&gt;Training… Finally!&lt;/h3&gt;

&lt;p&gt;If inference means energy minimization, then we want to find a training procedure that will arrange the connection weights such that the observed data have very low energy.
An equivalent way to put it is to say that we want to find weights and biases that define a Boltzmann distribution in which the training vectors have high probability.
Following the procedure described on the &lt;a href=&quot;http://www.scholarpedia.org/article/Boltzmann_machine&quot;&gt;scholarpedia page&lt;/a&gt;, assuming the training data were sampled i.i.d. we can write a gradient ascent formula for the log likelihood as&lt;/p&gt;

\[\Delta W = \sum\limits_{\mathbf{v}} ln \left ( \frac{ d p(\mathbf{v},\mathbf{h}) }{dW} \right ).\]

&lt;p&gt;To compute the expression above we need to define two quantities:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;\(\mathbf{s}^{\mathbf{v}}\) is a sample of the network state vector that was obtained by running the MCMC procedure to &lt;em&gt;convergence&lt;/em&gt;, &lt;strong&gt;while keeping the input vector \(\mathbf{v}\) fixed&lt;/strong&gt;;&lt;/li&gt;
  &lt;li&gt;\(\mathbf{s}^m\) is a sample of the network state vector that was obtained by running the MCMC procedure to &lt;em&gt;convergence&lt;/em&gt; having left all units unclamped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given that we know the relationship between the probability \(p(\mathbf{v},\mathbf{h})\) and the energy, i.e. \(p(\mathbf{v},\mathbf{h}) = \frac{ e^{-E(\mathbf{v})}}{\sum\limits_{\mathbf{u},\mathbf{g}} e^{-E(\mathbf{u},\mathbf{g})} }\) we can &lt;a href=&quot;https://theclevermachine.wordpress.com/tag/gibbs-sampling/&quot;&gt;write&lt;/a&gt;:&lt;/p&gt;

\[\Delta W = \sum\limits_{\mathbf{h}} p(\mathbf{h} | \mathbf{v} )\mathbf{s}^{\mathbf{v}}(\mathbf{s}^{\mathbf{v}})^T - \sum\limits_{\mathbf{h},\mathbf{v}} p(\mathbf{v}, \mathbf{h} )\mathbf{s}^m(\mathbf{s}^m)^T .\]

&lt;p&gt;Computing the exact quantities above requires summing over many many possible configurations.
In practice this can rapidly become unfeasible, so we substitute the above quantity with:&lt;/p&gt;

\[\Delta W = \frac{1}{N_{\mathbf{v}}}\sum\limits_{\mathbf{v}} \mathbf{s}^{\mathbf{v}}(\mathbf{s}^{\mathbf{v}})^T -\frac{1}{N_m} \sum \mathbf{s}^m(\mathbf{s}^m)^T,\]

&lt;p&gt;which corresponds to the following procedure (with a learning rate \(\eta\)):&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;initialize updates \(\Delta W = 0, \Delta \mathbf{b} = 0\);&lt;/li&gt;
  &lt;li&gt;for every training data point, do:
    &lt;ul&gt;
      &lt;li&gt;keeping the visible units clamped to the values of the training data point, run MCMC to convergence;&lt;/li&gt;
      &lt;li&gt;update  \(\Delta W = \Delta W + \mathbf{s}\mathbf{s}^T, \Delta \mathbf{b} = \Delta \mathbf{b} + \mathbf{s}\);&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;update \(W = W+ \frac{\eta}{N_{\mathbf{v}}} \Delta W, \mathbf{b} = \mathbf{b} + \frac{\eta}{N_{\mathbf{v}}} \Delta \mathbf{b}\);&lt;/li&gt;
  &lt;li&gt;reset updates \(\Delta W = 0, \Delta \mathbf{b} = 0\);&lt;/li&gt;
  &lt;li&gt;for a certain number iterations \(N_m\), do:
    &lt;ul&gt;
      &lt;li&gt;allow the MCMCM to run to convergence, without clamping anything;&lt;/li&gt;
      &lt;li&gt;update  \(\Delta W = \Delta W + \mathbf{s}\mathbf{s}^T, \Delta \mathbf{b} = \Delta \mathbf{b} + \mathbf{s}\);&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;update \(W = W - \frac{\eta}{N_m} \Delta W, \mathbf{b} = \mathbf{b} - \frac{\eta}{N_m} \Delta \mathbf{b}\);&lt;/li&gt;
&lt;/ul&gt;</content><author><name></name></author><summary type="html">In this post belonging to a series dedicated to machine learning methods that are in some way closer to biological networks than your run-of-the-mill fully connected network I will present Boltzmann Machines (BMs). These neural networks are characterized by some properties that make them seem biologically plausible: arbitrary recurrent connectivity; inference is equivalent to energy minimization; binary (and possibly stochastic) neuron dynamics; unsupervised learning.</summary></entry><entry><title type="html">A Summary of Biologically Plausible Deep Learning and Equilibrium Propagation</title><link href="/2017/06/20/bio-backprop.html" rel="alternate" type="text/html" title="A Summary of Biologically Plausible Deep Learning and Equilibrium Propagation" /><published>2017-06-20T11:53:10+00:00</published><updated>2017-06-20T11:53:10+00:00</updated><id>/2017/06/20/bio-backprop</id><content type="html" xml:base="/2017/06/20/bio-backprop.html">&lt;p&gt;In the past couple of years, Y. Bengio has been interested in the question of whether and how could backpropagation be implemented in a biologically-realistic framework.
I find his work extremely interesting, and have decided to summarize &lt;em&gt;my own personal understanding&lt;/em&gt; of it in this blog post.&lt;/p&gt;

&lt;p&gt;In particular, I will try to combine information from these three papers:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;[1] &lt;a href=&quot;https://arxiv.org/abs/1502.04156&quot;&gt;Towards Biologically Plausible Deep Learning&lt;/a&gt; (v3 on ArXiv) ;&lt;/li&gt;
  &lt;li&gt;[2] &lt;a href=&quot;https://arxiv.org/abs/1510.02777&quot;&gt;Early Inference in Energy-Based Models Approximates Back-Propagation&lt;/a&gt; (v2 on ArXiv);&lt;/li&gt;
  &lt;li&gt;[3] &lt;a href=&quot;https://arxiv.org/abs/1602.05179&quot;&gt;Equilibrium Propagation: Bridging the Gap Between Energy-Based Models and Backpropagation&lt;/a&gt; (v5 on ArXiv).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-problem-of-credit-assignment&quot;&gt;The problem of credit assignment&lt;/h2&gt;

&lt;p&gt;One of the main motivations for Bengio’s work (see [1]) is finding a&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;credible machine learning interpretation of the learning rules that exist in biological neurons […] accounting for credit assignment through a long chain of neural connections.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words, answering the question of &lt;em&gt;how does the brain propagate an error?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are many aspects to this question, all of which represent interesting unsolved problems:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;how does the brain propagate the signal that a certain prediction was wrong, through its many hidden layers and recurrent connections?&lt;/li&gt;
  &lt;li&gt;how does the brain propagate the signal of the magnitude of a given error?&lt;/li&gt;
  &lt;li&gt;how does the brain interpret that signal and produce a valid update of its synaptic weights?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now is a good time to make an important distinction: obviously there are several physiological implementations of all the mechanisms cited above (e.g. &lt;a href=&quot;https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3367554/&quot;&gt;NDMA recruitment&lt;/a&gt; in Long Term synaptic Potentiation), and studying these biological mechanisms is an extremely important aspect of neuroscience.
However, in this work we are going to be taking a more abstract view, and asking ourselves the question of &lt;em&gt;what are the relevant computational (mathematical) aspects related to the questions above?&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;biologically-plausible-whatever-do-you-mean&quot;&gt;Biologically plausible, whatever do you mean?&lt;/h2&gt;

&lt;p&gt;Well, this is where the boundaries of objectivity start to become blurred like the mists of Avalon.
There are many many properties observed experimentally in &lt;em&gt;biological&lt;/em&gt; neural networks, and ideally a biologically-plausible artificial neural network would implement them all.
Since this is proving to be very hard, researchers must make decisions on which experimentally observed properties they’d like to preserve in their models.
&lt;em&gt;Enter subjectivity.&lt;/em&gt;&lt;/p&gt;

&lt;h4 id=&quot;reasons-why-backprop-is-not-biologically-plausible&quot;&gt;Reasons why backprop is not biologically plausible&lt;/h4&gt;

&lt;p&gt;The paper [1] cites 6 reasons why backprop is not biologically plausible:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;backpropagation is &lt;em&gt;purely linear&lt;/em&gt; (I’m not sure in what sense);&lt;/li&gt;
  &lt;li&gt;if backpropagation were a thing (biologically), the feedback paths would require exact knowledge of the derivatives of the activation functions of neurons, and to this time this has never been observed experimentally;&lt;/li&gt;
  &lt;li&gt;similary, feedback paths would be required to have the same exact weights (but transposed) of the feedforward paths, a.k.a the &lt;a href=&quot;https://arxiv.org/pdf/1411.0247.pdf&quot;&gt;weight transport problem&lt;/a&gt;;&lt;/li&gt;
  &lt;li&gt;backpropagation is based on the idea that communication between neurons happens via (smooth) continuous values, but experimental evidence shows that neurons communicate via binary discontinuous events (spikes) although there is still a lot of &lt;a href=&quot;http://romainbrette.fr/why-do-neurons-spike/&quot;&gt;discussion&lt;/a&gt; on how the brain interprets these events;&lt;/li&gt;
  &lt;li&gt;backpropagation happens in two phases (feedforward and backprop), which would require the brain to have a perfectly synchronized clock that alternates between the two;&lt;/li&gt;
  &lt;li&gt;there is no widely-accepted theory of what the output targets (i.e. cost function) of the brain should be, nor what its biological substrate would look like.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;boltzmann-machines-as-a-biologically-plausible-model&quot;&gt;Boltzmann machines as a biologically plausible model&lt;/h4&gt;

&lt;p&gt;In Bengio’s point of view, &lt;a href=&quot;http://www.scholarpedia.org/article/Boltzmann_machine&quot;&gt;Boltzmann machines&lt;/a&gt; represent the most biologically plausible learning algorithm for deep architectures.
I will try to summarize what I think can be some explanations for this:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Boltzmann machines are based on (stochastic) binary units, so they address concern number 4 above.
These units are often divided in &lt;em&gt;visible&lt;/em&gt; and &lt;em&gt;hidden&lt;/em&gt; units, and in the case of &lt;a href=&quot;https://en.wikipedia.org/wiki/Restricted_Boltzmann_machine&quot;&gt;Restricted Boltzmann Machines&lt;/a&gt; this division gives rise to two separate layers without inter-layer connections.&lt;/li&gt;
  &lt;li&gt;Boltzmann machines do not implement backprop but an &lt;a href=&quot;http://ac.els-cdn.com/S0364021385800124/1-s2.0-S0364021385800124-main.pdf?_tid=4f334284-55d2-11e7-8e6b-00000aab0f27&amp;amp;acdnat=1497974921_7ca4e6ecdfae3729c94e4c45295808c0&quot;&gt;unsupervised learning&lt;/a&gt; algorithm, which addresses concern 6 above.
Indeed, Boltzmann machines are a &lt;em&gt;generative model&lt;/em&gt; in the sense that they learn a probability distribution of the input dataset conditioned on some internal representation.
In order to use BMs as a classifier, one needs to additionally train a classifier on top of the hidden units, an approach that is reminiscent of &lt;a href=&quot;https://en.wikipedia.org/wiki/Reservoir_computing&quot;&gt;reservoir computing&lt;/a&gt;.
In this case, however, the concerns raised in point 6 are still valid, as it is not clear what the output targets of the braing would be.&lt;/li&gt;
  &lt;li&gt;Training Boltzmann machines does not require explicitly computing any derivative, which addresses point 2.&lt;/li&gt;
  &lt;li&gt;I’m not sure how point 1 above should be interpreted, but Bengio’s work implicitly assumes that it is also addressed by Boltzmann machines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unfortunately, two issues remain with the biological plausibility of BMs: the &lt;em&gt;weight transport problem&lt;/em&gt; (point 3) and the two-phase training (point 5).&lt;/p&gt;

&lt;h1 id=&quot;equilibrium-propagation-an-even-more-biologically-plausible-model&quot;&gt;Equilibrium propagation: an even more biologically plausible model&lt;/h1&gt;

&lt;p&gt;Bengio proposes in [2] a new learning framework that addresses the issue of two-phase learning.
Bengio’s framework doesn’t completely remove the second phase, but still addresses the issue in the sense that &lt;em&gt;only one kind of neural computation is required for both phases&lt;/em&gt;, something that is not true for standard Boltzmann machines.&lt;/p&gt;

&lt;p&gt;The main steps of the Equilibrium Propagation algorithms are:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;inference is energy minimization, just like in Boltzmann machines, although the actual formual used for the energy is different;&lt;/li&gt;
  &lt;li&gt;we distinguish three subsets of the units in the network, namely input, hidden and output units;&lt;/li&gt;
  &lt;li&gt;a total energy is defined as the sum of an internal energy the models the interactions within the network and an external energy (modulated by a constant) that models how the targets influence the output units;&lt;/li&gt;
  &lt;li&gt;learning happens in two phases: the &lt;em&gt;free phase&lt;/em&gt; where all the neurons are performing inference, and the &lt;em&gt;weakly clamped phase&lt;/em&gt; where the output units are nudged towards an (observed) target;&lt;/li&gt;
  &lt;li&gt;the input units are always clamped, the hidden units are always free, the output units are free during the first phase and &lt;em&gt;weakly clamped&lt;/em&gt; during the second phase;&lt;/li&gt;
  &lt;li&gt;weights are updated according to a rule with a formulation similar to classical Boltzmann machines, but which is shown [2,3] to approximate gradient descent on the least squares error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are some additional compelling arguments for the biological plausibility of Equilibrium Propagation:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;although the two-phases are not completely removed, the fact that they require the same kind of neural computation means that they are more open to biologically-plausible interpretations, for example
    &lt;ul&gt;
      &lt;li&gt;when you move your arm you make a prediction of where it is going to be, and your eyes allow you to see if the prediction is correct;&lt;/li&gt;
      &lt;li&gt;you see an image and you are trying to understand what it contains, and after a while a “teacher” tells you its contents;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Bengio et al. show in [1,3] that their learning rule can be interpreted as the classic &lt;a href=&quot;http://www.scholarpedia.org/article/Spike-timing_dependent_plasticity&quot;&gt;STDP learning rule&lt;/a&gt;;&lt;/li&gt;
&lt;/ul&gt;</content><author><name></name></author><summary type="html">In the past couple of years, Y. Bengio has been interested in the question of whether and how could backpropagation be implemented in a biologically-realistic framework. I find his work extremely interesting, and have decided to summarize my own personal understanding of it in this blog post.</summary></entry><entry><title type="html">A naive explanation of backprop through time for recurrent neural networks</title><link href="/2017/06/12/naive-recurrent.html" rel="alternate" type="text/html" title="A naive explanation of backprop through time for recurrent neural networks" /><published>2017-06-12T11:53:10+00:00</published><updated>2017-06-12T11:53:10+00:00</updated><id>/2017/06/12/naive-recurrent</id><content type="html" xml:base="/2017/06/12/naive-recurrent.html">&lt;p&gt;In the second blog post of this series, I would like to repeat the same detailed computations for backpropagating the error in the case of &lt;em&gt;recurrent neural networks&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;As I mentioned before there are &lt;em&gt;many&lt;/em&gt; other explanations of backprop out there, much better than mine.
In addition to &lt;a href=&quot;http://neuralnetworksanddeeplearning.com/chap2.html&quot;&gt;M. Nielsen’s book&lt;/a&gt; and the &lt;a href=&quot;http://www.deeplearningbook.org/&quot;&gt;deep learning book&lt;/a&gt;, I’d like to mention a few other blogs that helped me understand things specifically in the case of recurrent networks.
A. Karpathy’s &lt;a href=&quot;http://karpathy.github.io/2015/05/21/rnn-effectiveness/&quot;&gt;unreasonable effectiveness of recurrent neural networks&lt;/a&gt; is one of the most exciting resources on RNNs out there.
Chen’s &lt;a href=&quot;https://arxiv.org/pdf/1610.02583&quot;&gt;arXiv paper&lt;/a&gt; is a nice, academic style, introduction to the topic, Lipton’s &lt;a href=&quot;https://arxiv.org/pdf/1506.00019.pdf&quot;&gt;review paper&lt;/a&gt; gives a nice overview of the challenges related to training recurrent networks, while H. Jaeger’s &lt;a href=&quot;http://minds.jacobs-university.de/sites/default/files/uploads/papers/ESNTutorialRev.pdf&quot;&gt;tutorial&lt;/a&gt; is a &lt;em&gt;mathematically intense but complete&lt;/em&gt; presentation which then moves on to talk about reservoir computing.
Finally, I took the idea for the training set from P. Roelants’ &lt;a href=&quot;http://peterroelants.github.io/posts/rnn_implementation_part01/&quot;&gt;blog post&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;what-is-a-recurrent-neural-network&quot;&gt;What is a recurrent neural network?&lt;/h2&gt;

&lt;p&gt;Recurrent Neural Networks (RNN) are special networks in which the concept of time is explicitly modeled.
Originally, they were developed to deal with input data in the form of sequences, where each &lt;em&gt;timestep&lt;/em&gt; corresponds to the processing of one element of the input sequence.
The main difference with feedforward networks is that:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;in RNNs, each neuron is characterized by a state (a variable) whose value can change according to some rules, but whose presence is persistent through time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In practice this means that we need an uglier notation to represent neurons, as shown in this table:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Network Type&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Notation&lt;/th&gt;
      &lt;th&gt;Description&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;feedforward&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;\(h^l_i\)&lt;/td&gt;
      &lt;td&gt;hidden state of the \(i^{th}\) neuron in the \(l^{th}\) layer&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;feedforward&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;\(\mathbf{h}^l\)&lt;/td&gt;
      &lt;td&gt;hidden state of the vector of neurons in the \(l^{th}\) layer&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;recurrent&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;\(h^l_{n,i}\)&lt;/td&gt;
      &lt;td&gt;hidden state of the \(i^{th}\) neuron in the \(l^{th}\) layer &lt;em&gt;at timestep&lt;/em&gt; \(n\)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;recurrent&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;\(\mathbf{h}^l_n\)&lt;/td&gt;
      &lt;td&gt;hidden state of the vector of neurons in the \(l^{th}\) layer &lt;em&gt;at timestep&lt;/em&gt; \(n\)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Typically we represent RNNs like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/recurrent-net.svg&quot; alt=&quot;rnn-net&quot; /&gt;&lt;/p&gt;

&lt;p&gt;If you compare this to the representation of a &lt;a href=&quot;../07/naive-backprop.html&quot;&gt;feedforward network&lt;/a&gt; you’ll notice the &lt;em&gt;recurrent&lt;/em&gt; arrows connecting a neuron to itself.
These arrows can be misleading because one may be tempted to think that a neuron’s state is immediately affected by the recurrent connections.
Instead, what happens is that&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;recurrent connections affect the state of a neuron through some delay (typically, a single timestep).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;the-layer-wise-point-of-view&quot;&gt;The layer-wise point of view&lt;/h4&gt;

&lt;p&gt;As we saw in the post on &lt;a href=&quot;../07/naive-backprop.html&quot;&gt;feedforward networks&lt;/a&gt;, it is often useful to take the layer-wise point of view by considering the full vector of neurons in each layer.
First, consider the visualization of a feedforward network which I have stretched out using my mad graphic skillz in order to give the 3D impression.
Here, I am going to use a nomenclature that no-one else uses, but I promise it’s only for a minor thing: I am going to refer to the different layers of a network as the &lt;em&gt;space dimension&lt;/em&gt; of the network.
The reason behind this is simply to contrast it to the &lt;em&gt;time dimension&lt;/em&gt; of recurrent neural networks (a nomenclature that everybody likes).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/feedforwards-layers.svg&quot; alt=&quot;feedforward-3D&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In the image, each ball is a neuron and each rectangle is a layer.
I have drawn all the connections, but only higlighted those originating from a specific neuron.
Keep an eye on that neuron for later.&lt;/p&gt;

&lt;p&gt;Now compare this to the image of a recurrent neural network, which has both a space and a time dimension.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/recurrent-layers.svg&quot; alt=&quot;recurrent-3D&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In the image, the same neural network from before is now &lt;em&gt;repeated&lt;/em&gt; through time, in the sense that the state of each neuron is is computed ad each timestep.
The highlighted neuron from the previous image now not only sends connections to the next layer, but also &lt;em&gt;sends connections within the same layer, to the next timestep&lt;/em&gt;.
This image would have been a mess had I drawn every single recurrent connection, so as an example I concentrated on the red neuron and drew all the incoming connections (from the previous layer at the same timestep, and from the same layer at the previous timestep) and all the outgoing connections (to the next layer at the same timestep, and to the same layer at the next timestep).
With your third eye, try to imagine that every recurrent layer sends connections both to its downstream layer in space, and to itself at the next timestep.&lt;/p&gt;

&lt;p&gt;In terms of notation, we need to distinguish between the weight matrix of recurrent connections, and the weight matrix of feedforward connections.
I am going to use a notation that is nonstandard, so let me apologize now.
Sorry.
I am going to denote the weight matrix of recurrent connections with an \(T\) left-subscript.
The \(T\) stands for the time dimension.
Similary, the feedforward weight matrix will be denoted with an \(S\) left-subscript, where \(S\) stands for (you guessed it) space.
See how I niftly combined all the nonstandard notations in one mess?
Here is a summary for you:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Weights&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Notation&lt;/th&gt;
      &lt;th&gt;Alternative Notations&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;feedforward from layer \(l-1\) to layer \(l\)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;\({}_SW^l\)&lt;/td&gt;
      &lt;td&gt;\(W^l, W^l_{xh}\)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;recurrent layer \(l\)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;\({}_TW^l\)&lt;/td&gt;
      &lt;td&gt;\(W^l_hh\)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Obviously, it is possible to mix non-recurrent and recurrent layers, and in this case only the recurrent layers would behave as I have been describing so far.
Also, it is possible to prescribe recurrent layers that do not connect to the future selves, but connect to future other layers.&lt;/p&gt;

&lt;h4 id=&quot;formulas-are-not-just-for-babies&quot;&gt;Formulas are not just for babies&lt;/h4&gt;

&lt;p&gt;If you think a formula can be more expressive than a thousand words, here is the algorithm to perform &lt;strong&gt;inference&lt;/strong&gt; on a recurrent layer \(l\)&lt;/p&gt;

\[\begin{align*}
    &amp;amp;\text{for every timestep } n\\
    &amp;amp;\mathbf{z}^l_n = {}_SW^l\mathbf{h}^{l-1}_n + {}_TW^l\mathbf{h}^l_{n-1} \\
    &amp;amp;\mathbf{h}^l_n = \sigma ( \mathbf{z}^l_n )
    \end{align*}\]

&lt;h2 id=&quot;bptt-backpropagation-through-time&quot;&gt;BPTT: Backpropagation Through Time&lt;/h2&gt;

&lt;p&gt;How do we apply the principles of backpropagation in a recurrent neural network?
The principle is the same as for a feedforward network, and indeed many sources claim that &lt;em&gt;training recurrent networks is just like training feedforward networks with unrolling.&lt;/em&gt;
I found this statement quite obscure, and preferred writing out the formulas explicitly.&lt;/p&gt;

&lt;h4 id=&quot;simple-example&quot;&gt;Simple example&lt;/h4&gt;

&lt;p&gt;As we did for feedforward nets, let’s start by considering the very simple example of a scalar network.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/recurrent-net.svg&quot; alt=&quot;rnn-net&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In this uber-simple network, the output \(y\) is simply the hidden state of the neuron in the last layer.&lt;/p&gt;

&lt;p&gt;Everyone has to start somewhere, so suppose we initialize the hidden states with some arbitrary initial values \(h^1_0, h^2_0\).
At the first timestep, the networks receives the first element of the sequence \(x_1\), and processes it by:&lt;/p&gt;

\[\begin{align*}
    h^1_1 &amp;amp;= \sigma \left ( {}_Sw^1x_1 + {}_Tw^1h^1_0 \right ) \\
    h^2_1 &amp;amp;= \sigma \left ( {}_Sw^2h^1_1 + {}_Tw^2h^2_0 \right ) \\
    y_1 &amp;amp;= h^2_1.
    \end{align*}\]

&lt;p&gt;At the next timestep, we have:&lt;/p&gt;

\[\begin{align*}
    h^1_2 &amp;amp;= \sigma \left ( {}_Sw^1x_2 + {}_Tw^1h^1_1 \right ) \\
    h^2_2 &amp;amp;= \sigma \left ( {}_Sw^2h^1_2 + {}_Tw^2h^2_1 \right ) \\
    y_2 &amp;amp;= h^2_2,
    \end{align*}\]

&lt;p&gt;which by the magic of substitution is nothing other than&lt;/p&gt;

\[\begin{align*}
    h^1_2 &amp;amp;= \sigma \left ( {}_Sw^1x_2 + {}_Tw^1  \sigma \left ( {}_Sw^1x_1 + {}_Tw^1h^1_0  \right ) \right ) \\
    h^2_2 &amp;amp;= \sigma \left ( {}_Sw^2\sigma h^1_2 + {}_Tw^2  \sigma \left ( {}_Sw^2h^1_1 + {}_Tw^2h^2_0 \right ) \right ) \\
    y_2 &amp;amp;= h^2_2,
    \end{align*}\]

&lt;p&gt;where I have substituted the expressions of \(h^1_1, h^2_1\).
Note that I could have also substituted \(h^1_2\), but I didn’t because the process for backpropagating the error through the layers is not in the scope of this post.&lt;/p&gt;

&lt;p&gt;If we kept going for \(N\) timesteps, the formulas would look like this&lt;/p&gt;

\[\begin{align*}
    h^1_N &amp;amp;= \sigma \left ( {}_Sw^1x_N + {}_Tw^1  \sigma \left ( {}_Sw^1x_{N-1} + {}_Tw^1 \sigma \left ( {}_Sw^1x_{N-2} + \dots {}_Tw^1 h^1_0  \right ) \right ) \right )\\
    h^2_N &amp;amp;= \sigma \left ( {}_Sw^2h^1_N + {}_Tw^2  \sigma \left ( {}_Sw^2h^1_{N-1} + {}_Tw^2 \sigma \left ( {}_Sw^2h^1_{N-2} + \dots {}_Tw^2 h^2_0  \right ) \right ) \right )\\
    y_N &amp;amp;= h^2_N,
    \end{align*}\]

&lt;p&gt;If you feel like this is too familiar for comfort, don’t worry you don’t suffer from &lt;a href=&quot;https://en.wikipedia.org/wiki/Fregoli_delusion&quot;&gt;Fregoli delusion&lt;/a&gt;.
The same exact recursive substitutions happen if we move thorugh layers and not through time.
As a matter of fact, we almost already know how to backprop through time thanks to our knowledge of backprop through layers!&lt;/p&gt;

&lt;h4 id=&quot;gradients-galore&quot;&gt;Gradients galore&lt;/h4&gt;

&lt;p&gt;Let’s compute some derivatives.
Suppose we have a cost function \(C = C (y)\) that we want to minimize.
Question: &lt;em&gt;how does the recurrent weight of the last layer influence the cost function at time \(N\)?&lt;/em&gt;&lt;/p&gt;

\[\begin{align*}
    \frac{dC(y_N)}{d{}_Tw^2} &amp;amp;= \frac{dC}{dy} \Big |_{y_N} \frac{ dy_N}{d{}_Tw^2} \\
    &amp;amp;= \frac{dC}{dy} \Big |_{y_N} \frac{d\sigma}{dz} \Big |_{z^2_N} \frac{d z^2_N }{d{}_Tw^2},
    \end{align*}\]

&lt;p&gt;to compute the last term \(\frac{d z^2_N }{d{}_Tw^2}\) we have to calculate the derivative of the long expression&lt;/p&gt;

\[\frac{d \left [ {}_Sw^2h^1_N + {}_Tw^2  \sigma \left ( {}_Sw^2h^1_{N-1} + {}_Tw^2 \sigma \left ( {}_Sw^2h^1_{N-2} +     \dots {}_Tw^2 h^2_0  \right )\right )\right ]} {d{}_Tw^2}.\]

&lt;p&gt;Fortunately, \(h^1_N\) does &lt;em&gt;not&lt;/em&gt; depend on \({}_Tw^2\), nor does \(h^1_{N-1}\) and so on.
Therefore we can already simplify&lt;/p&gt;

\[\begin{align*}
    &amp;amp;\frac{d \left [ {}_Sw^2h^1_N + {}_Tw^2  \sigma \left ( {}_Sw^2h^1_{N-1} + {}_Tw^2 \sigma \left ( {}_Sw^2h^1_{N-2} +     \dots {}_Tw^2 h^2_0  \right )\right )\right ]} {d{}_Tw^2} =\\
    &amp;amp;\frac{d \left [ {}_Tw^2  \sigma \left ( {}_Sw^2h^1_{N-1} + {}_Tw^2 \sigma \left ( {}_Sw^2h^1_{N-2} + \dots {}_Tw^2 h^2_0  \right )\right )\right ]} {d{}_Tw^2}.
    \end{align*}\]

&lt;p&gt;The annoying bit here is that we are facing the derivative of a product: both \({}_Tw^2\) and \(\sigma ( \dots )\) depend on \({}_Tw^2\)!
So we must write&lt;/p&gt;

\[\begin{align*}
    &amp;amp;\frac{d \left [ {}_Tw^2  \sigma \left ( {}_Sw^2h^1_{N-1} + {}_Tw^2 \sigma \left ( {}_Sw^2h^1_{N-2} + \dots {}_Tw^2 h^2_0  \right )\right )\right ]} {d{}_Tw^2} = \\
    &amp;amp; \sigma \left ( {}_Sw^2h^1_{N-1} + {}_Tw^2 \sigma \left ( {}_Sw^2h^1_{N-2} + \dots {}_Tw^2 h^2_0 \right )\right ) \\
    &amp;amp; + {}_Tw^2 \frac{d \left [  \sigma \left ( {}_Sw^2h^1_{N-1} + {}_Tw^2 \sigma \left ( {}_Sw^2h^1_{N-2} + \dots {}_Tw^2 h^2_0 \right )\right )\right ]}{d{}_Tw^2}.
    \end{align*}\]

&lt;p&gt;The expression that remains under the derivative sign here is the same as before, only one timestep &lt;em&gt;in the past&lt;/em&gt;.
That’s good news.
We can recursively simplify the expression for the derivative into:&lt;/p&gt;

\[\begin{align*}
    \frac{dC(y_N)}{d{}_Tw^2} &amp;amp;= \frac{dC}{dy} \Big |_{y_N} \frac{d\sigma}{dz} \Big |_{z^2_N} \\
            &amp;amp; \Bigg ( \sigma \left ( {}_Sw^2h^1_{N-1} + {}_Tw^2 \sigma \left ( {}_Sw^2h^1_{N-2} + \dots {}_Tw^2 h^2_0 \right )\right ) \\
            &amp;amp; + {}_Tw^2 \frac{d\sigma}{dz} \Big |_{z^2_{N-1}} \Big ( \sigma \left ( {}_Sw^2h^1_{N-2} + \dots {}_Tw^2 h^2_0 \right ) + \dots + \frac{d\sigma}{dz} \Big |_{z^2_{1}} h^2_0 \Big ) \Bigg ).
    \end{align*}\]

&lt;p&gt;Writing the full expression for what’s inside the calls to the \(\sigma\) function was useful to understand exactly who depends on what in order to compute the derivatives.
To make the notation lighter, but also to gain insight on backprop, let’s substitute back the directly the values of the hidden states.
We obtain the &lt;strong&gt;recursive formulation for the derivative&lt;/strong&gt;:&lt;/p&gt;

\[\frac{dC(y_N)}{d{}_Tw^2} = \frac{dC}{dy} \Big |_{y_N} \frac{d\sigma}{dz} \Big |_{z^2_N} \left ( h^2_N + {}_Tw^2 \frac{d\sigma}{dz} \Big |_{z^2_{N-1}} \left ( h^2_{N-1} + \dots +  \frac{d\sigma}{dz} \Big |_{z^2_{1}} h^2_0 \right ) \right ).\]

&lt;p&gt;The question now is, how does this extend to the upstream layers?
We start off pretty easily:&lt;/p&gt;

\[\begin{align*}
    \frac{dC(y_N)}{d{}_Tw^1} &amp;amp;= \frac{dC}{dy} \Big |_{y_N} \frac{ dy_N}{d{}_Tw^1} \\
    &amp;amp;= \frac{dC}{dy} \Big |_{y_N} \frac{d\sigma}{dz} \Big |_{z^2_N} \frac{d z^2_N }{d{}_Tw^1},
    \end{align*}\]

&lt;p&gt;and we have to now compute the annoying derivative&lt;/p&gt;

\[\frac{d z^2_N }{d{}_Tw^1} = \frac{ d \left [ {}_Sw^2h^1_N + {}_Tw^2  \sigma \left ( {}_Sw^2h^1_{N-1} + {}_Tw^2 \sigma \left ( {}_Sw^2h^1_{N-2} + \dots {}_Tw^2 h^2_0  \right ) \right ) \right ]}{d{}_Tw^1}.\]

&lt;p&gt;The caveat is that, in contrast to what happened above, many terms in this expression depend on \({}_Tw^1\), namely \(h^1_N, h^1_{N-1}, h^1_{N-2}, \dots\). We continue with our computations, trying not to make transcription mistakes:&lt;/p&gt;

\[\begin{align*}
    &amp;amp;\frac{ d \left [ {}_Sw^2h^1_N + {}_Tw^2  \sigma \left ( {}_Sw^2h^1_{N-1} + {}_Tw^2 \sigma \left ( {}_Sw^2h^1_{N-2} + \dots {}_Tw^2 h^2_0  \right ) \right ) \right ]}{d{}_Tw^1} \\
    &amp;amp;= {}_Sw^2 \frac{d h^1_N }{d{}_Tw^1} + {}_Tw^2 \frac{ d \left [ \sigma \left ( {}_Sw^2h^1_{N-1} + {}_Tw^2 \sigma \left ( {}_Sw^2h^1_{N-2} + \dots {}_Tw^2 h^2_0  \right ) \right ) \right ]}{d{}_Tw^1}
    \end{align*}\]

&lt;p&gt;As before, we have a nice recursive property: the expression that remains under the second derivative sign is simply the same as the one we wanted to compute, but &lt;em&gt;one timestep in the past&lt;/em&gt;.
Before we move on to the fully recursive formulation, we need an expression for \(\frac{d h^1_N }{d{}_Tw^1}\).
As before, \({}_Sw^1x_N\) doesn’t depend on \({}_Tw^1\), and for the second term we have to compute the derivative of a product:&lt;/p&gt;

\[\begin{align*}
    \frac{d h^1_N }{d{}_Tw^1} &amp;amp;= \frac{d \left [ \sigma \left ( {}_Sw^1x_N + {}_Tw^1  \sigma \left ( {}_Sw^1x_{N-1} + {}_Tw^1 \sigma \left ( {}_Sw^1x_{N-2} + \dots {}_Tw^1 h^1_0  \right ) \right ) \right ) \right ]}{d{}_Tw^1} \\
    &amp;amp;= \frac{d\sigma}{dz} \Big |_{z^1_N} \left ( \sigma \left ( {}_Sw^1x_{N-1} + {}_Tw^1 \sigma \left ( {}_Sw^1x_{N-2} + \dots {}_Tw^1 h^1_0  \right ) \right ) + {}_Tw^1 \frac{d \left [ \sigma \left ( {}_Sw^1x_{N-1} + {}_Tw^1 \sigma \left ( {}_Sw^1x_{N-2} + \dots {}_Tw^1 h^1_0  \right ) \right ) \right ]}{d{}_Tw^1} \right ) .
    \end{align*}\]

&lt;p&gt;And getting the &lt;strong&gt;recursive formulation of the derivative&lt;/strong&gt;:&lt;/p&gt;

\[\begin{align*}
    \frac{dC(y_N)}{d{}_Tw^1} &amp;amp;= \frac{dC}{dy} \Big |_{y_N} \frac{d\sigma}{dz} \Big |_{z^2_N} \left \{ {}_Sw^2 \frac{d\sigma}{dz} \Big |_{z^1_N} \left ( h^1_{N-1} + {}_Tw^1 \frac{d h^1_{N-1} }{d{}_Tw^1} \right ) + {}_Tw^2 \frac{ d h^2_{N-1} }{d{}_Tw^1} \right \} \\
    &amp;amp;=  \frac{dC}{dy} \Big |_{y_N} \frac{d\sigma}{dz} \Big |_{z^2_N} \Bigg \{ {}_Sw^2 \frac{d\sigma}{dz} \Big |_{z^1_N} \left ( h^1_{N-1} + {}_Tw^1 \frac{d\sigma}{dz} \Big |_{z^1_{N-1}} \left ( h^1_{N-2} + {}_Tw^1 \frac{d\sigma}{dz} \Big |_{z^1_{N-2}} \left ( h^1_{N-3} + \dots \right ) \right ) \right ) \\
    &amp;amp; + {}_Tw^2  \frac{d\sigma}{dz} \Big |_{z^2_{N-1}} \Bigg [ {}_Sw^2 \frac{d\sigma}{dz} \Big |_{z^1_{N-1}} \left ( h^1_{N-2} + {}_Tw^1 \frac{d\sigma}{dz} \Big |_{z^1_{N-2}} \left ( h^1_{N-3} + {}_Tw^1 \frac{d\sigma}{dz} \Big |_{z^1_{N-3}} \left ( h^1_{N-4} + \dots \right ) \right ) \right ) \\
    &amp;amp; + {}_Tw^2  \frac{d\sigma}{dz} \Big |_{z^2_{N-2}} \left (  \dots + {}_Tw^1 \frac{d\sigma}{dz} \Big |_{z^1_{1}} h^1_0 \right ) \dots \Bigg] \Bigg \}.
    \end{align*}\]

&lt;p&gt;That’s a pretty ugly-looking expression.
Lets’s try to group together terms by factors of \(h^1_{*}\):&lt;/p&gt;

\[\begin{align*}
    \frac{dC(y_N)}{d{}_Tw^1} &amp;amp;= \frac{dC}{dy} \Big |_{y_N} \Bigg \{ \\
    &amp;amp; \frac{d\sigma}{dz} \Big |_{z^2_N} {}_Sw^2 \frac{d\sigma}{dz} \Big |_{z^1_N} h^1_{N-1}\\
    &amp;amp; + \left (\frac{d\sigma}{dz} \Big |_{z^2_N} {}_Sw^2 \frac{d\sigma}{dz} \Big |_{z^1_N} {}_Tw^1 \frac{d\sigma}{dz} \Big |_{z^1_{N-1}} + \frac{d\sigma}{dz} \Big |_{z^2_N} {}_Tw^2  \frac{d\sigma}{dz} \Big |_{z^2_{N-1}} {}_Sw^2 \frac{d\sigma}{dz} \Big |_{z^1_{N-1}} \right ) h^1_{N-2} \\
    &amp;amp; + \left (\frac{d\sigma}{dz} \Big |_{z^2_N} {}_Sw^2 \frac{d\sigma}{dz} \Big |_{z^1_N} {}_Tw^1 \frac{d\sigma}{dz} \Big |_{z^1_{N-1}} {}_Tw^1 \frac{d\sigma}{dz} \Big |_{z^1_{N-2}} + \frac{d\sigma}{dz} \Big |_{z^2_N} {}_Tw^2  \frac{d\sigma }{dz} \Big |_{z^2_{N-1}} {}_Sw^2 \frac{d\sigma}{dz} \Big |_{z^1_{N-1}} {}_Tw^1 \frac{d\sigma}{d z} \Big |_{z^1_{N-2}} +\frac{d\sigma}{dz} \Big |_{z^2_N} {}_Tw^2  \frac{d\sigma}{dz} \Big |_{z^2_{N-2}} {}_Sw^2 \frac{d\sigma}{dz} \Big |_{z^1_{N-2}} \right ) h^1_{N-3} \\
    &amp;amp; + \dots  \Bigg \}.
    \end{align*}\]

&lt;p&gt;The nice thing about rearranging the terms in this way is that you get a visual idea of the error terms &lt;em&gt;trickling back&lt;/em&gt; through layers and through time.
For example, look at the line that ends with \(h^1_{N-1}\): it already contains the seed for the following line’s term \(\frac{d\sigma}{dz} \Big |_{z^2_N} {}_Sw^2 \frac{d\sigma}{dz} \Big |_{z^1_N} {}_Tw^1\).
This is the essence of backpropagation, where we can compute the errors at time \(N-1\) and backpropagate an error signal to compute the error at time \(N-2\).
In the end, the total error will be the sum of the errors at each instant.&lt;/p&gt;

&lt;p&gt;A similar derivation can and should be done for \(\frac{dC(y_N)}{d{}_Sw^2}, \frac{dC(y_N)}{d{}_Sw^1}\) but I don’t have the time or space to do it here.
Instead, let me go straight to the vector notation and a wonderful visualization!&lt;/p&gt;

&lt;h2 id=&quot;vector-notation&quot;&gt;Vector notation&lt;/h2&gt;

&lt;p&gt;Imagine now that instead of a single neuron per layer, we had multiple neurons.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/recurrent-layers.svg&quot; alt=&quot;recurrent-3D&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Doing the &lt;a href=&quot;../07/naive-backprop.html&quot;&gt;same tricks&lt;/a&gt; as for a feedforward network, we can write the backprop algorithm in vector notation:&lt;/p&gt;

\[\begin{align*}
&amp;amp;\text{ backwards for each layer } l = L, \dots, 1 \\
&amp;amp;\text{ backwards in time } n = N, \dots, 1 \\
&amp;amp;\delta = {}_T\delta^l_{n+1} + {}_S\delta^{l+1}_n \\
&amp;amp;\Delta {}_SW^l = \Delta {}_SW^l + \left [ \delta \odot \frac{d\sigma}{dz} \Bigg |_{\mathbf{z}^l_n} \right ] \otimes \mathbf{h}^{l-1}_n \\
&amp;amp;\Delta {}_TW^l = \Delta {}_TW^l + \left [ \delta \odot \frac{d\sigma}{dz} \Bigg |_{\mathbf{z}^l_n} \right ] \otimes \mathbf{h}^{l}_{n-1} \\
&amp;amp;{}_S\delta^l_n = ({}_SW^l)^T \left [ \delta \odot \frac{d\sigma}{dz} \Bigg |_{\mathbf{z}^l_n} \right ] \\
&amp;amp;{}_T\delta^l_n = ({}_TW^l)^T \left [ \delta \odot \frac{d\sigma}{dz} \Bigg |_{\mathbf{z}^l_n} \right ] .
\end{align*}\]

&lt;p&gt;It can be easy to visualize it all in sort of grid, with layers (space) on one axis and time on the other.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/recurrent.svg&quot; alt=&quot;recurrent-backprop&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To formulas in the image may not reflect with 100% accuracy the notation of the text.
Please refer to the text as the more up-to-date (and thus, hopefully, correct) version.&lt;/p&gt;

&lt;h3 id=&quot;tbttp-truncated-backpropagation-through-time&quot;&gt;TBTTP: Truncated Backpropagation Through Time&lt;/h3&gt;

&lt;p&gt;it may not be desirable to go back until the dawn of time for every backpropagation step.
So, a simple but effective technique is used which consists in &lt;em&gt;truncating&lt;/em&gt; the backward step in time after a given amount of steps \(\tau_{max}\).&lt;/p&gt;

&lt;p&gt;Another technique often coupled with this one is to avoid performing backprop through time at every time step, but only do it every \(\tau_b\) steps.&lt;/p&gt;

&lt;h2 id=&quot;show-me-the-code&quot;&gt;Show me the code!&lt;/h2&gt;

&lt;p&gt;A simple implementation that pretty much follows the notation from this blog post is &lt;a href=&quot;https://github.com/sharkovsky/sharkovsky.github.io/blob/master/code/FullyConnected-py3.ipynb&quot;&gt;here&lt;/a&gt;.
In particular, the code implements a many-to-one architecture where the network ingests a full sequence and produces a single number as output.
As such, it only performs BPTT at &lt;em&gt;the end&lt;/em&gt; of the sequence, instead of at every time step or every \(\tau_b\) steps as descrbied above.&lt;/p&gt;</content><author><name></name></author><summary type="html">In the second blog post of this series, I would like to repeat the same detailed computations for backpropagating the error in the case of recurrent neural networks.</summary></entry><entry><title type="html">A naive explanation of backprop</title><link href="/2017/06/07/naive-backprop.html" rel="alternate" type="text/html" title="A naive explanation of backprop" /><published>2017-06-07T11:53:10+00:00</published><updated>2017-06-07T11:53:10+00:00</updated><id>/2017/06/07/naive-backprop</id><content type="html" xml:base="/2017/06/07/naive-backprop.html">&lt;p&gt;I got lost trying to implement some simple neural networks from scratch in python.
In particular, I figured out that I didn’t really understant how backpropagation works and what is the magic behind it.
I decided to inaugurate a blog with my thoughts on backpropagation.&lt;/p&gt;

&lt;p&gt;Now, there are &lt;em&gt;many&lt;/em&gt; other explanations of backprop out there, much better than mine.
Chapter 2 of M. Nielsen’s &lt;a href=&quot;http://neuralnetworksanddeeplearning.com/chap2.html&quot;&gt;book&lt;/a&gt; is where I learned all this stuff in the beginning.
I then went to the &lt;a href=&quot;http://www.deeplearningbook.org/&quot;&gt;deep learning book&lt;/a&gt; by Ian Goodfellow, Yoshua Bengio, and Aaron Courville for a deeper but less gentle explanation.
Finally, C. Olah’s &lt;a href=&quot;http://colah.github.io/posts/2015-08-Backprop/&quot;&gt;blog&lt;/a&gt;, A. Karpahty’s &lt;a href=&quot;https://medium.com/@karpathy/yes-you-should-understand-backprop-e2f06eab496b&quot;&gt;articles&lt;/a&gt; and S. Raval’s hilarious &lt;a href=&quot;https://www.youtube.com/watch?v=q555kfIFUCM&quot;&gt;videos&lt;/a&gt; all allowed me to get more and more comfortable with different aspects of this problem.&lt;/p&gt;

&lt;p&gt;The purpose of this blog post is two fold: first of all, several of the resources above encourage the readers to &lt;em&gt;do the math&lt;/em&gt;.
This is my attempt at detailing some mathematical expressions, and in turn I encourage everyone to check their calculations against mine and point out any errors/inconsistencies.
Second of all, I wanted to get comfortable with the idea of having a blog, and thought this would be a good starting point.&lt;/p&gt;

&lt;h3 id=&quot;the-handwaving-backprop-explanation&quot;&gt;The handwaving backprop explanation&lt;/h3&gt;

&lt;p&gt;Everyone is talking about it, and everyone has their own, two-sentence explanation that they casually drop in conversation when they want to look cool at a conference.
I like to think of myself as a funny guy, so I usually tell the joke:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Backpropagation is merely a rebranding of the chain rule. Yes, I find it quite….. derivative.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;if you haven’t fallen off your chair, let me restate what pretty much everybody says about backprop: &lt;em&gt;“in neural networks, the error is associated with the gradient of the cost function. Thanks to the backpropagation algorithm, we have a fast and efficient way of computing this gradient w.r.t every parameter of the model.”&lt;/em&gt;&lt;/p&gt;

&lt;h4 id=&quot;why-backprop&quot;&gt;Why &lt;strong&gt;Back&lt;/strong&gt;prop?&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Why do we go &lt;strong&gt;backwards&lt;/strong&gt; in the backprop algorithm?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Consider the following simple example.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/simple-network.png&quot; alt=&quot;simple-network&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To compute \(y\), we have the following relation:&lt;/p&gt;

\[y = \sigma(z) = \sigma( w_2 h_1 ) = \sigma( w_2 \sigma( w_1 x ) ).\]

&lt;p&gt;Suppose we want to compute \(\frac{dy}{dw_1}\) and \(\frac{dy}{dw_2}\), applying the chain rule we get:&lt;/p&gt;

\[\begin{align*}
   \frac{dy}{dw_1} &amp;amp; = \frac{d \sigma(z_2) }{dz_2} \frac{ dz_2 }{dw_1 } \\
                   &amp;amp; = \frac{d \sigma(z_2) }{dz_2} \frac{ dz_2 }{dh_1} \frac{dh_1}{dw_1} \\
                   &amp;amp; = \frac{d \sigma(z_2) }{dz_2} \frac{ dz_2 }{dh_1} \frac{dh_1}{dz_1} \frac{dz_1}{dw_1} \\
                   &amp;amp; = \frac{d \sigma(z_2) }{dz_2} w_2 \frac{dh_1}{dz_1} x, \\
   \frac{dy}{dw_2} &amp;amp; = \frac{d \sigma(z_2) }{dz_2} \frac{ dz_2 }{dw_2} \\
                   &amp;amp; = \frac{d \sigma(z_2) }{dz_2} h_1.
   \end{align*}\]

&lt;p&gt;All these formulas look complicated, yet simple enough for our high-school selves to understand them.
Isn’t that wonderful!&lt;/p&gt;

&lt;p&gt;The trick in &lt;strong&gt;back&lt;/strong&gt;propagation is noticing that the term \(\frac{d \sigma(z_2) }{dz_2}\) appears several times.
First, we need it to compute \(\frac{dy}{dw_2}\).
Then, we &lt;em&gt;back-propagate&lt;/em&gt; it via \(\frac{d \sigma(z_2) }{dz_2} w_2\) because we also need it to compute \(\frac{dy}{dw_1}\).&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The idea is that each layer has the necessary information to compute this &lt;em&gt;error signal&lt;/em&gt;, and can pass it backwards to the previous layers which needs it to compute its own derivative (and its own error signals to pass on, if it has previous layers).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;cost-function&quot;&gt;Cost function&lt;/h4&gt;

&lt;p&gt;In the previous example, we wanted to compute the derivative of \(y\).
The question arises naturally, \(y\) do you want that? (ah ah).&lt;/p&gt;

&lt;p&gt;Well, ideally you have a &lt;em&gt;goal&lt;/em&gt; in mind for your network, some sort of cognitive function that you wish it would emulate(e.g. learning to classify handwritten digits from an image).
So how do you translate this into some beautiful math that your NN can digest?
You postulate the existence of a &lt;em&gt;cost function&lt;/em&gt; \(C\).
The purpose of a cost function is, in the words of Dr. Evil, to &lt;em&gt;throw us a friggin’ bone&lt;/em&gt;.
In this simplified point of view, a cost function is a way to massage the neural network’s output in something that is:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;scalar;&lt;/li&gt;
  &lt;li&gt;differentiable;&lt;/li&gt;
  &lt;li&gt;related to the error in the cognitive function you want to emulate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So what’s the relationship with learning?
The idea is that you are going to tweak and adjust the weights in your network (and generally jump through several hoops) in order to ensure that your neural network does one thing really well: &lt;strong&gt;minimize its cost function&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The way you’ll do this is by computing error signals in the form of gradients, and updating network weights accordingly.
Therefore you would change the formulas above to compute:&lt;/p&gt;

\[\begin{align*}
   \frac{dC}{dw_1} &amp;amp; =\frac{dC}{dy} \frac{dy}{dw_1}, \\
   \frac{dC}{dw_2} &amp;amp; =\frac{dC}{dy} \frac{dy}{dw_2}.
   \end{align*}\]

&lt;h4 id=&quot;why-gradients&quot;&gt;Why gradients?&lt;/h4&gt;

&lt;p&gt;This might seem like a pretty silly question, but it still required me some thinking: &lt;em&gt;why exactly do we use the gradient as an error signal?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are obviously some intuitive explanations for this: as a first approximation, a small change in a parameter corresponds to a change in the output that is proportional to the gradient w.r.t. that parameter.
However, the moment of truth that finally put my mind at ease was the realization that using gradients as an error signal is only a consequence of the &lt;strong&gt;arbitrary&lt;/strong&gt; decision of using Gradient Descent as an optimization algorithm.
If we were to use different methods (e.g. Newton’s) we would be computing more than just gradients (see e.g. section 8.6 of the &lt;a href=&quot;http://www.deeplearningbook.org/&quot;&gt;deep learning book&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;A gentle reminder, the update formula for gradient descent is&lt;/p&gt;

&lt;p&gt;\(w_1 \leftarrow w_1 - \eta \frac{dC}{dw_1}\).&lt;/p&gt;

&lt;p&gt;We call \(\eta\) the &lt;em&gt;learning rate&lt;/em&gt;, a parameter that can have a major effect on convergence.&lt;/p&gt;

&lt;h4 id=&quot;scary-math-in-the-form-of-vectors&quot;&gt;Scary math in the form of vectors&lt;/h4&gt;

&lt;p&gt;Let’s take a leap of imagination here and imagine that in the network from the previous example there was at each layer a vector of neurons instead of single ones.
I know I’m familiar with this concept, since I applied the same technique at age 14 so I could tell mom I had friends and a girlfriend.
Because the devil is in the details, let’s set some dimensions!&lt;/p&gt;

\[\mathbf{x} \in \mathbb{R}^{N_{in}},~~~ \mathbf{h}_1 \in \mathbb{R}^{N_1},~~~ \mathbf{h}_2 = \mathbf{y} \in \mathbb{R}^{N_2} .\]

&lt;p&gt;Let me state this clearly here, &lt;em&gt;all vectors here are &lt;strong&gt;column vectors&lt;/strong&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;To compute the feedforward part of the neural network, just apply the simple recipe:&lt;/p&gt;

\[\begin{align*}
   \mathbf{z}_1 = &amp;amp; W_1\mathbf{x} \\
   \mathbf{h}_1 = &amp;amp; \mathbf{ \sigma } (\mathbf{z}_1) \\
   \mathbf{z}_2 = &amp;amp; W_2 \mathbf{h}_1 \\
   \mathbf{y} = &amp;amp; \mathbf{ \sigma } (\mathbf{z}_2) \\
   cost = &amp;amp; C(\mathbf{y})
   \end{align*}\]

&lt;p&gt;Now, let me fill you in on some details: \(W_1,W_2\) are matrices whose dimensions are \(N_{this~layer} \times N_{previous~layer}\), i.e.:\(W_1 \in \mathbb{R}^{ N_1 \times N_{in} }, W_2 \in \mathbb{R}^{ N_2 \times N_1 }.\)
The function \(\mathbf{ \sigma}\) transforms a vector into another vector with same dimensions \(\mathbf{ \sigma}: \mathbb{R}^N \to \mathbb{R}^N\) whereas \(C\) transforms a vector into a scalar \(C: \mathbb{R}^{N_2} \to \mathbb{R}\).&lt;/p&gt;

&lt;p&gt;How do we rewrite the gradient descent formula for vectors?
Easy, just look at it component-wise:&lt;/p&gt;

\[w_{1,ij} \leftarrow w_{1,ij} + \frac{dC}{dw_{1,ij}}.\]

&lt;p&gt;Now, evaluating the derivative requires some math-fu.
We start by writing the feedforward relationship component-wise:&lt;/p&gt;

\[\begin{align*}
   y_u &amp;amp; = \sigma( z_{2,u} ) \\
       &amp;amp; = \sigma \left ( \sum\limits_m^{N_1} w_{2,um}h_{1,m} \right ) \\
       &amp;amp; = \sigma \left ( \sum\limits_m^{N_1} w_{2,um} \sigma \left ( \sum\limits_{p}^{N_{in}} w_{1,mp}x_p \right ) \right )
   \end{align*}\]

&lt;p&gt;so we can write:&lt;/p&gt;

\[\begin{align*}
   \frac{dC}{dw_{1,ij}} &amp;amp; = \sum\limits_u^{N_2} \frac{dC}{dy} \Bigg|_{y_u} \frac{dy_u}{dw_{1,ij}} \\
                        &amp;amp; = \sum\limits_u^{N_2} \frac{dC}{dy} \Bigg|_{y_u} \frac{ d\sigma }{dz} \Bigg|_{z_{2,u}} \frac{d z_{2,u} }{dw_{1,ij}} \\
                        &amp;amp; = \sum\limits_u^{N_2} \frac{dC}{dy} \Bigg|_{y_u} \frac{ d\sigma }{dz} \Bigg|_{z_{2,u}} \left [ \sum\limits_m^{N_1} w_{2,um} \frac{ d h_{1,m} }{dw_{1,ij}} \right ] \\
                        &amp;amp; = \sum\limits_u^{N_2} \frac{dC}{dy} \Bigg|_{y_u} \frac{ d\sigma }{dz} \Bigg|_{z_{2,u}} \left [ \sum\limits_m^{N_1} w_{2,um} \frac{ d\sigma }{dz} \Bigg|_{ z_{1,m} }  \left ( \frac{d}{dw_{1,ij}} \sum\limits_{p}^{N_{in}} w_{1,mp}x_p \right ) \right ].
   \end{align*}\]

&lt;p&gt;And now we simplify:&lt;/p&gt;

\[\frac{d}{dw_{1,ij}} \sum\limits_{p}^{N_{in}} w_{1,mp}x_p = 
   \begin{cases}
   x_j &amp;amp; \text{if } m = i\\
   0   &amp;amp;  \text{otherwise},
   \end{cases}\]

&lt;p&gt;and we are left with&lt;/p&gt;

\[\frac{dC}{dw_{1,ij}} = \sum\limits_u^{N_2} \frac{dC}{dy} \Bigg|_{y_u} \frac{ d\sigma }{dz} \Bigg|_{z_{2,u}} w_{2,ui} \frac{ d\sigma }{dz} \Bigg|_{ z_{1,i} } x_j.\]

&lt;p&gt;Now we’re gonna be fancy and write this back into matrix notation.
We’re going to use this rule&lt;/p&gt;

\[\sum\limits_u k_ua_{ui} \to \text{the } i^{th} \text{ component of } A^T\mathbf{k},\]

&lt;p&gt;and introduce the outer product \(\otimes\) between \(\mathbf{x} \in \mathbb{R}^{N_x}\) and \(\mathbf{y} \in \mathbb{R}^{N_y}\) such that&lt;/p&gt;

\[\begin{align*}
   &amp;amp; \mathbf{x} \otimes \mathbf{y} \in \mathbb{R}^{N_x \times N_y} \\
   &amp;amp; \left [ \mathbf{x} \otimes \mathbf{y} \right ]_{ij} = x_iy_j,
   \end{align*}\]

&lt;p&gt;and introduce also the simpler element-wise product \(\left [ \mathbf{x} \odot \mathbf{y} \right ]_i = x_iy_i\).&lt;/p&gt;

&lt;p&gt;We can finally say that \(\frac{dC}{dw_{1,ij}}\) will be the \((ij)^{th}\) component of the matrix&lt;/p&gt;

\[\left [ W_2^T \left ( \nabla C \Bigg|_{\mathbf{y}} \odot \frac{ d \sigma }{dz} \Bigg|_{\mathbf{z}_{2}} \right ) \right ] \odot \frac{ d \sigma }{dz} \Bigg|_{\mathbf{z}_{1}} \otimes \mathbf{x}.\]

&lt;p&gt;By repeating the exact same computations, but simpler, we can get the expression for \(\frac{dC}{dw_{2,ij}}\) as the  \((ij)^{th}\) component of the matrix&lt;/p&gt;

\[\left ( \nabla C \Bigg|_{\mathbf{y}} \odot \frac{ d \sigma }{dz} \Bigg|_{\mathbf{z}_{2}} \right ) \otimes \mathbf{h}_1.\]

&lt;p&gt;As a validation, you can check that the above formula is indeed an \((N_2 \times N_1)\) matrix.&lt;/p&gt;

&lt;h4 id=&quot;the-backprop-insight&quot;&gt;The backprop insight&lt;/h4&gt;

&lt;p&gt;This is where the magic happens! We can summarize our gradient descent formulas as&lt;/p&gt;

\[\begin{align*}
   W_2 \leftarrow &amp;amp; W_2 - \eta \left ( \nabla C \Bigg|_{\mathbf{y}} \odot \frac{ d \sigma }{dz} \Bigg|_{\mathbf{z}_{2}} \right ) \otimes \mathbf{h}_1 \\
   W_1 \leftarrow &amp;amp; W_1 - \eta \left [ W_2^T \left ( \nabla C \Bigg|_{\mathbf{y}} \odot \frac{ d \sigma }{dz} \Bigg|_{\mathbf{z}_{2}} \right ) \right ] \odot \frac{ d \sigma }{    dz} \Bigg|_{\mathbf{z}_{1}} \otimes \mathbf{x}.
   \end{align*}\]

&lt;p&gt;As we noted before, one term is repeated in the two expressions, namely \(\left ( \nabla C \Bigg|_{\mathbf{y}} \odot \frac{ d \sigma }{dz} \Bigg|_{\mathbf{z}_{2}} \right )\).
Moreover, this repeated term can be computed using only knowledge from the cost function and layer 2.
We can use this insight to define an &lt;em&gt;error signal&lt;/em&gt; that gets backpropagated through the network.&lt;/p&gt;

&lt;p&gt;We are left with the following &lt;strong&gt;backprop algorithm&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;compute the gradient of the cost function, evaluated on the activations of the last layer;&lt;/li&gt;
  &lt;li&gt;set the &lt;em&gt;error signal&lt;/em&gt;
\(\mathbf{ \delta } = \nabla C \Bigg|_{\mathbf{y}}\)&lt;/li&gt;
  &lt;li&gt;for each layer \(l\) going backwards:
    &lt;ul&gt;
      &lt;li&gt;update the weight matrix
\(W_l \leftarrow W_l - \eta \left ( \mathbf { \delta } \odot \frac{ d \sigma }{dz} \Bigg|_{\mathbf{z}_{l}} \right ) \otimes \mathbf{h}_{l-1}\);&lt;/li&gt;
      &lt;li&gt;backpropagate the error signal
\(\mathbf { \delta } = W_l^T \left ( \mathbf { \delta } \odot \frac{ d \sigma }{dz} \Bigg|_{\mathbf{z}_{l}} \right )\).&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;show-me-the-code&quot;&gt;Show me the code!&lt;/h3&gt;

&lt;p&gt;A simple implementation that pretty much follows the notation from this blog post is &lt;a href=&quot;https://github.com/sharkovsky/sharkovsky.github.io/blob/master/code/FullyConnected-py3.ipynb&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</content><author><name></name></author><summary type="html">I got lost trying to implement some simple neural networks from scratch in python. In particular, I figured out that I didn’t really understant how backpropagation works and what is the magic behind it. I decided to inaugurate a blog with my thoughts on backpropagation.</summary></entry></feed>