<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.5">Jekyll</generator><link href="https://chanind.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://chanind.github.io/" rel="alternate" type="text/html" /><updated>2024-02-19T13:01:59+00:00</updated><id>https://chanind.github.io/feed.xml</id><title type="html">chanind.github.io</title><subtitle>Machine Learning, Javascript, Python, and DevOps</subtitle><entry><title type="html">Auto-matching hidden layers in Pytorch LLMs</title><link href="https://chanind.github.io/ai/2024/02/19/auto-match-hidden-layers-pytorch-llm.html" rel="alternate" type="text/html" title="Auto-matching hidden layers in Pytorch LLMs" /><published>2024-02-19T00:00:00+00:00</published><updated>2024-02-19T00:00:00+00:00</updated><id>https://chanind.github.io/ai/2024/02/19/auto-match-hidden-layers-pytorch-llm</id><content type="html" xml:base="https://chanind.github.io/ai/2024/02/19/auto-match-hidden-layers-pytorch-llm.html"><![CDATA[<p><span class="gray"><em>Note: This is cross-posted on <a href="https://www.lesswrong.com/posts/MhFDxivjJrvZ2DMxw/auto-matching-hidden-layers-in-pytorch-llms?utm_campaign=post_share&amp;utm_source=link">LessWrong</a>.</em></span></p>

<p>Mechanistic interpretability and steering LLMs requires being able to read and modify activations during inference. For instance, to <a href="https://arxiv.org/abs/2312.06681">apply steering vectors</a> to control model generation, we need to first collect hidden activations to find a steering direction, then intervene by modifying hidden activations of the model during inference.</p>

<p>To read and patch activations from a LLM, you first need to find the relevant layers that you care about and either add hooks or wrap them. This tends to lead to two approaches, either 1. writing a custom model wrapper for every model you might want to work with (approach taken by <a href="https://github.com/andyzoujm/representation-engineering">Repe</a>, <a href="https://github.com/nrimsky/CAA">CAA</a>) or 2. leave it to the user to manually specify layer names to patch, and apply the patch using Pytorch hooks (approach taken by <a href="https://github.com/davidbau/baukit">Baukit</a>). The first approach is a never-ending battle as new models are released, and the second approach, while very flexible, passes on the complexity to anyone using what you’ve written.</p>

<p>In this post, I’ll discuss a third option, which is to auto-detect the types of layers in a Pytorch LLM and read/patch using Pytorch hooks, and is the approach used by the steering-vectors library. This leverages the fact that all transformer LMs have the same basic structure: a series of layers containing attention and MLP blocks. This post assumes the model is from Huggingface, although this same technique will likely work with any transformer LM that’s sanely constructed. This post will use the terms “transformer LM” and “LLM” interchangeably to refer to a decoder-only generative language model like GPT or LLaMa.</p>

<h2 id="guessing-layer-templates">Guessing layer templates</h2>

<p>Finding the component parts of any Pytorch module is easy by calling <code class="language-plaintext highlighter-rouge">named_modules()</code> on the model. This will return a dictionary containing the name of the submodule, and the submodule itself. This is demonstrated for GPT2-small below:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">transformers</span> <span class="kn">import</span> <span class="n">AutoModelForCausalLM</span>

<span class="n">model</span> <span class="o">=</span> <span class="n">AutoModelForCausalLM</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s">"gpt2"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="nb">dict</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">named_modules</span><span class="p">()).</span><span class="n">keys</span><span class="p">())</span>

<span class="c1"># transformer
# transformer.wte
# transformer.wpe
# transformer.drop
# transformer.h
# transformer.h.0
# transformer.h.0.ln_1
# transformer.h.0.attn
# transformer.h.0.attn.c_attn
# transformer.h.0.attn.c_proj
# transformer.h.0.attn.attn_dropout
# transformer.h.0.attn.resid_dropout
# transformer.h.0.ln_2
# transformer.h.0.mlp
# transformer.h.0.mlp.c_fc
# transformer.h.0.mlp.c_proj
# transformer.h.0.mlp.act
# transformer.h.0.mlp.dropout
# ...
# transformer.h.11
# transformer.h.11.ln_1
# transformer.h.11.attn
# transformer.h.11.attn.c_attn
# transformer.h.11.attn.c_proj
# transformer.h.11.attn.attn_dropout
# transformer.h.11.attn.resid_dropout
# transformer.h.11.ln_2
# transformer.h.11.mlp
# transformer.h.11.mlp.c_fc
# transformer.h.11.mlp.c_proj
# transformer.h.11.mlp.act
# transformer.h.11.mlp.dropout
# transformer.ln_f
# lm_head
</span></code></pre></div></div>

<p>Here, it’s clear that the 12 decoder block layers of the model are of the form <code class="language-plaintext highlighter-rouge">transformer.h.{num}</code>, the attention layers are <code class="language-plaintext highlighter-rouge">transformer.h.{num}.attn</code>, and the MLP layers are <code class="language-plaintext highlighter-rouge">transformer.h.{num}.mlp</code>. It’s similarly easy to see the input and ouput layer norms and dropout.</p>

<p>For LLaMa, the layers are of the form <code class="language-plaintext highlighter-rouge">model.layers.{num}</code> for each decoder block, <code class="language-plaintext highlighter-rouge">model.layers.{num}.self_attn</code> for attention, and <code class="language-plaintext highlighter-rouge">model.layers.{num}.mlp</code> for the MLP layers. For Pythia, the decoder block, attention and MLP layers are of the form <code class="language-plaintext highlighter-rouge">gpt_neox.layers.{num}</code>, <code class="language-plaintext highlighter-rouge">gpt_neox.layers.{num}.attention</code>, and <code class="language-plaintext highlighter-rouge">gpt_neox.layers.{num}.mlp</code>, respectively.</p>

<p>This hints at a simple rule to find relevant layer names in any transformer LM - simply look for the shortest template string of the form <code class="language-plaintext highlighter-rouge">*.{num}*</code> which also contains any other terms you might care about. For instance, for attention layers, looking for the shortest template that contains either “attn” or “attention” should cover nearly all LLMs. Likewise, looking for the shortest template with “mlp” should get the MLP layers in nearly all cases. We can generalize this in code below:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">re</span>
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">defaultdict</span>

<span class="c1"># look for layers of the form "*.{num}"
</span><span class="n">LAYER_GUESS_RE</span> <span class="o">=</span> <span class="sa">r</span><span class="s">"^([^\d]+)\.([\d]+)(.*)$"</span>

<span class="k">def</span> <span class="nf">guess_matcher_from_layers</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="nb">filter</span> <span class="o">=</span> <span class="bp">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span><span class="p">:</span>
    <span class="n">counts_by_guess</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">defaultdict</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">layer</span> <span class="ow">in</span> <span class="nb">dict</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">named_modules</span><span class="p">()).</span><span class="n">keys</span><span class="p">():</span>
        <span class="k">if</span> <span class="n">re</span><span class="p">.</span><span class="n">match</span><span class="p">(</span><span class="n">LAYER_GUESS_RE</span><span class="p">,</span> <span class="n">layer</span><span class="p">):</span>
            <span class="n">guess</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">sub</span><span class="p">(</span><span class="n">LAYER_GUESS_RE</span><span class="p">,</span> <span class="sa">r</span><span class="s">"\1.{num}\3"</span><span class="p">,</span> <span class="n">layer</span><span class="p">)</span>
            <span class="k">if</span> <span class="nb">filter</span> <span class="ow">is</span> <span class="bp">None</span> <span class="ow">or</span> <span class="nb">filter</span><span class="p">(</span><span class="n">guess</span><span class="p">):</span>
                <span class="n">counts_by_guess</span><span class="p">[</span><span class="n">guess</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">counts_by_guess</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">return</span> <span class="bp">None</span>

    <span class="c1"># score is higher for guesses that match more often, are and shorter in length
</span>    <span class="n">guess_scores</span> <span class="o">=</span> <span class="p">[</span>
        <span class="p">(</span><span class="n">guess</span><span class="p">,</span> <span class="n">count</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">guess</span><span class="p">))</span> <span class="k">for</span> <span class="n">guess</span><span class="p">,</span> <span class="n">count</span> <span class="ow">in</span> <span class="n">counts_by_guess</span><span class="p">.</span><span class="n">items</span><span class="p">()</span>
    <span class="p">]</span>
    <span class="k">return</span> <span class="nb">max</span><span class="p">(</span><span class="n">guess_scores</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">])[</span><span class="mi">0</span><span class="p">]</span>
</code></pre></div></div>

<p>Then we can find a layer matcher template for the base decoder block, attention, and MLP layers for a model like below:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span> <span class="o">=</span> <span class="n">AutoModelForCausalLM</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s">"gpt2"</span><span class="p">)</span>

<span class="n">guess_matcher_from_layers</span><span class="p">(</span><span class="n">model</span><span class="p">)</span>
<span class="c1"># "transformer.h.{num}"
</span>
<span class="n">guess_matcher_from_layers</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="k">lambda</span> <span class="n">l</span><span class="p">:</span> <span class="s">"attn"</span> <span class="ow">in</span> <span class="n">l</span> <span class="ow">or</span> <span class="s">"attention"</span> <span class="ow">in</span> <span class="n">l</span><span class="p">)</span>
<span class="c1"># "transformer.h.{num}.self_attn
</span>
<span class="n">guess_matcher_from_layers</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="k">lambda</span> <span class="n">l</span><span class="p">:</span> <span class="s">"mlp"</span> <span class="ow">in</span> <span class="n">l</span><span class="p">)</span>
<span class="c1"># "transformer.h.{num}.mlp
</span></code></pre></div></div>

<p>This code will also successfully guess the corresponding layer templates for LLaMa, Pythia, and any other transformer LM.</p>

<p>Extracting layers using a layer template
Now that we have a layer template string for each of the types of layers we care about, we just need a way to specify a layer number and get back the corresponding submodule to patch. Fortunately, we already have everything we need to do this. The <code class="language-plaintext highlighter-rouge">named_modules()</code> method of Pytorch modules gives use everything we need. First, lets start by finding all the numbered layers in the model which match a given template string:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">collect_matching_layers</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">layer_matcher</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
    <span class="n">all_layer_names</span> <span class="o">=</span> <span class="nb">set</span><span class="p">(</span><span class="nb">dict</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">named_modules</span><span class="p">()).</span><span class="n">keys</span><span class="p">())</span>
    <span class="n">matching_layers</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">layer_num</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">all_layer_names</span><span class="p">)):</span>
        <span class="n">layer_name</span> <span class="o">=</span> <span class="n">layer_matcher</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">num</span><span class="o">=</span><span class="n">layer_num</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">layer_name</span> <span class="ow">in</span> <span class="n">all_layer_names</span><span class="p">:</span>
            <span class="n">matching_layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">layer_name</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">break</span>
    <span class="k">return</span> <span class="n">matching_layers</span>
</code></pre></div></div>

<p>If we run this function on GPT2 with the decoder block layer matcher (<code class="language-plaintext highlighter-rouge">transformer.h.{num}</code>), we’ll get back an ordered list of all matching layers: <code class="language-plaintext highlighter-rouge">transformer.h.0</code>, <code class="language-plaintext highlighter-rouge">transformer.h.1</code>, etc…</p>

<p>Once we have this list, it’s trivial to select any layer number from it, and again, use <code class="language-plaintext highlighter-rouge">named_modules()</code> to get back the actual Pytorch module corresponding to that layer:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span> <span class="o">=</span> <span class="n">AutoModelForCausalLM</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s">"gpt2"</span><span class="p">)</span>
<span class="n">layer_matcher</span> <span class="o">=</span> <span class="n">guess_matcher_from_layers</span><span class="p">(</span><span class="n">model</span><span class="p">)</span> <span class="c1"># "transformer.h.{num}"
</span><span class="n">modules_by_name</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">named_modules</span><span class="p">())</span>

<span class="n">layer_names</span> <span class="o">=</span> <span class="n">collect_matching_layers</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">layer_matcher</span><span class="p">)</span>

<span class="c1"># layer 2
</span><span class="n">layer2</span> <span class="o">=</span> <span class="n">modules_by_name</span><span class="p">[</span><span class="n">layer_names</span><span class="p">[</span><span class="mi">2</span><span class="p">]]</span>

<span class="c1"># layer 7
</span><span class="n">layer7</span> <span class="o">=</span> <span class="n">modules_by_name</span><span class="p">[</span><span class="n">layer_names</span><span class="p">[</span><span class="mi">7</span><span class="p">]]</span>
<span class="n">Add</span> <span class="n">hooks</span> <span class="ow">and</span> <span class="n">profit</span>
<span class="n">We</span> <span class="n">now</span> <span class="n">have</span> <span class="n">a</span> <span class="n">way</span> <span class="n">to</span> <span class="n">automatically</span> <span class="n">find</span> <span class="ow">and</span> <span class="n">extract</span> <span class="nb">all</span> <span class="n">the</span> <span class="n">relevant</span> <span class="n">layers</span> <span class="k">from</span> <span class="n">a</span> <span class="n">Pytorch</span> <span class="n">LLM</span><span class="p">.</span> <span class="n">The</span> <span class="nb">next</span> <span class="n">step</span> <span class="ow">is</span> <span class="n">to</span> <span class="n">add</span> <span class="n">Pytorch</span> <span class="n">hooks</span> <span class="n">to</span> <span class="n">read</span> <span class="ow">or</span> <span class="n">modify</span> <span class="n">activations</span><span class="p">.</span>

<span class="c1"># add a hook to layer2 and layer7 from above
</span>
<span class="k">def</span> <span class="nf">do_something_cool</span><span class="p">(</span><span class="n">module</span><span class="p">,</span> <span class="n">args</span><span class="p">,</span> <span class="n">output</span><span class="p">):</span>
	<span class="c1"># save or modify the layer output
</span>	<span class="p">...</span>

<span class="k">for</span> <span class="n">layer</span> <span class="ow">in</span> <span class="p">[</span><span class="n">layer2</span><span class="p">,</span> <span class="n">layer7</span><span class="p">]:</span>
	<span class="n">layer</span><span class="p">.</span><span class="n">register_module_forward_hook</span><span class="p">(</span><span class="n">do_something_cool</span><span class="p">)</span>
</code></pre></div></div>

<p>… and that’s all there is to it! To see this in action, check out <a href="https://github.com/steering-vectors/steering-vectors/blob/main/steering_vectors/layer_matching.py">layer_matching.py</a> in the <a href="https://github.com/steering-vectors/steering-vectors">steering_vectors library</a>.</p>]]></content><author><name></name></author><category term="AI" /><summary type="html"><![CDATA[Note: This is cross-posted on LessWrong.]]></summary></entry><entry><title type="html">Solving Python SSL certificate verify failed on Linux / SGE</title><link href="https://chanind.github.io/python/2023/08/30/python-ssl-certificate-verify-failed.html" rel="alternate" type="text/html" title="Solving Python SSL certificate verify failed on Linux / SGE" /><published>2023-08-30T00:00:00+00:00</published><updated>2023-08-30T00:00:00+00:00</updated><id>https://chanind.github.io/python/2023/08/30/python-ssl-certificate-verify-failed</id><content type="html" xml:base="https://chanind.github.io/python/2023/08/30/python-ssl-certificate-verify-failed.html"><![CDATA[<p>On a system I’ve been working on I’ve been plagued by SSL errors whenever Python would try to download something from the internet. I know it’s possible to edit the requests to not verify SSL certs, but this is code in a third-party library (e.g. <code class="language-plaintext highlighter-rouge">nltk.download</code>) which I cannot edit easily. And even if I could, it’s unsettling to disable SSL verification since that opens you up to potentiall man-in-the-middle attacks. The errors would look something like below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>urlopen error [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed:
unable to get local issuer certificate (_ssl.c:1002)
</code></pre></div></div>

<p>I didn’t have any luck following most of what I found on Stack Overflow to solve this issue, but eventually stumbled on a solution combining ideas from <a href="https://access.redhat.com/articles/2039753">Redhat’s guide to Python cert errors</a>, and <a href="https://stackoverflow.com/a/31915123/245362">a Stack Overlow answer</a>. Specifically, I needed to install certifi certs via <code class="language-plaintext highlighter-rouge">pip install certifi</code>, but this was not enough. I then needed to set an ENV var called <code class="language-plaintext highlighter-rouge">SSL_CERT_FILE</code> to the location of the certs installed via certifi. I don’t know why Python wasn’t using these certs automatically as it should have been, but this solved the issue for me.</p>

<p>The full steps I took are as follows:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>certifi
</code></pre></div></div>

<p>Next, in Python, find the certifi install location by running</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">requests.utils</span> <span class="kn">import</span> <span class="n">DEFAULT_CA_BUNDLE_PATH</span>
<span class="k">print</span><span class="p">(</span><span class="n">DEFAULT_CA_BUNDLE_PATH</span><span class="p">)</span>
<span class="c1"># /path/to/python/site-packages/certifi/cacert.pem
</span></code></pre></div></div>

<p>Note the output of the above <code class="language-plaintext highlighter-rouge">cacert.pem</code> file, and add the following to <code class="language-plaintext highlighter-rouge">.bashrc</code> (or <code class="language-plaintext highlighter-rouge">.bash_profile</code> or <code class="language-plaintext highlighter-rouge">.zshrc</code>, etc… depending on your system).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">SSL_CERT_FILE</span><span class="o">=</span>/path/to/python/site-packages/certifi/cacert.pem
</code></pre></div></div>

<p>Of course, in the above make sure you use that actual path to cacert.pem on your system.</p>

<p>Next, restart the terminal and hopefully everything should work!</p>]]></content><author><name></name></author><category term="python" /><summary type="html"><![CDATA[On a system I’ve been working on I’ve been plagued by SSL errors whenever Python would try to download something from the internet. I know it’s possible to edit the requests to not verify SSL certs, but this is code in a third-party library (e.g. nltk.download) which I cannot edit easily. And even if I could, it’s unsettling to disable SSL verification since that opens you up to potentiall man-in-the-middle attacks. The errors would look something like below:]]></summary></entry><entry><title type="html">Academics: You’re Doing Open Source Wrong</title><link href="https://chanind.github.io/2023/06/04/academics-open-source-research-code-python-tips.html" rel="alternate" type="text/html" title="Academics: You’re Doing Open Source Wrong" /><published>2023-06-04T00:00:00+00:00</published><updated>2023-06-04T00:00:00+00:00</updated><id>https://chanind.github.io/2023/06/04/academics-open-source-research-code-python-tips</id><content type="html" xml:base="https://chanind.github.io/2023/06/04/academics-open-source-research-code-python-tips.html"><![CDATA[<div>
    <img src="/assets/lego_cake.png" />
</div>
<p><i class="small gray">Cake with melted plastic lego pieces, delicious. Generated by Midjourney</i></p>

<p>I recently started a PhD in Computer Science after spending the past 10 years working as a software engineer. One of the biggest shocks to me in this transition (aside from how incompetent I am as a researcher) has been the apalling state of code that accompanies published research papers. Usually when I complain about academic code, people think I’m just talking about code quality being poor (which it is), but it’s much deeper than that. The way code is open-sourced in most academic papers is typically completely broken, and shows a deep misunderstanding about what code is for, and what open-source is about.</p>

<p>Imagine you invite some friends to your apartment, and one of them brings a cake they baked. When you try to eat the cake, you find that it has melted plastic lego pieces in it. Shocked, you point this out to your friend, who just replies, “Oh, I’m not good at cooking.” You then realize your friend has a misunderstanding at a fundamental level about what cooking is for, and what food even is. The code that accompanies research papers is like that cake - it fails at the most basic thing code is meant to do, which is to run and be usable.</p>

<p>In this post, we’ll go over what I view as the problem, and share tips for academics on how to do a better job of open-sourcing their code. I’ll be focusing on Python, as that’s mainly what’s used in AI research, but a lot of this will apply to other languages as well. First and foremost, we’ll focus on the big picture of making the code fit for human consumption, and then we’ll go over how to improve the taste, aka code quality.</p>

<p>The current state of academic code is both a travesty and a huge missed opportunity. The few cases I’ve seen where research code is properly packaged and is made easy to use, both the repo and corresponding paper get massive numbers of citations and wide usage. In this article, I hope to show that doing a good job of open-sourcing research code is worth the effort and that it’s not difficult.</p>

<h2 id="the-problematic-academic-mindset">The problematic academic mindset</h2>

<p>Most code I encounter that’s released as part of academic papers is completely broken, as in it’s not possible to run the code as provided at all. This is typically due to things like missing files the researcher forgot to upload, or hardcoded file paths to stuff on the researcher’s own machine, missing documentation, or not pining Python dependency versions. This shows that the researcher never even tried running the code they open-sourced at all, and instead just copied and pasted some files from their local hard-drive into a Github repo, linked the repo in their paper, and high-fived everyone for a job well done. There is no notion that other people are going want to actually try to run that code, and that by uploading broken code and advertising it in a paper you are directly wasting thousands of hours of other people’s time.</p>

<p>Academics are not bad people, and I don’t believe they’re intentionally being malicious. Instead, I think the mindset of most researchers towards open-source code is the following:</p>

<ul>
  <li>Putting Python files in a Github repo is just a way to make a paper seem more legit, since if there’s code then people will believe the results.</li>
  <li>Code is just for looking at to get an idea of how something is implemented, not for running. It’s like an extended appendix to the paper.</li>
</ul>

<p>The problem with the academic mindset to open-sourcing code above is that it misses the core thing that code is for, which is for actually running and accomplishing a task.</p>

<h2 id="you-want-other-people-to-use-your-code-in-their-work">You want other people to use your code in their work</h2>

<p>As a researcher, success means having your work widely cited and used by other researchers. One of the most direct ways to accomplish that is for other researchers to use your code in their work. If you make your code easy to use and package it properly, other researchers will use it and then cite your papers. If your code is completely broken or not usable due to not being packaged properly, nobody will use it. Other researchers <em>want</em> to use your code too - it’s a win-win for everyone if research code is open-sourced properly.</p>

<h2 id="do-not-publish-broken-code">DO NOT PUBLISH BROKEN CODE!!!</h2>

<p>It should go without saying, but it’s not OK to publish code that’s completley broken. Your paper is a giant advertisement for your code, and people who read your paper will naturally go to the repo you link and try running what they find there. If the code is broken, you are collectively wasting thousands of hours of other people’s time. Typically, when research code is broken, I find it’s for one of the following reasons:</p>

<ul>
  <li><strong>The researcher never actually tested what they published:</strong> It seems like in a lot of cases, researchers simply copy/paste files from their local hard disk into a git repo, and never bother to actually check if what they uploaded is complete and can run following the instructions they give. There are often missing files, or references to hardcoded file paths on the researcher’s own hard disk. Avoiding broken code like this is a simple question of actually testing what you publish. This is a bare minimum which would only take a few mintues, but will save everyone else who reads your paper a lot of pain.</li>
  <li><strong>Dependency versions are not pinned:</strong> If you rely on other Python libraries and don’t pin a version range, your code is guaranteed to break in 6 months when one of those dependencies makes a breaking change. Don’t do this. It’s incredibly frustrating needing to go on an archeological dig through PyPI trying to guess what version of each dependency the researcher was probably using.</li>
</ul>

<div class="center">
    <div style="display: flex; justify-content: center; flex-wrap: wrap;">
        <img style="width: 200px; margin: 5px 10px; border: 1px solid #CCC; padding: 5px;" src="/assets/dependencies_fail.png" />
        <img style="width: 200px; margin: 5px 10px; border: 1px solid #CCC; padding: 5px;" src="/assets/dependencies_good.png" />
    </div>
    <i class="small gray">If you don't pin dependency versions, your code is guaranteed to be break</i>
</div>

<h2 id="release-a-library-not-a-collection-of-files">Release a library, not a collection of files</h2>

<p>After ensuring that the code is actually working, the next most important thing is packaging it properly so others can use it in their work. Ideally, your goal should be to release a <em>library</em> which does the thing in your paper, not a pile of random Python files.</p>

<div>
    <img src="/assets/drake_install_software.jpeg" />
</div>
<p><i class="small gray">Python libraries should be packaged and released on PyPI</i></p>

<p>If your code is just a bunch of Python scripts in a Github repo, it’s nearly impossible for other people to use that code in their work. What are they supposed to do, copy and paste files from your repo onto their hard drive? Are they supposed to open up the files and copy/paste individual chunks of Python code out? Nobody is going to do that. Fortunately, there’s a well-established way to import code into a Python project which makes it easy for your code to be used by others, and that’s for the code to packaged as a library on PyPI. This lets it be installed with <code class="language-plaintext highlighter-rouge">pip install &lt;your-library-name&gt;</code>.</p>

<p>The idea of releasing a library might sound daunting, but it’s really easy once you get used to it. The difference is a code organization question more than anything else, and some basic thought put to “what would someone want to do with this code?”. Once you’ve learned to package code into a library you’ll see that doing a decent job of packaging your code is far easier than learning LaTeX, or writng a paper, or finding a research idea to begin with. We’ll discuss how to make this easy in the section on Poetry later in the article.</p>

<h2 id="a-good-repo">A Good Repo™</h2>

<p>The git repo for your code should have the following components:</p>

<ul>
  <li>A basic README explaining how to use the library, which includes the following:
    <ul>
      <li>An Installation section, which says <code class="language-plaintext highlighter-rouge">pip install your-awesome-library</code></li>
      <li>Basic usage instructions, like:
        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">your_awesome_library</span> <span class="kn">import</span> <span class="n">do_awesome_thing</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">do_awesome_thing</span><span class="p">(</span><span class="nb">input</span><span class="p">)</span>
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>The library should be usable in Python, NOT (only) via running a Python script</li>
  <li>It needs to work</li>
</ul>

<p>… and that’s basically it. If do this, your code should be easy for others to use and you’re already better of 95% of the open-source code released by researchers.</p>

<h2 id="what-about-reproducing-the-results-in-my-paper-shouldnt-that-be-the-main-point-of-open-sourced-code">What about reproducing the results in my paper? Shouldn’t that be the main point of open-sourced code?</h2>

<p>By all means, do include code to reproduce the experiments in your paper, as reproducability of results is important. However, recognize that the majority of users of your code won’t want to reproduce your results, so it shouldn’t be the main focus. It’s fine to include an <code class="language-plaintext highlighter-rouge">experiments</code> folder in your git repo for reproducing your results that’s not published to PyPI, or even split apart the experiments into a separate git repo from the reusable library code so the library can evolve separately. If you take the approach of splitting the repos, then the experiments repo can import the library as a normal <code class="language-plaintext highlighter-rouge">pip</code> dependency, which also has the bonus of verifying that your library works when installed as a dependency in other projects. This leads naturally to the next point:</p>

<h2 id="its-ok-to-publish-multiple-repos-per-paper">It’s OK to publish multiple repos per paper</h2>

<p>If your paper has several distinct components, each of which could be used independently as its own library, there’s nothing wrong with releasing multiple open-source repos or libraries along with your paper. Your goal should be to make your code useful to others, and you might find that it’s more natural to release 2 or even 3 different libraries so the various parts of your paper can be used independently rather than trying to fit everything into 1 library. There’s no rule that says every paper must correspond to 1 and only 1 git repo. If splitting into separate libraries makes it easier for others to use, go for it!</p>

<h2 id="what-are-some-examples-of-this-done-well">What are some examples of this done well?</h2>

<p>The best examples of researchers releasing their code are also some of the best known projects in the field. I don’t believe this is a coincidence - if you package your code properly and release it on PyPI, then others will use it in their own projects and cite your paper. Two excellent examples that come to mind are the following:</p>

<h4 id="flashattention">FlashAttention</h4>

<p><a href="https://github.com/HazyResearch/flash-attention">FlashAttention</a> is a beautiful illustration of how you don’t need to overthink this to do a good job. This repo has a simple README with installation of the library via <code class="language-plaintext highlighter-rouge">pip install flash-attn</code> and basic instructions on how to use it in Python. There’s a <code class="language-plaintext highlighter-rouge">benchmarks</code> folder in the repo to reproduce the results in the paper, but it’s not the main focus of the library. The library itself is simple and focused. A+</p>

<h4 id="sentence-transformers">Sentence Transformers</h4>

<p><a href="https://github.com/UKPLab/sentence-transformers">Sentence Transformers</a> goes above and beyond, including a documentation website and continues to evolve and add pretrained models to the library. This library corresponds to an original paper by the author on a technique for sentence similarity, but was just packaged well and focused on ease of use, and the author clearly has put a lot of care into this library.</p>

<p>Both of these libraries were created by individual researchers along with their papers, by a PhD student in the case of FlashAttention, and a postdoc in the case of Sentence Transformers. In both these cases, the authors could have just copy/pasted a collection of unusable Python scripts into a Github repo and left it at that, but then likely neither of their papers would have achieved anywhere near the level of success they both have seen. I believe the level of polish that these libraries show is very achievable for all academics, and should be the norm rather than the exception.</p>

<h2 id="poetry-packaging-and-dependency-management-made-easy">Poetry: packaging and dependency management made easy</h2>

<p>Personally, I like using <a href="https://python-poetry.org/">Poetry</a> for managing Python projects. Poetry handles a lot of the complexity of virtual environments for Python, dependency management, and finally, publishing your library to PyPI so it can be installed with <code class="language-plaintext highlighter-rouge">pip install &lt;your-library&gt;</code>. Poetry isn’t the only way to do this, but it provides a good foundation.</p>

<p>Let’s assume we’re the authors of a paper about dog image classification using a technique called “DogBert”. We could start by making a new Poetry project:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>poetry new dogbert
</code></pre></div></div>

<p>This will give us the following file structure:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dogbert
├── README.md
├── dogbert
│   └── __init__.py
├── pyproject.toml
└── tests
    └── __init__.py
</code></pre></div></div>

<p>It may seem confusing that there’s 2 nested folders, both named <code class="language-plaintext highlighter-rouge">dogbert</code>, but this is a standard setup for Python projects. The inner <code class="language-plaintext highlighter-rouge">dogbert</code> folder containing <code class="language-plaintext highlighter-rouge">__init__.py</code> is where all our library Python files will go. If you write tests (and you should!), those go in the <code class="language-plaintext highlighter-rouge">tests</code> folder.</p>

<p>We <code class="language-plaintext highlighter-rouge">cd</code> into the outer <code class="language-plaintext highlighter-rouge">dogbert</code> folder, and run <code class="language-plaintext highlighter-rouge">poetry install</code> to initialize a new pyenv environment for our project, and make sure any needed dependencies are installed.</p>

<p>We can add any pip dependencies our project needs with <code class="language-plaintext highlighter-rouge">poetry add &lt;dependency&gt;</code>. Finally, when we want to publish our library on PyPI so it can be installed with <code class="language-plaintext highlighter-rouge">pip install dogbert</code>, we just run the following 2 commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>poetry build
poetry publish
</code></pre></div></div>

<p>And that’s it, our library is on PyPI! There’s really not much to it, that’s all it takes to package and publish a library to PyPI.</p>

<h4 id="imports-and-scripts-in-poetry">Imports and scripts in Poetry</h4>

<p>If you’re used to just writing standalone python scripts in a single file and running them with <code class="language-plaintext highlighter-rouge">python my_file.py</code>, Poetry might seem strange at first. If we have the file <code class="language-plaintext highlighter-rouge">utils.py</code> at <code class="language-plaintext highlighter-rouge">dogbert/utils.py</code> with a function called <code class="language-plaintext highlighter-rouge">preprocess()</code>, and we have have another file which wants to import that <code class="language-plaintext highlighter-rouge">preprocess</code> function, we can import it like below:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">dogbert.utils</span> <span class="kn">import</span> <span class="n">preprocess</span>
</code></pre></div></div>

<p>Poetry creates its own pyenv enviroment so different projects have independent sets of installed Python modules. This is great, but it means that instead of directly running <code class="language-plaintext highlighter-rouge">python</code>, you need to prefix all commands on the CLI with <code class="language-plaintext highlighter-rouge">poetry run</code> so the correct pyenv is used. Also, for scripts, it’s best to run them using python’s module flag. Where you may be used to directly running a script file with <code class="language-plaintext highlighter-rouge">python path/to/script.py</code>, when using Poetry you’d instead run <code class="language-plaintext highlighter-rouge">poetry run python -m path.to.script</code>. If we had a script called <code class="language-plaintext highlighter-rouge">train.py</code> at <code class="language-plaintext highlighter-rouge">dogbert/scripts/train.py</code>, we could run that with <code class="language-plaintext highlighter-rouge">poetry run python -m dogbert.scripts.train</code>.</p>

<p>This may take some getting used-to initially, but it’s a minor workflow change which quickly becomes second-nature.</p>

<h4 id="aside-poetry-with-pytorch">Aside: Poetry with Pytorch</h4>

<p>I’ve had issues in the past with adding Pytorch as a dependency from Poetry since Pytorch has multiple versions with different CUDA requirements, which Poetry doesn’t handle well. I find it’s best to simply leave it to the end-user of your library to install Pytorch, and not try to force it via <code class="language-plaintext highlighter-rouge">poetry add torch</code>, since it’s easy to end up with a non-CUDA version of pytorch that way. Oftentimes, if you’re relying on libraries like <code class="language-plaintext highlighter-rouge">pytorch-lightning</code> or other popular machine learning libraries, they’ll already handle making sure PyTorch is installed. If you want to include torch as a dependency, I’d recommend adding it as a dev dependency <code class="language-plaintext highlighter-rouge">poetry add --group dev torch</code> so you won’t accidentally end up with a CPU-only version of PyTorch being installed for end-users of your library. Hopefully this will be handled better in future versions of Poetry/PyTorch!</p>

<h2 id="bonus-points-make-the-most-common-use-case-easy">Bonus points: Make the most common use-case easy</h2>

<p>Whatever users of your code are most likely to want to do should “just work” out of the box if possible. For instance, in the case of our DogBert image classification paper example, the most likely thing a user would want to do with our code is to classify images with our pretrained model. We should make this use-case as painless as possible. For instance, we can upload our pretrained model to the <a href="https://huggingface.co/models">Huggingface Model Hub</a> and then have our code automatically download and use that pretrained model if the user doesn’t specify a different model to use. If you want to upload your pretrained model somewhere else, that’s fine, just make sure your library can auto-download it by default so your code can “just work”.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">dogbert</span> <span class="kn">import</span> <span class="n">DogbertModel</span>

<span class="c1"># default case, auto-download our pretrained model from Huggingface
</span><span class="n">model</span> <span class="o">=</span> <span class="n">DogbertModel</span><span class="p">()</span>

<span class="c1"># allow the user to specify their own model if they want
</span><span class="n">model</span> <span class="o">=</span> <span class="n">DogbertModel</span><span class="p">(</span><span class="s">"/path/to/model"</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="use-a-linter">Use a linter</h2>

<p>Nothing makes me doubt the results of a paper more than opening up the code the paper links to and seeing unused variables and linting errors strewn throughout the files. Linting errors like this are the coding equivalent of submitting a paper to journal written in crayon. Fortunately, this is easy to remedy by just using a linter during development.</p>

<p>Linters like <a href="https://flake8.pycqa.org/en/latest/">Flake8</a> or <a href="https://pylint.readthedocs.io/en/latest/">Pylint</a> can check your code for common code-quality issues like unused variables and report them as errors. All popular code editors have plugins for Python linters which will highlight linting errors directly in your code. You can also customize the errors the linters report if there are types of errors you want to ignore. Linting errors ofter correspond to real bugs in your code, and are an easy way to improve your code quality at almost no cost. There’s really no downside to using a linter.</p>

<p>Related to linters are code formatters like <a href="https://black.readthedocs.io/en/stable/">Black</a>. Black will automatically format your code for you so the formatting is consistent, and takes away the need for you to think about formatting entirely. Personally, I think code formatters are great and recommend using Black, but this isn’t a universal opinion in the Python world. I’d recommend experimenting with this and see if you like it. There are plugins for all code editors which will let you automatically run Black whenever you save a Python file, which makes it really seamless.</p>

<h2 id="type-hints">Type hints</h2>

<p>Type hinting is a new addition to the Python world, but is something I’m a big fan of. Type hints take some getting used to initially, but the payoff is worth it. Adding type hints to your code allows the editor to auto-suggest variable and method names to you, and automatically tell if you if you mistyped some parameter name somewhere rather than crashing at runtime. Furthermore, users of your code will benefit from your type hints since then your library functions and parameter names will autocomplete in their editor too! If you use type hints, you also need to use a type checker to make the sure types are correct as Python will not do this for you. The two most popular are <a href="https://mypy.readthedocs.io/en/stable/">MyPy</a> and <a href="https://github.com/microsoft/pyright">PyRight</a>. These work like linters, and can easily be added to your editor to automatically report type errors as you code.</p>

<p>I also recommend moving away from using Python dictionaries to pass structured data around and instead using <a href="https://docs.python.org/3/library/dataclasses.html">dataclasses</a>. Dataclasses allow you to specify exactly what fields some data should contain, and will ensure those fields exist. This fits in nicely with type hinting since MyPy and other type checkers can verify you’re using the dataclasses correctly, and you never have to worry about accidentally mistyping a key of some python dict ever again.</p>

<h2 id="writing-tests">Writing tests</h2>

<p>Testing is something that I didn’t understand the value of until I started working professionally. There’s a natural aversion to writing tests as it feels like a bunch of extra work you need to do, and everyone always feels like they don’t have time for that. However, as I’ve improved as a software engineer and gotten more comfortable with writing tests, I find the exact opposite: I don’t have time <strong>not</strong> to write tests.</p>

<p>If you don’t add test cases as you code, you’re probably testing manually. However, this manual testing means that anytime you want to make a change to your existing code, either to refactor or to add new features, you’re always terrified you might accidentally break something you already wrote. Then you need to either go back and manually test everything again, likely forgetting something, or you just hack in your change in whatever way is the least likely to break something, resulting in hacks on top of hacks. This “fear-driven development” leads to horrible messes of code that are almost certainly broken and I believe leads to a lot of the code quality issues endemic throughout academic codebases.</p>

<p>Testing doesn’t have to be difficult. If you can test some piece of code in a Pytest test case rather than manually, do it. Some further tips for testing practically:</p>

<ul>
  <li>Don’t be afraid to make your source code uglier if it helps with testing. If splitting out the core functionality of a function into a separate function is easier to test, do it.</li>
  <li>Don’t worry about “unit” vs “integration” tests - just test however is easiest for you. Feel free to mix hyper-focused tests on small functions with tests that run through your whole model. The important thing is to have tests, no matter what kind.</li>
  <li>If there’s stochastic outputs from a function, it’s fine to just test that the output looks sane (e.g. tensors have the correct sizes, things that should sum to 1 do, etc…). Even basic assertions will still catch a lot of bugs.</li>
  <li>Use approximate assertions to check that outputs are “close enough”. Pytest has <code class="language-plaintext highlighter-rouge">approx</code> which lets you write assertions like <code class="language-plaintext highlighter-rouge">assert pytest.approx(x) == 3.1</code>, and torch has <code class="language-plaintext highlighter-rouge">assert torch.allclose(tensor1, tensor2)</code> to check if tensors are “close enough”.</li>
  <li>Don’t be afraid to hardcode output snapshots into tests to aid with algorithm refactoring. It can be helpful to set a random seed and pin down exact outputs so that you can refactor a function for performance and want to make sure you have identical behavior before and after.</li>
  <li>Feel free to create tiny toy datasets inside of tests, e.g. a list with 5 training examples, and run training on it just to make sure everything works.</li>
  <li>Use tests as way to interacticely debug your code as you develop. You can stick a debugger statement <code class="language-plaintext highlighter-rouge">import pdb; pdb.set_trace()</code> into your code, then run a test that runs through that code path so you can interactively experiment as you work.</li>
</ul>

<h2 id="pet-peeves-with-research-code">Pet peeves with research code</h2>

<p>The following are a few things that drive me crazy when reading research code. These are just my personal preferences, so YMMV.</p>

<ul>
  <li><strong>Dead code should be deleted:</strong> Frequently the code that accompanies papers is strewn with code paths that are never used, which makes it extremely hard to understand what’s going on. If you have code you’re not using, delete it, especially in the code you release along with your paper. Git maintains a version history so you can go back and find it later if you want.</li>
  <li><strong>Avoid multiple return types from functions:</strong> I frequently see functions that sometimes return a string, sometimes an int, sometimes a dictionary, depending on a bunch of input parameters. This makes it incredibly difficult to understand what a function is doing and what to expect from it. Just stick with 1 return type per function. Most linters will complain about this as well.</li>
  <li><strong>Avoid reinventing the wheel:</strong> I’ll often see research code that implements from scratch math operations that exist already in numpy or PyTorch. Anytime you write code, there’s a chance your code has bugs, so if there’s a function in a well-established battled-tested library like sklearn or numpy, you should always use that version over writing your own.</li>
  <li><strong>Avoid 1-char variable names:</strong> I know this comes from math, where everyone uses a single obscure greek letter for everything, but this makes it difficult to understand what things are referring to in code. IMO it’s always more understandable when a variable has a name like <code class="language-plaintext highlighter-rouge">classification_vector</code> rather than just <code class="language-plaintext highlighter-rouge">c</code>.</li>
</ul>

<h2 id="takeaways">Takeaways</h2>

<p>If there’s a single thing I want to leave you with, it’s that code published along with research papers should be usable by others. If you can accomplish that, you’re most of the way there. I believe that everything discussed in this article is very achievable for researchers, and is a lot easier than doing research itself, or learning LaTeX, or publishing papers. Researchers are smart people, and none of this is difficult. Once you get used to packaging code into a library that can be installed with <code class="language-plaintext highlighter-rouge">pip install</code>, it becomes second-nature, and the benefits to your success as a researcher and to others who want to use your code are immense.</p>

<p>For further reading, I’d recommend this <a href="https://mitelman.engineering/blog/python-best-practice/automating-python-best-practices-for-a-new-project/">excellent article on Python best practices</a>. It’s a couple years old at this point, but I think the ideas in the article are still very valid today.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Cake with melted plastic lego pieces, delicious. Generated by Midjourney]]></summary></entry><entry><title type="html">Remote debugging Readthedocs builds with tmate</title><link href="https://chanind.github.io/2023/01/29/remote-debug-readthedocs-tmate.html" rel="alternate" type="text/html" title="Remote debugging Readthedocs builds with tmate" /><published>2023-01-29T00:00:00+00:00</published><updated>2023-01-29T00:00:00+00:00</updated><id>https://chanind.github.io/2023/01/29/remote-debug-readthedocs-tmate</id><content type="html" xml:base="https://chanind.github.io/2023/01/29/remote-debug-readthedocs-tmate.html"><![CDATA[<p>I recently ported a library I built, <a href="https://github.com/chanind/tensor-theorem-prover">tensor-theorem-prover</a>, from being a pure Python library to being a hybrid Python/Rust library using <a href="https://pyo3.rs/">PyO3</a>. Despite my being a beginner with Rust, it resulted in a nearly 20x speedup over the old pure Python implementation! However, it also broke the Readthedocs build for hosting the docs for the project. In the process of debugging this, I found a (very hacky) way to connect to a running Readthedocs build which made resolving my issue much easier, but I suspect it will be useful for anyone who’s struggling to debug builds on Readthedocs.</p>

<div>
    <img src="/assets/rtd_fail.png" />
</div>
<p><i class="small gray">A fun way to spend a Saturday</i></p>

<p>The specific error for my build was that Sphinx was throwing an <code class="language-plaintext highlighter-rouge">ImportError</code> whenever it tried to import the Python code with Rust bindings, despite everything working when I tested locally, and Readthedocs even being able to build the actual Python/Rust package without issue. After several hours of pushing a change to try to fix the build, waiting 5 minutes for the build to finish, see it fail, then repeat, I got frustrated and decided to just see if I can somehow ssh into the live build and debug it directly there.</p>

<p>I remembed the Github Action <a href="https://github.com/mxschmitt/action-tmate">actions-tmate</a> which provides exactly this functionality in Github using <a href="https://tmate.io/">tmate</a>, so figured it might work for Readthedocs too. However, there are several impediments to this working easily in Readthedocs. Specifically:</p>

<ul>
  <li>Readthedocs doesn’t output the results of a command until the command has finished running, so you can’t see the SSH command output by <code class="language-plaintext highlighter-rouge">tmate -F</code> in the build output in order to connect.</li>
  <li>Running <code class="language-plaintext highlighter-rouge">tmate</code> in the background also doesn’t work, I suspect Readthedocs must kill processes between commands or something</li>
</ul>

<p>Fortunately, tmate lets you set up webhooks which it calls whenever it starts up a session, and contains all the info needed to connect! Combining this with <a href="https://ngrok.com/">ngrok</a> makes it possible to get notified via webhook when the session starts so you can ssh in and debug to your heart’s desire.</p>

<p>The full steps to get this working are laid out below.</p>

<h3 id="1-set-up-and-run-ngrok-on-your-local-computer">1. Set up and run ngrok on your local computer</h3>

<p>Grab a copy of ngrok for your local machine from <a href="https://ngrok.com">https://ngrok.com</a> or via your package manager of choice. Start it up with <code class="language-plaintext highlighter-rouge">ngrok http 5000</code> (the port doesn’t really matter much), and you should see an ouput like below.</p>

<div>
    <img src="/assets/ngrok.png" />
</div>

<p>Keep note of the URL, which in the example above is “https://ed59-81-107-232-184.eu.ngrok.io” for the next step.</p>

<h3 id="2-add-the-ngrok-url-as-an-environment-variable-in-readthedocs">2. Add the ngrok URL as an environment variable in Readthedocs</h3>

<p>In the Readthedocs UI for your project, go to “Admin” then “Environment Variables” and add a new environment variable. The name should be “WEBHOOK”, and the value is the ngrok URL from step 1.</p>

<h3 id="3-set-up-your-readthedocsyml">3. Set up your .readthedocs.yml</h3>

<p>You can customize your Readthedocs build using a <code class="language-plaintext highlighter-rouge">.readthedocs.yml</code> file in your Git repo. To set up tmate and remote debugging, configure your <code class="language-plaintext highlighter-rouge">.readthedocs.yml</code> to look like the example below. This will install tmate, configure it to use your ngrok URL as a webhook, and begin running tmate during the build.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">build</span><span class="pi">:</span>
  <span class="na">os</span><span class="pi">:</span> <span class="s">ubuntu-22.04</span>
  <span class="na">apt_packages</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">tmate</span>
  <span class="na">jobs</span><span class="pi">:</span>
    <span class="na">post_install</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">echo "set-option -g tmate-webhook-url '${WEBHOOK}'" &gt;&gt; ~/.tmate.conf</span>
      <span class="pi">-</span> <span class="s">tmate -F</span>
</code></pre></div></div>

<p>Commit this change to your git repo so that Readthedocs starts building.</p>

<h3 id="4-monitor-for-the-webhook-at-http1270014000">4. Monitor for the webhook at http://127.0.0.1:4000</h3>

<p>Ngrok lets you see all incoming requests via a locally running web interface. Open you your web browser to <code class="language-plaintext highlighter-rouge">http://127.0.0.1:4000</code> and keep an eye out for the webhook from Readthedocs, which should show up as a <code class="language-plaintext highlighter-rouge">POST /</code> with a <code class="language-plaintext highlighter-rouge">403 Forbidden</code> response, since we’re not actually returning anything. We just want to see the info that got posted in the JSON</p>

<h3 id="5-ssh-into-the-running-readthedocs-build">5. SSH into the running Readthedocs build</h3>

<p>Once a webhook comes in, find the fields <code class="language-plaintext highlighter-rouge">ssh_cmd_format</code> and <code class="language-plaintext highlighter-rouge">stoken</code>. The <code class="language-plaintext highlighter-rouge">ssh_cmd_format</code> field should look something like <code class="language-plaintext highlighter-rouge">ssh %s@nyc1.tmate.io</code>, and the <code class="language-plaintext highlighter-rouge">stoken</code> field should look like a random string of characters.</p>

<div>
    <img src="/assets/ngrok_webhook_data.png" />
</div>
<p><i class="small gray">An example of what the webhook data looks like in the local ngrok UI, with the <code class="language-plaintext highlighter-rouge">ssh_cmd_format</code> and <code class="language-plaintext highlighter-rouge">stoken</code> fields highligted</i></p>

<p>Just replace the <code class="language-plaintext highlighter-rouge">%s</code> in the <code class="language-plaintext highlighter-rouge">ssh_cmd_format</code> with the value in <code class="language-plaintext highlighter-rouge">stoken</code>, and copy / paste the command into a terminal on your local computer and run it.</p>

<h3 id="6-fist-pump">6. Fist pump!</h3>

<p>You should now have a working tmate terminal into your running Readthedocs build. Hopefully it should be a breeze to debug from there!</p>

<p>In my case, I solved my docs build bug about 5 minutes after getting this working. In case it’s useful to anyone else with the same issue with hybrid Rust/Python apps, the solution was to delete the main module folder containing Python code (<code class="language-plaintext highlighter-rouge">tensor_theorem_prover</code> in my case) after running <code class="language-plaintext highlighter-rouge">pip install .</code>. I don’t fully understand why this works, but it seems like somehow Python was finding the local folder rather than the compiled wheel with the rust code in it, and deleting the local module folder forced it to find the compiled module instead. ¯\_(ツ)_/¯</p>

<p>Hopefully this technique is helpful if you’re ever stuck debugging Readthedocs builds.</p>

<h3 id="happy-debugging">Happy debugging!</h3>]]></content><author><name></name></author><summary type="html"><![CDATA[I recently ported a library I built, tensor-theorem-prover, from being a pure Python library to being a hybrid Python/Rust library using PyO3. Despite my being a beginner with Rust, it resulted in a nearly 20x speedup over the old pure Python implementation! However, it also broke the Readthedocs build for hosting the docs for the project. In the process of debugging this, I found a (very hacky) way to connect to a running Readthedocs build which made resolving my issue much easier, but I suspect it will be useful for anyone who’s struggling to debug builds on Readthedocs.]]></summary></entry><entry><title type="html">Deploying to Netlify on Release Tags with Github Actions</title><link href="https://chanind.github.io/2023/01/21/deploy-netlify-github-actions-tag-release.html" rel="alternate" type="text/html" title="Deploying to Netlify on Release Tags with Github Actions" /><published>2023-01-21T00:00:00+00:00</published><updated>2023-01-21T00:00:00+00:00</updated><id>https://chanind.github.io/2023/01/21/deploy-netlify-github-actions-tag-release</id><content type="html" xml:base="https://chanind.github.io/2023/01/21/deploy-netlify-github-actions-tag-release.html"><![CDATA[<p>I recently had to set up a workflow where tagging a release on Github would trigger a deploy to production on Netlify. This turned out to be less straightforward than I expected originally, but I think the solution to make this work is functional and elegant. The idea is as follows:</p>

<ul>
  <li>Create an intermediate branch called <code class="language-plaintext highlighter-rouge">release</code>. Whatever is in the branch will deploy to Netlify</li>
  <li>Set up a Github Action which runs on tags and points the <code class="language-plaintext highlighter-rouge">release</code> branch at the most recent tag</li>
  <li>Set up Netlify to deploy production from the <code class="language-plaintext highlighter-rouge">release</code> branch</li>
</ul>

<p>We’ll discuss the rationale behind this and go through how to do this in more detail below.</p>

<h2 id="why-not-just-directly-deploy-release-tags-to-netlify">Why not just directly deploy release tags to Netlify?</h2>

<p>Sadly, direcly pushing tags to Netlify is tricky for a few reasons. First, <a href="https://answers.netlify.com/t/deploy-on-git-tags-only/43759/3">Netlify doesn’t support building on tags</a>, which would be the most obvious way to get this to work. Next, you may think, why not have Github actions run the app build and push to Netlify? This would work, but <a href="https://github.com/netlify/open-api/issues/168">Netlify doesn’t allow creation of scoped API tokens</a>, so any API token you generate will have access to everything in your Netlify account, not just the app you’re trying to deploy. You can get around this by creating a new Netlify account which only has access to the single app you want to deploy, but Netlify charges for every account that can access the project.</p>

<p>In addition, deploying directly on a tag makes it hard to implement hotfix workflows, where you may have code in your <code class="language-plaintext highlighter-rouge">main</code> or <code class="language-plaintext highlighter-rouge">master</code> branch that isn’t ready to go to production, but you need to release a fix ASAP. It’s possible to get around this by checking out the latest release into its own branch, adding a fix, and then releasing that, but it’s an annoying process. Using a <code class="language-plaintext highlighter-rouge">release</code> branch as an intermediary means you can also just treat it as a normal branch and push hotfixes directly to that branch in an emergency.</p>

<h2 id="ok-how-do-i-set-this-up">OK, how do I set this up?</h2>

<p>First, create a branch in your Github repo called <code class="language-plaintext highlighter-rouge">release</code>.</p>

<p>Next, create the Github Action which will point the <code class="language-plaintext highlighter-rouge">release</code> branch at a tag whenever a new tag is created. This is done by creating a workflow file in your repo in the folder <code class="language-plaintext highlighter-rouge">.github/workflows/</code>. In the example below, we name our workflow file <code class="language-plaintext highlighter-rouge">release.yml</code>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/release.yml</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">release</span>
<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">tags</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">v*"</span>
<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">deploy_releases</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">fetch-depth</span><span class="pi">:</span> <span class="m">0</span>
          <span class="na">ref</span><span class="pi">:</span> <span class="s">release</span>
      <span class="c1"># Point release branch at the latest tag so Netlify can deploy it</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Point release branch at tag</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "Setting 'release' branch to 'tags/$'"</span>
          <span class="s">git checkout release</span>
          <span class="s">git reset --hard tags/$</span>
          <span class="s">git push -f</span>
</code></pre></div></div>

<p>This workflow will run on any tag that starts with a <code class="language-plaintext highlighter-rouge">v</code>, assuming your releases are named like <code class="language-plaintext highlighter-rouge">v1.2.3</code> for <code class="language-plaintext highlighter-rouge">v17</code>. If you want this to run on a different tag, or on all tags, just modify the <code class="language-plaintext highlighter-rouge">- "v*"</code> line in the workflow above to the pattern you’d like. Once you push this file to your repo the github side of things is good to go!</p>

<p>Finally, you just need to change the deploy branch for your netlify site to deploy to the <code class="language-plaintext highlighter-rouge">release</code> branch instead of the default <code class="language-plaintext highlighter-rouge">main</code> or <code class="language-plaintext highlighter-rouge">master</code>. You can find this setting in Netlify at “Site settings” → “Build &amp; deploy” → “Branches and deploy contexts” → “Production branch”.</p>

<p>And that’s it! You’re now set up to deploy to production in Netlify on Git release tags.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I recently had to set up a workflow where tagging a release on Github would trigger a deploy to production on Netlify. This turned out to be less straightforward than I expected originally, but I think the solution to make this work is functional and elegant. The idea is as follows:]]></summary></entry><entry><title type="html">Running Jupyter Notebooks in Grid Engine with Ngrok</title><link href="https://chanind.github.io/python/2022/10/31/jupyter-sge-ngrok.html" rel="alternate" type="text/html" title="Running Jupyter Notebooks in Grid Engine with Ngrok" /><published>2022-10-31T00:00:00+00:00</published><updated>2022-10-31T00:00:00+00:00</updated><id>https://chanind.github.io/python/2022/10/31/jupyter-sge-ngrok</id><content type="html" xml:base="https://chanind.github.io/python/2022/10/31/jupyter-sge-ngrok.html"><![CDATA[<p>A lot of universities use <a href="https://en.wikipedia.org/wiki/Oracle_Grid_Engine">Oracle Grid Engine</a> (aka Sun Grid Engine, or SGE) for high-performance computing. This system lets you submit jobs requesting varying amounts of CPUs, GPUs, and memory to run machine learning (ML) and other compute-intensive tasks. This is great for when you’ve built a pipeline to train a ML model and just need a lot of power to run the training, but is awkward for experimentation and development since you’re just given a command prompt.</p>

<p>On the other end of the development cycle, there’s <a href="https://jupyter.org/">Jupyter</a>, which lets you write Python code in an interactive notebook, mixing text and images in with executable code. Development and experimentation in Jupyter is a joy since you can easily print interactive tables with data to the screen, or draw images, output interactive tensorboards - basically anything that can be displayed in a web browser can be turned into a Jupyter widget. If we can combine Jupyter with Grid Engine we can get the power of Grid Engine with the development ease of Jupyter.</p>

<p>The issue is that usually Grid Engine jobs don’t have ports open to the outside or directly allow ssh access to the running job, so running Jupyter inside of a Grid Engine session is difficult. Fortunately that’s where <a href="https://ngrok.com/">Ngrok</a> comes in. Ngrok is a tool which can forward a service running on a local machine and give you a web URL where you can access that service from the internet. This is perfect since it solves the problem of letting you easily access a Jupyter notebook that’s running inside of a Grid Engine session.</p>

<h2 id="initial-setup">Initial setup</h2>

<p>First, sign up for a free Ngrok account at <a href="https://ngrok.com">ngrok.com</a>. After you sign in, find the link to download the ngrok client for Linux and copy the URL of this link.</p>

<p>Next, ssh into Grid Engine and install ngrok in your home directory. This should look something like below:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># paste the linux download URL for ngrok here, it may be different than what's below</span>
wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz
<span class="nb">tar</span> <span class="nt">-xvzpf</span> ngrok-v3-stable-linux-amd64.tgz
</code></pre></div></div>

<p>At this point, you should have an executable called <code class="language-plaintext highlighter-rouge">ngrok</code> in your home directory.</p>

<p>Next, copy your auth token from the ngrok website (it should be a long pseudo-random string like <code class="language-plaintext highlighter-rouge">c8132179a3cE725B4e267_51F32179C3eE725B4E267</code>) and run the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./ngrok config add-authtoken &lt;your token here&gt;
</code></pre></div></div>

<p>At this point, ngrok should be good to go! Next just make sure you have jupyter installed with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>notebook
</code></pre></div></div>

<p>Since the notebook will be accessed on the web, you’ll need to modify the notebook config to allow remote connections. First, ensure you have a jupyter config available by running the following:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jupyter notebook <span class="nt">--generate-config</span>
</code></pre></div></div>

<p>This will generate a config file in your home directory, which should be located at <code class="language-plaintext highlighter-rouge">~/.jupyter/jupyter_notebook_config.py</code>. Run the following command to update this config file and allow remote access:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"c.NotebookApp.allow_remote_access = True"</span> <span class="o">&gt;&gt;</span> ~/.jupyter/jupyter_notebook_config.py
</code></pre></div></div>

<p>It’s a good idea to set a password for jupyter since we’re going to make it accessible on the internet, and you don’t want random strangers on the internet to be able to run code in your notebook if they stumble on the URL somehow.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jupyter notebook password
</code></pre></div></div>

<h2 id="running-jupyter--ngrok-in-an-interactive-session">Running Jupyter + Ngrok in an interactive session</h2>

<p>Next, start an interactive session in Grid Engine, something like the following:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qrsh <span class="nt">-l</span> <span class="nv">tmem</span><span class="o">=</span>10G,h_rt<span class="o">=</span>2:00:00,gpu<span class="o">=</span><span class="nb">true</span> <span class="nt">-now</span> no <span class="nt">-verbose</span>
</code></pre></div></div>

<p>Once your session has started, you need to run both ngrok and Jupyter on the same port. The specific port number doesn’t matter much - you just don’t want to pick a number that someone else on the same machine might also be using. Below I’m using port 7923, but change this to whatever number you prefer (numbers in the 7000-9999 range tend to be good choices).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(trap 'kill 0' SIGINT; jupyter notebook --no-browser --port 7923 &amp;
~/ngrok http 7923 --log=stdout)
</code></pre></div></div>

<p>The command above just runs jupyter and ngrok in parallel, and kills them both when you exit the shell.</p>

<p>Now, ngrok should display a URL on the screen (something like <code class="language-plaintext highlighter-rouge">https://aba4-128-90-27-382.eu.ngrok.io</code>) which you can open up in your browser, and, voila, you should see your jupyter notebook running inside of your Grid Engine interactive shell! And that’s it, you’ve got Jupyter running inside of Grid Engine.</p>

<h2 id="running-jupyter--ngrok-in-a-sge-job">Running Jupyter + Ngrok in a SGE job</h2>

<p>You can of course run the same commands as a standalone SGE job which you submit with <code class="language-plaintext highlighter-rouge">qsub</code>. Copy the script below and save it as <code class="language-plaintext highlighter-rouge">remote_jupyter.qsub.sh</code>. Tweak the parameters as needed to suit your use case.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#$ -l mem=10G</span>
<span class="c">#$ -l h_rt=24:0:0</span>
<span class="c">#$ -S /bin/bash</span>
<span class="c">#$ -N remote-jupyter</span>

<span class="nb">set</span> <span class="nt">-e</span>

<span class="c"># pick a port at random between 7001-7999</span>
<span class="nv">PORT</span><span class="o">=</span><span class="sb">`</span><span class="nb">shuf</span> <span class="nt">-i</span> 7001-7999 <span class="nt">-n</span> 1<span class="sb">`</span>
<span class="nb">echo</span> <span class="s2">"Starting Jupyter and tunnel on port </span><span class="k">${</span><span class="nv">PORT</span><span class="k">}</span><span class="s2">"</span>

<span class="c"># run jupyter in the background and ngrok in the foreground</span>
<span class="c"># connect to the URL that ngrok outputs to the terminal</span>
<span class="o">(</span><span class="nb">trap</span> <span class="s1">'kill 0'</span> SIGINT<span class="p">;</span> jupyter notebook <span class="nt">--no-browser</span> <span class="nt">--port</span> <span class="k">${</span><span class="nv">PORT</span><span class="k">}</span> &amp;
~/ngrok http <span class="k">${</span><span class="nv">PORT</span><span class="k">}</span> <span class="nt">--log</span><span class="o">=</span>stdout<span class="o">)</span>
</code></pre></div></div>

<p>Then, submit the job as usual with <code class="language-plaintext highlighter-rouge">qsub remote_jupyter.qsub.sh</code>. When the job runs, you’ll be able to find the URL of the ngrok session in the logs, or you can check the ngrok web interface at <a href="https://ngrok.com">ngrok.com</a> and click “tunnels” to find your jupyter notebook.</p>

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

<p>This post takes a lot of inspiration and ideas from <a href="https://towardsdatascience.com/how-to-share-your-jupyter-notebook-in-3-lines-of-code-with-ngrok-bfe1495a9c0c">Khuyen Tran’s blog post</a>. Thanks, Khuyen!</p>

<p>If you have any improvements to this technique, let me know!</p>]]></content><author><name></name></author><category term="python" /><summary type="html"><![CDATA[A lot of universities use Oracle Grid Engine (aka Sun Grid Engine, or SGE) for high-performance computing. This system lets you submit jobs requesting varying amounts of CPUs, GPUs, and memory to run machine learning (ML) and other compute-intensive tasks. This is great for when you’ve built a pipeline to train a ML model and just need a lot of power to run the training, but is awkward for experimentation and development since you’re just given a command prompt.]]></summary></entry><entry><title type="html">Initial Thoughts on Abstract Meaning Representation (AMR)</title><link href="https://chanind.github.io/amr/2022/10/30/thoughts-on-amr.html" rel="alternate" type="text/html" title="Initial Thoughts on Abstract Meaning Representation (AMR)" /><published>2022-10-30T00:00:00+00:00</published><updated>2022-10-30T00:00:00+00:00</updated><id>https://chanind.github.io/amr/2022/10/30/thoughts-on-amr</id><content type="html" xml:base="https://chanind.github.io/amr/2022/10/30/thoughts-on-amr.html"><![CDATA[<p>I’m interested in extracting meaning from text in a form that can be reasoned over by computers, and was thus really excited when I stumbled on <a href="https://amr.isi.edu/">Abstract Meaning Representation</a> (AMR). AMR is a really cool idea. It parses sententes into a tree structure based around <a href="https://en.wikipedia.org/wiki/Frame_semantics_(linguistics)">semantic frames</a> while extracting out numbers and dates into a structured format within the tree. It feels like it combines the meaning extraction power of semantic frames with the tree structure of a <a href="https://en.wikipedia.org/wiki/Syntactic_parsing_(computational_linguistics)#Dependency_parsing">dependency parse</a>.</p>

<p>Below is a sample of how the AMR looks for the sentence <code class="language-plaintext highlighter-rouge">The boy must not go</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(o / obligate-01
    :ARG2 (g / go-02
        :ARG0 (b / boy)
        :polarity -))
</code></pre></div></div>

<p>Here, the “not” from “must not go” is extracted as the <code class="language-plaintext highlighter-rouge">:polarity -</code> attribute, which is helpfully machine-readable. <a href="https://propbank.github.io/v3.4.0/frames/alias-obligate.html#obligate">obligate-01</a> is a semantic frame from Propbank, as is <a href="https://propbank.github.io/v3.4.0/frames/go.html">go-02</a>. AMR strips away the tense of verbs and ideally results in sentences with the same meaning having the same AMR representation. AMR is also English-only, as the semantic frames correspond to Engish, but there are projects for AMR for other languages as well.</p>

<h2 id="amr-parsers">AMR Parsers</h2>

<p>Ideas like AMR are only as useful as the ecosystem around them. The most important parts of that ecosystem are parsers that can take a natural language sentence and return a representation of the sentence in AMR format. Here, there’s state-of-the-art work done by IBM with their <a href="https://github.com/IBM/transition-amr-parser">Transition AMR Parser</a>. This parser had the best parsing results when I played around with it, and it even returns alignments so you can map each token in the AMR back to the original sentence! However, it’s also extremely difficult to get to work due to it having finicky requirements, not being packaged in PyPi, and requires emailing someone at IBM to get acces to a pretrained checkpoint.</p>

<p>Sapienza University also has an open-source parser called <a href="https://github.com/SapienzaNLP/spring">Spring</a>. This is easier to get working since it just uses Huggingface internally which is very standard, and the pretrained model checkpoints are publicly available. However, like IBM’s parser, this is research code which isn’t properly packaged on PyPi, and isn’t really documented. So, using it in your own code requires reading through their source code.</p>

<p>Finally, there’s <a href="https://amrlib.readthedocs.io/en/latest/">Amrlib</a>. Amrlib is a true joy to use. It’s simple to set up, and even integrates with <a href="https://spacy.io/">Spacy</a>! However, Amrlib seems to be the least accurate of the parsers I experimented with. Still, its ease of use makes it definitely worth it for something that just works. Hopefully the parsers from IBM and Sapienza can learn from Amrlib’s usability.</p>

<h2 id="amrs-weakness-closed-datasets-and-closed-tools">AMR’s weakness: closed datasets and closed tools</h2>

<p>All of the parsers discussed above suffer from the same core problem: lack of diverse, freely-available AMR training data in large quantity. This is an unfortunate problem which I feel holds back AMR from reaching its true potential. The largest AMR training data set, which all the parsers mentioned above train on, is the offical <a href="https://catalog.ldc.upenn.edu/LDC2020T02">AMR Corpus</a>. This corpus contains 59,255 AMR-annotated sentences, which is significant but still tiny compared to the amount of data modern NLP systems are trained on. Furthermore, this dataset is not freely available - it requires paying $300 just to access it! This is almost certainly discouraging innovation in the AMR space by setting such a huge financial bar to even experiment with the data.</p>

<p>This wouldn’t be so bad if tools to create more high-quality annotated AMR training data were readily available. However, here, too, the only AMR editor that I could find, the official <a href="https://amr.isi.edu/editor.html">AMR Editor</a>, is closed-source and outdated. According to the editor page, it’s estimated it takes 10 minutes just to annotate a single sentence with the tool! I could imagine this editor could be improved if it were open-source so the community could contribute, or if another person in the community could create and open-source a high-quaity AMR editor.</p>

<h2 id="moving-amr-forward">Moving AMR forward</h2>

<p>I feel like AMR has so much potential. The core idea is simple yet powerful, and it feels like a natural way to parse a sentence for semantic meaning. The number of research papers still being written on AMR shows that there’s a lot of other people who can see the power of AMR as well. It’s just too bad that the closed, paywalled nature of the training data and the closed-source editor hinder AMR from reaching its full potential. Hopefully the community will address these issues in the future!</p>]]></content><author><name></name></author><category term="amr" /><summary type="html"><![CDATA[I’m interested in extracting meaning from text in a form that can be reasoned over by computers, and was thus really excited when I stumbled on Abstract Meaning Representation (AMR). AMR is a really cool idea. It parses sententes into a tree structure based around semantic frames while extracting out numbers and dates into a structured format within the tree. It feels like it combines the meaning extraction power of semantic frames with the tree structure of a dependency parse.]]></summary></entry><entry><title type="html">FrameNet Parsing with Transformers</title><link href="https://chanind.github.io/ai/2022/05/24/framenet-transformers.html" rel="alternate" type="text/html" title="FrameNet Parsing with Transformers" /><published>2022-05-24T00:00:00+00:00</published><updated>2022-05-24T00:00:00+00:00</updated><id>https://chanind.github.io/ai/2022/05/24/framenet-transformers</id><content type="html" xml:base="https://chanind.github.io/ai/2022/05/24/framenet-transformers.html"><![CDATA[<div>
    <img src="/assets/transformer_book.png" />
</div>
<p><i class="small gray">A transformer reading a book, generated by pixray-vqgan</i></p>

<p><strong>TLDR;</strong> If you want to skip the details of how this works, the end-project is available here: <a href="https://github.com/chanind/frame-semantic-transformer">Frame-Semantic-Transformer</a>. A <a href="https://chanind.github.io/frame-semantic-transformer/">live demo</a> is available as well.</p>

<p>If you want to get meaningful semantic information from a sentence that can be used by algorithms, you first need parse it. One powerful framework for this is the idea of <a href="https://en.wikipedia.org/wiki/Frame_semantics_(linguistics)">Frame Semantics</a>. Frame semantics break apart a sentence into concepts called “frames”, where each frame contains attributes called “frame elements” which describe what’s going on in the frame, and has a “trigger” in the sentence which evokes the frame.</p>

<p>For instance, consider the sentence below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sergey dodged the flower pot that Larry threw in disgust.
</code></pre></div></div>

<p>A frame that might be present is the idea of “dodging”:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>frame: Dodging
trigger: "dodged"
elements:
    Dodger="Sergey"
    Bad_entity="the flower pot that Larry threw in disgust"
</code></pre></div></div>

<p>There can be multiple frames present in a sentence at a time, and frames can relate to and inherit from other frames as well.</p>

<p>The gold standard for Frame Semantics is a project called <a href="https://framenet.icsi.berkeley.edu/">FrameNet</a>, which contains an open database of thousands of frames and annotated example texts.</p>

<h2 id="what-about-existing-frame-semantic-parsers">What about existing frame semantic parsers?</h2>

<p>I’m certainly not the first person to attempt to build a frame semantic parser (sometimes also called automatic semantic role labeling). The 2 state-of-the-art projects I found are <a href="https://github.com/swabhs/open-sesame">Open-Sesame</a>, and the paper <a href="https://arxiv.org/abs/2010.10998">Open-Domain Frame Semantic Parsing Using Transformers</a>.</p>

<p>Open-Sesame is the best performing open-source frame semantic parser, but has a number of problems that make it difficult to work with as an end-user:</p>

<ul>
  <li>Models trained for Open-Sesame can only run on the computer they were trained on, <a href="https://github.com/swabhs/open-sesame/issues/15">otherwise they perform poorly</a>. This includes the provided pre-trained models.</li>
  <li>Installation is difficult, and requires manually installing dependencies and uses rare ML libraries which are hard to get working properly.</li>
  <li>The pretrained models don’t work correctly on <a href="https://github.com/swabhs/open-sesame/issues/61">current systems</a></li>
</ul>

<p>The paper <a href="https://arxiv.org/abs/2010.10998">Open-Domain Frame Semantic Parsing Using Transformers</a> looks great - it uses Google’s <a href="https://arxiv.org/abs/1910.10683">T5 Transformer</a> and claims to achieve even better performance than Open-Sesame. However, it’s not open-source, so there’s no actual code to run or a library to work with.</p>

<h2 id="building-an-open-source-frame-semantic-parser">Building an open-source frame semantic parser</h2>

<p>I decided to combine the best of Open-Sesame and “Open-Domain Frame Semantic Parsing Using Transformers” to build an easy-to-use open-source frame semantic parser on modern technology. I used the data splitting, task definitions, and evaluation criteria from Open-Sesame, while using a T5 transformer as a base model as in the open-domain parsing paper.</p>

<p>My goal is to create a frame-semantic parser which meets the following criteria:</p>

<ol>
  <li>Match or exceed Open-Sesame’s performance</li>
  <li>Installable and usable with a single <code class="language-plaintext highlighter-rouge">pip install</code>.</li>
  <li>Built on modern tech like <a href="https://pytorch.org/">PyTorch</a> and <a href="https://huggingface.co/">HuggingFace</a>.</li>
</ol>

<h2 id="the-tasks">The Tasks</h2>

<p>Semantic parsing of a sentence as performed by Open-Sesame requires 3 steps:</p>

<ol>
  <li><strong>Trigger Identification</strong>: Find the frame trigger locations in the text.</li>
  <li><strong>Frame Classification</strong>: For each trigger, determine which frame it corresponds to.</li>
  <li><strong>Argument Extraction</strong>: After determining the frame, find the corresponding frame elements in the text.</li>
</ol>

<p>For example, consider the following the sentence:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>It was no use trying the lift.
</code></pre></div></div>

<p>For the first step, trigger identification, we would identify the 2 following locations, indicated by <code class="language-plaintext highlighter-rouge">*</code>’s in the sentence below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>It was no use *trying the *lift.
</code></pre></div></div>

<p>Next, we need to identify which frame corresponds with each trigger location:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>It was no use *trying the lift.
    -&gt; Attempt_means

It was no use trying the *lift.
    -&gt; Connecting_architecture
</code></pre></div></div>

<p>Finally, for each trigger and frame, we need to find the frame elements in the frame:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>It was no use *trying the lift. :: Attempt_means
    -&gt; Means="the lift"

It was no use trying the *lift. :: Connecting_architecture
    -&gt; Part="lift"
</code></pre></div></div>

<p>In FrameNet, there are tens of thousands of annotated sentences like this indicating the triggers, frames, and frame elements in the sentence which we can use to train our model.</p>

<h2 id="t5-transformer">T5 Transformer</h2>

<p>Transformer architectures have revolutionize the field of language processing (NLP) since their introduction in 2017. The typical idea is to start with a transformer model that’s already pre-trained on a massive quantity of text from the internet, and just “fine-tune” it on the actual task you care about.</p>

<p>In this case, we use the T5 transformer provided by HuggingFace. T5 uses the idea of having a single model perform multiple tasks, with each task simply being indicated by adding a keyword to the input text.</p>

<p>For example, for the sentence we discussed above, we could break apart the tasks as follows:</p>

<p>First, trigger identification</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>input:  "TRIGGER: It was no use trying the lift."
output: "It was no use *trying the *lift."
</code></pre></div></div>

<p>Next, frame classification</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>input:  "FRAME: It was no use *trying the lift."
output: "Attempt_means"

input:  "FRAME: It was no use trying the *lift."
output: "Connecting_architecture"
</code></pre></div></div>

<p>Finally, argument extraction:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>input:  "ARGS Attempt_means: It was no use *trying the lift."
output: "Means=the lift"

input:  "ARGS Connecting_architecture: It was no use trying the *lift."
output: "Parts=lift"
</code></pre></div></div>

<p>Notice above how all the tasks follow the same input/output format, where each task takes a string as input and returns a string as output. Furthermore, each task is specified by putting a keyword at the start of the input followed by a <code class="language-plaintext highlighter-rouge">:</code>, for example <code class="language-plaintext highlighter-rouge">Frame:</code> for frame classification, and <code class="language-plaintext highlighter-rouge">Trigger:</code> for trigger identification. For argument extraction, we also put the name of the frame as part of the task definition <code class="language-plaintext highlighter-rouge">ARGS &lt;frame_name&gt;:</code>.</p>

<p>I based the T5 training on <a href="https://github.com/Shivanandroy/simpleT5">SimpleT5</a>, which uses Pytorch Lighting and HuggingFace under the hood.</p>

<p>…and that’s all it really takes to get a working frame semantic parser using T5!</p>

<h2 id="providing-hints-to-t5">Providing hints to T5</h2>

<p>That’s not the end of the story, unfortunately. This approach performs well already - it actually beats Open-Sesame at argument extraction even without any extra tweaks! However, it doesn’t perform as well at frame classification, and we can do even better at argument extraction.</p>

<p>The key insight is that for the frame identification and argument extraction tasks, we can give T5 some extra hints to help it choose the best results. For frame classification, FrameNet includes a list of “<a href="https://framenet.icsi.berkeley.edu/fndrupal/luIndex">lexical units</a>” which are likely triggers of each frame. We can use this list to find some candidate frames for each trigger word.</p>

<p>For instance, for the word <code class="language-plaintext highlighter-rouge">try</code>, the following lexical units appear in FrameNet:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">try.v : Attempt</code></li>
  <li><code class="language-plaintext highlighter-rouge">try.v : Try_defendant</code></li>
  <li><code class="language-plaintext highlighter-rouge">try.v : Attempt_means</code></li>
  <li><code class="language-plaintext highlighter-rouge">try.v : Tasting</code></li>
</ul>

<p>With the sentence <code class="language-plaintext highlighter-rouge">It was no use *trying the lift.</code>, we can extract the labeled trigger word <code class="language-plaintext highlighter-rouge">trying</code>, stem it with NLTK to get <code class="language-plaintext highlighter-rouge">try</code>, and then check the lexical unit list in FrameNet to see a list of reasonable frames to guess. Then, we can pass these into T5 in the task definition, like below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>input: "FRAME Attempt Try_defendant Attempt_means Tasting: It was no use *trying the lift."
output: "Attempt_means"
</code></pre></div></div>

<p>By checking the lexical units list in FrameNet, we’re able to provide 4 possible frames to T5 in the task definition which makes it must easier for it to simply pick one of those 4 frames rather than needing to guess the frame out of thin air! In the <a href="https://github.com/chanind/frame-semantic-transformer">Frame-Semantic-Transformer</a> project, we take this even further and check bigrams of words involving the trigger as well to search for matching lexical units.</p>

<p>For the argument extraction task, we can similarly help T5 by pre-emptively pulling out a list of all the possible frame elements for the frame in question. For instance, the <code class="language-plaintext highlighter-rouge">Attempt_means</code> frame has the following possible elements, abbreviated for clarity:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Agent</code></li>
  <li><code class="language-plaintext highlighter-rouge">Means</code></li>
  <li><code class="language-plaintext highlighter-rouge">Goal</code></li>
  <li><code class="language-plaintext highlighter-rouge">Circumstances</code></li>
  <li>…</li>
</ul>

<p>We can similarly provide this list to T5 as part of the task header:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>input: "ARGS Attempt_means | Agent Means Goal Circumstances: It was no use *trying the lift."
output: "Attempt_means"
</code></pre></div></div>

<h2 id="augmenting-the-data">Augmenting the data</h2>

<p>After first trying this out, it became immediately apparent that the model was overfit to the data as it appears on FrameNet. Specifically, on FrameNet, all sentences end in proper punctuation. If you try asking for the frames of a sentence that doesn’t have a period at the end, the model often freaks out and starts repeating itself over-and-over and outputting nonsense. During training it never encountered an input sentence without a period at the end, it didn’t know what to do!</p>

<p>To alleviate this, Frame-Semantic-Transformer adds some extra data augmentations to the training samples, like occasionally dropping the period at the end of the sentence, or changing “can’t” into “cannot”, or making everything lowercase. These tweaks won’t help the model improve its score on the test data, but it should help it work better on unseen data.</p>

<h2 id="evaluating-performance">Evaluating performance</h2>

<p>So of course the question is: how does this T5-based approach compare to Open-Sesame? I trained the model on the same breakdown of train/dev/test documents from FrameNet as Open-Sesame, and I tried to use the same metrics as Open-Sesame so the results would be a fair apples-to-apples comparison. I also trained 2 variants of the T5 model - one variant using <code class="language-plaintext highlighter-rouge">t5-base</code> which is about 850MB, and another using <code class="language-plaintext highlighter-rouge">t5-small</code>, which is about 230MB.</p>

<p>The results are as follows on the Open-Sesame test set:</p>

<table>
  <thead>
    <tr>
      <th>Task</th>
      <th>Sesame F1</th>
      <th>Small Model F1</th>
      <th>Base Model F1</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Trigger identification</td>
      <td>0.73</td>
      <td>0.70</td>
      <td>0.72</td>
    </tr>
    <tr>
      <td>Frame classification</td>
      <td>0.87</td>
      <td>0.81</td>
      <td>0.87</td>
    </tr>
    <tr>
      <td>Argument extraction</td>
      <td>0.61</td>
      <td>0.70</td>
      <td>0.72</td>
    </tr>
  </tbody>
</table>

<p>The base model performs pretty similarly to Open-Sesame at task identification and frame classification, but performs significantly better at argument extraction. The small model performs a bit worse than the base model, and under-performs Open-Sesame on trigger identification and frame classification, but is still significantly better than Sesame at argument extraction.</p>

<h2 id="next-steps">Next steps</h2>

<p>I expect there’s still more improvements that can be made to help <a href="https://github.com/chanind/frame-semantic-transformer">Frame-Semantic-Transformer</a> perform even better than it does now:</p>
<ul>
  <li>I didn’t tune hyperparams much, so I’m sure there’s a few more f1 points to be squeezed out from that.</li>
  <li>I worry that the model is overfit on FrameNet training data and won’t perform as well on real-world tasks; this will need more investigation and testing.</li>
  <li>The <a href="https://arxiv.org/abs/2010.10998">Open-Domain Frame Parsing</a> paper talks about using numbers to mark spans in argument extraction which might improve performance on argument extraction.</li>
  <li>The paper also tries using custom decoders per task instead of the standard T5 text decoder.</li>
  <li>There are even larger T5 models to try using, like <code class="language-plaintext highlighter-rouge">t5-large</code> and <code class="language-plaintext highlighter-rouge">t5-3b</code> which could perform even better.</li>
  <li>FrameNet lexical units don’t cover everything, so it’s probably possible to use some sort of clustering of lexical units to give better hints to T5 for frame classification.</li>
</ul>

<p>Longer term, it would be great to expand this to bigger / better datasets than FrameNet (ex. multi-lingual framenet) that can be used to train the model. It would be awesome as well to try to generate more frames / lexical units for FrameNet automatically using a technique like what was done to generate <a href="https://github.com/peterwestai2/symbolic-knowledge-distillation">Atomic10x</a> in the paper <a href="https://arxiv.org/abs/2110.07178">Symbolic Knowledge Distillation - from General Language Models to Commonsense Models</a></p>

<p>Any contributions to the project or thoughts/feedback is welcome!</p>]]></content><author><name></name></author><category term="ai" /><summary type="html"><![CDATA[A transformer reading a book, generated by pixray-vqgan]]></summary></entry><entry><title type="html">Contextual Bandits in Python with Vowpal Wabbit</title><link href="https://chanind.github.io/ai/2022/03/15/vowpal-wabbit-contextual-bandits-python.html" rel="alternate" type="text/html" title="Contextual Bandits in Python with Vowpal Wabbit" /><published>2022-03-15T00:00:00+00:00</published><updated>2022-03-15T00:00:00+00:00</updated><id>https://chanind.github.io/ai/2022/03/15/vowpal-wabbit-contextual-bandits-python</id><content type="html" xml:base="https://chanind.github.io/ai/2022/03/15/vowpal-wabbit-contextual-bandits-python.html"><![CDATA[<div style="text-align: center; margin-bottom: 40px;">
    <img src="/assets/vw_logo.svg" alt="vowpal wabbit logo" style="max-width: 200px" />
</div>

<p>Over the past few weeks I’ve been using <a href="https://vowpalwabbit.org/">Vowpal Wabbit</a> (VW) to develop contextual bandit algorithms in Python. Vowpal Wabbit’s core functionality is excellent and it appears to be the industry standard for working with bandits. However, the library is not well documented and has numerous gotchas and partially-working features, especially in the Python bindings. The library overall feels like it was built by academics rather than engineers, so the documentation treats most of the core engineering tasks as trivial and not worth explaining, while frequenly linking off to 50-page long academic research papers as explanations of what the options in the library mean.</p>

<p>As an engineer, there’s a lot I’ve learned that I wish I knew when I first started using this library. This post is a brain-dump of what I’ve learned that’s been useful, important, or surprising for me working with this library. I hope it will be useful for others as well! the core functionality of the library is truly excellent, it just takes a bit of effort to get it into a state where it can really shine.</p>

<p>This post will focus on working with the Python bindings, but a most of this will apply to working with the command-line interface as well, since the Python wrapper is just a thin wrapper around the CLI. I used the <code class="language-plaintext highlighter-rouge">--cb_explore_adf</code> setting, which is the most complicated, least documented, and, in my opinion, most useful setting for bandits. This setting allows for picking from a different set of actions at each invocation of the library, and allows actions to have rich sets of features as well. This post will focus on using this setting, but a lot of this post will still be relevant for using other bandit settings in vowpal wabbit as well.</p>

<p>If you see any mistakes or places where there are misunderstandings in this post, please leave a comment and let me know! I’m still learning and will continue to make corrections and improvements to this article as I learn more.</p>

<h3 id="working-with-json-format">Working with JSON Format</h3>

<p>The default VW input format is a string format that looks like the following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>shared | UserAge:15
| elections maine SourceTV
0:3:.3 | Sourcewww topic:4
</code></pre></div></div>

<p>VW also supports a <a href="https://github.com/VowpalWabbit/vowpal_wabbit/wiki/JSON">JSON input format</a>, which would look like the following:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"UserAge"</span><span class="p">:</span><span class="w"> </span><span class="mi">15</span><span class="p">,</span><span class="w">
  </span><span class="nl">"_multi"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"_text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"elections maine"</span><span class="p">,</span><span class="w"> </span><span class="nl">"Source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TV"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"Source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"www"</span><span class="p">,</span><span class="w"> </span><span class="nl">"topic"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w"> </span><span class="nl">"_label"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0:3:.3"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>I went with the JSON format since it feels more structured, but this has been hard since this format isn’t super well documented. This JSON format is only valid JSON for individual examples. If you want to use it for more than a single example, you need to concat JSON examples with newlines between them, NOT use a JSON array, as you would probably expect. For example:</p>

<p>Correct:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
 "User": ...
 "_multi":[...]
}
{
 "User": ...
 "_multi":[...]
}
{
 "User": ...
 "_multi":[...]
}
</code></pre></div></div>

<p>Incorrect:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
    {
    "User": ...
    "_multi":[...]
    },
    {
    "User": ...
    "_multi":[...]
    },
    {
    "User": ...
    "_multi":[...]
    }
]
</code></pre></div></div>

<p>This was a surprise, because the “correct” way to use the JSON format here is to not actually valid JSON! Also, if you use this format you need to pass the <code class="language-plaintext highlighter-rouge">--json</code> param to VW.</p>

<p>There’s another json format called <code class="language-plaintext highlighter-rouge">--dsjson</code>. This is even less documented than the <code class="language-plaintext highlighter-rouge">--json</code> format, so I wasn’t able to figure out how to use it.</p>

<p>If you want to use the JSON format in python, you need to pass the JSON to VW as a JSON-encoded string, not a Python dict. So something like the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">vowpalwabbit</span>
<span class="kn">import</span> <span class="nn">json</span>

<span class="n">vw</span> <span class="o">=</span> <span class="n">vowpalwabbit</span><span class="p">.</span><span class="n">Workspace</span><span class="p">(</span><span class="s">"--cb_explore_adf --json"</span><span class="p">)</span>
<span class="n">example</span> <span class="o">=</span> <span class="p">{</span>
 <span class="s">"UserAge"</span><span class="p">:</span><span class="mi">15</span><span class="p">,</span>
 <span class="s">"_multi"</span><span class="p">:[</span>
   <span class="p">{</span><span class="s">"_text"</span><span class="p">:</span><span class="s">"elections maine"</span><span class="p">,</span> <span class="s">"Source"</span><span class="p">:</span><span class="s">"TV"</span><span class="p">},</span>
   <span class="p">{</span><span class="s">"Source"</span><span class="p">:</span><span class="s">"www"</span><span class="p">,</span> <span class="s">"topic"</span><span class="p">:</span><span class="mi">4</span><span class="p">,</span> <span class="s">"_label"</span><span class="p">:</span><span class="s">"0:3:.3"</span><span class="p">}</span>
 <span class="p">]</span>
<span class="p">}</span>
<span class="n">vw</span><span class="p">.</span><span class="n">learn</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">example</span><span class="p">))</span>
</code></pre></div></div>

<h3 id="namespaces">Namespaces</h3>

<p>You should put all your features into namespaces rather than on the top level, since this lets you make your model more powerful with the <code class="language-plaintext highlighter-rouge">--quadratic</code> and <code class="language-plaintext highlighter-rouge">--cubic</code> options as we’ll see later. For instance, below we put shared features into a namespace called “User”, and action features into a namespace called “Action”, although you can have multiple shared and action-level namespaces if you want. In JSON, this looks like the following:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"User"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"age"</span><span class="p">:</span><span class="w"> </span><span class="mi">15</span><span class="w"> </span><span class="p">},</span><span class="w">
  </span><span class="nl">"_multi"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"_text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"elections maine"</span><span class="p">,</span><span class="w"> </span><span class="nl">"Source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TV"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"Source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"www"</span><span class="p">,</span><span class="w"> </span><span class="nl">"topic"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"_label"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0:3:.3"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Note: the <code class="language-plaintext highlighter-rouge">_label</code> property appears outside of the namespace for the action that was chosen.</p>

<p>I was originally pretty confused by the format of <code class="language-plaintext highlighter-rouge">_label</code> for <code class="language-plaintext highlighter-rouge">--cb_explore_adf</code>. The label has 3 components, the action number, the cost, and the probability that this action was picked by the policy that generated the data. For <code class="language-plaintext highlighter-rouge">--cb_explore_adf</code>, the action number is meaningless, so just write <code class="language-plaintext highlighter-rouge">0</code> ¯\_(ツ)_/¯.</p>

<h3 id="model-architecture">Model Architecture</h3>

<p>VW will hash all your features into a large number of buckets (2^18 by default), and learns a weight for each bucket. Then, it just sums the weights of each bucket together to get a score for the action. This is demonstrated in the diagram below.</p>

<p><img src="/assets/vw_model.png" alt="vowpal wabbit model architecture" />
<i class="small">Basic Vowpal Wabbit model architecture</i></p>

<p>This is just a simple linear combination of the features passed in, which is very fast to compute, optimize, and understand, but this means it can’t learn a model that’s a combination of input features. For example, if users in Maine who watch TV react well to an action, but not users who watch TV in other places, this model cannot capture that. It can only capture features of users in Maine on the whole, and features of users who watch TV on the whole, but not together.</p>

<h3 id="improve-model-features-with---quadratic-and---cubic">Improve Model Features with <code class="language-plaintext highlighter-rouge">--quadratic</code> and <code class="language-plaintext highlighter-rouge">--cubic</code></h3>

<p>The default model architecture is almost never going to give good results, so you need to tweak the the model architecture to allow it to learn a better estimator. One of the simplest yet still powerful ways to do that is via the <code class="language-plaintext highlighter-rouge">--quadratic</code> or <code class="language-plaintext highlighter-rouge">-q</code> option. This option allows you to generate new features from every combination of features in namespaces.</p>

<p>The syntax to do this is pretty strange, you need to take the first letter of the name of each namespace and pass a 2-character string after <code class="language-plaintext highlighter-rouge">-q</code> to indicate which 2 namespaces to mix together. In our case above where we have an <code class="language-plaintext highlighter-rouge">Action</code> namespace at a <code class="language-plaintext highlighter-rouge">User</code> namespace, we could mix them with <code class="language-plaintext highlighter-rouge">-q UA</code>. We could even do <code class="language-plaintext highlighter-rouge">-q UU</code> to mix the <code class="language-plaintext highlighter-rouge">User</code> namespace with itself. You can also pass <code class="language-plaintext highlighter-rouge">-q</code> multiple times with different combinations of namespaces. You can use <code class="language-plaintext highlighter-rouge">:</code> to indicate everything across all namespaces. So <code class="language-plaintext highlighter-rouge">-q U:</code> would mix the <code class="language-plaintext highlighter-rouge">User</code> namespace with everything across all namespaces.</p>

<p>If you want generate features by mixing 3 namespaces together, you can use <code class="language-plaintext highlighter-rouge">--cubic</code> like <code class="language-plaintext highlighter-rouge">--cubic UAC</code> or <code class="language-plaintext highlighter-rouge">--cubic UUA</code>. If you want to mix more than 3 namespace permutations together, you can use <code class="language-plaintext highlighter-rouge">--interactions</code> to specify any number of namespaces to mix together. For example <code class="language-plaintext highlighter-rouge">--interactions UAXBY</code> to mix 5 namespaces together.</p>

<p>I think that if there are numeric features, only the value of the last feature in the namespace will be used as the numeric value, so if you have a namespace with a lot of numeric features it should probably go last. (I could be wrong about this!)</p>

<p>There’s a list of more feature enhancement settings available in the <a href="https://github.com/VowpalWabbit/vowpal_wabbit/wiki/Command-Line-Arguments#example-manipulation-options">VW Wiki</a>.</p>

<h3 id="neural-networks-with---nn">Neural Networks with <code class="language-plaintext highlighter-rouge">--nn</code></h3>

<p>In addition to mixing features together, you can a use simple feed-forward neural network as the model instead of just a pure linear model with the <code class="language-plaintext highlighter-rouge">--nn</code> param. The depth of the neural network is specified using an int, so <code class="language-plaintext highlighter-rouge">--nn 2</code> would be a 2-layer neural network. There are a number of options available to further tune the neural network architecture in the <a href="https://github.com/VowpalWabbit/vowpal_wabbit/wiki/Command-Line-Arguments#neural-network-options">VW Wiki</a>.</p>

<h3 id="evaluating-different-model-settings--params">Evaluating Different Model Settings / Params</h3>

<p>Vowpal Wabbit is extremely fast to train, which is nice because it makes it easy to test out lots of different model settings using offline policy evaluation (OPE). There’s a good tutorial on how to do this on the <a href="https://vowpalwabbit.org/docs/vowpal_wabbit/python/latest/tutorials/off_policy_evaluation.html">vowpal wabbit website</a>, so I won’t go into too much detail here, but I found offline policy evaluation essential to figuring out which model params to use to get good results.</p>

<p>One thing that confused me at first was that OPE outputs what it calls “average loss”, but really this means “average cost”. If you use negative cost like I did, then “average loss” will be negative. In all cases, the lower the number for “average loss” the better, even if it’s negative.</p>

<p>Make sure to try out lots of different settings for things like learning rate (<code class="language-plaintext highlighter-rouge">-l</code>) and number of passes over the data (<code class="language-plaintext highlighter-rouge">--passes</code>) as well. I also found <code class="language-plaintext highlighter-rouge">--cover 1</code> seems to work much better than <code class="language-plaintext highlighter-rouge">--cover 3</code> for some reason.</p>

<p>In Python, I found that you can use the <code class="language-plaintext highlighter-rouge">vw.get_sum_loss()</code> method after doing a test run and dividing by the number of test samples to get the “average loss” which is output by the CLI method, if you want to do this in Python rather than using the CLI.</p>

<h3 id="python-quirks">Python Quirks</h3>

<p>There are a number of strange quirks with the Python wrapper. It doesn’t always seem to accept examples in the same format always. For example, for <code class="language-plaintext highlighter-rouge">.learn()</code> and <code class="language-plaintext highlighter-rouge">.predict()</code> you can pass an example directly, but for some methods like <code class="language-plaintext highlighter-rouge">.audit_example()</code> you need to <a href="https://github.com/VowpalWabbit/vowpal_wabbit/issues/3794#issuecomment-1065950742">parse the example into multiple parts using vw.parse() first</a>.</p>

<p>For JSON input, you need to run the Python dict examples through <code class="language-plaintext highlighter-rouge">json.dumps</code> first before passing to vowpal wabbit</p>

<p>There are also methods that just print stuff out to stdout instead of returning a value which is obnoxious. For instance it’s not currently possible to get the results from <code class="language-plaintext highlighter-rouge">--audit</code> into a string in Python for further processing. If you don’t pass <code class="language-plaintext highlighter-rouge">--quiet</code>, the python library will just print stuff to stdout and stderr as it runs. As far as I can tell, there’s no good way to get this data into a more natural Python interface.</p>

<h3 id="python-tips">Python Tips</h3>

<p>I found it’s easier to just write data to temporary files on disk and train via passing in a reference to the training file rather than passing training examples in Python, due to some of the quirks around how the Python library handles example parsing. This of course assumes that the data for learning isn’t so large that it can’t fit into memory or on disk. The code might like something like the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kn">import</span> <span class="nn">vowpalwabbit</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="kn">from</span> <span class="nn">tempfile</span> <span class="kn">import</span> <span class="n">NamedTemporaryFile</span>

<span class="k">def</span> <span class="nf">create_and_train_vw</span><span class="p">(</span><span class="n">json_examples</span><span class="p">):</span>
    <span class="nb">file</span> <span class="o">=</span> <span class="n">NamedTemporaryFile</span><span class="p">(</span><span class="s">"w"</span><span class="p">)</span>
    <span class="nb">file</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">.</span><span class="n">join</span><span class="p">([</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">ex</span><span class="p">)</span> <span class="k">for</span> <span class="n">ex</span> <span class="ow">in</span> <span class="n">json_examples</span><span class="p">]))</span>
    <span class="nb">file</span><span class="p">.</span><span class="n">flush</span><span class="p">()</span>
    <span class="n">vw</span> <span class="o">=</span> <span class="n">vowpalwabbit</span><span class="p">.</span><span class="n">Workspace</span><span class="p">(</span><span class="sa">f</span><span class="s">"--cb_explore_adf --json --quiet -d </span><span class="si">{</span><span class="nb">file</span><span class="p">.</span><span class="n">name</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="nb">file</span><span class="p">.</span><span class="n">close</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">vw</span>
</code></pre></div></div>

<p>When you call <code class="language-plaintext highlighter-rouge">.predict()</code> on a vw instance, you’ll just get an array of probabilities mapping a probability to every potential action you could take. To use the output from vowpal wabbit for prediction, you’ll need to sample the predict results according to the probabilities it returns, like below:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">random</span>

<span class="k">def</span> <span class="nf">sample_prediction</span><span class="p">(</span><span class="n">action_probs</span><span class="p">):</span>
    <span class="s">"return the index of the selected action, and the probability of that action"</span>
    <span class="p">[</span><span class="n">selected_index</span><span class="p">]</span> <span class="o">=</span> <span class="n">random</span><span class="p">.</span><span class="n">choices</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">action_probs</span><span class="p">)),</span> <span class="n">weights</span><span class="o">=</span><span class="n">action_probs</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">selected_index</span><span class="p">,</span> <span class="n">action_probs</span><span class="p">[</span><span class="n">selected_index</span><span class="p">]</span>


<span class="n">action_index</span><span class="p">,</span> <span class="n">probability</span> <span class="o">=</span> <span class="n">sample_prediction</span><span class="p">(</span><span class="n">vw</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">ex</span><span class="p">))</span>
</code></pre></div></div>

<h3 id="getting-help-with-vowpal-wabbit">Getting Help with Vowpal Wabbit</h3>

<p>The documentation for Vowpal Wabbit leaves a lot to be desired, so you’ll likely need to venture outside of the offical website docs while trying to use the library. There’s an official <a href="https://github.com/VowpalWabbit/vowpal_wabbit/wiki">Wiki on Github</a> for VW which has some good info, but it also has a lot of gaps and some of the pages are incomplete. I found it helpful to ask questions in the <a href="https://gitter.im/VowpalWabbit/community">VW Community Gitter</a>, as there are people there who respond quickly to any questions. There’s also some good info in Stack Overflow as well. As a last resort, I also found posting issues on the <a href="https://github.com/VowpalWabbit/vowpal_wabbit">VW Github page</a> to also get a lot of in-depth responses from the devs when I thought something looked like a bug.</p>

<h3 id="go-forth-and-wabbit">Go Forth and Wabbit!</h3>

<p>I’ll keep updating this post as I learn more. If you see anything that’s not correct, please leave a comment to let me know and I’ll update it!</p>]]></content><author><name></name></author><category term="ai" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Understanding Inverse Propensity Score for Contextual Bandits</title><link href="https://chanind.github.io/ai/2022/03/14/inverse-propensity-score-contextual-bandits.html" rel="alternate" type="text/html" title="Understanding Inverse Propensity Score for Contextual Bandits" /><published>2022-03-14T00:00:00+00:00</published><updated>2022-03-14T00:00:00+00:00</updated><id>https://chanind.github.io/ai/2022/03/14/inverse-propensity-score-contextual-bandits</id><content type="html" xml:base="https://chanind.github.io/ai/2022/03/14/inverse-propensity-score-contextual-bandits.html"><![CDATA[<p>One of the hardest concepts to grasp about contextual bandits is understanding how to evaluate a bandit policy without actually deploying it and seeing how it performs with users. Intuitively it seems impossible to know how a new policy will perform looking only at past data because in a bandit problem you can only observe the rewards for an action that was taken. You don’t have any data about the rewards for conterfactual actions that weren’t tried. Inverse Propensity Scoring (IPS) is a simple technique which can solve this problem by giving an unbiased estimate of how a new bandit policy would perform if it were deployed using only data recorded by a previous, potentially different bandit policy. If none of these terms make sense, don’t worry, we’ll do a quick overview of the contextual bandit problem, and then go in depth on how to use IPS to evaluate policies.</p>

<h3 id="wtf-is-a-contextual-bandit">WTF is a “Contextual Bandit”?</h3>
<p>Contextual Bandits are probably one of the most bizarrely named concepts in reinforcement learning. The idea is an extention of “<a href="https://en.wikipedia.org/wiki/Multi-armed_bandit">multi-armed bandits</a>”, which come from an old name for slot machines. Imagine you’re at a casino faced with 4 slot machines. Each machine has a different chance of giving a payout, but you don’t know the chance of winning for each machine. How should you play so as to maximize your winnings? You can spend time testing out each slot machine to get a better sense of the reward for each machine, but then you might be missing out on any potential rewards from just sticking with the machine that seems like it’s the best from what you’ve seen so far.</p>

<p><img src="/assets/bandits.png" alt="row of 4 slot machines" />
<i class="small">How should you play each machine to maximize your reward?</i></p>

<p>For multi-armed bandits, an <strong>action</strong> corresponds to a possible choice you can make. In the example of 4 slot machines above, there are 4 possible actions, each refering to pulling the lever of one of the 4 slot machines. Furthermore, a <strong>policy</strong> is an algorithm which determines how you play. Typically a policy is probabilistic, so a policy expresses a probability distribution of taking each action. For instance, in the case above you could try a completely random policy and just pull an arm uniformly at random. Or you could try a policy that pulls lever 1 60% of the time and the other 3 levels each 10% of the time. Or the probability can change over type, starting out more random and becoming more deterministic over time.</p>

<p>Contextual bandits are a type of multi-armed bandit problem where you have some extra information that might be useful in determining which action to take. For instance, if you have an online store and you want to recommend an item to a user who visits your website, the item you choose to recommend might depend on the age and location of the user. Contextual bandit problems are very common in digital products where you have a set of items that can be shown to a user and the goal is to choose the best item to show that will optimize some metric, for instance the chance the user will buy your product.</p>

<h3 id="the-counterfactual-problem">The counterfactual problem</h3>

<p>Let’s say you have an online store with several items, and currently you show those items to your users at random. You know this isn’t optimal though, because you have some extra information about each of your users, like what they last purchased, their age, and their location. If someone just bought a pair of shoes from you, it probably doesn’t make sense to try to show them that same pair of shoes immediately after. You have a log of data from your store for each time you showed an item to a user, and whether or not the user ended up purchasing the item.</p>

<p>How can you use this to data to try out different policies for showing items to users? For each user you only what happened after you showed the user 1 item; you don’t have any way to know what would have happened if they had seen a different item instead. Maybe user X didn’t buy when you showed them shoes, but maybe they would have if you had shown them a shirt. Or maybe they wouldn’t have purchased anything regardless. You can try to come up with a new policy for how to pick which item to show to users on your website, but how can you tell how that policy would have performed given the data that’s been logged so far?</p>

<p>It seems like this should be impossible, but IPS offers a way to estimate how well any new policy would have performed given the log for how items were gifted and received in the past, with some caveats as we’ll see below.</p>

<h3 id="inverse-propensity-score-ips">Inverse Propensity Score (IPS)</h3>

<p>In order for IPS to work, the policy used to generate the log data must be probabilistic and have a non-zero probability of generating picking every action that the new policy we want to test can also generate. In general, as long as the policy that generates the data never assigns a 0 probability to any action at all you should be fine. Contextual Bandit libraries like <a href="https://vowpalwabbit.org">vowpal wabbit</a> do this automatically. Also, in our data log where we record every action taken and the reward generated, we also need to record the probability of taking that action as output from the generating policy.</p>

<p>The idea behind IPS is to replay the log, and weigh each reward that shows up inversely to how likely the generating policy was to pick that action. This helps correct for the fact that actions that the generating policy selects more often will also show up more frequently in the logs than actions that aren’t selected often. The adjusted reward is then either multiplied by 1 if the policy we’re testing out also selected the same action as the generating policy given the context, or set to 0 if it selects a different action. Finally, these are results are averaged to give the expected reward of the policy we’re testing. In python, this would look something like the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">ips_estimate_avg_reward</span><span class="p">(</span><span class="n">new_policy</span><span class="p">,</span> <span class="n">data_log</span><span class="p">):</span>
    <span class="n">total_reward</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">reward</span><span class="p">,</span> <span class="n">action</span><span class="p">,</span> <span class="n">probability</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span> <span class="ow">in</span> <span class="n">data_log</span><span class="p">:</span>
        <span class="n">new_action</span><span class="p">,</span> <span class="n">new_probability</span> <span class="o">=</span> <span class="n">new_policy</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">new_action</span> <span class="o">==</span> <span class="n">action</span><span class="p">:</span>
            <span class="n">total_reward</span> <span class="o">+=</span> <span class="n">reward</span> <span class="o">/</span> <span class="n">probability</span>
    <span class="k">return</span> <span class="n">total_reward</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">data_log</span><span class="p">)</span>
</code></pre></div></div>

<p>Not bad for 7 lines of code! Note that the <code class="language-plaintext highlighter-rouge">new_probability</code> generated by our new policy isn’t needed for IPS, but if we were to deploy this policy we’d want to record it in the data log so we could continue running ips estimates of policies in the future.</p>

<h3 id="improving-on-ips">Improving on IPS</h3>

<p>IPS isn’t perfect, however. While it is an unbiased estimator of the expected reward of a new policy, it can have high variance. This is especially if the policy used to generate the data and the policy being tested are very different. IPS gives 0 reward for every case where the test policy and the generating policy select different actions, so if the policies have little overlap it will require a lot of data before IPS gives good estimates. There are other more complicated estimators that handle this better than IPS, such as <a href="https://arxiv.org/abs/1103.4601">Doubly Robust</a>, or <a href="https://arxiv.org/abs/1802.04064">Importance-Weighted Regression</a>.</p>

<p>All these techniques are implemented in the also bizarrely named but otherwise excellent <a href="https://vowpalwabbit.org">Vowpal Wabbit library</a>.</p>]]></content><author><name></name></author><category term="ai" /><summary type="html"><![CDATA[One of the hardest concepts to grasp about contextual bandits is understanding how to evaluate a bandit policy without actually deploying it and seeing how it performs with users. Intuitively it seems impossible to know how a new policy will perform looking only at past data because in a bandit problem you can only observe the rewards for an action that was taken. You don’t have any data about the rewards for conterfactual actions that weren’t tried. Inverse Propensity Scoring (IPS) is a simple technique which can solve this problem by giving an unbiased estimate of how a new bandit policy would perform if it were deployed using only data recorded by a previous, potentially different bandit policy. If none of these terms make sense, don’t worry, we’ll do a quick overview of the contextual bandit problem, and then go in depth on how to use IPS to evaluate policies.]]></summary></entry></feed>